fix: UI issues #23-25 + note card type + promote modal #26
+55
-1
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+61
-4
@@ -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 += `<button class="action-btn primary" onclick="event.stopPropagation();nibApp.showPromote('${e.id}')">promote</button>`;
|
||||
}
|
||||
if (e.card_type) {
|
||||
actions += `<button class="action-btn primary" onclick="event.stopPropagation();nibApp.copyEntity('${e.id}')">copy</button>`;
|
||||
actions += `<button class="action-btn danger" onclick="event.stopPropagation();nibApp.demoteEntity('${e.id}')">demote</button>`;
|
||||
} else {
|
||||
actions += `<button class="action-btn danger" onclick="event.stopPropagation();nibApp.deleteEntity('${e.id}')">delete</button>`;
|
||||
}
|
||||
|
||||
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 => `<span class="peek-dec-rej">${escHtml(r)}</span>`).join('');
|
||||
content += `<div class="peek-sec">
|
||||
<div class="peek-sec-lbl">decision<span class="peek-sec-status">${data.status || 'decided'}</span></div>
|
||||
<div class="peek-sec-inner peek-decision">
|
||||
<div class="peek-dec-choice">${escHtml(data.chose)}</div>
|
||||
<div class="peek-dec-why"><span class="peek-dec-key">why</span>${escHtml(data.why || '')}</div>
|
||||
${rejected ? `<div><span class="peek-dec-key">considered</span><div class="peek-dec-rejected">${rejected}</div></div>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
if (hasLink && !hasDecision) {
|
||||
content += `<div class="peek-sec">
|
||||
<div class="peek-sec-lbl">link</div>
|
||||
<div class="peek-sec-inner"><div class="peek-link-url">↗ ${escHtml(data.url)}</div></div>
|
||||
</div>`;
|
||||
}
|
||||
if (hasSteps) {
|
||||
const steps = data.steps.map(s => `<div class="peek-step"><span class="peek-step-mark" style="color:var(--dim)">○</span><span class="peek-step-text">${escHtml(s.text || s)}</span></div>`).join('');
|
||||
content += `<div class="peek-sec">
|
||||
<div class="peek-sec-lbl">steps · ${data.steps.length}<button class="peek-sec-run" onclick="event.stopPropagation();nibApp.enterMode('run')">▶ run</button></div>
|
||||
<div class="peek-sec-inner"><div class="peek-steps">${steps}</div></div>
|
||||
</div>`;
|
||||
actions += `<button class="action-btn" onclick="event.stopPropagation();nibApp.enterMode('run')">run</button>`;
|
||||
}
|
||||
if (hasFill) {
|
||||
actions += `<button class="action-btn" onclick="event.stopPropagation();nibApp.enterMode('fill')">fill</button>`;
|
||||
}
|
||||
if (!hasDecision && e.body) {
|
||||
const lang = data.lang || '';
|
||||
const isCode = lang || e.card_type === 'snippet';
|
||||
const bodyHtml = isCode
|
||||
? `<div class="peek-code"><pre><code>${escHtml(e.body)}</code></pre></div>`
|
||||
: `<div class="exp-body md">${renderMd(e.body)}</div>`;
|
||||
content += `<div class="peek-sec">
|
||||
<div class="peek-sec-lbl">content${lang ? `<span class="peek-sec-lang">${lang}</span>` : ''}</div>
|
||||
<div class="peek-sec-inner">${bodyHtml}</div>
|
||||
</div>`;
|
||||
}
|
||||
} else {
|
||||
content = `<div class="exp-body md">${renderMd(e.body || '')}</div>`;
|
||||
}
|
||||
|
||||
return `<div class="exp-inner">
|
||||
<div class="exp-body md">${renderMd(e.body || '')}</div>
|
||||
${content}
|
||||
${tags ? `<div class="exp-tags">${tags}</div>` : ''}
|
||||
<div class="exp-acts">${actions}</div>
|
||||
<div class="exp-toolbar">
|
||||
@@ -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');
|
||||
},
|
||||
|
||||
|
||||
+33
-19
@@ -44,31 +44,45 @@
|
||||
<h3>promote to card</h3>
|
||||
<div class="modal-sub" id="promote-sub"></div>
|
||||
<div class="type-picker">
|
||||
<button data-type="snippet" class="type-btn">
|
||||
<span class="type-glyph glyph-snippet">◆</span>
|
||||
<span class="type-name">snippet</span>
|
||||
<span class="type-hint">quick reference, command, code</span>
|
||||
</button>
|
||||
<button data-type="template" class="type-btn">
|
||||
<span class="type-glyph glyph-template">◈</span>
|
||||
<span class="type-name">template</span>
|
||||
<span class="type-hint">fillable with ${slot}s</span>
|
||||
</button>
|
||||
<button data-type="checklist" class="type-btn">
|
||||
<span class="type-glyph glyph-checklist">☐</span>
|
||||
<span class="type-name">checklist</span>
|
||||
<span class="type-hint">step-by-step process</span>
|
||||
</button>
|
||||
<button data-type="decision" class="type-btn">
|
||||
<span class="type-glyph glyph-decision">⚖</span>
|
||||
<span class="type-name">decision</span>
|
||||
<span class="type-hint">record a choice + rationale</span>
|
||||
<div class="type-col">
|
||||
<div class="type-col-lbl">read</div>
|
||||
<button data-type="note" class="type-btn">
|
||||
<span class="type-glyph glyph-note">¶</span>
|
||||
<span class="type-name">note</span>
|
||||
<span class="type-hint">markdown content</span>
|
||||
</button>
|
||||
<button data-type="link" class="type-btn">
|
||||
<span class="type-glyph glyph-link">↗</span>
|
||||
<span class="type-name">link</span>
|
||||
<span class="type-hint">reference URL</span>
|
||||
</button>
|
||||
<button data-type="decision" class="type-btn">
|
||||
<span class="type-glyph glyph-decision">⚖</span>
|
||||
<span class="type-name">decision</span>
|
||||
<span class="type-hint">choice + rationale</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="type-col">
|
||||
<div class="type-col-lbl">grab</div>
|
||||
<button data-type="snippet" class="type-btn">
|
||||
<span class="type-glyph glyph-snippet">◆</span>
|
||||
<span class="type-name">snippet</span>
|
||||
<span class="type-hint">code, command, text</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="type-col">
|
||||
<div class="type-col-lbl">fill</div>
|
||||
<button data-type="template" class="type-btn">
|
||||
<span class="type-glyph glyph-template">◈</span>
|
||||
<span class="type-name">template</span>
|
||||
<span class="type-hint">fillable ${slot}s</span>
|
||||
</button>
|
||||
<button data-type="checklist" class="type-btn">
|
||||
<span class="type-glyph glyph-checklist">☐</span>
|
||||
<span class="type-name">checklist</span>
|
||||
<span class="type-hint">step-by-step</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close">esc to cancel</button>
|
||||
</div>
|
||||
|
||||
+55
-25
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user