Skip to content

Commit ae19dda

Browse files
authored
Add Markdown notes to bookmarks (#213)
* Add Markdown notes to bookmarks * Preserve omitted bookmark draft notes
1 parent 5b3866c commit ae19dda

7 files changed

Lines changed: 138 additions & 20 deletions

File tree

packages/web/src/components/CardInlineEditor.svelte

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,20 @@
130130
});
131131
});
132132
133-
const hasBody = $derived('body' in item && item.type !== 'bookmark');
133+
const hasBody = $derived(item.type === 'bookmark' || 'body' in item);
134134
const bodyLabel = $derived(
135-
item.type === 'todo'
135+
item.type === 'bookmark'
136+
? 'Notes'
137+
: item.type === 'todo'
136138
? 'Details'
137139
: item.type === 'audio' || item.type === 'video'
138140
? 'Transcription or notes'
139141
: 'Content',
140142
);
141143
const bodyPlaceholder = $derived(
142-
item.type === 'todo'
144+
item.type === 'bookmark'
145+
? 'Add personal notes about this bookmark…'
146+
: item.type === 'todo'
143147
? 'Add details…'
144148
: item.type === 'email'
145149
? 'Email body…'

packages/web/src/components/add-entry/BookmarkForm.svelte

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { autofocus } from '../../lib/actions';
44
import type { BuildItemFn } from '../../lib/add-entry-modal';
55
import { buildBookmarkItem } from '../../lib/build-item';
6+
import MarkdownContentField from './MarkdownContentField.svelte';
67
78
let {
89
editItem,
@@ -28,6 +29,9 @@
2829
draftUrl = url;
2930
});
3031
let description = $state(editItem?.description ?? '');
32+
let body = $state(
33+
editItem && 'body' in editItem ? (editItem.body ?? '') : '',
34+
);
3135
3236
// The URL field is the only required input — title falls back to the URL
3337
// and description is optional. Surface readiness to the shell so the Save
@@ -43,7 +47,7 @@
4347
buildItem = ({ id, createdAt, editItem: ctxEditItem }) =>
4448
buildBookmarkItem(
4549
{ id, createdAt, editItem: ctxEditItem },
46-
{ url, title, description },
50+
{ url, title, description, body },
4751
);
4852
</script>
4953

@@ -60,7 +64,12 @@
6064
<input type="text" bind:value={title} placeholder="Page title" />
6165
</label>
6266
<label class="field">
63-
<span>Note</span>
64-
<textarea bind:value={description} rows="2" placeholder="Optional note..."
67+
<span>Page description</span>
68+
<textarea bind:value={description} rows="2" placeholder="Fetched automatically when left blank..."
6569
></textarea>
6670
</label>
71+
<MarkdownContentField
72+
bind:value={body}
73+
label="Notes"
74+
placeholder="Add personal notes about this bookmark..."
75+
/>

