Create navbar dropdown menu
This commit is contained in:
parent
160c8a626d
commit
45d97388ee
9 changed files with 170 additions and 36 deletions
|
|
@ -10,6 +10,7 @@
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@floating-ui/vue": "^1.1.6",
|
||||||
"@remixicon/vue": "^4.6.0",
|
"@remixicon/vue": "^4.6.0",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"@tauri-apps/api": "^2.2.0",
|
"@tauri-apps/api": "^2.2.0",
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Navbar from "./components/Navbar.vue";
|
import Navbar from "./components/Navbar.vue";
|
||||||
import Test from "./components/Test.vue";
|
|
||||||
import { useFooStore } from "./stores/foo";
|
|
||||||
|
|
||||||
const foo = useFooStore();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-screen touch-pan-x touch-pan-y flex-col">
|
<div class="flex h-screen touch-pan-x touch-pan-y flex-col">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div class="h-full overflow-auto">
|
<div class="h-full overflow-auto">
|
||||||
<Test />
|
|
||||||
<br />
|
|
||||||
Yes, it's now {{ foo.name }}.
|
|
||||||
<template v-for="_ in 6">
|
<template v-for="_ in 6">
|
||||||
<p class="p-1 font-thin">The quick brown fox Qiii</p>
|
<p class="p-1 font-thin">The quick brown fox Qiii</p>
|
||||||
<p class="p-1 font-extralight">The quick brown fox Qiii</p>
|
<p class="p-1 font-extralight">The quick brown fox Qiii</p>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RiArrowDropDownLine, RiSettings3Fill } from "@remixicon/vue";
|
import { RiSettings3Fill } from "@remixicon/vue";
|
||||||
|
import NavbarDropdown from "./NavbarDropdown.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex justify-between bg-black p-2 text-white">
|
<div class="flex justify-between bg-black p-2 text-white">
|
||||||
<div
|
<NavbarDropdown />
|
||||||
class="cursor-default rounded-md bg-neutral-800 pl-2 text-lg hover:bg-neutral-700 active:bg-neutral-500"
|
|
||||||
>
|
|
||||||
GedächtNAS
|
|
||||||
<RiArrowDropDownLine class="inline" />
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="flex items-center rounded-md bg-neutral-800 px-1 hover:bg-neutral-700 active:bg-neutral-500"
|
class="flex items-center rounded-md bg-neutral-800 px-1 hover:bg-neutral-700 active:bg-neutral-500"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
69
gdn-app/src/components/NavbarDropdown.vue
Normal file
69
gdn-app/src/components/NavbarDropdown.vue
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useReposStore } from "@/stores/repos";
|
||||||
|
import { offset, useFloating } from "@floating-ui/vue";
|
||||||
|
import {
|
||||||
|
RiAddLine,
|
||||||
|
RiArrowDropDownLine,
|
||||||
|
RiArrowDropUpLine,
|
||||||
|
} from "@remixicon/vue";
|
||||||
|
import { ref, useTemplateRef } from "vue";
|
||||||
|
import NavbarDropdownEntry from "./NavbarDropdownEntry.vue";
|
||||||
|
|
||||||
|
const repos = useReposStore();
|
||||||
|
|
||||||
|
const open = ref(false);
|
||||||
|
|
||||||
|
// https://floating-ui.com/docs/vue
|
||||||
|
const reference = useTemplateRef("reference");
|
||||||
|
const floating = useTemplateRef("floating");
|
||||||
|
const { floatingStyles } = useFloating(reference, floating, {
|
||||||
|
placement: "bottom-start",
|
||||||
|
middleware: [offset(4)],
|
||||||
|
});
|
||||||
|
|
||||||
|
function onAddNewRepo() {
|
||||||
|
repos.addRepo({ id: "test", name: "test" });
|
||||||
|
console.log(repos.selectedRepo);
|
||||||
|
open.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Navbar entry -->
|
||||||
|
<div
|
||||||
|
ref="reference"
|
||||||
|
class="relative cursor-default rounded-md bg-neutral-800 pl-2 text-lg font-light hover:bg-neutral-700 active:bg-neutral-500"
|
||||||
|
@click="open = !open"
|
||||||
|
>
|
||||||
|
<span v-if="repos.selectedRepo">{{ repos.selectedRepo.name }}</span>
|
||||||
|
<span v-else class="italic">no repo selected</span>
|
||||||
|
|
||||||
|
<RiArrowDropUpLine v-if="open" class="inline" />
|
||||||
|
<RiArrowDropDownLine v-else class="inline" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Close dropdown when clicking outside it -->
|
||||||
|
<div
|
||||||
|
v-if="open"
|
||||||
|
class="fixed left-0 top-0 h-screen w-screen"
|
||||||
|
@click="open = false"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Dropdown -->
|
||||||
|
<div
|
||||||
|
v-if="open"
|
||||||
|
ref="floating"
|
||||||
|
class="absolute left-0 top-0 w-fit min-w-48 rounded-md bg-neutral-800 font-light"
|
||||||
|
:style="floatingStyles"
|
||||||
|
>
|
||||||
|
<NavbarDropdownEntry
|
||||||
|
v-for="repo of repos.reposByName"
|
||||||
|
:class="{ 'font-medium': repo.id === repos.selectedRepoId }"
|
||||||
|
>{{ repo.name }}</NavbarDropdownEntry
|
||||||
|
>
|
||||||
|
<hr v-if="repos.reposByName.length > 0" class="m-1 text-neutral-700" />
|
||||||
|
<NavbarDropdownEntry class="italic" @click="onAddNewRepo">
|
||||||
|
<RiAddLine class="-ml-2 inline h-4" />add new repo
|
||||||
|
</NavbarDropdownEntry>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
7
gdn-app/src/components/NavbarDropdownEntry.vue
Normal file
7
gdn-app/src/components/NavbarDropdownEntry.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<div class="m-1">
|
||||||
|
<div class="cursor-pointer select-none rounded px-2 hover:bg-neutral-700">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { useFooStore } from "@/stores/foo";
|
|
||||||
|
|
||||||
const foo = useFooStore();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
Foo is {{ foo.name }}.
|
|
||||||
<button @click="foo.setName('bar')">Change to bar.</button>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { defineStore } from "pinia";
|
|
||||||
import { ref } from "vue";
|
|
||||||
|
|
||||||
export const useFooStore = defineStore("foo", () => {
|
|
||||||
const name = ref("foo");
|
|
||||||
|
|
||||||
function setName(to: string) {
|
|
||||||
name.value = to;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { name, setName };
|
|
||||||
});
|
|
||||||
55
gdn-app/src/stores/repos.ts
Normal file
55
gdn-app/src/stores/repos.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
|
type Repo = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useReposStore = defineStore("repos", () => {
|
||||||
|
const repos = ref<Map<string, Repo>>(new Map());
|
||||||
|
const selectedRepoId = ref<string>();
|
||||||
|
|
||||||
|
const selectedRepo = computed<Repo | undefined>(() => {
|
||||||
|
if (selectedRepoId.value === undefined) return undefined;
|
||||||
|
return repos.value.get(selectedRepoId.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const reposByName = computed<Repo[]>(() => {
|
||||||
|
const values = [...repos.value.values()];
|
||||||
|
values.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
||||||
|
return values;
|
||||||
|
});
|
||||||
|
|
||||||
|
const repoIdsByName = computed<string[]>(() =>
|
||||||
|
reposByName.value.map((it) => it.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
function addRepo(repo: Repo) {
|
||||||
|
repos.value.set(repo.id, repo);
|
||||||
|
selectedRepoId.value = repo.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRepo(id: string) {
|
||||||
|
const i = repoIdsByName.value.indexOf(id);
|
||||||
|
repos.value.delete(id);
|
||||||
|
if (i >= 0) {
|
||||||
|
const j = Math.min(i, reposByName.value.length - 1);
|
||||||
|
selectRepo(repoIdsByName.value[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectRepo(id: string) {
|
||||||
|
if (repos.value.get(id) === undefined) return;
|
||||||
|
selectedRepoId.value = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
repos,
|
||||||
|
reposByName,
|
||||||
|
selectedRepo,
|
||||||
|
selectedRepoId,
|
||||||
|
addRepo,
|
||||||
|
removeRepo,
|
||||||
|
};
|
||||||
|
});
|
||||||
35
pnpm-lock.yaml
generated
35
pnpm-lock.yaml
generated
|
|
@ -17,6 +17,9 @@ importers:
|
||||||
|
|
||||||
gdn-app:
|
gdn-app:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@floating-ui/vue':
|
||||||
|
specifier: ^1.1.6
|
||||||
|
version: 1.1.6(vue@3.5.13(typescript@5.7.3))
|
||||||
'@remixicon/vue':
|
'@remixicon/vue':
|
||||||
specifier: ^4.6.0
|
specifier: ^4.6.0
|
||||||
version: 4.6.0(vue@3.5.13(typescript@5.7.3))
|
version: 4.6.0(vue@3.5.13(typescript@5.7.3))
|
||||||
|
|
@ -356,6 +359,18 @@ packages:
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@floating-ui/core@1.6.9':
|
||||||
|
resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==}
|
||||||
|
|
||||||
|
'@floating-ui/dom@1.6.13':
|
||||||
|
resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==}
|
||||||
|
|
||||||
|
'@floating-ui/utils@0.2.9':
|
||||||
|
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
|
||||||
|
|
||||||
|
'@floating-ui/vue@1.1.6':
|
||||||
|
resolution: {integrity: sha512-XFlUzGHGv12zbgHNk5FN2mUB7ROul3oG2ENdTpWdE+qMFxyNxWSRmsoyhiEnpmabNm6WnUvR1OvJfUfN4ojC1A==}
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.8':
|
'@jridgewell/gen-mapping@0.3.8':
|
||||||
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
|
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
@ -1606,6 +1621,26 @@ snapshots:
|
||||||
'@esbuild/win32-x64@0.24.2':
|
'@esbuild/win32-x64@0.24.2':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@floating-ui/core@1.6.9':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/utils': 0.2.9
|
||||||
|
|
||||||
|
'@floating-ui/dom@1.6.13':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/core': 1.6.9
|
||||||
|
'@floating-ui/utils': 0.2.9
|
||||||
|
|
||||||
|
'@floating-ui/utils@0.2.9': {}
|
||||||
|
|
||||||
|
'@floating-ui/vue@1.1.6(vue@3.5.13(typescript@5.7.3))':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/dom': 1.6.13
|
||||||
|
'@floating-ui/utils': 0.2.9
|
||||||
|
vue-demi: 0.14.10(vue@3.5.13(typescript@5.7.3))
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@vue/composition-api'
|
||||||
|
- vue
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.8':
|
'@jridgewell/gen-mapping@0.3.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/set-array': 1.2.1
|
'@jridgewell/set-array': 1.2.1
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue