feat: Notion-like rich text editor with TipTap, 4 note types, slash commands & bubble menu
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m33s

This commit is contained in:
2026-05-01 01:11:03 +02:00
parent 7053e242d2
commit 1345403a31
31 changed files with 3222 additions and 150 deletions

View File

@@ -135,6 +135,16 @@ export interface Translations {
viewTabsTooltip: string
viewModeGroup: string
reorderTabs: string
noteType: string
typeText: string
typeMarkdown: string
typeRichText: string
typeChecklist: string
richTextPlaceholder: string
switchTypeTitle: string
switchTypeWarning: string
switchTypeContentPreserved: string
switchType: string
}
pagination: {
previous: string

View File

@@ -44,6 +44,15 @@ export interface LinkMetadata {
siteName?: string;
}
export type NoteType = 'text' | 'markdown' | 'richtext' | 'checklist'
export const NOTE_TYPE_CONFIG: Record<NoteType, { icon: string; label: string }> = {
text: { icon: 'AlignLeft', label: 'Text' },
markdown: { icon: 'FileCode2', label: 'Markdown' },
richtext: { icon: 'PenLine', label: 'Rich Text' },
checklist: { icon: 'ListChecks', label: 'Checklist' },
}
export interface Note {
id: string;
title: string | null;
@@ -52,9 +61,9 @@ export interface Note {
isPinned: boolean;
isArchived: boolean;
trashedAt?: Date | null;
type: 'text' | 'checklist';
type: NoteType;
checkItems: CheckItem[] | null;
labels: string[] | null; // DEPRECATED: Array of label names stored as JSON string
labels: string[] | null;
images: string[] | null;
links: LinkMetadata[] | null;
reminder: Date | null;
@@ -70,13 +79,11 @@ export interface Note {
contentUpdatedAt: Date;
sharedWith?: string[];
userId?: string | null;
// Notebook relation (optional - null = "General Notes" / Inbox)
notebookId?: string | null;
notebook?: Notebook | null;
autoGenerated?: boolean | null;
aiProvider?: string | null;
historyEnabled?: boolean;
// Search result metadata (optional)
matchType?: 'exact' | 'related' | null;
searchScore?: number | null;
}
@@ -92,8 +99,7 @@ export interface NoteHistoryEntry {
color: string;
isPinned: boolean;
isArchived: boolean;
type: 'text' | 'checklist';
checkItems: CheckItem[] | null;
type: NoteType; checkItems: CheckItem[] | null;
labels: string[] | null;
images: string[] | null;
links: LinkMetadata[] | null;

View File

@@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import { LABEL_COLORS, LabelColorName, QueryType, Note } from "./types"
import { LABEL_COLORS, LabelColorName, QueryType, Note, NoteType } from "./types"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
@@ -50,8 +50,14 @@ export function asArray<T = unknown>(val: unknown, fallback: T[] = []): T[] {
* Guarantees array fields are always real arrays or null.
*/
export function parseNote(dbNote: any): Note {
let noteType: NoteType = dbNote.type || 'text'
if (noteType === 'text' && dbNote.isMarkdown) {
noteType = 'markdown'
}
return {
...dbNote,
type: noteType,
isMarkdown: noteType === 'markdown',
checkItems: asArray(dbNote.checkItems, null as any) ?? null,
labels: asArray(dbNote.labels) || null,
images: asArray(dbNote.images) || null,