Skip to content

Commit e1d5f35

Browse files
committed
feat: add signifiers to notes
1 parent ce99615 commit e1d5f35

File tree

18 files changed

+291
-28
lines changed

18 files changed

+291
-28
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
1515
- Meeting Notes (Attendees / Discussion / Action Items) with auto-tag `#meeting`
1616
- Learning Log (What I Learned / Source / How to Apply) with auto-tag `#learning`
1717
- Saved filters — save the current tag + text filter combination as a named quick-access filter in the sidebar
18-
- Pin entries — star icon on entry cards to pin important entries; pinned entries float to the top in a dedicated "Pinned" section above day groups
18+
- Pin entries — pin icon on entry cards to keep important entries at the top in a dedicated "Pinned" section above day groups
19+
- Entry signifiers — optional colored label (Note, Decision, Task, Question, Idea) on each entry; click the dot in the header to pick
1920

2021
## [2.2.3] - 2026-02-15
2122

src/app/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default function App() {
2626
function AppContent() {
2727
const isMobile = useIsMobile();
2828
const { loading } = useEntriesData();
29-
const { createEntry, updateEntry, updateEntryTags, archiveEntry, unarchiveEntry, deleteEntry, pinEntry, unpinEntry } = useEntriesActions();
29+
const { createEntry, updateEntry, updateEntryTags, archiveEntry, unarchiveEntry, deleteEntry, pinEntry, unpinEntry, updateEntrySignifier } = useEntriesActions();
3030
const { isOpen: sidebarOpen, archiveView } = useSidebarUI();
3131
const { textQuery, selectedTags, hasActiveFilters, removeTag, clearAllFilters, saveCurrentFilter, savedFilters } = useSidebarFilter();
3232
const { displayEntriesByDay, displayArchivedEntriesByDay } = useSidebarData();
@@ -80,6 +80,7 @@ function AppContent() {
8080
savedFilters={!archiveView ? savedFilters : undefined}
8181
onPin={archiveView ? undefined : pinEntry}
8282
onUnpin={archiveView ? undefined : unpinEntry}
83+
onSignifierChange={archiveView ? undefined : updateEntrySignifier}
8384
onOpenAI={aiSettings.enabled && aiAvailable && !isMobile ? handleOpenAI : undefined}
8485
focusedEntryId={focusedEntryId}
8586
onFocusEntry={handleFocusEntry}

src/features/entries/components/EntryCard.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { TagEditor } from "./TagEditor.tsx";
66
import { ConfirmAction } from "../../../components/ui/ConfirmAction.tsx";
77
import { ErrorBoundary } from "../../../components/ui/ErrorBoundary.tsx";
88
import { CheckIcon, ArchiveIcon, TrashIcon, AIIcon } from "../../../components/ui/Icons.tsx";
9+
import { SignifierPicker } from "./SignifierPicker.tsx";
910

1011
interface EntryCardProps {
1112
entry: WorkLedgerEntry;
@@ -17,12 +18,13 @@ interface EntryCardProps {
1718
onUnarchive?: (id: string) => void;
1819
onPin?: (id: string) => void;
1920
onUnpin?: (id: string) => void;
21+
onSignifierChange?: (id: string, signifier: string | undefined) => void;
2022
isArchiveView?: boolean;
2123
onOpenAI?: (entry: WorkLedgerEntry) => void;
2224
onFocus?: (entry: WorkLedgerEntry) => void;
2325
}
2426

25-
export const EntryCard = memo(function EntryCard({ entry, isLatest, onSave, onTagsChange, onArchive, onDelete, onUnarchive, onPin, onUnpin, isArchiveView, onOpenAI, onFocus }: EntryCardProps) {
27+
export const EntryCard = memo(function EntryCard({ entry, isLatest, onSave, onTagsChange, onArchive, onDelete, onUnarchive, onPin, onUnpin, onSignifierChange, isArchiveView, onOpenAI, onFocus }: EntryCardProps) {
2628
const isOld = entry.dayKey < todayKey();
2729
const [confirmArchive, setConfirmArchive] = useState(false);
2830
const [confirmDelete, setConfirmDelete] = useState(false);
@@ -52,6 +54,19 @@ export const EntryCard = memo(function EntryCard({ entry, isLatest, onSave, onTa
5254
archived
5355
</span>
5456
)}
57+
{onSignifierChange && !isArchiveView && (
58+
<SignifierPicker
59+
value={entry.signifier}
60+
onChange={(s) => onSignifierChange(entry.id, s)}
61+
/>
62+
)}
63+
{isArchiveView && entry.signifier && (
64+
<span className={`text-[11px] font-medium ${
65+
{ note: "text-blue-500", decision: "text-emerald-500", task: "text-violet-500", question: "text-amber-500", idea: "text-pink-500" }[entry.signifier]
66+
}`}>
67+
{entry.signifier}
68+
</span>
69+
)}
5570

5671
{/* Action buttons */}
5772
<div className="ml-auto flex items-center gap-1">

src/features/entries/components/EntryStream.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ interface EntryStreamProps {
2323
savedFilters?: SavedFilter[];
2424
onPin?: (id: string) => void;
2525
onUnpin?: (id: string) => void;
26+
onSignifierChange?: (id: string, signifier: string | undefined) => void;
2627
onOpenAI?: (entry: WorkLedgerEntry) => void;
2728
focusedEntryId?: string | null;
2829
onFocusEntry?: (entry: WorkLedgerEntry) => void;
2930
onExitFocus?: () => void;
3031
}
3132

32-
export function EntryStream({ entriesByDay, onSave, onTagsChange, onArchive, onDelete, onUnarchive, isArchiveView, textQuery, selectedTags, hasActiveFilters, onRemoveTag, onClearAllFilters, onSaveFilter, savedFilters, onPin, onUnpin, onOpenAI, focusedEntryId, onFocusEntry, onExitFocus }: EntryStreamProps) {
33+
export function EntryStream({ entriesByDay, onSave, onTagsChange, onArchive, onDelete, onUnarchive, isArchiveView, textQuery, selectedTags, hasActiveFilters, onRemoveTag, onClearAllFilters, onSaveFilter, savedFilters, onPin, onUnpin, onSignifierChange, onOpenAI, focusedEntryId, onFocusEntry, onExitFocus }: EntryStreamProps) {
3334
// Focus mode: render only the focused entry
3435
if (focusedEntryId) {
3536
let focusedEntry: WorkLedgerEntry | undefined;
@@ -79,6 +80,7 @@ export function EntryStream({ entriesByDay, onSave, onTagsChange, onArchive, onD
7980
onUnarchive={isArchiveView ? onUnarchive : undefined}
8081
onPin={isArchiveView ? undefined : onPin}
8182
onUnpin={isArchiveView ? undefined : onUnpin}
83+
onSignifierChange={isArchiveView ? undefined : onSignifierChange}
8284
isArchiveView={isArchiveView}
8385
onOpenAI={isArchiveView ? undefined : onOpenAI}
8486
/>
@@ -159,6 +161,7 @@ export function EntryStream({ entriesByDay, onSave, onTagsChange, onArchive, onD
159161
onDelete={onDelete}
160162
onPin={onPin}
161163
onUnpin={onUnpin}
164+
onSignifierChange={onSignifierChange}
162165
onOpenAI={onOpenAI}
163166
onFocus={onFocusEntry}
164167
/>
@@ -188,6 +191,7 @@ export function EntryStream({ entriesByDay, onSave, onTagsChange, onArchive, onD
188191
onUnarchive={isArchiveView ? onUnarchive : undefined}
189192
onPin={isArchiveView ? undefined : onPin}
190193
onUnpin={isArchiveView ? undefined : onUnpin}
194+
onSignifierChange={isArchiveView ? undefined : onSignifierChange}
191195
isArchiveView={isArchiveView}
192196
onOpenAI={isArchiveView ? undefined : onOpenAI}
193197
onFocus={isArchiveView ? undefined : onFocusEntry}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { useState, useRef, useEffect } from "react";
2+
import { SIGNIFIER_CONFIG, type EntrySignifier } from "../types/entry.ts";
3+
4+
interface SignifierPickerProps {
5+
value: EntrySignifier | undefined;
6+
onChange: (signifier: EntrySignifier | undefined) => void;
7+
}
8+
9+
const SIGNIFIERS = Object.entries(SIGNIFIER_CONFIG) as [EntrySignifier, typeof SIGNIFIER_CONFIG[EntrySignifier]][];
10+
11+
export function SignifierPicker({ value, onChange }: SignifierPickerProps) {
12+
const [open, setOpen] = useState(false);
13+
const ref = useRef<HTMLDivElement>(null);
14+
15+
useEffect(() => {
16+
if (!open) return;
17+
function handleClick(e: MouseEvent) {
18+
if (ref.current && !ref.current.contains(e.target as Node)) {
19+
setOpen(false);
20+
}
21+
}
22+
document.addEventListener("mousedown", handleClick);
23+
return () => document.removeEventListener("mousedown", handleClick);
24+
}, [open]);
25+
26+
const config = value ? SIGNIFIER_CONFIG[value] : null;
27+
28+
return (
29+
<div className="relative" ref={ref}>
30+
<button
31+
onClick={() => setOpen((p) => !p)}
32+
className={`flex items-center gap-1 px-1.5 py-0.5 rounded-md text-[11px] transition-colors ${
33+
config
34+
? `${config.color} hover:bg-gray-100 dark:hover:bg-gray-800`
35+
: "text-gray-300 dark:text-gray-600 hover:text-gray-400 dark:hover:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800"
36+
}`}
37+
title={config ? `Signifier: ${config.label}` : "Add signifier"}
38+
aria-label={config ? `Signifier: ${config.label}` : "Add signifier"}
39+
>
40+
{config ? (
41+
<>
42+
<span className={`w-2 h-2 rounded-full bg-current`} />
43+
<span className="font-medium">{config.label}</span>
44+
</>
45+
) : (
46+
<span className="w-2 h-2 rounded-full border border-current" />
47+
)}
48+
</button>
49+
50+
{open && (
51+
<div className="absolute left-0 top-full mt-1 bg-[var(--color-notebook-surface)] border border-[var(--color-notebook-border)] rounded-lg shadow-xl py-1 z-50 w-32">
52+
{SIGNIFIERS.map(([key, cfg]) => (
53+
<button
54+
key={key}
55+
onClick={() => { onChange(key === value ? undefined : key); setOpen(false); }}
56+
className={`w-full text-left px-3 py-1.5 text-xs flex items-center gap-2 transition-colors hover:bg-[var(--color-notebook-surface-alt)] ${
57+
key === value ? "font-medium" : ""
58+
}`}
59+
>
60+
<span className={`w-2 h-2 rounded-full ${cfg.color} ${key === value ? "bg-current" : "bg-current opacity-60"}`} />
61+
<span className={cfg.color}>{cfg.label}</span>
62+
</button>
63+
))}
64+
{value && (
65+
<>
66+
<div className="border-t border-[var(--color-notebook-border)] my-1" />
67+
<button
68+
onClick={() => { onChange(undefined); setOpen(false); }}
69+
className="w-full text-left px-3 py-1.5 text-xs text-gray-400 hover:bg-[var(--color-notebook-surface-alt)] transition-colors"
70+
>
71+
Remove signifier
72+
</button>
73+
</>
74+
)}
75+
</div>
76+
)}
77+
</div>
78+
);
79+
}

src/features/entries/context/EntriesContext.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ interface EntriesActions {
1919
deleteEntry: (id: string) => Promise<void>;
2020
pinEntry: (id: string) => Promise<void>;
2121
unpinEntry: (id: string) => Promise<void>;
22+
updateEntrySignifier: (id: string, signifier: string | undefined) => Promise<void>;
2223
refreshArchive: () => Promise<void>;
2324
refresh: () => Promise<void>;
2425
loadEntryById: (entryId: string) => Promise<WorkLedgerEntry | null>;
@@ -40,6 +41,7 @@ export function EntriesProvider({ children }: { children: ReactNode }) {
4041
deleteEntry,
4142
pinEntry,
4243
unpinEntry,
44+
updateEntrySignifier,
4345
archivedEntries,
4446
refreshArchive,
4547
refresh,
@@ -60,11 +62,12 @@ export function EntriesProvider({ children }: { children: ReactNode }) {
6062
deleteEntry,
6163
pinEntry,
6264
unpinEntry,
65+
updateEntrySignifier,
6366
refreshArchive,
6467
refresh,
6568
loadEntryById,
6669
}),
67-
[createEntry, updateEntry, updateEntryTags, archiveEntry, unarchiveEntry, deleteEntry, pinEntry, unpinEntry, refreshArchive, refresh, loadEntryById],
70+
[createEntry, updateEntry, updateEntryTags, archiveEntry, unarchiveEntry, deleteEntry, pinEntry, unpinEntry, updateEntrySignifier, refreshArchive, refresh, loadEntryById],
6871
);
6972

7073
return (

src/features/entries/hooks/useEntries.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
deleteEntry as dbDeleteEntry,
1111
pinEntry as dbPinEntry,
1212
unpinEntry as dbUnpinEntry,
13+
updateEntrySignifier as dbUpdateEntrySignifier,
1314
getArchivedEntries,
1415
getAllDayKeys,
1516
} from "../storage/entries.ts";
@@ -175,6 +176,15 @@ export function useEntries() {
175176
[refresh],
176177
);
177178

179+
const updateEntrySignifier = useCallback(
180+
async (id: string, signifier: string | undefined) => {
181+
await dbUpdateEntrySignifier(id, signifier);
182+
emit("entry-changed", { entryId: id });
183+
await refresh();
184+
},
185+
[refresh],
186+
);
187+
178188
const loadEntryById = useCallback(
179189
async (entryId: string): Promise<WorkLedgerEntry | null> => {
180190
const entry = await dbGetEntry(entryId);
@@ -205,6 +215,7 @@ export function useEntries() {
205215
deleteEntry,
206216
pinEntry,
207217
unpinEntry,
218+
updateEntrySignifier,
208219
archivedEntries,
209220
refreshArchive,
210221
refresh,

src/features/entries/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export { EntryCard } from "./components/EntryCard.tsx";
1111
export { NewEntryButton } from "./components/NewEntryButton.tsx";
1212

1313
// Types
14-
export type { WorkLedgerEntry, SearchIndexEntry } from "./types/entry.ts";
14+
export type { WorkLedgerEntry, SearchIndexEntry, EntrySignifier } from "./types/entry.ts";
15+
export { SIGNIFIER_CONFIG } from "./types/entry.ts";
1516

1617
// Storage operations (for cross-feature use)
1718
export { getEntry, getAllEntries, pinEntry, unpinEntry } from "./storage/entries.ts";

src/features/entries/storage/entries.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,17 @@ export async function pinEntry(id: string): Promise<void> {
111111
}
112112
}
113113

114+
export async function updateEntrySignifier(id: string, signifier: string | undefined): Promise<void> {
115+
const db = await getDB();
116+
const raw = await db.get("entries", id);
117+
if (raw) {
118+
const entry = normalizeEntry(raw as Record<string, unknown>);
119+
entry.signifier = signifier as WorkLedgerEntry["signifier"];
120+
entry.updatedAt = Date.now();
121+
await db.put("entries", entry);
122+
}
123+
}
124+
114125
export async function unpinEntry(id: string): Promise<void> {
115126
const db = await getDB();
116127
const raw = await db.get("entries", id);

src/features/entries/types/entry.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,19 @@ export interface WorkLedgerEntry {
99
isArchived: boolean;
1010
tags: string[];
1111
isPinned?: boolean;
12+
signifier?: EntrySignifier;
1213
}
1314

15+
export type EntrySignifier = "note" | "decision" | "task" | "question" | "idea";
16+
17+
export const SIGNIFIER_CONFIG: Record<EntrySignifier, { label: string; color: string; icon: string }> = {
18+
note: { label: "Note", color: "text-blue-500", icon: "N" },
19+
decision: { label: "Decision", color: "text-emerald-500", icon: "D" },
20+
task: { label: "Task", color: "text-violet-500", icon: "T" },
21+
question: { label: "Question", color: "text-amber-500", icon: "?" },
22+
idea: { label: "Idea", color: "text-pink-500", icon: "!" },
23+
};
24+
1425
export interface SearchIndexEntry {
1526
entryId: string;
1627
dayKey: string;

0 commit comments

Comments
 (0)