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:
+55
-1
@@ -3,8 +3,10 @@ package db
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
@@ -63,7 +65,7 @@ func (s *Store) migrate() error {
|
|||||||
pinned INTEGER NOT NULL DEFAULT 0,
|
pinned INTEGER NOT NULL DEFAULT 0,
|
||||||
deleted_at TEXT,
|
deleted_at TEXT,
|
||||||
card_type 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),
|
OR card_type IS NULL),
|
||||||
card_data TEXT,
|
card_data TEXT,
|
||||||
use_count INTEGER NOT NULL DEFAULT 0,
|
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 title TEXT`)
|
||||||
s.db.Exec(`ALTER TABLE entities ADD COLUMN description 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const (
|
|||||||
CardChecklist CardType = "checklist"
|
CardChecklist CardType = "checklist"
|
||||||
CardDecision CardType = "decision"
|
CardDecision CardType = "decision"
|
||||||
CardLink CardType = "link"
|
CardLink CardType = "link"
|
||||||
|
CardNote CardType = "note"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ValidGlyph(s string) bool {
|
func ValidGlyph(s string) bool {
|
||||||
@@ -39,7 +40,7 @@ func ValidGlyph(s string) bool {
|
|||||||
|
|
||||||
func ValidCardType(s string) bool {
|
func ValidCardType(s string) bool {
|
||||||
switch CardType(s) {
|
switch CardType(s) {
|
||||||
case CardSnippet, CardTemplate, CardChecklist, CardDecision, CardLink:
|
case CardSnippet, CardTemplate, CardChecklist, CardDecision, CardLink, CardNote:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ var cardGlyphMap = map[db.CardType]string{
|
|||||||
db.CardChecklist: "☐",
|
db.CardChecklist: "☐",
|
||||||
db.CardDecision: "⚖",
|
db.CardDecision: "⚖",
|
||||||
db.CardLink: "↗",
|
db.CardLink: "↗",
|
||||||
|
db.CardNote: "¶",
|
||||||
}
|
}
|
||||||
|
|
||||||
func DisplayGlyph(glyph db.Glyph, cardType *db.CardType) string {
|
func DisplayGlyph(glyph db.Glyph, cardType *db.CardType) string {
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ var validCardTypes = map[string]string{
|
|||||||
"checklist": "checklist",
|
"checklist": "checklist",
|
||||||
"decision": "decision",
|
"decision": "decision",
|
||||||
"link": "link",
|
"link": "link",
|
||||||
|
"note": "note",
|
||||||
|
"n": "note",
|
||||||
}
|
}
|
||||||
|
|
||||||
func Parse(input string) (*Result, error) {
|
func Parse(input string) (*Result, error) {
|
||||||
|
|||||||
+3
-3
@@ -4,14 +4,14 @@
|
|||||||
const GLYPHS = {
|
const GLYPHS = {
|
||||||
note: '—', todo: '○', event: '◇', reminder: '△',
|
note: '—', todo: '○', event: '◇', reminder: '△',
|
||||||
snippet: '◆', template: '◈', checklist: '☐',
|
snippet: '◆', template: '◈', checklist: '☐',
|
||||||
decision: '⚖', link: '↗',
|
decision: '⚖', link: '↗', note: '¶',
|
||||||
};
|
};
|
||||||
|
|
||||||
const GLYPH_CLASSES = {
|
const GLYPH_CLASSES = {
|
||||||
note: 'glyph-note', todo: 'glyph-todo', event: 'glyph-event', reminder: 'glyph-reminder',
|
note: 'glyph-note', todo: 'glyph-todo', event: 'glyph-event', reminder: 'glyph-reminder',
|
||||||
snippet: 'glyph-snippet', template: 'glyph-template',
|
snippet: 'glyph-snippet', template: 'glyph-template',
|
||||||
checklist: 'glyph-checklist', decision: 'glyph-decision',
|
checklist: 'glyph-checklist', decision: 'glyph-decision',
|
||||||
link: 'glyph-link',
|
link: 'glyph-link', note: 'glyph-note',
|
||||||
};
|
};
|
||||||
|
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
|
|
||||||
// ========== Grammar parser (mirrors Go parser) ==========
|
// ========== Grammar parser (mirrors Go parser) ==========
|
||||||
|
|
||||||
const VALID_CARDS = { card: 'snippet', c: 'snippet', snippet: 'snippet', template: 'template', checklist: 'checklist', decision: 'decision', link: 'link' };
|
const VALID_CARDS = { card: 'snippet', c: 'snippet', snippet: 'snippet', template: 'template', checklist: 'checklist', decision: 'decision', link: 'link', note: 'note', n: 'note' };
|
||||||
|
|
||||||
function validateTime(s) {
|
function validateTime(s) {
|
||||||
const parts = s.split(':');
|
const parts = s.split(':');
|
||||||
|
|||||||
@@ -64,6 +64,11 @@
|
|||||||
<span class="type-name">decision</span>
|
<span class="type-name">decision</span>
|
||||||
<span class="type-hint">record a choice + rationale</span>
|
<span class="type-hint">record a choice + rationale</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button data-type="note" class="type-btn">
|
||||||
|
<span class="type-glyph glyph-note">¶</span>
|
||||||
|
<span class="type-name">note</span>
|
||||||
|
<span class="type-hint">readable markdown content</span>
|
||||||
|
</button>
|
||||||
<button data-type="link" class="type-btn">
|
<button data-type="link" class="type-btn">
|
||||||
<span class="type-glyph glyph-link">↗</span>
|
<span class="type-glyph glyph-link">↗</span>
|
||||||
<span class="type-name">link</span>
|
<span class="type-name">link</span>
|
||||||
|
|||||||
@@ -462,6 +462,7 @@ main.focus-peek .resize-handle { visibility: hidden; }
|
|||||||
.glyph-checklist { color: var(--remind); }
|
.glyph-checklist { color: var(--remind); }
|
||||||
.glyph-decision { color: var(--note); }
|
.glyph-decision { color: var(--note); }
|
||||||
.glyph-link { color: var(--event); }
|
.glyph-link { color: var(--event); }
|
||||||
|
.glyph-note { color: var(--note); }
|
||||||
|
|
||||||
.entity-content {
|
.entity-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user