nandi/gleanpublic⑂ Fork 0
⑂ b7c422e
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Fix annotation hoverUnverified

Julien Robert committed 2026-04-20T16:34:36+02:00 Browse files
b7c422e parent: 10cff72
modified docs/design.md +67 -63
@@ -12,53 +12,55 @@ Surfaces breathe through rounded geometry: pill buttons (`9999px`), `12px` card
1212
1313 **Color-block rhythm (landing page):** Cream/forest hero → white card sections → House Green (`#1E3932`) feature band with white text → cream utility zone → House Green footer.
1414
15+Logo is a stylized bee: body with horizontal stripes (like text lines on a page), semi-transparent wings that evoke open book pages, round eyes, curved antennae, and a small smile. The bee represents gleaning (collecting nectar/knowledge), social behavior (hives/communities), and reading (the striped body reads like lines of text, wings like turning pages).
16+
1517 ## 2. Color Palette
1618
1719 ### Primary Greens
1820
19-| Name | Hex | Role |
20-| --------------- | --------- | ------------------------------------------------ |
21-| Green Accent | `#00754A` | CTAs, active states, link hovers, brand accent |
22-| Green Dark | `#006241` | Headings on landing page, stronger brand moments |
23-| House Green | `#1E3932` | Dark bands, footer, feature sections |
24-| Green Uplift | `#2b5148` | Decorative accents, mid-dark green |
25-| Green Light | `#d4e9e2` | Light green utility surfaces, valid-state tints |
21+| Name | Hex | Role |
22+| ------------ | --------- | ------------------------------------------------ |
23+| Green Accent | `#00754A` | CTAs, active states, link hovers, brand accent |
24+| Green Dark | `#006241` | Headings on landing page, stronger brand moments |
25+| House Green | `#1E3932` | Dark bands, footer, feature sections |
26+| Green Uplift | `#2b5148` | Decorative accents, mid-dark green |
27+| Green Light | `#d4e9e2` | Light green utility surfaces, valid-state tints |
2628
2729 ### Dark Theme (default)
2830
29-| Token | Value | Use |
30-| ------------------ | ------------------------------ | ------------------------------------ |
31-| `--spot-bg` | `#0f1f1a` | Page background, sidebar |
32-| `--spot-surface` | `#152b24` | Card background |
33-| `--spot-hover` | `#1a362e` | Hover state, input background |
34-| `--spot-text` | `#ffffff` | Primary text |
35-| `--spot-secondary` | `rgba(255,255,255,0.70)` | Secondary/metadata text |
36-| `--spot-body` | `rgba(255,255,255,0.87)` | Body copy, article content |
37-| `--spot-muted` | `rgba(255,255,255,0.25)` | Disabled/tertiary text |
38-| `--spot-divider` | `rgba(255,255,255,0.08)` | Borders, dividers |
39-| `--spot-outline` | `rgba(255,255,255,0.20)` | Button borders, input borders |
31+| Token | Value | Use |
32+| ------------------ | ------------------------ | ----------------------------- |
33+| `--spot-bg` | `#0f1f1a` | Page background, sidebar |
34+| `--spot-surface` | `#152b24` | Card background |
35+| `--spot-hover` | `#1a362e` | Hover state, input background |
36+| `--spot-text` | `#ffffff` | Primary text |
37+| `--spot-secondary` | `rgba(255,255,255,0.70)` | Secondary/metadata text |
38+| `--spot-body` | `rgba(255,255,255,0.87)` | Body copy, article content |
39+| `--spot-muted` | `rgba(255,255,255,0.25)` | Disabled/tertiary text |
40+| `--spot-divider` | `rgba(255,255,255,0.08)` | Borders, dividers |
41+| `--spot-outline` | `rgba(255,255,255,0.20)` | Button borders, input borders |
4042
4143 ### Light Theme
4244
43-| Token | Value | Use |
44-| ------------------ | ------------------------------ | ------------------------------------ |
45-| `--spot-bg` | `#f2f0eb` | Page canvas (warm cream) |
46-| `--spot-surface` | `#ffffff` | Card background |
47-| `--spot-hover` | `#edebe9` | Hover state (ceramic) |
48-| `--spot-text` | `rgba(0,0,0,0.87)` | Primary text (warm black) |
49-| `--spot-secondary` | `rgba(0,0,0,0.58)` | Secondary/metadata text |
50-| `--spot-body` | `rgba(0,0,0,0.70)` | Body copy |
51-| `--spot-muted` | `rgba(0,0,0,0.25)` | Disabled/tertiary text |
52-| `--spot-divider` | `rgba(0,0,0,0.08)` | Borders, dividers |
53-| `--spot-outline` | `rgba(0,0,0,0.15)` | Button borders, input borders |
45+| Token | Value | Use |
46+| ------------------ | ------------------ | ----------------------------- |
47+| `--spot-bg` | `#f2f0eb` | Page canvas (warm cream) |
48+| `--spot-surface` | `#ffffff` | Card background |
49+| `--spot-hover` | `#edebe9` | Hover state (ceramic) |
50+| `--spot-text` | `rgba(0,0,0,0.87)` | Primary text (warm black) |
51+| `--spot-secondary` | `rgba(0,0,0,0.58)` | Secondary/metadata text |
52+| `--spot-body` | `rgba(0,0,0,0.70)` | Body copy |
53+| `--spot-muted` | `rgba(0,0,0,0.25)` | Disabled/tertiary text |
54+| `--spot-divider` | `rgba(0,0,0,0.08)` | Borders, dividers |
55+| `--spot-outline` | `rgba(0,0,0,0.15)` | Button borders, input borders |
5456
5557 ### Semantic
5658
57-| Name | Hex | Use |
58-| ----- | --------- | ---------------- |
59-| Red | `#c82014` | Errors, likes |
60-| Orange| `#ffa42b` | Ratings, warnings|
61-| Blue | `#539df5` | External links |
59+| Name | Hex | Use |
60+| ------ | --------- | ----------------- |
61+| Red | `#c82014` | Errors, likes |
62+| Orange | `#ffa42b` | Ratings, warnings |
63+| Blue | `#539df5` | External links |
6264
6365 ## 3. Typography
6466
@@ -66,14 +68,14 @@ Surfaces breathe through rounded geometry: pill buttons (`9999px`), `12px` card
6668
6769 **Global:** `letter-spacing: -0.01em` on body
6870
69-| Role | Size | Weight | Tailwind Class |
70-| ------------- | ---------- | ------ | ------------------ |
71-| Page title | 24px | 700 | `text-2xl font-bold` |
72-| Section title | 18px | 600 | `text-lg font-semibold` |
73-| Body | 14px | 400 | `text-sm` |
74-| Small/meta | 12px | 400 | `text-xs` |
75-| Button label | 14px | 700 | `text-sm font-bold uppercase tracking-button` |
76-| Micro | 10px | 400 | `text-[10px]` |
71+| Role | Size | Weight | Tailwind Class |
72+| ------------- | ---- | ------ | --------------------------------------------- |
73+| Page title | 24px | 700 | `text-2xl font-bold` |
74+| Section title | 18px | 600 | `text-lg font-semibold` |
75+| Body | 14px | 400 | `text-sm` |
76+| Small/meta | 12px | 400 | `text-xs` |
77+| Button label | 14px | 700 | `text-sm font-bold uppercase tracking-button` |
78+| Micro | 10px | 400 | `text-[10px]` |
7779
7880 ## 4. Components
7981
@@ -82,11 +84,13 @@ Surfaces breathe through rounded geometry: pill buttons (`9999px`), `12px` card
8284 All buttons use full-pill radius (`rounded-pill` = `9999px`).
8385
8486 **Primary Filled:**
87+
8588 ```
8689 bg-spot-green text-white rounded-pill px-5 py-2 text-sm font-bold uppercase tracking-button hover:brightness-110 transition
8790 ```
8891
8992 **Primary Outlined:**
93+
9094 ```
9195 border border-spot-outline text-spot-text rounded-pill px-4 py-1.5 text-xs font-bold uppercase tracking-button hover:border-spot-text transition
9296 ```
@@ -101,10 +105,10 @@ bg-spot-surface rounded-xl p-4 shadow-spot hover:bg-spot-hover-50 transition
101105
102106 ### Shadows
103107
104-| Token | Value | Use |
105-| -------------- | ---------------------------------------------------- | ------------ |
106-| `shadow-spot` | `0 0 0.5px rgba(0,0,0,0.14), 0 1px 1px rgba(0,0,0,0.24)` | Cards |
107-| `shadow-spot-heavy` | `0 0 6px rgba(0,0,0,0.24), 0 8px 12px rgba(0,0,0,0.14)` | Modals, hero |
108+| Token | Value | Use |
109+| ------------------- | -------------------------------------------------------- | ------------ |
110+| `shadow-spot` | `0 0 0.5px rgba(0,0,0,0.14), 0 1px 1px rgba(0,0,0,0.24)` | Cards |
111+| `shadow-spot-heavy` | `0 0 6px rgba(0,0,0,0.24), 0 8px 12px rgba(0,0,0,0.14)` | Modals, hero |
108112
109113 ### Navigation
110114
@@ -134,11 +138,11 @@ bg-spot-surface rounded-xl p-4 shadow-spot hover:bg-spot-hover-50 transition
134138
135139 ### Responsive Breakpoints
136140
137-| Name | Width | Nav behavior |
138-| ------- | --------- | ----------------------------------- |
139-| Mobile | < 768px | Bottom tab nav, stacked layouts |
140-| Tablet | 768–1023px| Bottom nav, wider gutters |
141-| Desktop | 1024px+ | Sidebar nav, 3-column grids |
141+| Name | Width | Nav behavior |
142+| ------- | ---------- | ------------------------------- |
143+| Mobile | < 768px | Bottom tab nav, stacked layouts |
144+| Tablet | 768–1023px | Bottom nav, wider gutters |
145+| Desktop | 1024px+ | Sidebar nav, 3-column grids |
142146
143147 ## 6. Tailwind Config
144148
@@ -154,15 +158,15 @@ All custom colors live under the `spot` namespace in `tailwind.config.js`. CSS v
154158
155159 ## 8. Page Structure
156160
157-| Page | Layout | Key Features |
158-| -------------- | ------------------------------ | --------------------------------------- |
159-| Index (landing)| Full-width, no sidebar | Hero with mockup, feature cols, dark band, CTA |
160-| Login | Centered card | Bluesky + Atmosphere sign-in buttons |
161-| Dashboard | 2/3 + 1/3 grid | Articles + trending/recommendations sidebar |
162-| Articles | Full-width list | Keyboard nav (j/k/o/m), mark-all-read |
163-| Article Detail | `max-w-3xl` centered | Content, like/share/read buttons, annotations |
164-| Feeds | 2/3 + 1/3 grid | Feed list with categories + add/import sidebar |
165-| Trending | Full-width list | Like/annotation counts on each article |
166-| Discover | Mixed grid | Recommendations + people + browse all |
167-| Annotations | Full-width list | Filter by article URL, load more |
168-| Profile | `max-w-2xl` centered | Avatar, stats, feeds, annotations |
161+| Page | Layout | Key Features |
162+| --------------- | ---------------------- | ---------------------------------------------- |
163+| Index (landing) | Full-width, no sidebar | Hero with mockup, feature cols, dark band, CTA |
164+| Login | Centered card | Bluesky + Atmosphere sign-in buttons |
165+| Dashboard | 2/3 + 1/3 grid | Articles + trending/recommendations sidebar |
166+| Articles | Full-width list | Keyboard nav (j/k/o/m), mark-all-read |
167+| Article Detail | `max-w-3xl` centered | Content, like/share/read buttons, annotations |
168+| Feeds | 2/3 + 1/3 grid | Feed list with categories + add/import sidebar |
169+| Trending | Full-width list | Like/annotation counts on each article |
170+| Discover | Mixed grid | Recommendations + people + browse all |
171+| Annotations | Full-width list | Filter by article URL, load more |
172+| Profile | `max-w-2xl` centered | Avatar, stats, feeds, annotations |
@@ -12,53 +12,55 @@ Surfaces breathe through rounded geometry: pill buttons (`9999px`), `12px` card
12 12
13 **Color-block rhythm (landing page):** Cream/forest hero → white card sections → House Green (`#1E3932`) feature band with white text → cream utility zone → House Green footer.13 **Color-block rhythm (landing page):** Cream/forest hero → white card sections → House Green (`#1E3932`) feature band with white text → cream utility zone → House Green footer.
14 14
15+Logo is a stylized bee: body with horizontal stripes (like text lines on a page), semi-transparent wings that evoke open book pages, round eyes, curved antennae, and a small smile. The bee represents gleaning (collecting nectar/knowledge), social behavior (hives/communities), and reading (the striped body reads like lines of text, wings like turning pages).
16+
15 ## 2. Color Palette17 ## 2. Color Palette
16 18
17 ### Primary Greens19 ### Primary Greens
18 20
19-| Name | Hex | Role |21+| Name | Hex | Role |
20-| --------------- | --------- | ------------------------------------------------ |22+| ------------ | --------- | ------------------------------------------------ |
21-| Green Accent | `#00754A` | CTAs, active states, link hovers, brand accent |23+| Green Accent | `#00754A` | CTAs, active states, link hovers, brand accent |
22-| Green Dark | `#006241` | Headings on landing page, stronger brand moments |24+| Green Dark | `#006241` | Headings on landing page, stronger brand moments |
23-| House Green | `#1E3932` | Dark bands, footer, feature sections |25+| House Green | `#1E3932` | Dark bands, footer, feature sections |
24-| Green Uplift | `#2b5148` | Decorative accents, mid-dark green |26+| Green Uplift | `#2b5148` | Decorative accents, mid-dark green |
25-| Green Light | `#d4e9e2` | Light green utility surfaces, valid-state tints |27+| Green Light | `#d4e9e2` | Light green utility surfaces, valid-state tints |
26 28
27 ### Dark Theme (default)29 ### Dark Theme (default)
28 30
29-| Token | Value | Use |31+| Token | Value | Use |
30-| ------------------ | ------------------------------ | ------------------------------------ |32+| ------------------ | ------------------------ | ----------------------------- |
31-| `--spot-bg` | `#0f1f1a` | Page background, sidebar |33+| `--spot-bg` | `#0f1f1a` | Page background, sidebar |
32-| `--spot-surface` | `#152b24` | Card background |34+| `--spot-surface` | `#152b24` | Card background |
33-| `--spot-hover` | `#1a362e` | Hover state, input background |35+| `--spot-hover` | `#1a362e` | Hover state, input background |
34-| `--spot-text` | `#ffffff` | Primary text |36+| `--spot-text` | `#ffffff` | Primary text |
35-| `--spot-secondary` | `rgba(255,255,255,0.70)` | Secondary/metadata text |37+| `--spot-secondary` | `rgba(255,255,255,0.70)` | Secondary/metadata text |
36-| `--spot-body` | `rgba(255,255,255,0.87)` | Body copy, article content |38+| `--spot-body` | `rgba(255,255,255,0.87)` | Body copy, article content |
37-| `--spot-muted` | `rgba(255,255,255,0.25)` | Disabled/tertiary text |39+| `--spot-muted` | `rgba(255,255,255,0.25)` | Disabled/tertiary text |
38-| `--spot-divider` | `rgba(255,255,255,0.08)` | Borders, dividers |40+| `--spot-divider` | `rgba(255,255,255,0.08)` | Borders, dividers |
39-| `--spot-outline` | `rgba(255,255,255,0.20)` | Button borders, input borders |41+| `--spot-outline` | `rgba(255,255,255,0.20)` | Button borders, input borders |
40 42
41 ### Light Theme43 ### Light Theme
42 44
43-| Token | Value | Use |45+| Token | Value | Use |
44-| ------------------ | ------------------------------ | ------------------------------------ |46+| ------------------ | ------------------ | ----------------------------- |
45-| `--spot-bg` | `#f2f0eb` | Page canvas (warm cream) |47+| `--spot-bg` | `#f2f0eb` | Page canvas (warm cream) |
46-| `--spot-surface` | `#ffffff` | Card background |48+| `--spot-surface` | `#ffffff` | Card background |
47-| `--spot-hover` | `#edebe9` | Hover state (ceramic) |49+| `--spot-hover` | `#edebe9` | Hover state (ceramic) |
48-| `--spot-text` | `rgba(0,0,0,0.87)` | Primary text (warm black) |50+| `--spot-text` | `rgba(0,0,0,0.87)` | Primary text (warm black) |
49-| `--spot-secondary` | `rgba(0,0,0,0.58)` | Secondary/metadata text |51+| `--spot-secondary` | `rgba(0,0,0,0.58)` | Secondary/metadata text |
50-| `--spot-body` | `rgba(0,0,0,0.70)` | Body copy |52+| `--spot-body` | `rgba(0,0,0,0.70)` | Body copy |
51-| `--spot-muted` | `rgba(0,0,0,0.25)` | Disabled/tertiary text |53+| `--spot-muted` | `rgba(0,0,0,0.25)` | Disabled/tertiary text |
52-| `--spot-divider` | `rgba(0,0,0,0.08)` | Borders, dividers |54+| `--spot-divider` | `rgba(0,0,0,0.08)` | Borders, dividers |
53-| `--spot-outline` | `rgba(0,0,0,0.15)` | Button borders, input borders |55+| `--spot-outline` | `rgba(0,0,0,0.15)` | Button borders, input borders |
54 56
55 ### Semantic57 ### Semantic
56 58
57-| Name | Hex | Use |59+| Name | Hex | Use |
58-| ----- | --------- | ---------------- |60+| ------ | --------- | ----------------- |
59-| Red | `#c82014` | Errors, likes |61+| Red | `#c82014` | Errors, likes |
60-| Orange| `#ffa42b` | Ratings, warnings|62+| Orange | `#ffa42b` | Ratings, warnings |
61-| Blue | `#539df5` | External links |63+| Blue | `#539df5` | External links |
62 64
63 ## 3. Typography65 ## 3. Typography
64 66
@@ -66,14 +68,14 @@ Surfaces breathe through rounded geometry: pill buttons (`9999px`), `12px` card
66 68
67 **Global:** `letter-spacing: -0.01em` on body69 **Global:** `letter-spacing: -0.01em` on body
68 70
69-| Role | Size | Weight | Tailwind Class |71+| Role | Size | Weight | Tailwind Class |
70-| ------------- | ---------- | ------ | ------------------ |72+| ------------- | ---- | ------ | --------------------------------------------- |
71-| Page title | 24px | 700 | `text-2xl font-bold` |73+| Page title | 24px | 700 | `text-2xl font-bold` |
72-| Section title | 18px | 600 | `text-lg font-semibold` |74+| Section title | 18px | 600 | `text-lg font-semibold` |
73-| Body | 14px | 400 | `text-sm` |75+| Body | 14px | 400 | `text-sm` |
74-| Small/meta | 12px | 400 | `text-xs` |76+| Small/meta | 12px | 400 | `text-xs` |
75-| Button label | 14px | 700 | `text-sm font-bold uppercase tracking-button` |77+| Button label | 14px | 700 | `text-sm font-bold uppercase tracking-button` |
76-| Micro | 10px | 400 | `text-[10px]` |78+| Micro | 10px | 400 | `text-[10px]` |
77 79
78 ## 4. Components80 ## 4. Components
79 81
@@ -82,11 +84,13 @@ Surfaces breathe through rounded geometry: pill buttons (`9999px`), `12px` card
82 All buttons use full-pill radius (`rounded-pill` = `9999px`).84 All buttons use full-pill radius (`rounded-pill` = `9999px`).
83 85
84 **Primary Filled:**86 **Primary Filled:**
87+
85 ```88 ```
86 bg-spot-green text-white rounded-pill px-5 py-2 text-sm font-bold uppercase tracking-button hover:brightness-110 transition89 bg-spot-green text-white rounded-pill px-5 py-2 text-sm font-bold uppercase tracking-button hover:brightness-110 transition
87 ```90 ```
88 91
89 **Primary Outlined:**92 **Primary Outlined:**
93+
90 ```94 ```
91 border border-spot-outline text-spot-text rounded-pill px-4 py-1.5 text-xs font-bold uppercase tracking-button hover:border-spot-text transition95 border border-spot-outline text-spot-text rounded-pill px-4 py-1.5 text-xs font-bold uppercase tracking-button hover:border-spot-text transition
92 ```96 ```
@@ -101,10 +105,10 @@ bg-spot-surface rounded-xl p-4 shadow-spot hover:bg-spot-hover-50 transition
101 105
102 ### Shadows106 ### Shadows
103 107
104-| Token | Value | Use |108+| Token | Value | Use |
105-| -------------- | ---------------------------------------------------- | ------------ |109+| ------------------- | -------------------------------------------------------- | ------------ |
106-| `shadow-spot` | `0 0 0.5px rgba(0,0,0,0.14), 0 1px 1px rgba(0,0,0,0.24)` | Cards |110+| `shadow-spot` | `0 0 0.5px rgba(0,0,0,0.14), 0 1px 1px rgba(0,0,0,0.24)` | Cards |
107-| `shadow-spot-heavy` | `0 0 6px rgba(0,0,0,0.24), 0 8px 12px rgba(0,0,0,0.14)` | Modals, hero |111+| `shadow-spot-heavy` | `0 0 6px rgba(0,0,0,0.24), 0 8px 12px rgba(0,0,0,0.14)` | Modals, hero |
108 112
109 ### Navigation113 ### Navigation
110 114
@@ -134,11 +138,11 @@ bg-spot-surface rounded-xl p-4 shadow-spot hover:bg-spot-hover-50 transition
134 138
135 ### Responsive Breakpoints139 ### Responsive Breakpoints
136 140
137-| Name | Width | Nav behavior |141+| Name | Width | Nav behavior |
138-| ------- | --------- | ----------------------------------- |142+| ------- | ---------- | ------------------------------- |
139-| Mobile | < 768px | Bottom tab nav, stacked layouts |143+| Mobile | < 768px | Bottom tab nav, stacked layouts |
140-| Tablet | 768–1023px| Bottom nav, wider gutters |144+| Tablet | 768–1023px | Bottom nav, wider gutters |
141-| Desktop | 1024px+ | Sidebar nav, 3-column grids |145+| Desktop | 1024px+ | Sidebar nav, 3-column grids |
142 146
143 ## 6. Tailwind Config147 ## 6. Tailwind Config
144 148
@@ -154,15 +158,15 @@ All custom colors live under the `spot` namespace in `tailwind.config.js`. CSS v
154 158
155 ## 8. Page Structure159 ## 8. Page Structure
156 160
157-| Page | Layout | Key Features |161+| Page | Layout | Key Features |
158-| -------------- | ------------------------------ | --------------------------------------- |162+| --------------- | ---------------------- | ---------------------------------------------- |
159-| Index (landing)| Full-width, no sidebar | Hero with mockup, feature cols, dark band, CTA |163+| Index (landing) | Full-width, no sidebar | Hero with mockup, feature cols, dark band, CTA |
160-| Login | Centered card | Bluesky + Atmosphere sign-in buttons |164+| Login | Centered card | Bluesky + Atmosphere sign-in buttons |
161-| Dashboard | 2/3 + 1/3 grid | Articles + trending/recommendations sidebar |165+| Dashboard | 2/3 + 1/3 grid | Articles + trending/recommendations sidebar |
162-| Articles | Full-width list | Keyboard nav (j/k/o/m), mark-all-read |166+| Articles | Full-width list | Keyboard nav (j/k/o/m), mark-all-read |
163-| Article Detail | `max-w-3xl` centered | Content, like/share/read buttons, annotations |167+| Article Detail | `max-w-3xl` centered | Content, like/share/read buttons, annotations |
164-| Feeds | 2/3 + 1/3 grid | Feed list with categories + add/import sidebar |168+| Feeds | 2/3 + 1/3 grid | Feed list with categories + add/import sidebar |
165-| Trending | Full-width list | Like/annotation counts on each article |169+| Trending | Full-width list | Like/annotation counts on each article |
166-| Discover | Mixed grid | Recommendations + people + browse all |170+| Discover | Mixed grid | Recommendations + people + browse all |
167-| Annotations | Full-width list | Filter by article URL, load more |171+| Annotations | Full-width list | Filter by article URL, load more |
168-| Profile | `max-w-2xl` centered | Avatar, stats, feeds, annotations |172+| Profile | `max-w-2xl` centered | Avatar, stats, feeds, annotations |
modified internal/tmpl/article_detail.html +33 -9
@@ -66,23 +66,25 @@
6666 <section>
6767 <h2 class="text-lg font-semibold text-spot-text mb-4">Annotations</h2>
6868
69- <form hx-post="/library/create" hx-target="#annotations-list" hx-swap="beforeend"
69+ <form id="annotation-form" hx-post="/library/create" hx-target="#annotations-list" hx-swap="beforeend"
7070 hx-on::after-request="this.reset()"
7171 class="bg-spot-surface rounded-xl shadow-spot p-4 mb-4 space-y-3">
7272 {{csrfInput .CSRFToken}}
7373 <input type="hidden" name="feed_url" value="{{.Article.FeedURL}}">
7474 <input type="hidden" name="article_url" value="{{if .Article.URL.Valid}}{{.Article.URL.String}}{{end}}">
75+ <div id="quote-highlight" class="hidden mb-2">
76+ <blockquote id="quote-preview" class="border-l-2 border-spot-green pl-3 text-sm text-spot-secondary italic"></blockquote>
77+ </div>
78+ <input type="hidden" name="quote" id="quote-input">
7579 <div>
7680 <textarea name="note" rows="2" placeholder="Add a note..."
7781 class="w-full bg-spot-hover text-spot-text rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder resize-none"></textarea>
7882 </div>
7983 <div class="flex gap-2">
80- <input type="text" name="quote" placeholder="Quote (optional)"
81- class="flex-1 bg-spot-hover text-spot-text rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">
8284 <input type="text" name="tags" placeholder="Tags (comma separated)"
8385 class="flex-1 bg-spot-hover text-spot-text rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">
86+ <button type="submit" class="bg-spot-green text-white rounded-pill px-5 py-2 text-sm font-bold uppercase tracking-button hover:brightness-110 transition">Annotate</button>
8487 </div>
85- <button type="submit" class="bg-spot-green text-white rounded-pill px-5 py-2 text-sm font-bold uppercase tracking-button hover:brightness-110 transition">Annotate</button>
8688 </form>
8789
8890 <div id="annotations-list" class="space-y-3">
@@ -97,11 +99,33 @@
9799
98100 <script>
99101 (function() {
100- var readScrollPos = 0;
101- var articleBody = document.querySelector('.article-body');
102- if (articleBody) {
103- readScrollPos = window.scrollY;
104- }
102+ var form = document.getElementById('annotation-form');
103+ var quoteInput = document.getElementById('quote-input');
104+ var quotePreview = document.getElementById('quote-preview');
105+ var quoteHighlight = document.getElementById('quote-highlight');
106+
107+ document.addEventListener('mouseup', function(e) {
108+ var sel = window.getSelection();
109+ var text = sel.toString().trim();
110+ if (!text) return;
111+
112+ var body = e.target.closest('.article-body');
113+ if (!body) return;
114+
115+ if (text.length > 1000) text = text.substring(0, 1000);
116+ quoteInput.value = text;
117+ quotePreview.textContent = text;
118+ quoteHighlight.classList.remove('hidden');
119+ form.scrollIntoView({ behavior: 'smooth', block: 'center' });
120+ form.querySelector('textarea[name="note"]').focus();
121+ });
122+
123+ var origReset = form.reset.bind(form);
124+ form.reset = function() {
125+ origReset();
126+ quoteInput.value = '';
127+ quoteHighlight.classList.add('hidden');
128+ };
105129
106130 document.addEventListener('keydown', function(e) {
107131 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
@@ -66,23 +66,25 @@
66 <section>66 <section>
67 <h2 class="text-lg font-semibold text-spot-text mb-4">Annotations</h2>67 <h2 class="text-lg font-semibold text-spot-text mb-4">Annotations</h2>
68 68
69- <form hx-post="/library/create" hx-target="#annotations-list" hx-swap="beforeend"69+ <form id="annotation-form" hx-post="/library/create" hx-target="#annotations-list" hx-swap="beforeend"
70 hx-on::after-request="this.reset()"70 hx-on::after-request="this.reset()"
71 class="bg-spot-surface rounded-xl shadow-spot p-4 mb-4 space-y-3">71 class="bg-spot-surface rounded-xl shadow-spot p-4 mb-4 space-y-3">
72 {{csrfInput .CSRFToken}}72 {{csrfInput .CSRFToken}}
73 <input type="hidden" name="feed_url" value="{{.Article.FeedURL}}">73 <input type="hidden" name="feed_url" value="{{.Article.FeedURL}}">
74 <input type="hidden" name="article_url" value="{{if .Article.URL.Valid}}{{.Article.URL.String}}{{end}}">74 <input type="hidden" name="article_url" value="{{if .Article.URL.Valid}}{{.Article.URL.String}}{{end}}">
75+ <div id="quote-highlight" class="hidden mb-2">
76+ <blockquote id="quote-preview" class="border-l-2 border-spot-green pl-3 text-sm text-spot-secondary italic"></blockquote>
77+ </div>
78+ <input type="hidden" name="quote" id="quote-input">
75 <div>79 <div>
76 <textarea name="note" rows="2" placeholder="Add a note..."80 <textarea name="note" rows="2" placeholder="Add a note..."
77 class="w-full bg-spot-hover text-spot-text rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder resize-none"></textarea>81 class="w-full bg-spot-hover text-spot-text rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder resize-none"></textarea>
78 </div>82 </div>
79 <div class="flex gap-2">83 <div class="flex gap-2">
80- <input type="text" name="quote" placeholder="Quote (optional)"
81- class="flex-1 bg-spot-hover text-spot-text rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">
82 <input type="text" name="tags" placeholder="Tags (comma separated)"84 <input type="text" name="tags" placeholder="Tags (comma separated)"
83 class="flex-1 bg-spot-hover text-spot-text rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">85 class="flex-1 bg-spot-hover text-spot-text rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">
86+ <button type="submit" class="bg-spot-green text-white rounded-pill px-5 py-2 text-sm font-bold uppercase tracking-button hover:brightness-110 transition">Annotate</button>
84 </div>87 </div>
85- <button type="submit" class="bg-spot-green text-white rounded-pill px-5 py-2 text-sm font-bold uppercase tracking-button hover:brightness-110 transition">Annotate</button>
86 </form>88 </form>
87 89
88 <div id="annotations-list" class="space-y-3">90 <div id="annotations-list" class="space-y-3">
@@ -97,11 +99,33 @@
97 99
98 <script>100 <script>
99 (function() {101 (function() {
100- var readScrollPos = 0;102+ var form = document.getElementById('annotation-form');
101- var articleBody = document.querySelector('.article-body');103+ var quoteInput = document.getElementById('quote-input');
102- if (articleBody) {104+ var quotePreview = document.getElementById('quote-preview');
103- readScrollPos = window.scrollY;105+ var quoteHighlight = document.getElementById('quote-highlight');
104- }106+
107+ document.addEventListener('mouseup', function(e) {
108+ var sel = window.getSelection();
109+ var text = sel.toString().trim();
110+ if (!text) return;
111+
112+ var body = e.target.closest('.article-body');
113+ if (!body) return;
114+
115+ if (text.length > 1000) text = text.substring(0, 1000);
116+ quoteInput.value = text;
117+ quotePreview.textContent = text;
118+ quoteHighlight.classList.remove('hidden');
119+ form.scrollIntoView({ behavior: 'smooth', block: 'center' });
120+ form.querySelector('textarea[name="note"]').focus();
121+ });
122+
123+ var origReset = form.reset.bind(form);
124+ form.reset = function() {
125+ origReset();
126+ quoteInput.value = '';
127+ quoteHighlight.classList.add('hidden');
128+ };
105 129
106 document.addEventListener('keydown', function(e) {130 document.addEventListener('keydown', function(e) {
107 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;131 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
modified internal/tmpl/base.html +27 -7
@@ -220,15 +220,35 @@
220220 }
221221 updateThemeIcons(document.documentElement.getAttribute('data-theme') || 'dark');
222222
223- function toggleAnnotate(btn) {
224- var card = btn.closest('article');
225- var form = card.querySelector('.annotate-form');
226- form.classList.toggle('hidden');
227- if (!form.classList.contains('hidden')) {
228- form.querySelector('input[name="quote"]').focus();
229- }
223+ function closeAnnotate(el) {
224+ var form = el.closest('.annotate-form');
225+ form.classList.add('hidden');
226+ form.querySelector('form').reset();
230227 }
231228
229+ function openAnnotate(article, quote) {
230+ var form = article.querySelector('.annotate-form');
231+ var quoteInput = form.querySelector('input[name="quote"]');
232+ if (quote) quoteInput.value = quote;
233+ form.classList.remove('hidden');
234+ (quote ? form.querySelector('input[name="note"]') : quoteInput).focus();
235+ }
236+
237+ document.addEventListener('mouseup', function(e) {
238+ var sel = window.getSelection();
239+ var text = sel.toString().trim();
240+ if (!text) return;
241+
242+ var article = e.target.closest('article[data-article-id]');
243+ if (!article) return;
244+
245+ var form = article.querySelector('.annotate-form');
246+ if (form && !form.classList.contains('hidden')) return;
247+
248+ if (text.length > 500) text = text.substring(0, 500);
249+ openAnnotate(article, text);
250+ });
251+
232252 document.addEventListener('keydown', function(e) {
233253 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
234254 if (e.key === 'g') window.location.href = '/dashboard';
@@ -220,15 +220,35 @@
220 }220 }
221 updateThemeIcons(document.documentElement.getAttribute('data-theme') || 'dark');221 updateThemeIcons(document.documentElement.getAttribute('data-theme') || 'dark');
222 222
223- function toggleAnnotate(btn) {223+ function closeAnnotate(el) {
224- var card = btn.closest('article');224+ var form = el.closest('.annotate-form');
225- var form = card.querySelector('.annotate-form');225+ form.classList.add('hidden');
226- form.classList.toggle('hidden');226+ form.querySelector('form').reset();
227- if (!form.classList.contains('hidden')) {
228- form.querySelector('input[name="quote"]').focus();
229- }
230 }227 }
231 228
229+ function openAnnotate(article, quote) {
230+ var form = article.querySelector('.annotate-form');
231+ var quoteInput = form.querySelector('input[name="quote"]');
232+ if (quote) quoteInput.value = quote;
233+ form.classList.remove('hidden');
234+ (quote ? form.querySelector('input[name="note"]') : quoteInput).focus();
235+ }
236+
237+ document.addEventListener('mouseup', function(e) {
238+ var sel = window.getSelection();
239+ var text = sel.toString().trim();
240+ if (!text) return;
241+
242+ var article = e.target.closest('article[data-article-id]');
243+ if (!article) return;
244+
245+ var form = article.querySelector('.annotate-form');
246+ if (form && !form.classList.contains('hidden')) return;
247+
248+ if (text.length > 500) text = text.substring(0, 500);
249+ openAnnotate(article, text);
250+ });
251+
232 document.addEventListener('keydown', function(e) {252 document.addEventListener('keydown', function(e) {
233 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;253 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
234 if (e.key === 'g') window.location.href = '/dashboard';254 if (e.key === 'g') window.location.href = '/dashboard';
modified internal/tmpl/partials/article-card.html +7 -9
@@ -1,5 +1,5 @@
11 {{define "article-card.html"}}
2-<article data-article-id="{{.ID}}" class="group bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition shadow-spot relative">
2+<article data-article-id="{{.ID}}" data-feed-url="{{.FeedURL}}" data-article-url="{{if .URL.Valid}}{{.URL.String}}{{end}}" class="bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition shadow-spot relative">
33 <div class="flex items-start justify-between gap-4">
44 <div class="min-w-0 flex-1">
55 <a href="/articles/{{.ID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a>
@@ -14,21 +14,19 @@
1414 </div>
1515 <div class="flex flex-col items-center gap-1 shrink-0">
1616 {{template "like-button.html" .}}
17- <button type="button" onclick="toggleAnnotate(this)" class="opacity-0 group-hover:opacity-100 text-spot-muted hover:text-spot-green text-xs transition mt-1" title="Annotate">
18- <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"/></svg>
19- </button>
2017 </div>
2118 </div>
2219 <div class="annotate-form hidden mt-3 pt-3 border-t border-spot-divider">
23- <form hx-post="/library/create" hx-swap="none" hx-on::after-request="toggleAnnotate(this.closest('.annotate-form').previousElementSibling)">
20+ <form hx-post="/library/create" hx-swap="none" hx-on::after-request="closeAnnotate(this)" class="space-y-2">
2421 <input type="hidden" name="feed_url" value="{{.FeedURL}}">
2522 <input type="hidden" name="article_url" value="{{if .URL.Valid}}{{.URL.String}}{{end}}">
23+ <input type="text" name="quote" placeholder="Quote a passage..."
24+ class="w-full bg-spot-hover text-spot-text rounded-pill px-4 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">
2625 <div class="flex gap-2">
27- <input type="text" name="quote" placeholder="Quote a passage..."
26+ <input type="text" name="note" placeholder="Add a note..."
2827 class="flex-1 bg-spot-hover text-spot-text rounded-pill px-4 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">
29- <input type="text" name="note" placeholder="Note (optional)"
30- class="flex-1 bg-spot-hover text-spot-text rounded-pill px-4 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">
31- <button type="submit" class="bg-spot-green text-white rounded-pill px-4 py-1.5 text-xs font-bold uppercase tracking-button hover:brightness-110 transition">Save</button>
28+ <button type="submit" class="bg-spot-green text-white rounded-pill px-4 py-1.5 text-xs font-bold uppercase tracking-button hover:brightness-110 transition">Annotate</button>
29+ <button type="button" onclick="closeAnnotate(this)" class="text-spot-secondary hover:text-spot-text px-2 text-sm transition">&times;</button>
3230 </div>
3331 </form>
3432 </div>
@@ -1,5 +1,5 @@
1 {{define "article-card.html"}}1 {{define "article-card.html"}}
2-<article data-article-id="{{.ID}}" class="group bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition shadow-spot relative">2+<article data-article-id="{{.ID}}" data-feed-url="{{.FeedURL}}" data-article-url="{{if .URL.Valid}}{{.URL.String}}{{end}}" class="bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition shadow-spot relative">
3 <div class="flex items-start justify-between gap-4">3 <div class="flex items-start justify-between gap-4">
4 <div class="min-w-0 flex-1">4 <div class="min-w-0 flex-1">
5 <a href="/articles/{{.ID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a>5 <a href="/articles/{{.ID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a>
@@ -14,21 +14,19 @@
14 </div>14 </div>
15 <div class="flex flex-col items-center gap-1 shrink-0">15 <div class="flex flex-col items-center gap-1 shrink-0">
16 {{template "like-button.html" .}}16 {{template "like-button.html" .}}
17- <button type="button" onclick="toggleAnnotate(this)" class="opacity-0 group-hover:opacity-100 text-spot-muted hover:text-spot-green text-xs transition mt-1" title="Annotate">
18- <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"/></svg>
19- </button>
20 </div>17 </div>
21 </div>18 </div>
22 <div class="annotate-form hidden mt-3 pt-3 border-t border-spot-divider">19 <div class="annotate-form hidden mt-3 pt-3 border-t border-spot-divider">
23- <form hx-post="/library/create" hx-swap="none" hx-on::after-request="toggleAnnotate(this.closest('.annotate-form').previousElementSibling)">20+ <form hx-post="/library/create" hx-swap="none" hx-on::after-request="closeAnnotate(this)" class="space-y-2">
24 <input type="hidden" name="feed_url" value="{{.FeedURL}}">21 <input type="hidden" name="feed_url" value="{{.FeedURL}}">
25 <input type="hidden" name="article_url" value="{{if .URL.Valid}}{{.URL.String}}{{end}}">22 <input type="hidden" name="article_url" value="{{if .URL.Valid}}{{.URL.String}}{{end}}">
23+ <input type="text" name="quote" placeholder="Quote a passage..."
24+ class="w-full bg-spot-hover text-spot-text rounded-pill px-4 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">
26 <div class="flex gap-2">25 <div class="flex gap-2">
27- <input type="text" name="quote" placeholder="Quote a passage..."26+ <input type="text" name="note" placeholder="Add a note..."
28 class="flex-1 bg-spot-hover text-spot-text rounded-pill px-4 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">27 class="flex-1 bg-spot-hover text-spot-text rounded-pill px-4 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">
29- <input type="text" name="note" placeholder="Note (optional)"28+ <button type="submit" class="bg-spot-green text-white rounded-pill px-4 py-1.5 text-xs font-bold uppercase tracking-button hover:brightness-110 transition">Annotate</button>
30- class="flex-1 bg-spot-hover text-spot-text rounded-pill px-4 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">29+ <button type="button" onclick="closeAnnotate(this)" class="text-spot-secondary hover:text-spot-text px-2 text-sm transition">&times;</button>
31- <button type="submit" class="bg-spot-green text-white rounded-pill px-4 py-1.5 text-xs font-bold uppercase tracking-button hover:brightness-110 transition">Save</button>
32 </div>30 </div>
33 </form>31 </form>
34 </div>32 </div>