feat(cards): add 'note' card type for readable markdown content

New card type renders body as styled markdown with no copy/fill/run
affordance. Glyph: ¶, color: --note.

Migration uses transaction to safely rebuild table constraint.
Checks both 'note' presence and modified_at column to catch
partial migration state.
This commit is contained in:
2026-05-17 12:49:43 -04:00
parent 840084fbb0
commit 2b177eeae9
7 changed files with 69 additions and 5 deletions
+55 -1
View File
@@ -3,8 +3,10 @@ package db
import (
"database/sql"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
_ "modernc.org/sqlite"
)
@@ -63,7 +65,7 @@ func (s *Store) migrate() error {
pinned INTEGER NOT NULL DEFAULT 0,
deleted_at TEXT,
card_type TEXT
CHECK (card_type IN ('snippet', 'template', 'checklist', 'decision', 'link')
CHECK (card_type IN ('snippet', 'template', 'checklist', 'decision', 'link', 'note')
OR card_type IS NULL),
card_data TEXT,
use_count INTEGER NOT NULL DEFAULT 0,
@@ -91,6 +93,58 @@ func (s *Store) migrate() error {
s.db.Exec(`ALTER TABLE entities ADD COLUMN title TEXT`)
s.db.Exec(`ALTER TABLE entities ADD COLUMN description TEXT`)
// Migrate CHECK constraint to include 'note' card type
var needsMigrate bool
row := s.db.QueryRow(`SELECT sql FROM sqlite_master WHERE type='table' AND name='entities'`)
var ddl string
if row.Scan(&ddl) == nil {
hasNote := strings.Contains(ddl, "'link', 'note'")
hasModified := strings.Contains(ddl, "modified_at")
needsMigrate = !hasNote || !hasModified
}
if needsMigrate {
tx, err := s.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec(`ALTER TABLE entities RENAME TO _entities_migrate`); err != nil {
return fmt.Errorf("migrate rename: %w", err)
}
if _, err := tx.Exec(`CREATE TABLE entities (
id TEXT PRIMARY KEY,
created_at TEXT NOT NULL,
modified_at TEXT NOT NULL,
body TEXT NOT NULL,
glyph TEXT NOT NULL
CHECK (glyph IN ('todo', 'event', 'note')),
time_anchor TEXT,
completed_at TEXT,
pinned INTEGER NOT NULL DEFAULT 0,
deleted_at TEXT,
card_type TEXT
CHECK (card_type IN ('snippet', 'template', 'checklist', 'decision', 'link', 'note')
OR card_type IS NULL),
card_data TEXT,
use_count INTEGER NOT NULL DEFAULT 0,
last_used_at TEXT,
title TEXT,
description TEXT
)`); err != nil {
return fmt.Errorf("migrate create: %w", err)
}
if _, err := tx.Exec(`INSERT INTO entities SELECT * FROM _entities_migrate`); err != nil {
return fmt.Errorf("migrate copy: %w", err)
}
if _, err := tx.Exec(`DROP TABLE _entities_migrate`); err != nil {
return fmt.Errorf("migrate drop: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("migrate commit: %w", err)
}
}
return nil
}
+2 -1
View File
@@ -27,6 +27,7 @@ const (
CardChecklist CardType = "checklist"
CardDecision CardType = "decision"
CardLink CardType = "link"
CardNote CardType = "note"
)
func ValidGlyph(s string) bool {
@@ -39,7 +40,7 @@ func ValidGlyph(s string) bool {
func ValidCardType(s string) bool {
switch CardType(s) {
case CardSnippet, CardTemplate, CardChecklist, CardDecision, CardLink:
case CardSnippet, CardTemplate, CardChecklist, CardDecision, CardLink, CardNote:
return true
}
return false
+1
View File
@@ -15,6 +15,7 @@ var cardGlyphMap = map[db.CardType]string{
db.CardChecklist: "☐",
db.CardDecision: "⚖",
db.CardLink: "↗",
db.CardNote: "¶",
}
func DisplayGlyph(glyph db.Glyph, cardType *db.CardType) string {
+2
View File
@@ -27,6 +27,8 @@ var validCardTypes = map[string]string{
"checklist": "checklist",
"decision": "decision",
"link": "link",
"note": "note",
"n": "note",
}
func Parse(input string) (*Result, error) {