docs: add Pico CSS styling implementation plan
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
# Pico CSS Styling — Implementation Plan
|
||||
|
||||
**Goal:** Rewrite `spine-render-index` and `spine-render-empty-state` to emit Pico CSS-styled HTML via CDN, replacing the mockup-A `<div>` structure with Pico-native semantic elements plus a small inline `<style>` block.
|
||||
|
||||
**Architecture:** View-layer only change. No model or handler changes. The two view functions in `spine.el` are rewritten. Pico CSS v2 loads from `jsdelivr.net` CDN. Custom styles live in a `<style>` block in `<head>`.
|
||||
|
||||
**Tech Stack:** Emacs Lisp (view functions only), Pico CSS v2 (CDN).
|
||||
|
||||
---
|
||||
|
||||
## File structure
|
||||
|
||||
| File | Responsibility |
|
||||
|------|---------------|
|
||||
| `spine.el` | Rewrite `spine-render-index` and `spine-render-empty-state` |
|
||||
|
||||
No new files. No package changes.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Rewrite spine-render-empty-state
|
||||
|
||||
**Files:**
|
||||
- Modify: `spine.el` — `spine-render-empty-state`
|
||||
|
||||
- [ ] **Step 1: Replace `spine-render-empty-state` with Pico-styled version**
|
||||
|
||||
The function `spine-render-empty-state` is currently at approximately line 220 in `spine.el`. Replace the existing definition with:
|
||||
|
||||
```elisp
|
||||
(defun spine-render-empty-state ()
|
||||
"Render a page indicating no books file was found."
|
||||
(concat "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n"
|
||||
"<meta charset=\"utf-8\">\n"
|
||||
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"
|
||||
"<title>Spine</title>\n"
|
||||
"<link rel=\"stylesheet\""
|
||||
" href=\"https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css\">\n"
|
||||
"</head>\n<body>\n"
|
||||
"<main class=\"container\">\n"
|
||||
"<article>\n"
|
||||
"<header><strong>spine</strong></header>\n"
|
||||
"<p>No books yet.</p>\n"
|
||||
"</article>\n"
|
||||
"</main>\n</body>\n</html>"))
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify batch load**
|
||||
|
||||
```bash
|
||||
emacs --quick --batch --eval "(setq spine-skip-server-start t)" --load spine.el
|
||||
```
|
||||
|
||||
Expected: exits cleanly, no errors.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add spine.el
|
||||
git commit -m "feat: restyle empty-state page with Pico CSS"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Rewrite spine-render-index with Pico HTML
|
||||
|
||||
**Files:**
|
||||
- Modify: `spine.el` — `spine-render-index`
|
||||
|
||||
- [ ] **Step 1: Replace `spine-render-index` with Pico-styled version**
|
||||
|
||||
The function `spine-render-index` is currently at approximately line 123 in `spine.el`. Replace the entire function definition with:
|
||||
|
||||
```elisp
|
||||
(defun spine-render-index (books &optional selected-id)
|
||||
"Render the index page HTML for BOOKS, expanding the book matching SELECTED-ID."
|
||||
(let ((order '("WANT" "READING" "READ" "ABANDONED"))
|
||||
(grouped (make-hash-table :test 'equal))
|
||||
(total 0)
|
||||
(reading-count 0))
|
||||
;; Count totals
|
||||
(dolist (book books)
|
||||
(cl-incf total)
|
||||
(when (equal "READING" (plist-get book :status))
|
||||
(cl-incf reading-count)))
|
||||
;; Group books by status
|
||||
(dolist (book books)
|
||||
(let* ((status (or (plist-get book :status) "WANT")))
|
||||
(push book (gethash status grouped))))
|
||||
;; Render
|
||||
(with-temp-buffer
|
||||
(insert "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n")
|
||||
(insert "<meta charset=\"utf-8\">\n")
|
||||
(insert "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n")
|
||||
(insert "<title>Spine</title>\n")
|
||||
(insert "<link rel=\"stylesheet\""
|
||||
" href=\"https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css\">\n")
|
||||
(insert "<style>\n")
|
||||
(insert ".spine-row{display:flex;align-items:center;gap:0.75rem;")
|
||||
(insert "padding:0.4rem 0;border-bottom:1px solid var(--pico-muted-border-color)}\n")
|
||||
(insert ".spine-row:last-child{border-bottom:none}\n")
|
||||
(insert ".spine-selected{background:var(--pico-secondary-background)}\n")
|
||||
(insert ".spine-glyph{width:1.5rem;text-align:center;flex-shrink:0}\n")
|
||||
(insert ".spine-title{flex:1;min-width:0}\n")
|
||||
(insert ".spine-trail{font-size:0.85em;color:var(--pico-muted-color)}\n")
|
||||
(insert ".spine-tags{font-size:0.85em;color:var(--pico-muted-color)}\n")
|
||||
(insert ".spine-detail{margin:0 0 0 2rem;border-left:2px solid var(--pico-primary)}\n")
|
||||
(insert ".spine-detail p{margin-bottom:0.3rem}\n")
|
||||
(insert ".spine-detail footer{margin-top:0.5rem}\n")
|
||||
(insert "mark.want{background:var(--pico-mark-background);"
|
||||
"color:var(--pico-mark-color)}\n")
|
||||
(insert "mark.reading{background:#faeeda;color:#854f0b}\n")
|
||||
(insert "mark.read{background:#e1f5ee;color:#085041}\n")
|
||||
(insert "mark.abandoned{background:var(--pico-muted-color);"
|
||||
"color:var(--pico-background-color)}\n")
|
||||
(insert "article{margin-bottom:0}\n")
|
||||
(insert "article header{padding-bottom:0.5rem;margin-bottom:0.5rem}\n")
|
||||
(insert "</style>\n")
|
||||
(insert "</head>\n<body>\n")
|
||||
(insert "<main class=\"container\">\n")
|
||||
(insert "<article>\n")
|
||||
(insert (format "<header><strong>spine</strong>"
|
||||
"<small>%d books · %d reading</small></header>\n"
|
||||
total reading-count))
|
||||
;; Each group in order
|
||||
(dolist (status order)
|
||||
(let ((group-books (nreverse (gethash status grouped))))
|
||||
(when group-books
|
||||
(insert (format "<hgroup><h2>%s</h2><small>%d</small></hgroup>\n"
|
||||
status (length group-books)))
|
||||
(dolist (book group-books)
|
||||
(let ((selected (equal (plist-get book :id) selected-id)))
|
||||
(insert (format "<div class=\"spine-row%s\">\n"
|
||||
(if selected " spine-selected" "")))
|
||||
;; Format glyph (emoji)
|
||||
(let ((fmt (plist-get book :format)))
|
||||
(insert (format "<span class=\"spine-glyph\">%s</span>\n"
|
||||
(cond ((equal fmt "hardcover") "📖")
|
||||
((equal fmt "ebook") "📱")
|
||||
((equal fmt "audiobook") "🎧")
|
||||
(t "")))))
|
||||
;; Title + author
|
||||
(insert "<span class=\"spine-title\">")
|
||||
(insert (spine--h (plist-get book :title)))
|
||||
(let ((author (plist-get book :author)))
|
||||
(when author
|
||||
(insert (format " <small>%s</small>"
|
||||
(spine--h author)))))
|
||||
(insert "</span>\n")
|
||||
;; Tags
|
||||
(let ((tags (plist-get book :tags)))
|
||||
(when tags
|
||||
(insert (format "<small class=\"spine-tags\">%s</small>\n"
|
||||
(mapconcat (lambda (tag) (concat ":" tag ":"))
|
||||
tags " ")))))
|
||||
;; Recommendation trail
|
||||
(let ((rec-by (plist-get book :rec_by))
|
||||
(rec-note (plist-get book :rec_note)))
|
||||
(when (and rec-by (> (length rec-by) 0))
|
||||
(insert
|
||||
(format "<span class=\"spine-trail\">%s%s</span>\n"
|
||||
(spine--h rec-by)
|
||||
(if (and rec-note (> (length rec-note) 0))
|
||||
(concat ": " (spine--h rec-note))
|
||||
"")))))
|
||||
;; Status pill
|
||||
(let ((status (or (plist-get book :status) "WANT")))
|
||||
(insert (format "<mark class=\"%s\">%s</mark>\n"
|
||||
(downcase status) (downcase status))))
|
||||
;; Expanded detail
|
||||
(when selected
|
||||
(insert "<blockquote class=\"spine-detail\">\n")
|
||||
(dolist (note (plist-get book :notes))
|
||||
(insert (format "<p><small>[%s]</small> %s</p>\n"
|
||||
(spine--h (car note))
|
||||
(spine--h (cadr note)))))
|
||||
(let ((parts nil))
|
||||
(let ((rating (plist-get book :rating)))
|
||||
(when (and rating (> (length rating) 0))
|
||||
(push (format "Rating: %s/5" (spine--h rating)) parts)))
|
||||
(let ((isbn (plist-get book :isbn)))
|
||||
(when (and isbn (> (length isbn) 0))
|
||||
(push (format "ISBN: %s" (spine--h isbn)) parts)))
|
||||
(let ((fmt (plist-get book :format)))
|
||||
(when (and fmt (> (length fmt) 0))
|
||||
(push (format "Format: %s" (spine--h fmt)) parts)))
|
||||
(let ((added (plist-get book :added)))
|
||||
(when (and added (> (length added) 0))
|
||||
(push (format "Added: %s" (spine--h added)) parts)))
|
||||
(when parts
|
||||
(insert (format "<footer>%s</footer>\n"
|
||||
(mapconcat #'identity (nreverse parts) " · ")))))
|
||||
(insert "</blockquote>\n"))
|
||||
(insert "</div>\n"))))))
|
||||
(insert "</article>\n")
|
||||
(insert "</main>\n</body>\n</html>\n")
|
||||
(buffer-string))))
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify batch load**
|
||||
|
||||
```bash
|
||||
emacs --quick --batch --eval "(setq spine-skip-server-start t)" --load spine.el
|
||||
```
|
||||
|
||||
Expected: exits cleanly, no errors.
|
||||
|
||||
- [ ] **Step 3: Verify rendering against sample data**
|
||||
|
||||
```bash
|
||||
emacs --quick --batch \
|
||||
--eval "(setq spine-skip-server-start t spine-org-file \"$(pwd)/sample-books.org\")" \
|
||||
--load spine.el \
|
||||
--eval "(progn (message \"render-index length: %d\" (length (spine-render-index (spine-books) nil))))"
|
||||
```
|
||||
|
||||
Expected: non-zero length, no errors.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add spine.el
|
||||
git commit -m "feat: restyle index page with Pico CSS and emoji glyphs"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Smoke test end-to-end
|
||||
|
||||
**Files:**
|
||||
- No changes — verification only.
|
||||
|
||||
- [ ] **Step 1: Kill existing server and start fresh**
|
||||
|
||||
```bash
|
||||
emacsclient --socket-name=spine --eval '(kill-emacs)' 2>/dev/null
|
||||
pkill -f "emacs --quick" 2>/dev/null
|
||||
sleep 1
|
||||
emacs --quick \
|
||||
--eval "(setq spine-org-file \"$(pwd)/sample-books.org\")" \
|
||||
--load spine.el &
|
||||
EMACS_PID=$!
|
||||
sleep 5
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify Pico CSS is linked**
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8080/index | grep -c "picocss"
|
||||
```
|
||||
|
||||
Expected: `1` (the CDN `<link>` is present).
|
||||
|
||||
- [ ] **Step 3: Verify no Tabler Icons reference**
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8080/index | grep -c "tabler"
|
||||
```
|
||||
|
||||
Expected: `0`.
|
||||
|
||||
- [ ] **Step 4: Verify groups in correct order**
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8080/index | grep -oP '<h2>[^<]+'
|
||||
```
|
||||
|
||||
Expected:
|
||||
```
|
||||
WANT
|
||||
READING
|
||||
READ
|
||||
```
|
||||
No ABANDONED.
|
||||
|
||||
- [ ] **Step 5: Verify emoji glyphs and status pills**
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8080/index | grep -E "📖|📱|🎧" | wc -l
|
||||
```
|
||||
|
||||
Expected: `3` (three format glyphs present — Piranesi has no format).
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8080/index | grep -oP '<mark class="[^"]+">[^<]+'
|
||||
```
|
||||
|
||||
Expected: `want`, `reading`, `read` pills visible.
|
||||
|
||||
- [ ] **Step 6: Verify expanded detail**
|
||||
|
||||
```bash
|
||||
curl -s "http://localhost:8080/index?id=8c1e-uow" | grep -oP 'spine-detail'
|
||||
```
|
||||
|
||||
Expected: `spine-detail` present (2 occurrences — opening and closing).
|
||||
|
||||
```bash
|
||||
curl -s "http://localhost:8080/index?id=8c1e-uow" | grep -c "logline\|spine-detail"
|
||||
```
|
||||
|
||||
Expected: detail section renders.
|
||||
|
||||
- [ ] **Step 7: Verify recommendation trail**
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8080/index | grep "Priya"
|
||||
```
|
||||
|
||||
Expected: output contains `Priya: If you liked Player of Games, this one will wreck you`.
|
||||
|
||||
- [ ] **Step 8: Verify empty state**
|
||||
|
||||
```bash
|
||||
kill $EMACS_PID 2>/dev/null
|
||||
sleep 1
|
||||
emacs --quick \
|
||||
--eval "(setq spine-org-file \"/tmp/nonexistent-books.org\")" \
|
||||
--load spine.el &
|
||||
EMACS_PID=$!
|
||||
sleep 5
|
||||
curl -s http://localhost:8080/index | grep -c "No books yet"
|
||||
```
|
||||
|
||||
Expected: `1`.
|
||||
|
||||
- [ ] **Step 9: Stop server**
|
||||
|
||||
```bash
|
||||
kill $EMACS_PID 2>/dev/null
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Run ERT tests and final verification
|
||||
|
||||
**Files:**
|
||||
- No changes — verification only.
|
||||
|
||||
- [ ] **Step 1: Run full ERT test suite**
|
||||
|
||||
```bash
|
||||
emacs --quick --batch \
|
||||
--directory "$(pwd)" \
|
||||
--load test/spine-books-test.el \
|
||||
-f ert-run-tests-batch-and-exit
|
||||
```
|
||||
|
||||
Expected: all 6 tests pass.
|
||||
|
||||
- [ ] **Step 2: Verify git status**
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
```
|
||||
|
||||
Expected: no modified tracked files (only pre-existing untracked files).
|
||||
|
||||
- [ ] **Step 3: Review commit log**
|
||||
|
||||
```bash
|
||||
git log --oneline -4
|
||||
```
|
||||
|
||||
Expected: 2-3 new commits on top of previous work.
|
||||
Reference in New Issue
Block a user