diff --git a/internal/db/db.go b/internal/db/db.go index 15530d8..fc389ea 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -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 } diff --git a/internal/db/entities.go b/internal/db/entities.go index 4669416..c257d7a 100644 --- a/internal/db/entities.go +++ b/internal/db/entities.go @@ -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 diff --git a/internal/display/glyph.go b/internal/display/glyph.go index 9ba2bfc..e3cb21d 100644 --- a/internal/display/glyph.go +++ b/internal/display/glyph.go @@ -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 { diff --git a/internal/parse/grammar.go b/internal/parse/grammar.go index ee30ea0..0556320 100644 --- a/internal/parse/grammar.go +++ b/internal/parse/grammar.go @@ -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) { diff --git a/web/app.js b/web/app.js index 5d2904e..adbe789 100644 --- a/web/app.js +++ b/web/app.js @@ -4,14 +4,14 @@ const GLYPHS = { note: '—', todo: '○', event: '◇', reminder: '△', snippet: '◆', template: '◈', checklist: '☐', - decision: '⚖', link: '↗', + decision: '⚖', link: '↗', note: '¶', }; const GLYPH_CLASSES = { note: 'glyph-note', todo: 'glyph-todo', event: 'glyph-event', reminder: 'glyph-reminder', snippet: 'glyph-snippet', template: 'glyph-template', checklist: 'glyph-checklist', decision: 'glyph-decision', - link: 'glyph-link', + link: 'glyph-link', note: 'glyph-note', }; const PAGE_SIZE = 50; @@ -121,7 +121,7 @@ // ========== 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) { const parts = s.split(':'); @@ -664,12 +664,65 @@ actions += ``; } if (e.card_type) { + actions += ``; actions += ``; } else { actions += ``; } + + let content = ''; + if (e.card_type) { + const data = e.card_data ? (() => { try { return JSON.parse(e.card_data); } catch { return {}; } })() : {}; + const hasDecision = data.chose != null; + const hasSteps = data.steps && data.steps.length; + const hasLink = !!data.url; + const hasFill = /\$\{[^}]+\}/.test(e.body || ''); + + if (hasDecision) { + const rejected = (data.rejected || []).map(r => `${escHtml(r)}`).join(''); + content += `
+
decision${data.status || 'decided'}
+
+
${escHtml(data.chose)}
+
why${escHtml(data.why || '')}
+ ${rejected ? `
considered
${rejected}
` : ''} +
+
`; + } + if (hasLink && !hasDecision) { + content += `
+
link
+
+
`; + } + if (hasSteps) { + const steps = data.steps.map(s => `
${escHtml(s.text || s)}
`).join(''); + content += `
+
steps · ${data.steps.length}
+
${steps}
+
`; + actions += ``; + } + if (hasFill) { + actions += ``; + } + if (!hasDecision && e.body) { + const lang = data.lang || ''; + const isCode = lang || e.card_type === 'snippet'; + const bodyHtml = isCode + ? `
${escHtml(e.body)}
` + : `
${renderMd(e.body)}
`; + content += `
+
content${lang ? `${lang}` : ''}
+
${bodyHtml}
+
`; + } + } else { + content = `
${renderMd(e.body || '')}
`; + } + return `
-
${renderMd(e.body || '')}
+ ${content} ${tags ? `
${tags}
` : ''}
${actions}
@@ -1317,9 +1370,13 @@ }, async deleteEntity(id) { + const prevIdx = state.selectedIndex; await api.deleteEntity(id); await loadEntities(); await loadTags(); + if (state.entities.length > 0) { + selectEntity(Math.min(prevIdx, state.entities.length - 1)); + } showToast('deleted'); }, diff --git a/web/index.html b/web/index.html index f54638d..cfa9da4 100644 --- a/web/index.html +++ b/web/index.html @@ -44,31 +44,45 @@

promote to card