packages/web/src/components/view-card/BookmarkView.svelte

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
loadFileBlobUrl,
1212
userSettings,
1313
} from '../../lib/stores';
14+
import { renderMarkdown } from '../../lib/markdown';
1415
import Lightbox from '../Lightbox.svelte';
1516
1617
let {
@@ -29,6 +30,19 @@
2930
let fetchingPreview = $state(false);
3031
let previewStatus = $state<'' | 'none'>('');
3132
let previewError = $state('');
33+
let renderedBody = $state('');
34+
35+
$effect(() => {
36+
const currentItem = item;
37+
const currentBody = currentItem.body;
38+
renderedBody = '';
39+
if (!currentBody) return;
40+
renderMarkdown(currentBody).then((html) => {
41+
if (item.id === currentItem.id && item.body === currentBody) {
42+
renderedBody = html;
43+
}
44+
});
45+
});
3246
3347
// Fetch the page's metadata on demand for bookmarks saved before this
3448
// feature (or whose auto-fetch failed). Mirrors AudioView's transcribe
@@ -163,16 +177,15 @@
163177
/>
164178
{/if}
165179

166-
<!--
167-
Embedded body content captured by the extension (tweet text, article
168-
excerpt, etc.). We render it after the image so the bookmark's primary
169-
visual cue stays at the top, but before description so the user-supplied
170-
caption (if any) reads as a follow-up to the captured content.
171-
-->
172180
{#if item.body}
173181
<div class="content-block">
174-
<span class="content-label">Body</span>
175-
<p class="content-text">{item.body}</p>
182+
<span class="content-label">Notes</span>
183+
{#if renderedBody}
184+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
185+
<div class="markdown-body">{@html renderedBody}</div>
186+
{:else}
187+
<p class="content-text">{item.body}</p>
188+
{/if}
176189
</div>
177190
{/if}
178191
</section>

packages/web/src/lib/build-item.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,39 @@ describe('buildBookmarkItem', () => {
7171
expect((result.item as BookmarkItem).description).toBe(' ');
7272
});
7373

74+
it('stores Markdown notes separately from the fetched page description', () => {
75+
const result = buildBookmarkItem(ctx(), {
76+
url: 'https://example.com',
77+
title: 'Example',
78+
description: 'Page summary',
79+
body: '## My notes\n\nRemember **this**.',
80+
});
81+
82+
expect(result.item).toMatchObject({
83+
description: 'Page summary',
84+
body: '## My notes\n\nRemember **this**.',
85+
});
86+
});
87+
88+
it('can clear an existing bookmark body', () => {
89+
const editItem: BookmarkItem = {
90+
id: 'item-1',
91+
type: 'bookmark',
92+
title: 'Example',
93+
url: 'https://example.com',
94+
body: 'Old captured content',
95+
createdAt: '2026-04-29T08:00:00.000Z',
96+
};
97+
const result = buildBookmarkItem(ctx({ editItem }), {
98+
url: editItem.url,
99+
title: editItem.title,
100+
description: '',
101+
body: '',
102+
});
103+
104+
expect((result.item as BookmarkItem).body).toBeUndefined();
105+
});
106+
74107
it('preserves extension-fetched metadata across edits', () => {
75108
const editItem: BookmarkItem = {
76109
id: 'item-1',

packages/web/src/lib/build-item.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export interface BookmarkFormData {
7474
url: string;
7575
title: string;
7676
description: string;
77+
body?: string;
7778
}
7879

7980
export function buildBookmarkItem(
@@ -86,17 +87,20 @@ export function buildBookmarkItem(
8687
title: data.title || data.url,
8788
url: data.url,
8889
description: data.description || undefined,
90+
body: data.body || undefined,
8991
createdAt: ctx.createdAt,
9092
};
9193
// Preserve enrichment metadata fetched by the extension (favicon,
92-
// ogImage, siteName, embedded body, downloaded image) — the web form
93-
// doesn't surface these fields, so we'd lose them if we didn't copy
94-
// them through on edit.
94+
// ogImage, siteName, downloaded image). Older callers do not pass body,
95+
// so preserve extension-captured content for those callers; the bookmark
96+
// form passes body explicitly and can therefore edit or clear it.
9597
if (ctx.editItem && ctx.editItem.type === 'bookmark') {
9698
if (ctx.editItem.favicon) bookmark.favicon = ctx.editItem.favicon;
9799
if (ctx.editItem.ogImage) bookmark.ogImage = ctx.editItem.ogImage;
98100
if (ctx.editItem.siteName) bookmark.siteName = ctx.editItem.siteName;
99-
if (ctx.editItem.body) bookmark.body = ctx.editItem.body;
101+
if (data.body === undefined && ctx.editItem.body) {
102+
bookmark.body = ctx.editItem.body;
103+
}
100104
if (ctx.editItem.filePath) bookmark.filePath = ctx.editItem.filePath;
101105
if (ctx.editItem.mimeType) bookmark.mimeType = ctx.editItem.mimeType;
102106
}

packages/web/src/lib/card-draft.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,56 @@ describe('card drafts', () => {
6060
});
6161
});
6262

63+
it('adds Markdown notes to a bookmark that did not already have a body', () => {
64+
const bookmark: InboxItem = {
65+
id: 'bm-1',
66+
type: 'bookmark',
67+
title: 'Example',
68+
url: 'https://example.org',
69+
createdAt: note.createdAt,
70+
};
71+
const draft = createCardDraft(bookmark);
72+
draft.body = '## Remember\n\nUseful reference.';
73+
74+
expect(applyCardDraft(bookmark, draft)).toEqual({
75+
...bookmark,
76+
body: '## Remember\n\nUseful reference.',
77+
});
78+
});
79+
80+
it('preserves bookmark notes when a recovered draft omits body', () => {
81+
const bookmark: InboxItem = {
82+
id: 'bm-1',
83+
type: 'bookmark',
84+
title: 'Example',
85+
url: 'https://example.org',
86+
body: 'Existing notes',
87+
createdAt: note.createdAt,
88+
};
89+
const draft = createCardDraft(bookmark);
90+
delete draft.body;
91+
92+
expect(applyCardDraft(bookmark, draft)).toEqual(bookmark);
93+
});
94+
95+
it('clears bookmark notes only when body is explicitly empty', () => {
96+
const bookmark: InboxItem = {
97+
id: 'bm-1',
98+
type: 'bookmark',
99+
title: 'Example',
100+
url: 'https://example.org',
101+
body: 'Existing notes',
102+
createdAt: note.createdAt,
103+
};
104+
const draft = createCardDraft(bookmark);
105+
draft.body = '';
106+
107+
expect(applyCardDraft(bookmark, draft)).toEqual({
108+
...bookmark,
109+
body: undefined,
110+
});
111+
});
112+
63113
it('merges external updates only into untouched draft fields', () => {
64114
const bookmark: InboxItem = {
65115
id: 'bm-1',

packages/web/src/lib/card-draft.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function createCardDraft(item: InboxItem): CardDraft {
2121
title: item.title,
2222
description: item.description ?? '',
2323
};
24-
if ('body' in item) draft.body = item.body ?? '';
24+
if (item.type === 'bookmark' || 'body' in item) draft.body = item.body ?? '';
2525
if (item.type === 'bookmark') draft.url = item.url;
2626
if (item.type === 'email') {
2727
draft.from = item.from ?? '';
@@ -45,7 +45,12 @@ export function applyCardDraft(
4545
} as InboxItem;
4646
const record = updated as unknown as Record<string, unknown>;
4747

48-
if ('body' in item) record.body = draft.body ?? '';
48+
if (item.type === 'bookmark') {
49+
record.body =
50+
draft.body === undefined ? item.body : draft.body || undefined;
51+
} else if ('body' in item) {
52+
record.body = draft.body ?? '';
53+
}
4954
if (item.type === 'bookmark') record.url = draft.url ?? '';
5055
if (item.type === 'email') {
5156
record.from = draft.from || undefined;

0 commit comments

Comments
 (0)