4 Commits
v0.1.8 ... main

Author SHA1 Message Date
nicwands
779c96e702 remove env 2026-03-18 15:34:11 -04:00
nicwands
059329c696 download and find WIP 2026-03-17 22:58:10 -04:00
nicwands
5c826e6b93 Update nav + back buttons 2026-03-17 13:11:13 -04:00
nicwands
6f76c46299 page loading states 2026-03-17 12:51:05 -04:00
21 changed files with 584 additions and 179 deletions

5
.env
View File

@@ -1,5 +0,0 @@
VITE_DEV_SERVER_URL=http://localhost:5173
SUPABASE_KEY='sb_secret_xYDyNcB7Q46ZYhDISeXRHQ_95l-N33m'
SUPABASE_URL='https://jmuadfpmmejfvkwosaqh.supabase.co'
CSC_LINK='https://drive.google.com/uc?id=1fSmWPudBqoqRkxasBW6PNmMOCHbBAMdZ'
CSC_KEY_PASSWORD='1+1=1167jK'

44
package-lock.json generated
View File

@@ -18,6 +18,7 @@
"@tiptap/extension-document": "^3.19.0", "@tiptap/extension-document": "^3.19.0",
"@tiptap/extension-highlight": "^3.20.0", "@tiptap/extension-highlight": "^3.20.0",
"@tiptap/extension-list": "^3.20.0", "@tiptap/extension-list": "^3.20.0",
"@tiptap/markdown": "^3.20.4",
"@tiptap/starter-kit": "^3.19.0", "@tiptap/starter-kit": "^3.19.0",
"@tiptap/vue-3": "^3.19.0", "@tiptap/vue-3": "^3.19.0",
"@vueuse/core": "^14.2.1", "@vueuse/core": "^14.2.1",
@@ -2382,16 +2383,16 @@
} }
}, },
"node_modules/@tiptap/core": { "node_modules/@tiptap/core": {
"version": "3.20.0", "version": "3.20.4",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.0.tgz", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.4.tgz",
"integrity": "sha512-aC9aROgia/SpJqhsXFiX9TsligL8d+oeoI8W3u00WI45s0VfsqjgeKQLDLF7Tu7hC+7F02teC84SAHuup003VQ==", "integrity": "sha512-3i/DG89TFY/b34T5P+j35UcjYuB5d3+9K8u6qID+iUqNPiza015HPIZLuPfE5elNwVdV3EXIoPo0LLeBLgXXAg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/pm": "^3.20.0" "@tiptap/pm": "^3.20.4"
} }
}, },
"node_modules/@tiptap/extension-blockquote": { "node_modules/@tiptap/extension-blockquote": {
@@ -2752,10 +2753,27 @@
"@tiptap/pm": "^3.19.0" "@tiptap/pm": "^3.19.0"
} }
}, },
"node_modules/@tiptap/markdown": {
"version": "3.20.4",
"resolved": "https://registry.npmjs.org/@tiptap/markdown/-/markdown-3.20.4.tgz",
"integrity": "sha512-1ARtZzJ1skQCZi4LyVSmImgg6JIIMP5dEs0FvHXS3a7M3O+uMOUvY1sWeggVZExg8DXoVyHd7BjRIpm7uNRuSw==",
"license": "MIT",
"dependencies": {
"marked": "^17.0.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.20.4",
"@tiptap/pm": "^3.20.4"
}
},
"node_modules/@tiptap/pm": { "node_modules/@tiptap/pm": {
"version": "3.20.0", "version": "3.20.4",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.0.tgz", "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.4.tgz",
"integrity": "sha512-jn+2KnQZn+b+VXr8EFOJKsnjVNaA4diAEr6FOazupMt8W8ro1hfpYtZ25JL87Kao/WbMze55sd8M8BDXLUKu1A==", "integrity": "sha512-rCHYSBToilBEuI6PtjziHDdRkABH/XqwJ7dG4Amn/SD3yGiZKYCiEApQlTUS2zZeo8DsLeuqqqB4vEOeD4OEPg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"prosemirror-changeset": "^2.3.0", "prosemirror-changeset": "^2.3.0",
@@ -6310,6 +6328,18 @@
"url": "https://github.com/fb55/entities?sponsor=1" "url": "https://github.com/fb55/entities?sponsor=1"
} }
}, },
"node_modules/marked": {
"version": "17.0.4",
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz",
"integrity": "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/matcher": { "node_modules/matcher": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",

View File

@@ -34,6 +34,7 @@
"@tiptap/extension-document": "^3.19.0", "@tiptap/extension-document": "^3.19.0",
"@tiptap/extension-highlight": "^3.20.0", "@tiptap/extension-highlight": "^3.20.0",
"@tiptap/extension-list": "^3.20.0", "@tiptap/extension-list": "^3.20.0",
"@tiptap/markdown": "^3.20.4",
"@tiptap/starter-kit": "^3.19.0", "@tiptap/starter-kit": "^3.19.0",
"@tiptap/vue-3": "^3.19.0", "@tiptap/vue-3": "^3.19.0",
"@vueuse/core": "^14.2.1", "@vueuse/core": "^14.2.1",

View File

@@ -1,14 +1,15 @@
<template> <template>
<div :class="classes" :style="styles"> <div :class="classes" :style="styles">
<Nav /> <div class="layout">
<div class="page">
<Suspense> <Nav v-if="$route.name !== 'note'" ref="nav" />
<div class="layout-container"> <Suspense>
<router-view :key="$route.fullPath" /> <router-view :key="$route.fullPath" />
</Suspense>
<MoveMenu />
</div> </div>
</Suspense>
<MoveMenu />
</div>
<Menu /> <Menu />
@@ -17,18 +18,19 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import loadFonts from '@fuzzco/font-loader' import loadFonts from '@fuzzco/font-loader'
import { useWindowSize } from '@vueuse/core' import { useWindowSize, useElementBounding } from '@vueuse/core'
import Menu from '@/components/Menu.vue' import Menu from '@/components/Menu.vue'
import Nav from '@/components/Nav.vue' import Nav from '@/components/Nav.vue'
import MoveMenu from '@/components/MoveMenu.vue' import MoveMenu from '@/components/MoveMenu.vue'
import ScrollBar from '@/components/ScrollBar.vue' import ScrollBar from '@/components/ScrollBar.vue'
import useConfig from '@/composables/useConfig' import useConfig from '@/composables/useConfig'
const { height } = useWindowSize() const nav = ref()
// Theme state const { height } = useWindowSize()
const { height: navHeight } = useElementBounding(nav)
const { config } = useConfig() const { config } = useConfig()
const classes = computed(() => [ const classes = computed(() => [
@@ -60,6 +62,7 @@ onMounted(async () => {
const styles = computed(() => ({ const styles = computed(() => ({
'--vh': height.value ? height.value / 100 + 'px' : '100vh', '--vh': height.value ? height.value / 100 + 'px' : '100vh',
'--nav-height': navHeight.value + 'px',
})) }))
</script> </script>
@@ -72,10 +75,14 @@ const styles = computed(() => ({
color: var(--theme-fg); color: var(--theme-fg);
transition: opacity 400ms; transition: opacity 400ms;
.layout-container { .layout {
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
min-height: calc(100 * var(--vh)); min-height: calc(100 * var(--vh));
.page {
position: relative;
}
} }
&:not(.fonts-ready) { &:not(.fonts-ready) {

View File

@@ -64,7 +64,7 @@ const openNewCategory = () => {}
.menu-wrap { .menu-wrap {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-top: 1.2em; padding-top: var(--nav-height);
padding-bottom: 10px; padding-bottom: 10px;
.menu-item { .menu-item {

View File

@@ -1,5 +1,7 @@
<template> <template>
<div v-if="open" class="move-menu layout-block"> <div v-if="open" class="move-menu layout-block">
<button class="cancel-button" @click="close">Cancel</button>
<template v-for="(category, i) in categories"> <template v-for="(category, i) in categories">
<category-row <category-row
v-if="category !== fromCategory" v-if="category !== fromCategory"
@@ -53,5 +55,10 @@ watch(open, async () => {
width: 50vw; width: 50vw;
height: 100%; height: 100%;
border-left: 1px solid var(--grey-100); border-left: 1px solid var(--grey-100);
.cancel-button {
color: var(--grey-100);
padding: 9px 0 16px;
}
} }
</style> </style>

View File

@@ -1,14 +1,30 @@
<template> <template>
<nav class="nav layout-block"> <nav class="nav layout-block">
<button @click="toggleMenu">Menu</button> <div class="left">
<router-link v-if="HAS_BACK_BUTTON.includes($route.name)" to="/"
><- Back</router-link
>
</div>
<router-link to="/search">Search</router-link> <div class="right">
<button @click="toggleMenu">Menu</button>
<router-link to="/search">Search</router-link>
</div>
</nav> </nav>
</template> </template>
<script setup> <script setup>
import useMenu from '@/composables/useMenu' import useMenu from '@/composables/useMenu'
const HAS_BACK_BUTTON = [
'category',
'create-category',
'instructions',
'search',
'preferences',
]
const { menuOpen, closeMenu, openMenu } = useMenu() const { menuOpen, closeMenu, openMenu } = useMenu()
const toggleMenu = () => { const toggleMenu = () => {
@@ -24,9 +40,16 @@ const toggleMenu = () => {
.nav { .nav {
position: absolute; position: absolute;
display: flex; display: flex;
align-items: center; justify-content: space-between;
gap: 10px;
padding-top: 9px; padding-top: 9px;
padding-bottom: 16px;
color: var(--grey-100); color: var(--grey-100);
.left,
.right {
display: flex;
align-items: center;
gap: 10px;
}
} }
</style> </style>

View File

@@ -0,0 +1,24 @@
<template>
<div class="page-loading">
<svg-spinner />
</div>
</template>
<script setup>
import SvgSpinner from '@/components/svg/Spinner.vue'
</script>
<style lang="scss">
.page-loading {
position: absolute;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
svg {
width: 20px;
height: 20px;
}
}
</style>

View File

@@ -0,0 +1,74 @@
<template>
<div class="search-input">
<input
v-model="model"
type="text"
:placeholder="placeholder"
ref="input"
@input="emit('input', model.value)"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({
placeholder: String,
})
const model = defineModel()
const emit = defineEmits(['input'])
const input = ref()
defineExpose({
focus: () => input.value.focus(),
})
</script>
<style lang="scss">
.search-input {
position: relative;
input {
display: block;
width: 100%;
position: relative;
padding: 5px 15px 6px;
background: var(--theme-bg);
--clip-start: 16px;
clip-path: polygon(
var(--clip-start) 1px,
calc(100% - var(--clip-start)) 1px,
calc(100% - 1.5px) 50%,
calc(100% - var(--clip-start)) calc(100% - 1px),
var(--clip-start) calc(100% - 1px),
1.5px 50%
);
&::placeholder {
color: var(--grey-100);
}
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--theme-fg);
--clip-start: 15px;
clip-path: polygon(
var(--clip-start) 0,
calc(100% - var(--clip-start)) 0,
100% 50%,
calc(100% - var(--clip-start)) 100%,
var(--clip-start) 100%,
0% 50%
);
}
}
</style>

View File

@@ -0,0 +1,61 @@
<template>
<div class="note-download">
<button @click="download">{{ noteTitle }}.md </button>
</div>
</template>
<script setup>
import { computed } from 'vue'
import _kebabCase from 'lodash/kebabCase'
const DEFAULT_TITLE = 'Untitled'
const props = defineProps({
editor: {
type: Object,
required: true,
},
})
const noteTitle = computed(() => {
if (!props.editor) return DEFAULT_TITLE
let title
const doc = props.editor.state.doc
const firstNode = doc.firstChild
if (!firstNode || firstNode.type.name !== 'heading') title = DEFAULT_TITLE
title = firstNode.textContent.trim() || DEFAULT_TITLE
return _kebabCase(title)
})
const download = () => {
if (!props.editor) return
const content = props.editor.getMarkdown()
const blob = new Blob([content], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${noteTitle.value}.md`
a.click()
URL.revokeObjectURL(url)
}
</script>
<style lang="scss">
.note-download {
margin-bottom: 16px;
color: var(--grey-100);
display: flex;
justify-content: center;
button {
&:hover {
color: var(--theme-accent);
}
}
}
</style>

View File

@@ -4,6 +4,8 @@
<editor-menu :editor="editor" /> <editor-menu :editor="editor" />
</div> </div>
<page-loading v-else />
</template> </template>
<script setup> <script setup>
@@ -11,6 +13,7 @@ import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
import { onBeforeUnmount, onMounted, shallowRef } from 'vue' import { onBeforeUnmount, onMounted, shallowRef } from 'vue'
import { TaskList, TaskItem } from '@tiptap/extension-list' import { TaskList, TaskItem } from '@tiptap/extension-list'
import { Highlight } from '@tiptap/extension-highlight' import { Highlight } from '@tiptap/extension-highlight'
import PageLoading from '@/components/PageLoading.vue'
import { Editor, EditorContent } from '@tiptap/vue-3' import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document' import Document from '@tiptap/extension-document'
import { Placeholder } from '@tiptap/extensions' import { Placeholder } from '@tiptap/extensions'
@@ -18,6 +21,7 @@ import { all, createLowlight } from 'lowlight'
import useNotes from '@/composables/useNotes' import useNotes from '@/composables/useNotes'
import StarterKit from '@tiptap/starter-kit' import StarterKit from '@tiptap/starter-kit'
import _debounce from 'lodash/debounce' import _debounce from 'lodash/debounce'
import { Markdown } from '@tiptap/markdown'
import EditorMenu from './Menu.vue' import EditorMenu from './Menu.vue'
const props = defineProps({ const props = defineProps({
@@ -82,6 +86,7 @@ onMounted(async () => {
TaskList, TaskList,
TaskItem, TaskItem,
Highlight, Highlight,
Markdown,
CodeBlockLowlight.configure({ CodeBlockLowlight.configure({
lowlight, lowlight,
enableTabIndentation: true, enableTabIndentation: true,
@@ -94,6 +99,10 @@ onMounted(async () => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
editor.value?.destroy?.() editor.value?.destroy?.()
}) })
defineExpose({
editor,
})
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -139,7 +148,7 @@ onBeforeUnmount(() => {
li { li {
display: list-item; display: list-item;
margin-left: 1.5em; margin-left: 1.75em;
&::marker { &::marker {
@include p; @include p;
@@ -163,59 +172,6 @@ onBeforeUnmount(() => {
display: block; display: block;
color: inherit; color: inherit;
padding: 1em; padding: 1em;
/* Code styling */
.hljs-comment,
.hljs-quote {
color: #616161;
}
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-tag,
.hljs-name,
.hljs-regexp,
.hljs-link,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #f98181;
}
.hljs-number,
.hljs-meta,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params {
color: #fbbc88;
}
.hljs-string,
.hljs-symbol,
.hljs-bullet {
color: #b9f18d;
}
.hljs-title,
.hljs-section {
color: #faf594;
}
.hljs-keyword,
.hljs-selector-tag {
color: #70cff8;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
} }
blockquote { blockquote {
border-left: 4px solid var(--grey-100); border-left: 4px solid var(--grey-100);

View File

@@ -1,12 +1,242 @@
<template> <template>
<div class="note-find"></div> <div v-if="visible" class="note-find">
<div class="find-bar">
<!-- <input
@keydown.enter="findNext"
@keydown.escape="close"
ref="inputRef"
/> -->
<search-input
v-model="searchQuery"
placeholder="Find..."
ref="inputRef"
/>
<span class="match-count">
{{ currentMatch + 1 }} / {{ matches.length }}
</span>
<button @click="findPrev" :disabled="matches.length === 0">
</button>
<button @click="findNext" :disabled="matches.length === 0">
</button>
<button @click="close"></button>
</div>
</div>
</template> </template>
<script setup> <script setup>
const props = defineProps({}) import { ref, watch, nextTick } from 'vue'
import SearchInput from '@/components/SearchInput.vue'
import { Highlight } from '@tiptap/extension-highlight'
const props = defineProps({
editor: {
type: Object,
default: null,
},
visible: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['close'])
const searchQuery = ref('')
const matches = ref([])
const currentMatch = ref(0)
const inputRef = ref(null)
const findInDocument = () => {
if (!props.editor || !searchQuery.value) {
clearHighlights()
matches.value = []
return
}
clearHighlights()
const query = searchQuery.value.toLowerCase()
const doc = props.editor.state.doc
const foundMatches = []
doc.descendants((node, pos) => {
if (!node.isText || !node.text) return
const text = node.text.toLowerCase()
let start = 0
let index
while ((index = text.indexOf(query, start)) !== -1) {
foundMatches.push({
from: pos + index,
to: pos + index + query.length,
})
start = index + 1
}
})
matches.value = foundMatches
currentMatch.value = 0
if (foundMatches.length > 0) {
highlightMatch(0)
scrollToMatch(0)
}
}
const highlightMatch = (index) => {
if (!props.editor || !matches.value[index]) return
const { from, to } = matches.value[index]
props.editor
.chain()
.setTextSelection({ from, to })
.setHighlight({ color: 'var(--theme-accent)' })
.run()
inputRef.value?.focus()
}
const clearHighlights = () => {
if (!props.editor) return
const { state } = props.editor
const { doc } = state
doc.descendants((node, pos) => {
if (node.marks && node.marks.length > 0) {
node.marks.forEach((mark) => {
if (mark.type.name === 'highlight') {
props.editor.chain().focus().unsetHighlight().run()
}
})
}
})
props.editor.commands.unsetHighlight()
}
const scrollToMatch = (index) => {
if (!props.editor || !matches.value[index]) return
const { from } = matches.value[index]
props.editor.commands.setTextSelection({ from, to: from })
const dom = props.editor.view.dom
const selection = window.getSelection()
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0)
range.commonAncestorContainer.scrollIntoView({
behavior: 'smooth',
block: 'center',
})
}
}
const findNext = () => {
if (matches.value.length === 0) return
clearHighlights()
currentMatch.value = (currentMatch.value + 1) % matches.value.length
highlightMatch(currentMatch.value)
scrollToMatch(currentMatch.value)
}
const findPrev = () => {
if (matches.value.length === 0) return
clearHighlights()
currentMatch.value =
(currentMatch.value - 1 + matches.value.length) % matches.value.length
highlightMatch(currentMatch.value)
scrollToMatch(currentMatch.value)
}
const close = () => {
clearHighlights()
searchQuery.value = ''
matches.value = []
emit('close')
}
watch(
() => props.visible,
async (newVal) => {
if (newVal) {
await nextTick()
inputRef.value?.focus()
if (props.editor && searchQuery.value) {
console.log('visible change')
findInDocument()
}
}
},
)
watch(
() => props.editor,
(newEditor) => {
if (newEditor && props.visible && searchQuery.value) {
console.log('editor change')
findInDocument()
}
},
)
watch(searchQuery, () => {
console.log('search change')
findInDocument()
})
</script> </script>
<style lang="scss"> <style lang="scss">
.note-find { .note-find {
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 100;
.find-bar {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
background: var(--theme-bg);
border-bottom: 1px solid var(--grey-100);
padding: 12px 10px;
.search-input {
flex: 1;
}
.match-count {
font-size: 12px;
color: var(--grey-100);
min-width: 50px;
text-align: center;
}
button {
background: transparent;
border: 1px solid var(--grey-100);
border-radius: 4px;
color: var(--theme-fg);
cursor: pointer;
padding: 4px 8px;
font-size: 12px;
&:hover:not(:disabled) {
background: var(--grey-300);
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
}
} }
</style> </style>

View File

@@ -0,0 +1,52 @@
/* Code styling */
.hljs-comment,
.hljs-quote {
color: var(--grey-100);
}
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-tag,
.hljs-name,
.hljs-regexp,
.hljs-link,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #ff5b69;
}
.hljs-number,
.hljs-meta,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params {
color: #ffa55b;
}
.hljs-string,
.hljs-symbol,
.hljs-bullet {
color: var(--theme-accent);
}
.hljs-title,
.hljs-section {
color: #fcff5b;
}
.hljs-keyword,
.hljs-selector-tag {
color: #5b9aff;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}

View File

@@ -9,6 +9,7 @@
@use 'font-style' as *; @use 'font-style' as *;
@use 'layout' as *; @use 'layout' as *;
@use 'scroll' as *; @use 'scroll' as *;
@use 'syntax' as *;
@use 'transitions' as *; @use 'transitions' as *;
:root { :root {

View File

@@ -1,7 +1,5 @@
<template> <template>
<main class="category layout-block"> <main class="category layout-block">
<router-link class="back" to="/"><- Go Back</router-link>
<category-row <category-row
:index="categoryIndex" :index="categoryIndex"
:category="id" :category="id"
@@ -61,13 +59,8 @@ const categoryIndex = computed(() => {
<style lang="scss"> <style lang="scss">
main.category { main.category {
padding-top: 1.24em; padding-top: var(--nav-height);
.back {
display: block;
opacity: 0.25;
margin-top: 9px;
}
.category-row { .category-row {
margin-top: 4px; margin-top: 4px;
} }

View File

@@ -17,6 +17,6 @@ const onCategoryEdited = (name) => {
<style lang="scss"> <style lang="scss">
.create-category { .create-category {
padding-top: 1.2em; padding-top: var(--nav-height);
} }
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<main class="directory layout-block"> <main v-if="loaded" class="directory layout-block">
<category-row <category-row
v-for="(category, i) in categories" v-for="(category, i) in categories"
:index="i" :index="i"
@@ -13,6 +13,8 @@
<note-row v-for="note in notes" :note="note" :key="note.id" /> <note-row v-for="note in notes" :note="note" :key="note.id" />
</div> </div>
</main> </main>
<page-loading v-else />
</template> </template>
<script setup> <script setup>
@@ -22,6 +24,7 @@ import useConfig from '@/composables/useConfig'
import { onMounted, ref, watch } from 'vue' import { onMounted, ref, watch } from 'vue'
import CategoryRow from '@/components/CategoryRow.vue' import CategoryRow from '@/components/CategoryRow.vue'
import NoteRow from '@/components/NoteRow.vue' import NoteRow from '@/components/NoteRow.vue'
import PageLoading from '@/components/PageLoading.vue'
const { categories, loadCategories, loadCategoryNotes, notesChangeCount } = const { categories, loadCategories, loadCategoryNotes, notesChangeCount } =
useNotes() useNotes()
@@ -29,10 +32,13 @@ const { categories, loadCategories, loadCategoryNotes, notesChangeCount } =
const { config } = useConfig() const { config } = useConfig()
const notes = ref() const notes = ref()
const loaded = ref(false)
async function refreshNotes() { const refreshNotes = async () => {
loaded.value = false
await loadCategories() await loadCategories()
notes.value = await loadCategoryNotes() notes.value = await loadCategoryNotes()
loaded.value = true
} }
onMounted(async () => { onMounted(async () => {
@@ -46,7 +52,7 @@ watch(notesChangeCount, async () => {
<style lang="scss"> <style lang="scss">
main.directory { main.directory {
padding-top: 26px; padding-top: var(--nav-height);
padding-bottom: 30px; padding-bottom: 30px;
.label { .label {

View File

@@ -1,7 +1,5 @@
<template> <template>
<main class="instructions layout-block"> <main class="instructions layout-block">
<router-link class="back-link" to="/"><- Go Back</router-link>
<div class="content" v-html="renderedContent" /> <div class="content" v-html="renderedContent" />
</main> </main>
</template> </template>
@@ -16,14 +14,7 @@ const renderedContent = md.render(content)
<style lang="scss"> <style lang="scss">
main.instructions { main.instructions {
padding-top: 1.2em; padding-top: var(--nav-height);
.back-link {
opacity: 0.25;
display: block;
margin-top: 9px;
margin-bottom: 14px;
}
.content { .content {
display: flex; display: flex;

View File

@@ -1,29 +1,41 @@
<template> <template>
<main class="note layout-block"> <main class="note layout-block">
<note-editor :id="id" /> <note-download :editor="editorRef?.editor" />
<note-find
:editor="editorRef?.editor"
:visible="findVisible"
@close="findVisible = false"
/>
<note-editor ref="editorRef" :id="id" />
</main> </main>
</template> </template>
<script setup> <script setup>
import { watchEffect } from 'vue' import { ref, watchEffect } from 'vue'
import { useMagicKeys } from '@vueuse/core' import { useMagicKeys } from '@vueuse/core'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import NoteEditor from '@/components/note/Editor.vue' import NoteEditor from '@/components/note/Editor.vue'
import NoteFind from '@/components/note/Find.vue'
import NoteDownload from '@/components/note/Download.vue'
const route = useRoute() const route = useRoute()
const id = route.params.id const id = route.params.id
const editorRef = ref(null)
const findVisible = ref(false)
const { ctrl, f } = useMagicKeys() const { ctrl, f } = useMagicKeys()
watchEffect(() => { watchEffect(() => {
if (ctrl.value && f.value) { if (ctrl.value && f.value) {
console.log('find') findVisible.value = !findVisible.value
} }
}) })
</script> </script>
<style lang="scss"> <style lang="scss">
main.note { main.note {
padding-top: 2.2em; padding-top: 8px;
padding-bottom: 20px; padding-bottom: 20px;
} }
</style> </style>

View File

@@ -1,7 +1,5 @@
<template> <template>
<main class="preferences layout-block"> <main class="preferences layout-block">
<router-link to="/" class="back"><- Back</router-link>
<h1 class="mono">Storage Plugin</h1> <h1 class="mono">Storage Plugin</h1>
<div v-for="plugin in plugins" class="plugin" :key="plugin.id"> <div v-for="plugin in plugins" class="plugin" :key="plugin.id">
@@ -112,16 +110,9 @@ const save = async () => {
<style lang="scss"> <style lang="scss">
.preferences { .preferences {
padding-top: 1.2em; padding-top: var(--nav-height);
padding-bottom: 60px; padding-bottom: 60px;
.back {
opacity: 0.25;
display: block;
margin-top: 9px;
margin-bottom: 14px;
}
h1 { h1 {
margin-bottom: 20px; margin-bottom: 20px;
} }

View File

@@ -1,17 +1,12 @@
<template> <template>
<main class="search layout-block"> <main class="search layout-block">
<router-link class="back" to="/"><- Back</router-link>
<form @submit.prevent="onSearch"> <form @submit.prevent="onSearch">
<div class="input-wrap"> <search-input
<input v-model="query"
v-model="query" placeholder="Search"
type="text" ref="searchInput"
placeholder="Search" @input="onInput"
ref="searchInput" />
@input="onInput"
/>
</div>
</form> </form>
<div class="results"> <div class="results">
@@ -26,6 +21,7 @@
<script setup> <script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue' import { ref, onMounted, onBeforeUnmount } from 'vue'
import SearchInput from '@/components/SearchInput.vue'
import useNotes from '@/composables/useNotes' import useNotes from '@/composables/useNotes'
import NoteRow from '@/components/NoteRow.vue' import NoteRow from '@/components/NoteRow.vue'
import _debounce from 'lodash/debounce' import _debounce from 'lodash/debounce'
@@ -54,52 +50,7 @@ const onInput = _debounce(async () => {
<style lang="scss"> <style lang="scss">
main.search { main.search {
padding-top: 1.2em; padding-top: var(--nav-height);
.back {
display: block;
opacity: 0.25;
margin-top: 9px;
}
.input-wrap {
margin-top: 19px;
position: relative;
input {
display: block;
width: 100%;
position: relative;
padding: 5px 15px 6px;
background: var(--theme-bg);
--clip-start: 16px;
clip-path: polygon(
var(--clip-start) 1px,
calc(100% - var(--clip-start)) 1px,
calc(100% - 1.5px) 50%,
calc(100% - var(--clip-start)) calc(100% - 1px),
var(--clip-start) calc(100% - 1px),
1.5px 50%
);
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--theme-fg);
--clip-start: 15px;
clip-path: polygon(
var(--clip-start) 0,
calc(100% - var(--clip-start)) 0,
100% 50%,
calc(100% - var(--clip-start)) 100%,
var(--clip-start) 100%,
0% 50%
);
}
}
.results { .results {
margin-top: 20px; margin-top: 20px;