543 lines
26 KiB
EmacsLisp
543 lines
26 KiB
EmacsLisp
;;; research-shelf.el --- SQLite-backed research library -*- lexical-binding: t; -*-
|
||
|
||
;;; Commentary:
|
||
;; A useful ETAF application: a small reading/research shelf whose records
|
||
;; survive unmounts because the Data Controller is backed by etaf-sqlite.
|
||
|
||
;;; Code:
|
||
(require 'cl-lib)
|
||
(require 'subr-x)
|
||
(require 'etaf)
|
||
(require 'etaf-ui)
|
||
(require 'etaf-sqlite)
|
||
(require 'etaf-playground)
|
||
|
||
(defcustom etaf-research-shelf-database-file
|
||
(expand-file-name "research-shelf.sqlite" user-emacs-directory)
|
||
"SQLite file used by the Research Shelf application."
|
||
:type 'file
|
||
:group 'etaf)
|
||
|
||
(defconst etaf-research-shelf-seed-records
|
||
'((:id 1 :title "The Shape of Tools" :author "M. Abramson"
|
||
:kind "Essay" :status "reading" :progress 64 :priority "High"
|
||
:starred 1 :note "The best tools make attention feel larger, not more fragmented." :updated "Today")
|
||
(:id 2 :title "Ways of Seeing" :author "John Berger"
|
||
:kind "Book" :status "unread" :progress 0 :priority "Medium"
|
||
:starred 0 :note "Look again at the relationship between image and power." :updated "Yesterday")
|
||
(:id 3 :title "The Cathedral and the Bazaar" :author "Eric S. Raymond"
|
||
:kind "Paper" :status "finished" :progress 100 :priority "Low"
|
||
:starred 1 :note "A useful contrast between closed and open production." :updated "Mon")
|
||
(:id 4 :title "Designing for Calm" :author "A. Ito"
|
||
:kind "Notes" :status "reading" :progress 32 :priority "High"
|
||
:starred 0 :note "Reduce decisions before asking for attention." :updated "Sun")
|
||
(:id 5 :title "The Craftsman" :author "Richard Sennett"
|
||
:kind "Book" :status "unread" :progress 0 :priority "Medium"
|
||
:starred 1 :note "A long-form companion for making and revising." :updated "Sat")
|
||
(:id 6 :title "How Buildings Learn" :author "Stewart Brand"
|
||
:kind "Book" :status "reading" :progress 78 :priority "Medium"
|
||
:starred 0 :note "Change is a property of useful systems." :updated "Fri")
|
||
(:id 7 :title "The Interface Is the Message" :author "N. Hara"
|
||
:kind "Essay" :status "finished" :progress 100 :priority "Low"
|
||
:starred 0 :note "A short note on visible constraints." :updated "Thu")
|
||
(:id 8 :title "Notes on Attention" :author "Simone Weil"
|
||
:kind "Notes" :status "unread" :progress 0 :priority "High"
|
||
:starred 1 :note "Attention is not force; it is patient availability." :updated "Wed"))
|
||
"Seed records installed into a new Research Shelf database once.")
|
||
|
||
(defun etaf-research-shelf--static-child (form tag)
|
||
"Return the first child named TAG from static FORM."
|
||
(cl-find-if (lambda (entry) (and (consp entry) (eq (car entry) tag)))
|
||
(cdr form)))
|
||
|
||
(defun etaf-research-shelf--static-value (form key default)
|
||
"Return KEY from static FORM, or DEFAULT."
|
||
(or (plist-get (cdr form) key) default))
|
||
|
||
(defun etaf-research-shelf--database ()
|
||
"Return the configured typed Research Shelf database description."
|
||
(etaf-sqlite-database
|
||
etaf-research-shelf-database-file
|
||
(etaf-sqlite-table
|
||
'reading_items
|
||
(list (etaf-sqlite-column :id "id" :type 'integer :primary t)
|
||
(etaf-sqlite-column :title "title" :type 'text)
|
||
(etaf-sqlite-column :author "author" :type 'text)
|
||
(etaf-sqlite-column :kind "kind" :type 'text)
|
||
(etaf-sqlite-column :status "status" :type 'text)
|
||
(etaf-sqlite-column :progress "progress" :type 'integer)
|
||
(etaf-sqlite-column :priority "priority" :type 'text)
|
||
(etaf-sqlite-column :starred "starred" :type 'integer)
|
||
(etaf-sqlite-column :note "note" :type 'text)
|
||
(etaf-sqlite-column :updated "updated" :type 'text))
|
||
:id)))
|
||
|
||
(defun etaf-research-shelf--ensure-database ()
|
||
"Initialize the SQLite database and install seed data when empty."
|
||
(let* ((database (etaf-research-shelf--database))
|
||
(source (etaf-sqlite-source database))
|
||
(load (plist-get source :load))
|
||
(mutate (plist-get source :mutate)))
|
||
(etaf-sqlite-initialize database)
|
||
(when (= 0 (plist-get (funcall load nil 1 1) :total))
|
||
(dolist (record etaf-research-shelf-seed-records)
|
||
(funcall mutate 'insert record)))
|
||
database))
|
||
|
||
(defun etaf-research-shelf--create-model ()
|
||
"Create the application model and its SQLite-backed Data Controller."
|
||
(let* ((database (etaf-research-shelf--ensure-database))
|
||
(controller
|
||
(etaf-data-controller
|
||
(etaf-sqlite-source database)
|
||
:page-size 4 :auto-load nil :name 'etaf-research-shelf))
|
||
(filter (etaf-ref 'all :name 'research-shelf-filter))
|
||
(dark (etaf-ref nil :name 'research-shelf-dark-theme))
|
||
(toast (etaf-ref "Ready to read." :name 'research-shelf-toast))
|
||
(next-id (etaf-ref 100 :name 'research-shelf-next-id)))
|
||
(list :database database :database-file etaf-research-shelf-database-file
|
||
:controller controller :filter filter :dark dark :toast toast
|
||
:next-id next-id)))
|
||
|
||
(defun etaf-research-shelf--controller (model)
|
||
"Return MODEL's Data Controller."
|
||
(plist-get model :controller))
|
||
|
||
(defun etaf-research-shelf--query (filter)
|
||
"Translate FILTER into an allowlisted SQLite equality query."
|
||
(pcase filter
|
||
('reading '(:status "reading"))
|
||
('unread '(:status "unread"))
|
||
('finished '(:status "finished"))
|
||
('starred '(:starred 1))
|
||
(_ nil)))
|
||
|
||
(defun etaf-research-shelf--load (model &optional message)
|
||
"Load MODEL and expose a visible success/error MESSAGE."
|
||
(let ((controller (etaf-research-shelf--controller model)))
|
||
(condition-case err
|
||
(progn
|
||
(etaf-data-load controller)
|
||
(setf (etaf-value (plist-get model :toast))
|
||
(or message "✓ Saved locally")))
|
||
(error
|
||
(setf (etaf-value (plist-get model :toast))
|
||
(format "⚠ Storage issue: %s" (error-message-string err)))))))
|
||
|
||
(defun etaf-research-shelf--set-filter (model filter)
|
||
"Set MODEL FILTER and reload the SQLite-backed list."
|
||
(let ((controller (etaf-research-shelf--controller model)))
|
||
(setf (etaf-value (plist-get model :filter)) filter)
|
||
(etaf-data-set-query controller (etaf-research-shelf--query filter))
|
||
(etaf-data-set-page controller 1)
|
||
(etaf-research-shelf--load model
|
||
(format "Showing %s"
|
||
(capitalize (symbol-name filter))))))
|
||
|
||
(defun etaf-research-shelf--items (model)
|
||
"Return loaded items for MODEL."
|
||
(etaf-value (etaf-data-items (etaf-research-shelf--controller model))))
|
||
|
||
(defun etaf-research-shelf--selected (model)
|
||
"Return the selected item in MODEL, or nil."
|
||
(let* ((controller (etaf-research-shelf--controller model))
|
||
(id (car (etaf-value (etaf-data-selection controller)))))
|
||
(cl-find id (etaf-research-shelf--items model)
|
||
:key (lambda (row) (plist-get row :id)) :test #'equal)))
|
||
|
||
(defun etaf-research-shelf--mutate (model operation payload message)
|
||
"Apply SQLite OPERATION PAYLOAD for MODEL.
|
||
Show MESSAGE on success, or preserve the resulting error state."
|
||
(let ((controller (etaf-research-shelf--controller model)))
|
||
(condition-case err
|
||
(progn
|
||
(etaf-data-mutate controller operation payload)
|
||
(setf (etaf-value (plist-get model :toast)) message))
|
||
(error
|
||
(setf (etaf-value (plist-get model :toast))
|
||
(format "⚠ Could not save: %s" (error-message-string err)))))))
|
||
|
||
(defun etaf-research-shelf--update-selected (model fields message)
|
||
"Update selected MODEL record with FIELDS and show MESSAGE."
|
||
(when-let ((row (etaf-research-shelf--selected model)))
|
||
(etaf-research-shelf--mutate
|
||
model 'update
|
||
(append (list :id (plist-get row :id)) fields)
|
||
message)))
|
||
|
||
(defun etaf-research-shelf--theme (model)
|
||
"Return MODEL's current palette."
|
||
(if (etaf-value (plist-get model :dark))
|
||
'(:ink "#F4F7FF" :paper "#111827" :panel "#182235" :line "#34435A"
|
||
:muted "#AAB5C8" :accent "#8EA7FF" :mint "#76D6C3" :coral "#FF9A8E"
|
||
:amber "#F0C36A" :soft "#202C42")
|
||
'(:ink "#172033" :paper "#F7F3EA" :panel "#FFFDF8" :line "#D9D4C9"
|
||
:muted "#6D7482" :accent "#3657D6" :mint "#3E9B8F" :coral "#D86B5D"
|
||
:amber "#C58A3A" :soft "#F1EEE7")))
|
||
|
||
(defun etaf-research-shelf--color (model key)
|
||
"Return palette KEY for MODEL."
|
||
(plist-get (etaf-research-shelf--theme model) key))
|
||
|
||
(defun etaf-research-shelf--status-label (status)
|
||
"Return a readable status label for STATUS."
|
||
(pcase status
|
||
("reading" "◷ In progress")
|
||
("finished" "✓ Finished")
|
||
("archived" "↗ Archived")
|
||
(_ "○ Unread")))
|
||
|
||
(defun etaf-research-shelf--filter-label (model form key fallback)
|
||
"Return MODEL's active-aware label for KEY from static FILTER FORM."
|
||
(let* ((entry (cl-find-if
|
||
(lambda (item) (and (consp item) (eq (car item) 'filter)
|
||
(eq (plist-get (cdr item) :key) key)))
|
||
(cdr form)))
|
||
(label (or (and entry (plist-get (cdr entry) :label)) fallback)))
|
||
(if (eq key (etaf-value (plist-get model :filter)))
|
||
(concat "● " label)
|
||
label)))
|
||
|
||
(defun etaf-research-shelf--filter-view (model form)
|
||
"Return the filter rail from MODEL and static FILTER FORM."
|
||
(etaf-view
|
||
(column :class "research-shelf-filter-rail" :width 'stretch
|
||
:padding '(1 1)
|
||
:border (etaf-research-shelf--color model :line)
|
||
:color (etaf-research-shelf--color model :ink)
|
||
:bgcolor (etaf-research-shelf--color model :panel)
|
||
(label :text "Library" :face 'bold
|
||
:color (etaf-research-shelf--color model :ink)
|
||
:bgcolor (etaf-research-shelf--color model :panel))
|
||
(label :text "VIEWS" :color (etaf-research-shelf--color model :muted)
|
||
:bgcolor (etaf-research-shelf--color model :panel))
|
||
(button :label (etaf-research-shelf--filter-label model form 'all "All")
|
||
:ref 'research-shelf-filter-all
|
||
:variant (if (eq (etaf-value (plist-get model :filter)) 'all)
|
||
'secondary 'ghost)
|
||
:on-press (lambda ()
|
||
(etaf-research-shelf--set-filter model 'all)))
|
||
(button :label (etaf-research-shelf--filter-label
|
||
model form 'reading "In progress")
|
||
:ref 'research-shelf-filter-reading
|
||
:variant (if (eq (etaf-value (plist-get model :filter)) 'reading)
|
||
'secondary 'ghost)
|
||
:on-press (lambda ()
|
||
(etaf-research-shelf--set-filter model 'reading)))
|
||
(button :label (etaf-research-shelf--filter-label
|
||
model form 'unread "Unread")
|
||
:ref 'research-shelf-filter-unread
|
||
:variant (if (eq (etaf-value (plist-get model :filter)) 'unread)
|
||
'secondary 'ghost)
|
||
:on-press (lambda ()
|
||
(etaf-research-shelf--set-filter model 'unread)))
|
||
(button :label (etaf-research-shelf--filter-label
|
||
model form 'finished "Finished")
|
||
:ref 'research-shelf-filter-finished
|
||
:variant (if (eq (etaf-value (plist-get model :filter)) 'finished)
|
||
'secondary 'ghost)
|
||
:on-press (lambda ()
|
||
(etaf-research-shelf--set-filter model 'finished)))
|
||
(button :label (etaf-research-shelf--filter-label
|
||
model form 'starred "★ Starred")
|
||
:ref 'research-shelf-filter-starred
|
||
:variant (if (eq (etaf-value (plist-get model :filter)) 'starred)
|
||
'secondary 'ghost)
|
||
:on-press (lambda ()
|
||
(etaf-research-shelf--set-filter model 'starred)))
|
||
(spacer :height 1)
|
||
(label :text "STORAGE" :color (etaf-research-shelf--color model :muted)
|
||
:bgcolor (etaf-research-shelf--color model :panel))
|
||
(label :text (file-name-nondirectory
|
||
(plist-get model :database-file))
|
||
:color (etaf-research-shelf--color model :ink)
|
||
:bgcolor (etaf-research-shelf--color model :panel))
|
||
(button :label "↻ Reload library" :ref 'research-shelf-reload
|
||
:variant 'ghost
|
||
:on-press (lambda () (etaf-research-shelf--load model "✓ Library reloaded"))))))
|
||
|
||
(defun etaf-research-shelf--row-press (model row)
|
||
"Select ROW as the single current record in MODEL."
|
||
(etaf-data-select-one (etaf-research-shelf--controller model)
|
||
(plist-get row :id)))
|
||
|
||
(defun etaf-research-shelf--reading-list-view (model form)
|
||
"Return the main reading list for MODEL and static MAIN FORM."
|
||
(let ((controller (etaf-research-shelf--controller model)))
|
||
(etaf-view
|
||
(column :class "research-shelf-list" :width 'stretch
|
||
:padding '(1 2)
|
||
:border (etaf-research-shelf--color model :line)
|
||
:color (etaf-research-shelf--color model :ink)
|
||
:bgcolor (etaf-research-shelf--color model :panel)
|
||
(flex :width 'stretch :align-items 'center :gap '(1 (8))
|
||
(column :flex-grow 1 :flex-shrink 1 :flex-basis '(0) :min-width 0
|
||
(label :text (etaf-research-shelf--static-value form :title
|
||
"Reading queue")
|
||
:face 'bold
|
||
:color (etaf-research-shelf--color model :ink)
|
||
:bgcolor (etaf-research-shelf--color model :panel))
|
||
(label :text (format "%d items · SQLite-backed"
|
||
(or (etaf-value (etaf-data-total controller)) 0))
|
||
:color (etaf-research-shelf--color model :muted)
|
||
:bgcolor (etaf-research-shelf--color model :panel)))
|
||
(button :label "+ Add reading" :ref 'research-shelf-add
|
||
:variant 'secondary
|
||
:on-press
|
||
(lambda ()
|
||
(let ((id (etaf-value (plist-get model :next-id))))
|
||
(etaf-research-shelf--mutate
|
||
model 'insert
|
||
(list :id id :title (format "New reading %d" id)
|
||
:author "You" :kind "Notes" :status "unread"
|
||
:progress 0 :priority "Medium" :starred 0
|
||
:note "A new note waiting for your attention."
|
||
:updated "Just now")
|
||
"✓ Added to your shelf")
|
||
(setf (etaf-value (plist-get model :next-id)) (1+ id))))))
|
||
(data-grid
|
||
:controller controller
|
||
:columns '((:key :title :label "Title" :width 30)
|
||
(:key :author :label "Author" :width 16)
|
||
(:key :status :label "Status" :width 14)
|
||
(:key :progress :label "Progress" :width 10)
|
||
(:key :kind :label "Kind" :width 10))
|
||
:row-key (lambda (row) (plist-get row :id))
|
||
:row-ref (lambda (row)
|
||
(intern (format "research-shelf-row-%s"
|
||
(plist-get row :id))))
|
||
:row-selected-p
|
||
(lambda (row)
|
||
(etaf-data-selected-p controller (plist-get row :id)))
|
||
:on-row-press (lambda (row) (etaf-research-shelf--row-press model row))
|
||
:loading-label "◷ Loading library…"
|
||
:error-label "⚠ Could not read the shelf. Use Reload.")
|
||
(etaf-pagination :controller controller
|
||
:previous-ref 'research-shelf-page-previous
|
||
:next-ref 'research-shelf-page-next
|
||
:color (etaf-research-shelf--color model :ink)
|
||
:bgcolor (etaf-research-shelf--color model :panel))))))
|
||
|
||
(defun etaf-research-shelf--detail-view (model form)
|
||
"Return the selected record inspector for MODEL and static FORM."
|
||
(etaf-view
|
||
(column :class "research-shelf-detail" :width 'stretch
|
||
:padding '(2 2)
|
||
:border (etaf-research-shelf--color model :line)
|
||
:color (etaf-research-shelf--color model :ink)
|
||
:bgcolor (etaf-research-shelf--color model :panel)
|
||
(label :text (etaf-research-shelf--static-value form :title
|
||
"Selected item")
|
||
:color (etaf-research-shelf--color model :accent)
|
||
:bgcolor (etaf-research-shelf--color model :panel))
|
||
(text :face 'bold
|
||
(expr :value
|
||
(if-let ((row (etaf-research-shelf--selected model)))
|
||
(plist-get row :title)
|
||
"Choose a record")))
|
||
(text :color (etaf-research-shelf--color model :muted)
|
||
(expr :value
|
||
(if-let ((row (etaf-research-shelf--selected model)))
|
||
(format "%s · %s" (plist-get row :author)
|
||
(plist-get row :kind))
|
||
"Select one row to inspect it.")))
|
||
(spacer :height 1)
|
||
(text :face 'bold
|
||
(expr :value
|
||
(if-let ((row (etaf-research-shelf--selected model)))
|
||
(etaf-research-shelf--status-label (plist-get row :status))
|
||
"○ No selection")))
|
||
(text
|
||
(expr :value
|
||
(if-let ((row (etaf-research-shelf--selected model)))
|
||
(format "Progress %d%% · Priority %s"
|
||
(or (plist-get row :progress) 0)
|
||
(plist-get row :priority))
|
||
"Progress — · Priority —")))
|
||
(text :color (etaf-research-shelf--color model :accent)
|
||
(expr :value
|
||
(let* ((row (etaf-research-shelf--selected model))
|
||
(progress (or (and row (plist-get row :progress)) 0))
|
||
(filled (max 0 (min 10 (/ progress 10)))))
|
||
(format "%s %s" (make-string filled ?█)
|
||
(make-string (- 10 filled) ?░)))))
|
||
(spacer :height 1)
|
||
(text :color (etaf-research-shelf--color model :muted)
|
||
(expr :value
|
||
(if-let ((row (etaf-research-shelf--selected model)))
|
||
(concat "“" (or (plist-get row :note) "No note yet.") "”")
|
||
"Your notes and actions will appear here.")))
|
||
(spacer :height 1)
|
||
(etaf-research-shelf-detail-actions :model model))))
|
||
|
||
(etaf-define-component etaf-research-shelf-detail-actions (&key model)
|
||
"Render stable selected-record actions for MODEL."
|
||
:setup
|
||
(let ((model (etaf-current-prop :model)))
|
||
(lambda ()
|
||
(let* ((row (etaf-research-shelf--selected model))
|
||
(progress (or (and row (plist-get row :progress)) 0))
|
||
(finished (and row (equal (plist-get row :status) "finished")))
|
||
(archived (and row (equal (plist-get row :status) "archived"))))
|
||
(etaf-view
|
||
(flex :width 'stretch :flex-wrap 'wrap :gap '(1 (6))
|
||
(button :label "+ 10%" :ref 'research-shelf-progress
|
||
:variant 'secondary :disabled (or (null row) (>= progress 100))
|
||
:on-press
|
||
(lambda ()
|
||
(when-let* ((current (etaf-research-shelf--selected model))
|
||
(value (or (plist-get current :progress) 0)))
|
||
(etaf-research-shelf--update-selected
|
||
model (list :progress (min 100 (+ value 10))
|
||
:status (if (>= (+ value 10) 100)
|
||
"finished" "reading")
|
||
:updated "Just now")
|
||
"✓ Progress saved"))))
|
||
(button :label "✓ Finish" :ref 'research-shelf-finish
|
||
:variant 'secondary :disabled (or (null row) finished)
|
||
:on-press
|
||
(lambda ()
|
||
(etaf-research-shelf--update-selected
|
||
model '(:progress 100 :status "finished" :updated "Just now")
|
||
"✓ Marked finished")))
|
||
(button :label "★ Star" :ref 'research-shelf-star :variant 'ghost
|
||
:disabled (null row)
|
||
:on-press
|
||
(lambda ()
|
||
(when-let ((current (etaf-research-shelf--selected model)))
|
||
(let ((current-starred
|
||
(= 1 (or (plist-get current :starred) 0))))
|
||
(etaf-research-shelf--update-selected
|
||
model (list :starred (if current-starred 0 1)
|
||
:updated "Just now")
|
||
(if current-starred
|
||
"Removed star" "★ Starred locally"))))))
|
||
(button :label "Archive" :ref 'research-shelf-archive
|
||
:variant 'ghost :disabled (or (null row) archived)
|
||
:on-press
|
||
(lambda ()
|
||
(etaf-research-shelf--update-selected
|
||
model '(:status "archived" :updated "Just now")
|
||
"↗ Archived")))))))))
|
||
|
||
(etaf-define-component etaf-research-shelf-filter-rail
|
||
(&key model static-form)
|
||
"Render the reusable Research Shelf filter rail.
|
||
|
||
The rail is a Component boundary so its button Components remain ordinary
|
||
structural children instead of leaking through a direct material `expr'."
|
||
:setup
|
||
(let ((model (etaf-current-prop :model))
|
||
(static-form (etaf-current-prop :static-form)))
|
||
(lambda () (etaf-research-shelf--filter-view model static-form))))
|
||
|
||
(etaf-define-component etaf-research-shelf-reading-list
|
||
(&key model static-form)
|
||
"Render the reusable Research Shelf reading list."
|
||
:setup
|
||
(let ((model (etaf-current-prop :model))
|
||
(static-form (etaf-current-prop :static-form)))
|
||
(lambda () (etaf-research-shelf--reading-list-view model static-form))))
|
||
|
||
(etaf-define-component etaf-research-shelf-detail-inspector
|
||
(&key model static-form)
|
||
"Render the reusable Research Shelf detail inspector."
|
||
:setup
|
||
(let ((model (etaf-current-prop :model))
|
||
(static-form (etaf-current-prop :static-form)))
|
||
(lambda () (etaf-research-shelf--detail-view model static-form))))
|
||
|
||
(etaf-define-component etaf-research-shelf-shell (&key model static-form)
|
||
"Compose the SQLite-backed Research Shelf application."
|
||
:styles
|
||
(styles
|
||
(".research-shelf-shell" :width stretch)
|
||
(".research-shelf-header" :width stretch :padding (1 0))
|
||
(".research-shelf-filter-rail" :width stretch)
|
||
(".research-shelf-list" :width stretch)
|
||
(".research-shelf-detail" :width stretch)
|
||
(".research-shelf-footer" :width stretch :padding (0 1)))
|
||
:setup
|
||
(let ((model (etaf-current-prop :model))
|
||
(static-form (etaf-current-prop :static-form)))
|
||
(etaf-on-unmounted
|
||
(lambda () (etaf-data-stop (etaf-research-shelf--controller model))))
|
||
(lambda ()
|
||
(let* ((header (etaf-research-shelf--static-child static-form 'header))
|
||
(filters (etaf-research-shelf--static-child static-form 'filters))
|
||
(main (etaf-research-shelf--static-child static-form 'main))
|
||
(library (etaf-research-shelf--static-child main 'library))
|
||
(detail (etaf-research-shelf--static-child main 'detail))
|
||
(dark (etaf-value (plist-get model :dark)))
|
||
(palette (etaf-research-shelf--theme model)))
|
||
(etaf-view
|
||
(column :class "research-shelf-shell" :width '(viewport)
|
||
:height '(viewport-height)
|
||
:color (plist-get palette :ink)
|
||
:bgcolor (plist-get palette :paper)
|
||
(flex :class "research-shelf-header" :width 'stretch
|
||
:align-items 'center :gap '(1 (10))
|
||
(column :flex-grow 1 :flex-shrink 1 :flex-basis '(0) :min-width 0
|
||
(label :text (etaf-research-shelf--static-value
|
||
header :eyebrow "Research Shelf")
|
||
:color (plist-get palette :accent)
|
||
:bgcolor (plist-get palette :paper))
|
||
(label :text (etaf-research-shelf--static-value
|
||
static-form :title "Research Shelf") :face 'bold
|
||
:color (plist-get palette :ink)
|
||
:bgcolor (plist-get palette :paper))
|
||
(label :text (etaf-research-shelf--static-value
|
||
static-form :subtitle "A quiet place for unfinished ideas")
|
||
:color (plist-get palette :muted)
|
||
:bgcolor (plist-get palette :paper)))
|
||
(checkbox :label "Dark" :ref 'research-shelf-theme-toggle
|
||
:checked dark
|
||
:on-change (lambda (value)
|
||
(setf (etaf-value (plist-get model :dark)) value)
|
||
(setf (etaf-value (plist-get model :toast))
|
||
(if value "Dark theme" "Light theme")))))
|
||
(grid :class "research-shelf-workspace" :width 'stretch
|
||
:grid-template-columns '(2fr 5fr 3fr) :gap '(1 (10))
|
||
:color (plist-get palette :ink)
|
||
:bgcolor (plist-get palette :paper)
|
||
(etaf-research-shelf-filter-rail :model model :static-form filters)
|
||
(etaf-research-shelf-reading-list :model model :static-form library)
|
||
(etaf-research-shelf-detail-inspector :model model :static-form detail))
|
||
(flex :class "research-shelf-footer" :width 'stretch :gap '(1 (8))
|
||
(column :flex-grow 1 :flex-shrink 1 :flex-basis '(0) :min-width 0
|
||
(label :text (or (etaf-value (plist-get model :toast)) "Ready")
|
||
:color (plist-get palette :ink)
|
||
:bgcolor (plist-get palette :paper)))
|
||
(label :text (file-name-nondirectory
|
||
(plist-get model :database-file))
|
||
:color (plist-get palette :muted)
|
||
:bgcolor (plist-get palette :paper)))))))))
|
||
|
||
(defun etaf-research-shelf-view (model static-form)
|
||
"Return the root View for MODEL and validated STATIC-FORM."
|
||
(etaf-view (research-shelf-shell :model model :static-form static-form)))
|
||
|
||
(defun etaf-research-shelf-root (static-form)
|
||
"Consume validated STATIC-FORM and return one owned application root View."
|
||
(unless (eq (car static-form) 'research-shelf-shell)
|
||
(error "Unsupported Research Shelf static root: %S" static-form))
|
||
(let ((model (etaf-research-shelf--create-model)))
|
||
;; Load before the first retained generation. A DataGrid that is created
|
||
;; empty and populated from an on-mounted callback has no committed
|
||
;; material artifact to use as its first incremental anchor.
|
||
(etaf-research-shelf--load model "✓ SQLite shelf ready")
|
||
(lambda () (etaf-research-shelf-view model static-form))))
|
||
|
||
;;;###autoload
|
||
(defun etaf-research-shelf-open (&optional buffer-name)
|
||
"Mount and optionally display the Research Shelf application in BUFFER-NAME."
|
||
(interactive)
|
||
(let ((buffer (etaf-mount (or buffer-name "*ETAF Research Shelf*")
|
||
(etaf-research-shelf-root
|
||
(etaf-playground-read-static "research-shelf")))))
|
||
(when (called-interactively-p 'interactive) (pop-to-buffer buffer))
|
||
buffer))
|
||
|
||
(provide 'etaf-research-shelf)
|
||
;;; research-shelf.el ends here
|