- - - - - +
+
read
+ + + +
+
+
grab
+ +
+
+
fill
+ + +
diff --git a/web/style.css b/web/style.css index 07fb835..3a07d22 100644 --- a/web/style.css +++ b/web/style.css @@ -414,7 +414,8 @@ main.focus-peek .resize-handle { visibility: hidden; } overflow: hidden; } -.entity-item.exp-full .exp-body { +.entity-item.exp-full .exp-body, +.card-row.exp-full .exp-body { -webkit-line-clamp: unset; overflow: visible; } @@ -461,6 +462,7 @@ main.focus-peek .resize-handle { visibility: hidden; } .glyph-checklist { color: var(--remind); } .glyph-decision { color: var(--note); } .glyph-link { color: var(--event); } +.glyph-note { color: var(--note); } .entity-content { flex: 1; @@ -961,7 +963,8 @@ kbd { background: var(--raised); border: 1px solid var(--border); border-radius: .peek-body:hover { background: var(--raised); } -.peek-body.md { +.peek-body.md, +.exp-body.md { font-family: var(--sans); font-size: 13px; line-height: 1.65; @@ -969,35 +972,40 @@ kbd { background: var(--raised); border: 1px solid var(--border); border-radius: } .peek-body.md h1, .peek-body.md h2, .peek-body.md h3, -.peek-body.md h4, .peek-body.md h5, .peek-body.md h6 { +.peek-body.md h4, .peek-body.md h5, .peek-body.md h6, +.exp-body.md h1, .exp-body.md h2, .exp-body.md h3, +.exp-body.md h4, .exp-body.md h5, .exp-body.md h6 { font-weight: 600; color: var(--text); margin: 14px 0 6px; line-height: 1.3; } -.peek-body.md h1 { font-size: 17px; } -.peek-body.md h2 { font-size: 15px; } -.peek-body.md h3 { font-size: 14px; } +.peek-body.md h1, .exp-body.md h1 { font-size: 17px; } +.peek-body.md h2, .exp-body.md h2 { font-size: 15px; } +.peek-body.md h3, .exp-body.md h3 { font-size: 14px; } -.peek-body.md p { margin: 0 0 10px; } -.peek-body.md p:last-child { margin-bottom: 0; } +.peek-body.md p, .exp-body.md p { margin: 0 0 10px; } +.peek-body.md p:last-child, .exp-body.md p:last-child { margin-bottom: 0; } -.peek-body.md ul, .peek-body.md ol { +.peek-body.md ul, .peek-body.md ol, +.exp-body.md ul, .exp-body.md ol { padding-left: 20px; margin: 0 0 10px; } -.peek-body.md li { margin-bottom: 3px; } +.peek-body.md li, .exp-body.md li { margin-bottom: 3px; } -.peek-body.md blockquote { +.peek-body.md blockquote, +.exp-body.md blockquote { border-left: 2px solid var(--accent); padding-left: 12px; color: var(--muted); margin: 0 0 10px; } -.peek-body.md code { +.peek-body.md code, +.exp-body.md code { font-family: var(--mono); font-size: 11px; background: var(--bg); @@ -1006,7 +1014,8 @@ kbd { background: var(--raised); border: 1px solid var(--border); border-radius: padding: 1px 5px; } -.peek-body.md pre { +.peek-body.md pre, +.exp-body.md pre { background: var(--bg); border: 1px solid var(--border); border-radius: var(--r2); @@ -1015,7 +1024,8 @@ kbd { background: var(--raised); border: 1px solid var(--border); border-radius: margin: 0 0 10px; } -.peek-body.md pre code { +.peek-body.md pre code, +.exp-body.md pre code { background: none; border: none; padding: 0; @@ -1023,18 +1033,20 @@ kbd { background: var(--raised); border: 1px solid var(--border); border-radius: line-height: 1.6; } -.peek-body.md a { +.peek-body.md a, +.exp-body.md a { color: var(--event); text-decoration: none; border-bottom: 1px solid rgba(104,152,200,.3); } -.peek-body.md a:hover { border-bottom-color: var(--event); } +.peek-body.md a:hover, .exp-body.md a:hover { border-bottom-color: var(--event); } -.peek-body.md strong { font-weight: 600; } -.peek-body.md em { font-style: italic; color: var(--muted); } +.peek-body.md strong, .exp-body.md strong { font-weight: 600; } +.peek-body.md em, .exp-body.md em { font-style: italic; color: var(--muted); } -.peek-body.md hr { +.peek-body.md hr, +.exp-body.md hr { border: none; border-top: 1px solid var(--border); margin: 14px 0; @@ -1368,7 +1380,8 @@ kbd { background: var(--raised); border: 1px solid var(--border); border-radius: border-radius: var(--r3); padding: 22px; z-index: 101; - min-width: 300px; + min-width: 380px; + max-width: 90vw; box-shadow: 0 20px 60px rgba(0,0,0,.5); } @@ -1391,16 +1404,32 @@ kbd { background: var(--raised); border: 1px solid var(--border); border-radius: } .type-picker { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 10px; +} + +.type-col { display: flex; flex-direction: column; gap: 4px; } +.type-col-lbl { + font-family: var(--mono); + font-size: 9px; + text-transform: uppercase; + letter-spacing: .14em; + color: var(--dim); + padding: 0 4px 4px; +} + .type-btn { display: flex; - align-items: center; - gap: 10px; - padding: 8px 12px; + flex-direction: column; + align-items: flex-start; + gap: 3px; + padding: 8px 10px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--r2); @@ -1414,8 +1443,8 @@ kbd { background: var(--raised); border: 1px solid var(--border); border-radius: .type-btn:hover { border-color: var(--accent); background: var(--raised); } .type-btn.suggested { border-color: var(--accent); background: var(--a-bg); } -.type-glyph { font-size: 13px; width: 16px; flex-shrink: 0; } -.type-name { font-family: var(--mono); font-size: 12px; color: var(--text); min-width: 72px; } +.type-glyph { font-size: 13px; flex-shrink: 0; } +.type-name { font-family: var(--mono); font-size: 11px; color: var(--text); } .type-hint { font-family: var(--sans); @@ -1516,6 +1545,7 @@ kbd { background: var(--raised); border: 1px solid var(--border); border-radius: #tag-rail { display: none !important; } .resize-handle { display: none !important; } #entity-panel { overflow: auto; } + #capture-bar { position: sticky; bottom: 0; z-index: 10; } #detail-pane { position: fixed; inset: 0;