diff --git a/package.json b/package.json
index 417241c..3b6ded4 100644
--- a/package.json
+++ b/package.json
@@ -28,12 +28,15 @@
"@tiptap/extension-placeholder": "^3.20.0",
"@tiptap/extension-task-item": "^3.20.0",
"@tiptap/extension-task-list": "^3.20.0",
+ "@tiptap/extension-text-align": "^3.20.0",
+ "@tiptap/extension-underline": "^3.20.0",
"@tiptap/pm": "^3.20.0",
"@tiptap/starter-kit": "^3.20.0",
"argon2": "^0.43.0",
"better-sqlite3": "^11.0.0",
"drizzle-orm": "^0.44.0",
"idb": "^8.0.0",
+ "lucide-svelte": "^0.577.0",
"markdown-it": "^14.0.0",
"tiptap-markdown": "^0.9.0",
"uuid": "^11.0.0"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c00e590..ef0a449 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -26,6 +26,12 @@ importers:
'@tiptap/extension-task-list':
specifier: ^3.20.0
version: 3.20.0(@tiptap/extension-list@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))
+ '@tiptap/extension-text-align':
+ specifier: ^3.20.0
+ version: 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))
+ '@tiptap/extension-underline':
+ specifier: ^3.20.0
+ version: 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))
'@tiptap/pm':
specifier: ^3.20.0
version: 3.20.0
@@ -44,6 +50,9 @@ importers:
idb:
specifier: ^8.0.0
version: 8.0.3
+ lucide-svelte:
+ specifier: ^0.577.0
+ version: 0.577.0(svelte@5.53.7)
markdown-it:
specifier: ^14.0.0
version: 14.1.1
@@ -1584,6 +1593,11 @@ packages:
peerDependencies:
'@tiptap/extension-list': ^3.20.0
+ '@tiptap/extension-text-align@3.20.0':
+ resolution: {integrity: sha512-4s0r+bovtH6yeGDUD+Ui8j5WOV5koB5P6AuzOMqoLwaFGRSkKf64ly6DXjjmjIgnYCLZN/XO6llaQKVVdvad2g==}
+ peerDependencies:
+ '@tiptap/core': ^3.20.0
+
'@tiptap/extension-text@3.20.0':
resolution: {integrity: sha512-tf8bE8tSaOEWabCzPm71xwiUhyMFKqY9jkP5af3Kr1/F45jzZFIQAYZooHI/+zCHRrgJ99MQHKHe1ZNvODrKHQ==}
peerDependencies:
@@ -2544,6 +2558,11 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+ lucide-svelte@0.577.0:
+ resolution: {integrity: sha512-0i88o57KsaHWnc80J57fY99CWzlZsSdtH5kKjLUJa7z8dum/9/AbINNLzJ7NiRFUdOgMnfAmJt8jFbW2zeC5qQ==}
+ peerDependencies:
+ svelte: ^3 || ^4 || ^5.0.0-next.42
+
magic-string@0.25.9:
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
@@ -4675,6 +4694,10 @@ snapshots:
dependencies:
'@tiptap/extension-list': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)
+ '@tiptap/extension-text-align@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))':
+ dependencies:
+ '@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
+
'@tiptap/extension-text@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))':
dependencies:
'@tiptap/core': 3.20.0(@tiptap/pm@3.20.0)
@@ -5669,6 +5692,10 @@ snapshots:
dependencies:
yallist: 3.1.1
+ lucide-svelte@0.577.0(svelte@5.53.7):
+ dependencies:
+ svelte: 5.53.7
+
magic-string@0.25.9:
dependencies:
sourcemap-codec: 1.4.8
diff --git a/src/app.html b/src/app.html
index 1986966..377a233 100644
--- a/src/app.html
+++ b/src/app.html
@@ -1,14 +1,17 @@
-
-
-
-
-
- Crumbs
- %sveltekit.head%
-
-
- %sveltekit.body%
-
+
+
+
+
+
+ Crumbs
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
diff --git a/src/lib/components/FormattingToolbar.svelte b/src/lib/components/FormattingToolbar.svelte
index 2acd6b3..771136b 100644
--- a/src/lib/components/FormattingToolbar.svelte
+++ b/src/lib/components/FormattingToolbar.svelte
@@ -1,5 +1,29 @@
+
+
- {#each buttons as btn}
- {#if btn.separator}
-
- {:else}
-
+
+
+
+
+
+
+
+
+
+ {#if openDropdown === 'heading'}
+
+
+
+
+
{/if}
- {/each}
+
+
+
+
+
+ {#if openDropdown === 'list'}
+
+
+
+
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if openDropdown === 'link'}
+
+ {/if}
+
+
+
+
+
+
+
+ {#if openDropdown === 'align'}
+
+
+
+
+
+
+ {/if}
+
+
+
+
+
+
diff --git a/src/lib/components/NoteCard.svelte b/src/lib/components/NoteCard.svelte
index e95e235..c4cffa6 100644
--- a/src/lib/components/NoteCard.svelte
+++ b/src/lib/components/NoteCard.svelte
@@ -165,3 +165,25 @@
{/if}
+
+
diff --git a/src/lib/components/NoteEditor.svelte b/src/lib/components/NoteEditor.svelte
index 680925a..97f6c77 100644
--- a/src/lib/components/NoteEditor.svelte
+++ b/src/lib/components/NoteEditor.svelte
@@ -103,8 +103,8 @@
bind:this={textareaEl}
placeholder="Take a note..."
bind:value={content}
- class="min-h-[100px] w-full resize-none bg-transparent px-4 py-2 text-sm text-gray-800 outline-none placeholder:text-gray-500 dark:text-gray-200"
- rows="6"
+ class="min-h-[300px] w-full resize-none bg-transparent px-4 py-2 text-sm text-gray-800 outline-none placeholder:text-gray-500 dark:text-gray-200"
+ rows="12"
data-testid="note-content-input"
>
{:else}
diff --git a/src/lib/components/TiptapEditor.svelte b/src/lib/components/TiptapEditor.svelte
index c5d0653..73c83c1 100644
--- a/src/lib/components/TiptapEditor.svelte
+++ b/src/lib/components/TiptapEditor.svelte
@@ -2,9 +2,9 @@
import { onMount } from 'svelte';
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
- import TaskList from '@tiptap/extension-task-list';
- import TaskItem from '@tiptap/extension-task-item';
import Link from '@tiptap/extension-link';
+ import Underline from '@tiptap/extension-underline';
+ import TextAlign from '@tiptap/extension-text-align';
import Placeholder from '@tiptap/extension-placeholder';
import { Markdown } from 'tiptap-markdown';
@@ -25,9 +25,9 @@
element: element!,
extensions: [
StarterKit,
- TaskList,
- TaskItem.configure({ nested: true }),
Link.configure({ openOnClick: false }),
+ Underline,
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
Placeholder.configure({ placeholder }),
Markdown
],
@@ -56,7 +56,7 @@
@@ -73,19 +73,4 @@
pointer-events: none;
height: 0;
}
-
- .tiptap-wrapper :global(.tiptap ul[data-type='taskList']) {
- list-style: none;
- padding: 0;
- }
-
- .tiptap-wrapper :global(.tiptap ul[data-type='taskList'] li) {
- display: flex;
- align-items: flex-start;
- gap: 0.5rem;
- }
-
- .tiptap-wrapper :global(.tiptap ul[data-type='taskList'] li label) {
- margin-top: 0.25rem;
- }
diff --git a/tests/e2e/formatting.spec.ts b/tests/e2e/formatting.spec.ts
index c025187..e72d234 100644
--- a/tests/e2e/formatting.spec.ts
+++ b/tests/e2e/formatting.spec.ts
@@ -101,6 +101,38 @@ test.describe('Rich text formatting', () => {
await expect(page.getByTestId('tiptap-editor').locator('strong')).toHaveText('bold');
});
+ test('Scenario: Toolbar dropdown closes when clicking outside', async ({ authenticatedPage: page }) => {
+ // Given the user has opened the heading dropdown
+ await page.getByTestId('new-note-btn').click();
+ await page.getByTestId('format-heading').click();
+ await expect(page.getByTestId('format-h1')).toBeVisible();
+
+ // When the user clicks inside the editor area
+ await page.getByTestId('tiptap-editor').click();
+
+ // Then the dropdown closes
+ await expect(page.getByTestId('format-h1')).not.toBeVisible();
+ });
+
+ test('Scenario: Link popover allows inserting a link inline', async ({ authenticatedPage: page }) => {
+ // Given the user is editing a note with selected text
+ await page.getByTestId('new-note-btn').click();
+ await typeViaMarkdown(page, 'visit example');
+ await runTiptapCommand(page, 'editor.chain().focus().setTextSelection({from:7,to:14}).run()');
+
+ // When the user opens the link popover and enters a URL
+ await page.getByTestId('format-link').click();
+ await expect(page.getByTestId('format-link-input')).toBeVisible();
+ await page.getByTestId('format-link-input').fill('https://example.com');
+ await page.getByTestId('format-link-apply').click();
+
+ // Then the selected text becomes a link
+ const editor = page.getByTestId('tiptap-editor').locator('.tiptap');
+ const link = editor.locator('a');
+ await expect(link).toHaveText('example');
+ await expect(link).toHaveAttribute('href', 'https://example.com');
+ });
+
test('Scenario: Markdown toggle shows raw markdown content', async ({ authenticatedPage: page }) => {
// Given the user is editing a note with bold content
await page.getByTestId('new-note-btn').click();