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 ? `
` : ''}
+
+
`;
+ }
+ if (hasLink && !hasDecision) {
+ content += ``;
+ }
+ if (hasSteps) {
+ const steps = data.steps.map(s => `○${escHtml(s.text || s)}
`).join('');
+ content += `
+
steps · ${data.steps.length}
+
+
`;
+ actions += ``;
+ }
+ if (hasFill) {
+ actions += ``;
+ }
+ if (!hasDecision && e.body) {
+ const lang = data.lang || '';
+ const isCode = lang || e.card_type === 'snippet';
+ const bodyHtml = isCode
+ ? ``
+ : `${renderMd(e.body)}
`;
+ content += `
+
content${lang ? `${lang}` : ''}
+
${bodyHtml}
+
`;
+ }
+ } else {
+ content = `${renderMd(e.body || '')}
`;
+ }
+
return `
-
${renderMd(e.body || '')}
+ ${content}
${tags ? `
${tags}
` : ''}
${actions}
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;