Pin notes

This commit is contained in:
Joscha 2025-02-09 18:21:45 +01:00
parent de6080c3ad
commit 0b485e6cfe
4 changed files with 50 additions and 11 deletions

View file

@ -1,13 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { useNotesStore } from "@/stores/notes"; import { useNotesStore } from "@/stores/notes";
import { useUiStore } from "@/stores/ui"; import { useUiStore } from "@/stores/ui";
import { pathAppend } from "@/util"; import { pathAppend, pathSlice } from "@/util";
import { import {
RiAddLine, RiAddLine,
RiArrowDownSLine, RiArrowDownSLine,
RiArrowRightSLine, RiArrowRightSLine,
RiCornerUpRightLine, RiCornerUpRightLine,
RiEditLine, RiEditLine,
RiPushpinFill,
RiPushpinLine,
RiStickyNoteAddLine, RiStickyNoteAddLine,
} from "@remixicon/vue"; } from "@remixicon/vue";
import { computed, ref, watchEffect } from "vue"; import { computed, ref, watchEffect } from "vue";
@ -53,6 +55,7 @@ const mayOpen = computed(() => children.value.length > 0);
const open = computed(() => mayOpen.value && ui.isOpen(props.path)); const open = computed(() => mayOpen.value && ui.isOpen(props.path));
const focused = computed(() => ui.focusPath === props.path); const focused = computed(() => ui.focusPath === props.path);
const pinned = computed(() => ui.isPinned(props.path));
const hover = computed(() => hovering.value && mode.value !== "editing"); const hover = computed(() => hovering.value && mode.value !== "editing");
const creating = computed(() => mode.value === "creating"); const creating = computed(() => mode.value === "creating");
@ -86,6 +89,11 @@ function onClick() {
ui.toggleOpen(props.path); ui.toggleOpen(props.path);
} }
function onPinButtonClick() {
if (pinned.value) ui.unsetPinned();
else ui.setPinned(props.path);
}
function onEditButtonClick() { function onEditButtonClick() {
if (!note.value) return; if (!note.value) return;
mode.value = "editing"; mode.value = "editing";
@ -168,13 +176,17 @@ function onCreateEditorFinish(text: string) {
<div v-else class="px-1 font-light italic">note not found</div> <div v-else class="px-1 font-light italic">note not found</div>
<!-- Controls --> <!-- Controls -->
<div v-if="hover" class="absolute right-0 flex h-6 items-center gap-0.5"> <div v-if="hover || pinned" class="absolute right-0 flex h-6 items-center gap-0.5">
<CNoteButton @click.stop="onEditButtonClick"> <CNoteButton v-if="hover" @click.stop="onEditButtonClick">
<RiEditLine size="16px" /> <RiEditLine size="16px" />
</CNoteButton> </CNoteButton>
<CNoteButton @click.stop="onCreateButtonClick"> <CNoteButton v-if="hover" @click.stop="onCreateButtonClick">
<RiStickyNoteAddLine size="16px" /> <RiStickyNoteAddLine size="16px" />
</CNoteButton> </CNoteButton>
<CNoteButton :inverted="pinned" @click.stop="onPinButtonClick">
<RiPushpinFill v-if="pinned" size="16px" />
<RiPushpinLine v-else size="16px" />
</CNoteButton>
</div> </div>
</div> </div>

View file

@ -1,6 +1,13 @@
<script setup lang="ts">
const props = defineProps<{
inverted?: boolean;
}>();
</script>
<template> <template>
<div <div
class="flex select-none items-center rounded-sm border bg-white p-0.5 transition hover:scale-110 active:scale-95" class="flex select-none items-center rounded-sm border p-0.5 transition hover:scale-110 active:scale-95"
:class="props.inverted ? 'bg-black text-white' : 'bg-white text-black'"
> >
<slot></slot> <slot></slot>
</div> </div>

View file

@ -1,4 +1,4 @@
import { pathAncestors, pathLiesOn } from "@/util"; import { pathAncestors, pathLiesOn, pathSlice } from "@/util";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { ref, watchEffect } from "vue"; import { ref, watchEffect } from "vue";
@ -6,6 +6,7 @@ export const useUiStore = defineStore("ui", () => {
const anchorId = ref<string>(); const anchorId = ref<string>();
const focusPath = ref<string>(""); const focusPath = ref<string>("");
const openPaths = ref<Set<string>>(new Set()); const openPaths = ref<Set<string>>(new Set());
const pinned = ref<string>(); // The last two segments of the path
// Ensure all nodes on the focusPath are unfolded. // Ensure all nodes on the focusPath are unfolded.
watchEffect(() => { watchEffect(() => {
@ -19,27 +20,42 @@ export const useUiStore = defineStore("ui", () => {
return openPaths.value.has(path); return openPaths.value.has(path);
} }
function setOpen(path: string, open: boolean) { function setOpen(path: string, value: boolean) {
// Move the focusPath if necessary // Move the focusPath if necessary
if (!open && isOpen(path) && pathLiesOn(path, focusPath.value)) { if (!value && isOpen(path) && pathLiesOn(path, focusPath.value)) {
focusPath.value = path; focusPath.value = path;
} }
// Don't update openPaths unnecessarily. // Don't update openPaths unnecessarily.
// Just in case vue itself doesn't debounce Set operations. // Just in case vue itself doesn't debounce Set operations.
if (open && !isOpen(path)) openPaths.value.add(path); if (value && !isOpen(path)) openPaths.value.add(path);
else if (!open && isOpen(path)) openPaths.value.delete(path); else if (!value && isOpen(path)) openPaths.value.delete(path);
} }
function toggleOpen(path: string) { function toggleOpen(path: string) {
setOpen(path, !isOpen(path)); setOpen(path, !isOpen(path));
} }
function isPinned(path: string): boolean {
return pathSlice(path, -2) === pinned.value;
}
function setPinned(path: string) {
pinned.value = pathSlice(path, -2);
}
function unsetPinned() {
pinned.value = undefined;
}
return { return {
anchorId, anchorId,
focusPath, focusPath,
isOpen, isOpen,
setOpen, setOpen,
toggleOpen, toggleOpen,
isPinned,
setPinned,
unsetPinned,
}; };
}); });

View file

@ -7,12 +7,16 @@ function pathParse(path: string): string[] {
return path.split("/"); return path.split("/");
} }
export function pathSlice(path: string, start: number, end?: number): string {
return pathString(pathParse(path).slice(start, end));
}
export function pathAppend(path: string, key: string): string { export function pathAppend(path: string, key: string): string {
return pathString(pathParse(path).concat(key)); return pathString(pathParse(path).concat(key));
} }
export function pathAncestor(path: string): string { export function pathAncestor(path: string): string {
return pathString(pathParse(path).slice(0, -1)); return pathSlice(path, 0, -1);
} }
export function pathAncestors(path: string): string[] { export function pathAncestors(path: string): string[] {