···296296 root.AddCommand(patchCmd)
297297298298 pushCmd := &cobra.Command{
299299- Use: "push [note-ids...]",
299299+ Use: "push [note-ids...] [--file files...]",
300300 Short: "Create or update multiple documents on leaflet",
301301 Long: `Batch publish or update multiple local notes to leaflet.pub.
302302···307307This is useful for bulk operations and continuous publishing workflows.
308308309309Examples:
310310- noteleaf pub push 1 2 3 # Publish/update notes 1, 2, and 3
311311- noteleaf pub push 42 99 --draft # Create/update as drafts`,
312312- Args: cobra.MinimumNArgs(1),
310310+ noteleaf pub push 1 2 3 # Publish/update notes 1, 2, and 3
311311+ noteleaf pub push 42 99 --draft # Create/update as drafts
312312+ noteleaf pub push --file article.md # Create note from file and push
313313+ noteleaf pub push --file a.md b.md --draft # Create notes from multiple files
314314+ noteleaf pub push 1 2 --dry-run # Validate without pushing
315315+ noteleaf pub push --file article.md --dry-run # Create note but don't push`,
313316 RunE: func(cmd *cobra.Command, args []string) error {
317317+ isDraft, _ := cmd.Flags().GetBool("draft")
318318+ dryRun, _ := cmd.Flags().GetBool("dry-run")
319319+ files, _ := cmd.Flags().GetStringSlice("file")
320320+321321+ defer c.handler.Close()
322322+323323+ if len(files) > 0 {
324324+ return c.handler.PushFromFiles(cmd.Context(), files, isDraft, dryRun)
325325+ }
326326+327327+ if len(args) == 0 {
328328+ return fmt.Errorf("no note IDs or files provided")
329329+ }
330330+314331 noteIDs := make([]int64, len(args))
315332 for i, arg := range args {
316333 id, err := parseNoteID(arg)
···320337 noteIDs[i] = id
321338 }
322339323323- isDraft, _ := cmd.Flags().GetBool("draft")
324324-325325- defer c.handler.Close()
326326- return c.handler.Push(cmd.Context(), noteIDs, isDraft)
340340+ return c.handler.Push(cmd.Context(), noteIDs, isDraft, dryRun)
327341 },
328342 }
329343 pushCmd.Flags().Bool("draft", false, "Create/update as drafts instead of publishing")
344344+ pushCmd.Flags().Bool("dry-run", false, "Create note records but skip leaflet push")
345345+ pushCmd.Flags().StringSliceP("file", "f", []string{}, "Create notes from markdown files before pushing")
330346 root.AddCommand(pushCmd)
331331-332347 return root
333348}
334349
+33-2
internal/docs/ROADMAP.md
···4455## Core Usability
6677-The foundation across all domains is implemented. Tasks support CRUD operations, projects, tags, contexts, and time tracking. Notes have create, list, read, edit, and remove commands with interactive and static modes. Media queues exist for books, movies, and TV with progress and status management. SQLite persistence is in place with setup, seed, and reset commands. TUIs and colorized output are available.
77+The foundation across all domains is implemented. Tasks support CRUD operations, projects, tags, contexts, and time tracking.
88+Notes have create, list, read, edit, and remove commands with interactive and static modes. Media queues exist for books, movies, and TV with progress and status management. SQLite persistence is in place with setup, seed, and reset commands. TUIs and colorized output are available.
89910## RC
1011···4344#### Publication
44454546- [x] Implement authentication with BlueSky/leaflet (AT Protocol).
4646- - [ ] Add OAuth2
4747+ - [ ] Add [OAuth2](#publications--authentication)
4748- [x] Verify `pub pull` fetches and syncs documents from leaflet.
4849- [x] Confirm `pub list` with status filtering (`all`, `published`, `draft`).
4950- [ ] Test `pub post` creates new documents with draft/preview/validate modes.
···205206- [ ] Enhanced parsing coverage
206207- [ ] Export to multiple formats
207208- [ ] Linking with tasks and notes
209209+210210+### Publications & Authentication
211211+212212+- [ ] OAuth2 authentication for AT Protocol
213213+ - [ ] Client metadata server for publishing application details
214214+ - [ ] DPoP (Demonstrating Proof of Possession) implementation
215215+ - [ ] ES256 JWT generation with unique JTI nonces
216216+ - [ ] Server-issued nonce management with 5-minute rotation
217217+ - [ ] Separate nonce tracking for authorization and resource servers
218218+ - [ ] PAR (Pushed Authorization Requests) flow
219219+ - [ ] PKCE code challenge generation
220220+ - [ ] State token management
221221+ - [ ] Request URI handling
222222+ - [ ] Identity resolution and verification
223223+ - [ ] Bidirectional handle verification
224224+ - [ ] DID resolution from handles
225225+ - [ ] Authorization server discovery via .well-known endpoints
226226+ - [ ] Token lifecycle management
227227+ - [ ] Access token refresh (5-15 min lifetime recommended)
228228+ - [ ] Refresh token rotation (180 day max for confidential clients)
229229+ - [ ] Concurrent request handling to prevent duplicate refreshes
230230+ - [ ] Secure token storage (encrypted at rest)
231231+ - [ ] Local callback server for OAuth redirects
232232+ - [ ] Ephemeral HTTP server on localhost
233233+ - [ ] Browser launch integration
234234+ - [ ] Timeout handling for abandoned flows
235235+ - [ ] Migration path from app passwords to OAuth
236236+ - [ ] Detect existing app password sessions
237237+ - [ ] Prompt users to upgrade authentication
238238+ - [ ] Maintain backward compatibility
208239209240### User Experience
210241
+152-14
internal/handlers/publication.go
···77 "fmt"
88 "os"
99 "path/filepath"
1010+ "strings"
1011 "time"
11121213 "github.com/stormlightlabs/noteleaf/internal/models"
···384385 return nil
385386}
386387388388+// createNoteFromFile creates a note from a markdown file and returns its ID
389389+func (h *PublicationHandler) createNoteFromFile(ctx context.Context, filePath string) (int64, error) {
390390+ if _, err := os.Stat(filePath); os.IsNotExist(err) {
391391+ return 0, fmt.Errorf("file does not exist: %s", filePath)
392392+ }
393393+394394+ content, err := os.ReadFile(filePath)
395395+ if err != nil {
396396+ return 0, fmt.Errorf("failed to read file: %w", err)
397397+ }
398398+399399+ contentStr := string(content)
400400+ if strings.TrimSpace(contentStr) == "" {
401401+ return 0, fmt.Errorf("file is empty: %s", filePath)
402402+ }
403403+404404+ title, noteContent, tags := parseNoteContent(contentStr)
405405+ if title == "" {
406406+ title = strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath))
407407+ }
408408+409409+ note := &models.Note{
410410+ Title: title,
411411+ Content: noteContent,
412412+ Tags: tags,
413413+ FilePath: filePath,
414414+ }
415415+416416+ noteID, err := h.repos.Notes.Create(ctx, note)
417417+ if err != nil {
418418+ return 0, fmt.Errorf("failed to create note: %w", err)
419419+ }
420420+421421+ ui.Infoln("Created note from file: %s", filePath)
422422+ ui.Infoln(" Note: %s (ID: %d)", title, noteID)
423423+ if len(tags) > 0 {
424424+ ui.Infoln(" Tags: %s", strings.Join(tags, ", "))
425425+ }
426426+427427+ return noteID, nil
428428+}
429429+430430+// parseNoteContent extracts title, content, and tags from markdown content
431431+func parseNoteContent(content string) (title, noteContent string, tags []string) {
432432+ lines := strings.Split(content, "\n")
433433+434434+ for _, line := range lines {
435435+ line = strings.TrimSpace(line)
436436+ if after, ok := strings.CutPrefix(line, "# "); ok {
437437+ title = after
438438+ break
439439+ }
440440+ }
441441+442442+ for _, line := range lines {
443443+ line = strings.TrimSpace(line)
444444+ if strings.HasPrefix(line, "<!-- Tags:") && strings.HasSuffix(line, "-->") {
445445+ tagStr := strings.TrimPrefix(line, "<!-- Tags:")
446446+ tagStr = strings.TrimSuffix(tagStr, "-->")
447447+ tagStr = strings.TrimSpace(tagStr)
448448+449449+ if tagStr != "" {
450450+ for tag := range strings.SplitSeq(tagStr, ",") {
451451+ tag = strings.TrimSpace(tag)
452452+ if tag != "" {
453453+ tags = append(tags, tag)
454454+ }
455455+ }
456456+ }
457457+ }
458458+ }
459459+460460+ noteContent = content
461461+462462+ return title, noteContent, tags
463463+}
464464+465465+// PushFromFiles creates notes from files and pushes them to leaflet
466466+func (h *PublicationHandler) PushFromFiles(ctx context.Context, filePaths []string, isDraft bool, dryRun bool) error {
467467+ if len(filePaths) == 0 {
468468+ return fmt.Errorf("no file paths provided")
469469+ }
470470+471471+ ui.Infoln("Creating notes from %d file(s)...\n", len(filePaths))
472472+473473+ noteIDs := make([]int64, 0, len(filePaths))
474474+ var failed int
475475+476476+ for _, filePath := range filePaths {
477477+ noteID, err := h.createNoteFromFile(ctx, filePath)
478478+ if err != nil {
479479+ ui.Warningln("Failed to create note from %s: %v", filePath, err)
480480+ failed++
481481+ continue
482482+ }
483483+ noteIDs = append(noteIDs, noteID)
484484+ }
485485+486486+ if len(noteIDs) == 0 {
487487+ return fmt.Errorf("failed to create any notes from files")
488488+ }
489489+490490+ ui.Newline()
491491+ if dryRun {
492492+ ui.Successln("Created %d note(s) from files. Skipping leaflet push (dry run).", len(noteIDs))
493493+ ui.Infoln("Note IDs: %v", noteIDs)
494494+ return nil
495495+ }
496496+497497+ return h.Push(ctx, noteIDs, isDraft, dryRun)
498498+}
499499+387500// Push creates or updates multiple documents on leaflet from local notes
388388-func (h *PublicationHandler) Push(ctx context.Context, noteIDs []int64, isDraft bool) error {
389389- if !h.atproto.IsAuthenticated() {
501501+func (h *PublicationHandler) Push(ctx context.Context, noteIDs []int64, isDraft bool, dryRun bool) error {
502502+ if !dryRun && !h.atproto.IsAuthenticated() {
390503 return fmt.Errorf("not authenticated - run 'noteleaf pub auth' first")
391504 }
392505···394507 return fmt.Errorf("no note IDs provided")
395508 }
396509397397- ui.Infoln("Processing %d note(s)...\n", len(noteIDs))
510510+ if dryRun {
511511+ ui.Infoln("Dry run: validating %d note(s)...\n", len(noteIDs))
512512+ } else {
513513+ ui.Infoln("Processing %d note(s)...\n", len(noteIDs))
514514+ }
398515399516 var created, updated, failed int
400517 var errors []string
···408525 continue
409526 }
410527411411- if note.HasLeafletAssociation() {
412412- err = h.Patch(ctx, noteID)
528528+ if dryRun {
529529+ _, _, err := h.prepareDocumentForPublish(ctx, noteID, isDraft, note.HasLeafletAssociation())
413530 if err != nil {
414414- ui.Warningln(" [%d] Failed to update '%s': %v", noteID, note.Title, err)
531531+ ui.Warningln(" [%d] Validation failed for '%s': %v", noteID, note.Title, err)
415532 errors = append(errors, fmt.Sprintf("note %d (%s): %v", noteID, note.Title, err))
416533 failed++
417534 } else {
418418- updated++
535535+ ui.Infoln(" [%d] '%s' - validation passed", noteID, note.Title)
536536+ if note.HasLeafletAssociation() {
537537+ updated++
538538+ } else {
539539+ created++
540540+ }
419541 }
420542 } else {
421421- err = h.Post(ctx, noteID, isDraft)
422422- if err != nil {
423423- ui.Warningln(" [%d] Failed to create '%s': %v", noteID, note.Title, err)
424424- errors = append(errors, fmt.Sprintf("note %d (%s): %v", noteID, note.Title, err))
425425- failed++
543543+ if note.HasLeafletAssociation() {
544544+ err = h.Patch(ctx, noteID)
545545+ if err != nil {
546546+ ui.Warningln(" [%d] Failed to update '%s': %v", noteID, note.Title, err)
547547+ errors = append(errors, fmt.Sprintf("note %d (%s): %v", noteID, note.Title, err))
548548+ failed++
549549+ } else {
550550+ updated++
551551+ }
426552 } else {
427427- created++
553553+ err = h.Post(ctx, noteID, isDraft)
554554+ if err != nil {
555555+ ui.Warningln(" [%d] Failed to create '%s': %v", noteID, note.Title, err)
556556+ errors = append(errors, fmt.Sprintf("note %d (%s): %v", noteID, note.Title, err))
557557+ failed++
558558+ } else {
559559+ created++
560560+ }
428561 }
429562 }
430563 }
431564432565 ui.Newline()
433433- ui.Successln("Push complete: %d created, %d updated, %d failed", created, updated, failed)
566566+ if dryRun {
567567+ ui.Successln("Dry run complete: %d would be created, %d would be updated, %d failed validation", created, updated, failed)
568568+ ui.Infoln("No changes made to leaflet")
569569+ } else {
570570+ ui.Successln("Push complete: %d created, %d updated, %d failed", created, updated, failed)
571571+ }
434572435573 if len(errors) > 0 {
436574 return fmt.Errorf("push completed with %d error(s)", failed)
+8-8
internal/handlers/publication_test.go
···16621662 handler := CreateHandler(t, NewPublicationHandler)
16631663 ctx := context.Background()
1664166416651665- err := handler.Push(ctx, []int64{1, 2, 3}, false)
16651665+ err := handler.Push(ctx, []int64{1, 2, 3}, false, false)
16661666 if err == nil {
16671667 t.Error("Expected error when not authenticated")
16681668 }
···16941694 t.Fatalf("Failed to restore session: %v", err)
16951695 }
1696169616971697- err = handler.Push(ctx, []int64{}, false)
16971697+ err = handler.Push(ctx, []int64{}, false, false)
16981698 if err == nil {
16991699 t.Error("Expected error when no note IDs provided")
17001700 }
···17261726 t.Fatalf("Failed to restore session: %v", err)
17271727 }
1728172817291729- err = handler.Push(ctx, []int64{999}, false)
17291729+ err = handler.Push(ctx, []int64{999}, false, false)
17301730 if err == nil {
17311731 t.Error("Expected error when note not found")
17321732 }
···17731773 t.Fatalf("Failed to restore session: %v", err)
17741774 }
1775177517761776- err = handler.Push(ctx, []int64{id1, id2}, false)
17761776+ err = handler.Push(ctx, []int64{id1, id2}, false, false)
1777177717781778 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
17791779 t.Logf("Got error during push (expected for external service call): %v", err)
···18301830 t.Fatalf("Failed to restore session: %v", err)
18311831 }
1832183218331833- err = handler.Push(ctx, []int64{id1, id2}, false)
18331833+ err = handler.Push(ctx, []int64{id1, id2}, false, false)
1834183418351835 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
18361836 t.Logf("Got error during push (expected for external service call): %v", err)
···18821882 t.Fatalf("Failed to restore session: %v", err)
18831883 }
1884188418851885- err = handler.Push(ctx, []int64{newID, existingID}, false)
18851885+ err = handler.Push(ctx, []int64{newID, existingID}, false, false)
1886188618871887 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
18881888 t.Logf("Got error during push (expected for external service call): %v", err)
···19201920 }
1921192119221922 invalidID := int64(999)
19231923- err = handler.Push(ctx, []int64{id1, invalidID}, false)
19231923+ err = handler.Push(ctx, []int64{id1, invalidID}, false, false)
1924192419251925 if err == nil {
19261926 t.Error("Expected error due to invalid note ID")
···19611961 t.Fatalf("Failed to restore session: %v", err)
19621962 }
1963196319641964- err = handler.Push(ctx, []int64{id}, true)
19641964+ err = handler.Push(ctx, []int64{id}, true, false)
1965196519661966 if err != nil && !strings.Contains(err.Error(), "not authenticated") {
19671967 t.Logf("Got error during push (expected for external service call): %v", err)
+39-47
internal/ui/palette.go
···19192020var NoteleafColorScheme fang.ColorSchemeFunc = noteleafColorScheme
21212222+// noteleafColorScheme provides Iceberg-inspired colors for CLI help/documentation
2323+//
2424+// Philosophy: Cool blues as primary, warm accents for emphasis, hierarchical text colors
2525+// See: https://github.com/cocopon/iceberg.vim for more information
2226func noteleafColorScheme(c lipglossv2.LightDarkFunc) fang.ColorScheme {
2327 return fang.ColorScheme{
2424- Base: c(Salt, Pepper), // Light/Dark base text
2525- Title: c(Guac, Julep), // Green primary for titles
2626- Description: c(Squid, Smoke), // Muted gray for descriptions
2727- Codeblock: c(Butter, BBQ), // Light/Dark background for code
2828- Program: c(Malibu, Sardine), // Blue for program names
2929- DimmedArgument: c(Oyster, Ash), // Subtle gray for dimmed text
3030- Comment: c(Pickle, NeueGuac), // Green for comments
3131- Flag: c(Violet, Mauve), // Purple for flags
3232- FlagDefault: c(Lichen, Turtle), // Teal for flag defaults
3333- Command: c(Julep, Guac), // Bright green for commands
3434- QuotedString: c(Citron, Mustard), // Yellow for quoted strings
3535- Argument: c(Sapphire, Guppy), // Blue for arguments
3636- Help: c(Smoke, Iron), // Gray for help text
3737- Dash: c(Iron, Oyster), // Medium gray for dashes
3838- ErrorHeader: [2]color.Color{Cherry, Sriracha}, // Red for error headers (fg, bg)
3939- ErrorDetails: c(Coral, Salmon), // Red/pink for error details
2828+ Base: c(Salt, Pepper), // Primary text on dark background
2929+ Title: c(Malibu, Malibu), // Blue primary for titles (Iceberg primary)
3030+ Description: c(Smoke, Smoke), // Secondary text for descriptions
3131+ Codeblock: c(Butter, BBQ), // Light/Dark background for code blocks
3232+ Program: c(Malibu, Sardine), // Blue for program names (primary accent)
3333+ DimmedArgument: c(Oyster, Ash), // Dimmed text for optional arguments
3434+ Comment: c(Squid, Squid), // Muted gray for comments (Iceberg comment)
3535+ Flag: c(Hazy, Jelly), // Purple for flags (Iceberg special)
3636+ FlagDefault: c(Lichen, Turtle), // Teal for flag defaults (secondary accent)
3737+ Command: c(Julep, Julep), // Green for commands (success/positive)
3838+ QuotedString: c(Tang, Tang), // Orange for quoted strings (warning/warm)
3939+ Argument: c(Lichen, Lichen), // Teal for arguments (secondary accent)
4040+ Help: c(Squid, Squid), // Muted gray for help text
4141+ Dash: c(Oyster, Oyster), // Dimmed gray for dashes/separators
4242+ ErrorHeader: [2]color.Color{Sriracha, Sriracha}, // Red for error headers (Iceberg error)
4343+ ErrorDetails: c(Coral, Salmon), // Pink/coral for error details
4044 }
4145}
4246···5357}
54585559var (
5656-// Background colors (dark mode, Iceberg-inspired)
5760 ColorBGBase = Pepper.Hex() // #201F26 - Darkest base
5861 ColorBGSecondary = BBQ.Hex() // #2d2c35 - Secondary background
5962 ColorBGTertiary = Charcoal.Hex() // #3A3943 - Tertiary/elevated
6063 ColorBGInput = Iron.Hex() // #4D4C57 - Input fields/focus
61646262- // Text colors (light to dark hierarchy)
6365 ColorTextPrimary = Salt.Hex() // #F1EFEF - Primary text (brightest)
6466 ColorTextSecondary = Smoke.Hex() // #BFBCC8 - Secondary text
6567 ColorTextMuted = Squid.Hex() // #858392 - Muted/comments
6668 ColorTextDimmed = Oyster.Hex() // #605F6B - Dimmed text
67696868- // Semantic colors (Iceberg-inspired: cool blues/purples with warm accents)
6970 ColorPrimary = Malibu.Hex() // #00A4FF - Blue (primary accent)
7071 ColorSuccess = Julep.Hex() // #00FFB2 - Green (success/positive)
7172 ColorError = Sriracha.Hex() // #EB4268 - Red (errors)
···7374 ColorInfo = Violet.Hex() // #C259FF - Purple (info)
7475 ColorAccent = Lichen.Hex() // #5CDFEA - Teal (secondary accent)
75767676- // Base styles
7777 PrimaryStyle = newStyle().Foreground(lipgloss.Color(ColorPrimary))
7878 SuccessStyle = newBoldStyle().Foreground(lipgloss.Color(ColorSuccess))
7979 ErrorStyle = newBoldStyle().Foreground(lipgloss.Color(ColorError))
···8585 TitleStyle = newPBoldStyle(0, 1).Foreground(lipgloss.Color(ColorPrimary))
8686 SubtitleStyle = newEmStyle().Foreground(lipgloss.Color(ColorAccent))
87878888- // Layout styles
8988 BoxStyle = newPStyle(1, 2).Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color(ColorPrimary))
9089 ErrorBoxStyle = newPStyle(1, 2).Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color(ColorError))
9190 HeaderStyle = newPBoldStyle(0, 1).Foreground(lipgloss.Color(ColorPrimary))
9291 CellStyle = newPStyle(0, 1).Foreground(lipgloss.Color(ColorTextPrimary))
93929494- // List styles
9593 ListItemStyle = newStyle().Foreground(lipgloss.Color(ColorTextPrimary)).PaddingLeft(2)
9694 SelectedItemStyle = newBoldStyle().Foreground(lipgloss.Color(ColorPrimary)).PaddingLeft(2)
97959898- // Table/data view styles (replacing ANSI code references)
9996 TableStyle = newStyle().BorderStyle(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color(ColorTextMuted))
10097 TableHeaderStyle = newBoldStyle().Foreground(lipgloss.Color(ColorAccent))
10198 TableTitleStyle = newBoldStyle().Foreground(lipgloss.Color(ColorPrimary))
10299 TableSelectedStyle = newBoldStyle().Foreground(lipgloss.Color(ColorTextPrimary)).Background(lipgloss.Color(ColorBGInput))
103100104104- // Task-specific styles
105101 TaskTitleStyle = newBoldStyle().Foreground(lipgloss.Color(ColorTextPrimary))
106102 TaskIDStyle = newStyle().Foreground(lipgloss.Color(ColorTextMuted)).Width(8)
107103108108- // Status styles (Iceberg-inspired: muted โ blue โ red โ green)
109109- StatusTodo = newStyle().Foreground(lipgloss.Color(ColorTextMuted)) // Gray (muted)
110110- StatusInProgress = newStyle().Foreground(lipgloss.Color(ColorPrimary)) // Blue (active)
111111- StatusBlocked = newStyle().Foreground(lipgloss.Color(ColorError)) // Red (blocked)
112112- StatusDone = newStyle().Foreground(lipgloss.Color(ColorSuccess)) // Green (success)
113113- StatusPending = newStyle().Foreground(lipgloss.Color(ColorWarning)) // Orange (pending)
114114- StatusCompleted = newStyle().Foreground(lipgloss.Color(ColorSuccess)) // Green (completed)
115115- StatusAbandoned = newStyle().Foreground(lipgloss.Color(ColorTextDimmed)) // Dimmed gray (abandoned)
116116- StatusDeleted = newStyle().Foreground(lipgloss.Color(Cherry.Hex())) // Dark red (deleted)
104104+ StatusTodo = newStyle().Foreground(lipgloss.Color(ColorTextMuted)) // Gray (muted)
105105+ StatusInProgress = newStyle().Foreground(lipgloss.Color(ColorPrimary)) // Blue (active)
106106+ StatusBlocked = newStyle().Foreground(lipgloss.Color(ColorError)) // Red (blocked)
107107+ StatusDone = newStyle().Foreground(lipgloss.Color(ColorSuccess)) // Green (success)
108108+ StatusPending = newStyle().Foreground(lipgloss.Color(ColorWarning)) // Orange (pending)
109109+ StatusCompleted = newStyle().Foreground(lipgloss.Color(ColorSuccess)) // Green (completed)
110110+ StatusAbandoned = newStyle().Foreground(lipgloss.Color(ColorTextDimmed)) // Dimmed gray (abandoned)
111111+ StatusDeleted = newStyle().Foreground(lipgloss.Color(Pom.Hex())) // Dark red (deleted)
117112118118- // Priority styles (Iceberg-inspired: red โ orange โ gray)
119119- PriorityHigh = newBoldStyle().Foreground(lipgloss.Color(Cherry.Hex())) // #FF388B - Bright red
120120- PriorityMedium = newStyle().Foreground(lipgloss.Color(Tang.Hex())) // #FF985A - Orange
121121- PriorityLow = newStyle().Foreground(lipgloss.Color(ColorAccent)) // Teal (low)
122122- PriorityNone = newStyle().Foreground(lipgloss.Color(ColorTextMuted)) // Gray (no priority)
123123- PriorityLegacy = newStyle().Foreground(lipgloss.Color(Urchin.Hex())) // #C337E0 - Magenta (legacy)
113113+ PriorityHigh = newBoldStyle().Foreground(lipgloss.Color(Pom.Hex())) // #FF388B - Bright red
114114+ PriorityMedium = newStyle().Foreground(lipgloss.Color(Tang.Hex())) // #FF985A - Orange
115115+ PriorityLow = newStyle().Foreground(lipgloss.Color(ColorAccent)) // #5CDFEA - Teal (low)
116116+ PriorityNone = newStyle().Foreground(lipgloss.Color(ColorTextMuted)) // #858392 - Gray (no priority)
117117+ PriorityLegacy = newStyle().Foreground(lipgloss.Color(Urchin.Hex())) // #C337E0 - Magenta (legacy)
124118125125- // Content type styles (distinctive colors for different media)
126126- MovieStyle = newBoldStyle().Foreground(lipgloss.Color(Coral.Hex())) // #FF577D - Pink/coral
127127- TVStyle = newBoldStyle().Foreground(lipgloss.Color(Violet.Hex())) // #C259FF - Purple
128128- BookStyle = newBoldStyle().Foreground(lipgloss.Color(Guac.Hex())) // #12C78F - Green
129129- MusicStyle = newBoldStyle().Foreground(lipgloss.Color(Lichen.Hex())) // #5CDFEA - Teal
119119+ MovieStyle = newBoldStyle().Foreground(lipgloss.Color(Coral.Hex())) // #FF577D - Pink/coral
120120+ TVStyle = newBoldStyle().Foreground(lipgloss.Color(Violet.Hex())) // #C259FF - Purple
121121+ BookStyle = newBoldStyle().Foreground(lipgloss.Color(Guac.Hex())) // #12C78F - Green
122122+ MusicStyle = newBoldStyle().Foreground(lipgloss.Color(Lichen.Hex())) // #5CDFEA - Teal
130123131131- // Diff styles
132132- AdditionStyle = newStyle().Foreground(lipgloss.Color(Pickle.Hex())) // #00A475 - Green
133133- DeletionStyle = newStyle().Foreground(lipgloss.Color(Pom.Hex())) // #AB2454 - Red
124124+ AdditionStyle = newStyle().Foreground(lipgloss.Color(Pickle.Hex())) // #00A475 - Green
125125+ DeletionStyle = newStyle().Foreground(lipgloss.Color(Pom.Hex())) // #AB2454 - Red
134126)