Skip to content

Commit d859e8e

Browse files
fjbarrettclaude
andauthored
chore: remove tags feature from UI (#109)
Tags remain in the DB schema but are no longer shown or edited in the interface — cleans up the editor toolbar, sidebar filter strip, and note-level tag state. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6355193 commit d859e8e

5 files changed

Lines changed: 19 additions & 158 deletions

File tree

__tests__/inferTitle.test.ts

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -79,40 +79,26 @@ describe("needsInferredTitle", () => {
7979

8080
describe("searchableText", () => {
8181
it("uses body when present", () => {
82-
expect(searchableText({ body: "body", title: "title", tags: [] })).toBe(
83-
"body",
84-
);
82+
expect(searchableText({ body: "body", title: "title" })).toBe("body");
8583
});
8684

8785
it("falls back to title when body is empty", () => {
88-
expect(searchableText({ body: "", title: "title", tags: [] })).toBe(
89-
"title",
90-
);
91-
});
92-
93-
it("appends tags", () => {
94-
expect(
95-
searchableText({ body: "body", title: "title", tags: ["a", "b"] }),
96-
).toBe("body a b");
86+
expect(searchableText({ body: "", title: "title" })).toBe("title");
9787
});
9888
});
9989

10090
describe("previewText", () => {
10191
it("infers title for notes without explicit title", () => {
102-
const result = previewText({ body: "Hello world", title: "", tags: [] });
92+
const result = previewText({ body: "Hello world", title: "" });
10393
expect(result).toBe("Hello world");
10494
});
10595

10696
it("uses saved title when valid", () => {
107-
const result = previewText({
108-
body: "long body text here",
109-
title: "My Note",
110-
tags: [],
111-
});
97+
const result = previewText({ body: "long body text here", title: "My Note" });
11298
expect(result).toBe("My Note");
11399
});
114100

115101
it("returns fallback for blank note", () => {
116-
expect(previewText({ body: "", title: "", tags: [] })).toBe("Untitled note");
102+
expect(previewText({ body: "", title: "" })).toBe("Untitled note");
117103
});
118104
});

components/NoteEditor.tsx

Lines changed: 7 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
PinFilledIcon,
1414
PinIcon,
1515
ShareIcon,
16-
TagIcon,
1716
TrashIcon,
1817
UnarchiveIcon,
1918
XIcon,
@@ -63,9 +62,6 @@ export function NoteEditor({
6362
const [body, setBody] = useState("");
6463
const [pinned, setPinned] = useState(false);
6564
const [archived, setArchived] = useState(false);
66-
const [tags, setTags] = useState<string[]>([]);
67-
const [tagInput, setTagInput] = useState("");
68-
const [tagOpen, setTagOpen] = useState(false);
6965
const [dirty, setDirty] = useState(false);
7066
const [uploading, setUploading] = useState(false);
7167
const [previewOpen, setPreviewOpen] = useState(false);
@@ -95,16 +91,12 @@ export function NoteEditor({
9591
setPinned(target.note.pinned);
9692
setArchived(target.note.archived);
9793
setHighlight(Boolean(target.note.highlight));
98-
setTags(target.note.tags ?? []);
9994
} else {
10095
setBody("");
10196
setPinned(false);
10297
setArchived(false);
10398
setHighlight(false);
104-
setTags([]);
10599
}
106-
setTagInput("");
107-
setTagOpen(false);
108100
setDirty(false);
109101
setHistoryOpen(false);
110102
setVersions([]);
@@ -134,7 +126,7 @@ export function NoteEditor({
134126
if (!target || !dirty) return;
135127
if (target.mode === "edit") {
136128
const timer = window.setTimeout(() => {
137-
onUpdate(target.note.id, { body, pinned, archived, highlight, tags });
129+
onUpdate(target.note.id, { body, pinned, archived, highlight });
138130
setDirty(false);
139131
}, 550);
140132
return () => window.clearTimeout(timer);
@@ -143,7 +135,7 @@ export function NoteEditor({
143135
if (createdIdRef.current) {
144136
const id = createdIdRef.current;
145137
const timer = window.setTimeout(() => {
146-
onUpdate(id, { body, pinned, archived, highlight, tags });
138+
onUpdate(id, { body, pinned, archived, highlight });
147139
setDirty(false);
148140
}, 550);
149141
return () => window.clearTimeout(timer);
@@ -152,7 +144,7 @@ export function NoteEditor({
152144
const timer = window.setTimeout(async () => {
153145
if (createdIdRef.current || creatingRef.current) return;
154146
creatingRef.current = true;
155-
const note = await onCreate({ body, pinned, archived, highlight, tags });
147+
const note = await onCreate({ body, pinned, archived, highlight });
156148
creatingRef.current = false;
157149
if (note) {
158150
createdIdRef.current = note.id;
@@ -161,7 +153,7 @@ export function NoteEditor({
161153
}, 550);
162154
return () => window.clearTimeout(timer);
163155
}
164-
}, [archived, body, dirty, highlight, onCreate, onUpdate, pinned, tags, target]);
156+
}, [archived, body, dirty, highlight, onCreate, onUpdate, pinned, target]);
165157

166158
function markBody(value: string) {
167159
setBody(value);
@@ -178,19 +170,6 @@ export function NoteEditor({
178170
setDirty(true);
179171
}
180172

181-
function addTag(tag: string) {
182-
const t = tag.trim().toLowerCase();
183-
if (!t || tags.includes(t)) return;
184-
setTags((prev) => [...prev, t]);
185-
setTagInput("");
186-
setDirty(true);
187-
}
188-
189-
function removeTag(tag: string) {
190-
setTags((prev) => prev.filter((t) => t !== tag));
191-
setDirty(true);
192-
}
193-
194173
async function loadHistory() {
195174
if (!target || target.mode !== "edit") return;
196175
try {
@@ -270,18 +249,18 @@ export function NoteEditor({
270249

271250
function flushEdit() {
272251
if (!target || target.mode !== "edit") return;
273-
onUpdate(target.note.id, { body, pinned, archived, highlight, tags });
252+
onUpdate(target.note.id, { body, pinned, archived, highlight });
274253
setDirty(false);
275254
}
276255

277256
function close() {
278257
if (!target) return;
279258
if (target.mode === "new") {
280259
if (createdIdRef.current) {
281-
onUpdate(createdIdRef.current, { body, pinned, archived, highlight, tags });
260+
onUpdate(createdIdRef.current, { body, pinned, archived, highlight });
282261
} else if (body.trim() && !creatingRef.current) {
283262
creatingRef.current = true;
284-
onCreate({ body, pinned, archived, highlight, tags });
263+
onCreate({ body, pinned, archived, highlight });
285264
}
286265
} else {
287266
flushEdit();
@@ -310,15 +289,6 @@ export function NoteEditor({
310289
)}
311290
{!isTrashed ? (
312291
<>
313-
<button
314-
type="button"
315-
onClick={() => setTagOpen((v) => !v)}
316-
className={iconToggle(tags.length > 0 || tagOpen)}
317-
title="Tags"
318-
aria-label="Tags"
319-
>
320-
<TagIcon className="h-4 w-4" />
321-
</button>
322292
<button
323293
type="button"
324294
onClick={() => { setHighlight((v) => !v); setDirty(true); }}
@@ -494,61 +464,6 @@ export function NoteEditor({
494464
<>
495465
{!historyOpen && header}
496466

497-
{(tags.length > 0 || tagOpen) && (
498-
<div className="flex w-full max-w-3xl flex-wrap items-center gap-1.5 px-6 py-2">
499-
{tags.map((tag) => (
500-
<span
501-
key={tag}
502-
className="inline-flex items-center gap-1 rounded-full bg-[var(--color-surface-hover)] px-2 py-0.5 text-xs text-[var(--color-text)]"
503-
>
504-
{tag}
505-
<button
506-
type="button"
507-
onClick={() => removeTag(tag)}
508-
className="ml-0.5 text-[var(--color-muted)] hover:text-[var(--color-text)]"
509-
aria-label={`Remove tag ${tag}`}
510-
>
511-
<XIcon className="h-2.5 w-2.5" />
512-
</button>
513-
</span>
514-
))}
515-
{tagOpen && (
516-
<input
517-
autoFocus
518-
value={tagInput}
519-
onChange={(e) => setTagInput(e.target.value)}
520-
onKeyDown={(e) => {
521-
if (e.key === "Enter" || e.key === ",") {
522-
e.preventDefault();
523-
addTag(tagInput);
524-
}
525-
if (e.key === "Backspace" && !tagInput && tags.length > 0) {
526-
removeTag(tags[tags.length - 1]);
527-
}
528-
if (e.key === "Escape") {
529-
setTagOpen(false);
530-
setTagInput("");
531-
}
532-
}}
533-
onBlur={() => {
534-
if (tagInput.trim()) addTag(tagInput);
535-
setTagOpen(false);
536-
}}
537-
placeholder="Add tag..."
538-
name="note-tag"
539-
autoComplete="off"
540-
autoCorrect="off"
541-
autoCapitalize="off"
542-
data-1p-ignore
543-
data-lpignore="true"
544-
data-bwignore
545-
data-form-type="other"
546-
className="min-w-[80px] flex-1 border-0 bg-transparent text-xs text-[var(--color-text)] placeholder:text-[var(--color-muted)] focus:outline-none"
547-
/>
548-
)}
549-
</div>
550-
)}
551-
552467
{historyOpen ? (
553468
<VersionHistory
554469
versions={versions}

components/NotesView.tsx

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ export function NotesView({
5858
const [viewMode, setViewMode] = useState<"active" | "archive" | "trash">(
5959
"active",
6060
);
61-
const [tagFilter, setTagFilter] = useState<string | null>(null);
6261
const [bannerDismissed, setBannerDismissed] = useState(false);
6362
const searchRef = useRef<HTMLInputElement>(null);
6463
const importRef = useRef<HTMLInputElement>(null);
@@ -72,23 +71,14 @@ export function NotesView({
7271
[notes],
7372
);
7473

75-
const allTags = useMemo(() => {
76-
const set = new Set<string>();
77-
for (const n of notes) {
78-
if (!n.trashed) for (const t of n.tags) set.add(t);
79-
}
80-
return [...set].sort();
81-
}, [notes]);
82-
8374
const filtered = useMemo(() => {
8475
const q = searchOpen ? query.trim() : "";
8576
const viewFiltered = notes
8677
.filter((n) => {
8778
if (viewMode === "trash") return n.trashed;
8879
if (viewMode === "archive") return n.archived && !n.trashed;
8980
return !n.archived && !n.trashed;
90-
})
91-
.filter((n) => !tagFilter || n.tags.includes(tagFilter));
81+
});
9282
if (!q) return viewFiltered.sort((a, b) => b.updatedAt - a.updatedAt);
9383
const fuse = new Fuse(viewFiltered, {
9484
keys: [
@@ -100,7 +90,7 @@ export function NotesView({
10090
minMatchCharLength: 2,
10191
});
10292
return fuse.search(q).map((r) => r.item);
103-
}, [notes, query, searchOpen, tagFilter, viewMode]);
93+
}, [notes, query, searchOpen, viewMode]);
10494

10595
const visibleNotes = filtered;
10696
const activeNote =
@@ -344,11 +334,8 @@ export function NotesView({
344334
filtered,
345335
activeNoteId,
346336
viewMode,
347-
allTags,
348-
tagFilter,
349-
onTagFilter: setTagFilter,
350337
syncStatus,
351-
onExitFilteredView: () => { setViewMode("active"); setTagFilter(null); },
338+
onExitFilteredView: () => setViewMode("active"),
352339
onOpenNote: openNote,
353340
onNewNote: () => {
354341
setActiveNoteId(null);

components/Sidebar.tsx

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,6 @@ export function Sidebar({
4747
filtered,
4848
activeNoteId,
4949
viewMode,
50-
allTags,
51-
tagFilter,
52-
onTagFilter,
5350
syncStatus,
5451
onExitFilteredView,
5552
onOpenNote,
@@ -66,9 +63,6 @@ export function Sidebar({
6663
filtered: Note[];
6764
activeNoteId: string | null;
6865
viewMode: "active" | "archive" | "trash";
69-
allTags: string[];
70-
tagFilter: string | null;
71-
onTagFilter: (tag: string | null) => void;
7266
syncStatus: SyncStatus;
7367
onExitFilteredView: () => void;
7468
onOpenNote: (note: Note) => void;
@@ -107,25 +101,6 @@ export function Sidebar({
107101
</button>
108102
</div>
109103

110-
{allTags.length > 0 && viewMode === "active" && (
111-
<div className="flex flex-wrap gap-1 px-3 py-2">
112-
{allTags.map((tag) => (
113-
<button
114-
key={tag}
115-
type="button"
116-
onClick={() => onTagFilter(tagFilter === tag ? null : tag)}
117-
className={`rounded-full px-2 py-0.5 text-xs transition-colors ${
118-
tagFilter === tag
119-
? "bg-[var(--color-accent)] text-[var(--color-accent-fg)]"
120-
: "bg-[var(--color-surface-hover)] text-[var(--color-muted)] hover:text-[var(--color-text)]"
121-
}`}
122-
>
123-
{tag}
124-
</button>
125-
))}
126-
</div>
127-
)}
128-
129104
{filteredTitle && (
130105
<div className="flex items-center justify-between px-3 py-2">
131106
<span className="text-xs font-medium uppercase tracking-wide text-[var(--color-muted)]">

lib/inferTitle.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,19 @@ export function inferNoteTitle(body: string, fallback = "Untitled note") {
3434
return fallback;
3535
}
3636

37-
export function searchableText(note: { body: string; title: string; tags?: string[] }) {
38-
const base = note.body.trim() || note.title.trim();
39-
if (note.tags?.length) return base + " " + note.tags.join(" ");
40-
return base;
37+
export function searchableText(note: { body: string; title: string }) {
38+
return note.body.trim() || note.title.trim();
4139
}
4240

43-
export function previewText(note: { body: string; title: string; tags?: string[] }) {
41+
export function previewText(note: { body: string; title: string }) {
4442
const title = needsInferredTitle(note.title, note.body)
4543
? inferNoteTitle(searchableText(note))
4644
: note.title;
4745
const text = title.replace(/\s+/g, " ").trim();
4846
return text || "(empty)";
4947
}
5048

51-
export function needsInferredTitle(title: string, body: string) {
49+
export function needsInferredTitle(title: string, body: string): boolean {
5250
const compactTitle = title.replace(/\s+/g, " ").trim();
5351
const compactBody = body.replace(/\s+/g, " ").trim();
5452
return (

0 commit comments

Comments
 (0)