Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/frontend/src/actions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type NoteContentItem } from "shared";
import { createApp, h } from "vue";

import NoteFloatModal from "@/components/shared/NoteFloatModal.vue";
import NoteSearchModal from "@/components/shared/NoteSearchModal.vue";
import { SDKPlugin } from "@/plugins/sdk";
import { useNotesStore } from "@/stores/notes";
import type { FrontendSDK } from "@/types";
Expand Down Expand Up @@ -44,6 +45,34 @@ export const showNoteModal = (sdk: FrontendSDK) => {
modalApp.mount(modalContainer);
};

/**
* Shows the search modal for finding and viewing existing notes
*/
export const showSearchModal = (sdk: FrontendSDK) => {
const modalContainer = document.createElement("div");
modalContainer.id = "note-search-modal-container";
document.body.appendChild(modalContainer);

const position = {
x: Math.max(0, window.innerWidth / 2 - 250),
y: Math.max(0, window.innerHeight / 2 - 200),
};

const modalApp = createApp({
render: () =>
h(NoteSearchModal, {
initialPosition: position,
onClose: () => {
modalApp.unmount();
modalContainer.remove();
},
}),
});

modalApp.use(SDKPlugin, sdk);
modalApp.mount(modalContainer);
};

/**
* Sends selected text to the currently open note
*/
Expand Down
8 changes: 2 additions & 6 deletions packages/frontend/src/components/shared/NoteFloatModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@ import { onBeforeUnmount, onMounted } from "vue";

import { useNoteModal } from "@/composables/useNoteModal";
import { useSDK } from "@/plugins/sdk";
import type { ModalPosition } from "@/types";

const sdk = useSDK();

interface Position {
x: number;
y: number;
}

const props = defineProps({
initialPosition: {
type: Object as () => Position,
type: Object as () => ModalPosition,
default: () => ({ x: 100, y: 100 }),
},
});
Expand Down
188 changes: 188 additions & 0 deletions packages/frontend/src/components/shared/NoteSearchModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted } from "vue";

import SearchInput from "@/components/shared/SearchInput.vue";
import SearchNoteViewer from "@/components/shared/SearchNoteViewer.vue";
import SearchResults from "@/components/shared/SearchResults.vue";
import { useSearchModal } from "@/composables/useSearchModal";
import type { ModalPosition } from "@/types";

const props = defineProps({
initialPosition: {
type: Object as () => ModalPosition,
default: () => ({ x: 100, y: 100 }),
},
});

const emit = defineEmits<{
close: [];
}>();

const {
position,
size,
searchQuery,
searchResults,
selectedIndex,
viewedNote,
isViewingNote,
startDrag,
startResize,
search,
openNote,
goBackToSearch,
saveNoteContent,
close,
moveSelection,
openSelectedNote,
initialize,
} = useSearchModal({
initialPosition: props.initialPosition,
onClose: () => emit("close"),
});

function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
if (isViewingNote.value) {
goBackToSearch();
} else {
close();
}
return;
}

if (isViewingNote.value) return;

if (event.key === "ArrowDown") {
event.preventDefault();
moveSelection(1);
} else if (event.key === "ArrowUp") {
event.preventDefault();
moveSelection(-1);
} else if (event.key === "Enter") {
event.preventDefault();
openSelectedNote();
}
}

onMounted(async () => {
await initialize();
document.addEventListener("keydown", handleKeyDown);
});

onBeforeUnmount(() => {
document.removeEventListener("keydown", handleKeyDown);
});
</script>

<template>
<div
class="fixed z-50 flex flex-col bg-surface-800 border border-surface-700 rounded-md shadow-md overflow-hidden"
:style="{
top: position.y + 'px',
left: position.x + 'px',
width: size.width + 'px',
height: size.height + 'px',
}"
>
<!-- Header -->
<div
class="flex items-center gap-2 px-2 py-1.5 bg-surface-900 cursor-move shrink-0"
@mousedown="startDrag"
>
<button
v-if="isViewingNote"
class="flex items-center gap-1 text-xs text-surface-400 hover:text-surface-200 transition-colors bg-transparent border-none cursor-pointer px-1 py-0.5 rounded hover:bg-surface-700"
@click="goBackToSearch"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="w-3 h-3"
>
<polyline points="15 18 9 12 15 6" />
</svg>
Back
</button>
<span class="text-xs text-surface-300 truncate flex-1">
{{
isViewingNote
? viewedNote?.name.replace(/\.json$/, "")
: "Search Notes"
}}
</span>
<button
class="text-surface-500 hover:text-surface-200 transition-colors bg-transparent border-none cursor-pointer p-0.5 rounded hover:bg-surface-700"
@click="close"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="w-3.5 h-3.5"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>

<!-- Search State -->
<template v-if="!isViewingNote">
<SearchInput v-model="searchQuery" @search="search" />
<SearchResults
:results="searchResults"
:selected-index="selectedIndex"
@select="openNote"
/>
<div
class="flex items-center justify-end gap-2 px-2 py-1 text-xs text-surface-500 border-t border-surface-700 shrink-0"
>
<span>
<strong class="bg-surface-700 px-1 py-0.5 rounded">↑↓</strong>
navigate
</span>
<span>
<strong class="bg-surface-700 px-1 py-0.5 rounded">↩</strong>
open
</span>
<span>
<strong class="bg-surface-700 px-1 py-0.5 rounded">esc</strong>
close
</span>
</div>
</template>

<!-- View State -->
<template v-if="viewedNote">
<SearchNoteViewer :content="viewedNote.content" @save="saveNoteContent" />
<div
class="flex items-center justify-end gap-2 px-2 py-1 text-xs text-surface-500 border-t border-surface-700 shrink-0"
>
<span>
<strong class="bg-surface-700 px-1 py-0.5 rounded">esc</strong>
back
</span>
</div>
</template>

<!-- Resize Handle -->
<div
class="resize-handle absolute right-0 bottom-0 w-3 h-3 cursor-se-resize"
@mousedown.stop="startResize"
>
<div
class="absolute right-[3px] bottom-[3px] w-[5px] h-[5px] border-r-2 border-b-2 border-surface-500"
/>
</div>
</div>
</template>
63 changes: 63 additions & 0 deletions packages/frontend/src/components/shared/SearchInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<script setup lang="ts">
import { onMounted, ref, watch } from "vue";

const props = defineProps<{
modelValue: string;
}>();

const emit = defineEmits<{
"update:modelValue": [value: string];
search: [query: string];
}>();

const inputRef = ref<HTMLInputElement>();
const query = ref(props.modelValue);

watch(
() => props.modelValue,
(newVal) => {
if (newVal !== query.value) {
query.value = newVal;
}
},
);

function onInput() {
emit("update:modelValue", query.value);
emit("search", query.value);
}

function focus() {
inputRef.value?.focus();
}

onMounted(() => {
setTimeout(focus, 50);
});
</script>

<template>
<div class="flex items-center gap-2 px-2 py-1.5 border-b border-surface-700">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="w-4 h-4 text-surface-400 shrink-0"
>
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
<input
ref="inputRef"
v-model="query"
type="text"
placeholder="Search notes..."
class="w-full bg-transparent text-sm text-surface-100 placeholder-surface-500 outline-none border-none"
@input="onInput"
/>
</div>
</template>
Loading
Loading