Compare commits

...

2 Commits

Author SHA1 Message Date
nicwands
555cfd4a93 new title structure 2026-03-31 13:39:08 -04:00
nicwands
a3f82fb74c add summarium to move menu 2026-03-27 10:47:51 -04:00
9 changed files with 148 additions and 67 deletions

View File

@@ -2,14 +2,18 @@
<div v-if="open" class="move-menu layout-block">
<button class="cancel-button" @click="close">Cancel</button>
<button class="summarium-button" @click="onCategoryClick(null)">
SUMMARIUM
</button>
<template v-for="(category, i) in categories">
<category-row
v-if="category !== fromCategory"
:category="category"
:index="i"
wrapper="button"
@click="onCategoryClick(category)"
:key="category"
@click="onCategoryClick(category)"
/>
</template>
</div>
@@ -17,10 +21,9 @@
<script setup>
import CategoryRow from '@/components/CategoryRow.vue'
import { computed, onBeforeUnmount, watch } from 'vue'
import { computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import useNotes from '@/composables/useNotes'
import useState from '@/composables/useState'
import _omit from 'lodash/omit'
const { categories, updateNote } = useNotes()
@@ -39,7 +42,7 @@ const close = async () => {
await window.api.moveClosed()
}
const onCategoryClick = async (category) => {
if (!category || !noteId.value) return
if (!noteId.value) return
await updateNote(noteId.value, { category: category })
@@ -55,10 +58,20 @@ watch(open, async () => {
width: 50vw;
height: 100%;
border-left: 1px solid var(--grey-100);
display: flex;
flex-direction: column;
.cancel-button {
color: var(--grey-100);
padding: 9px 0 16px;
}
.summarium-button {
display: block;
margin: 10px 0;
&:hover {
color: var(--theme-accent);
}
}
}
</style>

View File

@@ -60,6 +60,10 @@ const moveActive = computed(() => route.query.move === props.note.id)
align-items: flex-start;
gap: 2px;
.title {
white-space: break-spaces;
word-break: break-word;
}
.action {
opacity: 0;

View File

@@ -1,38 +1,27 @@
<template>
<div class="note-download">
<button @click="download">{{ noteTitle }}.md </button>
<button @click="download">
<span class="title-text">{{ noteTitle }}</span>
<span class="extension">.md </span>
</button>
</div>
</template>
<script setup>
import { computed } from 'vue'
import _kebabCase from 'lodash/kebabCase'
import { computed } from 'vue'
const DEFAULT_TITLE = 'Untitled'
const props = defineProps({
editor: {
type: Object,
required: true,
},
title: String,
editor: Object,
})
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 noteTitle = computed(() => _kebabCase(props.title || DEFAULT_TITLE))
const download = () => {
if (!props.editor) return
if (!noteTitle.value || !props.editor) return
const content = props.editor.getMarkdown()
const blob = new Blob([content], { type: 'text/plain' })
@@ -53,6 +42,24 @@ const download = () => {
justify-content: center;
button {
display: inline-flex;
margin: auto;
justify-content: center;
.title-text {
display: block;
max-width: min(
300px,
calc(100vw - var(--layout-margin) * 2) - 36px
);
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.extension {
display: block;
flex-shrink: 0;
}
&:hover {
color: var(--theme-accent);
}

View File

@@ -1,5 +1,15 @@
<template>
<div v-if="editor" class="note-editor">
<div class="title">
<div
:class="['title-input p', { placeholder: !title }]"
contenteditable="true"
ref="titleInput"
@input="onTitleChange"
@keydown="onTitleKeydown"
/>
</div>
<editor-content :editor="editor" class="editor-wrap" />
<editor-menu :editor="editor" />
@@ -9,13 +19,22 @@
</template>
<script setup>
import { onBeforeUnmount, onMounted, shallowRef } from 'vue'
import {
onBeforeUnmount,
onMounted,
shallowRef,
ref,
watch,
nextTick,
} from 'vue'
import PageLoading from '@/components/PageLoading.vue'
import { Editor, EditorContent } from '@tiptap/vue-3'
import { extensions } from '@/libs/editorExtensions'
import useNotes from '@/composables/useNotes'
import _debounce from 'lodash/debounce'
import EditorMenu from './Menu.vue'
import { extensions } from '@/libs/editorExtensions'
const TITLE_CHAR_LIMIT = 100
const props = defineProps({
id: {
@@ -25,6 +44,8 @@ const props = defineProps({
})
const editor = shallowRef()
const title = ref('')
const titleInput = ref()
const { loadNote, updateNote } = useNotes()
@@ -32,25 +53,17 @@ const onUpdate = _debounce(async ({ editor }) => {
const json = editor.getJSON()
const text = editor.getText()
// Get doc title
let title
const doc = editor.state.doc
const firstNode = doc.firstChild
if (!firstNode || firstNode.type.name !== 'heading') title = 'Untitled'
title = firstNode.textContent.trim() || 'Untitled'
await updateNote(props.id, {
title,
content: json,
plainText: text,
})
}, 300)
}, 500)
onMounted(async () => {
const note = await loadNote(props.id)
title.value = note.title || ''
editor.value = new Editor({
extensions,
content: note.content || [],
@@ -59,31 +72,69 @@ onMounted(async () => {
// Clear any highlights from find feature
editor.value.chain().selectAll().unsetHighlight().run()
await nextTick()
if (titleInput.value) {
titleInput.value.textContent = title.value
}
})
onBeforeUnmount(() => {
editor.value?.destroy?.()
})
// Make sure title input is synced
watch(title, async (newVal) => {
if (!titleInput.value) return
if (titleInput.value.textContent !== newVal) {
titleInput.value.textContent = newVal
}
})
const updateTitle = _debounce((title) => {
updateNote(props.id, {
title: title === '' ? 'Untitled' : title,
})
}, 500)
const onTitleChange = (e) => {
title.value = e.target.textContent
updateTitle(title.value)
}
const onTitleKeydown = (e) => {
if (title.value.length >= TITLE_CHAR_LIMIT && e.key.length === 1) {
e.preventDefault()
}
}
defineExpose({
title,
editor,
})
</script>
<style lang="scss">
.note-editor {
.title {
width: 100%;
margin-bottom: 40px;
padding-bottom: 0.5em;
border-bottom: 1px dashed currentColor;
.title-input {
white-space: break-spaces;
word-break: break-word;
@include drop-cap;
&.placeholder:before {
content: 'Title';
color: var(--grey-100);
}
}
}
h1 {
font-weight: 700 !important;
@include p;
&:first-child {
@include drop-cap;
}
}
h1.is-editor-empty:first-child::before {
color: var(--grey-100);
content: attr(data-placeholder);
pointer-events: none;
@include drop-cap;
}
p strong {
font-weight: 700;
@@ -155,6 +206,9 @@ defineExpose({
background: currentColor;
}
}
u {
text-decoration: underline;
}
.highlighted {
background: var(--theme-accent);
color: var(--theme-bg);

View File

@@ -3,34 +3,18 @@ import StarterKit from '@tiptap/starter-kit'
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
import { TaskList, TaskItem } from '@tiptap/extension-list'
import { Highlight } from '@tiptap/extension-highlight'
import Document from '@tiptap/extension-document'
import { all, createLowlight } from 'lowlight'
import { Placeholder } from '@tiptap/extensions'
// Lowlight setup
const lowlight = createLowlight(all)
// Force note format
const CustomDocument = Document.extend({
content: 'heading block*',
})
export const extensions = [
CustomDocument,
StarterKit.configure({
document: false,
heading: { levels: [1] },
trailingNode: {
node: 'paragraph',
},
}),
Placeholder.configure({
placeholder: ({ node }) => {
if (node.type.name === 'heading') {
return 'Title'
}
},
}),
TaskList,
TaskItem,
Highlight.configure({

View File

@@ -57,5 +57,6 @@
&:first-child::first-letter {
font-family: var(--font-display);
font-size: 42px;
line-height: 0.7;
}
}

View File

@@ -27,8 +27,13 @@ const route = useRoute()
const id = route.params?.id
const router = useRouter()
const { categories, loadCategoryNotes, updateCategory, notesChangeCount } =
useNotes()
const {
categories,
loadCategoryNotes,
updateCategory,
notesChangeCount,
loadCategories,
} = useNotes()
const notes = ref()
@@ -40,6 +45,11 @@ async function refreshNotes() {
onMounted(async () => {
await refreshNotes()
if (!categories.value?.length) {
await loadCategories()
console.log(categories.value)
}
})
watch(notesChangeCount, async () => {
@@ -53,7 +63,8 @@ const onCategoryEdited = async (editedCategory) => {
}
const categoryIndex = computed(() => {
return categories.value?.findIndex((category) => category === id) || 1
if (!categories.value) return 0
return categories.value?.findIndex((category) => category === id)
})
</script>

View File

@@ -12,6 +12,8 @@
<div class="notes">
<note-row v-for="note in notes" :note="note" :key="note.id" />
</div>
<new-note />
</main>
<page-loading v-else />
@@ -23,6 +25,7 @@ import { onMounted, ref, watch } from 'vue'
import CategoryRow from '@/components/CategoryRow.vue'
import NoteRow from '@/components/NoteRow.vue'
import PageLoading from '@/components/PageLoading.vue'
import NewNote from '@/components/NewNote.vue'
const { categories, loadCategories, loadCategoryNotes, notesChangeCount } =
useNotes()
@@ -61,5 +64,9 @@ main.directory {
flex-direction: column;
gap: 14px;
}
.new-note {
display: block;
margin: 50px auto 0;
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<main class="note layout-block">
<note-download :editor="editorRef?.editor" />
<note-download :title="editorRef?.title" :editor="editorRef?.editor" />
<note-find
:editor="editorRef?.editor"