feat(tui): add cards view, mode switching, promote picker, and card detail
Stream/cards toggle with 1/2 keys. Cards view with intent filtering (tab cycles grab/read/fill/all), sort cycling (s key), pinned-first ordering, and affordance badges. Promote picker (p key) with card type selection and auto-detection from body content. Detail view renders card_data per type: checklist steps, template slots, decision fields, link URLs. Extracts generateCardData to internal/carddata for reuse across cmd and tui packages.
This commit is contained in:
+99
-1
@@ -54,6 +54,9 @@ func (d detailModel) view(width int) string {
|
||||
|
||||
glyph := display.DisplayGlyph(e.Glyph, e.CardType)
|
||||
header := fmt.Sprintf("%s %s", glyph, display.FormatID(e.ID))
|
||||
if e.CardType != nil {
|
||||
header += " " + affordanceStyle.Render(string(*e.CardType))
|
||||
}
|
||||
b.WriteString(detailHeaderStyle.Render(header))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
@@ -65,6 +68,14 @@ func (d detailModel) view(width int) string {
|
||||
b.WriteString(detailBodyStyle.Render(e.Body))
|
||||
b.WriteString("\n")
|
||||
|
||||
if e.CardType != nil {
|
||||
cardSection := renderCardData(e)
|
||||
if cardSection != "" {
|
||||
b.WriteString("\n")
|
||||
b.WriteString(cardSection)
|
||||
}
|
||||
}
|
||||
|
||||
if len(e.Tags) > 0 {
|
||||
tagParts := make([]string, len(e.Tags))
|
||||
for i, t := range e.Tags {
|
||||
@@ -84,11 +95,14 @@ func (d detailModel) view(width int) string {
|
||||
meta += fmt.Sprintf("\nanchored @%s", *e.TimeAnchor)
|
||||
}
|
||||
if e.Pinned {
|
||||
meta += "\npinned"
|
||||
meta += "\n" + pinnedStyle.Render("pinned")
|
||||
}
|
||||
if e.CardType != nil {
|
||||
meta += fmt.Sprintf("\ncard %s", *e.CardType)
|
||||
}
|
||||
if e.UseCount > 0 {
|
||||
meta += fmt.Sprintf("\nused %d×", e.UseCount)
|
||||
}
|
||||
if e.CompletedAt != nil {
|
||||
meta += fmt.Sprintf("\ndone %s", e.CompletedAt.Format(time.DateTime))
|
||||
}
|
||||
@@ -104,3 +118,87 @@ func (d detailModel) view(width int) string {
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func renderCardData(e *db.Entity) string {
|
||||
if e.CardData == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
data, err := e.CardDataJSON()
|
||||
if err != nil || data == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
|
||||
switch *e.CardType {
|
||||
case db.CardChecklist:
|
||||
steps, ok := data["steps"].([]interface{})
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
done := 0
|
||||
for _, s := range steps {
|
||||
step, ok := s.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
text, _ := step["text"].(string)
|
||||
isDone, _ := step["done"].(bool)
|
||||
if isDone {
|
||||
done++
|
||||
b.WriteString(" " + checkDoneStyle.Render("[✓] "+text) + "\n")
|
||||
} else {
|
||||
b.WriteString(" " + checkPendingStyle.Render("[ ] "+text) + "\n")
|
||||
}
|
||||
}
|
||||
progress := fmt.Sprintf(" %d/%d steps", done, len(steps))
|
||||
b.WriteString(detailLabelStyle.Render(progress))
|
||||
|
||||
case db.CardTemplate:
|
||||
slots, ok := data["slots"].([]interface{})
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
b.WriteString(detailLabelStyle.Render(" slots:") + "\n")
|
||||
for _, s := range slots {
|
||||
slot, ok := s.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name, _ := slot["name"].(string)
|
||||
def, _ := slot["default"].(string)
|
||||
line := " ${" + name + "}"
|
||||
if def != "" {
|
||||
line += " " + detailValueStyle.Render("default: "+def)
|
||||
}
|
||||
b.WriteString(line + "\n")
|
||||
}
|
||||
|
||||
case db.CardDecision:
|
||||
if chose, ok := data["chose"].(string); ok && chose != "" {
|
||||
b.WriteString(" " + detailLabelStyle.Render("chose: ") + detailValueStyle.Render(chose) + "\n")
|
||||
}
|
||||
if why, ok := data["why"].(string); ok && why != "" {
|
||||
b.WriteString(" " + detailLabelStyle.Render("why: ") + detailValueStyle.Render(why) + "\n")
|
||||
}
|
||||
if rejected, ok := data["rejected"].([]interface{}); ok && len(rejected) > 0 {
|
||||
items := make([]string, 0, len(rejected))
|
||||
for _, r := range rejected {
|
||||
if s, ok := r.(string); ok {
|
||||
items = append(items, s)
|
||||
}
|
||||
}
|
||||
if len(items) > 0 {
|
||||
b.WriteString(" " + detailLabelStyle.Render("rejected: ") + detailValueStyle.Render(strings.Join(items, ", ")) + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
case db.CardLink:
|
||||
if url, ok := data["url"].(string); ok && url != "" {
|
||||
b.WriteString(" " + detailLabelStyle.Render("↗ ") + detailValueStyle.Render(url) + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user