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'} +
+
{ e.preventDefault(); applyLink(); }} + > + + +
+ + +
+
+ {/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();