Merge remote-tracking branch 'theme-upstream/main'

This commit is contained in:
svemagie
2026-03-07 23:22:52 +01:00
95 changed files with 3086 additions and 1066 deletions

View File

@@ -29,19 +29,32 @@
## Domain Colors
Every section of the site has a color identity. On domain pages, links, hover states, and card borders use the domain color instead of amber.
Every post type and section has its own unique color identity. On collection pages, sparklines, card borders, icons, labels, hover states, and permalink links all use the domain color. No two post types share the same color.
### Domain Map (complete — every page accounted for)
### Post Type Colors (each unique — no sharing)
| Domain | Tailwind color | Light text | Dark text | Pages |
|--------|---------------|------------|-----------|-------|
| **Writing** | amber (= accent) | amber-700 | amber-400 | `/blog/`, `/articles/`, `/notes/`, `/bookmarks/`, `/digest/`, `/news/`, `/categories/`, individual posts |
| **Social** | rose | rose-600 | rose-400 | `/likes/`, `/replies/`, `/reposts/`, `/interactions/` |
| Post Type | Tailwind color | Light text | Dark text | Icon | Pages |
|-----------|---------------|------------|-----------|------|-------|
| **Articles** | indigo | indigo-600 | indigo-400 | document | `/articles/` |
| **Notes** | teal | teal-600 | teal-400 | chat bubble | `/notes/` |
| **Bookmarks** | amber | amber-600 | amber-400 | bookmark | `/bookmarks/` |
| **Likes** | red | red-600 | red-400 | heart (filled) | `/likes/` |
| **Replies** | sky | sky-600 | sky-400 | reply arrow | `/replies/` |
| **Reposts** | green | green-600 | green-400 | refresh arrows | `/reposts/` |
| **Photos** | purple | purple-600 | purple-400 | camera | `/photos/` |
Each color is applied consistently across: sparkline wrapper, card `border-l`, SVG icon, label text, title hover, and permalink link.
### Section Colors (non-post-type pages)
| Section | Tailwind color | Light text | Dark text | Pages |
|---------|---------------|------------|-----------|-------|
| **Blog** (mixed) | amber | amber-600 | amber-400 | `/blog/` (sparkline only — individual cards use their post-type color) |
| **Code** | emerald | emerald-600 | emerald-400 | `/github/`, `/github/starred/` |
| **Music** | purple | purple-600 | purple-400 | `/funkwhale/`, `/listening/` |
| **Video** | red | red-600 | red-400 | `/youtube/` |
| **Reading** | orange | orange-600 | orange-400 | `/blogroll/`, `/podroll/`, `/readlater/` |
| **Neutral** | — (use accent) | — | — | `/` (home), `/about/`, `/cv/`, `/slashes/`, `/search/`, `/changelog/`, `/404` |
| **Neutral** | accent (amber) | — | — | `/` (home), `/about/`, `/cv/`, `/slashes/`, `/search/`, `/changelog/`, `/404` |
### Brand Colors (hardcoded hex — not domain colors)
@@ -93,60 +106,294 @@ Gradients are NOT used for:
| Role | Style | Usage |
|------|-------|-------|
| Headlines | Inter, `font-bold` | Page titles, section headings |
| Body | Inter, normal weight | Paragraphs, descriptions |
| Page titles | Inter, `text-2xl sm:text-3xl font-bold` | Main page headings |
| Section headings | Inter, `text-xl sm:text-2xl font-bold` | Widget titles, section headers |
| Subheadings | Inter, `text-lg font-semibold` | Card titles, list item titles |
| Body | Inter, `text-sm` or `text-base` | Paragraphs, descriptions |
| Labels | Inter, `font-medium` or `font-semibold` | Badges, nav items, metadata labels |
| **Dates/timestamps** | **`font-mono text-sm`** | Every `<time>` element, stat numbers, version numbers |
| Code | `font-mono` | Commit SHAs, code blocks, technical identifiers |
| Small text | `text-xs` | Metadata, secondary info, captions |
### Date treatment rule
Every rendered date (via `dateDisplay` or `date()` filter) gets `font-mono`. This adds technical texture throughout the site — like timestamps in a log.
### Weight scale
| Weight | Class | Frequency | Usage |
|--------|-------|-----------|-------|
| 400 | (default) | Body text | Paragraphs, descriptions |
| 500 | `font-medium` | 146x | Labels, metadata, nav items |
| 600 | `font-semibold` | 100x | Subheadings, emphasis |
| 700 | `font-bold` | 138x | Page titles, section headings |
## Spacing
Base: 4px (Tailwind default rem scale).
Extracted dominant patterns:
- Component internal: `p-4` (cards), `p-3` (compact), `p-5` (featured)
- Gaps: `gap-2` (tight lists), `gap-3` (standard), `gap-4` (spacious)
- Section separation: `mb-6` to `mb-8`
- Micro: `px-2 py-0.5` (badges), `px-3 py-1.5` (pills)
### Spacing scale (by frequency)
| Token | px | Frequency | Primary usage |
|-------|-----|-----------|---------------|
| `0.5` | 2px | 62x | Micro gaps (badge padding-y, icon margins) |
| `1` | 4px | 150x | Tight internal spacing |
| `1.5` | 6px | 45x | Button padding-y, small gaps |
| `2` | 8px | 350x+ | Standard small spacing (px, py, gap, m) |
| `3` | 12px | 180x | Standard medium spacing |
| `4` | 16px | 200x+ | Card padding, section gaps |
| `5` | 20px | 30x | Featured card padding |
| `6` | 24px | 80x | Section margins |
| `8` | 32px | 40x | Large section separation |
| `10` | 40px | 8x | Page-level vertical rhythm |
| `12` | 48px | 5x | Major section breaks |
### Common spacing patterns
| Pattern | Classes | Where |
|---------|---------|-------|
| Card padding | `p-4` | Standard cards, widgets |
| Compact padding | `p-3` | List items, tight cards |
| Featured padding | `p-5` | Hero cards, featured items |
| Tight list gap | `gap-2` | Inline elements, tag lists |
| Standard gap | `gap-3` | Card grids, form elements |
| Spacious gap | `gap-4` | Section-level grids |
| Section break | `mb-6` to `mb-8` | Between page sections |
| Badge padding | `px-2 py-0.5` | Small badges, pills |
| Pill padding | `px-3 py-1.5` | Larger pills, filter buttons |
## Border Radius
| Element | Radius |
|---------|--------|
| Cards, inputs, buttons | `rounded-lg` (dominant: 124×) |
| Avatars, status dots, badges | `rounded-full` (89×) |
| Featured/hero cards | `rounded-xl` (21×) |
| Now-playing sections | `rounded-xl sm:rounded-2xl` |
| Element | Radius | Frequency |
|---------|--------|-----------|
| Cards, inputs, buttons | `rounded-lg` | 154x (dominant) |
| Avatars, status dots, badges | `rounded-full` | 134x |
| Featured/hero cards | `rounded-xl` | 23x |
| Now-playing sections | `rounded-xl sm:rounded-2xl` | 2x |
| Audio players | `rounded-md` | — |
## Card Patterns
Five distinct card variants used across the site:
### Standard card (`.post-card`)
```
p-5 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm
hover: border-[domain]-400 dark:border-[domain]-600
```
Used for: blog post listings, search results
### Widget card (`.widget`)
```
p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm
```
Used for: sidebar widgets, info panels
### Compact list card
```
p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm
```
Used for: list view items in news/podroll, compact listings
### Featured card
```
p-5 sm:p-6 bg-gradient-to-br from-[color]-500/10 rounded-xl border border-surface-200 dark:border-surface-700 shadow-sm
```
Used for: now-playing, YouTube hero, featured items
### Stat card
```
p-3 sm:p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm text-center
```
Used for: statistics grids (GitHub, Funkwhale, Last.fm)
## Button Patterns
Six button variants:
### Primary action
```
px-4 py-2 bg-accent-600 hover:bg-accent-700 text-white rounded-lg font-medium
focus:ring-2 focus:ring-accent-500 transition-colors
```
Used for: form submits, main CTAs
### Secondary action
```
px-3 py-1.5 bg-surface-100 dark:bg-surface-800 hover:bg-surface-200 dark:hover:bg-surface-700
border border-surface-200 dark:border-surface-700 rounded-lg text-sm font-medium
focus:ring-2 focus:ring-accent-500 transition-colors
```
Used for: filter toggles, view mode switches, secondary actions
### Icon button
```
p-2 rounded-lg hover:bg-surface-200 dark:hover:bg-surface-700
focus:ring-2 focus:ring-accent-500 transition-colors
```
Used for: theme toggle, menu toggle, refresh buttons
### Domain-colored button
```
px-3 py-1.5 bg-[domain]-600 hover:bg-[domain]-700 text-white rounded-lg text-sm font-medium
focus:ring-2 focus:ring-[domain]-500 transition-colors
```
Used for: domain-specific actions (e.g., orange "Load More" on podroll)
### Link button
```
text-[domain]-600 dark:text-[domain]-400 hover:text-[domain]-700 dark:hover:text-[domain]-300
hover:underline font-medium transition-colors
```
Used for: inline actions, "View all" links
### Pagination button
```
px-3 py-1 bg-surface-100 dark:bg-surface-800 hover:bg-surface-200 dark:hover:bg-surface-700
border border-surface-200 dark:border-surface-700 rounded-lg text-sm
focus:ring-2 focus:ring-accent-500 transition-colors
disabled:opacity-50 disabled:cursor-not-allowed
```
Used for: pagination controls
## Badge/Pill Patterns
Four badge variants:
### Post-type badge
```
px-2 py-0.5 text-xs font-medium rounded-full
bg-[domain]-100 dark:bg-[domain]-900/30 text-[domain]-700 dark:text-[domain]-300
```
Used for: post type indicators on cards
### Category tag
```
px-2 py-0.5 text-xs bg-surface-100 dark:bg-surface-800
text-surface-600 dark:text-surface-400 rounded-full
hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors
```
Used for: category tags, hashtags
### Status badge
```
px-2 py-0.5 text-xs font-medium rounded-full
bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300
```
Variants: emerald (active/success), amber (warning), red (error)
Used for: status indicators, sync state
### Syndication badge
```
px-2 py-0.5 text-xs font-medium rounded-full text-white
bg-[brand-hex]
```
Used for: Mastodon/Bluesky/LinkedIn syndication indicators
## Layout Patterns
### Page layouts
| Layout | Classes | Usage |
|--------|---------|-------|
| Full-width | `max-w-7xl mx-auto px-4 sm:px-6` | Page container |
| With sidebar | `.layout-with-sidebar` = `grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6 lg:gap-8` | Blog, post pages |
| Content area | `.main-content` = `min-w-0` (prevents overflow in grid) | Main column |
| Sidebar | `.sidebar` = `space-y-4 lg:space-y-6` | Sidebar column |
| Centered narrow | `max-w-3xl mx-auto` | About, CV pages |
### Grid patterns
| Pattern | Classes | Usage |
|---------|---------|-------|
| Stats grid | `grid grid-cols-2 sm:grid-cols-4 gap-3` | Statistics panels |
| Card grid | `grid grid-cols-1 sm:grid-cols-2 gap-4` | Card view mode |
| Post list | `space-y-4` or `space-y-3` | List view mode |
| Widget stack | `space-y-4 lg:space-y-6` | Sidebar widgets |
### Responsive breakpoints
| Breakpoint | px | Frequency | Purpose |
|------------|-----|-----------|---------|
| `sm:` | 640px | 170x+ | Primary responsive step (dominant) |
| `md:` | 768px | 19x | Tablet-specific adjustments |
| `lg:` | 1024px | 6x | Sidebar layout switch |
**Mobile-first:** Base styles are mobile. `sm:` is the primary breakpoint for responsive changes. Most layouts switch from stacked to side-by-side at `sm:`.
## Interaction States
Every interactive element needs:
- **hover:** color shift (`transition-colors` already dominant at 93×)
- **hover:** color shift (`transition-colors` — dominant at 131x)
- **focus:** visible ring (`focus:ring-2 focus:ring-accent-500` or domain equivalent)
- **active:** not currently implemented — add where it matters (buttons)
Card hover pattern: border color shifts to domain color, no shadow change.
### Hover patterns
| Element | Hover treatment |
|---------|----------------|
| Text links | `hover:underline` (163x — dominant link hover) |
| Card borders | `hover:border-[domain]-400 dark:hover:border-[domain]-600` |
| Buttons (filled) | Background darken (`hover:bg-[color]-700`) |
| Buttons (ghost) | `hover:bg-surface-200 dark:hover:bg-surface-700` |
| Nav items | `hover:text-surface-900 dark:hover:text-surface-100` |
### Focus pattern
All interactive elements: `focus:ring-2 focus:ring-[domain]-500 rounded` (or `focus:ring-accent-500` on neutral pages).
### Transitions
Default: `transition-colors` (131x). No duration override — uses Tailwind default (150ms).
Exceptions:
- Collapsible widgets: `transition-all` for height animation
- Mobile menu: `transition-transform` for slide-in
## Dark Mode
- Class-based: `darkMode: "class"` — toggled via button in header
- Surfaces invert: light canvas (`surface-50`) dark canvas (`surface-900`)
- Cards: `surface-100` `surface-800`
- Surfaces invert: light canvas (`surface-50`) -> dark canvas (`surface-900`)
- Cards: `surface-100` -> `surface-800`
- Domain colors shift to 400-weight (brighter) in dark mode
- Borders: `surface-200` `surface-700`
- Borders: `surface-200` -> `surface-700`
- Shadows remain `shadow-sm` (less visible but still present for subtle lift)
### Dark mode pairs (reference)
| Light | Dark |
|-------|------|
| `bg-surface-50` | `dark:bg-surface-900` (canvas) |
| `bg-surface-100` | `dark:bg-surface-800` (cards) |
| `bg-surface-200` | `dark:bg-surface-700` (hover bg) |
| `border-surface-200` | `dark:border-surface-700` |
| `text-surface-900` | `dark:text-surface-100` (primary text) |
| `text-surface-600` | `dark:text-surface-400` (secondary text) |
| `text-[domain]-600` | `dark:text-[domain]-400` (domain color) |
| `bg-[domain]-100` | `dark:bg-[domain]-900/30` (badge bg) |
## CSS Component Classes
Reusable utility classes defined in `css/tailwind.css`:
| Class | Definition | Usage |
|-------|------------|-------|
| `.widget` | `p-4 bg-surface-50 rounded-lg border shadow-sm` + dark | Sidebar widgets |
| `.widget-title` | `text-lg font-semibold` | Widget headings |
| `.widget-header` | `flex items-center justify-between mb-3` | Widget header row |
| `.widget-collapsible` | Alpine.js collapsible wrapper | Expandable widgets |
| `.post-card` | `p-5 bg-surface-50 rounded-lg border shadow-sm` + dark | Post listing cards |
| `.post-list` | `space-y-4` | Post list container |
| `.layout-with-sidebar` | `grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6 lg:gap-8` | Two-column layout |
| `.main-content` | `min-w-0` | Main column (prevents grid overflow) |
| `.sidebar` | `space-y-4 lg:space-y-6` | Sidebar stack |
| `.share-post-btn` | Blue share button | Post sharing |
| `.save-later-btn` | Accent save button | Read-later action |
## What Needs Implementation
Audit findings — these are the gaps between this system and the current code:
Audit findings — remaining gaps between this system and the current code:
1. **font-mono on dates** — 80+ date elements need `font-mono text-sm` added
2. **Domain colors on section pages** — page titles, links, hovers, card borders need domain color on their respective pages
3. **Shadow standardization** — currently mixed; standardize to the levels defined above
4. **Gradient cleanup** — remove `to-white` (github.njk), standardize gradient pattern
5. **Focus states** — add `focus:ring-2` to all interactive elements (currently only 10 across 6 files)
6. **Active states** — add to buttons
1. ~~**Domain colors on section pages**~~ — ✅ Done: all 7 post-type collections + blog.njk mixed view + recent-posts widget use per-type colors
2. **Active states** — add to buttons where appropriate
3. **Consistent card hover** — some older templates use `hover:border-surface-400` instead of domain-colored border hover

View File

@@ -0,0 +1,30 @@
{
"audit_id": "indiekit-eleventy-theme_20260307_r2",
"target": "entire indiekit-eleventy-theme codebase",
"wcag_level": "AA",
"focus_areas": ["all"],
"status": "complete",
"started_at": "2026-03-07T00:00:00Z",
"completed_at": "2026-03-07T00:00:00Z",
"files_audited": 95,
"issues_found": {
"critical": 0,
"serious": 1,
"moderate": 3,
"minor": 1
},
"criteria_checked": 28,
"criteria_passed": 26,
"compliance_status": "substantially_compliant",
"previous_audit": {
"audit_id": "indiekit-eleventy-theme_20260307",
"issues_found": {
"critical": 8,
"serious": 12,
"moderate": 22,
"minor": 8
},
"criteria_passed": 15,
"compliance_status": "needs_improvement"
}
}

View File

@@ -25,6 +25,7 @@
{% elif widget.type == "toc" %}{% set widgetTitle = "Table of Contents" %}
{% elif widget.type == "post-categories" %}{% set widgetTitle = "Categories" %}
{% elif widget.type == "share" %}{% set widgetTitle = "Share" %}
{% elif widget.type == "ai-usage" %}{% set widgetTitle = "AI Transparency" %}
{% elif widget.type == "custom-html" %}{% set widgetTitle = (widget.config.title if widget.config and widget.config.title) or "Custom" %}
{% else %}{% set widgetTitle = widget.type %}
{% endif %}
@@ -45,21 +46,23 @@
{% elif widget.type == "fediverse-follow" %}
{% set widgetIcon = "user-plus" %}{% set widgetIconClass = "w-5 h-5 text-[#a730b8]" %}{% set widgetBorder = "border-l-[3px] border-l-[#a730b8]" %}
{% elif widget.type == "author-card" or widget.type == "author-card-compact" %}
{% set widgetIcon = "user" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "user" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% elif widget.type == "recent-posts" %}
{% set widgetIcon = "list" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "list" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% elif widget.type == "categories" or widget.type == "post-categories" %}
{% set widgetIcon = "tag" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "tag" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% elif widget.type == "recent-comments" %}
{% set widgetIcon = "chat" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "chat" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% elif widget.type == "search" %}
{% set widgetIcon = "search" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "search" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% elif widget.type == "webmentions" %}
{% set widgetIcon = "share" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "share" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% elif widget.type == "ai-usage" %}
{% set widgetIcon = "zap" %}{% set widgetIconClass = "w-5 h-5 text-amber-500" %}{% set widgetBorder = "border-l-[3px] border-l-amber-400 dark:border-l-amber-500" %}
{% elif widget.type == "toc" %}
{% set widgetIcon = "list" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "list" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% elif widget.type == "share" %}
{% set widgetIcon = "share" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "share" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% else %}
{% set widgetIcon = "" %}{% set widgetIconClass = "" %}{% set widgetBorder = "" %}
{% endif %}
@@ -86,6 +89,7 @@
class="widget-chevron"
:class="open && 'rotate-180'"
fill="none" stroke="currentColor" viewBox="0 0 24 24"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
@@ -136,10 +140,12 @@
{% include "components/widgets/search.njk" %}
{% elif widget.type == "fediverse-follow" %}
{% include "components/widgets/fediverse-follow.njk" %}
{% elif widget.type == "ai-usage" %}
{% include "components/widgets/ai-usage.njk" ignore missing %}
{% elif widget.type == "custom-html" %}
{% set wConfig = widget.config or {} %}
<is-land on:visible>
<div class="widget">
<div class="widget" role="region" aria-label="Custom content">
{% if wConfig.content %}
<div class="prose dark:prose-invert prose-sm max-w-none">
{{ wConfig.content | safe }}
@@ -164,8 +170,8 @@
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : true }">
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("user", "w-5 h-5 text-surface-500") }} Author</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("user", "w-5 h-5 text-surface-600 dark:text-surface-400") }} Author</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/author-card-compact.njk" %}
@@ -178,8 +184,8 @@
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : true }">
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("list", "w-5 h-5 text-surface-500") }} Table of Contents</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("list", "w-5 h-5 text-surface-600 dark:text-surface-400") }} Table of Contents</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/toc.njk" %}
@@ -192,8 +198,8 @@
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : true }">
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("tag", "w-5 h-5 text-surface-500") }} Categories</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("tag", "w-5 h-5 text-surface-600 dark:text-surface-400") }} Categories</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/post-categories.njk" %}
@@ -206,8 +212,8 @@
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : false }">
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("list", "w-5 h-5 text-surface-500") }} Recent Posts</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("list", "w-5 h-5 text-surface-600 dark:text-surface-400") }} Recent Posts</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/recent-posts-blog.njk" %}
@@ -220,8 +226,8 @@
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : false }">
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("share", "w-5 h-5 text-surface-500") }} Webmentions</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("share", "w-5 h-5 text-surface-600 dark:text-surface-400") }} Webmentions</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/webmentions.njk" %}
@@ -234,8 +240,8 @@
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : false }">
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("share", "w-5 h-5 text-surface-500") }} Share</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("share", "w-5 h-5 text-surface-600 dark:text-surface-400") }} Share</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/share.njk" %}
@@ -249,7 +255,7 @@
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden border-l-[3px] border-l-orange-400 dark:border-l-orange-500">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("rss", "w-5 h-5 text-orange-500") }} Subscribe</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/subscribe.njk" %}
@@ -262,8 +268,8 @@
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : false }">
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("chat", "w-5 h-5 text-surface-500") }} Recent Comments</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("chat", "w-5 h-5 text-surface-600 dark:text-surface-400") }} Recent Comments</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/recent-comments.njk" %}

View File

@@ -21,6 +21,7 @@
<div class="mt-4">
{# Status messages #}
<div x-show="statusMessage" x-cloak
role="alert"
x-bind:class="statusType === 'error' ? 'bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400' :
statusType === 'success' ? 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400' :
'bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400'"
@@ -36,7 +37,7 @@
<label for="comment-me" class="block text-sm font-medium mb-1">Your website</label>
<input id="comment-me" type="url" x-model="meUrl"
placeholder="https://yourdomain.com" required
class="w-full px-3 py-2 border rounded-lg dark:bg-surface-800 dark:border-surface-600">
class="w-full px-3 py-2 border rounded-lg dark:bg-surface-800 dark:border-surface-700 dark:text-surface-100">
</div>
<button type="submit" class="button" x-bind:disabled="authLoading">
<span x-show="!authLoading">Sign In</span>
@@ -54,12 +55,13 @@
</div>
<form x-on:submit.prevent="submitComment()">
<textarea x-model="commentText" rows="4" required
<label for="comment-text" class="sr-only">Your comment</label>
<textarea id="comment-text" x-model="commentText" rows="4" required
placeholder="Share your thoughts... (supports **bold**, *italic*, and [links](url))"
class="w-full px-3 py-2 border rounded-lg mb-2 dark:bg-surface-800 dark:border-surface-600"
class="w-full px-3 py-2 border rounded-lg mb-2 dark:bg-surface-800 dark:border-surface-700 dark:text-surface-100"
x-bind:maxlength="maxLength"></textarea>
<div class="flex items-center justify-between">
<span class="text-xs text-surface-500" x-text="commentText.length + '/' + maxLength"></span>
<span class="text-xs text-surface-600 dark:text-surface-400" x-text="commentText.length + '/' + maxLength"></span>
<button type="submit" class="button" x-bind:disabled="submitting">
<span x-show="!submitting">Post Comment</span>
<span x-show="submitting" x-cloak>Posting...</span>
@@ -71,11 +73,11 @@
{# Comment list #}
<div class="mt-6 space-y-4">
<template x-if="loading">
<p class="text-sm text-surface-500">Loading comments...</p>
<p class="text-sm text-surface-600 dark:text-surface-400">Loading comments...</p>
</template>
<template x-for="comment in comments" x-bind:key="comment.published">
<div class="p-4 bg-surface-100 dark:bg-surface-800 rounded-lg">
<div class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
<div class="flex items-start gap-3">
<template x-if="comment.author?.photo">
<img x-bind:src="comment.author.photo" x-bind:alt="comment.author.name"
@@ -90,7 +92,7 @@
<div class="flex items-center gap-2">
<a x-bind:href="comment.author?.url" class="font-medium text-sm hover:underline" target="_blank" rel="noopener"
x-text="comment.author?.name || comment.author?.url"></a>
<time class="text-xs text-surface-500" x-bind:datetime="comment.published"
<time class="text-xs text-surface-600 dark:text-surface-400 font-mono" x-bind:datetime="comment.published"
x-text="new Date(comment.published).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })"></time>
</div>
<div class="mt-1 text-sm prose dark:prose-invert" x-html="comment.content?.html || comment.content?.text"></div>
@@ -100,7 +102,7 @@
</template>
<template x-if="!loading && comments.length === 0">
<p class="text-sm text-surface-500">No comments yet. Be the first to share your thoughts!</p>
<p class="text-sm text-surface-600 dark:text-surface-400">No comments yet. Be the first to share your thoughts!</p>
</template>
</div>
</div>

View File

@@ -74,7 +74,7 @@
{% endif %}
{# Contact details — location, organization, website, email, PGP #}
{% if cvLocality or cvCountry or cvOrg or cvUrl or cvEmail or cvKeyUrl %}
<div class="flex flex-wrap gap-x-4 gap-y-1 mt-4 text-sm text-surface-500 dark:text-surface-400">
<div class="flex flex-wrap gap-x-4 gap-y-1 mt-4 text-sm text-surface-600 dark:text-surface-400">
{% if cvLocality or cvCountry %}
<span>{% if cvLocality %}{{ cvLocality }}{% endif %}{% if cvLocality and cvCountry %}, {% endif %}{% if cvCountry %}{{ cvCountry }}{% endif %}</span>
{% endif %}
@@ -160,8 +160,8 @@
{# Last Updated #}
{% if cv.lastUpdated %}
<p class="text-sm text-surface-500 text-center mt-8">
Last updated: <time datetime="{{ cv.lastUpdated }}">{{ cv.lastUpdated | date("PPP") }}</time>
<p class="text-sm text-surface-600 dark:text-surface-400 text-center mt-8">
Last updated: <time class="font-mono text-sm" datetime="{{ cv.lastUpdated }}">{{ cv.lastUpdated | date("PPP") }}</time>
</p>
{% endif %}

View File

@@ -5,14 +5,14 @@
{% if pt.type == postType %}{% set typeInfo = pt %}{% endif %}
{% endfor %}
<div class="text-center py-12 px-4">
<div class="text-center py-12 px-4" role="status">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-surface-100 dark:bg-surface-800 mb-4">
<svg class="w-8 h-8 text-surface-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
</svg>
</div>
<h2 class="text-lg font-semibold text-surface-700 dark:text-surface-300 mb-2">No {{ title | lower }} yet</h2>
<p class="text-surface-500 dark:text-surface-400 mb-6 max-w-md mx-auto">
<p class="text-surface-600 dark:text-surface-400 mb-6 max-w-md mx-auto">
This is where your {{ title | lower }} will appear once you start creating content.
</p>
{% if typeInfo %}

View File

@@ -2,7 +2,10 @@
{# Used by post.njk (interact), fediverse-follow.njk (follow), share.njk (share) #}
{# Requires: modalTitle, modalDescription variables set before include #}
<template x-if="showModal">
<div class="fixed inset-0 z-50 flex items-center justify-center p-4" @keydown.escape.window="showModal = false">
<div class="fixed inset-0 z-50 flex items-center justify-center p-4"
@keydown.escape.window="showModal = false"
x-init="$nextTick(() => { const input = $el.querySelector('input'); if (input) input.focus(); })"
@keydown.tab="trapFocus($event)">
{# Backdrop #}
<div class="fixed inset-0 bg-black/40"
x-transition:enter="transition ease-out duration-200"
@@ -14,6 +17,7 @@
@click="showModal = false"></div>
{# Panel #}
<div class="relative bg-surface-50 dark:bg-surface-800 rounded-xl shadow-xl w-full max-w-sm p-6"
role="dialog" aria-modal="true" aria-labelledby="fediverse-modal-title"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
@@ -21,22 +25,22 @@
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
@click.stop>
<h3 class="text-lg font-semibold text-surface-900 dark:text-surface-100 mb-1">{{ modalTitle }}</h3>
<p class="text-sm text-surface-500 dark:text-surface-400 mb-4">{{ modalDescription }}</p>
<h3 id="fediverse-modal-title" class="text-lg font-semibold text-surface-900 dark:text-surface-100 mb-1">{{ modalTitle }}</h3>
<p class="text-sm text-surface-600 dark:text-surface-400 mb-4">{{ modalDescription }}</p>
{# Saved domains list #}
<template x-if="savedDomains.length > 0 && !showInput">
<div>
<div class="flex flex-col gap-2 mb-3">
<template x-for="item in savedDomains" :key="item.domain">
<div class="flex items-center gap-2 rounded-lg bg-surface-50 dark:bg-surface-700 hover:bg-surface-100 dark:hover:bg-surface-600 transition-colors">
<div class="flex items-center gap-2 rounded-lg bg-surface-50 dark:bg-surface-800 hover:bg-surface-100 dark:hover:bg-surface-600 transition-colors">
<button class="flex-1 px-3 py-2.5 text-left text-sm font-medium text-surface-900 dark:text-surface-100 cursor-pointer"
@click="useSaved(item.domain)"
x-text="item.domain"></button>
<button class="px-2 py-2.5 text-surface-400 hover:text-red-500 transition-colors cursor-pointer"
@click="deleteSaved(item.domain)"
title="Remove">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
:aria-label="'Remove ' + item.domain">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
</template>
@@ -45,7 +49,7 @@
@click="showAddNew()">Use a different instance</button>
<div class="flex mt-3">
<button @click="showModal = false"
class="w-full px-4 py-2 text-sm font-medium text-surface-600 dark:text-surface-300 bg-surface-100 dark:bg-surface-700 hover:bg-surface-200 dark:hover:bg-surface-600 rounded-lg transition-colors">
class="w-full px-4 py-2 text-sm font-medium text-surface-600 dark:text-surface-300 bg-surface-100 dark:bg-surface-800 hover:bg-surface-200 dark:hover:bg-surface-600 rounded-lg transition-colors">
Cancel
</button>
</div>
@@ -55,18 +59,20 @@
{# New domain input #}
<template x-if="savedDomains.length === 0 || showInput">
<div>
<input x-ref="instanceInput"
<label for="fediverse-instance-input" class="sr-only">Fediverse instance domain</label>
<input id="fediverse-instance-input"
x-ref="instanceInput"
x-model="instance"
@keydown.enter.prevent="confirm()"
type="text"
placeholder="mastodon.social"
class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-surface-50 dark:bg-surface-700 text-surface-900 dark:text-surface-100 placeholder-surface-400 focus:outline-none focus:ring-2 focus:ring-[#a730b8] focus:border-transparent text-sm">
class="w-full px-3 py-2 border border-surface-300 dark:border-surface-600 rounded-lg bg-surface-50 dark:bg-surface-800 text-surface-900 dark:text-surface-100 placeholder-surface-400 text-sm">
<template x-if="error">
<p class="text-xs text-red-500 mt-1" x-text="error"></p>
<p class="text-xs text-red-500 mt-1" x-text="error" role="alert"></p>
</template>
<div class="flex gap-3 mt-4">
<button @click="showInput ? (showInput = false) : (showModal = false)"
class="flex-1 px-4 py-2 text-sm font-medium text-surface-600 dark:text-surface-300 bg-surface-100 dark:bg-surface-700 hover:bg-surface-200 dark:hover:bg-surface-600 rounded-lg transition-colors"
class="flex-1 px-4 py-2 text-sm font-medium text-surface-600 dark:text-surface-300 bg-surface-100 dark:bg-surface-800 hover:bg-surface-200 dark:hover:bg-surface-600 rounded-lg transition-colors"
x-text="showInput && savedDomains.length > 0 ? 'Back' : 'Cancel'">
</button>
<button @click="confirm()"

View File

@@ -1,21 +1,21 @@
{# Stats Summary Cards #}
{% if summary %}
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4 mb-6 sm:mb-8">
<div class="p-4 bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 text-center">
<span class="text-2xl font-bold text-purple-600 dark:text-purple-400 block">{{ summary.totalPlays or 0 }}</span>
<span class="text-xs text-surface-500 uppercase tracking-wide">Plays</span>
<div class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm text-center">
<span class="text-2xl font-bold font-mono text-purple-600 dark:text-purple-400 block">{{ summary.totalPlays or 0 }}</span>
<span class="text-xs text-surface-600 dark:text-surface-400 uppercase tracking-wide">Plays</span>
</div>
<div class="p-4 bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 text-center">
<span class="text-2xl font-bold text-purple-600 dark:text-purple-400 block">{{ summary.uniqueTracks or 0 }}</span>
<span class="text-xs text-surface-500 uppercase tracking-wide">Tracks</span>
<div class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm text-center">
<span class="text-2xl font-bold font-mono text-purple-600 dark:text-purple-400 block">{{ summary.uniqueTracks or 0 }}</span>
<span class="text-xs text-surface-600 dark:text-surface-400 uppercase tracking-wide">Tracks</span>
</div>
<div class="p-4 bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 text-center">
<span class="text-2xl font-bold text-purple-600 dark:text-purple-400 block">{{ summary.uniqueArtists or 0 }}</span>
<span class="text-xs text-surface-500 uppercase tracking-wide">Artists</span>
<div class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm text-center">
<span class="text-2xl font-bold font-mono text-purple-600 dark:text-purple-400 block">{{ summary.uniqueArtists or 0 }}</span>
<span class="text-xs text-surface-600 dark:text-surface-400 uppercase tracking-wide">Artists</span>
</div>
<div class="p-4 bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 text-center">
<span class="text-2xl font-bold text-purple-600 dark:text-purple-400 block">{{ summary.totalDurationFormatted or '0m' }}</span>
<span class="text-xs text-surface-500 uppercase tracking-wide">Listened</span>
<div class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm text-center">
<span class="text-2xl font-bold font-mono text-purple-600 dark:text-purple-400 block">{{ summary.totalDurationFormatted or '0m' }}</span>
<span class="text-xs text-surface-600 dark:text-surface-400 uppercase tracking-wide">Listened</span>
</div>
</div>
{% endif %}
@@ -26,10 +26,10 @@
<h3 class="text-lg font-semibold text-surface-900 dark:text-surface-100 mb-4">Top Artists</h3>
<div class="space-y-2">
{% for artist in topArtists | head(5) %}
<div class="flex items-center gap-3 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700">
<span class="w-6 h-6 flex items-center justify-center text-sm font-bold text-surface-400 bg-surface-100 dark:bg-surface-700 rounded-full">{{ loop.index }}</span>
<div class="flex items-center gap-3 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
<span class="w-6 h-6 flex items-center justify-center text-sm font-bold text-surface-600 dark:text-surface-400 bg-surface-100 dark:bg-surface-700 rounded-full">{{ loop.index }}</span>
<span class="flex-1 font-medium text-surface-900 dark:text-surface-100">{{ artist.name }}</span>
<span class="text-sm text-surface-500">{{ artist.playCount }} plays</span>
<span class="text-sm text-surface-600 dark:text-surface-400">{{ artist.playCount }} plays</span>
</div>
{% endfor %}
</div>
@@ -44,7 +44,7 @@
{% for album in topAlbums | head(5) %}
<div class="text-center">
{% if album.coverUrl %}
<img src="{{ album.coverUrl }}" alt="" class="w-full aspect-square object-cover rounded-lg mb-2" loading="lazy" eleventy:ignore>
<img src="{{ album.coverUrl }}" alt="" class="w-full aspect-square object-cover rounded-lg mb-2 shadow-lg" loading="lazy" eleventy:ignore>
{% else %}
<div class="w-full aspect-square bg-surface-200 dark:bg-surface-700 rounded-lg mb-2 flex items-center justify-center">
<svg class="w-8 h-8 text-surface-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -53,8 +53,8 @@
</div>
{% endif %}
<p class="text-sm font-medium text-surface-900 dark:text-surface-100 truncate">{{ album.title }}</p>
<p class="text-xs text-surface-500 truncate">{{ album.artist }}</p>
<p class="text-xs text-surface-400">{{ album.playCount }} plays</p>
<p class="text-xs text-surface-600 dark:text-surface-400 truncate">{{ album.artist }}</p>
<p class="text-xs text-surface-600 dark:text-surface-400">{{ album.playCount }} plays</p>
</div>
{% endfor %}
</div>

View File

@@ -29,7 +29,9 @@
<img
src="{{ authorAvatar }}"
alt="{{ authorName }}"
class="w-16 h-16 rounded-full object-cover"
width="64"
height="64"
class="w-16 h-16 rounded-full object-cover shadow-lg"
loading="lazy"
itemprop="image"
>
@@ -39,11 +41,11 @@
{{ authorName }}
</a>
{% if authorPronoun %}
<span class="p-pronoun text-xs text-surface-500">({{ authorPronoun }})</span>
<span class="p-pronoun text-xs text-surface-600 dark:text-surface-400">({{ authorPronoun }})</span>
{% endif %}
<p class="p-job-title text-sm text-surface-600 dark:text-surface-400" itemprop="jobTitle">{{ authorTitle }}</p>
{# Structured address #}
<p class="p-adr h-adr text-sm text-surface-500 dark:text-surface-500" itemprop="address" itemscope itemtype="http://schema.org/PostalAddress">
<p class="p-adr h-adr text-sm text-surface-600 dark:text-surface-400" itemprop="address" itemscope itemtype="http://schema.org/PostalAddress">
{% if authorLocality %}
<span class="p-locality" itemprop="addressLocality">{{ authorLocality }}</span>{% if authorCountry %}, {% endif %}
{% endif %}
@@ -72,12 +74,12 @@
<div class="mt-2 flex flex-wrap gap-3 text-sm">
{% if authorEmail %}
{# Display text obfuscated to deter spam harvesters; href kept plain for browser compatibility #}
<a href="mailto:{{ authorEmail }}" class="u-email text-accent-600 dark:text-accent-400 hover:underline" itemprop="email">
<a href="mailto:{{ authorEmail }}" class="u-email text-accent-600 dark:text-accent-400 hover:underline" itemprop="email" aria-label="Email {{ authorEmail }}">
✉️ {{ authorEmail | obfuscateEmail | safe }}
</a>
{% endif %}
{% if authorKeyUrl %}
<a href="{{ authorKeyUrl }}" class="u-key text-surface-500 dark:text-surface-400 hover:underline" rel="pgpkey">
<a href="{{ authorKeyUrl }}" class="u-key text-surface-600 dark:text-surface-400 hover:underline" rel="pgpkey">
🔐 PGP Key
</a>
{% endif %}
@@ -85,11 +87,11 @@
{# Categories / Skills #}
{% if authorCategories and authorCategories.length %}
<div class="mt-3 flex flex-wrap gap-1">
<ul class="mt-3 flex flex-wrap gap-1 list-none p-0 m-0" role="list" aria-label="Skills and interests">
{% for category in authorCategories %}
<span class="p-category text-xs px-2 py-0.5 bg-surface-100 dark:bg-surface-800 rounded">{{ category }}</span>
<li class="p-category text-xs px-2 py-0.5 bg-surface-100 dark:bg-surface-800 rounded-full">{{ category }}</li>
{% endfor %}
</div>
</ul>
{% endif %}
{# Social links with rel="me" - critical for IndieWeb identity verification #}
@@ -100,8 +102,8 @@
<a
href="{{ link.url }}"
rel="{{ link.rel }} noopener"
class="u-url text-surface-500 hover:text-accent-600 dark:hover:text-accent-400 transition-colors"
aria-label="{{ link.name }}"
class="u-url text-surface-600 dark:text-surface-400 hover:text-accent-600 dark:hover:text-accent-400 transition-colors"
aria-label="{{ link.name }} (opens in new tab)"
target="_blank">
{{ socialIcon(link.icon, "w-5 h-5") }}
</a>

View File

@@ -51,6 +51,8 @@
{% include "components/sections/lastfm.njk" ignore missing %}
{% elif section.type == "posting-activity" %}
{% include "components/sections/posting-activity.njk" ignore missing %}
{% elif section.type == "ai-usage" %}
{% include "components/sections/ai-usage.njk" ignore missing %}
{% else %}
<!-- Unknown section type: {{ section.type }} -->
{% endif %}

View File

@@ -18,6 +18,7 @@
{% elif widget.type == "recent-comments" %}{% set widgetTitle = "Recent Comments" %}
{% elif widget.type == "fediverse-follow" %}{% set widgetTitle = "Fediverse" %}
{% elif widget.type == "author-card" %}{% set widgetTitle = "Author" %}
{% elif widget.type == "ai-usage" %}{% set widgetTitle = "AI Transparency" %}
{% elif widget.type == "custom-html" %}{% set widgetTitle = (widget.config.title if widget.config and widget.config.title) or "Custom" %}
{% else %}{% set widgetTitle = widget.type %}
{% endif %}
@@ -38,17 +39,19 @@
{% elif widget.type == "fediverse-follow" %}
{% set widgetIcon = "user-plus" %}{% set widgetIconClass = "w-5 h-5 text-[#a730b8]" %}{% set widgetBorder = "border-l-[3px] border-l-[#a730b8]" %}
{% elif widget.type == "author-card" %}
{% set widgetIcon = "user" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "user" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% elif widget.type == "recent-posts" %}
{% set widgetIcon = "list" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "list" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% elif widget.type == "categories" %}
{% set widgetIcon = "tag" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "tag" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% elif widget.type == "recent-comments" %}
{% set widgetIcon = "chat" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "chat" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% elif widget.type == "search" %}
{% set widgetIcon = "search" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "search" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% elif widget.type == "webmentions" %}
{% set widgetIcon = "share" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "share" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% elif widget.type == "ai-usage" %}
{% set widgetIcon = "zap" %}{% set widgetIconClass = "w-5 h-5 text-amber-500" %}{% set widgetBorder = "border-l-[3px] border-l-amber-400 dark:border-l-amber-500" %}
{% else %}
{% set widgetIcon = "" %}{% set widgetIconClass = "" %}{% set widgetBorder = "" %}
{% endif %}
@@ -75,6 +78,7 @@
class="widget-chevron"
:class="open && 'rotate-180'"
fill="none" stroke="currentColor" viewBox="0 0 24 24"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
@@ -115,10 +119,12 @@
{% include "components/widgets/recent-comments.njk" %}
{% elif widget.type == "fediverse-follow" %}
{% include "components/widgets/fediverse-follow.njk" %}
{% elif widget.type == "ai-usage" %}
{% include "components/widgets/ai-usage.njk" ignore missing %}
{% elif widget.type == "custom-html" %}
{% set wConfig = widget.config or {} %}
<is-land on:visible>
<div class="widget">
<div class="widget" role="region" aria-label="Custom content">
{% if wConfig.content %}
<div class="prose dark:prose-invert prose-sm max-w-none">
{{ wConfig.content | safe }}

View File

@@ -61,6 +61,8 @@
<svg class="{{ cls }}" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17.657 18.657A8 8 0 016.343 7.343"/><path d="M9.879 16.121A3 3 0 1012.015 11L11 17H9c-2 0-3-2-3-3l.879-.879z"/></svg>
{%- elif name == "user-plus" -%}
<svg class="{{ cls }}" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="20" y1="8" x2="20" y2="14"/><line x1="23" y1="11" x2="17" y2="11"/></svg>
{%- elif name == "zap" -%}
<svg class="{{ cls }}" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
{%- else -%}
<!-- Unknown icon: {{ name }} -->
{%- endif -%}

View File

@@ -31,7 +31,7 @@
{% endif %}
{% endif %}
<a href="{{ _prevPost.url }}" class="group relative block rounded-lg overflow-hidden bg-surface-100 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 hover:border-accent-400 dark:hover:border-accent-600 transition-colors">
<a href="{{ _prevPost.url }}" class="group relative block rounded-lg overflow-hidden bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 hover:border-accent-400 dark:hover:border-accent-600 transition-colors shadow-sm">
{% if _prevHasOg %}
<img src="/og/{{ _prevOgSlug }}.png" alt="{{ _prevTitle }}" class="w-full aspect-[1.91/1] object-cover opacity-85 group-hover:opacity-100 transition-opacity" loading="lazy" decoding="async" eleventy:ignore>
<span class="absolute top-2 left-2 text-[10px] sm:text-xs font-semibold uppercase tracking-wide bg-white/90 dark:bg-surface-900/90 text-surface-700 dark:text-surface-300 px-2 py-0.5 rounded">
@@ -39,17 +39,17 @@
</span>
{% else %}
<div class="p-4 sm:p-5">
<span class="text-[10px] sm:text-xs font-semibold uppercase tracking-wide text-surface-500 block mb-2">&larr; Previous</span>
<span class="text-[10px] sm:text-xs font-semibold uppercase tracking-wide text-surface-600 dark:text-surface-400 block mb-2">&larr; Previous</span>
<span class="text-sm sm:text-base font-medium text-surface-900 dark:text-surface-100 group-hover:text-accent-600 dark:group-hover:text-accent-400 line-clamp-2 transition-colors">
{{ _prevTitle }}
</span>
<time class="text-xs text-surface-500 mt-1 block" datetime="{{ _prevPost.date | isoDate }}">{{ _prevPost.date | dateDisplay }}</time>
<time class="text-xs text-surface-600 dark:text-surface-400 mt-1 block font-mono" datetime="{{ _prevPost.date | isoDate }}">{{ _prevPost.date | dateDisplay }}</time>
</div>
{% endif %}
</a>
{% else %}
<div class="rounded-lg bg-surface-50 dark:bg-surface-800/50 border border-surface-200/50 dark:border-surface-700/50"></div>
<div class="rounded-lg bg-surface-50 dark:bg-surface-800/50 border border-surface-200/50 dark:border-surface-700/50" aria-hidden="true"></div>
{% endif %}
{# ── Next Post ── #}
@@ -77,7 +77,7 @@
{% endif %}
{% endif %}
<a href="{{ _nextPost.url }}" class="group relative block rounded-lg overflow-hidden bg-surface-100 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 hover:border-accent-400 dark:hover:border-accent-600 transition-colors">
<a href="{{ _nextPost.url }}" class="group relative block rounded-lg overflow-hidden bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 hover:border-accent-400 dark:hover:border-accent-600 transition-colors shadow-sm">
{% if _nextHasOg %}
<img src="/og/{{ _nextOgSlug }}.png" alt="{{ _nextTitle }}" class="w-full aspect-[1.91/1] object-cover opacity-85 group-hover:opacity-100 transition-opacity" loading="lazy" decoding="async" eleventy:ignore>
<span class="absolute top-2 right-2 text-[10px] sm:text-xs font-semibold uppercase tracking-wide bg-white/90 dark:bg-surface-900/90 text-surface-700 dark:text-surface-300 px-2 py-0.5 rounded">
@@ -85,17 +85,17 @@
</span>
{% else %}
<div class="p-4 sm:p-5 text-right">
<span class="text-[10px] sm:text-xs font-semibold uppercase tracking-wide text-surface-500 block mb-2">Next &rarr;</span>
<span class="text-[10px] sm:text-xs font-semibold uppercase tracking-wide text-surface-600 dark:text-surface-400 block mb-2">Next &rarr;</span>
<span class="text-sm sm:text-base font-medium text-surface-900 dark:text-surface-100 group-hover:text-accent-600 dark:group-hover:text-accent-400 line-clamp-2 transition-colors">
{{ _nextTitle }}
</span>
<time class="text-xs text-surface-500 mt-1 block" datetime="{{ _nextPost.date | isoDate }}">{{ _nextPost.date | dateDisplay }}</time>
<time class="text-xs text-surface-600 dark:text-surface-400 mt-1 block font-mono" datetime="{{ _nextPost.date | isoDate }}">{{ _nextPost.date | dateDisplay }}</time>
</div>
{% endif %}
</a>
{% else %}
<div class="rounded-lg bg-surface-50 dark:bg-surface-800/50 border border-surface-200/50 dark:border-surface-700/50"></div>
<div class="rounded-lg bg-surface-50 dark:bg-surface-800/50 border border-surface-200/50 dark:border-surface-700/50" aria-hidden="true"></div>
{% endif %}
</div>

View File

@@ -10,17 +10,17 @@
{% set bookmarkedUrl = bookmarkOf or bookmark_of %}
{% if replyTo or likedUrl or repostedUrl or bookmarkedUrl %}
<aside class="reply-context mb-6">
<aside class="reply-context mb-6" aria-label="Reply context">
{% if replyTo %}
<div class="u-in-reply-to h-cite">
<p class="text-sm text-surface-500 dark:text-surface-400 mb-2 flex items-center gap-2">
<p class="text-sm text-surface-600 dark:text-surface-400 mb-2 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/>
</svg>
<span>In reply to:</span>
</p>
{% unfurl replyTo %}
<a class="u-url text-xs text-surface-400 dark:text-surface-500 hover:underline break-all" href="{{ replyTo }}">
<a class="u-url text-xs text-surface-600 dark:text-surface-400 hover:underline break-all" href="{{ replyTo }}">
{{ replyTo }}
</a>
</div>
@@ -28,14 +28,14 @@
{% if likedUrl %}
<div class="u-like-of h-cite">
<p class="text-sm text-surface-500 dark:text-surface-400 mb-2 flex items-center gap-2">
<p class="text-sm text-surface-600 dark:text-surface-400 mb-2 flex items-center gap-2">
<svg class="w-4 h-4 text-red-500" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
</svg>
<span>Liked:</span>
</p>
{% unfurl likedUrl %}
<a class="u-url text-xs text-surface-400 dark:text-surface-500 hover:underline break-all" href="{{ likedUrl }}">
<a class="u-url text-xs text-surface-600 dark:text-surface-400 hover:underline break-all" href="{{ likedUrl }}">
{{ likedUrl }}
</a>
</div>
@@ -43,14 +43,14 @@
{% if repostedUrl %}
<div class="u-repost-of h-cite">
<p class="text-sm text-surface-500 dark:text-surface-400 mb-2 flex items-center gap-2">
<p class="text-sm text-surface-600 dark:text-surface-400 mb-2 flex items-center gap-2">
<svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
<span>Reposted:</span>
</p>
{% unfurl repostedUrl %}
<a class="u-url text-xs text-surface-400 dark:text-surface-500 hover:underline break-all" href="{{ repostedUrl }}">
<a class="u-url text-xs text-surface-600 dark:text-surface-400 hover:underline break-all" href="{{ repostedUrl }}">
{{ repostedUrl }}
</a>
</div>
@@ -58,14 +58,14 @@
{% if bookmarkedUrl %}
<div class="u-bookmark-of h-cite">
<p class="text-sm text-surface-500 dark:text-surface-400 mb-2 flex items-center gap-2">
<p class="text-sm text-surface-600 dark:text-surface-400 mb-2 flex items-center gap-2">
<svg class="w-4 h-4 text-yellow-500" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2z"/>
</svg>
<span>Bookmarked:</span>
</p>
{% unfurl bookmarkedUrl %}
<a class="u-url text-xs text-surface-400 dark:text-surface-500 hover:underline break-all" href="{{ bookmarkedUrl }}">
<a class="u-url text-xs text-surface-600 dark:text-surface-400 hover:underline break-all" href="{{ bookmarkedUrl }}">
{{ bookmarkedUrl }}
</a>
</div>

View File

@@ -0,0 +1,66 @@
{# AI Usage Section — full-width AI transparency stats and contribution graph #}
{% set sectionConfig = section.config or {} %}
{% set sectionTitle = sectionConfig.title or "AI Transparency" %}
{% set stats = collections.posts | aiStats %}
{% set aiPostsList = collections.posts | aiPosts %}
{% if stats and stats.total > 0 %}
<section class="mb-8 sm:mb-12">
<h2 class="text-xl sm:text-2xl font-bold text-surface-900 dark:text-surface-100 mb-4 sm:mb-6">
{{ sectionTitle }}
</h2>
<div class="p-6 rounded-lg bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 shadow-sm">
{# Stats grid — 4 columns #}
<div class="grid gap-4 sm:grid-cols-4 mb-6">
<div class="text-center p-3 rounded-lg bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 shadow-sm">
<div class="text-2xl font-bold font-mono text-surface-900 dark:text-surface-100">{{ stats.total }}</div>
<div class="text-xs text-surface-600 dark:text-surface-400">Total posts</div>
</div>
<div class="text-center p-3 rounded-lg bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 shadow-sm">
<div class="text-2xl font-bold font-mono text-amber-600 dark:text-amber-400">{{ stats.aiCount }}</div>
<div class="text-xs text-surface-600 dark:text-surface-400">AI-involved</div>
</div>
<div class="text-center p-3 rounded-lg bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 shadow-sm">
<div class="text-2xl font-bold font-mono text-emerald-600 dark:text-emerald-400">{{ stats.total - stats.aiCount }}</div>
<div class="text-xs text-surface-600 dark:text-surface-400">Human-only</div>
</div>
<div class="text-center p-3 rounded-lg bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 shadow-sm">
<div class="text-2xl font-bold font-mono text-surface-900 dark:text-surface-100">{{ stats.percentage }}%</div>
<div class="text-xs text-surface-600 dark:text-surface-400">AI ratio</div>
</div>
</div>
{# Level breakdown #}
<div class="flex flex-wrap gap-3 text-sm mb-6">
<span class="px-3 py-1 rounded-full bg-surface-100 dark:bg-surface-700 text-surface-600 dark:text-surface-300">
Level 0 (None): {{ stats.byLevel[0] }}
</span>
<span class="px-3 py-1 rounded-full bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-300">
Level 1 (Editorial): {{ stats.byLevel[1] }}
</span>
<span class="px-3 py-1 rounded-full bg-amber-100 dark:bg-amber-900/40 text-amber-800 dark:text-amber-200">
Level 2 (Co-drafted): {{ stats.byLevel[2] }}
</span>
<span class="px-3 py-1 rounded-full bg-amber-200 dark:bg-amber-900/60 text-amber-900 dark:text-amber-100">
Level 3 (AI-generated): {{ stats.byLevel[3] }}
</span>
</div>
{# Post graph — AI-involved posts highlighted #}
{% if aiPostsList and aiPostsList.length %}
<h3 class="text-lg font-semibold text-surface-900 dark:text-surface-100 mb-3">AI-Involved Posts Over Time</h3>
<p class="text-sm text-surface-600 dark:text-surface-400 mb-4">Highlighted days had posts with AI involvement (level 1+). Empty boxes represent days with no AI-involved posts.</p>
{% set graphLimit = sectionConfig.limit or 1 %}
{% postGraph aiPostsList, { prefix: "ai-section", limit: graphLimit, boxColorDark: "#44403c", highlightColorLight: "#d97706", highlightColorDark: "#fbbf24" } %}
{% endif %}
</div>
<a href="/ai/" class="text-sm text-amber-600 dark:text-amber-400 hover:underline mt-4 inline-flex items-center gap-1">
View full AI transparency report
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</a>
</section>
{% endif %}

View File

@@ -16,7 +16,7 @@
{% for item in cv.education %}
{% if not filterType or item.educationType == filterType or not item.educationType %}
{% set ci = loop.index0 % 8 %}
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 transition-colors overflow-hidden border-l-[3px]
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm transition-colors overflow-hidden border-l-[3px]
{% if ci == 0 %}border-l-amber-400 dark:border-l-amber-500
{% elif ci == 1 %}border-l-emerald-400 dark:border-l-emerald-500
{% elif ci == 2 %}border-l-sky-400 dark:border-l-sky-500
@@ -40,11 +40,11 @@
</div>
<div class="flex items-center gap-2 shrink-0">
{% if item.startDate %}
<span class="text-xs text-surface-500 hidden sm:inline">
<span class="text-xs text-surface-600 dark:text-surface-400 hidden sm:inline font-mono">
{{ item.startDate }}{% if item.endDate %} {{ item.endDate }}{% else %} Present{% endif %}
</span>
{% elif item.year %}
<span class="text-xs text-surface-500 hidden sm:inline">{{ item.year }}</span>
<span class="text-xs text-surface-600 dark:text-surface-400 hidden sm:inline font-mono">{{ item.year }}</span>
{% endif %}
<svg
class="w-4 h-4 text-surface-400 transition-transform duration-200"
@@ -69,11 +69,11 @@
class="px-4 pb-4"
>
{% if item.startDate %}
<p class="text-xs text-surface-500 mb-1 sm:hidden">
<p class="text-xs text-surface-600 dark:text-surface-400 mb-1 sm:hidden font-mono">
{{ item.startDate }}{% if item.endDate %} {{ item.endDate }}{% else %} Present{% endif %}
</p>
{% elif item.year %}
<p class="text-xs text-surface-500 mb-1 sm:hidden">{{ item.year }}</p>
<p class="text-xs text-surface-600 dark:text-surface-400 mb-1 sm:hidden font-mono">{{ item.year }}</p>
{% endif %}
{% if item.description %}

View File

@@ -24,7 +24,7 @@
{% if item.type %} &middot; <span class="capitalize">{{ item.type }}</span>{% endif %}
</p>
{% if item.startDate %}
<p class="text-xs text-surface-500 mt-0.5">
<p class="text-xs text-surface-600 dark:text-surface-400 mt-0.5 font-mono">
{{ item.startDate }}{% if item.endDate %} {{ item.endDate }}{% else %} Present{% endif %}
</p>
{% endif %}

View File

@@ -15,7 +15,7 @@
{% if not filterType or (cv.interestTypes and cv.interestTypes[category] == filterType) or not cv.interestTypes or not cv.interestTypes[category] %}
{# Cycle through 8 distinct colors per family using loop.index0 #}
{% set ci = loop.index0 % 8 %}
<div class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700">
<div class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
<h3 class="font-semibold text-sm uppercase tracking-wide text-surface-600 dark:text-surface-400 mb-2">
{{ category }}
</h3>

View File

@@ -13,7 +13,7 @@
{% for lang in cv.languages %}
<div class="flex items-center gap-2 px-3 py-1.5 bg-surface-50 dark:bg-surface-800 rounded-full border border-surface-200 dark:border-surface-700">
<span class="font-medium text-sm text-surface-900 dark:text-surface-100">{{ lang.name }}</span>
<span class="text-xs text-surface-500 capitalize">{{ lang.level }}</span>
<span class="text-xs text-surface-600 dark:text-surface-400 capitalize">{{ lang.level }}</span>
</div>
{% endfor %}
</div>

View File

@@ -26,7 +26,7 @@
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 items-start">
{% for item in personalProjects | head(maxItems) %}
{% set ci = loop.index0 % 8 %}
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 transition-colors overflow-hidden border-l-[3px]
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm transition-colors overflow-hidden border-l-[3px]
{% if ci == 0 %}border-l-amber-400 dark:border-l-amber-500
{% elif ci == 1 %}border-l-emerald-400 dark:border-l-emerald-500
{% elif ci == 2 %}border-l-sky-400 dark:border-l-sky-500
@@ -62,7 +62,7 @@
</div>
<div class="flex items-center gap-2 shrink-0">
{% if item.startDate %}
<span class="text-xs text-surface-500 hidden sm:inline">
<span class="text-xs text-surface-600 dark:text-surface-400 hidden sm:inline font-mono">
{{ item.startDate }}{% if item.endDate %} {{ item.endDate }}{% else %} Present{% endif %}
</span>
{% endif %}
@@ -89,7 +89,7 @@
class="px-4 pb-4"
>
{% if item.startDate %}
<p class="text-xs text-surface-500 mb-1 sm:hidden">
<p class="text-xs text-surface-600 dark:text-surface-400 mb-1 sm:hidden font-mono">
{{ item.startDate }}{% if item.endDate %} {{ item.endDate }}{% else %} Present{% endif %}
</p>
{% endif %}
@@ -101,7 +101,7 @@
{% if showTechnologies and item.technologies and item.technologies.length %}
<div class="flex flex-wrap gap-1">
{% for tech in item.technologies %}
<span class="text-xs px-2 py-0.5 rounded
<span class="text-xs px-2 py-0.5 rounded-full
{% if ci == 0 %}bg-amber-50 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300
{% elif ci == 1 %}bg-emerald-50 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300
{% elif ci == 2 %}bg-sky-50 dark:bg-sky-900/30 text-sky-700 dark:text-sky-300

View File

@@ -26,7 +26,7 @@
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 items-start">
{% for item in workProjects | head(maxItems) %}
{% set ci = loop.index0 % 8 %}
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 transition-colors overflow-hidden border-l-[3px]
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm transition-colors overflow-hidden border-l-[3px]
{% if ci == 0 %}border-l-amber-400 dark:border-l-amber-500
{% elif ci == 1 %}border-l-emerald-400 dark:border-l-emerald-500
{% elif ci == 2 %}border-l-sky-400 dark:border-l-sky-500
@@ -62,7 +62,7 @@
</div>
<div class="flex items-center gap-2 shrink-0">
{% if item.startDate %}
<span class="text-xs text-surface-500 hidden sm:inline">
<span class="text-xs text-surface-600 dark:text-surface-400 hidden sm:inline font-mono">
{{ item.startDate }}{% if item.endDate %} {{ item.endDate }}{% else %} Present{% endif %}
</span>
{% endif %}
@@ -89,7 +89,7 @@
class="px-4 pb-4"
>
{% if item.startDate %}
<p class="text-xs text-surface-500 mb-1 sm:hidden">
<p class="text-xs text-surface-600 dark:text-surface-400 mb-1 sm:hidden font-mono">
{{ item.startDate }}{% if item.endDate %} {{ item.endDate }}{% else %} Present{% endif %}
</p>
{% endif %}
@@ -101,7 +101,7 @@
{% if showTechnologies and item.technologies and item.technologies.length %}
<div class="flex flex-wrap gap-1">
{% for tech in item.technologies %}
<span class="text-xs px-2 py-0.5 rounded
<span class="text-xs px-2 py-0.5 rounded-full
{% if ci == 0 %}bg-amber-50 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300
{% elif ci == 1 %}bg-emerald-50 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300
{% elif ci == 2 %}bg-sky-50 dark:bg-sky-900/30 text-sky-700 dark:text-sky-300

View File

@@ -16,7 +16,7 @@
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 items-start">
{% for item in cv.projects | head(maxItems) %}
{% set ci = loop.index0 % 8 %}
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 transition-colors overflow-hidden border-l-[3px]
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm transition-colors overflow-hidden border-l-[3px]
{% if ci == 0 %}border-l-amber-400 dark:border-l-amber-500
{% elif ci == 1 %}border-l-emerald-400 dark:border-l-emerald-500
{% elif ci == 2 %}border-l-sky-400 dark:border-l-sky-500
@@ -52,7 +52,7 @@
</div>
<div class="flex items-center gap-2 shrink-0">
{% if item.startDate %}
<span class="text-xs text-surface-500 hidden sm:inline">
<span class="text-xs text-surface-600 dark:text-surface-400 hidden sm:inline font-mono">
{{ item.startDate }}{% if item.endDate %} {{ item.endDate }}{% else %} Present{% endif %}
</span>
{% endif %}
@@ -79,7 +79,7 @@
class="px-4 pb-4"
>
{% if item.startDate %}
<p class="text-xs text-surface-500 mb-1 sm:hidden">
<p class="text-xs text-surface-600 dark:text-surface-400 mb-1 sm:hidden font-mono">
{{ item.startDate }}{% if item.endDate %} {{ item.endDate }}{% else %} Present{% endif %}
</p>
{% endif %}
@@ -91,7 +91,7 @@
{% if showTechnologies and item.technologies and item.technologies.length %}
<div class="flex flex-wrap gap-1">
{% for tech in item.technologies %}
<span class="text-xs px-2 py-0.5 rounded
<span class="text-xs px-2 py-0.5 rounded-full
{% if ci == 0 %}bg-amber-50 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300
{% elif ci == 1 %}bg-emerald-50 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300
{% elif ci == 2 %}bg-sky-50 dark:bg-sky-900/30 text-sky-700 dark:text-sky-300

View File

@@ -15,7 +15,7 @@
{% if not filterType or (cv.skillTypes and cv.skillTypes[category] == filterType) or not cv.skillTypes or not cv.skillTypes[category] %}
{# Cycle through 8 distinct colors per family using loop.index0 #}
{% set ci = loop.index0 % 8 %}
<div class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700">
<div class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
<h3 class="font-semibold text-sm uppercase tracking-wide text-surface-600 dark:text-surface-400 mb-2">
{{ category }}
</h3>

View File

@@ -42,7 +42,7 @@
{% set borderClass = "border-l-[3px] border-l-surface-300 dark:border-l-surface-600" %}
{% endif %}
<article class="h-entry p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-surface-400 dark:hover:border-surface-500 transition-colors {{ borderClass }}">
<article class="h-entry p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-accent-400 dark:hover:border-accent-600 transition-colors {{ borderClass }}">
{% if likedUrl %}
{# ── Like card ── #}
@@ -53,14 +53,14 @@
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 text-xs text-surface-500">
<div class="flex items-center gap-3 text-xs text-surface-600 dark:text-surface-400">
<span class="font-medium text-red-600 dark:text-red-400">Liked</span>
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-mono" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
</div>
{{ likedUrl | unfurlCard | safe }}
<a class="u-like-of text-xs text-surface-400 dark:text-surface-500 hover:underline break-all mt-1 inline-block" href="{{ likedUrl }}">
<a class="u-like-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ likedUrl }}">
{{ likedUrl }}
</a>
{% if post.templateContent %}
@@ -68,7 +68,7 @@
{{ post.templateContent | safe }}
</div>
{% endif %}
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}">Permalink</a>
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or (post.date | dateDisplay) }}">Permalink</a>
</div>
</div>
@@ -81,9 +81,9 @@
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 text-xs text-surface-500">
<div class="flex items-center gap-3 text-xs text-surface-600 dark:text-surface-400">
<span class="font-medium text-amber-600 dark:text-amber-400">Bookmarked</span>
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-mono" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
</div>
@@ -93,7 +93,7 @@
</h3>
{% endif %}
{{ bookmarkedUrl | unfurlCard | safe }}
<a class="u-bookmark-of text-xs text-surface-400 dark:text-surface-500 hover:underline break-all mt-1 inline-block" href="{{ bookmarkedUrl }}">
<a class="u-bookmark-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ bookmarkedUrl }}">
{{ bookmarkedUrl }}
</a>
{% if post.templateContent %}
@@ -101,7 +101,7 @@
{{ post.templateContent | safe }}
</div>
{% endif %}
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}">Permalink</a>
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or (post.date | dateDisplay) }}">Permalink</a>
</div>
</div>
@@ -114,14 +114,14 @@
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 text-xs text-surface-500">
<div class="flex items-center gap-3 text-xs text-surface-600 dark:text-surface-400">
<span class="font-medium text-green-600 dark:text-green-400">Reposted</span>
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-mono" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
</div>
{{ repostedUrl | unfurlCard | safe }}
<a class="u-repost-of text-xs text-surface-400 dark:text-surface-500 hover:underline break-all mt-1 inline-block" href="{{ repostedUrl }}">
<a class="u-repost-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ repostedUrl }}">
{{ repostedUrl }}
</a>
{% if post.templateContent %}
@@ -129,7 +129,7 @@
{{ post.templateContent | safe }}
</div>
{% endif %}
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}">Permalink</a>
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or (post.date | dateDisplay) }}">Permalink</a>
</div>
</div>
@@ -142,14 +142,14 @@
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 text-xs text-surface-500">
<div class="flex items-center gap-3 text-xs text-surface-600 dark:text-surface-400">
<span class="font-medium text-sky-600 dark:text-sky-400">In reply to</span>
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-mono" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
</div>
{{ replyToUrl | unfurlCard | safe }}
<a class="u-in-reply-to text-xs text-surface-400 dark:text-surface-500 hover:underline break-all mt-1 inline-block" href="{{ replyToUrl }}">
<a class="u-in-reply-to text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ replyToUrl }}">
{{ replyToUrl }}
</a>
{% if post.templateContent %}
@@ -157,7 +157,7 @@
{{ post.templateContent | safe }}
</div>
{% endif %}
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}">Permalink</a>
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or (post.date | dateDisplay) }}">Permalink</a>
</div>
</div>
@@ -171,9 +171,9 @@
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 text-xs text-surface-500">
<div class="flex items-center gap-3 text-xs text-surface-600 dark:text-surface-400">
<span class="font-medium text-purple-600 dark:text-purple-400">Photo</span>
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-mono" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
</div>
@@ -184,7 +184,7 @@
{% set photoUrl = '/' + photoUrl %}
{% endif %}
<a href="{{ post.url }}" class="photo-link">
<img src="{{ photoUrl }}" alt="{{ img.alt | default('Photo') }}" class="u-photo rounded max-h-48 object-cover" loading="lazy" eleventy:ignore>
<img src="{{ photoUrl }}" alt="{{ img.alt | default('Photo from: ' + post.data.title) }}" class="u-photo rounded max-h-48 object-cover" loading="lazy" eleventy:ignore>
</a>
{% endfor %}
</div>
@@ -193,7 +193,7 @@
{{ post.templateContent | safe }}
</div>
{% endif %}
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}">Permalink</a>
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or (post.date | dateDisplay) }}">Permalink</a>
</div>
</div>
@@ -209,12 +209,12 @@
{{ post.templateContent | striptags | truncate(250) }}
</p>
{% endif %}
<div class="flex items-center gap-3 text-xs text-surface-500">
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<div class="flex items-center gap-3 text-xs text-surface-600 dark:text-surface-400">
<time class="dt-published font-mono" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
{% if post.data.postType %}
<span class="px-2 py-0.5 bg-surface-100 dark:bg-surface-700 rounded">
<span class="px-2 py-0.5 bg-surface-100 dark:bg-surface-700 rounded-full">
{{ post.data.postType }}
</span>
{% endif %}
@@ -222,14 +222,14 @@
{% else %}
{# ── Note card ── #}
<div class="flex items-center gap-3 text-xs text-surface-500 mb-2">
<div class="flex items-center gap-3 text-xs text-surface-600 dark:text-surface-400 mb-2">
<a class="u-url" href="{{ post.url }}">
<time class="dt-published font-medium text-surface-500 dark:text-surface-400" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-medium font-mono text-surface-600 dark:text-surface-400" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
</a>
{% if post.data.postType %}
<span class="px-2 py-0.5 bg-surface-100 dark:bg-surface-700 rounded">
<span class="px-2 py-0.5 bg-surface-100 dark:bg-surface-700 rounded-full">
{{ post.data.postType }}
</span>
{% endif %}
@@ -239,7 +239,7 @@
{{ post.templateContent | safe }}
</div>
{% endif %}
<a href="{{ post.url }}" class="text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block">
<a href="{{ post.url }}" class="text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" aria-label="Permalink: {{ post.data.title or (post.date | dateDisplay) }}">
Permalink
</a>
{% endif %}

View File

@@ -19,6 +19,8 @@
<img
src="{{ authorAvatar }}"
alt="{{ authorName }}"
width="96"
height="96"
class="w-24 h-24 sm:w-32 sm:h-32 rounded-full object-cover shadow-lg flex-shrink-0"
loading="eager"
>

View File

@@ -50,14 +50,14 @@
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 text-xs text-surface-500">
<div class="flex items-center gap-3 text-xs text-surface-600 dark:text-surface-400">
<span class="font-medium text-red-600 dark:text-red-400">Liked</span>
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-mono" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
</div>
{{ likedUrl | unfurlCard | safe }}
<a class="u-like-of text-xs text-surface-400 dark:text-surface-500 hover:underline break-all mt-1 inline-block" href="{{ likedUrl }}">
<a class="u-like-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ likedUrl }}">
{{ likedUrl }}
</a>
{% if post.templateContent %}
@@ -65,7 +65,7 @@
{{ post.templateContent | safe }}
</div>
{% endif %}
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}">Permalink</a>
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or (post.date | dateDisplay) }}">Permalink</a>
</div>
</div>
@@ -78,9 +78,9 @@
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 text-xs text-surface-500">
<div class="flex items-center gap-3 text-xs text-surface-600 dark:text-surface-400">
<span class="font-medium text-amber-600 dark:text-amber-400">Bookmarked</span>
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-mono" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
</div>
@@ -90,7 +90,7 @@
</h3>
{% endif %}
{{ bookmarkedUrl | unfurlCard | safe }}
<a class="u-bookmark-of text-xs text-surface-400 dark:text-surface-500 hover:underline break-all mt-1 inline-block" href="{{ bookmarkedUrl }}">
<a class="u-bookmark-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ bookmarkedUrl }}">
{{ bookmarkedUrl }}
</a>
{% if post.templateContent %}
@@ -98,7 +98,7 @@
{{ post.templateContent | safe }}
</div>
{% endif %}
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}">Permalink</a>
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or (post.date | dateDisplay) }}">Permalink</a>
</div>
</div>
@@ -111,14 +111,14 @@
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 text-xs text-surface-500">
<div class="flex items-center gap-3 text-xs text-surface-600 dark:text-surface-400">
<span class="font-medium text-green-600 dark:text-green-400">Reposted</span>
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-mono" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
</div>
{{ repostedUrl | unfurlCard | safe }}
<a class="u-repost-of text-xs text-surface-400 dark:text-surface-500 hover:underline break-all mt-1 inline-block" href="{{ repostedUrl }}">
<a class="u-repost-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ repostedUrl }}">
{{ repostedUrl }}
</a>
{% if post.templateContent %}
@@ -126,7 +126,7 @@
{{ post.templateContent | safe }}
</div>
{% endif %}
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}">Permalink</a>
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or (post.date | dateDisplay) }}">Permalink</a>
</div>
</div>
@@ -139,14 +139,14 @@
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 text-xs text-surface-500">
<div class="flex items-center gap-3 text-xs text-surface-600 dark:text-surface-400">
<span class="font-medium text-sky-600 dark:text-sky-400">In reply to</span>
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-mono" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
</div>
{{ replyToUrl | unfurlCard | safe }}
<a class="u-in-reply-to text-xs text-surface-400 dark:text-surface-500 hover:underline break-all mt-1 inline-block" href="{{ replyToUrl }}">
<a class="u-in-reply-to text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ replyToUrl }}">
{{ replyToUrl }}
</a>
{% if post.templateContent %}
@@ -154,7 +154,7 @@
{{ post.templateContent | safe }}
</div>
{% endif %}
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}">Permalink</a>
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or (post.date | dateDisplay) }}">Permalink</a>
</div>
</div>
@@ -168,9 +168,9 @@
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 text-xs text-surface-500">
<div class="flex items-center gap-3 text-xs text-surface-600 dark:text-surface-400">
<span class="font-medium text-purple-600 dark:text-purple-400">Photo</span>
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-mono" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
</div>
@@ -181,7 +181,7 @@
{% set photoUrl = '/' + photoUrl %}
{% endif %}
<a href="{{ post.url }}" class="photo-link">
<img src="{{ photoUrl }}" alt="{{ img.alt | default('Photo') }}" class="u-photo rounded max-h-48 object-cover" loading="lazy" eleventy:ignore>
<img src="{{ photoUrl }}" alt="{{ img.alt | default('Photo from: ' + post.data.title) }}" class="u-photo rounded max-h-48 object-cover" loading="lazy" eleventy:ignore>
</a>
{% endfor %}
</div>
@@ -190,7 +190,7 @@
{{ post.templateContent | safe }}
</div>
{% endif %}
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}">Permalink</a>
<a class="u-url text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or (post.date | dateDisplay) }}">Permalink</a>
</div>
</div>
@@ -206,12 +206,12 @@
{{ post.templateContent | striptags | truncate(250) }}
</p>
{% endif %}
<div class="flex items-center gap-3 text-xs text-surface-500">
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<div class="flex items-center gap-3 text-xs text-surface-600 dark:text-surface-400">
<time class="dt-published font-mono" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
{% if post.data.postType %}
<span class="px-2 py-0.5 bg-surface-100 dark:bg-surface-700 rounded">
<span class="px-2 py-0.5 bg-surface-100 dark:bg-surface-700 rounded-full">
{{ post.data.postType }}
</span>
{% endif %}
@@ -219,14 +219,14 @@
{% else %}
{# ── Note card ── #}
<div class="flex items-center gap-3 text-xs text-surface-500 mb-2">
<div class="flex items-center gap-3 text-xs text-surface-600 dark:text-surface-400 mb-2">
<a class="u-url" href="{{ post.url }}">
<time class="dt-published font-medium text-surface-500 dark:text-surface-400" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-medium font-mono text-surface-600 dark:text-surface-400" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
</a>
{% if post.data.postType %}
<span class="px-2 py-0.5 bg-surface-100 dark:bg-surface-700 rounded">
<span class="px-2 py-0.5 bg-surface-100 dark:bg-surface-700 rounded-full">
{{ post.data.postType }}
</span>
{% endif %}
@@ -236,7 +236,7 @@
{{ post.templateContent | safe }}
</div>
{% endif %}
<a href="{{ post.url }}" class="text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block">
<a href="{{ post.url }}" class="text-xs text-accent-600 dark:text-accent-400 hover:underline mt-2 inline-block" aria-label="Permalink: {{ post.data.title or (post.date | dateDisplay) }}">
Permalink
</a>
{% endif %}

View File

@@ -22,6 +22,7 @@
{% elif widget.type == "author-card" %}{% set widgetTitle = "Author" %}
{% elif widget.type == "author-card-compact" %}{% set widgetTitle = "Author" %}
{% elif widget.type == "subscribe" %}{% set widgetTitle = "Subscribe" %}
{% elif widget.type == "ai-usage" %}{% set widgetTitle = "AI Transparency" %}
{% elif widget.type == "custom-html" %}{% set widgetTitle = (widget.config.title if widget.config and widget.config.title) or "Custom" %}
{% else %}{% set widgetTitle = widget.type %}
{% endif %}
@@ -42,17 +43,19 @@
{% elif widget.type == "fediverse-follow" %}
{% set widgetIcon = "user-plus" %}{% set widgetIconClass = "w-5 h-5 text-[#a730b8]" %}{% set widgetBorder = "border-l-[3px] border-l-[#a730b8]" %}
{% elif widget.type == "author-card" or widget.type == "author-card-compact" %}
{% set widgetIcon = "user" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "user" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% elif widget.type == "recent-posts" %}
{% set widgetIcon = "list" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "list" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% elif widget.type == "categories" %}
{% set widgetIcon = "tag" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "tag" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% elif widget.type == "recent-comments" %}
{% set widgetIcon = "chat" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "chat" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% elif widget.type == "search" %}
{% set widgetIcon = "search" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "search" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% elif widget.type == "webmentions" %}
{% set widgetIcon = "share" %}{% set widgetIconClass = "w-5 h-5 text-surface-500" %}{% set widgetBorder = "" %}
{% set widgetIcon = "share" %}{% set widgetIconClass = "w-5 h-5 text-surface-600 dark:text-surface-400" %}{% set widgetBorder = "" %}
{% elif widget.type == "ai-usage" %}
{% set widgetIcon = "zap" %}{% set widgetIconClass = "w-5 h-5 text-amber-500" %}{% set widgetBorder = "border-l-[3px] border-l-amber-400 dark:border-l-amber-500" %}
{% else %}
{% set widgetIcon = "" %}{% set widgetIconClass = "" %}{% set widgetBorder = "" %}
{% endif %}
@@ -79,6 +82,7 @@
class="widget-chevron"
:class="open && 'rotate-180'"
fill="none" stroke="currentColor" viewBox="0 0 24 24"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
@@ -125,10 +129,12 @@
{% include "components/widgets/webmentions.njk" %}
{% elif widget.type == "fediverse-follow" %}
{% include "components/widgets/fediverse-follow.njk" %}
{% elif widget.type == "ai-usage" %}
{% include "components/widgets/ai-usage.njk" ignore missing %}
{% elif widget.type == "custom-html" %}
{% set wConfig = widget.config or {} %}
<is-land on:visible>
<div class="widget">
<div class="widget" role="region" aria-label="Custom content">
{% if wConfig.content %}
<div class="prose dark:prose-invert prose-sm max-w-none">
{{ wConfig.content | safe }}
@@ -153,8 +159,8 @@
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : true }">
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("user", "w-5 h-5 text-surface-500") }} Author</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("user", "w-5 h-5 text-surface-600 dark:text-surface-400") }} Author</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/author-card.njk" %}
@@ -168,7 +174,7 @@
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden border-l-[3px] border-l-[#0085ff]">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("globe", "w-5 h-5 text-[#0085ff]") }} Social Activity</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/social-activity.njk" %}
@@ -182,7 +188,7 @@
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden border-l-[3px] border-l-surface-400 dark:border-l-surface-500">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("github", "w-5 h-5 text-surface-800 dark:text-surface-200") }} GitHub</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/github-repos.njk" %}
@@ -196,7 +202,7 @@
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden border-l-[3px] border-l-purple-400 dark:border-l-purple-500">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("headphones", "w-5 h-5 text-purple-500") }} Listening</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/funkwhale.njk" %}
@@ -209,8 +215,8 @@
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : false }">
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("list", "w-5 h-5 text-surface-500") }} Recent Posts</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("list", "w-5 h-5 text-surface-600 dark:text-surface-400") }} Recent Posts</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/recent-posts.njk" %}
@@ -225,7 +231,7 @@
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden border-l-[3px] border-l-amber-400 dark:border-l-amber-500">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("book-open", "w-5 h-5 text-amber-500") }} Blogroll</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/blogroll.njk" %}
@@ -241,7 +247,7 @@
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden border-l-[3px] border-l-amber-400 dark:border-l-amber-500">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("rss", "w-5 h-5 text-amber-500") }} FeedLand</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/feedland.njk" %}
@@ -255,8 +261,8 @@
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : false }">
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("chat", "w-5 h-5 text-surface-500") }} Recent Comments</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("chat", "w-5 h-5 text-surface-600 dark:text-surface-400") }} Recent Comments</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/recent-comments.njk" %}
@@ -269,8 +275,8 @@
<div class="widget-collapsible mb-4" x-data="{ open: localStorage.getItem('{{ widgetKey }}') !== null ? localStorage.getItem('{{ widgetKey }}') === 'true' : false }">
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<button class="widget-header w-full p-4" @click="open = !open; localStorage.setItem('{{ widgetKey }}', open)" :aria-expanded="open ? 'true' : 'false'">
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("tag", "w-5 h-5 text-surface-500") }} Categories</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
<h3 class="widget-title font-bold text-lg flex items-center gap-2">{{ icon("tag", "w-5 h-5 text-surface-600 dark:text-surface-400") }} Categories</h3>
<svg class="widget-chevron" :class="open && 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-100" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" x-cloak>
{% include "components/widgets/categories.njk" %}

View File

@@ -11,7 +11,7 @@
{# Returns Tailwind color classes for an icon's brand color (light + dark) #}
{% macro socialIconColorClass(name) %}
{%- if name == "activitypub" -%}text-[#f1027e]
{%- if name == "activitypub" -%}text-[#a730b8]
{%- elif name == "github" -%}text-[#181717] dark:text-[#e6edf3]
{%- elif name == "gitlab" -%}text-[#FC6D26]
{%- elif name == "forgejo" -%}text-[#609926]

View File

@@ -28,22 +28,24 @@
{{ likes.length }} Like{% if likes.length != 1 %}s{% endif %}
</h3>
<is-land on:visible>
<div class="facepile">
<ul class="facepile" role="list">
{% for like in likes %}
<li class="inline">
<a href="{{ like.author.url }}"
class="facepile-avatar"
title="{{ like.author.name }}"
aria-label="{{ like.author.name }} (opens in new tab)"
target="_blank"
rel="noopener">
<img
src="{{ like.author.photo or '/images/default-avatar.svg' }}"
alt="{{ like.author.name }}"
alt=""
class="w-8 h-8 rounded-full ring-2 ring-white dark:ring-surface-900"
loading="lazy"
>
</a>
</li>
{% endfor %}
</div>
</ul>
</is-land>
</div>
{% endif %}
@@ -56,22 +58,24 @@
{{ reposts.length }} Repost{% if reposts.length != 1 %}s{% endif %}
</h3>
<is-land on:visible>
<div class="facepile">
<ul class="facepile" role="list">
{% for repost in reposts %}
<li class="inline">
<a href="{{ repost.author.url }}"
class="facepile-avatar"
title="{{ repost.author.name }}"
aria-label="{{ repost.author.name }} (opens in new tab)"
target="_blank"
rel="noopener">
<img
src="{{ repost.author.photo or '/images/default-avatar.svg' }}"
alt="{{ repost.author.name }}"
alt=""
class="w-8 h-8 rounded-full ring-2 ring-white dark:ring-surface-900"
loading="lazy"
>
</a>
</li>
{% endfor %}
</div>
</ul>
</is-land>
</div>
{% endif %}
@@ -84,22 +88,24 @@
{{ bookmarks.length }} Bookmark{% if bookmarks.length != 1 %}s{% endif %}
</h3>
<is-land on:visible>
<div class="facepile">
<ul class="facepile" role="list">
{% for bookmark in bookmarks %}
<li class="inline">
<a href="{{ bookmark.author.url }}"
class="facepile-avatar"
title="{{ bookmark.author.name }}"
aria-label="{{ bookmark.author.name }} (opens in new tab)"
target="_blank"
rel="noopener">
<img
src="{{ bookmark.author.photo or '/images/default-avatar.svg' }}"
alt="{{ bookmark.author.name }}"
alt=""
class="w-8 h-8 rounded-full ring-2 ring-white dark:ring-surface-900"
loading="lazy"
>
</a>
</li>
{% endfor %}
</div>
</ul>
</is-land>
</div>
{% endif %}
@@ -113,7 +119,7 @@
</h3>
<ul class="space-y-4">
{% for reply in replies %}
<li class="p-4 bg-surface-100 dark:bg-surface-800 rounded-lg">
<li class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
<div class="flex gap-3">
<a href="{{ reply.author.url }}" target="_blank" rel="noopener">
<img
@@ -132,10 +138,10 @@
{{ reply.author.name }}
</a>
<a href="{{ reply.url }}"
class="text-xs text-surface-500 hover:underline"
class="text-xs text-surface-600 dark:text-surface-400 hover:underline"
target="_blank"
rel="noopener">
<time datetime="{{ reply.published }}">
<time class="font-mono" datetime="{{ reply.published }}">
{{ reply.published | date("MMM d, yyyy") }}
</time>
</a>
@@ -165,7 +171,7 @@
class="text-accent-600 dark:text-accent-400 hover:underline"
target="_blank"
rel="noopener">
{{ mention.author.name }} mentioned this on <time datetime="{{ mention.published }}">{{ mention.published | date("MMM d, yyyy") }}</time>
{{ mention.author.name }} mentioned this on <time class="font-mono" datetime="{{ mention.published }}">{{ mention.published | date("MMM d, yyyy") }}</time>
</a>
</li>
{% endfor %}
@@ -177,24 +183,26 @@
{# Webmention send form — collapsed by default #}
<details class="mt-8">
<summary class="text-sm font-semibold text-surface-600 dark:text-surface-400 cursor-pointer hover:text-surface-700 dark:hover:text-surface-300 list-none [&::-webkit-details-marker]:hidden flex items-center gap-1.5">
<summary class="text-sm font-semibold text-surface-600 dark:text-surface-400 cursor-pointer hover:text-surface-700 dark:hover:text-surface-300 transition-colors list-none [&::-webkit-details-marker]:hidden flex items-center gap-1.5">
<svg class="w-3.5 h-3.5 transition-transform [[open]>&]:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
Send a Webmention
</summary>
<div class="mt-3 p-4 bg-surface-100 dark:bg-surface-800 rounded-lg">
<div class="mt-3 p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
<p class="text-xs text-surface-600 dark:text-surface-400 mb-3">
Have you written a response to this post? Send a webmention by entering your post URL below.
</p>
<form action="https://webmention.io/{{ site.webmentions.domain }}/webmention" method="post" class="flex gap-2">
<input type="hidden" name="target" value="{{ site.url }}{{ page.url }}">
<label for="webmention-source" class="sr-only">Your post URL</label>
<input
id="webmention-source"
type="url"
name="source"
placeholder="https://your-site.com/response"
required
class="flex-1 px-3 py-2 text-sm bg-surface-50 dark:bg-surface-700 border border-surface-300 dark:border-surface-600 rounded focus:outline-none focus:ring-2 focus:ring-accent-500"
class="flex-1 px-3 py-2 text-sm bg-surface-50 dark:bg-surface-700 border border-surface-300 dark:border-surface-600 rounded"
>
<button
type="submit"

View File

@@ -0,0 +1,62 @@
{# AI Usage Widget — compact sidebar version of the /ai/ page AI transparency graph #}
{% set stats = collections.posts | aiStats %}
{% set aiPostsList = collections.posts | aiPosts %}
{% if stats and stats.total > 0 %}
<is-land on:visible>
<div class="widget">
<h3 class="widget-title flex items-center gap-2">
<svg class="w-5 h-5 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
AI Transparency
</h3>
{# Mini stats — 2x2 grid #}
<div class="grid grid-cols-2 gap-2 mb-3">
<div class="text-center p-2 rounded-lg bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700">
<div class="text-lg font-bold text-surface-900 dark:text-surface-100">{{ stats.total }}</div>
<div class="text-[10px] text-surface-600 dark:text-surface-400">Total</div>
</div>
<div class="text-center p-2 rounded-lg bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700">
<div class="text-lg font-bold text-amber-600 dark:text-amber-400">{{ stats.aiCount }}</div>
<div class="text-[10px] text-surface-600 dark:text-surface-400">AI-involved</div>
</div>
<div class="text-center p-2 rounded-lg bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700">
<div class="text-lg font-bold text-emerald-600 dark:text-emerald-400">{{ stats.total - stats.aiCount }}</div>
<div class="text-[10px] text-surface-600 dark:text-surface-400">Human-only</div>
</div>
<div class="text-center p-2 rounded-lg bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700">
<div class="text-lg font-bold text-surface-900 dark:text-surface-100">{{ stats.percentage }}%</div>
<div class="text-[10px] text-surface-600 dark:text-surface-400">AI ratio</div>
</div>
</div>
{# Level breakdown — compact pills #}
<div class="flex flex-wrap gap-1.5 text-[10px] mb-3">
<span class="px-2 py-0.5 rounded-full bg-surface-100 dark:bg-surface-700 text-surface-600 dark:text-surface-300">
L0: {{ stats.byLevel[0] }}
</span>
<span class="px-2 py-0.5 rounded-full bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-300">
L1: {{ stats.byLevel[1] }}
</span>
<span class="px-2 py-0.5 rounded-full bg-amber-100 dark:bg-amber-900/40 text-amber-800 dark:text-amber-200">
L2: {{ stats.byLevel[2] }}
</span>
<span class="px-2 py-0.5 rounded-full bg-amber-200 dark:bg-amber-900/60 text-amber-900 dark:text-amber-100">
L3: {{ stats.byLevel[3] }}
</span>
</div>
{# Compact post-graph — current year only, AI posts highlighted #}
{% if aiPostsList and aiPostsList.length %}
<div class="text-[10px] text-surface-600 dark:text-surface-400 mb-2">AI-involved posts this year</div>
{% postGraph aiPostsList, { prefix: "ai-widget", limit: 1, noLabels: true, boxColorDark: "#44403c", highlightColorLight: "#d97706", highlightColorDark: "#fbbf24" } %}
{% endif %}
<a href="/ai/" class="text-sm text-amber-600 dark:text-amber-400 hover:underline flex items-center gap-1 mt-3">
View full AI report
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
</a>
</div>
</is-land>
{% endif %}

View File

@@ -4,21 +4,21 @@
<div class="h-card p-author flex items-center gap-3">
{# Hidden u-photo for reliable microformat parsing #}
<data class="u-photo hidden" value="{{ site.author.avatar }}"></data>
<a href="{{ site.author.url }}" class="u-url u-uid" rel="me" itemprop="url">
<a href="{{ site.author.url }}" class="u-url u-uid" rel="me" itemprop="url" aria-label="{{ site.author.name }}">
<img
src="{{ site.author.avatar }}"
alt="{{ site.author.name }}"
class="w-12 h-12 rounded-full object-cover"
alt=""
class="w-12 h-12 rounded-full object-cover shadow-lg"
loading="lazy"
>
</a>
<div>
<a href="{{ site.author.url }}" class="u-url p-name font-medium text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">
<a href="{{ site.author.url }}" class="u-url p-name font-medium text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400 transition-colors">
{{ site.author.name }}
</a>
<p class="p-job-title text-xs text-surface-500">{{ site.author.title }}</p>
<p class="p-job-title text-xs text-surface-600 dark:text-surface-400">{{ site.author.title }}</p>
{% if site.author.locality %}
<p class="p-locality text-xs text-surface-500">{{ site.author.locality }}{% if site.author.country %}, <span class="p-country-name">{{ site.author.country }}</span>{% endif %}</p>
<p class="p-locality text-xs text-surface-600 dark:text-surface-400">{{ site.author.locality }}{% if site.author.country %}, <span class="p-country-name">{{ site.author.country }}</span>{% endif %}</p>
{% endif %}
</div>
</div>

View File

@@ -9,25 +9,29 @@
</h3>
{# Source tabs - only shown when multiple sources exist #}
<div x-show="tabs.length > 1" class="flex gap-1 mt-3 mb-2 border-b border-surface-200 dark:border-surface-700">
<div x-show="tabs.length > 1" class="flex gap-1 mt-3 mb-2 border-b border-surface-200 dark:border-surface-700" role="tablist" aria-label="Blogroll sources">
<template x-for="tab in tabs" :key="tab.key">
<button
role="tab"
:id="'blogroll-tab-' + tab.key"
:aria-selected="(activeTab === tab.key).toString()"
aria-controls="blogroll-panel"
@click="activeTab = tab.key"
:class="activeTab === tab.key
? 'border-b-2 border-accent-600 text-accent-600 dark:text-accent-400 dark:border-accent-400'
: 'text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
: 'text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
class="px-2 py-1 text-xs font-medium transition-colors -mb-px"
x-text="tab.label + ' (' + tab.count + ')'"
></button>
</template>
</div>
<ul x-show="filteredBlogs.length > 0" class="space-y-2" :class="tabs.length > 1 ? '' : 'mt-3'">
<ul x-show="filteredBlogs.length > 0" class="space-y-2" :class="tabs.length > 1 ? '' : 'mt-3'" role="tabpanel" id="blogroll-panel" :aria-labelledby="'blogroll-tab-' + activeTab">
<template x-for="blog in filteredBlogs.slice(0, 8)" :key="blog.id">
<li>
<a
:href="blog.siteUrl || blog.feedUrl"
class="flex items-center gap-2 text-sm text-surface-700 dark:text-surface-300 hover:text-accent-600 dark:hover:text-accent-400 transition-colors"
class="flex items-center gap-2 text-sm text-surface-700 dark:text-surface-300 hover:text-accent-600 dark:hover:text-accent-400 hover:underline transition-colors"
target="_blank"
rel="noopener"
>
@@ -40,7 +44,7 @@
</template>
</ul>
<div x-show="filteredBlogs.length === 0 && !loading" class="text-sm text-surface-500 py-2">
<div x-show="filteredBlogs.length === 0 && !loading" class="text-sm text-surface-600 dark:text-surface-400 py-2">
No blogs loaded yet.
</div>

View File

@@ -5,7 +5,7 @@
<h3 class="widget-title">Categories</h3>
<div class="flex flex-wrap gap-2">
{% for category in categories %}
<a href="/categories/{{ category | slugify }}/" class="p-category">
<a href="/categories/{{ category | slugify }}/" class="p-category hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors">
{{ category }}
</a>
{% endfor %}

View File

@@ -20,14 +20,12 @@
<is-land on:visible>
<div class="widget" x-data="fediverseInteract('{{ actorUrl }}', 'interact')">
<h3 class="widget-title">Follow Me</h3>
<p class="text-sm text-surface-500 dark:text-surface-400 mb-3">Follow me from your fediverse instance.</p>
<p class="text-sm text-surface-600 dark:text-surface-400 mb-3">Follow me from your fediverse instance.</p>
<a href="{{ actorUrl }}"
@click="handleClick($event)"
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-[#a730b8]/10 text-[#a730b8] hover:bg-[#a730b8]/20 transition-colors text-sm font-medium cursor-pointer"
title="Follow from your fediverse instance (Shift+click to change)">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="20" y1="8" x2="20" y2="14"/><line x1="23" y1="11" x2="17" y2="11"/>
</svg>
aria-label="Follow on the Fediverse (use the modal to pick your instance)">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M13.09 4.43L24 10.73v2.51L13.09 19.58v-2.51L21.83 12 13.09 6.98v-2.55zM13.09 9.49L17.44 12l-4.35 2.51V9.49z"/><path d="M10.91 4.43L0 10.73v2.51l8.74-5.03v10.09l2.18 1.28V4.43zM6.56 12L2.18 14.51l4.35 2.51V12z"/></svg>
<span>Follow on the Fediverse</span>
</a>
{% set modalTitle = "Follow on the Fediverse" %}

View File

@@ -240,8 +240,8 @@
{# Sort links #}
<div class="fl-sort">
<span :class="sortBy === 'title' ? 'selected' : ''" @click="sortBy = 'title'">Title</span>
<span :class="sortBy === 'when' ? 'selected' : ''" @click="sortBy = 'when'">When</span>
<span :class="sortBy === 'title' ? 'selected' : ''" @click="sortBy = 'title'" @keydown.enter="sortBy = 'title'" @keydown.space.prevent="sortBy = 'title'" tabindex="0" role="button" :aria-pressed="sortBy === 'title'">Title</span>
<span :class="sortBy === 'when' ? 'selected' : ''" @click="sortBy = 'when'" @keydown.enter="sortBy = 'when'" @keydown.space.prevent="sortBy = 'when'" tabindex="0" role="button" :aria-pressed="sortBy === 'when'">When</span>
</div>
{# Feed list — pure divs, no table #}
@@ -253,7 +253,13 @@
<span class="fl-caret"
:class="expandedId === blog.id ? 'fl-caret-dark' : (selectedId === blog.id ? 'fl-caret-dark' : 'fl-caret-light')"
x-text="expandedId === blog.id ? '\u25BC' : '\u25B6'"
@click.stop="toggleExpand(blog)"></span>
@click.stop="toggleExpand(blog)"
@keydown.enter.stop="toggleExpand(blog)"
@keydown.space.prevent.stop="toggleExpand(blog)"
tabindex="0"
role="button"
:aria-label="expandedId === blog.id ? 'Collapse ' + blog.title : 'Expand ' + blog.title"
:aria-expanded="expandedId === blog.id"></span>
<span class="fl-name">
<a :href="blog.siteUrl || blog.feedUrl" target="_blank" rel="noopener"
x-text="blog.title" @click.stop></a>

View File

@@ -20,7 +20,7 @@
{% set npColor = "purple" if fwNow else "red" %}
<div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-3 mb-3">
<div class="flex items-center gap-1.5 text-xs text-green-600 dark:text-green-400 mb-2">
<span class="flex gap-0.5 items-end h-2.5">
<span class="flex gap-0.5 items-end h-2.5" aria-hidden="true">
<span class="w-0.5 bg-green-500 animate-pulse" style="height: 30%;"></span>
<span class="w-0.5 bg-green-500 animate-pulse" style="height: 70%; animation-delay: 0.2s;"></span>
<span class="w-0.5 bg-green-500 animate-pulse" style="height: 50%; animation-delay: 0.4s;"></span>
@@ -30,7 +30,7 @@
</div>
<div class="flex items-center gap-3">
{% if np.coverUrl %}
<img src="{{ np.coverUrl }}" alt="" class="w-10 h-10 rounded object-cover flex-shrink-0" loading="lazy" eleventy:ignore>
<img src="{{ np.coverUrl }}" alt="" class="w-10 h-10 rounded object-cover flex-shrink-0 shadow-lg" loading="lazy" eleventy:ignore>
{% endif %}
<div class="min-w-0 flex-1">
<p class="font-medium text-sm text-surface-900 dark:text-surface-100 truncate">
@@ -52,7 +52,7 @@
{% for listening in funkwhaleActivity.listenings | head(2) %}
<li class="flex items-center gap-2">
{% if listening.coverUrl %}
<img src="{{ listening.coverUrl }}" alt="" class="w-8 h-8 rounded object-cover flex-shrink-0" loading="lazy" eleventy:ignore>
<img src="{{ listening.coverUrl }}" alt="" class="w-8 h-8 rounded object-cover flex-shrink-0 shadow-lg" loading="lazy" eleventy:ignore>
{% else %}
<div class="w-8 h-8 rounded bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center flex-shrink-0">
<svg class="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -68,7 +68,7 @@
{{ listening.track }}
{% endif %}
</p>
<p class="text-xs text-surface-500 truncate">{{ listening.artist }}
<p class="text-xs text-surface-600 dark:text-surface-400 truncate">{{ listening.artist }}
<span class="text-purple-500 ml-1">Funkwhale</span>
</p>
</div>
@@ -80,7 +80,7 @@
{% for scrobble in lastfmActivity.scrobbles | head(2) %}
<li class="flex items-center gap-2">
{% if scrobble.coverUrl %}
<img src="{{ scrobble.coverUrl }}" alt="" class="w-8 h-8 rounded object-cover flex-shrink-0" loading="lazy" eleventy:ignore>
<img src="{{ scrobble.coverUrl }}" alt="" class="w-8 h-8 rounded object-cover flex-shrink-0 shadow-lg" loading="lazy" eleventy:ignore>
{% else %}
<div class="w-8 h-8 rounded bg-red-100 dark:bg-red-900/30 flex items-center justify-center flex-shrink-0">
<svg class="w-4 h-4 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -97,7 +97,7 @@
{% endif %}
{% if scrobble.loved %}<span class="text-red-500 ml-0.5">&#9829;</span>{% endif %}
</p>
<p class="text-xs text-surface-500 truncate">{{ scrobble.artist }}
<p class="text-xs text-surface-600 dark:text-surface-400 truncate">{{ scrobble.artist }}
<span class="text-red-500 ml-1">Last.fm</span>
</p>
</div>

View File

@@ -9,31 +9,39 @@
</h3>
{# Tab buttons — order: Commits, Repos, Featured, PRs #}
<div class="flex gap-1 mb-4 border-b border-surface-200 dark:border-surface-700">
<div class="flex gap-1 mb-4 border-b border-surface-200 dark:border-surface-700" role="tablist" aria-label="GitHub activity">
<button
@click="activeTab = 'commits'"
:class="activeTab === 'commits' ? 'border-b-2 border-accent-500 text-accent-600 dark:text-accent-400' : 'text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
:class="activeTab === 'commits' ? 'border-b-2 border-accent-500 text-accent-600 dark:text-accent-400' : 'text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
:aria-selected="(activeTab === 'commits').toString()"
role="tab" id="gh-tab-commits" aria-controls="gh-panel-commits"
class="flex items-center gap-1.5 px-2 py-2 text-xs font-medium transition-colors -mb-px"
>
Commits
</button>
<button
@click="activeTab = 'repos'"
:class="activeTab === 'repos' ? 'border-b-2 border-accent-500 text-accent-600 dark:text-accent-400' : 'text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
:class="activeTab === 'repos' ? 'border-b-2 border-accent-500 text-accent-600 dark:text-accent-400' : 'text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
:aria-selected="(activeTab === 'repos').toString()"
role="tab" id="gh-tab-repos" aria-controls="gh-panel-repos"
class="flex items-center gap-1.5 px-2 py-2 text-xs font-medium transition-colors -mb-px"
>
Repos
</button>
<button
@click="activeTab = 'featured'"
:class="activeTab === 'featured' ? 'border-b-2 border-accent-500 text-accent-600 dark:text-accent-400' : 'text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
:class="activeTab === 'featured' ? 'border-b-2 border-accent-500 text-accent-600 dark:text-accent-400' : 'text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
:aria-selected="(activeTab === 'featured').toString()"
role="tab" id="gh-tab-featured" aria-controls="gh-panel-featured"
class="flex items-center gap-1.5 px-2 py-2 text-xs font-medium transition-colors -mb-px"
>
Featured
</button>
<button
@click="activeTab = 'prs'"
:class="activeTab === 'prs' ? 'border-b-2 border-accent-500 text-accent-600 dark:text-accent-400' : 'text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
:class="activeTab === 'prs' ? 'border-b-2 border-accent-500 text-accent-600 dark:text-accent-400' : 'text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
:aria-selected="(activeTab === 'prs').toString()"
role="tab" id="gh-tab-prs" aria-controls="gh-panel-prs"
class="flex items-center gap-1.5 px-2 py-2 text-xs font-medium transition-colors -mb-px"
>
PRs
@@ -44,31 +52,31 @@
<div class="h-[420px] overflow-y-auto">
{# Loading state #}
<div x-show="loading" class="text-sm text-surface-500 py-4 text-center">
<div x-show="loading" class="text-sm text-surface-600 dark:text-surface-400 py-4 text-center" role="status" aria-live="polite">
Loading...
</div>
{# Commits Tab #}
<div x-show="activeTab === 'commits' && !loading" x-cloak>
<div x-show="activeTab === 'commits' && !loading" x-cloak role="tabpanel" id="gh-panel-commits" aria-labelledby="gh-tab-commits">
<ul x-show="commits.length > 0" class="space-y-3">
<template x-for="commit in commits.slice(0, 5)" :key="commit.sha">
<li class="border-b border-surface-200 dark:border-surface-700 pb-3 last:border-0">
<a :href="commit.url" target="_blank" rel="noopener" class="block group">
<p class="text-sm text-surface-700 dark:text-surface-300 group-hover:text-surface-900 dark:group-hover:text-surface-100 transition-colors line-clamp-2" x-text="commit.message"></p>
<div class="flex items-center gap-2 mt-1.5 text-xs text-surface-500">
<div class="flex items-center gap-2 mt-1.5 text-xs text-surface-600 dark:text-surface-400">
<code class="text-xs font-mono bg-surface-100 dark:bg-surface-800 px-1 py-0.5 rounded" x-text="commit.sha"></code>
<span class="truncate" x-text="commit.repo?.split('/')[1] || commit.repo"></span>
<span x-text="formatDate(commit.date)"></span>
<span class="font-mono" x-text="formatDate(commit.date)"></span>
</div>
</a>
</li>
</template>
</ul>
<div x-show="commits.length === 0" class="text-sm text-surface-500 py-2">No recent commits.</div>
<div x-show="commits.length === 0" class="text-sm text-surface-600 dark:text-surface-400 py-2">No recent commits.</div>
</div>
{# Repos Tab #}
<div x-show="activeTab === 'repos' && !loading" x-cloak>
<div x-show="activeTab === 'repos' && !loading" x-cloak role="tabpanel" id="gh-panel-repos" aria-labelledby="gh-tab-repos">
<ul x-show="repos.length > 0" class="space-y-3">
<template x-for="repo in repos.slice(0, 5)" :key="repo.name">
<li class="border-b border-surface-200 dark:border-surface-700 pb-3 last:border-0">
@@ -78,22 +86,22 @@
<span x-show="repo.language" class="text-xs px-1.5 py-0.5 rounded bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-400 flex-shrink-0" x-text="repo.language"></span>
</div>
<p x-show="repo.description" class="text-xs text-surface-600 dark:text-surface-400 mt-1 line-clamp-2" x-text="repo.description"></p>
<div class="flex items-center gap-3 mt-1.5 text-xs text-surface-500">
<div class="flex items-center gap-3 mt-1.5 text-xs text-surface-600 dark:text-surface-400">
<span x-show="repo.stargazers_count > 0" class="flex items-center gap-1">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>
<span x-text="repo.stargazers_count"></span>
</span>
<span x-text="formatDate(repo.updated_at)"></span>
<span class="font-mono" x-text="formatDate(repo.updated_at)"></span>
</div>
</a>
</li>
</template>
</ul>
<div x-show="repos.length === 0" class="text-sm text-surface-500 py-2">No repositories found.</div>
<div x-show="repos.length === 0" class="text-sm text-surface-600 dark:text-surface-400 py-2">No repositories found.</div>
</div>
{# Featured Tab #}
<div x-show="activeTab === 'featured' && !loading" x-cloak>
<div x-show="activeTab === 'featured' && !loading" x-cloak role="tabpanel" id="gh-panel-featured" aria-labelledby="gh-tab-featured">
<ul x-show="featured.length > 0" class="space-y-3">
<template x-for="repo in featured.slice(0, 5)" :key="repo.fullName || repo.name">
<li class="border-b border-surface-200 dark:border-surface-700 pb-3 last:border-0">
@@ -103,7 +111,7 @@
<span x-show="repo.language" class="text-xs px-1.5 py-0.5 rounded bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-400 flex-shrink-0" x-text="repo.language"></span>
</div>
<p x-show="repo.description" class="text-xs text-surface-600 dark:text-surface-400 mt-1 line-clamp-2" x-text="repo.description"></p>
<div class="flex items-center gap-3 mt-1.5 text-xs text-surface-500">
<div class="flex items-center gap-3 mt-1.5 text-xs text-surface-600 dark:text-surface-400">
<span x-show="repo.stars > 0" class="flex items-center gap-1">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>
<span x-text="repo.stars"></span>
@@ -117,11 +125,11 @@
</li>
</template>
</ul>
<div x-show="featured.length === 0" class="text-sm text-surface-500 py-2">No featured projects.</div>
<div x-show="featured.length === 0" class="text-sm text-surface-600 dark:text-surface-400 py-2">No featured projects.</div>
</div>
{# PRs Tab #}
<div x-show="activeTab === 'prs' && !loading" x-cloak>
<div x-show="activeTab === 'prs' && !loading" x-cloak role="tabpanel" id="gh-panel-prs" aria-labelledby="gh-tab-prs">
<ul x-show="contributions.length > 0" class="space-y-3">
<template x-for="item in contributions.slice(0, 5)" :key="item.url">
<li class="border-b border-surface-200 dark:border-surface-700 pb-3 last:border-0">
@@ -136,16 +144,16 @@
</span>
<span class="text-sm text-surface-700 dark:text-surface-300 group-hover:text-surface-900 dark:group-hover:text-surface-100 transition-colors truncate" x-text="item.title"></span>
</div>
<div class="flex items-center gap-2 mt-1.5 text-xs text-surface-500 pl-6">
<div class="flex items-center gap-2 mt-1.5 text-xs text-surface-600 dark:text-surface-400 pl-6">
<span x-text="item.repo?.split('/')[1] || item.repo"></span>
<span x-show="item.number" x-text="'#' + item.number"></span>
<span x-text="formatDate(item.date)"></span>
<span class="font-mono" x-text="formatDate(item.date)"></span>
</div>
</a>
</li>
</template>
</ul>
<div x-show="contributions.length === 0" class="text-sm text-surface-500 py-2">No recent PRs or issues.</div>
<div x-show="contributions.length === 0" class="text-sm text-surface-600 dark:text-surface-400 py-2">No recent PRs or issues.</div>
</div>
</div>

View File

@@ -5,12 +5,12 @@
<h3 class="widget-title">Categories</h3>
<div class="flex flex-wrap gap-2">
{% if category is string %}
<a href="/categories/{{ category | slugify }}/" class="p-category text-xs px-2 py-1 bg-accent-100 dark:bg-accent-900 text-accent-700 dark:text-accent-300 rounded-full hover:bg-accent-200 dark:hover:bg-accent-800 transition-colors">
<a href="/categories/{{ category | slugify }}/" class="p-category text-xs px-2 py-1 bg-accent-100 dark:bg-accent-900/30 text-accent-700 dark:text-accent-300 rounded-full hover:bg-accent-200 dark:hover:bg-accent-800 transition-colors">
{{ category }}
</a>
{% else %}
{% for cat in category %}
<a href="/categories/{{ cat | slugify }}/" class="p-category text-xs px-2 py-1 bg-accent-100 dark:bg-accent-900 text-accent-700 dark:text-accent-300 rounded-full hover:bg-accent-200 dark:hover:bg-accent-800 transition-colors">
<a href="/categories/{{ cat | slugify }}/" class="p-category text-xs px-2 py-1 bg-accent-100 dark:bg-accent-900/30 text-accent-700 dark:text-accent-300 rounded-full hover:bg-accent-200 dark:hover:bg-accent-800 transition-colors">
{{ cat }}
</a>
{% endfor %}

View File

@@ -10,7 +10,7 @@
<div class="space-y-3">
{% if _prevPost %}
<div class="border-b border-surface-200 dark:border-surface-700 pb-3">
<span class="text-xs text-surface-500 uppercase tracking-wide block mb-1">Previous</span>
<span class="text-xs text-surface-600 dark:text-surface-400 uppercase tracking-wide block mb-1">Previous</span>
{% set _likedUrl = _prevPost.data.likeOf or _prevPost.data.like_of %}
{% set _bookmarkedUrl = _prevPost.data.bookmarkOf or _prevPost.data.bookmark_of %}
{% set _repostedUrl = _prevPost.data.repostOf or _prevPost.data.repost_of %}
@@ -36,7 +36,7 @@
{% endif %}
{% if _nextPost %}
<div>
<span class="text-xs text-surface-500 uppercase tracking-wide block mb-1">Next</span>
<span class="text-xs text-surface-600 dark:text-surface-400 uppercase tracking-wide block mb-1">Next</span>
{% set _likedUrl = _nextPost.data.likeOf or _nextPost.data.like_of %}
{% set _bookmarkedUrl = _nextPost.data.bookmarkOf or _nextPost.data.bookmark_of %}
{% set _repostedUrl = _nextPost.data.repostOf or _nextPost.data.repost_of %}

View File

@@ -9,7 +9,7 @@
<div class="flex items-start gap-2">
{% if comment.author and comment.author.photo %}
<img src="{{ comment.author.photo }}" alt="{{ comment.author.name }}"
class="w-6 h-6 rounded-full flex-shrink-0 mt-0.5" loading="lazy">
class="w-6 h-6 rounded-full flex-shrink-0 mt-0.5 shadow-lg" loading="lazy">
{% endif %}
<div>
<span class="font-medium">{{ comment.author.name or "Anonymous" }}</span>

View File

@@ -20,7 +20,7 @@
<a href="{{ post.url }}" class="text-sm text-accent-600 dark:text-accent-400 hover:underline line-clamp-1">
Liked {{ _likedUrl | replace("https://", "") | truncate(40) }}
</a>
<time class="text-xs text-surface-500 block" datetime="{{ post.data.published or post.date }}">
<time class="text-xs text-surface-600 dark:text-surface-400 block font-mono" datetime="{{ post.data.published or post.date }}">
{{ (post.data.published or post.date) | dateDisplay }}
</time>
</div>
@@ -33,7 +33,7 @@
<a href="{{ post.url }}" class="text-sm text-accent-600 dark:text-accent-400 hover:underline line-clamp-1">
{{ post.data.title or ("Bookmarked " + (_bookmarkedUrl | replace("https://", "") | truncate(35))) }}
</a>
<time class="text-xs text-surface-500 block" datetime="{{ post.data.published or post.date }}">
<time class="text-xs text-surface-600 dark:text-surface-400 block font-mono" datetime="{{ post.data.published or post.date }}">
{{ (post.data.published or post.date) | dateDisplay }}
</time>
</div>
@@ -46,7 +46,7 @@
<a href="{{ post.url }}" class="text-sm text-accent-600 dark:text-accent-400 hover:underline line-clamp-1">
Reposted {{ _repostedUrl | replace("https://", "") | truncate(40) }}
</a>
<time class="text-xs text-surface-500 block" datetime="{{ post.data.published or post.date }}">
<time class="text-xs text-surface-600 dark:text-surface-400 block font-mono" datetime="{{ post.data.published or post.date }}">
{{ (post.data.published or post.date) | dateDisplay }}
</time>
</div>
@@ -59,7 +59,7 @@
<a href="{{ post.url }}" class="text-sm text-sky-600 dark:text-sky-400 hover:underline line-clamp-1">
Reply to {{ _replyToUrl | replace("https://", "") | truncate(40) }}
</a>
<time class="text-xs text-surface-500 block" datetime="{{ post.data.published or post.date }}">
<time class="text-xs text-surface-600 dark:text-surface-400 block font-mono" datetime="{{ post.data.published or post.date }}">
{{ (post.data.published or post.date) | dateDisplay }}
</time>
</div>
@@ -69,7 +69,7 @@
<a href="{{ post.url }}" class="text-sm text-accent-600 dark:text-accent-400 hover:underline line-clamp-2">
{{ post.data.title or post.data.name or (post.templateContent | striptags | truncate(50)) or "Note" }}
</a>
<time class="text-xs text-surface-500 block" datetime="{{ post.data.published or post.date }}">
<time class="text-xs text-surface-600 dark:text-surface-400 block font-mono" datetime="{{ post.data.published or post.date }}">
{{ (post.data.published or post.date) | dateDisplay }}
</time>
{% endif %}

View File

@@ -19,10 +19,10 @@
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
</svg>
<div class="min-w-0">
<a href="{{ post.url }}" class="text-sm text-accent-600 dark:text-accent-400 hover:underline break-all line-clamp-1">
<a href="{{ post.url }}" class="text-sm text-red-600 dark:text-red-400 hover:underline break-all line-clamp-1">
Liked {{ likedUrl | replace("https://", "") | truncate(40) }}
</a>
<time class="text-xs text-surface-500 block" datetime="{{ post.data.published }}">
<time class="text-xs text-surface-600 dark:text-surface-400 block font-mono" datetime="{{ post.data.published }}">
{{ (post.data.published or post.date) | dateDisplay }}
</time>
</div>
@@ -34,10 +34,10 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"/>
</svg>
<div class="min-w-0">
<a href="{{ post.url }}" class="text-sm text-accent-600 dark:text-accent-400 hover:underline line-clamp-1">
<a href="{{ post.url }}" class="text-sm text-amber-600 dark:text-amber-400 hover:underline line-clamp-1">
{{ post.data.title or ("Bookmarked " + (bookmarkedUrl | replace("https://", "") | truncate(35))) }}
</a>
<time class="text-xs text-surface-500 block" datetime="{{ post.data.published }}">
<time class="text-xs text-surface-600 dark:text-surface-400 block font-mono" datetime="{{ post.data.published }}">
{{ (post.data.published or post.date) | dateDisplay }}
</time>
</div>
@@ -49,10 +49,10 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
<div class="min-w-0">
<a href="{{ post.url }}" class="text-sm text-accent-600 dark:text-accent-400 hover:underline break-all line-clamp-1">
<a href="{{ post.url }}" class="text-sm text-green-600 dark:text-green-400 hover:underline break-all line-clamp-1">
Reposted {{ repostedUrl | replace("https://", "") | truncate(40) }}
</a>
<time class="text-xs text-surface-500 block" datetime="{{ post.data.published }}">
<time class="text-xs text-surface-600 dark:text-surface-400 block font-mono" datetime="{{ post.data.published }}">
{{ (post.data.published or post.date) | dateDisplay }}
</time>
</div>
@@ -67,20 +67,43 @@
<a href="{{ post.url }}" class="text-sm text-sky-600 dark:text-sky-400 hover:underline break-all line-clamp-1">
Reply to {{ replyToUrl | replace("https://", "") | truncate(40) }}
</a>
<time class="text-xs text-surface-500 block" datetime="{{ post.data.published }}">
<time class="text-xs text-surface-600 dark:text-surface-400 block font-mono" datetime="{{ post.data.published }}">
{{ (post.data.published or post.date) | dateDisplay }}
</time>
</div>
</div>
{% elif post.data.title %}
{# Article #}
<div class="flex items-start gap-2">
<svg class="w-4 h-4 text-indigo-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<div class="min-w-0">
<a href="{{ post.url }}" class="text-sm text-indigo-600 dark:text-indigo-400 hover:underline line-clamp-1">
{{ post.data.title }}
</a>
<time class="text-xs text-surface-600 dark:text-surface-400 block font-mono" datetime="{{ post.data.published }}">
{{ (post.data.published or post.date) | dateDisplay }}
</time>
</div>
</div>
{% else %}
{# Article / Note / other #}
<a href="{{ post.url }}" class="text-sm text-accent-600 dark:text-accent-400 hover:underline line-clamp-1">
{{ post.data.title or post.data.name or (post.templateContent | striptags | truncate(50)) or "Note" }}
{# Note #}
<div class="flex items-start gap-2">
<svg class="w-4 h-4 text-teal-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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>
<div class="min-w-0">
<a href="{{ post.url }}" class="text-sm text-teal-600 dark:text-teal-400 hover:underline line-clamp-1">
{{ post.templateContent | striptags | truncate(50) or "Note" }}
</a>
<time class="text-xs text-surface-500 block" datetime="{{ post.data.published }}">
<time class="text-xs text-surface-600 dark:text-surface-400 block font-mono" datetime="{{ post.data.published }}">
{{ (post.data.published or post.date) | dateDisplay }}
</time>
</div>
</div>
{% endif %}
</li>
{% endfor %}

View File

@@ -1,8 +1,9 @@
{# Search Widget — redirects to /search/?q=query #}
<form action="/search/" method="get" class="flex gap-2">
<input type="text" name="q" placeholder="Search..."
class="flex-1 min-w-0 px-3 py-2 text-sm rounded-lg border border-surface-300 dark:border-surface-600 bg-white dark:bg-surface-800 text-surface-900 dark:text-surface-100 placeholder-surface-400 dark:placeholder-surface-500 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500">
<button type="submit" class="px-3 py-2 text-sm font-medium rounded-lg bg-primary-600 text-white hover:bg-primary-700 transition-colors" aria-label="Search">
<label for="widget-search" class="sr-only">Search this site</label>
<input id="widget-search" type="text" name="q" placeholder="Search..."
class="flex-1 min-w-0 px-3 py-2 text-sm rounded-lg border border-surface-300 dark:border-surface-600 bg-white dark:bg-surface-800 text-surface-900 dark:text-surface-100 placeholder-surface-400 dark:placeholder-surface-500">
<button type="submit" class="px-3 py-2 text-sm font-medium rounded-lg bg-accent-600 text-white hover:bg-accent-700 transition-colors" aria-label="Search">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>

View File

@@ -8,7 +8,8 @@
target="_blank"
rel="noopener"
class="flex-1 inline-flex items-center justify-center gap-2 px-3 py-2 rounded-lg bg-[#0085ff]/10 text-[#0085ff] hover:bg-[#0085ff]/20 transition-colors text-sm font-medium"
title="Share on Bluesky">
title="Share on Bluesky"
aria-label="Share on Bluesky">
<svg class="w-4 h-4" viewBox="0 0 568 501" fill="currentColor" aria-hidden="true">
<path d="M123.121 33.664C188.241 82.553 258.281 181.68 284 234.873c25.719-53.192 95.759-152.32 160.879-201.21C491.866-1.611 568-28.906 568 57.947c0 17.346-9.945 145.713-15.778 166.555-20.275 72.453-94.155 90.933-159.875 79.748C507.222 323.8 536.444 388.56 473.333 453.32c-119.86 122.992-172.272-30.859-185.702-70.281-2.462-7.227-3.614-10.608-3.631-7.733-.017-2.875-1.169.506-3.631 7.733-13.43 39.422-65.842 193.273-185.702 70.281-63.111-64.76-33.89-129.52 80.986-149.071-65.72 11.185-139.6-7.295-159.875-79.748C9.945 203.659 0 75.291 0 57.946 0-28.906 76.135-1.612 123.121 33.664Z"/>
</svg>
@@ -16,10 +17,11 @@
<span x-data="fediverseInteract('{{ shareText }}', 'share')" class="flex-1 inline-flex">
<a href="https://share.joinmastodon.org/#text={{ shareText | urlencode }}"
@click="handleClick($event)"
class="w-full inline-flex items-center justify-center gap-2 px-3 py-2 rounded-lg bg-[#6364ff]/10 text-[#6364ff] hover:bg-[#6364ff]/20 transition-colors text-sm font-medium cursor-pointer"
title="Share on Mastodon / Fediverse (Shift+click to change instance)">
class="w-full inline-flex items-center justify-center gap-2 px-3 py-2 rounded-lg bg-[#a730b8]/10 text-[#a730b8] hover:bg-[#a730b8]/20 transition-colors text-sm font-medium cursor-pointer"
title="Share on the Fediverse (Shift+click to change instance)"
aria-label="Share on the Fediverse">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.668 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/>
<path d="M13.09 4.43L24 10.73v2.51L13.09 19.58v-2.51L21.83 12 13.09 6.98v-2.55zM13.09 9.49L17.44 12l-4.35 2.51V9.49z"/><path d="M10.91 4.43L0 10.73v2.51l8.74-5.03v10.09l2.18 1.28V4.43zM6.56 12L2.18 14.51l4.35 2.51V12z"/>
</svg>
</a>
{% set modalTitle = "Share on Mastodon / Fediverse" %}

View File

@@ -5,11 +5,13 @@
<h3 class="widget-title">Social Activity</h3>
{# Tab buttons #}
<div class="flex gap-1 mb-4 border-b border-surface-200 dark:border-surface-700">
<div class="flex gap-1 mb-4 border-b border-surface-200 dark:border-surface-700" role="tablist" aria-label="Social feeds">
{% if blueskyFeed and blueskyFeed.length %}
<button
@click="activeTab = 'bluesky'"
:class="activeTab === 'bluesky' ? 'border-b-2 border-[#0085ff] text-[#0085ff]' : 'text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
:class="activeTab === 'bluesky' ? 'border-b-2 border-[#0085ff] text-[#0085ff]' : 'text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
:aria-selected="(activeTab === 'bluesky').toString()"
role="tab" id="social-tab-bluesky" aria-controls="social-panel-bluesky"
class="flex items-center gap-1.5 px-3 py-2 text-sm font-medium transition-colors -mb-px"
>
<svg class="w-4 h-4 text-[#0085ff]" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
@@ -21,7 +23,9 @@
{% if mastodonFeed and mastodonFeed.length %}
<button
@click="activeTab = 'mastodon'"
:class="activeTab === 'mastodon' ? 'border-b-2 border-[#a730b8] text-[#a730b8]' : 'text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
:class="activeTab === 'mastodon' ? 'border-b-2 border-[#a730b8] text-[#a730b8]' : 'text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
:aria-selected="(activeTab === 'mastodon').toString()"
role="tab" id="social-tab-mastodon" aria-controls="social-panel-mastodon"
class="flex items-center gap-1.5 px-3 py-2 text-sm font-medium transition-colors -mb-px"
>
<svg class="w-4 h-4 text-[#6364ff]" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
@@ -34,7 +38,7 @@
{# Bluesky Tab Content #}
{% if blueskyFeed and blueskyFeed.length %}
<div x-show="activeTab === 'bluesky'" x-cloak>
<div x-show="activeTab === 'bluesky'" x-cloak role="tabpanel" id="social-panel-bluesky" aria-labelledby="social-tab-bluesky">
<ul class="space-y-3">
{% for post in blueskyFeed | head(5) %}
<li class="border-b border-surface-200 dark:border-surface-700 pb-3 last:border-0">
@@ -42,8 +46,8 @@
<p class="text-sm text-surface-700 dark:text-surface-300 group-hover:text-[#0085ff] transition-colors">
{{ post.text | truncate(140) }}
</p>
<div class="flex items-center gap-3 mt-2 text-xs text-surface-500">
<time datetime="{{ post.createdAt }}">{{ post.createdAt | date("MMM d, yyyy") }}</time>
<div class="flex items-center gap-3 mt-2 text-xs text-surface-600 dark:text-surface-400">
<time class="font-mono" datetime="{{ post.createdAt }}">{{ post.createdAt | date("MMM d, yyyy") }}</time>
{% if post.likeCount > 0 %}
<span class="flex items-center gap-1">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
@@ -64,7 +68,7 @@
{# Mastodon Tab Content #}
{% if mastodonFeed and mastodonFeed.length %}
<div x-show="activeTab === 'mastodon'" x-cloak>
<div x-show="activeTab === 'mastodon'" x-cloak role="tabpanel" id="social-panel-mastodon" aria-labelledby="social-tab-mastodon">
<ul class="space-y-3">
{% for post in mastodonFeed | head(5) %}
<li class="border-b border-surface-200 dark:border-surface-700 pb-3 last:border-0">
@@ -72,8 +76,8 @@
<p class="text-sm text-surface-700 dark:text-surface-300 group-hover:text-[#a730b8] transition-colors">
{{ post.text | truncate(140) }}
</p>
<div class="flex items-center gap-3 mt-2 text-xs text-surface-500">
<time datetime="{{ post.createdAt }}">{{ post.createdAt | date("MMM d, yyyy") }}</time>
<div class="flex items-center gap-3 mt-2 text-xs text-surface-600 dark:text-surface-400">
<time class="font-mono" datetime="{{ post.createdAt }}">{{ post.createdAt | date("MMM d, yyyy") }}</time>
{% if post.favouritesCount > 0 %}
<span class="flex items-center gap-1">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>

View File

@@ -3,14 +3,14 @@
<div class="widget">
<h3 class="widget-title">Subscribe</h3>
<div class="space-y-2">
<a href="/feed.xml" class="flex items-center gap-2 text-sm text-surface-600 dark:text-surface-400 hover:text-orange-600 dark:hover:text-orange-400 transition-colors">
<svg class="w-4 h-4 text-orange-500" fill="currentColor" viewBox="0 0 24 24">
<a href="/feed.xml" class="flex items-center gap-2 text-sm text-surface-600 dark:text-surface-400 hover:text-orange-600 dark:hover:text-orange-400 hover:underline transition-colors">
<svg class="w-4 h-4 text-orange-500" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M6.18 15.64a2.18 2.18 0 0 1 2.18 2.18C8.36 19 7.38 20 6.18 20C5 20 4 19 4 17.82a2.18 2.18 0 0 1 2.18-2.18M4 4.44A15.56 15.56 0 0 1 19.56 20h-2.83A12.73 12.73 0 0 0 4 7.27V4.44m0 5.66a9.9 9.9 0 0 1 9.9 9.9h-2.83A7.07 7.07 0 0 0 4 12.93V10.1Z"/>
</svg>
RSS Feed
</a>
<a href="/feed.json" class="flex items-center gap-2 text-sm text-surface-600 dark:text-surface-400 hover:text-orange-600 dark:hover:text-orange-400 transition-colors">
<svg class="w-4 h-4 text-yellow-500" fill="currentColor" viewBox="0 0 24 24">
<a href="/feed.json" class="flex items-center gap-2 text-sm text-surface-600 dark:text-surface-400 hover:text-orange-600 dark:hover:text-orange-400 hover:underline transition-colors">
<svg class="w-4 h-4 text-orange-500" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2m3 12h2v2H8v-2m4-8h2v10h-2V7m4 4h2v6h-2v-6Z"/>
</svg>
JSON Feed

View File

@@ -3,11 +3,11 @@
<is-land on:visible>
<div class="widget">
<h3 class="widget-title">Contents</h3>
<nav class="toc">
<nav class="toc" aria-label="Table of contents">
<ul class="space-y-1 text-sm">
{% for item in toc %}
<li class="{% if item.level > 2 %}ml-{{ (item.level - 2) * 3 }}{% endif %}">
<a href="#{{ item.slug }}" class="text-surface-600 dark:text-surface-400 hover:text-accent-600 dark:hover:text-accent-400 transition-colors">
<li class="{% if item.level == 3 %}ml-3{% elif item.level == 4 %}ml-6{% elif item.level == 5 %}ml-9{% elif item.level == 6 %}ml-12{% endif %}">
<a href="#{{ item.slug }}" class="text-surface-600 dark:text-surface-400 hover:text-accent-600 dark:hover:text-accent-400 hover:underline transition-colors">
{{ item.text }}
</a>
</li>

View File

@@ -14,14 +14,14 @@
<div class="flex border-b border-surface-200 dark:border-surface-700 mb-3">
<button
@click="tab = 'inbound'"
:class="tab === 'inbound' ? 'border-accent-500 text-accent-600 dark:text-accent-400' : 'border-transparent text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
:class="tab === 'inbound' ? 'border-accent-500 text-accent-600 dark:text-accent-400' : 'border-transparent text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
class="px-2 py-1.5 text-xs font-medium border-b-2 -mb-px transition-colors">
Received
<span x-show="mentions.length" x-text="mentions.length" class="ml-0.5 text-xs opacity-75"></span>
</button>
<button
@click="tab = 'outbound'"
:class="tab === 'outbound' ? 'border-accent-500 text-accent-600 dark:text-accent-400' : 'border-transparent text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
:class="tab === 'outbound' ? 'border-accent-500 text-accent-600 dark:text-accent-400' : 'border-transparent text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
class="px-2 py-1.5 text-xs font-medium border-b-2 -mb-px transition-colors">
Sent
</button>
@@ -52,14 +52,14 @@
<span x-show="wm['wm-property'] === 'in-reply-to'" class="text-sky-500"> replied to</span>
<span x-show="wm['wm-property'] === 'mention-of'" class="text-amber-500"> mentioned</span>
<span x-show="wm['wm-property'] === 'bookmark-of'" class="text-purple-500"> bookmarked</span>
<a :href="wm['wm-target']" class="text-surface-500 hover:underline block truncate" x-text="formatPath(wm['wm-target'])"></a>
<a :href="wm['wm-target']" class="text-surface-600 dark:text-surface-400 hover:underline block truncate" x-text="formatPath(wm['wm-target'])"></a>
</div>
</div>
</template>
</div>
{# Empty #}
<p x-show="!loading && !mentions.length && !error" class="text-xs text-surface-500 py-2">No webmentions received yet.</p>
<p x-show="!loading && !mentions.length && !error" class="text-xs text-surface-600 dark:text-surface-400 py-2">No webmentions received yet.</p>
{# Error #}
<p x-show="error" class="text-xs text-red-500 py-2" x-text="error"></p>

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="{{ site.locale | default('en') }}">
<html lang="{{ site.locale | default('en') }}" class="loading">
<head>
{# OG image resolution handled by og-fix transform in eleventy.config.js
to bypass Eleventy 3.x parallel rendering race condition (#3183).
@@ -57,7 +57,7 @@
{# Critical CSS — inlined for fast first paint #}
<style>{{ "css/critical.css" | inlineFile | safe }}</style>
{# Defer full stylesheet — loads after first paint #}
<link rel="stylesheet" href="/css/style.css?v={{ '/css/style.css' | hash }}" media="print" onload="this.media='all'">
<link rel="stylesheet" href="/css/style.css?v={{ '/css/style.css' | hash }}" media="print" onload="this.media='all';document.documentElement.classList.remove('loading')">
<noscript><link rel="stylesheet" href="/css/style.css?v={{ '/css/style.css' | hash }}"></noscript>
<link rel="stylesheet" href="/css/prism-theme.css?v={{ '/css/prism-theme.css' | hash }}" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="/css/prism-theme.css?v={{ '/css/prism-theme.css' | hash }}"></noscript>
@@ -67,7 +67,8 @@
var _pfQueue = [];
function initPagefind(sel, opts) { _pfQueue.push([sel, opts]); }
</script>
<link rel="stylesheet" href="/css/lite-yt-embed.css?v={{ '/css/lite-yt-embed.css' | hash }}">
<link rel="stylesheet" href="/css/lite-yt-embed.css?v={{ '/css/lite-yt-embed.css' | hash }}" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="/css/lite-yt-embed.css?v={{ '/css/lite-yt-embed.css' | hash }}"></noscript>
<script src="/js/vendor/lite-yt-embed.js?v={{ '/js/vendor/lite-yt-embed.js' | hash }}" defer></script>
{# Alpine.js components — MUST load before Alpine core (Alpine.data() registration via alpine:init) #}
<script src="/js/comments.js?v={{ '/js/comments.js' | hash }}" defer></script>
@@ -89,6 +90,9 @@
[x-data] > .flex.border-b { display: none !important; }
/* Hide loading spinners and JS-only buttons */
[x-show*="loading"], button[\\@click*="fetch"], button[\\@click*="loadMore"] { display: none !important; }
/* Show content and hide skeleton for no-JS (stylesheet loads synchronously via noscript link) */
.page-skeleton { display: none !important; }
html.loading main.container > .page-content { display: block !important; }
</style>
</noscript>
<link rel="canonical" href="{{ site.url }}{{ page.url }}">
@@ -132,18 +136,19 @@
}
})();
</script>
<a href="#main-content" class="skip-link">Skip to main content</a>
<header class="site-header">
<div class="container header-container">
<a href="/" class="site-title">{{ site.name }}</a>
{# Mobile menu button #}
<button id="menu-toggle" type="button" class="menu-toggle" aria-label="Toggle menu" aria-expanded="false">
<svg class="menu-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<svg class="menu-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<line x1="3" y1="6" x2="21" y2="6"/>
<line x1="3" y1="12" x2="21" y2="12"/>
<line x1="3" y1="18" x2="21" y2="18"/>
</svg>
<svg class="close-icon hidden" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<svg class="close-icon hidden" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
@@ -154,58 +159,38 @@
<nav class="site-nav" id="site-nav">
<a href="/">Home</a>
<a href="/about/">About</a>
<a href="/cv/">CV</a>
{# Slash pages dropdown - all root pages in one menu #}
<div class="nav-dropdown" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
<a href="/slashes/" class="nav-dropdown-trigger">
/
<svg class="w-3 h-3 ml-1 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</a>
<div class="nav-dropdown-menu" x-show="open" x-transition x-cloak>
<a href="/slashes/">All Pages</a>
<a href="/cv/">/cv</a>
{% for item in collections.pages %}
<a href="{{ item.url }}">/{{ item.fileSlug }}</a>
{% endfor %}
{# Plugin pages — only show when their data source is configured #}
{% set hasPluginPages = (funkwhaleActivity and funkwhaleActivity.source == "indiekit") or
(githubActivity and githubActivity.source != "error") or
(lastfmActivity and lastfmActivity.source == "indiekit") or
(newsActivity and newsActivity.source == "indiekit") or
(youtubeChannel and youtubeChannel.source == "indiekit") or
(blogrollStatus and blogrollStatus.source == "indiekit") or
(podrollStatus and podrollStatus.source == "indiekit") %}
{% if hasPluginPages %}
<div class="nav-dropdown-divider"></div>
{% if blogrollStatus and blogrollStatus.source == "indiekit" %}<a href="/blogroll/">/blogroll</a>{% endif %}
{% if funkwhaleActivity and funkwhaleActivity.source == "indiekit" %}<a href="/funkwhale/">/funkwhale</a>{% endif %}
{% if githubActivity and githubActivity.source != "error" %}<a href="/github/">/github</a>{% endif %}
{% if lastfmActivity and lastfmActivity.source == "indiekit" %}<a href="/listening/">/listening</a>{% endif %}
{% if newsActivity and newsActivity.source == "indiekit" %}<a href="/news/">/news</a>{% endif %}
{% if podrollStatus and podrollStatus.source == "indiekit" %}<a href="/podroll/">/podroll</a>{% endif %}
{% if youtubeChannel and youtubeChannel.source == "indiekit" %}<a href="/youtube/">/youtube</a>{% endif %}
{% endif %}
</div>
</div>
<a href="/now/">Now</a>
{# Blog dropdown #}
<div class="nav-dropdown" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
<a href="/blog/" class="nav-dropdown-trigger">
<div class="nav-dropdown" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false" @focusin="open = true" @focusout="!$el.contains($event.relatedTarget) && (open = false)" @keydown.escape="open = false">
<a href="/blog/" class="nav-dropdown-trigger" :aria-expanded="open.toString()" aria-haspopup="true">
Blog
<svg class="w-3 h-3 ml-1 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-3 h-3 ml-1 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</a>
<div class="nav-dropdown-menu" x-show="open" x-transition x-cloak>
<a href="/blog/">All Posts</a>
<div class="nav-dropdown-menu" x-show="open" x-transition x-cloak role="menu">
<a href="/blog/" role="menuitem">All Posts</a>
{% for pt in enabledPostTypes %}
<a href="{{ pt.path }}">{{ pt.label }}</a>
<a href="{{ pt.path }}" role="menuitem">{{ pt.label }}</a>
{% endfor %}
</div>
</div>
{# Pages dropdown #}
<div class="nav-dropdown" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false" @focusin="open = true" @focusout="!$el.contains($event.relatedTarget) && (open = false)" @keydown.escape="open = false">
<a href="/slashes/" class="nav-dropdown-trigger" :aria-expanded="open.toString()" aria-haspopup="true">
Pages
<svg class="w-3 h-3 ml-1 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</a>
<div class="nav-dropdown-menu" x-show="open" x-transition x-cloak role="menu">
{% if blogrollStatus and blogrollStatus.source == "indiekit" %}<a href="/blogroll/" role="menuitem">Blogroll</a>{% endif %}
{% if podrollStatus and podrollStatus.source == "indiekit" %}<a href="/podroll/" role="menuitem">Podroll</a>{% endif %}
{% if newsActivity and newsActivity.source == "indiekit" %}<a href="/news/" role="menuitem">News</a>{% endif %}
<a href="/slashes/" role="menuitem">All Pages</a>
</div>
</div>
<a href="/interactions/">Interactions</a>
<a href="/digest/">Digest</a>
<a href="/dashboard"
x-data="{ show: false }"
x-show="show"
@@ -213,23 +198,23 @@
x-transition
@indiekit:auth.window="show = $event.detail.loggedIn"
class="admin-nav-link">
<svg class="w-4 h-4 inline -mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg class="w-4 h-4 inline -mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>
</svg>
Dashboard
</a>
</nav>
<a href="/search/" aria-label="Search" title="Search" class="p-2 rounded-lg text-surface-600 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/>
</svg>
</a>
<button id="theme-toggle" type="button" class="theme-toggle" aria-label="Toggle dark mode" title="Toggle dark mode">
<svg class="sun-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<svg class="sun-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<circle cx="12" cy="12" r="5"/>
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
</svg>
<svg class="moon-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<svg class="moon-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
</button>
@@ -237,54 +222,41 @@
</div>
{# Mobile nav dropdown #}
<nav class="mobile-nav hidden" id="mobile-nav" x-data="{ blogOpen: false, slashOpen: false }">
<nav class="mobile-nav hidden" id="mobile-nav" x-data="{ blogOpen: false, pagesOpen: false }">
<a href="/">Home</a>
<a href="/about/">About</a>
<a href="/cv/">CV</a>
{# Slash pages section - all root pages in one menu #}
<div class="mobile-nav-section">
<button type="button" class="mobile-nav-toggle" @click="slashOpen = !slashOpen">
/
<svg class="w-4 h-4 transition-transform" :class="{ 'rotate-180': slashOpen }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div class="mobile-nav-submenu" x-show="slashOpen" x-collapse>
<a href="/slashes/">All Pages</a>
<a href="/cv/">/cv</a>
{% for item in collections.pages %}
<a href="{{ item.url }}">/{{ item.fileSlug }}</a>
{% endfor %}
{# Plugin pages — only show when configured #}
{% if hasPluginPages %}
<div class="mobile-nav-divider"></div>
{% if blogrollStatus and blogrollStatus.source == "indiekit" %}<a href="/blogroll/">/blogroll</a>{% endif %}
{% if funkwhaleActivity and funkwhaleActivity.source == "indiekit" %}<a href="/funkwhale/">/funkwhale</a>{% endif %}
{% if githubActivity and githubActivity.source != "error" %}<a href="/github/">/github</a>{% endif %}
{% if lastfmActivity and lastfmActivity.source == "indiekit" %}<a href="/listening/">/listening</a>{% endif %}
{% if newsActivity and newsActivity.source == "indiekit" %}<a href="/news/">/news</a>{% endif %}
{% if podrollStatus and podrollStatus.source == "indiekit" %}<a href="/podroll/">/podroll</a>{% endif %}
{% if youtubeChannel and youtubeChannel.source == "indiekit" %}<a href="/youtube/">/youtube</a>{% endif %}
{% endif %}
</div>
</div>
<a href="/now/">Now</a>
{# Blog section #}
<div class="mobile-nav-section">
<button type="button" class="mobile-nav-toggle" @click="blogOpen = !blogOpen">
<button type="button" class="mobile-nav-toggle" @click="blogOpen = !blogOpen" :aria-expanded="blogOpen.toString()" aria-controls="mobile-blog-submenu">
Blog
<svg class="w-4 h-4 transition-transform" :class="{ 'rotate-180': blogOpen }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-4 h-4 transition-transform" :class="{ 'rotate-180': blogOpen }" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div class="mobile-nav-submenu" x-show="blogOpen" x-collapse>
<div id="mobile-blog-submenu" class="mobile-nav-submenu" x-show="blogOpen" x-collapse>
<a href="/blog/">All Posts</a>
{% for pt in enabledPostTypes %}
<a href="{{ pt.path }}">{{ pt.label }}</a>
{% endfor %}
</div>
</div>
{# Pages section #}
<div class="mobile-nav-section">
<button type="button" class="mobile-nav-toggle" @click="pagesOpen = !pagesOpen" :aria-expanded="pagesOpen.toString()" aria-controls="mobile-pages-submenu">
Pages
<svg class="w-4 h-4 transition-transform" :class="{ 'rotate-180': pagesOpen }" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div id="mobile-pages-submenu" class="mobile-nav-submenu" x-show="pagesOpen" x-collapse>
{% if blogrollStatus and blogrollStatus.source == "indiekit" %}<a href="/blogroll/">Blogroll</a>{% endif %}
{% if podrollStatus and podrollStatus.source == "indiekit" %}<a href="/podroll/">Podroll</a>{% endif %}
{% if newsActivity and newsActivity.source == "indiekit" %}<a href="/news/">News</a>{% endif %}
<a href="/slashes/">All Pages</a>
</div>
</div>
<a href="/interactions/">Interactions</a>
<a href="/digest/">Digest</a>
<a href="/search/">Search</a>
<a href="/dashboard"
x-data="{ show: false }"
@@ -297,11 +269,11 @@
<button type="button" class="mobile-theme-toggle" aria-label="Toggle dark mode">
<span class="theme-label">Theme</span>
<span class="theme-icons">
<svg class="sun-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<svg class="sun-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<circle cx="12" cy="12" r="5"/>
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
</svg>
<svg class="moon-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<svg class="moon-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
</span>
@@ -309,7 +281,24 @@
</nav>
</header>
<main class="container py-8" data-pagefind-body>
<main class="container py-8" id="main-content" data-pagefind-body>
{# Skeleton loader — shown until Tailwind stylesheet loads #}
<div class="page-skeleton" aria-hidden="true">
<div style="display:flex;gap:1.5rem;align-items:flex-start;margin-bottom:2rem">
<div class="skel-bone skel-circle" style="width:96px;height:96px;flex-shrink:0"></div>
<div style="flex:1">
<div class="skel-bone" style="height:1.75rem;width:50%;margin-bottom:.75rem"></div>
<div class="skel-bone" style="height:1rem;width:35%;margin-bottom:.75rem"></div>
<div class="skel-bone" style="height:3rem;width:90%"></div>
</div>
</div>
<div class="skel-bone" style="height:5rem;margin-bottom:.75rem"></div>
<div class="skel-bone" style="height:5rem;margin-bottom:.75rem"></div>
<div class="skel-bone" style="height:5rem;margin-bottom:.75rem"></div>
<div class="skel-bone" style="height:5rem"></div>
</div>
<div class="page-content">
{% if withSidebar and page.url == "/" and homepageConfig and homepageConfig.sections %}
{# Homepage: builder controls its own layout and sidebar #}
{{ content | safe }}
@@ -334,6 +323,7 @@
{% else %}
{{ content | safe }}
{% endif %}
</div>
</main>
<footer class="border-t border-surface-200 dark:border-surface-700 mt-12 pt-8 pb-6">
@@ -341,39 +331,37 @@
<div class="grid grid-cols-2 md:grid-cols-4 gap-8 mb-8">
{# Navigate #}
<div>
<h4 class="text-sm font-semibold uppercase tracking-wider text-surface-500 dark:text-surface-400 mb-3">Navigate</h4>
<p class="text-sm font-semibold uppercase tracking-wider text-surface-600 dark:text-surface-400 mb-3" role="heading" aria-level="2">Navigate</p>
<ul class="space-y-2">
<li><a href="/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">Home</a></li>
<li><a href="/about/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">About</a></li>
<li><a href="/cv/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">CV</a></li>
<li><a href="/changelog/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">Changelog</a></li>
<li><a href="/search/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">Search</a></li>
<li x-data="{ show: false }" x-show="show" x-cloak @indiekit:auth.window="show = $event.detail.loggedIn"><a href="/dashboard" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">Dashboard</a></li>
</ul>
</div>
{# Content #}
<div>
<h4 class="text-sm font-semibold uppercase tracking-wider text-surface-500 dark:text-surface-400 mb-3">Content</h4>
<p class="text-sm font-semibold uppercase tracking-wider text-surface-600 dark:text-surface-400 mb-3" role="heading" aria-level="2">Content</p>
<ul class="space-y-2">
<li><a href="/blog/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">Blog</a></li>
{% for pt in enabledPostTypes %}
<li><a href="{{ pt.path }}" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">{{ pt.label }}</a></li>
{% endfor %}
<li><a href="/interactions/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">Interactions</a></li>
<li><a href="/digest/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">Digest</a></li>
</ul>
</div>
{# Connect #}
<div>
<h4 class="text-sm font-semibold uppercase tracking-wider text-surface-500 dark:text-surface-400 mb-3">Connect</h4>
<p class="text-sm font-semibold uppercase tracking-wider text-surface-600 dark:text-surface-400 mb-3" role="heading" aria-level="2">Connect</p>
<ul class="space-y-2">
{% for social in site.social %}
<li><a href="{{ social.url }}" rel="{{ social.rel }}" target="_blank" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">{{ social.name }}</a></li>
<li><a href="{{ social.url }}" rel="{{ social.rel }}" target="_blank" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline" aria-label="{{ social.name }} (opens in new tab)">{{ social.name }}</a></li>
{% endfor %}
</ul>
</div>
{# Meta #}
<div>
<h4 class="text-sm font-semibold uppercase tracking-wider text-surface-500 dark:text-surface-400 mb-3">Meta</h4>
<p class="text-sm font-semibold uppercase tracking-wider text-surface-600 dark:text-surface-400 mb-3" role="heading" aria-level="2">Meta</p>
<ul class="space-y-2">
<li><a href="/feed.xml" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">RSS Feed</a></li>
<li><a href="/feed.json" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">JSON Feed</a></li>
@@ -381,7 +369,7 @@
</ul>
</div>
</div>
<p class="text-center text-sm text-surface-500 dark:text-surface-400">Powered by <a href="https://getindiekit.com" class="hover:text-surface-900 dark:hover:text-surface-100 hover:underline">Indiekit</a> + <a href="https://11ty.dev" class="hover:text-surface-900 dark:hover:text-surface-100 hover:underline">Eleventy</a></p>
<p class="text-center text-sm text-surface-600 dark:text-surface-400">Powered by <a href="https://getindiekit.com" class="hover:text-surface-900 dark:hover:text-surface-100 hover:underline">Indiekit</a> + <a href="https://11ty.dev" class="hover:text-surface-900 dark:hover:text-surface-100 hover:underline">Eleventy</a></p>
</div>
</footer>
<script>
@@ -483,43 +471,43 @@
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-4"
class="fab-menu">
class="fab-menu" role="menu" aria-label="Create new post">
{% if mpUrl %}
<a href="/posts/edit?url={{ mpUrl | urlencode }}" @click="open = false" class="fab-menu-item" rel="nofollow">
<svg class="w-5 h-5 text-accent-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<a href="/posts/edit?url={{ mpUrl | urlencode }}" @click="open = false" class="fab-menu-item" rel="nofollow" role="menuitem">
<svg class="w-5 h-5 text-accent-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
<span>Edit this post</span>
</a>
<div class="fab-menu-divider"></div>
<div class="fab-menu-divider" role="separator"></div>
{% endif %}
<a href="/posts/create?type=page" @click="open = false" class="fab-menu-item" rel="nofollow">
<svg class="w-5 h-5 text-surface-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<a href="/posts/create?type=page" @click="open = false" class="fab-menu-item" rel="nofollow" role="menuitem">
<svg class="w-5 h-5 text-surface-600 dark:text-surface-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>
</svg>
<span>Page</span>
</a>
<a href="/posts/create?type=bookmark" @click="open = false" class="fab-menu-item" rel="nofollow">
<svg class="w-5 h-5 text-surface-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<a href="/posts/create?type=bookmark" @click="open = false" class="fab-menu-item" rel="nofollow" role="menuitem">
<svg class="w-5 h-5 text-surface-600 dark:text-surface-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>
</svg>
<span>Bookmark</span>
</a>
<a href="/posts/create?type=photo" @click="open = false" class="fab-menu-item" rel="nofollow">
<svg class="w-5 h-5 text-surface-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<a href="/posts/create?type=photo" @click="open = false" class="fab-menu-item" rel="nofollow" role="menuitem">
<svg class="w-5 h-5 text-surface-600 dark:text-surface-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>
</svg>
<span>Photo</span>
</a>
<a href="/posts/create?type=article" @click="open = false" class="fab-menu-item" rel="nofollow">
<svg class="w-5 h-5 text-surface-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<a href="/posts/create?type=article" @click="open = false" class="fab-menu-item" rel="nofollow" role="menuitem">
<svg class="w-5 h-5 text-surface-600 dark:text-surface-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>
</svg>
<span>Article</span>
</a>
<a href="/posts/create?type=note" @click="open = false" class="fab-menu-item" rel="nofollow">
<svg class="w-5 h-5 text-surface-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<a href="/posts/create?type=note" @click="open = false" class="fab-menu-item" rel="nofollow" role="menuitem">
<svg class="w-5 h-5 text-surface-600 dark:text-surface-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
</svg>
<span>Note</span>
@@ -529,8 +517,9 @@
<button @click="open = !open"
class="fab-button"
:aria-expanded="open"
aria-label="Create new post">
<svg class="w-7 h-7 transition-transform duration-200" :class="{ 'rotate-45': open }" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5" stroke-linecap="round">
aria-label="Create new post"
aria-haspopup="menu">
<svg class="w-7 h-7 transition-transform duration-200" :class="{ 'rotate-45': open }" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg>
</button>

View File

@@ -15,6 +15,8 @@ withSidebar: true
<img
src="{{ site.author.avatar }}"
alt="{{ site.author.name }}"
width="96"
height="96"
class="w-24 h-24 sm:w-32 sm:h-32 rounded-full object-cover shadow-lg flex-shrink-0"
loading="eager"
>
@@ -79,7 +81,7 @@ withSidebar: true
<h2 class="text-xl sm:text-2xl font-bold text-surface-900 dark:text-surface-100 mb-4 sm:mb-6">Recent Posts</h2>
<div class="space-y-4">
{% for post in collections.posts | head(10) %}
<article class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-accent-400 dark:hover:border-accent-600 transition-colors">
<article class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-accent-400 dark:hover:border-accent-600 transition-colors shadow-sm">
<h3 class="font-semibold text-surface-900 dark:text-surface-100 mb-1">
<a href="{{ post.url }}" class="hover:text-accent-600 dark:hover:text-accent-400">
{{ post.data.title or post.data.name or "Untitled" }}
@@ -88,12 +90,12 @@ withSidebar: true
{% if post.data.summary %}
<p class="text-sm text-surface-600 dark:text-surface-400 mb-2 line-clamp-2">{{ post.data.summary }}</p>
{% endif %}
<div class="flex items-center gap-3 text-xs text-surface-500">
<time datetime="{{ post.data.published or post.date }}">
<div class="flex items-center gap-3 text-xs text-surface-600 dark:text-surface-400">
<time class="font-mono" datetime="{{ post.data.published or post.date }}">
{{ (post.data.published or post.date) | date("MMM d, yyyy") }}
</time>
{% if post.data.postType %}
<span class="px-2 py-0.5 bg-surface-100 dark:bg-surface-700 rounded">{{ post.data.postType }}</span>
<span class="px-2 py-0.5 bg-surface-100 dark:bg-surface-700 rounded-full">{{ post.data.postType }}</span>
{% endif %}
</div>
</article>
@@ -110,7 +112,7 @@ withSidebar: true
<section class="mb-8 sm:mb-12">
<h2 class="text-xl sm:text-2xl font-bold text-surface-900 dark:text-surface-100 mb-4 sm:mb-6">Explore</h2>
<div class="grid gap-3 sm:grid-cols-3">
<a href="/blog/" class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-accent-400 dark:hover:border-accent-600 transition-colors text-center">
<a href="/blog/" class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-accent-400 dark:hover:border-accent-600 transition-colors text-center shadow-sm">
<div class="text-2xl mb-2">
<svg class="w-8 h-8 mx-auto text-accent-600 dark:text-accent-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>
@@ -119,7 +121,7 @@ withSidebar: true
<span class="font-semibold text-surface-900 dark:text-surface-100">Blog</span>
<p class="text-sm text-surface-600 dark:text-surface-400 mt-1">Articles, notes, and photos</p>
</a>
<a href="/cv/" class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-accent-400 dark:hover:border-accent-600 transition-colors text-center">
<a href="/cv/" class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-accent-400 dark:hover:border-accent-600 transition-colors text-center shadow-sm">
<div class="text-2xl mb-2">
<svg class="w-8 h-8 mx-auto text-accent-600 dark:text-accent-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>
@@ -128,7 +130,7 @@ withSidebar: true
<span class="font-semibold text-surface-900 dark:text-surface-100">CV</span>
<p class="text-sm text-surface-600 dark:text-surface-400 mt-1">Experience and projects</p>
</a>
<a href="/about/" class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-accent-400 dark:hover:border-accent-600 transition-colors text-center">
<a href="/about/" class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-accent-400 dark:hover:border-accent-600 transition-colors text-center shadow-sm">
<div class="text-2xl mb-2">
<svg class="w-8 h-8 mx-auto text-accent-600 dark:text-accent-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>

View File

@@ -16,8 +16,8 @@ withSidebar: true
</p>
{% endif %}
{% if updated %}
<p class="text-sm text-surface-500 dark:text-surface-400 mt-2">
Last updated: <time class="dt-updated" datetime="{{ updated | isoDate }}">{{ updated | dateDisplay }}</time>
<p class="text-sm text-surface-600 dark:text-surface-400 mt-2">
Last updated: <time class="dt-updated font-mono text-sm" datetime="{{ updated | isoDate }}">{{ updated | dateDisplay }}</time>
</p>
{% endif %}
</header>
@@ -30,24 +30,24 @@ withSidebar: true
{% if page.url == "/ai/" and collections.posts %}
{% set stats = collections.posts | aiStats %}
{% set aiPostsList = collections.posts | aiPosts %}
<section class="mt-8 sm:mt-12 p-6 rounded-xl bg-surface-50 dark:bg-surface-800/50 border border-surface-200 dark:border-surface-700">
<section class="mt-8 sm:mt-12 p-6 rounded-lg bg-surface-50 dark:bg-surface-800/50 border border-surface-200 dark:border-surface-700 shadow-sm">
<h2 class="text-xl font-bold text-surface-900 dark:text-surface-100 mb-4">AI Usage Across Posts</h2>
<div class="grid gap-4 sm:grid-cols-4 mb-6">
<div class="text-center p-3 rounded-lg bg-white dark:bg-surface-800 border border-surface-200 dark:border-surface-700">
<div class="text-center p-3 rounded-lg bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 shadow-sm">
<div class="text-2xl font-bold text-surface-900 dark:text-surface-100">{{ stats.total }}</div>
<div class="text-xs text-surface-500 dark:text-surface-400">Total posts</div>
<div class="text-xs text-surface-600 dark:text-surface-400">Total posts</div>
</div>
<div class="text-center p-3 rounded-lg bg-white dark:bg-surface-800 border border-surface-200 dark:border-surface-700">
<div class="text-center p-3 rounded-lg bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 shadow-sm">
<div class="text-2xl font-bold text-amber-600 dark:text-amber-400">{{ stats.aiCount }}</div>
<div class="text-xs text-surface-500 dark:text-surface-400">AI-involved</div>
<div class="text-xs text-surface-600 dark:text-surface-400">AI-involved</div>
</div>
<div class="text-center p-3 rounded-lg bg-white dark:bg-surface-800 border border-surface-200 dark:border-surface-700">
<div class="text-center p-3 rounded-lg bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 shadow-sm">
<div class="text-2xl font-bold text-emerald-600 dark:text-emerald-400">{{ stats.total - stats.aiCount }}</div>
<div class="text-xs text-surface-500 dark:text-surface-400">Human-only</div>
<div class="text-xs text-surface-600 dark:text-surface-400">Human-only</div>
</div>
<div class="text-center p-3 rounded-lg bg-white dark:bg-surface-800 border border-surface-200 dark:border-surface-700">
<div class="text-center p-3 rounded-lg bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 shadow-sm">
<div class="text-2xl font-bold text-surface-900 dark:text-surface-100">{{ stats.percentage }}%</div>
<div class="text-xs text-surface-500 dark:text-surface-400">AI ratio</div>
<div class="text-xs text-surface-600 dark:text-surface-400">AI ratio</div>
</div>
</div>
@@ -69,7 +69,7 @@ withSidebar: true
{# Post graph showing AI posts (highlighted) on the full year grid #}
<h3 class="text-lg font-semibold text-surface-900 dark:text-surface-100 mb-3">AI-Involved Posts Over Time</h3>
<p class="text-sm text-surface-500 dark:text-surface-400 mb-4">Highlighted days had posts with AI involvement (level 1+). Empty boxes represent days with no AI-involved posts.</p>
<p class="text-sm text-surface-600 dark:text-surface-400 mb-4">Highlighted days had posts with AI involvement (level 1+). Empty boxes represent days with no AI-involved posts.</p>
{% postGraph aiPostsList, { prefix: "ai", highlightColorLight: "#d97706", highlightColorDark: "#fbbf24" } %}
</section>
{% endif %}
@@ -80,32 +80,32 @@ withSidebar: true
{% set aiTools = aiTools or ai_tools %}
{% set aiDescription = aiDescription or ai_description %}
{% if aiTextLevel or aiCodeLevel or aiTools %}
<aside class="mt-6 p-4 rounded-lg bg-surface-50 dark:bg-surface-800/50 border border-surface-200 dark:border-surface-700">
<aside class="mt-6 p-4 rounded-lg bg-surface-50 dark:bg-surface-800/50 border border-surface-200 dark:border-surface-700 shadow-sm">
<div class="flex items-center gap-2 mb-2">
<svg class="w-4 h-4 text-surface-500 dark:text-surface-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<svg class="w-4 h-4 text-surface-600 dark:text-surface-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2 2 0 01-2 2H7a2 2 0 01-2-2v-2.5"/>
</svg>
<strong class="text-sm font-semibold text-surface-700 dark:text-surface-300">AI Usage</strong>
</div>
<div class="flex flex-wrap gap-3 text-xs text-surface-600 dark:text-surface-400">
{% if aiTextLevel %}
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-surface-100 dark:bg-surface-700">
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-surface-100 dark:bg-surface-700">
Text: {% if aiTextLevel === "0" %}None{% elif aiTextLevel === "1" %}Editorial{% elif aiTextLevel === "2" %}Co-drafted{% elif aiTextLevel === "3" %}AI-generated{% endif %}
</span>
{% endif %}
{% if aiCodeLevel %}
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-surface-100 dark:bg-surface-700">
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-surface-100 dark:bg-surface-700">
Code: {% if aiCodeLevel === "0" %}Human{% elif aiCodeLevel === "1" %}AI-assisted{% elif aiCodeLevel === "2" %}AI-generated{% endif %}
</span>
{% endif %}
{% if aiTools %}
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-surface-100 dark:bg-surface-700">
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-surface-100 dark:bg-surface-700">
Tools: {{ aiTools }}
</span>
{% endif %}
</div>
{% if aiDescription %}
<p class="mt-2 text-xs text-surface-500 dark:text-surface-400">{{ aiDescription }}</p>
<p class="mt-2 text-xs text-surface-600 dark:text-surface-400">{{ aiDescription }}</p>
{% endif %}
<p class="mt-2 text-xs"><a href="/ai/" class="text-accent-600 dark:text-accent-400 hover:underline">Learn more about AI usage on this site &rarr;</a></p>
</aside>
@@ -133,4 +133,18 @@ withSidebar: true
{# Hidden metadata for microformats #}
<a class="u-url hidden" href="{{ page.url }}"></a>
<data class="p-author h-card hidden" value="{{ site.author.name }}"></data>
{# Pagefind filter metadata #}
<div hidden>
<span data-pagefind-filter="type">Page</span>
{% if category %}
{% if category is string %}
<span data-pagefind-filter="category">{{ category }}</span>
{% else %}
{% for cat in category %}
<span data-pagefind-filter="category">{{ cat }}</span>
{% endfor %}
{% endif %}
{% endif %}
</div>
</article>

View File

@@ -13,27 +13,27 @@ withBlogSidebar: true
<h1 class="p-name text-2xl sm:text-3xl font-bold text-surface-900 dark:text-surface-100 mb-3 sm:mb-4">{{ title }}</h1>
{% else %}
<div class="flex items-center gap-2 mb-1">
<span class="text-sm font-medium text-surface-500 dark:text-surface-400">
<span class="text-sm font-medium text-surface-600 dark:text-surface-400">
{% if replyTo %}&#8617; Reply{% elif likedUrl %}&#9829; Like{% elif repostedUrl %}&#9851; Repost{% elif bookmarkedUrl %}&#128278; Bookmark{% else %}&#9998; Note{% endif %}
</span>
</div>
{% endif %}
<div class="post-meta mb-4 sm:mb-6">
<time-difference><time class="dt-published" datetime="{{ date.toISOString() }}">
<time-difference><time class="dt-published font-mono text-sm" datetime="{{ date.toISOString() }}">
{{ date | dateDisplay }}
</time></time-difference>
{% if category %}
<span class="post-categories">
<ul class="post-categories flex flex-wrap gap-2 list-none p-0 m-0" role="list" aria-label="Categories">
{# Handle both string and array categories #}
{% if category is string %}
<a href="/categories/{{ category | slugify }}/" class="p-category">{{ category }}</a>
<li><a href="/categories/{{ category | slugify }}/" class="p-category">{{ category }}</a></li>
{% else %}
{% for cat in category %}
<a href="/categories/{{ cat | slugify }}/" class="p-category">{{ cat }}</a>
<li><a href="/categories/{{ cat | slugify }}/" class="p-category">{{ cat }}</a></li>
{% endfor %}
{% endif %}
</span>
</ul>
{% endif %}
</div>
@@ -54,7 +54,7 @@ withBlogSidebar: true
{% if photoUrl and photoUrl[0] != '/' and 'http' not in photoUrl %}
{% set photoUrl = '/' + photoUrl %}
{% endif %}
<img src="{{ photoUrl }}" alt="{{ img.alt | default('Photo') }}" class="u-photo rounded-lg max-w-full" loading="lazy" eleventy:ignore>
<img src="{{ photoUrl }}" alt="{{ img.alt | default('Photo from: ' + title) }}" class="u-photo rounded-lg max-w-full" loading="lazy" eleventy:ignore>
{% endfor %}
</div>
{% endif %}
@@ -73,7 +73,7 @@ withBlogSidebar: true
{% set aiCodeLevel = aiCodeLevel or ai_code_level %}
{% set aiTools = aiTools or ai_tools %}
{% set aiDescription = aiDescription or ai_description %}
<details class="mt-4 text-xs text-surface-500 dark:text-surface-400">
<details class="mt-4 text-xs text-surface-600 dark:text-surface-400">
<summary class="cursor-pointer hover:text-surface-600 dark:hover:text-surface-300 list-none flex items-center gap-1.5 [&::-webkit-details-marker]:hidden">
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2 2 0 01-2 2H7a2 2 0 01-2-2v-2.5"/>
@@ -115,7 +115,7 @@ withBlogSidebar: true
{% if externalSyndication.length or selfHostedApUrl %}
<footer class="post-footer mt-8 pt-6 border-t border-surface-200 dark:border-surface-700">
<div class="flex flex-wrap items-center gap-4">
<span class="text-sm text-surface-500 dark:text-surface-400">Also on:</span>
<span class="text-sm text-surface-600 dark:text-surface-400">Also on:</span>
<div class="flex flex-wrap gap-3">
{# Fediverse remote interaction button (self-hosted ActivityPub) #}
{% if selfHostedApUrl %}
@@ -125,9 +125,7 @@ withBlogSidebar: true
rel="syndication"
title="Interact from your fediverse instance (Shift+click to change)"
@click="handleClick($event)">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="18" cy="5" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="19" r="2.5"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
</svg>
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M13.09 4.43L24 10.73v2.51L13.09 19.58v-2.51L21.83 12 13.09 6.98v-2.55zM13.09 9.49L17.44 12l-4.35 2.51V9.49z"/><path d="M10.91 4.43L0 10.73v2.51l8.74-5.03v10.09l2.18 1.28V4.43zM6.56 12L2.18 14.51l4.35 2.51V12z"/></svg>
<span>Fediverse</span>
</a>
{% set modalTitle = "Fediverse Interaction" %}
@@ -166,6 +164,16 @@ withBlogSidebar: true
</svg>
<span>IndieNews</span>
</a>
{% elif "/@" in url %}
{# Mastodon-compatible instance (any URL with /@username pattern) #}
{% set mastoHandle = url | replace("https://", "") | replace("http://", "") %}
{% set mastoHandle = mastoHandle.split("/")[0] + "/" + mastoHandle.split("/")[1] %}
<a class="u-syndication inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-[#6364ff]/10 text-[#6364ff] hover:bg-[#6364ff]/20 transition-colors text-sm font-medium" href="{{ url }}" target="_blank" rel="noopener syndication" title="View on Mastodon">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.668 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/>
</svg>
<span>{{ mastoHandle }}</span>
</a>
{% else %}
<a class="u-syndication inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-300 hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors text-sm font-medium" href="{{ url }}" target="_blank" rel="noopener syndication">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
@@ -190,6 +198,28 @@ withBlogSidebar: true
<img class="u-photo" src="{{ site.author.avatar }}" alt="{{ site.author.name }}" hidden>
</span>
{# Pagefind filter metadata — hidden elements for search filtering #}
<div hidden>
{% if replyTo %}<span data-pagefind-filter="type">Reply</span>
{% elif likedUrl %}<span data-pagefind-filter="type">Like</span>
{% elif repostedUrl %}<span data-pagefind-filter="type">Repost</span>
{% elif bookmarkedUrl %}<span data-pagefind-filter="type">Bookmark</span>
{% elif photo and photo.length %}<span data-pagefind-filter="type">Photo</span>
{% elif title %}<span data-pagefind-filter="type">Article</span>
{% else %}<span data-pagefind-filter="type">Note</span>
{% endif %}
<span data-pagefind-filter="year">{{ date | date("yyyy") }}</span>
{% if category %}
{% if category is string %}
<span data-pagefind-filter="category">{{ category }}</span>
{% else %}
{% for cat in category %}
<span data-pagefind-filter="category">{{ cat }}</span>
{% endfor %}
{% endif %}
{% endif %}
</div>
{# JSON-LD Structured Data for SEO #}
{# Handle photo as potentially an array #}
{% set postImage = photo %}
@@ -239,9 +269,10 @@ withBlogSidebar: true
{# Lightbox overlay for article images #}
<template x-teleport="body">
<div x-show="open" x-transition.opacity.duration.200ms
role="dialog" aria-modal="true" aria-label="Image viewer"
class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/90 backdrop-blur-sm"
@click.self="close()">
<button @click="close()" class="absolute top-4 right-4 text-white/70 hover:text-white text-3xl leading-none p-2 z-10" aria-label="Close">&times;</button>
<button x-ref="closeBtn" @click="close()" class="absolute top-4 right-4 text-white/70 hover:text-white text-3xl leading-none p-2 z-10" aria-label="Close lightbox">&times;</button>
<template x-if="images.length > 1">
<button @click="prev()" class="absolute left-4 top-1/2 -translate-y-1/2 text-white/70 hover:text-white text-4xl leading-none p-2 z-10" aria-label="Previous">&lsaquo;</button>
</template>

View File

@@ -14,7 +14,7 @@ permalink: "articles/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNu
<h1 class="text-2xl sm:text-3xl font-bold text-surface-900 dark:text-surface-100">Articles</h1>
{% set sparklineSvg = collections.articles | postingFrequency %}
{% if sparklineSvg %}
<span class="text-amber-600 dark:text-amber-400">{{ sparklineSvg | safe }}</span>
<div class="flex-1 min-w-0 text-indigo-600 dark:text-indigo-400">{{ sparklineSvg | safe }}</div>
{% endif %}
</div>
<p class="text-surface-600 dark:text-surface-400 mb-6 sm:mb-8">
@@ -25,16 +25,16 @@ permalink: "articles/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNu
{% if paginatedArticles.length > 0 %}
<ul class="post-list">
{% for post in paginatedArticles %}
<li class="h-entry post-card border-l-[3px] border-l-surface-300 dark:border-l-surface-600">
<li class="h-entry post-card border-l-[3px] border-l-indigo-400 dark:border-l-indigo-500">
<div class="post-header">
<h2 class="text-xl font-semibold mb-1 flex-1">
<a class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400" href="{{ post.url }}">
<a class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-indigo-600 dark:hover:text-indigo-400" href="{{ post.url }}">
{{ post.data.title or "Untitled" }}
</a>
</h2>
</div>
<div class="post-meta mt-2">
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
{% if post.data.category %}
@@ -52,7 +52,7 @@ permalink: "articles/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNu
<p class="p-summary text-surface-700 dark:text-surface-300 mt-3">
{{ post.templateContent | striptags | truncate(250) }}
</p>
<a href="{{ post.url }}" class="text-sm text-accent-600 dark:text-accent-400 hover:underline mt-3 inline-block">
<a href="{{ post.url }}" class="text-sm text-indigo-600 dark:text-indigo-400 hover:underline mt-3 inline-block">
Read more &rarr;
</a>
</li>
@@ -72,7 +72,7 @@ permalink: "articles/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNu
Previous
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
Previous
</span>
@@ -84,7 +84,7 @@ permalink: "articles/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNu
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
Next
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</span>

View File

@@ -13,7 +13,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
<h1 class="text-2xl sm:text-3xl font-bold text-surface-900 dark:text-surface-100">Blog</h1>
{% set sparklineSvg = collections.posts | postingFrequency %}
{% if sparklineSvg %}
<span class="text-amber-600 dark:text-amber-400">{{ sparklineSvg | safe }}</span>
<div class="flex-1 min-w-0 text-amber-600 dark:text-amber-400">{{ sparklineSvg | safe }}</div>
{% endif %}
</div>
<p class="text-surface-600 dark:text-surface-400 mb-6 sm:mb-8">
@@ -22,20 +22,13 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
</p>
{% if paginatedPosts.length > 0 %}
<filter-container oninit leave-url-alone>
<div class="flex flex-wrap gap-3 mb-6">
<select data-filter-key="type" class="px-3 py-1.5 text-sm bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 rounded-lg">
<option value="">All Types</option>
<option value="article">Articles</option>
<option value="note">Notes</option>
<option value="photo">Photos</option>
<option value="bookmark">Bookmarks</option>
<option value="like">Likes</option>
<option value="reply">Replies</option>
<option value="repost">Reposts</option>
</select>
<span data-filter-results class="text-sm text-surface-500 dark:text-surface-400 self-center"></span>
</div>
<nav class="flex flex-wrap gap-2 mb-6" aria-label="Filter by post type">
<a href="/blog/" class="px-3 py-1.5 text-sm font-medium rounded-full bg-accent-600 text-white dark:bg-accent-700">All Posts <span class="opacity-75">({{ collections.posts.length }})</span></a>
{% for pt in enabledPostTypes %}
{% set collName = pt.label | lower %}
<a href="{{ pt.path }}" class="px-3 py-1.5 text-sm font-medium rounded-full bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-300 hover:bg-surface-200 dark:hover:bg-surface-700 border border-surface-200 dark:border-surface-700 transition-colors">{{ pt.label }}{% if collections[collName] %} <span class="text-surface-600 dark:text-surface-400">({{ collections[collName].length }})</span>{% endif %}</a>
{% endfor %}
</nav>
<ul class="post-list">
{% for post in paginatedPosts %}
{# Detect post type from frontmatter properties #}
@@ -44,7 +37,6 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
{% set repostedUrl = post.data.repostOf or post.data.repost_of %}
{% set replyToUrl = post.data.inReplyTo or post.data.in_reply_to %}
{% set hasPhotos = post.data.photo and post.data.photo.length %}
{% set _postType %}{% if likedUrl %}like{% elif bookmarkedUrl %}bookmark{% elif repostedUrl %}repost{% elif replyToUrl %}reply{% elif hasPhotos %}photo{% elif post.data.title %}article{% else %}note{% endif %}{% endset %}
{% set borderClass = "" %}
{% if likedUrl %}
{% set borderClass = "border-l-[3px] border-l-red-400 dark:border-l-red-500" %}
@@ -56,10 +48,12 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
{% set borderClass = "border-l-[3px] border-l-sky-400 dark:border-l-sky-500" %}
{% elif hasPhotos %}
{% set borderClass = "border-l-[3px] border-l-purple-400 dark:border-l-purple-500" %}
{% elif post.data.title %}
{% set borderClass = "border-l-[3px] border-l-indigo-400 dark:border-l-indigo-500" %}
{% else %}
{% set borderClass = "border-l-[3px] border-l-surface-300 dark:border-l-surface-600" %}
{% set borderClass = "border-l-[3px] border-l-teal-400 dark:border-l-teal-500" %}
{% endif %}
<li class="h-entry post-card {{ borderClass }}" data-filter-type="{{ _postType | trim }}">
<li class="h-entry post-card {{ borderClass }}">
{% if likedUrl %}
{# ── Like card ── #}
@@ -72,7 +66,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
<div class="flex-1 min-w-0">
<div class="post-meta">
<span class="font-medium text-red-600 dark:text-red-400">Liked</span>
<time-difference><time class="dt-published" datetime="{{ post.date | isoDate }}">
<time-difference><time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time></time-difference>
{% if post.data.category %}
@@ -88,7 +82,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
{% endif %}
</div>
{% unfurl likedUrl %}
<a class="u-like-of text-xs text-surface-400 dark:text-surface-500 hover:underline break-all mt-1 inline-block" href="{{ likedUrl }}">
<a class="u-like-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ likedUrl }}">
{{ likedUrl }}
</a>
{% if post.templateContent %}
@@ -96,7 +90,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
{{ post.templateContent | safe }}
</div>
{% endif %}
<a class="u-url text-sm text-accent-600 dark:text-accent-400 hover:underline mt-3 inline-block" href="{{ post.url }}">Permalink</a>
<a class="u-url text-sm text-red-600 dark:text-red-400 hover:underline mt-3 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or ('Like from ' + (post.date | dateDisplay)) }}">Permalink</a>
</div>
</div>
@@ -111,7 +105,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
<div class="flex-1 min-w-0">
<div class="post-meta">
<span class="font-medium text-amber-600 dark:text-amber-400">Bookmarked</span>
<time-difference><time class="dt-published" datetime="{{ post.date | isoDate }}">
<time-difference><time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time></time-difference>
{% if post.data.category %}
@@ -128,11 +122,11 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
</div>
{% if post.data.title %}
<h2 class="p-name text-lg font-semibold text-surface-900 dark:text-surface-100 mt-2">
<a class="hover:text-accent-600 dark:hover:text-accent-400" href="{{ post.url }}">{{ post.data.title }}</a>
<a class="hover:text-amber-600 dark:hover:text-amber-400" href="{{ post.url }}">{{ post.data.title }}</a>
</h2>
{% endif %}
{% unfurl bookmarkedUrl %}
<a class="u-bookmark-of text-xs text-surface-400 dark:text-surface-500 hover:underline break-all mt-1 inline-block" href="{{ bookmarkedUrl }}">
<a class="u-bookmark-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ bookmarkedUrl }}">
{{ bookmarkedUrl }}
</a>
{% if post.templateContent %}
@@ -140,7 +134,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
{{ post.templateContent | safe }}
</div>
{% endif %}
<a class="u-url text-sm text-accent-600 dark:text-accent-400 hover:underline mt-3 inline-block" href="{{ post.url }}">Permalink</a>
<a class="u-url text-sm text-amber-600 dark:text-amber-400 hover:underline mt-3 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or ('Bookmark from ' + (post.date | dateDisplay)) }}">Permalink</a>
</div>
</div>
@@ -155,7 +149,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
<div class="flex-1 min-w-0">
<div class="post-meta">
<span class="font-medium text-green-600 dark:text-green-400">Reposted</span>
<time-difference><time class="dt-published" datetime="{{ post.date | isoDate }}">
<time-difference><time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time></time-difference>
{% if post.data.category %}
@@ -171,7 +165,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
{% endif %}
</div>
{% unfurl repostedUrl %}
<a class="u-repost-of text-xs text-surface-400 dark:text-surface-500 hover:underline break-all mt-1 inline-block" href="{{ repostedUrl }}">
<a class="u-repost-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ repostedUrl }}">
{{ repostedUrl }}
</a>
{% if post.templateContent %}
@@ -179,7 +173,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
{{ post.templateContent | safe }}
</div>
{% endif %}
<a class="u-url text-sm text-accent-600 dark:text-accent-400 hover:underline mt-3 inline-block" href="{{ post.url }}">Permalink</a>
<a class="u-url text-sm text-green-600 dark:text-green-400 hover:underline mt-3 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or ('Repost from ' + (post.date | dateDisplay)) }}">Permalink</a>
</div>
</div>
@@ -194,7 +188,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
<div class="flex-1 min-w-0">
<div class="post-meta">
<span class="font-medium text-sky-600 dark:text-sky-400">In reply to</span>
<time-difference><time class="dt-published" datetime="{{ post.date | isoDate }}">
<time-difference><time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time></time-difference>
{% if post.data.category %}
@@ -210,13 +204,13 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
{% endif %}
</div>
{% unfurl replyToUrl %}
<a class="u-in-reply-to text-xs text-surface-400 dark:text-surface-500 hover:underline break-all mt-1 inline-block" href="{{ replyToUrl }}">
<a class="u-in-reply-to text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ replyToUrl }}">
{{ replyToUrl }}
</a>
<div class="e-content prose dark:prose-invert prose-sm mt-3 max-w-none">
{{ post.templateContent | safe }}
</div>
<a class="u-url text-sm text-accent-600 dark:text-accent-400 hover:underline mt-3 inline-block" href="{{ post.url }}">Permalink</a>
<a class="u-url text-sm text-sky-600 dark:text-sky-400 hover:underline mt-3 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or ('Reply from ' + (post.date | dateDisplay)) }}">Permalink</a>
</div>
</div>
@@ -232,7 +226,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
<div class="flex-1 min-w-0">
<div class="post-meta">
<span class="font-medium text-purple-600 dark:text-purple-400">Photo</span>
<time-difference><time class="dt-published" datetime="{{ post.date | isoDate }}">
<time-difference><time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time></time-difference>
{% if post.data.category %}
@@ -263,7 +257,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
{{ post.templateContent | safe }}
</div>
{% endif %}
<a class="u-url text-sm text-accent-600 dark:text-accent-400 hover:underline mt-3 inline-block" href="{{ post.url }}">Permalink</a>
<a class="u-url text-sm text-purple-600 dark:text-purple-400 hover:underline mt-3 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or ('Photo from ' + (post.date | dateDisplay)) }}">Permalink</a>
</div>
</div>
@@ -271,12 +265,12 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
{# ── Article card (unchanged) ── #}
<div class="post-header">
<h2 class="text-xl font-semibold mb-1">
<a class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400" href="{{ post.url }}">
<a class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-indigo-600 dark:hover:text-indigo-400" href="{{ post.url }}">
{{ post.data.title }}
</a>
</h2>
<div class="post-meta">
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
{% if post.data.category %}
@@ -295,7 +289,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
<p class="p-summary text-surface-700 dark:text-surface-300 mt-3">
{{ post.templateContent | striptags | truncate(250) }}
</p>
<a href="{{ post.url }}" class="text-sm text-accent-600 dark:text-accent-400 hover:underline mt-3 inline-block">
<a href="{{ post.url }}" class="text-sm text-indigo-600 dark:text-indigo-400 hover:underline mt-3 inline-block">
Read more &rarr;
</a>
@@ -303,7 +297,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
{# ── Note card (unchanged) ── #}
<div class="post-header">
<a class="u-url" href="{{ post.url }}">
<time-difference><time class="dt-published text-sm text-surface-500 dark:text-surface-400 font-medium" datetime="{{ post.date | isoDate }}">
<time-difference><time class="dt-published text-sm text-surface-600 dark:text-surface-400 font-medium font-mono" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time></time-difference>
</a>
@@ -323,7 +317,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
{{ post.templateContent | safe }}
</div>
<div class="post-footer mt-3">
<a href="{{ post.url }}" class="text-sm text-accent-600 dark:text-accent-400 hover:underline">
<a href="{{ post.url }}" class="text-sm text-teal-600 dark:text-teal-400 hover:underline" aria-label="Permalink: {{ post.data.title or ('Note from ' + (post.date | dateDisplay)) }}">
Permalink
</a>
</div>
@@ -333,7 +327,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
{% set postAiText = post.data.aiTextLevel or post.data.ai_text_level %}
{% set postAiCode = post.data.aiCodeLevel or post.data.ai_code_level %}
{% if (postAiText and postAiText !== "0") or (postAiCode and postAiCode !== "0") %}
<span class="inline-flex items-center gap-1 mt-2 px-1.5 py-0.5 rounded text-[10px] font-medium bg-surface-100 dark:bg-surface-700 text-surface-500 dark:text-surface-400" title="AI usage: Text level {{ postAiText or '' }}, Code level {{ postAiCode or '' }}">
<span class="inline-flex items-center gap-1 mt-2 px-1.5 py-0.5 rounded text-[10px] font-medium bg-surface-100 dark:bg-surface-700 text-surface-600 dark:text-surface-400" title="AI usage: Text level {{ postAiText or '' }}, Code level {{ postAiCode or '' }}">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2 2 0 01-2 2H7a2 2 0 01-2-2v-2.5"/></svg>
AI{% if postAiText %}: T{{ postAiText }}{% endif %}{% if postAiCode %}/C{{ postAiCode }}{% endif %}
</span>
@@ -341,7 +335,6 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
</li>
{% endfor %}
</ul>
</filter-container>
{# Pagination controls #}
{% if pagination.pages.length > 1 %}
@@ -356,7 +349,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
Previous
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
Previous
</span>
@@ -368,7 +361,7 @@ permalink: "blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
Next
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</span>

View File

@@ -14,17 +14,19 @@ permalink: /blogroll/
<p class="text-surface-600 dark:text-surface-400">
Blogs I follow - <span x-text="blogs.length" class="font-medium"></span> feeds
</p>
<p class="text-xs text-surface-500 mt-2" x-show="status?.lastSync">
Last synced: <span x-text="formatDate(status?.lastSync, 'full')"></span>
<p class="text-xs text-surface-600 dark:text-surface-400 mt-2" x-show="status?.lastSync">
Last synced: <span class="font-mono" x-text="formatDate(status?.lastSync, 'full')"></span>
</p>
</header>
{# Tab Navigation - All Blogs first, then one tab per category #}
<div class="mb-6 border-b border-surface-200 dark:border-surface-700">
<nav class="flex gap-1 overflow-x-auto -mb-px" aria-label="Tabs">
<nav class="flex gap-1 overflow-x-auto -mb-px" role="tablist" aria-label="Blogroll categories">
<button
@click="switchTab('blogs')"
:class="activeTab === 'blogs' ? 'border-orange-500 text-orange-600 dark:text-orange-400' : 'border-transparent text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
:class="activeTab === 'blogs' ? 'border-orange-500 text-orange-600 dark:text-orange-400' : 'border-transparent text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
:aria-selected="(activeTab === 'blogs').toString()"
role="tab" id="blogroll-tab-blogs" aria-controls="blogroll-panel-blogs"
class="pb-3 px-3 border-b-2 font-medium text-sm transition-colors whitespace-nowrap flex-shrink-0"
>
All Blogs
@@ -32,7 +34,9 @@ permalink: /blogroll/
<template x-for="cat in categories" :key="cat.name">
<button
@click="switchTab('category:' + cat.name)"
:class="activeTab === 'category:' + cat.name ? 'border-orange-500 text-orange-600 dark:text-orange-400' : 'border-transparent text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
:class="activeTab === 'category:' + cat.name ? 'border-orange-500 text-orange-600 dark:text-orange-400' : 'border-transparent text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
:aria-selected="(activeTab === 'category:' + cat.name).toString()"
role="tab" :id="'blogroll-tab-' + cat.name.toLowerCase().replace(/\s+/g, '-')" :aria-controls="'blogroll-panel-' + cat.name.toLowerCase().replace(/\s+/g, '-')"
class="pb-3 px-3 border-b-2 font-medium text-sm transition-colors whitespace-nowrap flex-shrink-0"
>
<span x-text="cat.name"></span>
@@ -46,8 +50,8 @@ permalink: /blogroll/
{# Main Content #}
<div class="main-content">
{# Loading State #}
<div x-show="loading" class="text-center py-12">
<svg class="w-8 h-8 mx-auto text-orange-600 animate-spin mb-4" fill="none" viewBox="0 0 24 24">
<div x-show="loading" class="text-center py-12" role="status">
<svg class="w-8 h-8 mx-auto text-orange-600 animate-spin mb-4" fill="none" viewBox="0 0 24 24" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
@@ -55,32 +59,32 @@ permalink: /blogroll/
</div>
{# Error State #}
<div x-show="error" class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6">
<div x-show="error" role="alert" class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6">
<p class="text-red-700 dark:text-red-400" x-text="error"></p>
<button @click="fetchData()" class="mt-2 text-sm text-red-600 hover:text-red-700 underline">Try again</button>
</div>
{# All Blogs Tab #}
<div x-show="activeTab === 'blogs' && !loading" class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div x-show="activeTab === 'blogs' && !loading" role="tabpanel" id="blogroll-panel-blogs" aria-labelledby="blogroll-tab-blogs" class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<template x-for="blog in blogs" :key="blog.id">
<a
:href="blog.siteUrl || blog.feedUrl"
class="block bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-4 hover:border-orange-400 dark:hover:border-orange-600 transition-colors group"
class="block bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 p-4 hover:border-orange-400 dark:hover:border-orange-600 transition-colors group shadow-sm"
target="_blank"
rel="noopener"
>
<div class="flex items-center gap-3 mb-2">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-orange-400 to-orange-600 flex items-center justify-center flex-shrink-0 overflow-hidden">
<img x-show="blog.photo" :src="blog.photo" class="w-10 h-10 object-cover" loading="lazy" />
<img x-show="blog.photo" :src="blog.photo" :alt="blog.title" class="w-10 h-10 object-cover" loading="lazy" />
<span x-show="!blog.photo" class="text-white text-sm font-bold" x-text="blog.title?.charAt(0)?.toUpperCase()"></span>
</div>
<div class="flex-1 min-w-0">
<h3 class="font-medium text-surface-900 dark:text-surface-100 truncate group-hover:text-orange-600 dark:group-hover:text-orange-400 transition-colors" x-text="blog.title"></h3>
<p x-show="blog.category" class="text-xs text-surface-500 truncate" x-text="blog.category"></p>
<p x-show="blog.category" class="text-xs text-surface-600 dark:text-surface-400 truncate" x-text="blog.category"></p>
</div>
</div>
<p x-show="blog.description" class="text-sm text-surface-600 dark:text-surface-400 line-clamp-2 mb-3" x-text="blog.description"></p>
<div class="flex items-center gap-3 text-xs text-surface-500">
<div class="flex items-center gap-3 text-xs text-surface-600 dark:text-surface-400">
<span x-text="`${blog.itemCount || 0} posts`"></span>
<span :class="blog.status === 'active' ? 'text-green-500' : 'text-red-500'">
<span x-show="blog.status === 'active'" class="flex items-center gap-1">
@@ -99,17 +103,17 @@ permalink: /blogroll/
{# Empty State for Blogs #}
<div x-show="!loading && blogs.length === 0 && activeTab === 'blogs' && !error" class="text-center py-12">
<svg class="w-16 h-16 mx-auto text-surface-300 dark:text-surface-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-16 h-16 mx-auto text-surface-300 dark:text-surface-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"/>
</svg>
<p class="text-surface-600 dark:text-surface-400 text-lg">No blogs yet.</p>
<p class="text-surface-500 text-sm mt-2">Add blogs via the admin dashboard.</p>
<p class="text-surface-600 dark:text-surface-400 text-sm mt-2">Add blogs via the admin dashboard.</p>
</div>
{# Category Items Tab (one for each category) #}
<div x-show="activeTab.startsWith('category:') && !loading" class="space-y-4">
<div x-show="activeTab.startsWith('category:') && !loading" role="tabpanel" :id="'blogroll-panel-' + activeTab.replace('category:', '').toLowerCase().replace(/\s+/g, '-')" :aria-labelledby="'blogroll-tab-' + activeTab.replace('category:', '').toLowerCase().replace(/\s+/g, '-')" class="space-y-4">
<template x-for="item in categoryItems" :key="item.id">
<article class="bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-4 sm:p-6 hover:border-orange-400 dark:hover:border-orange-600 transition-colors">
<article class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 p-4 sm:p-6 hover:border-orange-400 dark:hover:border-orange-600 transition-colors shadow-sm">
<div class="flex items-start gap-4">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
@@ -126,7 +130,7 @@ permalink: /blogroll/
Upcoming
</span>
</div>
<div class="flex flex-wrap items-center gap-2 text-sm text-surface-500 mb-3">
<div class="flex flex-wrap items-center gap-2 text-sm text-surface-600 dark:text-surface-400 mb-3">
<a
:href="item.blog?.siteUrl || '#'"
class="inline-flex items-center gap-1 px-2 py-0.5 bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-400 rounded-full hover:bg-orange-200 dark:hover:bg-orange-900/50 transition-colors"
@@ -139,7 +143,7 @@ permalink: /blogroll/
</svg>
<span x-text="item.blog?.title || 'Unknown'"></span>
</a>
<time :datetime="item.published" x-text="formatDate(item.published)"></time>
<time class="font-mono text-sm" :datetime="item.published" x-text="formatDate(item.published)"></time>
</div>
<p x-show="item.summary" class="text-sm text-surface-600 dark:text-surface-400 line-clamp-3" x-text="item.summary"></p>
</div>
@@ -167,7 +171,7 @@ permalink: /blogroll/
<a
x-show="item.blog?.siteUrl"
:href="item.blog?.siteUrl"
class="inline-flex items-center gap-2 text-sm text-surface-500 hover:text-surface-700 dark:hover:text-surface-300"
class="inline-flex items-center gap-2 text-sm text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300"
target="_blank"
rel="noopener"
>
@@ -221,11 +225,11 @@ permalink: /blogroll/
{# Empty State for Category Items #}
<div x-show="categoryItems.length === 0 && !error" class="text-center py-12">
<svg class="w-16 h-16 mx-auto text-surface-300 dark:text-surface-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-16 h-16 mx-auto text-surface-300 dark:text-surface-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"/>
</svg>
<p class="text-surface-600 dark:text-surface-400 text-lg">No posts in this category yet.</p>
<p class="text-surface-500 text-sm mt-2">Posts will appear once blogs are synced.</p>
<p class="text-surface-600 dark:text-surface-400 text-sm mt-2">Posts will appear once blogs are synced.</p>
</div>
</div>
</div>
@@ -256,11 +260,11 @@ permalink: /blogroll/
<div class="grid grid-cols-2 gap-3 text-center">
<div class="p-3 bg-surface-50 dark:bg-surface-800 rounded-lg">
<span class="text-2xl font-bold text-orange-600 dark:text-orange-400 block" x-text="status?.blogs?.count || 0"></span>
<span class="text-xs text-surface-500 uppercase">Blogs</span>
<span class="text-xs text-surface-600 dark:text-surface-400 uppercase">Blogs</span>
</div>
<div class="p-3 bg-surface-50 dark:bg-surface-800 rounded-lg">
<span class="text-2xl font-bold text-orange-600 dark:text-orange-400 block" x-text="status?.items?.count || 0"></span>
<span class="text-xs text-surface-500 uppercase">Posts</span>
<span class="text-xs text-surface-600 dark:text-surface-400 uppercase">Posts</span>
</div>
</div>
</div>
@@ -283,7 +287,7 @@ permalink: /blogroll/
class="w-full text-left px-3 py-2 rounded-lg text-sm transition-colors"
>
<span x-text="cat.name"></span>
<span class="text-surface-500" x-text="`(${cat.count})`"></span>
<span class="text-surface-600 dark:text-surface-400" x-text="`(${cat.count})`"></span>
</button>
</li>
</template>

View File

@@ -14,7 +14,7 @@ permalink: "bookmarks/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageN
<h1 class="text-2xl sm:text-3xl font-bold text-surface-900 dark:text-surface-100">Bookmarks</h1>
{% set sparklineSvg = collections.bookmarks | postingFrequency %}
{% if sparklineSvg %}
<span class="text-amber-600 dark:text-amber-400">{{ sparklineSvg | safe }}</span>
<div class="flex-1 min-w-0 text-amber-600 dark:text-amber-400">{{ sparklineSvg | safe }}</div>
{% endif %}
</div>
<p class="text-surface-600 dark:text-surface-400 mb-6 sm:mb-8">
@@ -36,7 +36,7 @@ permalink: "bookmarks/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageN
{% endif %}
</div>
<div class="post-meta mt-2">
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
{% if post.data.category %}
@@ -55,7 +55,7 @@ permalink: "bookmarks/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageN
{% set bookmarkedUrl = post.data.bookmarkOf or post.data.bookmark_of %}
{% if bookmarkedUrl %}
{% unfurl bookmarkedUrl %}
<a class="u-bookmark-of text-xs text-surface-400 dark:text-surface-500 hover:underline break-all mt-1 inline-block" href="{{ bookmarkedUrl }}">
<a class="u-bookmark-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ bookmarkedUrl }}">
{{ bookmarkedUrl }}
</a>
{% endif %}
@@ -81,7 +81,7 @@ permalink: "bookmarks/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageN
Previous
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
Previous
</span>
@@ -93,7 +93,7 @@ permalink: "bookmarks/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageN
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
Next
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</span>

View File

@@ -2,6 +2,7 @@
layout: layouts/base.njk
title: Categories
withSidebar: true
pagefindIgnore: true
permalink: categories/
eleventyImport:
collections:

View File

@@ -1,6 +1,7 @@
---
layout: layouts/base.njk
withSidebar: true
pagefindIgnore: true
pagination:
data: collections.categories
size: 1
@@ -31,7 +32,7 @@ eleventyComputed:
{% endfor %}
{% if categoryPosts.length > 0 %}
<p class="text-sm text-surface-500 dark:text-surface-400 mb-4">{{ categoryPosts.length }} post{% if categoryPosts.length != 1 %}s{% endif %}</p>
<p class="text-sm text-surface-600 dark:text-surface-400 mb-4">{{ categoryPosts.length }} post{% if categoryPosts.length != 1 %}s{% endif %}</p>
<ul class="post-list">
{% for post in categoryPosts %}
{% set postType = post.inputPath | replace("./content/", "") %}
@@ -59,7 +60,7 @@ eleventyComputed:
</h2>
</div>
<div class="post-meta mt-2">
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
<span class="post-type">{{ postType }}</span>

View File

@@ -14,11 +14,13 @@ withSidebar: false
<div x-data="changelogApp()" x-init="init()">
{# Tab navigation #}
<div class="flex gap-1 mb-6 border-b border-surface-200 dark:border-surface-700 overflow-x-auto">
<div class="flex gap-1 mb-6 border-b border-surface-200 dark:border-surface-700 overflow-x-auto" role="tablist" aria-label="Changelog categories">
<template x-for="tab in tabs" :key="tab.key">
<button
@click="activeTab = tab.key"
:class="activeTab === tab.key ? 'border-b-2 border-accent-500 text-accent-600 dark:text-accent-400' : 'text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
:class="activeTab === tab.key ? 'border-b-2 border-accent-500 text-accent-600 dark:text-accent-400' : 'text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
:aria-selected="(activeTab === tab.key).toString()"
role="tab"
class="flex items-center gap-1.5 px-3 py-2 text-sm font-medium transition-colors -mb-px whitespace-nowrap flex-shrink-0"
>
<span x-text="tab.label"></span>
@@ -37,18 +39,18 @@ withSidebar: false
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
<span class="ml-3 text-surface-500">Loading changelog...</span>
<span class="ml-3 text-surface-600 dark:text-surface-400">Loading changelog...</span>
</div>
{# Commit list #}
<div x-show="!loading" x-cloak>
<template x-if="filteredCommits().length === 0">
<p class="text-surface-500 py-8 text-center">No recent activity in this category.</p>
<p class="text-surface-600 dark:text-surface-400 py-8 text-center">No recent activity in this category.</p>
</template>
<ul class="space-y-4">
<template x-for="commit in filteredCommits()" :key="commit.fullSha">
<li class="border border-surface-200 dark:border-surface-700 rounded-lg p-4">
<li class="bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 rounded-lg p-4 shadow-sm">
<div class="flex items-start gap-3">
<a :href="commit.url" target="_blank" rel="noopener"
class="font-mono text-xs bg-surface-100 dark:bg-surface-800 px-1.5 py-0.5 rounded text-accent-600 dark:text-accent-400 hover:underline flex-shrink-0 mt-0.5"
@@ -64,12 +66,12 @@ withSidebar: false
<a :href="commit.repoUrl" target="_blank" rel="noopener"
class="text-xs px-2 py-0.5 rounded-full bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-400 hover:text-accent-600 dark:hover:text-accent-400"
x-text="commit.repoName"></a>
<span class="text-xs text-surface-500" x-text="formatDate(commit.date)"></span>
<span class="text-xs text-surface-400" x-text="'by ' + commit.author"></span>
<span class="text-xs text-surface-600 dark:text-surface-400 font-mono" x-text="formatDate(commit.date)"></span>
<span class="text-xs text-surface-600 dark:text-surface-400" x-text="'by ' + commit.author"></span>
</div>
<template x-if="commit.body">
<details class="mt-2">
<summary class="text-xs text-surface-500 cursor-pointer hover:text-surface-700 dark:hover:text-surface-300">Show details</summary>
<summary class="text-xs text-surface-600 dark:text-surface-400 cursor-pointer hover:text-surface-700 dark:hover:text-surface-300">Show details</summary>
<pre class="mt-1 text-xs text-surface-600 dark:text-surface-400 whitespace-pre-wrap break-words bg-surface-50 dark:bg-surface-800 rounded p-2" x-text="commit.body"></pre>
</details>
</template>
@@ -95,7 +97,7 @@ withSidebar: false
</div>
{# Summary #}
<div x-show="commits.length > 0" class="mt-6 text-center text-xs text-surface-400">
<div x-show="commits.length > 0" class="mt-6 text-center text-xs text-surface-600 dark:text-surface-400">
<span x-text="commits.length + ' commits'"></span>
<span x-show="currentDays !== 'all'"> from the last <span x-text="currentDays"></span> days</span>
<span x-show="currentDays === 'all'"> (all time)</span>

View File

@@ -45,6 +45,8 @@ main.container{padding-top:1.5rem;padding-bottom:1.5rem}
.layout-with-sidebar{display:grid;grid-template-columns:1fr;gap:1.5rem}
@media(min-width:1024px){.layout-with-sidebar{grid-template-columns:2fr 1fr;gap:2rem}}
.main-content{min-width:0;overflow-x:hidden}
/* Reserve sidebar space on desktop to prevent CLS when Alpine.js hydrates collapsible widgets */
@media(min-width:1024px){.sidebar{min-height:600px}}
/* Basic typography — prevent FOUT */
h1,h2,h3,h4{margin:0;line-height:1.25}
@@ -55,3 +57,28 @@ a{color:#b45309}
.site-nav{display:flex;align-items:center;gap:1rem}
.site-nav>a,.site-nav .nav-dropdown-trigger{color:#5c5750;text-decoration:none;padding-top:0.5rem;padding-bottom:0.5rem}
.dark .site-nav>a,.dark .site-nav .nav-dropdown-trigger{color:#a09a90}
/* Prevent FOUC — constrain images and SVG icons before Tailwind loads */
img{max-width:100%;height:auto}
svg:not(:root):not([width]){width:1.25rem;height:1.25rem}
/* Focus indicators — visible in critical CSS before Tailwind loads */
a:focus-visible{outline:2px solid #b45309;outline-offset:2px;border-radius:2px}
.dark a:focus-visible{outline-color:#fbbf24}
button:focus-visible,[type="button"]:focus-visible{outline:2px solid #b45309;outline-offset:2px;border-radius:4px}
.dark button:focus-visible,.dark [type="button"]:focus-visible{outline-color:#fbbf24}
/* Skip link */
.skip-link{position:absolute;top:-100%;left:0;z-index:100;background:#b45309;color:#fff;padding:0.5rem 1rem;font-weight:600;text-decoration:none}
.skip-link:focus{top:0;outline:none}
/* Skeleton loader — visible until Tailwind stylesheet loads */
html.loading main.container>.page-content{display:none}
html:not(.loading) .page-skeleton{display:none}
@keyframes skel-pulse{0%,100%{opacity:1}50%{opacity:.4}}
.skel-bone{background:#e8e5df;border-radius:.5rem;animation:skel-pulse 1.5s ease-in-out infinite}
.dark .skel-bone{background:#3f3b35}
.skel-circle{border-radius:50%}
/* Reduced motion — disable animations for users who prefer it */
@media(prefers-reduced-motion:reduce){.skel-bone{animation:none}*{transition-duration:0.01ms!important;animation-duration:0.01ms!important}}

View File

@@ -6,7 +6,7 @@ lite-youtube {
background-position: center center;
background-size: cover;
cursor: pointer;
max-width: 720px;
max-width: 100%;
}
/* gradient */
@@ -70,8 +70,11 @@ lite-youtube > .lty-playbtn {
}
lite-youtube:hover > .lty-playbtn,
lite-youtube .lty-playbtn:focus {
lite-youtube .lty-playbtn:focus-visible {
filter: none;
outline: 2px solid #fbbf24;
outline-offset: -2px;
border-radius: 4px;
}
/* Post-click styles */

View File

@@ -49,7 +49,7 @@ pre[class*="language-"] {
.token.prolog,
.token.doctype,
.token.cdata {
color: #6a737d;
color: #586069;
}
.token.punctuation {
@@ -57,7 +57,7 @@ pre[class*="language-"] {
}
.token.namespace {
opacity: 0.7;
opacity: 0.85;
}
.token.property,

View File

@@ -187,7 +187,7 @@
}
.nav-dropdown-menu a {
@apply block px-4 py-2 text-sm text-surface-600 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-700 hover:text-surface-900 dark:hover:text-surface-100 no-underline;
@apply block px-4 py-2.5 text-sm text-surface-600 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-700 hover:text-surface-900 dark:hover:text-surface-100 no-underline;
}
.nav-dropdown-divider {
@@ -224,7 +224,7 @@
}
.mobile-nav-submenu a {
@apply pl-8 py-2 text-sm border-b-0;
@apply pl-8 py-3 text-sm border-b-0;
}
.mobile-nav-divider {
@@ -288,7 +288,7 @@
/* Site footer */
.site-footer {
@apply mt-12 py-8 border-t border-surface-200 dark:border-surface-700 text-center text-sm text-surface-500;
@apply mt-12 py-8 border-t border-surface-200 dark:border-surface-700 text-center text-sm text-surface-600 dark:text-surface-400;
}
.site-footer a {
@@ -332,7 +332,7 @@
/* Category tags (post metadata pills) */
.p-category {
@apply inline-block px-2 py-0.5 text-xs bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-300 rounded border border-surface-200 dark:border-surface-700 hover:border-surface-400 dark:hover:border-surface-500 transition-colors;
@apply inline-block px-3 py-1.5 text-xs bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-300 rounded border border-surface-200 dark:border-surface-700 hover:border-accent-400 dark:hover:border-accent-600 transition-colors;
}
/* Inline hashtags in post content — styled as subtle links, not pills */
@@ -353,12 +353,12 @@
}
.facepile-avatar img {
@apply w-8 h-8 rounded-full;
@apply w-10 h-10 rounded-full;
}
/* GitHub components */
.repo-card {
@apply p-4 border border-surface-200 dark:border-surface-700 rounded-lg;
@apply p-4 border border-surface-200 dark:border-surface-700 rounded-lg shadow-sm;
}
.repo-meta {
@@ -391,11 +391,11 @@
/* Widget cards */
.widget {
@apply p-4 mb-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden;
@apply p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden;
}
.widget-title {
@apply font-bold text-lg mb-4;
@apply font-semibold text-lg mb-4;
}
/* Collapsible widget wrapper */
@@ -495,23 +495,19 @@
}
}
/* Focus states */
@layer base {
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
@apply outline-2 outline-offset-2 outline-accent-500;
}
}
/* Active states — subtle press feedback on buttons */
@layer base {
button:active:not(:disabled),
.pagination-link:active:not(.disabled) {
transform: scale(0.97);
}
@media (prefers-reduced-motion: reduce) {
button:active:not(:disabled),
.pagination-link:active:not(.disabled) {
transform: none;
}
}
}
/* Video embeds */
@@ -548,7 +544,7 @@
}
.fab-menu-item {
@apply flex items-center gap-3 px-4 py-3 rounded-xl bg-surface-50 dark:bg-surface-800 shadow-md hover:shadow-lg border border-surface-200 dark:border-surface-700 text-surface-700 dark:text-surface-200 hover:text-accent-600 dark:hover:text-accent-400 no-underline transition-all duration-150 text-sm font-medium;
@apply flex items-center gap-3 px-4 py-3 rounded-lg bg-surface-50 dark:bg-surface-800 shadow-sm border border-surface-200 dark:border-surface-700 text-surface-700 dark:text-surface-200 hover:text-accent-600 dark:hover:text-accent-400 no-underline transition-all duration-150 text-sm font-medium;
}
.fab-menu-divider {
@@ -568,13 +564,6 @@
}
}
/* Dates — monospace for technical texture (system.md: every <time> gets font-mono) */
@layer base {
time {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
}
/* Apply content-visibility to images and post items for performance */
@layer base {
/* Responsive typography */
@@ -648,7 +637,7 @@
transition: opacity 0.15s;
}
.prose :is(h2, h3, h4):hover > a.header-anchor::after {
opacity: 0.4;
opacity: 1;
}
.post-list li {
@@ -695,7 +684,7 @@
--pagefind-ui-primary: #fbbf24;
--pagefind-ui-text: #faf8f5;
--pagefind-ui-background: #0f0e0d;
--pagefind-ui-border: #3f3b35;
--pagefind-ui-border: #5c5750;
--pagefind-ui-tag: #2a2722;
}
@@ -710,7 +699,7 @@
.dark #search .pagefind-ui__search-input {
background-color: #1c1b19;
color: #faf8f5;
border-color: #3f3b35;
border-color: #5c5750;
}
#search .pagefind-ui__search-input:focus {
@@ -865,14 +854,13 @@
/* Sparkline — inline SVG posting frequency chart */
.sparkline {
width: 120px;
width: 100%;
height: 28px;
display: block;
}
@media (min-width: 640px) {
.sparkline {
width: 180px;
height: 32px;
}
}
@@ -907,6 +895,14 @@ body[data-indiekit-auth="true"] .save-later-btn:hover {
pointer-events: none;
}
.dark body[data-indiekit-auth="true"] .save-later-btn {
color: #9ca3af;
}
.dark body[data-indiekit-auth="true"] .save-later-btn:hover {
border-color: #4b5563;
color: #60a5fa;
}
/* Share Post buttons — hidden until auth confirmed */
.share-post-btn {
display: none;
@@ -931,6 +927,14 @@ body[data-indiekit-auth="true"] .share-post-btn:hover {
color: #10b981;
}
.dark body[data-indiekit-auth="true"] .share-post-btn {
color: #9ca3af;
}
.dark body[data-indiekit-auth="true"] .share-post-btn:hover {
border-color: #4b5563;
color: #10b981;
}
/* Post type dropdown */
.post-type-dropdown {
display: none;
@@ -970,17 +974,15 @@ body[data-indiekit-auth="true"] .share-post-btn:hover {
color: #10b981;
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
.post-type-dropdown {
/* Dark mode — class-based (matches site's darkMode: "class" config) */
.dark .post-type-dropdown {
background: #1f2937;
border-color: #374151;
}
.post-type-dropdown-item {
}
.dark .post-type-dropdown-item {
color: #d1d5db;
}
.post-type-dropdown-item:hover {
}
.dark .post-type-dropdown-item:hover {
background: #374151;
color: #34d399;
}
}

6
cv.njk
View File

@@ -78,7 +78,7 @@ pagefindIgnore: true
{% endif %}
{# Contact details #}
{% if cvLocality or cvCountry or cvOrg or cvUrl or cvEmail or cvKeyUrl %}
<div class="flex flex-wrap gap-x-4 gap-y-1 mt-4 text-sm text-surface-500 dark:text-surface-400">
<div class="flex flex-wrap gap-x-4 gap-y-1 mt-4 text-sm text-surface-600 dark:text-surface-400">
{% if cvLocality or cvCountry %}
<span>{% if cvLocality %}{{ cvLocality }}{% endif %}{% if cvLocality and cvCountry %}, {% endif %}{% if cvCountry %}{{ cvCountry }}{% endif %}</span>
{% endif %}
@@ -126,8 +126,8 @@ pagefindIgnore: true
{# Last Updated #}
{% if cv.lastUpdated %}
<p class="text-sm text-surface-500 text-center mt-8">
Last updated: <time datetime="{{ cv.lastUpdated }}">{{ cv.lastUpdated | date("PPP") }}</time>
<p class="text-sm text-surface-600 dark:text-surface-400 text-center mt-8">
Last updated: <time class="font-mono text-sm" datetime="{{ cv.lastUpdated }}">{{ cv.lastUpdated | date("PPP") }}</time>
</p>
{% endif %}

View File

@@ -2,6 +2,7 @@
layout: layouts/base.njk
title: Weekly Digest
withSidebar: true
pagefindIgnore: true
eleventyExcludeFromCollections: true
eleventyImport:
collections:
@@ -15,19 +16,19 @@ permalink: "digest/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumb
<div>
<h1 class="text-2xl sm:text-3xl font-bold text-surface-900 dark:text-surface-100 mb-2">Weekly Digest</h1>
<p class="text-surface-600 dark:text-surface-400 mb-6 sm:mb-8">
A weekly summary of all posts. Subscribe via <a href="/digest/feed.xml" class="text-orange-600 dark:text-orange-400 hover:underline">RSS</a> for one update per week.
A weekly summary of all posts. Subscribe via <a href="/digest/feed.xml" class="text-amber-600 dark:text-amber-400 hover:underline">RSS</a> for one update per week.
</p>
{% if paginatedDigests.length > 0 %}
<ul class="space-y-4">
{% for d in paginatedDigests %}
<li class="p-4 bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 rounded-lg hover:border-accent-300 dark:hover:border-accent-600 transition-colors">
<li class="p-4 bg-surface-50 dark:bg-surface-800 border border-surface-200 dark:border-surface-700 rounded-lg hover:border-amber-400 dark:hover:border-amber-600 transition-colors shadow-sm">
<a href="/digest/{{ d.slug }}/" class="block">
<h2 class="font-semibold text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">
{{ d.label }}
</h2>
<p class="text-sm text-surface-500 dark:text-surface-400 mt-1">
<time datetime="{{ d.startDate | isoDate }}">{{ d.startDate | dateDisplay }}</time> &ndash; <time datetime="{{ d.endDate | isoDate }}">{{ d.endDate | dateDisplay }}</time>
<p class="text-sm text-surface-600 dark:text-surface-400 mt-1">
<time class="font-mono" datetime="{{ d.startDate | isoDate }}">{{ d.startDate | dateDisplay }}</time> &ndash; <time class="font-mono" datetime="{{ d.endDate | isoDate }}">{{ d.endDate | dateDisplay }}</time>
&middot; {{ d.posts.length }} post{% if d.posts.length != 1 %}s{% endif %}
</p>
{% set typeLabels = [] %}
@@ -35,7 +36,7 @@ permalink: "digest/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumb
{% set typeLabels = (typeLabels.push(key + " (" + posts.length + ")"), typeLabels) %}
{% endfor %}
{% if typeLabels.length %}
<p class="text-xs text-surface-400 dark:text-surface-500 mt-1">
<p class="text-xs text-surface-600 dark:text-surface-400 mt-1">
{{ typeLabels | join(", ") }}
</p>
{% endif %}
@@ -56,7 +57,7 @@ permalink: "digest/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumb
Previous
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
Previous
</span>
@@ -68,7 +69,7 @@ permalink: "digest/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumb
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
Next
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</span>

View File

@@ -1,6 +1,7 @@
---
layout: layouts/base.njk
withSidebar: true
pagefindIgnore: true
eleventyExcludeFromCollections: true
eleventyImport:
collections:
@@ -18,7 +19,7 @@ permalink: "digest/{{ digest.slug }}/"
{{ digest.label }}
</h1>
<p class="text-surface-600 dark:text-surface-400 mb-6 sm:mb-8">
<time datetime="{{ digest.startDate | isoDate }}">{{ digest.startDate | dateDisplay }}</time> &ndash; <time datetime="{{ digest.endDate | isoDate }}">{{ digest.endDate | dateDisplay }}</time>
<time class="font-mono" datetime="{{ digest.startDate | isoDate }}">{{ digest.startDate | dateDisplay }}</time> &ndash; <time class="font-mono" datetime="{{ digest.endDate | isoDate }}">{{ digest.endDate | dateDisplay }}</time>
<span class="text-sm">({{ digest.posts.length }} post{% if digest.posts.length != 1 %}s{% endif %})</span>
</p>
@@ -38,7 +39,7 @@ permalink: "digest/{{ digest.slug }}/"
<section class="mb-8">
<h2 class="text-lg sm:text-xl font-semibold text-surface-800 dark:text-surface-200 mb-4 border-b border-surface-200 dark:border-surface-700 pb-2">
{{ typeInfo.label }}
<span class="text-sm font-normal text-surface-500 dark:text-surface-400">({{ typePosts.length }})</span>
<span class="text-sm font-normal text-surface-600 dark:text-surface-400">({{ typePosts.length }})</span>
</h2>
<ul class="space-y-4">
{% for post in typePosts %}
@@ -49,9 +50,9 @@ permalink: "digest/{{ digest.slug }}/"
<span class="text-red-500 flex-shrink-0">&#x2764;</span>
<div>
<a href="{{ targetUrl }}" class="text-accent-600 dark:text-accent-400 hover:underline break-all">{{ targetUrl }}</a>
<div class="text-sm text-surface-500 dark:text-surface-400 mt-1">
<time class="dt-published" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
&middot; <a href="{{ post.url }}" class="hover:underline">Permalink</a>
<div class="text-sm text-surface-600 dark:text-surface-400 mt-1">
<time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
&middot; <a href="{{ post.url }}" class="hover:underline" aria-label="Permalink: {{ post.data.title or (post.date | dateDisplay) }}">Permalink</a>
</div>
</div>
</div>
@@ -66,9 +67,9 @@ permalink: "digest/{{ digest.slug }}/"
{% else %}
<a href="{{ targetUrl }}" class="text-accent-600 dark:text-accent-400 hover:underline break-all">{{ targetUrl }}</a>
{% endif %}
<div class="text-sm text-surface-500 dark:text-surface-400 mt-1">
<time class="dt-published" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
&middot; <a href="{{ post.url }}" class="hover:underline">Permalink</a>
<div class="text-sm text-surface-600 dark:text-surface-400 mt-1">
<time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
&middot; <a href="{{ post.url }}" class="hover:underline" aria-label="Permalink: {{ post.data.title or (post.date | dateDisplay) }}">Permalink</a>
</div>
</div>
</div>
@@ -79,9 +80,9 @@ permalink: "digest/{{ digest.slug }}/"
<span class="text-green-500 flex-shrink-0">&#x1F501;</span>
<div>
<a href="{{ targetUrl }}" class="text-accent-600 dark:text-accent-400 hover:underline break-all">{{ targetUrl }}</a>
<div class="text-sm text-surface-500 dark:text-surface-400 mt-1">
<time class="dt-published" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
&middot; <a href="{{ post.url }}" class="hover:underline">Permalink</a>
<div class="text-sm text-surface-600 dark:text-surface-400 mt-1">
<time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
&middot; <a href="{{ post.url }}" class="hover:underline" aria-label="Permalink: {{ post.data.title or (post.date | dateDisplay) }}">Permalink</a>
</div>
</div>
</div>
@@ -102,9 +103,9 @@ permalink: "digest/{{ digest.slug }}/"
{% elif post.templateContent %}
<p class="text-surface-700 dark:text-surface-300 text-sm">{{ post.templateContent | striptags | truncate(120) }}</p>
{% endif %}
<div class="text-sm text-surface-500 dark:text-surface-400 mt-1">
<time class="dt-published" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
&middot; <a href="{{ post.url }}" class="hover:underline">Permalink</a>
<div class="text-sm text-surface-600 dark:text-surface-400 mt-1">
<time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
&middot; <a href="{{ post.url }}" class="hover:underline" aria-label="Permalink: {{ post.data.title or (post.date | dateDisplay) }}">Permalink</a>
</div>
</div>
@@ -116,18 +117,18 @@ permalink: "digest/{{ digest.slug }}/"
{% if post.templateContent %}
<p class="text-surface-700 dark:text-surface-300 text-sm mt-1">{{ post.templateContent | striptags | truncate(200) }}</p>
{% endif %}
<div class="text-sm text-surface-500 dark:text-surface-400 mt-1">
<time class="dt-published" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
&middot; <a href="{{ post.url }}" class="hover:underline">Permalink</a>
<div class="text-sm text-surface-600 dark:text-surface-400 mt-1">
<time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
&middot; <a href="{{ post.url }}" class="hover:underline" aria-label="Permalink: {{ post.data.title or (post.date | dateDisplay) }}">Permalink</a>
</div>
</div>
{% else %}
<div>
<p class="text-surface-700 dark:text-surface-300">{{ post.templateContent | striptags | truncate(200) }}</p>
<div class="text-sm text-surface-500 dark:text-surface-400 mt-1">
<time class="dt-published" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
&middot; <a href="{{ post.url }}" class="hover:underline">Permalink</a>
<div class="text-sm text-surface-600 dark:text-surface-400 mt-1">
<time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
&middot; <a href="{{ post.url }}" class="hover:underline" aria-label="Permalink: {{ post.data.title or (post.date | dateDisplay) }}">Permalink</a>
</div>
</div>
{% endif %}
@@ -150,7 +151,7 @@ permalink: "digest/{{ digest.slug }}/"
<nav class="flex justify-between items-center mt-8 pt-6 border-t border-surface-200 dark:border-surface-700" aria-label="Digest navigation">
{% if currentIndex > 0 %}
{% set newer = allDigests[currentIndex - 1] %}
<a href="/digest/{{ newer.slug }}/" class="text-accent-600 dark:text-accent-400 hover:underline">
<a href="/digest/{{ newer.slug }}/" class="text-amber-600 dark:text-amber-400 hover:underline">
&larr; {{ newer.label }}
</a>
{% else %}
@@ -158,7 +159,7 @@ permalink: "digest/{{ digest.slug }}/"
{% endif %}
{% if currentIndex < allDigests.length - 1 %}
{% set older = allDigests[currentIndex + 1] %}
<a href="/digest/{{ older.slug }}/" class="text-accent-600 dark:text-accent-400 hover:underline">
<a href="/digest/{{ older.slug }}/" class="text-amber-600 dark:text-amber-400 hover:underline">
{{ older.label }} &rarr;
</a>
{% else %}

View File

@@ -0,0 +1,439 @@
# Navigation Redesign Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Replace the unusable 22-item "/" dropdown with a curated header nav (Home, About, Now, Blog dropdown, Pages dropdown, Interactions, Dashboard, Search, Theme), update the footer to match the approved design, and refactor /slashes/ into a comprehensive site map covering all three page sources.
**Architecture:** Three files change — `base.njk` (header desktop nav, mobile nav, footer), `slashes.njk` (add Site Pages section), and `tailwind.css` (no structural CSS changes needed, existing nav component styles are reused). The "/" dropdown becomes a "Pages" dropdown with 4 curated items. CV and Digest move to footer only. /slashes/ gains a hardcoded "Site Pages" section for theme .njk pages.
**Tech Stack:** Nunjucks templates, Tailwind CSS utility classes, Alpine.js (dropdowns, auth-gated Dashboard link)
---
### Task 1: Replace desktop header nav in base.njk
**Files:**
- Modify: `_includes/layouts/base.njk:154-221` (desktop nav inside `.site-nav` and search/dashboard area)
**Step 1: Replace the desktop nav links and dropdowns**
Replace lines 154-221 of `base.njk` (from `<nav class="site-nav"` through the closing `</div>` of `.header-actions` before the mobile nav) with:
```nunjucks
<nav class="site-nav" id="site-nav">
<a href="/">Home</a>
<a href="/about/">About</a>
<a href="/now/">Now</a>
{# Blog dropdown #}
<div class="nav-dropdown" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
<a href="/blog/" class="nav-dropdown-trigger">
Blog
<svg class="w-3 h-3 ml-1 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</a>
<div class="nav-dropdown-menu" x-show="open" x-transition x-cloak>
<a href="/blog/">All Posts</a>
{% for pt in enabledPostTypes %}
<a href="{{ pt.path }}">{{ pt.label }}</a>
{% endfor %}
</div>
</div>
{# Pages dropdown #}
<div class="nav-dropdown" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
<a href="/slashes/" class="nav-dropdown-trigger">
Pages
<svg class="w-3 h-3 ml-1 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</a>
<div class="nav-dropdown-menu" x-show="open" x-transition x-cloak>
{% if blogrollStatus and blogrollStatus.source == "indiekit" %}<a href="/blogroll/">Blogroll</a>{% endif %}
{% if podrollStatus and podrollStatus.source == "indiekit" %}<a href="/podroll/">Podroll</a>{% endif %}
{% if newsActivity and newsActivity.source == "indiekit" %}<a href="/news/">News</a>{% endif %}
<a href="/slashes/">All Pages</a>
</div>
</div>
<a href="/interactions/">Interactions</a>
<a href="/dashboard"
x-data="{ show: false }"
x-show="show"
x-cloak
x-transition
@indiekit:auth.window="show = $event.detail.loggedIn"
class="admin-nav-link">
<svg class="w-4 h-4 inline -mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>
</svg>
Dashboard
</a>
</nav>
<a href="/search/" aria-label="Search" title="Search" class="p-2 rounded-lg text-surface-600 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/>
</svg>
</a>
<button id="theme-toggle" type="button" class="theme-toggle" aria-label="Toggle dark mode" title="Toggle dark mode">
<svg class="sun-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5"/>
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
</svg>
<svg class="moon-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
</button>
</div>
```
**Key changes from current:**
- Removed: CV direct link, Digest direct link, the "/" dropdown with 22+ items
- Added: Now direct link, "Pages" dropdown (Blogroll, Podroll, News, All Pages)
- Kept: Blog dropdown (unchanged), Interactions, Dashboard (auth-only), Search icon, Theme toggle
- The `hasPluginPages` variable is no longer needed in the header — plugin checks are inline in the Pages dropdown
**Step 2: Verify the edit didn't break the surrounding HTML structure**
Check that `<div class="header-actions">` still wraps the nav, search icon, and theme toggle. Check the closing `</div>` for `.header-container` is still in place at around line 237.
---
### Task 2: Replace mobile nav in base.njk
**Files:**
- Modify: `_includes/layouts/base.njk:240-309` (mobile nav `<nav class="mobile-nav">`)
**Step 1: Replace the mobile nav**
Replace the entire `<nav class="mobile-nav" ...>` block (lines 240-309) with:
```nunjucks
<nav class="mobile-nav hidden" id="mobile-nav" x-data="{ blogOpen: false, pagesOpen: false }">
<a href="/">Home</a>
<a href="/about/">About</a>
<a href="/now/">Now</a>
{# Blog section #}
<div class="mobile-nav-section">
<button type="button" class="mobile-nav-toggle" @click="blogOpen = !blogOpen">
Blog
<svg class="w-4 h-4 transition-transform" :class="{ 'rotate-180': blogOpen }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div class="mobile-nav-submenu" x-show="blogOpen" x-collapse>
<a href="/blog/">All Posts</a>
{% for pt in enabledPostTypes %}
<a href="{{ pt.path }}">{{ pt.label }}</a>
{% endfor %}
</div>
</div>
{# Pages section #}
<div class="mobile-nav-section">
<button type="button" class="mobile-nav-toggle" @click="pagesOpen = !pagesOpen">
Pages
<svg class="w-4 h-4 transition-transform" :class="{ 'rotate-180': pagesOpen }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div class="mobile-nav-submenu" x-show="pagesOpen" x-collapse>
{% if blogrollStatus and blogrollStatus.source == "indiekit" %}<a href="/blogroll/">Blogroll</a>{% endif %}
{% if podrollStatus and podrollStatus.source == "indiekit" %}<a href="/podroll/">Podroll</a>{% endif %}
{% if newsActivity and newsActivity.source == "indiekit" %}<a href="/news/">News</a>{% endif %}
<a href="/slashes/">All Pages</a>
</div>
</div>
<a href="/interactions/">Interactions</a>
<a href="/search/">Search</a>
<a href="/dashboard"
x-data="{ show: false }"
x-show="show"
x-cloak
@indiekit:auth.window="show = $event.detail.loggedIn">
Dashboard
</a>
{# Mobile theme toggle #}
<button type="button" class="mobile-theme-toggle" aria-label="Toggle dark mode">
<span class="theme-label">Theme</span>
<span class="theme-icons">
<svg class="sun-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5"/>
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
</svg>
<svg class="moon-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
</span>
</button>
</nav>
```
**Key changes from current:**
- Removed: CV link, Digest link, the "/" accordion with 22+ items
- Added: Now direct link, "Pages" accordion (Blogroll, Podroll, News, All Pages)
- Renamed Alpine variable: `slashOpen``pagesOpen`
- Kept: Blog accordion (unchanged), Interactions, Search, Dashboard (auth-only), theme toggle
---
### Task 3: Update footer in base.njk
**Files:**
- Modify: `_includes/layouts/base.njk:339-386` (footer `<footer>` block)
**Step 1: Replace the footer grid**
Replace lines 339-386 (the entire `<footer>` element) with:
```nunjucks
<footer class="border-t border-surface-200 dark:border-surface-700 mt-12 pt-8 pb-6">
<div class="container">
<div class="grid grid-cols-2 md:grid-cols-4 gap-8 mb-8">
{# Navigate #}
<div>
<h4 class="text-sm font-semibold uppercase tracking-wider text-surface-500 dark:text-surface-400 mb-3">Navigate</h4>
<ul class="space-y-2">
<li><a href="/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">Home</a></li>
<li><a href="/about/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">About</a></li>
<li><a href="/cv/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">CV</a></li>
<li x-data="{ show: false }" x-show="show" x-cloak @indiekit:auth.window="show = $event.detail.loggedIn">
<a href="/dashboard" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">Dashboard</a>
</li>
</ul>
</div>
{# Content #}
<div>
<h4 class="text-sm font-semibold uppercase tracking-wider text-surface-500 dark:text-surface-400 mb-3">Content</h4>
<ul class="space-y-2">
<li><a href="/blog/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">Blog</a></li>
{% for pt in enabledPostTypes %}
<li><a href="{{ pt.path }}" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">{{ pt.label }}</a></li>
{% endfor %}
<li><a href="/digest/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">Digest</a></li>
</ul>
</div>
{# Connect #}
<div>
<h4 class="text-sm font-semibold uppercase tracking-wider text-surface-500 dark:text-surface-400 mb-3">Connect</h4>
<ul class="space-y-2">
{% for social in site.social %}
<li><a href="{{ social.url }}" rel="{{ social.rel }}" target="_blank" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">{{ social.name }}</a></li>
{% endfor %}
</ul>
</div>
{# Meta #}
<div>
<h4 class="text-sm font-semibold uppercase tracking-wider text-surface-500 dark:text-surface-400 mb-3">Meta</h4>
<ul class="space-y-2">
<li><a href="/feed.xml" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">RSS Feed</a></li>
<li><a href="/feed.json" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">JSON Feed</a></li>
<li><a href="/changelog/" class="text-sm text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100 hover:underline">Changelog</a></li>
</ul>
</div>
</div>
<p class="text-center text-sm text-surface-500 dark:text-surface-400">Powered by <a href="https://getindiekit.com" class="hover:text-surface-900 dark:hover:text-surface-100 hover:underline">Indiekit</a> + <a href="https://11ty.dev" class="hover:text-surface-900 dark:hover:text-surface-100 hover:underline">Eleventy</a></p>
</div>
</footer>
```
**Key changes from current:**
- Navigate: Removed Changelog (was duplicated in Meta), removed Search (header icon suffices), added Dashboard (auth-only via Alpine.js)
- Content: Removed Interactions, added Digest
- Connect: Unchanged (dynamic social links)
- Meta: Unchanged (RSS, JSON Feed, Changelog)
---
### Task 4: Add "Site Pages" section to slashes.njk
**Files:**
- Modify: `slashes.njk:143-153` (after Activity Feeds section, before inspiration box)
**Step 1: Add the Site Pages section**
Replace lines 143-153 (from `{% endif %}` closing Activity Feeds through the inspiration `<div>`) with:
```nunjucks
{% endif %}
{# Site pages — theme-provided .njk pages not in collections.pages or activity feeds #}
<div class="mb-8">
<h2 class="text-lg font-semibold text-surface-800 dark:text-surface-200 mb-4">Site Pages</h2>
<p class="text-surface-600 dark:text-surface-400 text-sm mb-4">
Theme-provided pages for content aggregation, search, and site info.
</p>
<ul class="post-list">
<li class="post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/blog/" class="text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/blog</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">All posts chronologically</p>
</li>
<li class="post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/cv/" class="text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/cv</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Curriculum vitae</p>
</li>
<li class="post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/changelog/" class="text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/changelog</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Site changes and updates</p>
</li>
<li class="post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/digest/" class="text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/digest</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Content digest</p>
</li>
<li class="post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/featured/" class="text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/featured</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Featured posts</p>
</li>
<li class="post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/graph/" class="text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/graph</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Content graph visualization</p>
</li>
<li class="post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/interactions/" class="text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/interactions</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Social interactions (likes, reposts, replies)</p>
</li>
<li class="post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/readlater/" class="text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/readlater</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Read later queue</p>
</li>
<li class="post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/search/" class="text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/search</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Full-text search</p>
</li>
<li class="post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/starred/" class="text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/starred</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Starred GitHub repositories</p>
</li>
</ul>
</div>
{# Inspiration section #}
<div class="mt-8 p-4 bg-surface-100 dark:bg-surface-800 rounded-lg">
<h2 class="text-lg font-semibold text-surface-800 dark:text-surface-200 mb-2">Want more slash pages?</h2>
<p class="text-surface-600 dark:text-surface-400 text-sm">
Check out <a href="https://slashpages.net" class="text-accent-600 dark:text-accent-400 hover:underline" target="_blank" rel="noopener">slashpages.net</a>
for inspiration on pages like <code>/now</code>, <code>/uses</code>, <code>/colophon</code>, <code>/blogroll</code>, and more.
</p>
</div>
```
**Key additions:** /blog, /cv, /changelog, /digest, /featured, /graph, /interactions, /readlater, /search, /starred — all theme .njk pages that were previously invisible on /slashes/.
---
### Task 5: Verify the build locally
**Step 1: Build Eleventy and check for errors**
Run from the theme directory:
```bash
cd /home/rick/code/indiekit-dev/indiekit-eleventy-theme
npm run build 2>&1 | tail -20
```
Expected: Build completes with zero errors. Template syntax errors would fail the build.
**Step 2: Spot-check the output HTML**
```bash
# Check header nav has "Now" and "Pages" but not "CV" or "Digest"
grep -c 'href="/now/"' _site/index.html
# Expected: at least 1
grep 'nav-dropdown-trigger' _site/index.html | head -4
# Expected: "Blog" and "Pages" triggers, no "/" trigger
# Check footer has Dashboard with Alpine.js auth
grep 'Dashboard' _site/index.html | grep 'indiekit:auth'
# Expected: 1 match in footer
# Check /slashes/ has "Site Pages" section
grep 'Site Pages' _site/slashes/index.html
# Expected: 1 match
```
---
### Task 6: Commit, push, and update submodule
**Step 1: Commit the theme changes**
```bash
cd /home/rick/code/indiekit-dev/indiekit-eleventy-theme
git add _includes/layouts/base.njk slashes.njk
git commit -m "feat: redesign navigation - curated header, updated footer, comprehensive /slashes/"
git push origin main
```
**Step 2: Update the submodule in indiekit-cloudron**
```bash
cd /home/rick/code/indiekit-dev/indiekit-cloudron
git submodule update --remote eleventy-site
git add eleventy-site
git commit -m "chore: update eleventy-site submodule (navigation redesign)"
git push origin main
```
**Step 3: Deploy**
```bash
cd /home/rick/code/indiekit-dev/indiekit-cloudron
make prepare
cloudron build --no-cache && cloudron update --app rmendes.net --no-backup
```
---
## Summary of Changes
| Before | After |
|--------|-------|
| "/" dropdown with 22+ items | "Pages" dropdown with 4 curated items |
| CV in header | CV in footer only |
| Digest in header | Digest in footer Content column |
| No "Now" in header | Now as direct link |
| Footer: Changelog in Navigate | Changelog in Meta only (no duplicate) |
| Footer: Search in Navigate | Search removed (header icon suffices) |
| Footer: Interactions in Content | Interactions removed from footer |
| Footer: No Dashboard | Dashboard in Navigate (auth-only) |
| Footer: No Digest | Digest in Content column |
| /slashes/: 2 sections (Pages + Activity) | 3 sections (Pages + Activity + Site Pages) |
| /slashes/: Missing 10+ theme pages | All theme .njk pages listed |

View File

@@ -0,0 +1,730 @@
# Design System Compliance Fix — Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Fix all 368 design system violations found by the comprehensive audit across ~80 Nunjucks template files and CSS.
**Architecture:** Fix CSS-level component classes first (highest leverage — one fix eliminates violations across many templates), then fix templates by violation category, grouped by file similarity. Each task handles one violation category across a batch of related files.
**Tech Stack:** Nunjucks templates, Tailwind CSS, Alpine.js
**Important context:**
- Source of truth: `/home/rick/code/indiekit-dev/indiekit-eleventy-theme/` (this repo is a Git submodule)
- Design system: `.interface-design/system.md`
- No test suite — verification is visual via `cloudron restart` after deployment
- CSS focus-visible base layer already exists in `css/tailwind.css:117-142,498-506` — most "missing focus:ring-2" violations are false positives since the CSS handles focus-visible globally on `button`, `a`, `input`, `textarea`, `select`. Only elements using `focus:outline-none` (which suppresses the global style) actually need inline `focus:ring-2`.
- CSS `time` base rule already sets monospace font-family on all `<time>` elements (`css/tailwind.css:112-115,571-576`). Only `<span>` elements rendering dates via Alpine.js `x-text="formatDate()"` need explicit `font-mono` class.
---
## Task Overview
| Task | Category | Files | Violations Fixed |
|------|----------|-------|-----------------|
| 1 | CSS component class fixes | `css/tailwind.css` | ~30 (cascading to many templates) |
| 2 | Cards missing `shadow-sm` | 25 files | ~69 |
| 3 | Wrong domain colors | 8 files | ~33 |
| 4 | Date `font-mono` on `<span>` elements | 10 files | ~20 |
| 5 | Date `font-mono` on `<time>` elements (class consistency) | 20 files | ~55 |
| 6 | Hover violations | 8 files | ~14 |
| 7 | Border radius violations | 10 files | ~12 |
| 8 | Dark mode violations | 8 files | ~10 |
| 9 | Depth violations (shadow levels) | 6 files | ~14 |
| 10 | Transition violations | 6 files | ~11 |
| 11 | Inline `focus:ring-2` cleanup | 7 files | ~20 |
| 12 | Remaining button/focus violations | 15 files | ~80 |
---
### Task 1: CSS Component Class Fixes (tailwind.css)
**Files:**
- Modify: `css/tailwind.css`
These CSS-level fixes cascade to many templates automatically.
**Step 1: Fix `.widget-title` weight**
Line 398: Change `font-bold` to `font-semibold` per system.md.
```css
/* Before */
.widget-title {
@apply font-bold text-lg mb-4;
}
/* After */
.widget-title {
@apply font-semibold text-lg mb-4;
}
```
**Step 2: Fix `.repo-card` missing `shadow-sm`**
Line 361: Add `shadow-sm`.
```css
/* Before */
.repo-card {
@apply p-4 border border-surface-200 dark:border-surface-700 rounded-lg;
}
/* After */
.repo-card {
@apply p-4 border border-surface-200 dark:border-surface-700 rounded-lg shadow-sm;
}
```
**Step 3: Fix `.fab-menu-item` border radius**
Line 551: Change `rounded-xl` to `rounded-lg`.
```css
/* Before */
.fab-menu-item {
@apply ... rounded-xl bg-surface-50 ... shadow-md ...;
}
/* After */
.fab-menu-item {
@apply ... rounded-lg bg-surface-50 ... shadow-sm ...;
}
```
Also change `shadow-md hover:shadow-lg` to `shadow-sm` (system says cards/menu items = `shadow-sm`).
**Step 4: Fix `.p-category` hover border**
Line 335: Change `hover:border-surface-400 dark:hover:border-surface-500` to `hover:border-accent-400 dark:hover:border-accent-600`.
```css
/* Before */
.p-category {
@apply ... hover:border-surface-400 dark:hover:border-surface-500 transition-colors;
}
/* After */
.p-category {
@apply ... hover:border-accent-400 dark:hover:border-accent-600 transition-colors;
}
```
**Step 5: Fix `.pagination-link` — add `transition-colors` explicitly**
Line 490: The class already has `transition-colors`. Verify it also covers focus states adequately via the base CSS layer. No change needed if base layer covers it.
**Step 6: Remove `.widget` `mb-4`**
Line 394: Remove `mb-4` since widgets are inside `space-y-*` containers which handle spacing. The `mb-4` conflicts with container spacing.
```css
/* Before */
.widget {
@apply p-4 mb-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden;
}
/* After */
.widget {
@apply p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden;
}
```
**Step 7: Consolidate duplicate focus-visible systems**
Lines 117-142 and 498-506 define TWO competing focus-visible systems. Remove the duplicate at 498-506 (the outline-based one) and keep the ring-based one at 117-142 which is more specific and matches the system.md pattern.
```css
/* DELETE lines 498-506 */
/* Focus states */
@layer base {
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
@apply outline-2 outline-offset-2 outline-accent-500;
}
}
```
**Step 8: Fix `.post-type-dropdown` dark mode**
Lines 974-986: Change `@media (prefers-color-scheme: dark)` to `.dark` class selector (the site uses class-based dark mode).
```css
/* Before */
@media (prefers-color-scheme: dark) {
.post-type-dropdown { ... }
.post-type-dropdown-item { ... }
.post-type-dropdown-item:hover { ... }
}
/* After */
.dark .post-type-dropdown {
background: #1f2937;
border-color: #374151;
}
.dark .post-type-dropdown-item {
color: #d1d5db;
}
.dark .post-type-dropdown-item:hover {
background: #374151;
color: #34d399;
}
```
**Step 9: Fix `.save-later-btn` and `.share-post-btn` dark mode**
Lines 881-932: Add `.dark` variants for these buttons (currently no dark mode support).
```css
/* Add after line 907 */
.dark body[data-indiekit-auth="true"] .save-later-btn {
color: #9ca3af;
}
.dark body[data-indiekit-auth="true"] .save-later-btn:hover {
border-color: #4b5563;
color: #60a5fa;
}
/* Add after line 932 */
.dark body[data-indiekit-auth="true"] .share-post-btn {
color: #9ca3af;
}
.dark body[data-indiekit-auth="true"] .share-post-btn:hover {
border-color: #4b5563;
color: #34d399;
}
```
**Step 10: Commit**
```bash
git add css/tailwind.css
git commit -m "fix(css): fix 10 design system violations in component classes
- .widget-title: font-bold -> font-semibold
- .repo-card: add shadow-sm
- .fab-menu-item: rounded-xl -> rounded-lg, shadow-md -> shadow-sm
- .p-category: hover border surface -> accent
- .widget: remove mb-4 (conflicts with space-y containers)
- Remove duplicate focus-visible system (outline vs ring)
- .post-type-dropdown: prefers-color-scheme -> .dark class
- .save-later-btn/.share-post-btn: add dark mode variants
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596"
```
---
### Task 2: Cards Missing `shadow-sm` (25 files)
**Files to modify:**
Group A — Page templates:
- `github.njk` — lines 30, 104, 137, 171, 226 (5 cards)
- `funkwhale.njk` — lines 188, 238 (2 cards)
- `youtube.njk` — lines 37, 176 (2 cards)
- `listening.njk` — lines 273, 327, 399, 460 (4 cards)
- `blogroll.njk` — lines 68, 112 (2 cards)
- `readlater.njk` — lines 83 (1 card)
- `starred.njk` — line 187 (1 card)
- `interactions.njk` — lines 44, 60, 76, 92, 108, 217 (6 cards)
- `changelog.njk` — line 51 (1 card)
- `digest-index.njk` — line 25 (1 card)
Group B — Components/sections:
- `_includes/components/webmentions.njk` — lines 116, 186 (2 cards)
- `_includes/components/comments.njk` — line 78 (1 card)
- `_includes/components/funkwhale-stats-content.njk` — lines 4, 29 (2 cards)
- `_includes/components/fediverse-modal.njk` — line 32 (1 card)
- `_includes/components/post-navigation.njk` — lines 34, 80 (2 cards)
- `_includes/components/sections/cv-education.njk` — line 19 (1 card)
- `_includes/components/sections/cv-skills.njk` — line 18 (1 card)
- `_includes/components/sections/cv-interests.njk` — line 18 (1 card)
- `_includes/components/sections/cv-projects.njk` — line 19 (1 card)
- `_includes/components/sections/cv-projects-work.njk` — line 29 (1 card)
- `_includes/components/sections/cv-projects-personal.njk` — line 29 (1 card)
- `_includes/components/sections/ai-usage.njk` — lines 13, 16 (2 cards)
Group C — Layout templates:
- `_includes/layouts/page.njk` — lines 36, 40, 44, 48, 83 (5 cards)
- `_includes/layouts/home.njk` — lines 82, 113, 122, 131 (4 cards)
**Pattern:** For each card element, find the `border border-surface-200 dark:border-surface-700` class string and add `shadow-sm` after it. If the element has `border` but no surface tokens, add the full pattern: `border border-surface-200 dark:border-surface-700 shadow-sm`.
**Special cases:**
- `interactions.njk` cards (lines 44-108): These cards have NO border at all — add `border border-surface-200 dark:border-surface-700 shadow-sm`
- `changelog.njk:51`: Also add `bg-surface-50 dark:bg-surface-800` (missing background)
- `page.njk:36,40,44,48`: Change `bg-white` to `bg-surface-50` (token compliance)
- `webmentions.njk:116,186` and `comments.njk:78`: Change `bg-surface-100` to `bg-surface-50` (system says card bg = surface-50)
- `post-navigation.njk:34,80`: Change `bg-surface-100` to `bg-surface-50`
**Step 1: Fix Group A files** (page templates — add `shadow-sm` to each card)
**Step 2: Fix Group B files** (components/sections)
**Step 3: Fix Group C files** (layouts)
**Step 4: Commit**
```bash
git add *.njk _includes/
git commit -m "fix(cards): add shadow-sm to all card elements across 25 files
Design system requires shadow-sm + border on all cards.
Also fixes bg-white -> bg-surface-50 and bg-surface-100 -> bg-surface-50
where card backgrounds used wrong tokens.
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596"
```
---
### Task 3: Wrong Domain Colors (8 files)
**Domain color reference:**
- Writing (amber): articles, notes, bookmarks, photos, blog, digest, categories, news
- Social (rose): likes, replies, reposts, interactions
- Code (emerald): github, starred
- Music (purple): funkwhale, listening
- Video (red): youtube
- Reading (orange): blogroll, podroll, readlater
**Files to modify:**
**3a. `starred.njk` — 12 violations (accent -> emerald)**
Every `accent-*` reference on this page should be `emerald-*`:
- Line 11: `text-accent-600 dark:text-accent-400` -> `text-emerald-600 dark:text-emerald-400`
- Lines 61-93: Tab button active states: `border-accent-600 text-accent-700 dark:text-accent-400` -> `border-emerald-600 text-emerald-700 dark:text-emerald-400`
- Line 111: `focus:ring-accent-500` -> `focus:ring-emerald-500`
- Line 126: Same
- Line 141: Same
- Lines 155: `bg-accent-600` -> `bg-emerald-600`
- Line 165: `text-accent-600 focus:ring-accent-500` -> `text-emerald-600 focus:ring-emerald-500`
- Lines 172-174: `text-accent-600 dark:text-accent-400` -> `text-emerald-600 dark:text-emerald-400`
- Line 193: `hover:text-surface-600` -> `hover:text-emerald-600 dark:hover:text-emerald-400`
- Lines 255-259: `bg-accent-600 hover:bg-accent-700` -> `bg-emerald-600 hover:bg-emerald-700`
**3b. `photos.njk` — 4 violations (purple -> amber)**
- Line 17: `text-purple-600 dark:text-purple-400` -> `text-amber-600 dark:text-amber-400` (sparkline)
- Line 28: `border-l-purple-400 dark:border-l-purple-500` -> `border-l-amber-400 dark:border-l-amber-500`
- Line 62: `text-purple-600 dark:text-purple-400` -> `text-amber-600 dark:text-amber-400`
**3c. `reposts.njk` — 1 violation (emerald sparkline -> rose)**
- Line 17: `text-emerald-600 dark:text-emerald-400` -> `text-rose-600 dark:text-rose-400`
**3d. `blog.njk` — 6 violations (red/green/sky -> rose for social types)**
- Line 42: `border-l-red-400 dark:border-l-red-500` -> `border-l-rose-400 dark:border-l-rose-500` (like card)
- Line 46: `border-l-green-400 dark:border-l-green-500` -> `border-l-rose-400 dark:border-l-rose-500` (repost card)
- Line 48: `border-l-sky-400 dark:border-l-sky-500` -> `border-l-rose-400 dark:border-l-rose-500` (reply card)
- Lines 60-61: `text-red-500` / `text-red-600 dark:text-red-400` -> `text-rose-500` / `text-rose-600 dark:text-rose-400`
- Lines 143-144: `text-green-500` / `text-green-600 dark:text-green-400` -> `text-rose-500` / `text-rose-600 dark:text-rose-400`
- Lines 182-184: `text-sky-500` / `text-sky-600 dark:text-sky-400` -> `text-rose-500` / `text-rose-600 dark:text-rose-400`
**3e. `featured.njk` — 3 violations (red/green/sky -> rose)**
- Line 56: `text-red-600 dark:text-red-400` / `text-red-500` -> `text-rose-600 dark:text-rose-400` / `text-rose-500`
- Line 85: `text-green-600 dark:text-green-400` -> `text-rose-600 dark:text-rose-400`
- Line 98: `text-sky-600 dark:text-sky-400` -> `text-rose-600 dark:text-rose-400`
**3f. `digest.njk` — 2 violations (accent -> amber on nav links)**
- Lines 154, 162: `text-accent-600 dark:text-accent-400` -> `text-amber-600 dark:text-amber-400`
**3g. `digest-index.njk` — 1 violation (orange -> amber)**
- Line 19: `text-orange-600 dark:text-orange-400` -> `text-amber-600 dark:text-amber-400`
**3h. `_includes/components/widgets/search.njk` — 2 violations (primary -> accent)**
- Line 4: `focus:ring-primary-500` -> `focus:ring-accent-500`
- Line 5: `bg-primary-600 hover:bg-primary-700` -> `bg-accent-600 hover:bg-accent-700`
**Step 1: Fix all files above**
**Step 2: Commit**
```bash
git add starred.njk photos.njk reposts.njk blog.njk featured.njk digest.njk digest-index.njk _includes/components/widgets/search.njk
git commit -m "fix(domain-colors): correct domain color assignments across 8 files
- starred.njk: accent -> emerald (Code domain)
- photos.njk: purple -> amber (Writing domain)
- reposts.njk: emerald -> rose (Social domain)
- blog.njk: red/green/sky -> rose (Social domain unified)
- featured.njk: red/green/sky -> rose (Social domain unified)
- digest.njk: accent -> amber (Writing domain)
- digest-index.njk: orange -> amber (Writing domain)
- search widget: primary -> accent (stale token)
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596"
```
---
### Task 4: Date `font-mono` on `<span>` Elements (Alpine.js renders)
These are dates rendered by Alpine.js `x-text="formatDate()"` into `<span>` elements (not `<time>`). The CSS base layer only covers `<time>` elements, so these `<span>` elements need explicit `font-mono`.
**Files to modify:**
- `starred.njk:27``<span x-text="formatDate(lastSync)">` — add `font-mono`
- `starred.njk:236``<span x-text="'Starred ' + formatDate(...)">` — add `font-mono`
- `changelog.njk:67``<span class="text-xs text-surface-500" x-text="formatDate(commit.date)">` — add `font-mono`
- `blogroll.njk:18``<span x-text="formatDate(status?.lastSync, 'full')">` — add `font-mono`
- `_includes/components/widgets/github-repos.njk:61``<span x-text="formatDate(commit.date)">` — add `font-mono`
- `_includes/components/widgets/github-repos.njk:86``<span x-text="formatDate(repo.updated_at)">` — add `font-mono`
- `_includes/components/widgets/github-repos.njk:142``<span x-text="formatDate(item.date)">` — add `font-mono`
- `interactions.njk:264``<time ... x-text="formatDate(...)">` — add `font-mono text-sm`
- `readlater.njk:99``<time ... x-text="formatDate(item.savedAt)">` — add `font-mono text-sm`
- `blogroll.njk:142``<time ... x-text="formatDate(item.published)">` — add `font-mono text-sm`
- `_includes/components/comments.njk:94``<time ... x-text="new Date(...)">` — add `font-mono text-sm`
**Step 1: Add `font-mono` class to each `<span>` and `font-mono text-sm` to each `<time>` listed above**
**Step 2: Commit**
```bash
git add starred.njk changelog.njk blogroll.njk interactions.njk readlater.njk _includes/components/widgets/github-repos.njk _includes/components/comments.njk
git commit -m "fix(dates): add font-mono to Alpine.js-rendered date spans
CSS base layer covers <time> elements automatically, but dates
rendered via x-text into <span> elements need explicit font-mono.
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596"
```
---
### Task 5: Date `font-mono` on `<time>` Elements (class consistency)
The CSS base layer sets `font-family: monospace` on all `<time>` elements globally, so these render correctly already. However, the design system convention is to also add `font-mono text-sm` as Tailwind classes for consistency and to ensure `text-sm` sizing. This task adds the classes to all `<time>` elements that are missing them.
**Files to modify:**
Group A — Post collection pages:
- `articles.njk:37-39``<time class="dt-published">` add `font-mono text-sm`
- `notes.njk:31-33``<time class="dt-published text-sm ... font-medium">` add `font-mono`
- `bookmarks.njk:39-41` — same pattern
- `photos.njk:30-32` — same
- `likes.njk:37-39` — same
- `replies.njk:42-44` — same
- `reposts.njk:42-44` — same
- `blog.njk:67,106,150,189,227,271,298` — 7 `<time>` elements, add `font-mono`
- `digest.njk:22,54,71,84,107,121,130` — 7 `<time>` elements
- `digest-index.njk:31` — 2 `<time>` elements
- `featured.njk:58,70,87,100,116,130` — 6 `<time>` elements
- `categories.njk:63-65` — 1 `<time>` element
Group B — Layouts:
- `_includes/layouts/page.njk:20``<time class="dt-updated">` add `font-mono text-sm`
- `_includes/layouts/post.njk:23-25``<time class="dt-published">` add `font-mono text-sm`
- `_includes/layouts/home.njk:92-94``<time>` add `font-mono`
Group C — Sections/components:
- `_includes/components/sections/recent-posts.njk:55,83,116,144,173,210,224` — 7 `<time>` elements
- `_includes/components/sections/featured-posts.njk:58,86,119,148,176,213,227` — 7 `<time>` elements
- `_includes/components/webmentions.njk:138,168` — 2 `<time>` elements
- `_includes/components/post-navigation.njk:46,92` — 2 `<time>` elements (also change `text-xs` to `text-sm`)
- `_includes/components/sections/cv-experience.njk:27` — date text, add `font-mono`
- `_includes/components/sections/cv-education.njk:43,47,72,76` — date text, add `font-mono`
- `_includes/components/sections/cv-projects.njk:55,82` — date text, add `font-mono`
- `_includes/components/sections/cv-projects-work.njk:65,92` — date text
- `_includes/components/sections/cv-projects-personal.njk:65,92` — date text
- `_includes/components/cv-builder.njk:163``<time>` add `font-mono text-sm`
- `cv.njk:130``<time>` add `font-mono text-sm`
Group D — Widgets:
- `_includes/components/widgets/social-activity.njk:46,76` — 2 `<time>` elements
- `_includes/components/widgets/recent-posts.njk:25,40,55,70,81` — 5 `<time>` elements
- `_includes/components/widgets/recent-posts-blog.njk:23,36,49,62,72` — 5 `<time>` elements
- `github.njk:114,150` — 2 `<time>` elements
**Pattern for each fix:**
For `<time class="dt-published">`:
```html
<!-- Before -->
<time class="dt-published">
<!-- After -->
<time class="dt-published font-mono text-sm">
```
For `<time class="dt-published text-sm ... font-medium">` (already has text-sm):
```html
<!-- Before -->
<time class="dt-published text-sm text-surface-500 dark:text-surface-400 font-medium">
<!-- After -->
<time class="dt-published font-mono text-sm text-surface-500 dark:text-surface-400 font-medium">
```
For CV date text in `<span>` or `<p>`:
```html
<!-- Before -->
<span class="text-xs text-surface-500">Jan 2020 - Present</span>
<!-- After -->
<span class="text-xs text-surface-500 font-mono">Jan 2020 - Present</span>
```
**Step 1: Fix Group A** (collection pages)
**Step 2: Fix Group B** (layouts)
**Step 3: Fix Group C** (sections/components)
**Step 4: Fix Group D** (widgets)
**Step 5: Commit**
```bash
git add *.njk _includes/
git commit -m "fix(dates): add font-mono text-sm to all <time> elements
System convention: every rendered date gets font-mono class.
CSS base layer handles font-family, but classes ensure consistency
and proper text-sm sizing across all templates.
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596"
```
---
### Task 6: Hover Violations (8 files)
**Files to modify:**
- `github.njk:226``hover:border-surface-400 dark:hover:border-surface-600` -> `hover:border-emerald-400 dark:hover:border-emerald-600`
- `starred.njk:187``hover:border-surface-400 dark:hover:border-surface-500` -> `hover:border-emerald-400 dark:hover:border-emerald-600`
- `_includes/components/sections/featured-posts.njk:45``hover:border-surface-400 dark:hover:border-surface-500` -> `hover:border-accent-400 dark:hover:border-accent-600`
- `_includes/components/widgets/toc.njk:10` — add `hover:underline` to ToC links
- `_includes/components/widgets/subscribe.njk:6,12` — add `hover:underline` to RSS/JSON feed links
- `_includes/components/widgets/categories.njk:8` — add `hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors` to category links
- `_includes/components/widgets/blogroll.njk:30` — add `hover:underline` to blog list links
- `news.njk:128,193``hover:border-accent-400` -> `hover:border-amber-400` (Writing domain)
- `digest-index.njk:25``hover:border-accent-300` -> `hover:border-amber-400 dark:hover:border-amber-600`
**Step 1: Fix all files above**
**Step 2: Commit**
```bash
git add github.njk starred.njk news.njk digest-index.njk _includes/
git commit -m "fix(hover): correct card hover borders to domain colors
Replace hover:border-surface-400 with domain-colored borders.
Add hover:underline to text links missing it.
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596"
```
---
### Task 7: Border Radius Violations (10 files)
**Files to modify:**
- `_includes/layouts/page.njk:92,97,102` — AI level badges: `rounded` -> `rounded-full`
- `_includes/layouts/page.njk:33` — AI stats container: `rounded-xl` -> `rounded-lg`
- `_includes/layouts/home.njk:96` — post type badge: `rounded` -> `rounded-full`
- `blogroll.njk:68,112` — standard cards: `rounded-xl` -> `rounded-lg`
- `readlater.njk:83` — standard cards: `rounded-xl` -> `rounded-lg`
- `news.njk:193,246` — standard cards: `rounded-xl` -> `rounded-lg`
- `podroll.njk:66` — standard cards: `rounded-xl` -> `rounded-lg`
- `_includes/components/sections/cv-projects.njk:94` — tech badges: `rounded` -> `rounded-full`
- `_includes/components/sections/cv-projects-work.njk:104` — same
- `_includes/components/sections/cv-projects-personal.njk:104` — same
- `_includes/components/sections/ai-usage.njk:13` — stats panel: `rounded-xl` -> `rounded-lg`
- `_includes/components/sections/funkwhale-stats-content.njk:4` — stat cards: `rounded-xl` -> `rounded-lg`
- `_includes/components/h-card.njk:90` — category span: `rounded` -> `rounded-full`
- `_includes/components/sections/recent-posts.njk:214,229` — post type badges: `rounded` -> `rounded-full`
- `_includes/components/sections/featured-posts.njk:217,232` — same
- `funkwhale.njk:147` — trends chart container: `rounded-xl` -> `rounded-lg`
**Step 1: Fix all files above**
**Step 2: Commit**
```bash
git add *.njk _includes/
git commit -m "fix(radius): correct border-radius to match system
- rounded -> rounded-full for badges/pills
- rounded-xl -> rounded-lg for standard cards (xl reserved for hero/featured)
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596"
```
---
### Task 8: Dark Mode Violations (8 files)
**Files to modify:**
- `_includes/layouts/page.njk:36,40,44,48``bg-white` -> `bg-surface-50` (with existing `dark:bg-surface-800`)
- `_includes/components/sections/ai-usage.njk:16,20,24,28``bg-white` -> `bg-surface-50`
- `_includes/components/sections/ai-usage.njk:13``dark:bg-surface-800/50` -> `dark:bg-surface-800` (remove opacity)
- `_includes/components/comments.njk:41` — add `dark:text-surface-100`, fix `dark:border-surface-600` -> `dark:border-surface-700`
- `_includes/components/comments.njk:59` — same fix for textarea
- `_includes/components/fediverse-modal.njk:32``dark:bg-surface-700` -> `dark:bg-surface-800`
- `_includes/components/widgets/post-categories.njk:8,13``bg-accent-900` -> `bg-accent-900/30` (badge pattern)
- `changelog.njk:51` — add `bg-surface-50 dark:bg-surface-800`
**Step 1: Fix all files above**
**Step 2: Commit**
```bash
git add *.njk _includes/
git commit -m "fix(dark-mode): correct dark mode token pairs
- bg-white -> bg-surface-50 (token compliance)
- Add missing dark:text-surface-100 on inputs
- Fix dark:border-surface-600 -> dark:border-surface-700
- Fix badge bg opacity (dark:bg-accent-900/30)
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596"
```
---
### Task 9: Depth Violations (6 files)
**Files to modify:**
Avatar/album art images need `shadow-lg`:
- `_includes/components/h-card.njk:32` — avatar img: add `shadow-lg`
- `_includes/components/widgets/author-card-compact.njk:12` — avatar: add `shadow-lg`
- `_includes/components/widgets/funkwhale.njk:33` — now-playing cover: add `shadow-lg`
- `_includes/components/widgets/funkwhale.njk:55` — track thumbnail: add `shadow-lg`
- `_includes/components/widgets/funkwhale.njk:83` — scrobble thumbnail: add `shadow-lg`
- `_includes/components/widgets/recent-comments.njk:11` — commenter avatar: add `shadow-lg`
- `_includes/components/sections/funkwhale-stats-content.njk:47` — album cover: add `shadow-lg`
Stat number `font-mono`:
- `_includes/components/sections/funkwhale-stats-content.njk:5` — stat numbers: add `font-mono`
- `_includes/components/sections/ai-usage.njk:17` — stat numbers: add `font-mono`
**Step 1: Fix all files above**
**Step 2: Commit**
```bash
git add _includes/
git commit -m "fix(depth): add shadow-lg to avatars/album art, font-mono to stat numbers
System: avatars/album art get shadow-lg for presence.
Stat numbers get font-mono like dates/timestamps.
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596"
```
---
### Task 10: Transition Violations (6 files)
**Files to modify:**
- `_includes/components/widgets/author-card-compact.njk:16` — author name link: add `transition-colors`
- `_includes/components/widgets/categories.njk:8` — category links: add `transition-colors`
- `_includes/components/widgets/post-navigation.njk:18,44` — nav links: add `transition-colors`
- `_includes/components/widgets/webmentions.njk:55,69,83,94,105,114` — links: add `transition-colors`
- `podroll.njk:119-128` — "View Episode" link: add `transition-colors`
- `podroll.njk:131-137` — "Subscribe to feed" link: add `transition-colors`
**Step 1: Fix all files above**
**Step 2: Commit**
```bash
git add _includes/ podroll.njk
git commit -m "fix(transitions): add transition-colors to interactive elements
System requires transition-colors on all elements with hover states.
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596"
```
---
### Task 11: Inline `focus:ring-2` Cleanup (7 files)
Some templates use `focus:outline-none focus:ring-2 focus:ring-accent-500` which **suppresses** the CSS base layer focus-visible styles and replaces them with a focus (not focus-visible) ring. This means mouse clicks also trigger the ring. The system uses `focus-visible` in CSS. These inline `focus:outline-none` should be removed so the base CSS handles focus consistently.
**Files to modify:**
- `news.njk:15,48,59,70,332,342` — remove `focus:outline-none focus:ring-2 focus:ring-accent-500` (CSS base layer handles it)
- `podroll.njk:19,50,175` — remove inline focus classes
- `starred.njk:111,126,141,165` — remove inline focus classes (but domain color is handled in Task 3)
- `readlater.njk:44,61` — remove inline focus classes
- `_includes/components/webmentions.njk` line with `focus:ring-2` — remove
- `_includes/components/fediverse-modal.njk:63` — remove `focus:ring-2 focus:ring-[#a730b8]`
- `_includes/components/widgets/search.njk:4` — remove inline focus (after Task 3 fixes primary -> accent)
**Step 1: In each file, remove `focus:outline-none focus:ring-2 focus:ring-*-500` class groups**
The CSS base layer at `tailwind.css:117-142` provides `ring-2 ring-amber-500/70` on `:focus-visible` for all buttons, links, and inputs. This is the correct, centralized approach.
**Step 2: Commit**
```bash
git add *.njk _includes/
git commit -m "fix(focus): remove inline focus:ring-2 classes, rely on CSS base layer
The CSS base layer provides focus-visible rings on all interactive
elements. Inline focus:outline-none suppresses this and replaces
it with focus (not focus-visible) behavior. Removing these lets
the centralized system handle focus states consistently.
Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596"
```
---
### Task 12: Remaining Button/Focus Violations
After Task 11 removes conflicting inline focus classes, the CSS base layer handles focus-visible for all `button`, `a`, `input`, `textarea`, `select` elements. The remaining "missing focus:ring-2" violations from the audit are now **resolved by the CSS base layer** — no further template changes needed.
**Verification step:** After all previous tasks are committed, do a grep to confirm no `focus:outline-none` remains that would suppress the base styles:
```bash
grep -rn "focus:outline-none" --include="*.njk" /home/rick/code/indiekit-dev/indiekit-eleventy-theme/ | grep -v node_modules
```
If any remain, remove them.
**Commit:** No commit needed if grep finds nothing.
---
## Deployment
After all tasks are complete:
```bash
cd /home/rick/code/indiekit-dev/indiekit-eleventy-theme
git push origin main
cd /home/rick/code/indiekit-dev/indiekit-cloudron
git submodule update --remote eleventy-site
git add eleventy-site
git commit -m "chore: update eleventy-site submodule (design system compliance)"
git push origin main
make prepare
cloudron build --no-cache && cloudron update --app rmendes.net --no-backup
```
## Visual Verification
After deployment, verify key pages with `playwright-cli`:
- `/` (home) — cards have shadows, dates are mono
- `/blog/` — social types use rose domain color
- `/github/` — emerald domain color throughout
- `/github/starred/` — emerald, not accent
- `/listening/` — cards have shadows
- `/podroll/` — rounded-lg, not rounded-xl
- Dark mode toggle — check all pages render correctly

View File

@@ -7,6 +7,7 @@ import markdownIt from "markdown-it";
import markdownItAnchor from "markdown-it-anchor";
import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
import { minify } from "html-minifier-terser";
import { minify as minifyJS } from "terser";
import registerUnfurlShortcode, { getCachedCard, prefetchUrl } from "./lib/unfurl-shortcode.js";
import matter from "gray-matter";
import { createHash, createHmac } from "crypto";
@@ -245,11 +246,18 @@ export default function (eleventyConfig) {
});
// Embed Everything - auto-embed YouTube, Vimeo, Bluesky, Mastodon, etc.
// YouTube uses lite-yt-embed facade: shows thumbnail + play button,
// only loads full iframe on click (~800 KiB savings).
// CSS/JS disabled here — already loaded in base.njk.
eleventyConfig.addPlugin(embedEverything, {
use: ["youtube", "vimeo", "twitter", "mastodon", "bluesky", "spotify", "soundcloud"],
youtube: {
options: {
lite: false,
lite: {
css: { enabled: false },
js: { enabled: false },
responsive: true,
},
recommendSelfOnly: true,
},
},
@@ -269,7 +277,8 @@ export default function (eleventyConfig) {
// Usage: {{ url | unfurlCard | safe }}
eleventyConfig.addFilter("unfurlCard", getCachedCard);
// Custom transform to convert YouTube links to embeds
// Custom transform to convert YouTube links to lite-youtube embeds
// Catches bare YouTube links in Markdown that the embed plugin misses
eleventyConfig.addTransform("youtube-link-to-embed", function (content, outputPath) {
if (!outputPath || !outputPath.endsWith(".html")) {
return content;
@@ -279,8 +288,8 @@ export default function (eleventyConfig) {
const youtubePattern = /<a[^>]+href="https?:\/\/(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]+)[^"]*"[^>]*>(?:https?:\/\/)?(?:www\.)?[^<]*(?:youtube|youtu\.be)[^<]*<\/a>/gi;
content = content.replace(youtubePattern, (match, videoId) => {
// Use standard YouTube iframe with exact oEmbed parameters
return `</p><div class="video-embed"><iframe width="560" height="315" src="https://www.youtube.com/embed/${videoId}?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen title="YouTube video"></iframe></div><p>`;
// Use lite-youtube facade — loads full iframe only on click
return `</p><div class="video-embed eleventy-plugin-youtube-embed"><lite-youtube videoid="${videoId}" style="background-image: url('https://i.ytimg.com/vi/${videoId}/hqdefault.jpg');"><div class="lty-playbtn"></div></lite-youtube></div><p>`;
});
// Clean up empty <p></p> tags created by the replacement
@@ -316,7 +325,7 @@ export default function (eleventyConfig) {
cacheOptions: {
duration: process.env.ELEVENTY_RUN_MODE === "build" ? "1d" : "30d",
},
concurrency: 4,
concurrency: 1,
defaultAttributes: {
loading: "lazy",
decoding: "async",
@@ -389,6 +398,52 @@ export default function (eleventyConfig) {
return content;
});
// Auto-unfurl standalone external links in note content
// Finds <a> tags that are the primary content of a <p> tag and injects OG preview cards
eleventyConfig.addTransform("auto-unfurl-notes", async function (content, outputPath) {
if (!outputPath || !outputPath.endsWith(".html")) return content;
// Only process note pages (individual + listing)
if (!outputPath.includes("/notes/")) return content;
// Match <p> tags whose content is short text + a single external <a> as the last element
// Pattern: <p>optional short text <a href="https://external.example">...</a></p>
const linkParagraphRe = /<p>([^<]{0,80})?<a\s+href="(https?:\/\/[^"]+)"[^>]*>[^<]*<\/a>\s*<\/p>/g;
const siteHost = new URL(siteUrl).hostname;
const matches = [];
let match;
while ((match = linkParagraphRe.exec(content)) !== null) {
const url = match[2];
try {
const linkHost = new URL(url).hostname;
// Skip same-domain links and common non-content URLs
if (linkHost === siteHost || linkHost.endsWith("." + siteHost)) continue;
matches.push({ fullMatch: match[0], url, index: match.index });
} catch {
continue;
}
}
if (matches.length === 0) return content;
// Unfurl all matched URLs in parallel (uses cache, throttles network)
const cards = await Promise.all(matches.map(m => prefetchUrl(m.url)));
// Replace in reverse order to preserve indices
let result = content;
for (let i = matches.length - 1; i >= 0; i--) {
const m = matches[i];
const card = cards[i];
// Skip if unfurl returned just a fallback link (no OG data)
if (!card || !card.includes("unfurl-card")) continue;
// Insert the unfurl card after the paragraph
const insertPos = m.index + m.fullMatch.length;
result = result.slice(0, insertPos) + "\n" + card + "\n" + result.slice(insertPos);
}
return result;
});
// HTML minification — only during initial build, skip during watch rebuilds
eleventyConfig.addTransform("htmlmin", async function (content, outputPath) {
if (outputPath && outputPath.endsWith(".html") && process.env.ELEVENTY_RUN_MODE === "build") {
@@ -783,7 +838,7 @@ export default function (eleventyConfig) {
// Closed polygon for gradient fill (line path + bottom corners)
const fillPoints = `${points} ${w},${h} 0,${h}`;
return [
`<svg viewBox="0 0 ${w} ${h}" class="sparkline" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Posting frequency over the last 12 months">`,
`<svg viewBox="0 0 ${w} ${h}" width="100%" height="100%" preserveAspectRatio="none" class="sparkline" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Posting frequency over the last 12 months">`,
`<defs><linearGradient id="spk-fill" x1="0" y1="0" x2="0" y2="1">`,
`<stop offset="0%" stop-color="currentColor" stop-opacity="0.25"/>`,
`<stop offset="100%" stop-color="currentColor" stop-opacity="0.02"/>`,
@@ -1216,6 +1271,36 @@ export default function (eleventyConfig) {
}
// JS minification — minify source JS files in output (skip vendor, already-minified)
if (runMode === "build" && !incremental) {
const jsOutputDir = directories?.output || dir.output;
const jsDir = resolve(jsOutputDir, "js");
if (existsSync(jsDir)) {
let jsMinified = 0;
let jsSaved = 0;
for (const file of readdirSync(jsDir).filter(f => f.endsWith(".js") && !f.endsWith(".min.js"))) {
const filePath = resolve(jsDir, file);
try {
const src = readFileSync(filePath, "utf-8");
const result = await minifyJS(src, { compress: true, mangle: true });
if (result.code) {
const saved = src.length - result.code.length;
if (saved > 0) {
writeFileSync(filePath, result.code);
jsSaved += saved;
jsMinified++;
}
}
} catch (err) {
console.error(`[js-minify] Failed to minify ${file}:`, err.message);
}
}
if (jsMinified > 0) {
console.log(`[js-minify] Minified ${jsMinified} JS files, saved ${(jsSaved / 1024).toFixed(1)} KiB`);
}
}
}
// Syndication webhook — trigger after incremental rebuilds (new posts are now live)
// Cuts syndication latency from ~2 min (poller) to ~5 sec (immediate trigger)
if (incremental) {

View File

@@ -35,13 +35,13 @@ permalink: "featured/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNu
{# Determine border color by post type #}
{% if likedUrl %}
{% set borderClass = "border-l-red-400 dark:border-l-red-500" %}
{% set borderClass = "border-l-rose-400 dark:border-l-rose-500" %}
{% elif bookmarkedUrl %}
{% set borderClass = "border-l-amber-400 dark:border-l-amber-500" %}
{% elif repostedUrl %}
{% set borderClass = "border-l-green-400 dark:border-l-green-500" %}
{% set borderClass = "border-l-rose-400 dark:border-l-rose-500" %}
{% elif replyToUrl %}
{% set borderClass = "border-l-sky-400 dark:border-l-sky-500" %}
{% set borderClass = "border-l-rose-400 dark:border-l-rose-500" %}
{% elif hasPhotos %}
{% set borderClass = "border-l-purple-400 dark:border-l-purple-500" %}
{% else %}
@@ -53,9 +53,9 @@ permalink: "featured/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNu
{% if likedUrl %}
{# ── Like ── #}
<div class="post-header">
<span class="text-xs font-medium text-red-600 dark:text-red-400">Liked</span>
<span class="text-xs font-medium text-rose-600 dark:text-rose-400">Liked</span>
<a class="u-url ml-2" href="{{ post.url }}">
<time class="dt-published text-sm text-surface-500 dark:text-surface-400" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
<time class="dt-published text-sm text-surface-600 dark:text-surface-400 font-mono" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
</a>
</div>
<a class="u-like-of text-sm text-surface-600 dark:text-surface-400 hover:underline break-all mt-2 inline-block" href="{{ likedUrl }}">{{ likedUrl }}</a>
@@ -67,7 +67,7 @@ permalink: "featured/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNu
{# ── Bookmark ── #}
<div class="post-header">
<span class="text-xs font-medium text-amber-600 dark:text-amber-400">Bookmarked</span>
<time class="dt-published text-sm text-surface-500 dark:text-surface-400 ml-2" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
<time class="dt-published text-sm text-surface-600 dark:text-surface-400 ml-2 font-mono" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
</div>
{% if post.data.title %}
<h2 class="p-name text-lg font-semibold mt-2">
@@ -82,9 +82,9 @@ permalink: "featured/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNu
{% elif repostedUrl %}
{# ── Repost ── #}
<div class="post-header">
<span class="text-xs font-medium text-green-600 dark:text-green-400">Reposted</span>
<span class="text-xs font-medium text-rose-600 dark:text-rose-400">Reposted</span>
<a class="u-url ml-2" href="{{ post.url }}">
<time class="dt-published text-sm text-surface-500 dark:text-surface-400" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
<time class="dt-published text-sm text-surface-600 dark:text-surface-400 font-mono" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
</a>
</div>
<a class="u-repost-of text-sm text-surface-600 dark:text-surface-400 hover:underline break-all mt-2 inline-block" href="{{ repostedUrl }}">{{ repostedUrl }}</a>
@@ -95,9 +95,9 @@ permalink: "featured/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNu
{% elif replyToUrl %}
{# ── Reply ── #}
<div class="post-header">
<span class="text-xs font-medium text-sky-600 dark:text-sky-400">In reply to</span>
<span class="text-xs font-medium text-rose-600 dark:text-rose-400">In reply to</span>
<a class="u-url ml-2" href="{{ post.url }}">
<time class="dt-published text-sm text-surface-500 dark:text-surface-400" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
<time class="dt-published text-sm text-surface-600 dark:text-surface-400 font-mono" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
</a>
</div>
<a class="u-in-reply-to text-sm text-surface-600 dark:text-surface-400 hover:underline break-all mt-2 inline-block" href="{{ replyToUrl }}">{{ replyToUrl }}</a>
@@ -113,7 +113,7 @@ permalink: "featured/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNu
</h2>
</div>
<div class="post-meta mt-2">
<time class="dt-published" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
<time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
{% if post.data.postType %}
<span class="px-2 py-0.5 bg-surface-100 dark:bg-surface-700 rounded text-xs ml-2">{{ post.data.postType }}</span>
{% endif %}
@@ -127,7 +127,7 @@ permalink: "featured/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNu
{# ── Note ── #}
<div class="post-header">
<a class="u-url" href="{{ post.url }}">
<time class="dt-published text-sm text-surface-500 dark:text-surface-400 font-medium" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
<time class="dt-published text-sm text-surface-600 dark:text-surface-400 font-medium font-mono" datetime="{{ post.date | isoDate }}">{{ post.date | dateDisplay }}</time>
</a>
{% if post.data.postType %}
<span class="px-2 py-0.5 bg-surface-100 dark:bg-surface-700 rounded text-xs ml-2">{{ post.data.postType }}</span>
@@ -137,7 +137,7 @@ permalink: "featured/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNu
<div class="e-content prose dark:prose-invert prose-sm mt-3 max-w-none">{{ post.templateContent | safe }}</div>
{% endif %}
<div class="post-footer mt-3">
<a href="{{ post.url }}" class="text-sm text-accent-600 dark:text-accent-400 hover:underline">Permalink</a>
<a href="{{ post.url }}" class="text-sm text-accent-600 dark:text-accent-400 hover:underline" aria-label="Permalink: {{ post.data.title or ('Post from ' + (post.date | dateDisplay)) }}">Permalink</a>
</div>
{% endif %}
@@ -158,7 +158,7 @@ permalink: "featured/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNu
Previous
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
Previous
</span>
@@ -170,7 +170,7 @@ permalink: "featured/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNu
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
Next
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</span>

View File

@@ -66,9 +66,9 @@ withSidebar: true
</h2>
<p class="text-surface-600 dark:text-surface-400">{{ funkwhaleActivity.nowPlaying.artist }}</p>
{% if funkwhaleActivity.nowPlaying.album %}
<p class="text-sm text-surface-500 mt-1">{{ funkwhaleActivity.nowPlaying.album }}</p>
<p class="text-sm text-surface-600 dark:text-surface-400 mt-1">{{ funkwhaleActivity.nowPlaying.album }}</p>
{% endif %}
<p class="text-xs text-surface-500 mt-2">{{ funkwhaleActivity.nowPlaying.relativeTime }}</p>
<p class="text-xs text-surface-600 dark:text-surface-400 mt-2">{{ funkwhaleActivity.nowPlaying.relativeTime }}</p>
</div>
</div>
</div>
@@ -86,31 +86,39 @@ withSidebar: true
</h2>
{# Tab buttons #}
<div class="flex gap-1 mb-6 border-b border-surface-200 dark:border-surface-700 overflow-x-auto">
<div class="flex gap-1 mb-6 border-b border-surface-200 dark:border-surface-700 overflow-x-auto" role="tablist" aria-label="Listening statistics period">
<button
@click="activeTab = 'all'"
:class="activeTab === 'all' ? 'border-b-2 border-purple-500 text-purple-600 dark:text-purple-400' : 'text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
:class="activeTab === 'all' ? 'border-b-2 border-purple-500 text-purple-600 dark:text-purple-400' : 'text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
:aria-selected="(activeTab === 'all').toString()"
role="tab" id="fw-tab-all" aria-controls="fw-panel-all"
class="px-4 py-2 text-sm font-medium transition-colors -mb-px whitespace-nowrap"
>
All Time
</button>
<button
@click="activeTab = 'month'"
:class="activeTab === 'month' ? 'border-b-2 border-purple-500 text-purple-600 dark:text-purple-400' : 'text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
:class="activeTab === 'month' ? 'border-b-2 border-purple-500 text-purple-600 dark:text-purple-400' : 'text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
:aria-selected="(activeTab === 'month').toString()"
role="tab" id="fw-tab-month" aria-controls="fw-panel-month"
class="px-4 py-2 text-sm font-medium transition-colors -mb-px whitespace-nowrap"
>
This Month
</button>
<button
@click="activeTab = 'week'"
:class="activeTab === 'week' ? 'border-b-2 border-purple-500 text-purple-600 dark:text-purple-400' : 'text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
:class="activeTab === 'week' ? 'border-b-2 border-purple-500 text-purple-600 dark:text-purple-400' : 'text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
:aria-selected="(activeTab === 'week').toString()"
role="tab" id="fw-tab-week" aria-controls="fw-panel-week"
class="px-4 py-2 text-sm font-medium transition-colors -mb-px whitespace-nowrap"
>
This Week
</button>
<button
@click="activeTab = 'trends'"
:class="activeTab === 'trends' ? 'border-b-2 border-purple-500 text-purple-600 dark:text-purple-400' : 'text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
:class="activeTab === 'trends' ? 'border-b-2 border-purple-500 text-purple-600 dark:text-purple-400' : 'text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
:aria-selected="(activeTab === 'trends').toString()"
role="tab" id="fw-tab-trends" aria-controls="fw-panel-trends"
class="px-4 py-2 text-sm font-medium transition-colors -mb-px whitespace-nowrap"
>
Trends
@@ -118,7 +126,7 @@ withSidebar: true
</div>
{# All Time Tab #}
<div x-show="activeTab === 'all'" x-cloak>
<div x-show="activeTab === 'all'" x-cloak role="tabpanel" id="fw-panel-all" aria-labelledby="fw-tab-all">
{% set summary = funkwhaleActivity.stats.summary.all %}
{% set topArtists = funkwhaleActivity.stats.topArtists.all %}
{% set topAlbums = funkwhaleActivity.stats.topAlbums.all %}
@@ -126,7 +134,7 @@ withSidebar: true
</div>
{# This Month Tab #}
<div x-show="activeTab === 'month'" x-cloak>
<div x-show="activeTab === 'month'" x-cloak role="tabpanel" id="fw-panel-month" aria-labelledby="fw-tab-month">
{% set summary = funkwhaleActivity.stats.summary.month %}
{% set topArtists = funkwhaleActivity.stats.topArtists.month %}
{% set topAlbums = funkwhaleActivity.stats.topAlbums.month %}
@@ -134,7 +142,7 @@ withSidebar: true
</div>
{# This Week Tab #}
<div x-show="activeTab === 'week'" x-cloak>
<div x-show="activeTab === 'week'" x-cloak role="tabpanel" id="fw-panel-week" aria-labelledby="fw-tab-week">
{% set summary = funkwhaleActivity.stats.summary.week %}
{% set topArtists = funkwhaleActivity.stats.topArtists.week %}
{% set topAlbums = funkwhaleActivity.stats.topAlbums.week %}
@@ -142,9 +150,9 @@ withSidebar: true
</div>
{# Trends Tab #}
<div x-show="activeTab === 'trends'" x-cloak>
<div x-show="activeTab === 'trends'" x-cloak role="tabpanel" id="fw-panel-trends" aria-labelledby="fw-tab-trends">
{% if funkwhaleActivity.stats.trends and funkwhaleActivity.stats.trends.length %}
<div class="bg-surface-50 dark:bg-surface-800 rounded-xl p-6 border border-surface-200 dark:border-surface-700">
<div class="bg-surface-50 dark:bg-surface-800 rounded-lg p-6 border border-surface-200 dark:border-surface-700 shadow-sm">
<h3 class="text-lg font-semibold text-surface-900 dark:text-surface-100 mb-4">Daily Listening (Last 30 Days)</h3>
<div class="flex items-end gap-1 h-32">
{% set maxCount = 1 %}
@@ -161,7 +169,7 @@ withSidebar: true
></div>
{% endfor %}
</div>
<div class="flex justify-between text-xs text-surface-500 mt-2">
<div class="flex justify-between text-xs text-surface-600 dark:text-surface-400 mt-2">
<span>{{ funkwhaleActivity.stats.trends[0].date }}</span>
<span>{{ funkwhaleActivity.stats.trends[funkwhaleActivity.stats.trends.length - 1].date }}</span>
</div>
@@ -185,7 +193,7 @@ withSidebar: true
{% if funkwhaleActivity.listenings.length %}
<div class="space-y-3">
{% for listening in funkwhaleActivity.listenings | head(15) %}
<div class="flex items-center gap-4 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-purple-400 dark:hover:border-purple-600 transition-colors">
<div class="flex items-center gap-4 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-purple-400 dark:hover:border-purple-600 transition-colors shadow-sm">
{% if listening.coverUrl %}
<img src="{{ listening.coverUrl }}" alt="" class="w-12 h-12 rounded object-cover flex-shrink-0" loading="lazy" eleventy:ignore>
{% else %}
@@ -210,9 +218,9 @@ withSidebar: true
</div>
<div class="text-right flex-shrink-0">
<span class="text-xs text-surface-500">{{ listening.relativeTime }}</span>
<span class="text-xs text-surface-600 dark:text-surface-400">{{ listening.relativeTime }}</span>
{% if listening.duration %}
<span class="text-xs text-surface-400 block">{{ listening.duration }}</span>
<span class="text-xs text-surface-600 dark:text-surface-400 block">{{ listening.duration }}</span>
{% endif %}
</div>
</div>
@@ -235,7 +243,7 @@ withSidebar: true
<div class="grid gap-3 sm:gap-4 md:grid-cols-2">
{% for favorite in funkwhaleActivity.favorites | head(10) %}
<div class="flex items-center gap-3 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700">
<div class="flex items-center gap-3 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
{% if favorite.coverUrl %}
<img src="{{ favorite.coverUrl }}" alt="" class="w-14 h-14 rounded object-cover flex-shrink-0" loading="lazy" eleventy:ignore>
{% else %}
@@ -258,7 +266,7 @@ withSidebar: true
</h3>
<p class="text-sm text-surface-600 dark:text-surface-400 truncate">{{ favorite.artist }}</p>
{% if favorite.album %}
<p class="text-xs text-surface-500 truncate">{{ favorite.album }}</p>
<p class="text-xs text-surface-600 dark:text-surface-400 truncate">{{ favorite.album }}</p>
{% endif %}
</div>
</div>

View File

@@ -43,7 +43,7 @@ withSidebar: true
<p class="text-sm text-surface-600 dark:text-surface-400 mb-4">{{ repo.description }}</p>
{% endif %}
<div class="flex flex-wrap items-center gap-3 text-sm text-surface-500 mb-4">
<div class="flex flex-wrap items-center gap-3 text-sm text-surface-600 dark:text-surface-400 mb-4">
{% if repo.language %}
<span class="flex items-center gap-1">
<span class="w-3 h-3 rounded-full bg-surface-500"></span>
@@ -101,7 +101,7 @@ withSidebar: true
{% if githubActivity.commits.length %}
<div class="space-y-3">
{% for commit in githubActivity.commits %}
<div class="flex items-start gap-3 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700">
<div class="flex items-start gap-3 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
<code class="text-xs font-mono bg-surface-100 dark:bg-surface-700 px-2 py-1 rounded">
{{ commit.sha }}
</code>
@@ -109,9 +109,9 @@ withSidebar: true
<a href="{{ commit.url }}" class="text-surface-900 dark:text-surface-100 hover:text-emerald-600 dark:hover:text-emerald-400" target="_blank" rel="noopener">
{{ commit.message }}
</a>
<p class="text-xs text-surface-500 mt-1">
<p class="text-xs text-surface-600 dark:text-surface-400 mt-1">
<a href="{{ commit.repoUrl }}" class="hover:underline" target="_blank" rel="noopener">{{ commit.repo }}</a>
· <time datetime="{{ commit.date }}">{{ commit.date | date("MMM d, yyyy") }}</time>
· <time class="font-mono" datetime="{{ commit.date }}">{{ commit.date | date("MMM d, yyyy") }}</time>
</p>
</div>
</div>
@@ -134,7 +134,7 @@ withSidebar: true
<div class="space-y-3">
{% for item in githubActivity.contributions %}
<div class="flex items-start gap-3 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700">
<div class="flex items-start gap-3 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
{% if item.type == "pr" %}
<span class="flex-shrink-0 px-2 py-1 text-xs font-medium bg-emerald-100 dark:bg-emerald-900 text-emerald-800 dark:text-emerald-200 rounded">PR</span>
{% else %}
@@ -144,10 +144,10 @@ withSidebar: true
<a href="{{ item.url }}" class="text-surface-900 dark:text-surface-100 hover:text-emerald-600 dark:hover:text-emerald-400" target="_blank" rel="noopener">
{{ item.title }}
</a>
<p class="text-xs text-surface-500 mt-1">
<p class="text-xs text-surface-600 dark:text-surface-400 mt-1">
<a href="{{ item.repoUrl }}" class="hover:underline" target="_blank" rel="noopener">{{ item.repo }}</a>
#{{ item.number }}
· <time datetime="{{ item.date }}">{{ item.date | date("MMM d, yyyy") }}</time>
· <time class="font-mono" datetime="{{ item.date }}">{{ item.date | date("MMM d, yyyy") }}</time>
</p>
</div>
</div>
@@ -168,7 +168,7 @@ withSidebar: true
{% if githubRepos.length %}
<div class="grid gap-3 sm:gap-4 md:grid-cols-2">
{% for repo in githubRepos | head(6) %}
<article class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700">
<article class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
<h3 class="font-semibold text-surface-900 dark:text-surface-100 mb-1">
<a href="{{ repo.html_url }}" class="hover:text-emerald-600 dark:hover:text-emerald-400" target="_blank" rel="noopener">
{{ repo.name }}
@@ -179,7 +179,7 @@ withSidebar: true
<p class="text-sm text-surface-600 dark:text-surface-400 mb-3">{{ repo.description | truncate(100) }}</p>
{% endif %}
<div class="flex flex-wrap items-center gap-3 text-sm text-surface-500">
<div class="flex flex-wrap items-center gap-3 text-sm text-surface-600 dark:text-surface-400">
{% if repo.language %}
<span class="flex items-center gap-1">
<span class="w-3 h-3 rounded-full bg-surface-500"></span>
@@ -223,7 +223,7 @@ withSidebar: true
{% if githubActivity.stars.length %}
<div class="grid gap-3 sm:gap-4 md:grid-cols-2">
{% for repo in githubActivity.stars | head(10) %}
<article class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-surface-400 dark:hover:border-surface-600 transition-colors">
<article class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-emerald-400 dark:hover:border-emerald-600 transition-colors shadow-sm">
<h3 class="font-semibold text-surface-900 dark:text-surface-100 mb-1">
<a href="{{ repo.url }}" class="hover:text-emerald-600 dark:hover:text-emerald-400" target="_blank" rel="noopener">
{{ repo.name }}
@@ -242,7 +242,7 @@ withSidebar: true
{% endfor %}
</div>
<div class="flex flex-wrap items-center gap-3 text-sm text-surface-500">
<div class="flex flex-wrap items-center gap-3 text-sm text-surface-600 dark:text-surface-400">
{% if repo.language %}
<span class="flex items-center gap-1">
<span class="w-3 h-3 rounded-full bg-surface-500"></span>

View File

@@ -14,5 +14,5 @@ withSidebar: true
{% if collections.posts and collections.posts.length %}
{% postGraph collections.posts, { limit: 0 } %}
{% else %}
<p class="text-surface-500 dark:text-surface-400">No posts found.</p>
<p class="text-surface-600 dark:text-surface-400">No posts found.</p>
{% endif %}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -17,31 +17,35 @@ permalink: /interactions/
{# Tab navigation for Outbound/Inbound #}
<div x-data="interactionsApp()" x-init="init()">
{# Tab buttons #}
<div class="flex border-b border-surface-200 dark:border-surface-700 mb-6">
<div class="flex border-b border-surface-200 dark:border-surface-700 mb-6" role="tablist" aria-label="Interaction direction">
<button
@click="activeTab = 'outbound'"
:class="activeTab === 'outbound' ? 'border-rose-500 text-rose-600 dark:text-rose-400' : 'border-transparent text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
:class="activeTab === 'outbound' ? 'border-rose-500 text-rose-600 dark:text-rose-400' : 'border-transparent text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
:aria-selected="(activeTab === 'outbound').toString()"
role="tab" id="interactions-tab-outbound" aria-controls="interactions-panel-outbound"
class="px-4 py-3 text-sm font-medium border-b-2 -mb-px transition-colors">
My Activity
<span class="ml-1 text-xs text-surface-400">(outbound)</span>
<span class="ml-1 text-xs text-surface-600 dark:text-surface-400">(outbound)</span>
</button>
<button
@click="activeTab = 'inbound'"
:class="activeTab === 'inbound' ? 'border-rose-500 text-rose-600 dark:text-rose-400' : 'border-transparent text-surface-500 hover:text-surface-700 dark:hover:text-surface-300'"
:class="activeTab === 'inbound' ? 'border-rose-500 text-rose-600 dark:text-rose-400' : 'border-transparent text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300'"
:aria-selected="(activeTab === 'inbound').toString()"
role="tab" id="interactions-tab-inbound" aria-controls="interactions-panel-inbound"
class="px-4 py-3 text-sm font-medium border-b-2 -mb-px transition-colors">
Received
<span class="ml-1 text-xs text-surface-400">(inbound)</span>
<span class="ml-1 text-xs text-surface-600 dark:text-surface-400">(inbound)</span>
<span x-show="totalInbound > 0" x-text="totalInbound" class="ml-1 px-1.5 py-0.5 text-xs bg-rose-100 dark:bg-rose-900 text-rose-700 dark:text-rose-300 rounded-full"></span>
</button>
</div>
{# ===== OUTBOUND TAB - My Activity ===== #}
<div x-show="activeTab === 'outbound'" x-transition>
<div x-show="activeTab === 'outbound'" x-transition role="tabpanel" id="interactions-panel-outbound" aria-labelledby="interactions-tab-outbound">
<p class="text-surface-600 dark:text-surface-400 text-sm mb-6">Content I've interacted with across the web.</p>
<div class="grid gap-4 sm:gap-6 md:grid-cols-2 lg:grid-cols-3">
{# Likes #}
<a href="/likes/" class="block p-6 bg-surface-100 dark:bg-surface-800 rounded-lg hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors group">
<a href="/likes/" class="block p-6 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors group shadow-sm">
<div class="flex items-center gap-4 mb-4">
<div class="p-3 bg-rose-100 dark:bg-rose-900/30 rounded-full">
<svg class="w-6 h-6 text-rose-500" fill="currentColor" viewBox="0 0 24 24">
@@ -50,14 +54,14 @@ permalink: /interactions/
</div>
<div>
<h2 class="text-lg font-semibold text-surface-900 dark:text-surface-100 group-hover:text-rose-600 dark:group-hover:text-rose-400">Likes</h2>
<p class="text-sm text-surface-500">{{ collections.likes.length }} item{% if collections.likes.length != 1 %}s{% endif %}</p>
<p class="text-sm text-surface-600 dark:text-surface-400">{{ collections.likes.length }} item{% if collections.likes.length != 1 %}s{% endif %}</p>
</div>
</div>
<p class="text-surface-600 dark:text-surface-400 text-sm">Content I've appreciated across the web.</p>
</a>
{# Replies #}
<a href="/replies/" class="block p-6 bg-surface-100 dark:bg-surface-800 rounded-lg hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors group">
<a href="/replies/" class="block p-6 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors group shadow-sm">
<div class="flex items-center gap-4 mb-4">
<div class="p-3 bg-rose-100 dark:bg-rose-900/30 rounded-full">
<svg class="w-6 h-6 text-rose-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -66,14 +70,14 @@ permalink: /interactions/
</div>
<div>
<h2 class="text-lg font-semibold text-surface-900 dark:text-surface-100 group-hover:text-rose-600 dark:group-hover:text-rose-400">Replies</h2>
<p class="text-sm text-surface-500">{{ collections.replies.length }} item{% if collections.replies.length != 1 %}s{% endif %}</p>
<p class="text-sm text-surface-600 dark:text-surface-400">{{ collections.replies.length }} item{% if collections.replies.length != 1 %}s{% endif %}</p>
</div>
</div>
<p class="text-surface-600 dark:text-surface-400 text-sm">My responses to posts across the web.</p>
</a>
{# Bookmarks #}
<a href="/bookmarks/" class="block p-6 bg-surface-100 dark:bg-surface-800 rounded-lg hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors group">
<a href="/bookmarks/" class="block p-6 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors group shadow-sm">
<div class="flex items-center gap-4 mb-4">
<div class="p-3 bg-yellow-100 dark:bg-yellow-900/30 rounded-full">
<svg class="w-6 h-6 text-yellow-500" fill="currentColor" viewBox="0 0 24 24">
@@ -82,14 +86,14 @@ permalink: /interactions/
</div>
<div>
<h2 class="text-lg font-semibold text-surface-900 dark:text-surface-100 group-hover:text-amber-600 dark:group-hover:text-amber-400">Bookmarks</h2>
<p class="text-sm text-surface-500">{{ collections.bookmarks.length }} item{% if collections.bookmarks.length != 1 %}s{% endif %}</p>
<p class="text-sm text-surface-600 dark:text-surface-400">{{ collections.bookmarks.length }} item{% if collections.bookmarks.length != 1 %}s{% endif %}</p>
</div>
</div>
<p class="text-surface-600 dark:text-surface-400 text-sm">Links I've saved for later.</p>
</a>
{# Reposts #}
<a href="/reposts/" class="block p-6 bg-surface-100 dark:bg-surface-800 rounded-lg hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors group">
<a href="/reposts/" class="block p-6 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors group shadow-sm">
<div class="flex items-center gap-4 mb-4">
<div class="p-3 bg-rose-100 dark:bg-rose-900/30 rounded-full">
<svg class="w-6 h-6 text-rose-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -98,14 +102,14 @@ permalink: /interactions/
</div>
<div>
<h2 class="text-lg font-semibold text-surface-900 dark:text-surface-100 group-hover:text-rose-600 dark:group-hover:text-rose-400">Reposts</h2>
<p class="text-sm text-surface-500">{{ collections.reposts.length }} item{% if collections.reposts.length != 1 %}s{% endif %}</p>
<p class="text-sm text-surface-600 dark:text-surface-400">{{ collections.reposts.length }} item{% if collections.reposts.length != 1 %}s{% endif %}</p>
</div>
</div>
<p class="text-surface-600 dark:text-surface-400 text-sm">Content I've shared from others.</p>
</a>
{# Photos #}
<a href="/photos/" class="block p-6 bg-surface-100 dark:bg-surface-800 rounded-lg hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors group">
<a href="/photos/" class="block p-6 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors group shadow-sm">
<div class="flex items-center gap-4 mb-4">
<div class="p-3 bg-purple-100 dark:bg-purple-900/30 rounded-full">
<svg class="w-6 h-6 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -114,14 +118,14 @@ permalink: /interactions/
</div>
<div>
<h2 class="text-lg font-semibold text-surface-900 dark:text-surface-100 group-hover:text-purple-600 dark:group-hover:text-purple-400">Photos</h2>
<p class="text-sm text-surface-500">{{ collections.photos.length }} item{% if collections.photos.length != 1 %}s{% endif %}</p>
<p class="text-sm text-surface-600 dark:text-surface-400">{{ collections.photos.length }} item{% if collections.photos.length != 1 %}s{% endif %}</p>
</div>
</div>
<p class="text-surface-600 dark:text-surface-400 text-sm">Photo posts and images.</p>
</a>
</div>
<div class="mt-12 p-6 bg-surface-100 dark:bg-surface-800/50 rounded-lg">
<div class="mt-12 p-6 bg-surface-50 dark:bg-surface-800/50 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
<h2 class="text-lg font-semibold text-surface-900 dark:text-surface-100 mb-2">About IndieWeb Interactions</h2>
<p class="text-surface-600 dark:text-surface-400 text-sm mb-4">
These pages show different types of IndieWeb interactions I've made. Each type uses specific microformat properties
@@ -137,7 +141,7 @@ permalink: /interactions/
</div>
{# ===== INBOUND TAB - Received Webmentions ===== #}
<div x-show="activeTab === 'inbound'" x-transition>
<div x-show="activeTab === 'inbound'" x-transition role="tabpanel" id="interactions-panel-inbound" aria-labelledby="interactions-tab-inbound">
<div class="flex items-center justify-between mb-6">
<p class="text-surface-600 dark:text-surface-400 text-sm">Webmentions and interactions others have made with my content.</p>
<button
@@ -154,7 +158,7 @@ permalink: /interactions/
{# Loading state #}
<div x-show="loading && !webmentions.length" class="text-center py-12">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-rose-500"></div>
<p class="mt-4 text-surface-500">Loading webmentions...</p>
<p class="mt-4 text-surface-600 dark:text-surface-400">Loading webmentions...</p>
</div>
{# Setup required state — shown when webmentions proxy is not configured #}
@@ -214,7 +218,7 @@ permalink: /interactions/
{# Webmentions list #}
<div x-show="!notConfigured && (!loading || webmentions.length)" class="space-y-4">
<template x-for="wm in paginatedWebmentions" :key="wm['wm-id']">
<div class="p-4 bg-surface-100 dark:bg-surface-800 rounded-lg">
<div class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
<div class="flex gap-3">
{# Author avatar #}
<a :href="wm.author?.url || '#'" target="_blank" rel="noopener" class="flex-shrink-0">
@@ -257,11 +261,14 @@ permalink: /interactions/
<svg class="w-3 h-3" viewBox="0 0 568 501" fill="currentColor"><path d="M123.121 33.664C188.241 82.553 258.281 181.68 284 234.873c25.719-53.192 95.759-152.32 160.879-201.21C491.866-1.611 568-28.906 568 57.947c0 17.346-9.945 145.713-15.778 166.555-20.275 72.453-94.155 90.933-159.875 79.748C507.222 323.8 536.444 388.56 473.333 453.32c-119.86 122.992-172.272-30.859-185.702-70.281-2.462-7.227-3.614-10.608-3.631-7.733-.017-2.875-1.169.506-3.631 7.733-13.43 39.422-65.842 193.273-185.702 70.281-63.111-64.76-33.89-129.52 80.986-149.071-65.72 11.185-139.6-7.295-159.875-79.748C9.945 203.659 0 75.291 0 57.946 0-28.906 76.135-1.612 123.121 33.664Z"/></svg>
</span>
<span x-show="wm.platform === 'activitypub'" class="inline-flex items-center gap-1 px-1.5 py-0.5 text-xs bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400 rounded-full" title="Fediverse (ActivityPub)">
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="19" r="2.5"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M13.09 4.43L24 10.73v2.51L13.09 19.58v-2.51L21.83 12 13.09 6.98v-2.55zM13.09 9.49L17.44 12l-4.35 2.51V9.49z"/><path d="M10.91 4.43L0 10.73v2.51l8.74-5.03v10.09l2.18 1.28V4.43zM6.56 12L2.18 14.51l4.35 2.51V12z"/></svg>
</span>
<span x-show="wm.platform === 'webmention' || !wm.platform" class="inline-flex items-center gap-1 px-1.5 py-0.5 text-xs bg-rose-100 dark:bg-rose-900/30 text-rose-600 dark:text-rose-400 rounded-full" title="IndieWeb (Webmention)">
<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></svg>
</span>
<a :href="wm.url || '#'" target="_blank" rel="noopener" class="text-xs text-surface-500 hover:underline">
<time :datetime="wm.published || wm['wm-received']" x-text="formatDate(wm.published || wm['wm-received'])"></time>
<a :href="wm.url || '#'" target="_blank" rel="noopener" class="text-xs text-surface-600 dark:text-surface-400 hover:underline">
<time class="font-mono text-sm" :datetime="wm.published || wm['wm-received']" x-text="formatDate(wm.published || wm['wm-received'])"></time>
</a>
</div>
@@ -269,7 +276,7 @@ permalink: /interactions/
<div x-show="wm.content?.text" class="text-surface-700 dark:text-surface-300 text-sm mt-2" x-text="truncateText(wm.content?.text, 280)"></div>
{# Target URL - which of my posts this is about #}
<div class="mt-2 text-xs text-surface-500">
<div class="mt-2 text-xs text-surface-600 dark:text-surface-400">
<span>on </span>
<a :href="wm['wm-target']" class="text-rose-600 dark:text-rose-400 hover:underline" x-text="formatTargetUrl(wm['wm-target'])"></a>
</div>
@@ -279,23 +286,52 @@ permalink: /interactions/
</template>
{# Empty state #}
<div x-show="!loading && filteredWebmentions.length === 0" class="text-center py-12 text-surface-500">
<div x-show="!loading && filteredWebmentions.length === 0" class="text-center py-12 text-surface-600 dark:text-surface-400">
<p>No webmentions found for this filter.</p>
</div>
{# Pagination / Load more #}
<div x-show="hasMore" class="text-center pt-4">
{# Pagination controls #}
<div x-show="totalPages > 1" class="pt-6">
<nav class="pagination" aria-label="Webmentions pagination">
<div class="pagination-info">
Page <span x-text="currentPage"></span> of <span x-text="totalPages"></span>
<span class="text-surface-600 dark:text-surface-400 ml-1">(<span x-text="filteredWebmentions.length"></span> total)</span>
</div>
<div class="pagination-links">
<button
@click="loadMore()"
:disabled="loadingMore"
class="px-4 py-2 bg-surface-100 dark:bg-surface-800 hover:bg-surface-200 dark:hover:bg-surface-700 rounded-lg text-sm transition-colors disabled:opacity-50">
<span x-text="loadingMore ? 'Loading...' : 'Load More'"></span>
@click="goToPage(currentPage - 1)"
:disabled="currentPage <= 1"
class="pagination-link"
:class="currentPage <= 1 ? 'disabled' : ''">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
Previous
</button>
<template x-for="p in pageNumbers" :key="p">
<button
@click="typeof p === 'number' && goToPage(p)"
:disabled="p === '…'"
:class="p === currentPage ? 'bg-accent-600 text-white dark:bg-accent-500' : p === '…' ? 'cursor-default opacity-50' : 'bg-surface-100 dark:bg-surface-800 hover:bg-surface-200 dark:hover:bg-surface-700'"
class="px-3 py-1.5 text-sm rounded-lg transition-colors"
x-text="p">
</button>
</template>
<button
@click="goToPage(currentPage + 1)"
:disabled="currentPage >= totalPages"
class="pagination-link"
:class="currentPage >= totalPages ? 'disabled' : ''">
Next
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</button>
</div>
</nav>
</div>
</div>
{# Info box #}
<div class="mt-12 p-6 bg-surface-100 dark:bg-surface-800/50 rounded-lg">
<div class="mt-12 p-6 bg-surface-50 dark:bg-surface-800/50 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
<h2 class="text-lg font-semibold text-surface-900 dark:text-surface-100 mb-2">About Webmentions</h2>
<p class="text-surface-600 dark:text-surface-400 text-sm mb-4">
Webmentions are a W3C standard for cross-site communication. When someone likes, reposts, or replies to my content
@@ -316,14 +352,13 @@ function interactionsApp() {
return {
activeTab: 'inbound',
loading: false,
loadingMore: false,
error: null,
notConfigured: false,
webmentions: [],
filterType: 'all',
page: 0,
perPage: 50,
hasMore: true,
currentPage: 1,
displayPerPage: 20,
fetchPerPage: 200,
refreshInterval: null,
get likes() {
@@ -351,14 +386,41 @@ function interactionsApp() {
return this.webmentions.filter(wm => wm['wm-property'] === this.filterType);
},
get totalPages() {
return Math.max(1, Math.ceil(this.filteredWebmentions.length / this.displayPerPage));
},
get paginatedWebmentions() {
// Client-side pagination of filtered results
return this.filteredWebmentions;
const start = (this.currentPage - 1) * this.displayPerPage;
return this.filteredWebmentions.slice(start, start + this.displayPerPage);
},
get pageNumbers() {
const total = this.totalPages;
const current = this.currentPage;
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
const pages = [];
pages.push(1);
if (current > 3) pages.push('…');
for (let i = Math.max(2, current - 1); i <= Math.min(total - 1, current + 1); i++) {
pages.push(i);
}
if (current < total - 2) pages.push('…');
pages.push(total);
return pages;
},
goToPage(page) {
if (page < 1 || page > this.totalPages) return;
this.currentPage = page;
// Scroll to top of inbound tab
this.$el.closest('[x-show]')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
},
async init() {
// Reset page when filter changes
this.$watch('filterType', () => { this.currentPage = 1; });
await this.fetchWebmentions();
// Auto-refresh every 5 minutes (skip if not configured)
if (!this.notConfigured) {
this.refreshInterval = setInterval(() => this.fetchWebmentions(true), 5 * 60 * 1000);
}
@@ -367,24 +429,27 @@ function interactionsApp() {
async fetchWebmentions(silent = false) {
if (!silent) {
this.loading = true;
this.page = 0;
this.webmentions = [];
this.hasMore = true;
}
this.error = null;
try {
// Fetch from both webmention-io and conversations APIs in parallel
const wmUrl = `/webmentions/api/mentions?per-page=${this.perPage}&page=${this.page}`;
const convUrl = `/conversations/api/mentions?per-page=${this.perPage}&page=${this.page}`;
// Fetch all available webmentions by paging through the APIs
let allWm = [];
let allConv = [];
let wmPage = 0;
let wmDone = false;
let wmConfigured = true;
// Fetch all pages from both APIs
while (!wmDone) {
const [wmResult, convResult] = await Promise.allSettled([
fetch(wmUrl).then(r => {
fetch(`/webmentions/api/mentions?per-page=${this.fetchPerPage}&page=${wmPage}`).then(r => {
if (r.status === 404) return { children: [], notConfigured: true };
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
}),
fetch(convUrl).then(r => {
fetch(`/conversations/api/mentions?per-page=${this.fetchPerPage}&page=${wmPage}`).then(r => {
if (!r.ok) return { children: [] };
return r.json();
}).catch(() => ({ children: [] })),
@@ -393,20 +458,31 @@ function interactionsApp() {
const wmData = wmResult.status === 'fulfilled' ? wmResult.value : { children: [] };
const convData = convResult.status === 'fulfilled' ? convResult.value : { children: [] };
// Check if webmention-io is configured
if (wmData.notConfigured && (!convData.children || !convData.children.length)) {
if (wmData.notConfigured) wmConfigured = false;
allWm = allWm.concat(wmData.children || []);
allConv = allConv.concat(convData.children || []);
// Stop if both APIs returned fewer than a full page
const wmCount = (wmData.children || []).length;
const convCount = (convData.children || []).length;
if (wmCount < this.fetchPerPage && convCount < this.fetchPerPage) {
wmDone = true;
} else {
wmPage++;
// Safety cap to prevent infinite loops
if (wmPage > 20) wmDone = true;
}
}
if (!wmConfigured && allConv.length === 0) {
this.notConfigured = true;
return;
}
this.notConfigured = false;
// Merge and deduplicate - conversations items (with platform field) take priority
const merged = this.mergeAndDeduplicate(
wmData.children || [],
convData.children || []
);
const merged = this.mergeAndDeduplicate(allWm, allConv);
// Sort by date, newest first
merged.sort((a, b) => {
const dateA = new Date(a.published || a['wm-received'] || 0);
const dateB = new Date(b.published || b['wm-received'] || 0);
@@ -414,7 +490,7 @@ function interactionsApp() {
});
this.webmentions = merged;
this.hasMore = (wmData.children || []).length === this.perPage;
if (!silent) this.currentPage = 1;
} catch (err) {
this.error = `Failed to load webmentions: ${err.message}`;
console.error('[Interactions]', err);
@@ -426,22 +502,21 @@ function interactionsApp() {
detectPlatform(item) {
const source = item['wm-source'] || '';
const authorUrl = item.author?.url || '';
// Bridgy source URLs: brid.gy/{action}/{platform}/...
if (source.includes('brid.gy/') && source.includes('/mastodon/')) return 'mastodon';
if (source.includes('brid.gy/') && source.includes('/bluesky/')) return 'bluesky';
// Author URL heuristics
if (source.includes('fed.brid.gy')) return 'activitypub';
if (authorUrl.includes('bsky.app')) return 'bluesky';
if (authorUrl.includes('mstdn') || authorUrl.includes('mastodon') || authorUrl.includes('social.')) return 'mastodon';
return null;
if (authorUrl.includes('mstdn') || authorUrl.includes('mastodon') || authorUrl.includes('social.') ||
authorUrl.includes('fosstodon.') || authorUrl.includes('hachyderm.') || authorUrl.includes('infosec.exchange') ||
authorUrl.includes('pleroma.') || authorUrl.includes('misskey.') || authorUrl.includes('pixelfed.')) return 'mastodon';
return 'webmention';
},
mergeAndDeduplicate(wmItems, convItems) {
// Build a Set of source URLs from conversations for dedup
const convUrls = new Set(convItems.map(c => c.url).filter(Boolean));
const seen = new Set();
const result = [];
// Add all conversations items first (they have richer metadata)
for (const item of convItems) {
const key = item['wm-id'] || item.url;
if (key && !seen.has(key)) {
@@ -450,16 +525,11 @@ function interactionsApp() {
}
}
// Add webmention-io items that aren't duplicated by conversations
for (const item of wmItems) {
const wmKey = item['wm-id'];
if (seen.has(wmKey)) continue;
// Also check if this webmention's source URL matches a conversations item
// (same interaction from Bridgy webmention AND direct poll)
if (item.url && convUrls.has(item.url)) continue;
// Infer platform from Bridgy source URL or author URL
if (!item.platform) {
const detected = this.detectPlatform(item);
if (detected) item.platform = detected;
@@ -472,36 +542,6 @@ function interactionsApp() {
return result;
},
async loadMore() {
this.loadingMore = true;
this.page++;
try {
const wmUrl = `/webmentions/api/mentions?per-page=${this.perPage}&page=${this.page}`;
const convUrl = `/conversations/api/mentions?per-page=${this.perPage}&page=${this.page}`;
const [wmResult, convResult] = await Promise.allSettled([
fetch(wmUrl).then(r => r.ok ? r.json() : { children: [] }),
fetch(convUrl).then(r => r.ok ? r.json() : { children: [] }).catch(() => ({ children: [] })),
]);
const wmData = wmResult.status === 'fulfilled' ? wmResult.value : { children: [] };
const convData = convResult.status === 'fulfilled' ? convResult.value : { children: [] };
const merged = this.mergeAndDeduplicate(
wmData.children || [],
convData.children || []
);
this.webmentions = [...this.webmentions, ...merged];
this.hasMore = (wmData.children || []).length === this.perPage;
} catch (err) {
this.error = `Failed to load more: ${err.message}`;
} finally {
this.loadingMore = false;
}
},
formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
@@ -518,7 +558,6 @@ function interactionsApp() {
if (!url) return '';
try {
const u = new URL(url);
// Return just the pathname, trimmed
let path = u.pathname;
if (path.length > 50) {
path = path.slice(0, 47) + '...';

View File

@@ -133,6 +133,15 @@ document.addEventListener("alpine:init", () => {
}
},
trapFocus(event) {
const focusable = [...this.$el.querySelectorAll('button, input, a, [tabindex]:not([tabindex="-1"])')].filter(el => !el.closest('[x-show]') || el.closest('[x-show]').style.display !== 'none');
if (!focusable.length) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
},
redirectToInstance(domain) {
if (this.mode === "share") {
window.location.href = `https://${domain}/share?text=${encodeURIComponent(this.targetUrl)}`;

View File

@@ -11,6 +11,7 @@ document.addEventListener("alpine:init", () => {
alt: "",
images: [],
currentIndex: 0,
triggerElement: null,
init() {
const container = this.$root;
@@ -21,14 +22,24 @@ document.addEventListener("alpine:init", () => {
this.images.forEach((img, i) => {
img.style.cursor = "zoom-in";
img.setAttribute("tabindex", "0");
img.setAttribute("role", "button");
img.setAttribute("aria-label", (img.alt || "Image") + " — click to enlarge");
img.addEventListener("click", (e) => {
e.preventDefault();
this.show(i);
});
img.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
this.show(i);
}
});
});
},
show(index) {
this.triggerElement = this.images[index];
this.currentIndex = index;
const img = this.images[index];
// Use the largest source available
@@ -48,12 +59,22 @@ document.addEventListener("alpine:init", () => {
this.alt = img.alt || "";
this.open = true;
document.body.style.overflow = "hidden";
// Move focus to close button for keyboard users
this.$nextTick(() => {
const closeBtn = document.querySelector('[x-ref="closeBtn"]');
if (closeBtn) closeBtn.focus();
});
},
close() {
this.open = false;
this.src = "";
document.body.style.overflow = "";
// Return focus to the image that triggered the lightbox
if (this.triggerElement) {
this.triggerElement.focus();
this.triggerElement = null;
}
},
next() {
@@ -75,6 +96,23 @@ document.addEventListener("alpine:init", () => {
if (e.key === "Escape") this.close();
if (e.key === "ArrowRight") this.next();
if (e.key === "ArrowLeft") this.prev();
if (e.key === "Tab") {
const dialog = document.querySelector('[role="dialog"][aria-modal="true"]');
if (!dialog) return;
const focusable = Array.from(
dialog.querySelectorAll('button, [tabindex]:not([tabindex="-1"])')
).filter((el) => el.offsetParent !== null);
if (!focusable.length) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
},
}));
});

View File

@@ -78,9 +78,12 @@ class TimeDifference extends HTMLElement {
const relative = rtf.format(value, unit);
// Store original text as title for hover tooltip
const originalText = time.textContent.trim();
if (!time.hasAttribute("title")) {
time.setAttribute("title", time.textContent.trim());
time.setAttribute("title", originalText);
}
// aria-label provides the full context: "2 days ago (March 5, 2026)"
time.setAttribute("aria-label", relative + " (" + originalText + ")");
time.textContent = relative;
} catch {
// Intl.RelativeTimeFormat not supported, keep static text

View File

@@ -197,22 +197,26 @@
items.forEach((item) => {
const author = item.author || {};
const li = document.createElement('li');
li.className = 'inline';
const link = document.createElement('a');
link.href = author.url || '#';
link.className = 'facepile-avatar';
link.title = author.name || 'Anonymous';
link.setAttribute('aria-label', (author.name || 'Anonymous') + ' (opens in new tab)');
link.target = '_blank';
link.rel = 'noopener';
link.dataset.new = 'true';
const img = document.createElement('img');
img.src = author.photo || '/images/default-avatar.svg';
img.alt = author.name || 'Anonymous';
img.alt = '';
img.className = 'w-8 h-8 rounded-full ring-2 ring-white dark:ring-surface-900';
img.loading = 'lazy';
link.appendChild(img);
row.appendChild(link);
li.appendChild(link);
row.appendChild(li);
});
}
@@ -278,7 +282,7 @@
const dateLink = document.createElement('a');
dateLink.href = item.url || '#';
dateLink.className = 'text-xs text-surface-500 hover:underline';
dateLink.className = 'text-xs text-surface-600 dark:text-surface-400 hover:underline';
dateLink.target = '_blank';
dateLink.rel = 'noopener';
@@ -396,8 +400,9 @@
header.className = 'text-sm font-semibold text-surface-600 dark:text-surface-400 uppercase tracking-wide mb-3';
header.textContent = `0 ${type === 'likes' ? 'Likes' : 'Reposts'}`;
const row = document.createElement('div');
const row = document.createElement('ul');
row.className = 'facepile';
row.setAttribute('role', 'list');
section.appendChild(header);
section.appendChild(row);
@@ -446,6 +451,8 @@
const section = document.createElement('section');
section.className = 'webmentions mt-8 pt-8 border-t border-surface-200 dark:border-surface-700';
section.id = 'webmentions';
section.setAttribute('aria-live', 'polite');
section.setAttribute('aria-label', 'Webmentions');
const header = document.createElement('h2');
header.className = 'text-xl font-bold text-surface-900 dark:text-surface-100 mb-6';

View File

@@ -109,8 +109,8 @@ export function renderCard(url, metadata) {
<a href="${escapeHtml(url)}" rel="noopener" target="_blank" class="flex no-underline text-inherit hover:text-inherit">
<div class="flex-1 p-3 sm:p-4 min-w-0">
<p class="font-semibold text-sm sm:text-base text-surface-900 dark:text-surface-100 truncate m-0">${escapeHtml(title)}</p>
${desc ? `<p class="text-xs sm:text-sm text-surface-500 dark:text-surface-400 mt-1 m-0 line-clamp-2">${escapeHtml(desc)}</p>` : ""}
<p class="text-xs text-surface-400 dark:text-surface-500 mt-2 m-0">${faviconHtml}${escapeHtml(domain)}</p>
${desc ? `<p class="text-xs sm:text-sm text-surface-600 dark:text-surface-400 mt-1 m-0 line-clamp-2">${escapeHtml(desc)}</p>` : ""}
<p class="text-xs text-surface-600 dark:text-surface-400 mt-2 m-0">${faviconHtml}${escapeHtml(domain)}</p>
</div>
${imgHtml}
</a>

View File

@@ -14,7 +14,7 @@ permalink: "likes/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumbe
<h1 class="text-2xl sm:text-3xl font-bold text-surface-900 dark:text-surface-100">Likes</h1>
{% set sparklineSvg = collections.likes | postingFrequency %}
{% if sparklineSvg %}
<span class="text-rose-600 dark:text-rose-400">{{ sparklineSvg | safe }}</span>
<div class="flex-1 min-w-0 text-red-600 dark:text-red-400">{{ sparklineSvg | safe }}</div>
{% endif %}
</div>
<p class="text-surface-600 dark:text-surface-400 mb-6 sm:mb-8">
@@ -25,16 +25,16 @@ permalink: "likes/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumbe
{% if paginatedLikes.length > 0 %}
<ul class="post-list">
{% for post in paginatedLikes %}
<li class="h-entry post-card border-l-[3px] border-l-rose-400 dark:border-l-rose-500">
<li class="h-entry post-card border-l-[3px] border-l-red-400 dark:border-l-red-500">
<div class="post-header flex items-start gap-3">
<div class="flex-shrink-0 mt-1">
<svg class="w-5 h-5 text-rose-500" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<svg class="w-5 h-5 text-red-500" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="post-meta">
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
{% if post.data.category %}
@@ -53,7 +53,7 @@ permalink: "likes/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumbe
{% set likedUrl = post.data.likeOf or post.data.like_of %}
{% if likedUrl %}
{% unfurl likedUrl %}
<a class="u-like-of text-xs text-surface-400 dark:text-surface-500 hover:underline break-all mt-1 inline-block" href="{{ likedUrl }}">
<a class="u-like-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ likedUrl }}">
{{ likedUrl }}
</a>
{% endif %}
@@ -62,7 +62,7 @@ permalink: "likes/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumbe
{{ post.templateContent | safe }}
</div>
{% endif %}
<a class="u-url text-sm text-rose-600 dark:text-rose-400 hover:underline mt-3 inline-block" href="{{ post.url }}">Permalink</a>
<a class="u-url text-sm text-red-600 dark:text-red-400 hover:underline mt-3 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or ('Like from ' + (post.date | dateDisplay)) }}">Permalink</a>
</div>
</div>
</li>
@@ -82,7 +82,7 @@ permalink: "likes/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumbe
Previous
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
Previous
</span>
@@ -94,7 +94,7 @@ permalink: "likes/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumbe
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
Next
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</span>

View File

@@ -99,9 +99,9 @@ withSidebar: true
</h2>
<p class="text-surface-600 dark:text-surface-400">{{ fwNowPlaying.artist }}</p>
{% if fwNowPlaying.album %}
<p class="text-sm text-surface-500 mt-1">{{ fwNowPlaying.album }}</p>
<p class="text-sm text-surface-600 dark:text-surface-400 mt-1">{{ fwNowPlaying.album }}</p>
{% endif %}
<p class="text-xs text-surface-500 mt-2">{{ fwNowPlaying.relativeTime }}</p>
<p class="text-xs text-surface-600 dark:text-surface-400 mt-2">{{ fwNowPlaying.relativeTime }}</p>
</div>
</div>
</div>
@@ -154,9 +154,9 @@ withSidebar: true
</h2>
<p class="text-surface-600 dark:text-surface-400">{{ lfmNowPlaying.artist }}</p>
{% if lfmNowPlaying.album %}
<p class="text-sm text-surface-500 mt-1">{{ lfmNowPlaying.album }}</p>
<p class="text-sm text-surface-600 dark:text-surface-400 mt-1">{{ lfmNowPlaying.album }}</p>
{% endif %}
<p class="text-xs text-surface-500 mt-2">{{ lfmNowPlaying.relativeTime }}</p>
<p class="text-xs text-surface-600 dark:text-surface-400 mt-2">{{ lfmNowPlaying.relativeTime }}</p>
</div>
</div>
</div>
@@ -179,7 +179,7 @@ withSidebar: true
<div class="grid gap-4 sm:gap-6 md:grid-cols-2">
{# Funkwhale Stats #}
{% if funkwhaleActivity.stats %}
<div x-show="activeSource === 'all' || activeSource === 'funkwhale'" class="bg-surface-50 dark:bg-surface-800 rounded-xl p-6 border border-purple-200 dark:border-purple-800">
<div x-show="activeSource === 'all' || activeSource === 'funkwhale'" class="bg-surface-50 dark:bg-surface-800 rounded-xl p-6 border border-purple-200 dark:border-purple-800 shadow-sm">
<h3 class="text-lg font-semibold text-purple-700 dark:text-purple-400 mb-4 flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-purple-500"></span>
Funkwhale
@@ -187,15 +187,15 @@ withSidebar: true
<div class="grid grid-cols-3 gap-4 text-center">
<div>
<div class="text-2xl font-bold text-surface-900 dark:text-surface-100">{{ funkwhaleActivity.stats.summary.all.totalPlays | default(0) }}</div>
<div class="text-xs text-surface-500 uppercase">Plays</div>
<div class="text-xs text-surface-600 dark:text-surface-400 uppercase">Plays</div>
</div>
<div>
<div class="text-2xl font-bold text-surface-900 dark:text-surface-100">{{ funkwhaleActivity.stats.summary.all.uniqueArtists | default(0) }}</div>
<div class="text-xs text-surface-500 uppercase">Artists</div>
<div class="text-xs text-surface-600 dark:text-surface-400 uppercase">Artists</div>
</div>
<div>
<div class="text-2xl font-bold text-surface-900 dark:text-surface-100">{{ funkwhaleActivity.stats.summary.all.uniqueTracks | default(0) }}</div>
<div class="text-xs text-surface-500 uppercase">Tracks</div>
<div class="text-xs text-surface-600 dark:text-surface-400 uppercase">Tracks</div>
</div>
</div>
{# Top Artists #}
@@ -206,7 +206,7 @@ withSidebar: true
{% for artist in funkwhaleActivity.stats.topArtists.all | head(5) %}
<div class="flex justify-between text-sm">
<span class="text-surface-600 dark:text-surface-400 truncate">{{ artist.name }}</span>
<span class="text-surface-500 ml-2">{{ artist.playCount }}</span>
<span class="text-surface-600 dark:text-surface-400 ml-2">{{ artist.playCount }}</span>
</div>
{% endfor %}
</div>
@@ -217,7 +217,7 @@ withSidebar: true
{# Last.fm Stats #}
{% if lastfmActivity.stats %}
<div x-show="activeSource === 'all' || activeSource === 'lastfm'" class="bg-surface-50 dark:bg-surface-800 rounded-xl p-6 border border-purple-200 dark:border-purple-800">
<div x-show="activeSource === 'all' || activeSource === 'lastfm'" class="bg-surface-50 dark:bg-surface-800 rounded-xl p-6 border border-purple-200 dark:border-purple-800 shadow-sm">
<h3 class="text-lg font-semibold text-purple-700 dark:text-purple-400 mb-4 flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-purple-500"></span>
Last.fm
@@ -225,15 +225,15 @@ withSidebar: true
<div class="grid grid-cols-3 gap-4 text-center">
<div>
<div class="text-2xl font-bold text-surface-900 dark:text-surface-100">{{ lastfmActivity.stats.summary.all.totalPlays | default(0) }}</div>
<div class="text-xs text-surface-500 uppercase">Scrobbles</div>
<div class="text-xs text-surface-600 dark:text-surface-400 uppercase">Scrobbles</div>
</div>
<div>
<div class="text-2xl font-bold text-surface-900 dark:text-surface-100">{{ lastfmActivity.stats.summary.all.uniqueArtists | default(0) }}</div>
<div class="text-xs text-surface-500 uppercase">Artists</div>
<div class="text-xs text-surface-600 dark:text-surface-400 uppercase">Artists</div>
</div>
<div>
<div class="text-2xl font-bold text-surface-900 dark:text-surface-100">{{ lastfmActivity.stats.summary.all.lovedCount | default(0) }}</div>
<div class="text-xs text-surface-500 uppercase">Loved</div>
<div class="text-xs text-surface-600 dark:text-surface-400 uppercase">Loved</div>
</div>
</div>
{# Top Artists from Last.fm #}
@@ -244,7 +244,7 @@ withSidebar: true
{% for artist in lastfmActivity.stats.topArtists.all | head(5) %}
<div class="flex justify-between text-sm">
<span class="text-surface-600 dark:text-surface-400 truncate">{{ artist.name }}</span>
<span class="text-surface-500 ml-2">{{ artist.playCount }}</span>
<span class="text-surface-600 dark:text-surface-400 ml-2">{{ artist.playCount }}</span>
</div>
{% endfor %}
</div>
@@ -270,7 +270,7 @@ withSidebar: true
{% if funkwhaleActivity.listenings.length %}
<div x-show="activeSource === 'all' || activeSource === 'funkwhale'">
{% for listening in funkwhaleActivity.listenings | head(10) %}
<div class="flex items-center gap-4 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-purple-400 dark:hover:border-purple-600 transition-colors mb-2">
<div class="flex items-center gap-4 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-purple-400 dark:hover:border-purple-600 transition-colors mb-2 shadow-sm">
{% if listening.coverUrl %}
<img src="{{ listening.coverUrl }}" alt="" class="w-12 h-12 rounded object-cover flex-shrink-0" loading="lazy" eleventy:ignore>
{% else %}
@@ -294,7 +294,7 @@ withSidebar: true
<div class="text-right flex-shrink-0">
<span class="inline-block px-2 py-0.5 text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400 rounded-full mb-1">Funkwhale</span>
<span class="text-xs text-surface-500 block">{{ listening.relativeTime }}</span>
<span class="text-xs text-surface-600 dark:text-surface-400 block">{{ listening.relativeTime }}</span>
<button
class="share-post-btn mt-1"
data-share-url="{{ listening.trackUrl }}"
@@ -324,7 +324,7 @@ withSidebar: true
{% if lastfmActivity.scrobbles.length %}
<div x-show="activeSource === 'all' || activeSource === 'lastfm'">
{% for scrobble in lastfmActivity.scrobbles | head(10) %}
<div class="flex items-center gap-4 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-purple-400 dark:hover:border-purple-600 transition-colors mb-2">
<div class="flex items-center gap-4 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-purple-400 dark:hover:border-purple-600 transition-colors mb-2 shadow-sm">
{% if scrobble.coverUrl %}
<img src="{{ scrobble.coverUrl }}" alt="" class="w-12 h-12 rounded object-cover flex-shrink-0" loading="lazy" eleventy:ignore>
{% else %}
@@ -351,7 +351,7 @@ withSidebar: true
<div class="text-right flex-shrink-0">
<span class="inline-block px-2 py-0.5 text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400 rounded-full mb-1">Last.fm</span>
<span class="text-xs text-surface-500 block">{{ scrobble.relativeTime }}</span>
<span class="text-xs text-surface-600 dark:text-surface-400 block">{{ scrobble.relativeTime }}</span>
<button
class="share-post-btn mt-1"
data-share-url="{{ scrobble.trackUrl }}"
@@ -391,12 +391,12 @@ withSidebar: true
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
</svg>
Loved Tracks
<span class="text-sm font-normal text-surface-500">(Last.fm)</span>
<span class="text-sm font-normal text-surface-600 dark:text-surface-400">(Last.fm)</span>
</h2>
<div class="grid gap-3 sm:gap-4 md:grid-cols-2">
{% for track in lastfmActivity.loved | head(10) %}
<div class="flex items-center gap-3 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700">
<div class="flex items-center gap-3 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
{% if track.coverUrl %}
<img src="{{ track.coverUrl }}" alt="" class="w-14 h-14 rounded object-cover flex-shrink-0" loading="lazy" eleventy:ignore>
{% else %}
@@ -452,12 +452,12 @@ withSidebar: true
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
</svg>
Favorite Tracks
<span class="text-sm font-normal text-surface-500">(Funkwhale)</span>
<span class="text-sm font-normal text-surface-600 dark:text-surface-400">(Funkwhale)</span>
</h2>
<div class="grid gap-3 sm:gap-4 md:grid-cols-2">
{% for favorite in funkwhaleActivity.favorites | head(10) %}
<div class="flex items-center gap-3 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700">
<div class="flex items-center gap-3 p-3 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
{% if favorite.coverUrl %}
<img src="{{ favorite.coverUrl }}" alt="" class="w-14 h-14 rounded object-cover flex-shrink-0" loading="lazy" eleventy:ignore>
{% else %}
@@ -478,7 +478,7 @@ withSidebar: true
</h3>
<p class="text-sm text-surface-600 dark:text-surface-400 truncate">{{ favorite.artist }}</p>
{% if favorite.album %}
<p class="text-xs text-surface-500 truncate">{{ favorite.album }}</p>
<p class="text-xs text-surface-600 dark:text-surface-400 truncate">{{ favorite.album }}</p>
{% endif %}
</div>
<button

View File

@@ -10,10 +10,10 @@ withSidebar: true
<p class="text-surface-600 dark:text-surface-400">
Aggregated content from my favorite feeds
</p>
<p class="text-xs text-surface-500 mt-2" x-show="lastUpdated">
Last updated: <span x-text="formatDate(lastUpdated, 'full')"></span>
<button @click="refresh()" class="ml-2 text-accent-600 hover:text-accent-700 dark:text-accent-400" :disabled="loading">
<svg class="w-3 h-3 inline" :class="{ 'animate-spin': loading }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<p class="text-xs text-surface-600 dark:text-surface-400 mt-2" x-show="lastUpdated">
Last updated: <span class="font-mono" x-text="formatDate(lastUpdated, 'full')"></span>
<button @click="refresh()" class="ml-2 text-accent-600 hover:text-accent-700 dark:text-accent-400 rounded" :disabled="loading" aria-label="Refresh news">
<svg class="w-3 h-3 inline" :class="{ 'animate-spin': loading }" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
</button>
@@ -21,8 +21,8 @@ withSidebar: true
</header>
{# Loading State #}
<div x-show="loading && items.length === 0" class="text-center py-12">
<svg class="w-8 h-8 mx-auto text-accent-600 animate-spin mb-4" fill="none" viewBox="0 0 24 24">
<div x-show="loading && items.length === 0" class="text-center py-12" role="status">
<svg class="w-8 h-8 mx-auto text-accent-600 animate-spin mb-4" fill="none" viewBox="0 0 24 24" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
@@ -30,7 +30,7 @@ withSidebar: true
</div>
{# Error State #}
<div x-show="error" class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6">
<div x-show="error" role="alert" class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6">
<p class="text-red-700 dark:text-red-400" x-text="error"></p>
<button @click="refresh()" class="mt-2 text-sm text-red-600 hover:text-red-700 underline">Try again</button>
</div>
@@ -45,6 +45,7 @@ withSidebar: true
<button
@click="viewMode = 'list'"
:class="viewMode === 'list' ? 'bg-accent-600 text-white' : 'bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-300 hover:bg-surface-200 dark:hover:bg-surface-700'"
:aria-pressed="(viewMode === 'list').toString()"
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
title="List view"
>
@@ -56,6 +57,7 @@ withSidebar: true
<button
@click="viewMode = 'card'"
:class="viewMode === 'card' ? 'bg-accent-600 text-white' : 'bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-300 hover:bg-surface-200 dark:hover:bg-surface-700'"
:aria-pressed="(viewMode === 'card').toString()"
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
title="Card view"
>
@@ -67,6 +69,7 @@ withSidebar: true
<button
@click="viewMode = 'full'"
:class="viewMode === 'full' ? 'bg-accent-600 text-white' : 'bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-300 hover:bg-surface-200 dark:hover:bg-surface-700'"
:aria-pressed="(viewMode === 'full').toString()"
class="px-3 py-2 rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
title="Expanded view"
>
@@ -79,30 +82,32 @@ withSidebar: true
{# Feed Filter Dropdown #}
<div class="relative" x-show="feeds.length > 1">
<label for="news-feed-filter" class="sr-only">Filter by feed source</label>
<select
id="news-feed-filter"
x-model="filterFeed"
class="appearance-none bg-surface-100 dark:bg-surface-800 border border-surface-300 dark:border-surface-600 rounded-lg px-4 py-2 pr-8 text-sm text-surface-700 dark:text-surface-300 focus:outline-none focus:ring-2 focus:ring-accent-500"
class="appearance-none bg-surface-100 dark:bg-surface-800 border border-surface-300 dark:border-surface-600 rounded-lg px-4 py-2 pr-8 text-sm text-surface-700 dark:text-surface-300 transition-colors"
>
<option value="all">All Sources (<span x-text="feeds.length"></span>)</option>
<template x-for="feed in feeds" :key="feed.id">
<option :value="feed.id" x-text="feed.title"></option>
</template>
</select>
<svg class="absolute right-2 top-1/2 transform -translate-y-1/2 w-4 h-4 text-surface-500 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="absolute right-2 top-1/2 transform -translate-y-1/2 w-4 h-4 text-surface-600 dark:text-surface-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</div>
</div>
{# Stats Bar #}
<div class="flex flex-wrap gap-4 mb-6 p-4 bg-surface-50 dark:bg-surface-800/50 rounded-lg text-sm">
<div class="flex flex-wrap gap-4 mb-6 p-4 bg-surface-50 dark:bg-surface-800/50 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm text-sm">
<div class="flex items-center gap-2">
<span class="text-surface-500">Feeds:</span>
<span class="font-medium text-surface-900 dark:text-surface-100" x-text="status?.stats?.feedsCount || feeds.length"></span>
<span class="text-surface-600 dark:text-surface-400">Feeds:</span>
<span class="font-medium font-mono text-surface-900 dark:text-surface-100" x-text="status?.stats?.feedsCount || feeds.length"></span>
</div>
<div class="flex items-center gap-2">
<span class="text-surface-500">Items:</span>
<span class="font-medium text-surface-900 dark:text-surface-100" x-text="status?.stats?.itemsCount || items.length"></span>
<span class="text-surface-600 dark:text-surface-400">Items:</span>
<span class="font-medium font-mono text-surface-900 dark:text-surface-100" x-text="status?.stats?.itemsCount || items.length"></span>
</div>
<div x-show="status?.status === 'syncing'" class="flex items-center gap-2 text-orange-600 dark:text-orange-400">
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
@@ -125,7 +130,7 @@ withSidebar: true
{# List View #}
<div x-show="viewMode === 'list'" class="space-y-3">
<template x-for="item in filteredItems" :key="item.id">
<article class="flex items-start gap-4 p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-accent-400 dark:hover:border-accent-600 transition-colors">
<article class="flex items-start gap-4 p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm hover:border-amber-400 dark:hover:border-amber-600 transition-colors">
<img
x-show="item.imageUrl"
:src="item.imageUrl"
@@ -144,7 +149,7 @@ withSidebar: true
></a>
</h2>
<p x-show="item.description" class="text-sm text-surface-600 dark:text-surface-400 line-clamp-2 mb-2" x-text="item.description"></p>
<div class="flex flex-wrap items-center gap-2 text-xs text-surface-500">
<div class="flex flex-wrap items-center gap-2 text-xs text-surface-600 dark:text-surface-400">
<a
:href="item.sourceUrl || getFeedUrl(item.feedId) || item.link"
class="inline-flex items-center gap-1 px-2 py-0.5 bg-surface-100 dark:bg-surface-700 rounded-full hover:bg-surface-200 dark:hover:bg-surface-600 transition-colors"
@@ -154,7 +159,7 @@ withSidebar: true
x-text="truncate(item.sourceTitle || item.feedTitle, 25)"
></a>
<span x-show="item.author" x-text="'by ' + item.author"></span>
<time :datetime="item.pubDate" x-text="formatDate(item.pubDate)"></time>
<time class="font-mono text-sm" :datetime="item.pubDate" x-text="formatDate(item.pubDate)"></time>
<span class="hidden sm:inline" x-show="item.categories?.length">
<template x-for="cat in item.categories.slice(0, 3)" :key="cat">
<span class="text-accent-600 dark:text-accent-400" x-text="'#' + cat"></span>
@@ -190,7 +195,7 @@ withSidebar: true
{# Card View #}
<div x-show="viewMode === 'card'" class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<template x-for="item in filteredItems" :key="item.id">
<article class="bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden hover:border-surface-400 dark:hover:border-surface-500 transition-colors">
<article class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden hover:border-amber-400 dark:hover:border-amber-600 transition-colors">
<div x-show="item.imageUrl" class="aspect-video bg-surface-100 dark:bg-surface-700">
<img
:src="item.imageUrl"
@@ -210,9 +215,9 @@ withSidebar: true
></a>
</h2>
<p x-show="item.description" class="text-sm text-surface-600 dark:text-surface-400 line-clamp-3 mb-3" x-text="item.description"></p>
<div class="flex items-center justify-between text-xs text-surface-500">
<div class="flex items-center justify-between text-xs text-surface-600 dark:text-surface-400">
<span class="truncate max-w-[60%]" x-text="truncate(item.sourceTitle || item.feedTitle, 20)"></span>
<time :datetime="item.pubDate" x-text="formatDate(item.pubDate)"></time>
<time class="font-mono text-sm" :datetime="item.pubDate" x-text="formatDate(item.pubDate)"></time>
</div>
<button
class="share-post-btn mt-2"
@@ -243,7 +248,7 @@ withSidebar: true
{# Full/Expanded View #}
<div x-show="viewMode === 'full'" class="space-y-6">
<template x-for="item in filteredItems" :key="item.id">
<article class="bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 overflow-hidden">
<article class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm overflow-hidden">
<div x-show="item.imageUrl" class="aspect-[3/1] bg-surface-100 dark:bg-surface-700">
<img
:src="item.imageUrl"
@@ -264,7 +269,7 @@ withSidebar: true
<span class="font-medium text-surface-700 dark:text-surface-300" x-text="item.sourceTitle || item.feedTitle"></span>
</a>
<span x-show="item.author" class="text-surface-600 dark:text-surface-400" x-text="'by ' + item.author"></span>
<time :datetime="item.pubDate" class="text-surface-500" x-text="formatDate(item.pubDate, 'long')"></time>
<time :datetime="item.pubDate" class="font-mono text-sm text-surface-600 dark:text-surface-400" x-text="formatDate(item.pubDate, 'long')"></time>
</div>
<h2 class="text-xl sm:text-2xl font-bold text-surface-900 dark:text-surface-100 mb-4">
@@ -334,7 +339,7 @@ withSidebar: true
Previous
</button>
<span class="px-4 py-2 text-sm text-surface-600 dark:text-surface-400">
Page <span x-text="pagination.page"></span> of <span x-text="pagination.totalPages"></span>
Page <span class="font-mono" x-text="pagination.page"></span> of <span class="font-mono" x-text="pagination.totalPages"></span>
</span>
<button
@click="loadPage(pagination.page + 1)"
@@ -349,11 +354,11 @@ withSidebar: true
{# Empty State #}
<div x-show="!loading && items.length === 0 && !error" class="text-center py-12">
<svg class="w-16 h-16 mx-auto text-surface-300 dark:text-surface-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-16 h-16 mx-auto text-surface-300 dark:text-surface-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"/>
</svg>
<p class="text-surface-600 dark:text-surface-400 text-lg">No news items yet.</p>
<p class="text-surface-500 text-sm mt-2">Add some RSS feeds to get started.</p>
<p class="text-surface-600 dark:text-surface-400 text-sm mt-2">Add some RSS feeds to get started.</p>
</div>
</div>
@@ -406,7 +411,9 @@ function newsApp() {
this.loading = true;
try {
const res = await fetch(`/rssapi/api/items?limit=50&page=${page}`).then(r => r.json());
let url = `/rssapi/api/items?limit=50&page=${page}`;
if (this.filterFeed !== 'all') url += `&feedId=${this.filterFeed}`;
const res = await fetch(url).then(r => r.json());
this.items = res.items || [];
this.pagination = res.pagination || null;
window.scrollTo({ top: 0, behavior: 'smooth' });

View File

@@ -14,7 +14,7 @@ permalink: "notes/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumbe
<h1 class="text-2xl sm:text-3xl font-bold text-surface-900 dark:text-surface-100">Notes</h1>
{% set sparklineSvg = collections.notes | postingFrequency %}
{% if sparklineSvg %}
<span class="text-amber-600 dark:text-amber-400">{{ sparklineSvg | safe }}</span>
<div class="flex-1 min-w-0 text-teal-600 dark:text-teal-400">{{ sparklineSvg | safe }}</div>
{% endif %}
</div>
<p class="text-surface-600 dark:text-surface-400 mb-6 sm:mb-8">
@@ -25,10 +25,10 @@ permalink: "notes/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumbe
{% if paginatedNotes.length > 0 %}
<ul class="post-list">
{% for post in paginatedNotes %}
<li class="h-entry post-card border-l-[3px] border-l-surface-300 dark:border-l-surface-600">
<li class="h-entry post-card border-l-[3px] border-l-teal-400 dark:border-l-teal-500">
<div class="post-header">
<a class="u-url" href="{{ post.url }}">
<time class="dt-published text-sm text-surface-500 dark:text-surface-400 font-medium" datetime="{{ post.date | isoDate }}">
<time class="dt-published text-sm text-surface-600 dark:text-surface-400 font-medium font-mono" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
</a>
@@ -52,7 +52,7 @@ permalink: "notes/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumbe
{% endif %}
</div>
<div class="post-footer mt-3">
<a href="{{ post.url }}" class="text-sm text-accent-600 dark:text-accent-400 hover:underline">
<a href="{{ post.url }}" class="text-sm text-teal-600 dark:text-teal-400 hover:underline" aria-label="Permalink: {{ post.data.title or ('Note from ' + (post.date | dateDisplay)) }}">
Permalink
</a>
</div>
@@ -73,7 +73,7 @@ permalink: "notes/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumbe
Previous
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
Previous
</span>
@@ -85,7 +85,7 @@ permalink: "notes/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumbe
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
Next
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</span>

View File

@@ -14,7 +14,7 @@ permalink: "photos/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumb
<h1 class="text-2xl sm:text-3xl font-bold text-surface-900 dark:text-surface-100">Photos</h1>
{% set sparklineSvg = collections.photos | postingFrequency %}
{% if sparklineSvg %}
<span class="text-purple-600 dark:text-purple-400">{{ sparklineSvg | safe }}</span>
<div class="flex-1 min-w-0 text-purple-600 dark:text-purple-400">{{ sparklineSvg | safe }}</div>
{% endif %}
</div>
<p class="text-surface-600 dark:text-surface-400 mb-6 sm:mb-8">
@@ -27,7 +27,7 @@ permalink: "photos/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumb
{% for post in paginatedPhotos %}
<li class="h-entry post-card border-l-[3px] border-l-purple-400 dark:border-l-purple-500">
<div class="post-meta">
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
{% if post.data.category %}
@@ -59,7 +59,7 @@ permalink: "photos/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumb
{% if post.templateContent %}
<div class="e-content photo-caption prose dark:prose-invert prose-sm mt-3 max-w-none">{{ post.templateContent | safe }}</div>
{% endif %}
<a class="u-url text-sm text-purple-600 dark:text-purple-400 hover:underline mt-3 inline-block" href="{{ post.url }}">Permalink</a>
<a class="u-url text-sm text-purple-600 dark:text-purple-400 hover:underline mt-3 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or ('Photo from ' + (post.date | dateDisplay)) }}">Permalink</a>
</li>
{% endfor %}
</ul>
@@ -77,7 +77,7 @@ permalink: "photos/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumb
Previous
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
Previous
</span>
@@ -89,7 +89,7 @@ permalink: "photos/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumb
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
Next
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</span>

View File

@@ -6,21 +6,18 @@ permalink: /podroll/
<div class="podroll-page" x-data="podrollApp()" x-init="init()">
<header class="mb-6 sm:mb-8">
<h1 class="text-2xl sm:text-3xl md:text-4xl font-bold text-surface-900 dark:text-surface-100 mb-2">
<svg class="w-8 h-8 inline-block mr-2 text-orange-600" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
<circle cx="12" cy="12" r="3"/>
<path d="M12 6a6 6 0 0 0-6 6h2a4 4 0 0 1 4-4V6z"/>
<path d="M12 2v2a8 8 0 0 1 8 8h2c0-5.52-4.48-10-10-10z"/>
<svg class="w-8 h-8 inline-block mr-2 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"/>
</svg>
Podroll
</h1>
<p class="text-surface-600 dark:text-surface-400">
My podcast subscriptions - recent episodes from <span x-text="sources.length" class="font-medium"></span> podcasts
My podcast subscriptions - recent episodes from <span x-text="sources.length" class="font-medium font-mono"></span> podcasts
</p>
<p class="text-xs text-surface-500 mt-2" x-show="status?.episodes?.lastSync">
Last synced: <span x-text="formatDate(status?.episodes?.lastSync, 'full')"></span>
<button @click="refresh()" class="ml-2 text-orange-600 hover:text-orange-700 dark:text-orange-400" :disabled="loading">
<svg class="w-3 h-3 inline" :class="{ 'animate-spin': loading }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<p class="text-xs text-surface-600 dark:text-surface-400 mt-2" x-show="status?.episodes?.lastSync">
Last synced: <span class="font-mono" x-text="formatDate(status?.episodes?.lastSync, 'full')"></span>
<button @click="refresh()" class="ml-2 text-orange-600 hover:text-orange-700 dark:text-orange-400 transition-colors rounded" :disabled="loading" aria-label="Refresh episodes">
<svg class="w-3 h-3 inline" :class="{ 'animate-spin': loading }" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
</button>
@@ -31,8 +28,8 @@ permalink: /podroll/
{# Main Content - Episodes #}
<div class="main-content">
{# Loading State #}
<div x-show="loading && episodes.length === 0" class="text-center py-12">
<svg class="w-8 h-8 mx-auto text-orange-600 animate-spin mb-4" fill="none" viewBox="0 0 24 24">
<div x-show="loading && episodes.length === 0" class="text-center py-12" role="status">
<svg class="w-8 h-8 mx-auto text-orange-600 animate-spin mb-4" fill="none" viewBox="0 0 24 24" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
@@ -40,7 +37,7 @@ permalink: /podroll/
</div>
{# Error State #}
<div x-show="error" class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6">
<div x-show="error" role="alert" class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6">
<p class="text-red-700 dark:text-red-400" x-text="error"></p>
<button @click="refresh()" class="mt-2 text-sm text-red-600 hover:text-red-700 underline">Try again</button>
</div>
@@ -48,16 +45,18 @@ permalink: /podroll/
{# Filter by Podcast #}
<div x-show="episodes.length > 0" class="mb-6">
<div class="relative">
<label for="podroll-filter" class="sr-only">Filter by podcast</label>
<select
id="podroll-filter"
x-model="filterPodcast"
class="w-full sm:w-auto appearance-none bg-surface-100 dark:bg-surface-800 border border-surface-300 dark:border-surface-600 rounded-lg px-4 py-2 pr-10 text-sm text-surface-700 dark:text-surface-300 focus:outline-none focus:ring-2 focus:ring-orange-500"
class="w-full sm:w-auto appearance-none bg-surface-100 dark:bg-surface-800 border border-surface-300 dark:border-surface-600 rounded-lg px-4 py-2 pr-10 text-sm text-surface-700 dark:text-surface-300"
>
<option value="all">All Podcasts</option>
<template x-for="source in sortedSources" :key="source.title">
<option :value="source.title" x-text="source.title"></option>
</template>
</select>
<svg class="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-surface-500 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-surface-600 dark:text-surface-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</div>
@@ -66,14 +65,14 @@ permalink: /podroll/
{# Episodes List #}
<div x-show="episodes.length > 0" class="space-y-4">
<template x-for="episode in filteredEpisodes" :key="episode.id">
<article class="bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-4 sm:p-6 hover:border-orange-400 dark:hover:border-orange-600 transition-colors">
<article class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm p-4 sm:p-6 hover:border-orange-400 dark:hover:border-orange-600 transition-colors">
{# Episode Header #}
<div class="flex items-start gap-4 mb-4">
<div class="flex-1 min-w-0">
<h2 class="font-semibold text-lg text-surface-900 dark:text-surface-100 mb-1">
<a :href="episode.url" class="hover:text-orange-600 dark:hover:text-orange-400" target="_blank" rel="noopener" x-text="episode.title"></a>
<a :href="episode.url" class="hover:text-orange-600 dark:hover:text-orange-400 transition-colors" target="_blank" rel="noopener" x-text="episode.title"></a>
</h2>
<div class="flex flex-wrap items-center gap-2 text-sm text-surface-500">
<div class="flex flex-wrap items-center gap-2 text-sm text-surface-600 dark:text-surface-400">
<a
x-show="episode.podcast"
:href="episode.podcast?.url || '#'"
@@ -87,8 +86,8 @@ permalink: /podroll/
</svg>
<span x-text="episode.podcast?.title || 'Unknown'"></span>
</a>
<time :datetime="episode.published" x-text="formatDate(episode.published)"></time>
<span x-show="episode.enclosure" class="text-surface-400">
<time class="font-mono text-sm" :datetime="episode.published" x-text="formatDate(episode.published)"></time>
<span x-show="episode.enclosure" class="text-surface-600 dark:text-surface-400">
<svg class="w-3 h-3 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15.536a5 5 0 001.414 1.414m2.828-9.9a9 9 0 012.828-2.828"/>
</svg>
@@ -105,6 +104,7 @@ permalink: /podroll/
preload="none"
class="w-full h-10 rounded-lg"
:src="episode.enclosure?.url"
:aria-label="'Play: ' + episode.title"
>
Your browser does not support the audio element.
</audio>
@@ -121,7 +121,7 @@ permalink: /podroll/
<div class="flex flex-wrap items-center gap-3 mt-4 pt-4 border-t border-surface-200 dark:border-surface-700">
<a
:href="episode.url"
class="inline-flex items-center gap-2 text-sm text-orange-600 hover:text-orange-700 dark:text-orange-400"
class="inline-flex items-center gap-2 text-sm text-orange-600 hover:text-orange-700 dark:text-orange-400 transition-colors"
target="_blank"
rel="noopener"
>
@@ -133,7 +133,7 @@ permalink: /podroll/
<a
x-show="episode.podcast?.feedUrl"
:href="episode.podcast?.feedUrl"
class="inline-flex items-center gap-2 text-sm text-surface-500 hover:text-surface-700 dark:hover:text-surface-300"
class="inline-flex items-center gap-2 text-sm text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300 transition-colors"
target="_blank"
rel="noopener"
title="Subscribe to feed"
@@ -190,11 +190,11 @@ permalink: /podroll/
{# Empty State #}
<div x-show="!loading && episodes.length === 0 && !error" class="text-center py-12">
<svg class="w-16 h-16 mx-auto text-surface-300 dark:text-surface-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-16 h-16 mx-auto text-surface-300 dark:text-surface-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"/>
</svg>
<p class="text-surface-600 dark:text-surface-400 text-lg">No podcast episodes yet.</p>
<p class="text-surface-500 text-sm mt-2">Episodes will appear once the sync completes.</p>
<p class="text-surface-600 dark:text-surface-400 text-sm mt-2">Episodes will appear once the sync completes.</p>
</div>
</div>
@@ -207,10 +207,10 @@ permalink: /podroll/
<path d="M4 4.44v2.83c7.03 0 12.73 5.7 12.73 12.73h2.83c0-8.59-6.97-15.56-15.56-15.56zm0 5.66v2.83c3.9 0 7.07 3.17 7.07 7.07h2.83c0-5.47-4.43-9.9-9.9-9.9z"/>
</svg>
Subscriptions
<span class="text-sm font-normal text-surface-500" x-text="'(' + sources.length + ')'"></span>
<span class="text-sm font-normal font-mono text-surface-600 dark:text-surface-400" x-text="'(' + sources.length + ')'"></span>
</h3>
<div x-show="sources.length === 0 && !loading" class="text-sm text-surface-500 text-center py-4">
<div x-show="sources.length === 0 && !loading" class="text-sm text-surface-600 dark:text-surface-400 text-center py-4">
No subscriptions loaded yet.
</div>
@@ -228,11 +228,11 @@ permalink: /podroll/
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-surface-900 dark:text-surface-100 truncate" x-text="source.title"></p>
<p x-show="source.category" class="text-xs text-surface-500 truncate" x-text="source.category"></p>
<p x-show="source.category" class="text-xs text-surface-600 dark:text-surface-400 truncate" x-text="source.category"></p>
</div>
<a
:href="source.xmlUrl"
class="opacity-0 group-hover:opacity-100 text-surface-400 hover:text-orange-600 transition-opacity"
class="opacity-0 group-hover:opacity-100 text-surface-400 hover:text-orange-600 transition-opacity transition-colors"
target="_blank"
rel="noopener"
title="RSS Feed"

View File

@@ -20,8 +20,8 @@ permalink: /readlater/
{# Main Content #}
<div class="main-content">
{# Loading State #}
<div x-show="loading" class="text-center py-12">
<svg class="w-8 h-8 mx-auto text-orange-600 animate-spin mb-4" fill="none" viewBox="0 0 24 24">
<div x-show="loading" class="text-center py-12" role="status">
<svg class="w-8 h-8 mx-auto text-orange-600 animate-spin mb-4" fill="none" viewBox="0 0 24 24" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
@@ -29,7 +29,7 @@ permalink: /readlater/
</div>
{# Error State #}
<div x-show="error" class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6">
<div x-show="error" role="alert" class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6">
<p class="text-red-700 dark:text-red-400" x-text="error"></p>
<button @click="fetchData()" class="mt-2 text-sm text-red-600 hover:text-red-700 underline">Try again</button>
</div>
@@ -38,10 +38,12 @@ permalink: /readlater/
<div x-show="!loading && items.length > 0" class="mb-6">
<div class="flex flex-wrap items-center gap-3">
<div class="relative">
<label for="readlater-source" class="sr-only">Filter by source</label>
<select
id="readlater-source"
x-model="selectedSource"
@change="fetchData()"
class="appearance-none bg-surface-50 dark:bg-surface-800 border border-surface-300 dark:border-surface-600 rounded-lg pl-3 pr-8 py-2 text-sm focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
class="appearance-none bg-surface-50 dark:bg-surface-800 border border-surface-300 dark:border-surface-600 rounded-lg pl-3 pr-8 py-2 text-sm"
>
<option value="">All sources</option>
<template x-for="src in sources" :key="src">
@@ -53,12 +55,14 @@ permalink: /readlater/
</svg>
</div>
<div class="relative flex-1 max-w-xs">
<label for="readlater-search" class="sr-only">Search reading list</label>
<input
id="readlater-search"
type="search"
x-model.debounce.300ms="searchQuery"
@input="fetchData()"
placeholder="Search..."
class="w-full bg-surface-50 dark:bg-surface-800 border border-surface-300 dark:border-surface-600 rounded-lg pl-9 pr-3 py-2 text-sm focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
class="w-full bg-surface-50 dark:bg-surface-800 border border-surface-300 dark:border-surface-600 rounded-lg pl-9 pr-3 py-2 text-sm"
/>
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-surface-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
@@ -80,7 +84,7 @@ permalink: /readlater/
{# Items List #}
<div x-show="!loading" class="space-y-4">
<template x-for="item in items" :key="item.id">
<article class="bg-surface-50 dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-4 sm:p-5 hover:border-orange-400 dark:hover:border-orange-600 transition-colors">
<article class="bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 p-4 sm:p-5 hover:border-orange-400 dark:hover:border-orange-600 transition-colors shadow-sm">
<div class="flex items-start gap-4">
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-gradient-to-br from-orange-400 to-orange-600 flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -91,14 +95,14 @@ permalink: /readlater/
<h2 class="font-semibold text-surface-900 dark:text-surface-100 mb-1">
<a :href="item.url" class="hover:text-orange-600 dark:hover:text-orange-400 transition-colors" target="_blank" rel="noopener" x-text="item.title"></a>
</h2>
<div class="flex flex-wrap items-center gap-2 text-sm text-surface-500">
<div class="flex flex-wrap items-center gap-2 text-sm text-surface-600 dark:text-surface-400">
<span
class="inline-flex items-center px-2 py-0.5 bg-surface-100 dark:bg-surface-700 rounded-full text-xs"
x-text="item.source"
></span>
<time :datetime="item.savedAt" x-text="formatDate(item.savedAt)"></time>
<time class="font-mono text-sm" :datetime="item.savedAt" x-text="formatDate(item.savedAt)"></time>
</div>
<p class="text-xs text-surface-400 mt-1 truncate" x-text="item.url"></p>
<p class="text-xs text-surface-600 dark:text-surface-400 mt-1 truncate" x-text="item.url"></p>
</div>
</div>
@@ -132,11 +136,11 @@ permalink: /readlater/
{# Empty State #}
<div x-show="!loading && items.length === 0 && !error" class="text-center py-12">
<svg class="w-16 h-16 mx-auto text-surface-300 dark:text-surface-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-16 h-16 mx-auto text-surface-300 dark:text-surface-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"/>
</svg>
<p class="text-surface-600 dark:text-surface-400 text-lg">No saved items yet.</p>
<p class="text-surface-500 text-sm mt-2">Save articles from around the web using the bookmark button.</p>
<p class="text-surface-600 dark:text-surface-400 text-sm mt-2">Save articles from around the web using the bookmark button.</p>
</div>
</div>
@@ -148,11 +152,11 @@ permalink: /readlater/
<div class="grid grid-cols-2 gap-3 text-center">
<div class="p-3 bg-surface-50 dark:bg-surface-800 rounded-lg">
<span class="text-2xl font-bold text-orange-600 dark:text-orange-400 block" x-text="items.length"></span>
<span class="text-xs text-surface-500 uppercase">Saved</span>
<span class="text-xs text-surface-600 dark:text-surface-400 uppercase">Saved</span>
</div>
<div class="p-3 bg-surface-50 dark:bg-surface-800 rounded-lg">
<span class="text-2xl font-bold text-orange-600 dark:text-orange-400 block" x-text="sources.length"></span>
<span class="text-xs text-surface-500 uppercase">Sources</span>
<span class="text-xs text-surface-600 dark:text-surface-400 uppercase">Sources</span>
</div>
</div>
</div>

View File

@@ -14,7 +14,7 @@ permalink: "replies/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNum
<h1 class="text-2xl sm:text-3xl font-bold text-surface-900 dark:text-surface-100">Replies</h1>
{% set sparklineSvg = collections.replies | postingFrequency %}
{% if sparklineSvg %}
<span class="text-rose-600 dark:text-rose-400">{{ sparklineSvg | safe }}</span>
<div class="flex-1 min-w-0 text-sky-600 dark:text-sky-400">{{ sparklineSvg | safe }}</div>
{% endif %}
</div>
<p class="text-surface-600 dark:text-surface-400 mb-6 sm:mb-8">
@@ -25,21 +25,21 @@ permalink: "replies/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNum
{% if paginatedReplies.length > 0 %}
<ul class="post-list">
{% for post in paginatedReplies %}
<li class="h-entry post-card border-l-[3px] border-l-rose-400 dark:border-l-rose-500">
<li class="h-entry post-card border-l-[3px] border-l-sky-400 dark:border-l-sky-500">
<div class="post-header flex items-start gap-3">
<div class="flex-shrink-0 mt-1">
<svg class="w-5 h-5 text-rose-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<svg class="w-5 h-5 text-sky-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/>
</svg>
</div>
<div class="flex-1 min-w-0">
{% if post.data.title %}
<h2 class="p-name text-lg font-semibold text-surface-900 dark:text-surface-100 mb-2">
<a class="hover:text-rose-600 dark:hover:text-rose-400" href="{{ post.url }}">{{ post.data.title }}</a>
<a class="hover:text-sky-600 dark:hover:text-sky-400" href="{{ post.url }}">{{ post.data.title }}</a>
</h2>
{% endif %}
<div class="post-meta">
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
{% if post.data.category %}
@@ -60,8 +60,8 @@ permalink: "replies/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNum
{% set protocol = replyTo | protocolType %}
{% unfurl replyTo %}
<p class="mt-2 text-sm flex items-center gap-2 flex-wrap">
<span class="text-surface-500">In reply to:</span>
<a class="u-in-reply-to text-xs text-surface-400 dark:text-surface-500 hover:underline break-all" href="{{ replyTo }}">
<span class="text-surface-600">In reply to:</span>
<a class="u-in-reply-to text-xs text-surface-600 dark:text-surface-400 hover:underline break-all" href="{{ replyTo }}">
{{ replyTo | replace("https://", "") | replace("http://", "") | truncate(60) }}
</a>
{% if protocol == "atmosphere" %}
@@ -91,7 +91,7 @@ permalink: "replies/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNum
<div class="e-content prose dark:prose-invert prose-sm mt-3 max-w-none">
{{ post.templateContent | safe }}
</div>
<a class="u-url text-sm text-rose-600 dark:text-rose-400 hover:underline mt-3 inline-block" href="{{ post.url }}">Permalink</a>
<a class="u-url text-sm text-sky-600 dark:text-sky-400 hover:underline mt-3 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or ('Reply from ' + (post.date | dateDisplay)) }}">Permalink</a>
</div>
</div>
</li>
@@ -111,7 +111,7 @@ permalink: "replies/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNum
Previous
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
Previous
</span>
@@ -123,7 +123,7 @@ permalink: "replies/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNum
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
Next
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</span>

View File

@@ -14,7 +14,7 @@ permalink: "reposts/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNum
<h1 class="text-2xl sm:text-3xl font-bold text-surface-900 dark:text-surface-100">Reposts</h1>
{% set sparklineSvg = collections.reposts | postingFrequency %}
{% if sparklineSvg %}
<span class="text-emerald-600 dark:text-emerald-400">{{ sparklineSvg | safe }}</span>
<div class="flex-1 min-w-0 text-green-600 dark:text-green-400">{{ sparklineSvg | safe }}</div>
{% endif %}
</div>
<p class="text-surface-600 dark:text-surface-400 mb-6 sm:mb-8">
@@ -25,21 +25,21 @@ permalink: "reposts/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNum
{% if paginatedReposts.length > 0 %}
<ul class="post-list">
{% for post in paginatedReposts %}
<li class="h-entry post-card border-l-[3px] border-l-rose-400 dark:border-l-rose-500">
<li class="h-entry post-card border-l-[3px] border-l-green-400 dark:border-l-green-500">
<div class="post-header flex items-start gap-3">
<div class="flex-shrink-0 mt-1">
<svg class="w-5 h-5 text-rose-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
</div>
<div class="flex-1 min-w-0">
{% if post.data.title %}
<h2 class="p-name text-lg font-semibold text-surface-900 dark:text-surface-100 mb-2">
<a class="hover:text-rose-600 dark:hover:text-rose-400" href="{{ post.url }}">{{ post.data.title }}</a>
<a class="hover:text-green-600 dark:hover:text-green-400" href="{{ post.url }}">{{ post.data.title }}</a>
</h2>
{% endif %}
<div class="post-meta">
<time class="dt-published" datetime="{{ post.date | isoDate }}">
<time class="dt-published font-mono text-sm" datetime="{{ post.date | isoDate }}">
{{ post.date | dateDisplay }}
</time>
{% if post.data.category %}
@@ -58,7 +58,7 @@ permalink: "reposts/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNum
{% set repostedUrl = post.data.repostOf or post.data.repost_of %}
{% if repostedUrl %}
{% unfurl repostedUrl %}
<a class="u-repost-of text-xs text-surface-400 dark:text-surface-500 hover:underline break-all mt-1 inline-block" href="{{ repostedUrl }}">
<a class="u-repost-of text-xs text-surface-600 dark:text-surface-400 hover:underline break-all mt-1 inline-block" href="{{ repostedUrl }}">
{{ repostedUrl }}
</a>
{% endif %}
@@ -67,7 +67,7 @@ permalink: "reposts/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNum
{{ post.templateContent | safe }}
</div>
{% endif %}
<a class="u-url text-sm text-rose-600 dark:text-rose-400 hover:underline mt-3 inline-block" href="{{ post.url }}">Permalink</a>
<a class="u-url text-sm text-green-600 dark:text-green-400 hover:underline mt-3 inline-block" href="{{ post.url }}" aria-label="Permalink: {{ post.data.title or ('Repost from ' + (post.date | dateDisplay)) }}">Permalink</a>
</div>
</div>
</li>
@@ -87,7 +87,7 @@ permalink: "reposts/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNum
Previous
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
Previous
</span>
@@ -99,7 +99,7 @@ permalink: "reposts/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNum
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</a>
{% else %}
<span class="pagination-link disabled">
<span class="pagination-link disabled" aria-disabled="true">
Next
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</span>

View File

@@ -14,25 +14,28 @@ pagefindIgnore: true
<noscript>
<div class="p-6 bg-surface-100 dark:bg-surface-800 rounded-lg mt-4">
<p class="text-surface-700 dark:text-surface-300">Search requires JavaScript to be enabled. Please enable JavaScript in your browser settings to use the search feature.</p>
<p class="text-surface-500 text-sm mt-2">Alternatively, you can browse content via the <a href="/blog/" class="text-accent-600 dark:text-accent-400 hover:underline">blog archive</a> or <a href="/categories/" class="text-accent-600 dark:text-accent-400 hover:underline">categories</a>.</p>
<p class="text-surface-600 dark:text-surface-400 text-sm mt-2">Alternatively, you can browse content via the <a href="/blog/" class="text-accent-600 dark:text-accent-400 hover:underline">blog archive</a> or <a href="/categories/" class="text-accent-600 dark:text-accent-400 hover:underline">categories</a>.</p>
</div>
</noscript>
<script>
initPagefind("#search", { showSubResults: true });
initPagefind("#search", { showSubResults: true, showEmptyFilters: false });
// Support ?q= query parameter and auto-focus
window.addEventListener("DOMContentLoaded", () => {
const input = document.querySelector("#search input[type='text']");
if (input) {
// Pagefind generates the input without a label — add one
input.setAttribute("aria-label", "Search all content");
}
const params = new URLSearchParams(window.location.search);
const query = params.get("q");
if (query) {
const input = document.querySelector("#search input[type='text']");
if (input) {
input.value = query;
input.dispatchEvent(new Event("input", { bubbles: true }));
}
} else {
const input = document.querySelector("#search input[type='text']");
if (input) input.focus();
}
});

View File

@@ -14,7 +14,7 @@ eleventyImport:
</p>
{# Dynamic pages (created via Indiekit) #}
<div class="mb-8">
<div class="mb-8 pl-4 border-l-4 border-accent-500 dark:border-accent-400">
<h2 class="text-lg font-semibold text-surface-800 dark:text-surface-200 mb-4">Pages</h2>
{% if collections.pages.length > 0 %}
<ul class="post-list">
@@ -33,15 +33,15 @@ eleventyImport:
<p class="text-surface-600 dark:text-surface-400 mt-2">{{ page.data.title }}</p>
{% endif %}
{% if page.data.updated %}
<p class="text-sm text-surface-500 mt-2">
Updated: <time datetime="{{ page.data.updated | isoDate }}">{{ page.data.updated | dateDisplay }}</time>
<p class="text-sm text-surface-600 dark:text-surface-400 mt-2">
Updated: <time class="font-mono text-sm" datetime="{{ page.data.updated | isoDate }}">{{ page.data.updated | dateDisplay }}</time>
</p>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<div class="p-4 bg-surface-100 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700">
<div class="p-4 bg-surface-100 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
<p class="text-surface-600 dark:text-surface-400 text-sm mb-2">
No root pages yet. To create pages like <code>/now</code>, <code>/uses</code>, or <code>/colophon</code>, you need two plugins:
</p>
@@ -49,7 +49,7 @@ eleventyImport:
<li><code>@rmdes/indiekit-post-type-page</code> — registers the "page" post type with Indiekit, using root-level URL paths (<code>/slug</code> instead of <code>/type/YYYY/MM/DD/slug</code>)</li>
<li><code>@rmdes/indiekit-endpoint-posts</code> — publishing UI that sends the <code>h=page</code> Micropub type so pages are created at root level</li>
</ul>
<p class="text-surface-500 dark:text-surface-500 text-xs mt-3">
<p class="text-surface-600 dark:text-surface-400 text-xs mt-3">
Once both plugins are installed, "Page" appears as a post type in the Indiekit admin UI, and pages are published directly at <code>/slug</code>.
</p>
</div>
@@ -65,74 +65,74 @@ eleventyImport:
(blogrollStatus and blogrollStatus.source == "indiekit") or
(podrollStatus and podrollStatus.source == "indiekit") %}
{% if hasActivityPages %}
<div class="mb-8">
<div class="mb-8 pl-4 border-l-4 border-emerald-500 dark:border-emerald-400">
<h2 class="text-lg font-semibold text-surface-800 dark:text-surface-200 mb-4">Activity Feeds</h2>
<ul class="post-list">
{% if blogrollStatus and blogrollStatus.source == "indiekit" %}
<li class="post-card">
<li class="h-entry post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/blogroll/" class="text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/blogroll</a>
<a href="/blogroll/" class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/blogroll</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Sites I follow</p>
</li>
{% endif %}
{% if funkwhaleActivity and funkwhaleActivity.source == "indiekit" %}
<li class="post-card">
<li class="h-entry post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/funkwhale/" class="text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/funkwhale</a>
<a href="/funkwhale/" class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/funkwhale</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Funkwhale activity</p>
</li>
{% endif %}
{% if githubActivity and githubActivity.source != "error" %}
<li class="post-card">
<li class="h-entry post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/github/" class="text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/github</a>
<a href="/github/" class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/github</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">GitHub activity</p>
</li>
{% endif %}
{% if lastfmActivity and lastfmActivity.source == "indiekit" %}
<li class="post-card">
<li class="h-entry post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/listening/" class="text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/listening</a>
<a href="/listening/" class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/listening</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Last.fm scrobbles</p>
</li>
{% endif %}
{% if newsActivity and newsActivity.source == "indiekit" %}
<li class="post-card">
<li class="h-entry post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/news/" class="text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/news</a>
<a href="/news/" class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/news</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">RSS feed aggregator</p>
</li>
{% endif %}
{% if podrollStatus and podrollStatus.source == "indiekit" %}
<li class="post-card">
<li class="h-entry post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/podroll/" class="text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/podroll</a>
<a href="/podroll/" class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/podroll</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Podcasts I listen to</p>
</li>
{% endif %}
{% if youtubeChannel and youtubeChannel.source == "indiekit" %}
<li class="post-card">
<li class="h-entry post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/youtube/" class="text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/youtube</a>
<a href="/youtube/" class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/youtube</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">YouTube channel</p>
@@ -142,8 +142,98 @@ eleventyImport:
</div>
{% endif %}
{# Site pages — theme-provided .njk pages not in collections.pages or activity feeds #}
<div class="mb-8 pl-4 border-l-4 border-surface-300 dark:border-surface-600">
<h2 class="text-lg font-semibold text-surface-800 dark:text-surface-200 mb-4">Site Pages</h2>
<p class="text-surface-600 dark:text-surface-400 text-sm mb-4">
Theme-provided pages for content aggregation, search, and site info.
</p>
<ul class="post-list">
<li class="h-entry post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/blog/" class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/blog</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">All posts chronologically</p>
</li>
<li class="h-entry post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/cv/" class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/cv</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Curriculum vitae</p>
</li>
<li class="h-entry post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/changelog/" class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/changelog</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Site changes and updates</p>
</li>
<li class="h-entry post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/digest/" class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/digest</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Content digest</p>
</li>
<li class="h-entry post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/featured/" class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/featured</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Featured posts</p>
</li>
<li class="h-entry post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/graph/" class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/graph</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Content graph visualization</p>
</li>
<li class="h-entry post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/interactions/" class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/interactions</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Social interactions (likes, reposts, replies)</p>
</li>
<li class="h-entry post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/readlater/" class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/readlater</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Read later queue</p>
</li>
<li class="h-entry post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/search/" class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/search</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Full-text search</p>
</li>
<li class="h-entry post-card">
<div class="post-header">
<h3 class="text-xl font-semibold">
<a href="/github/starred/" class="p-name u-url text-surface-900 dark:text-surface-100 hover:text-accent-600 dark:hover:text-accent-400">/github/starred</a>
</h3>
</div>
<p class="text-surface-600 dark:text-surface-400 mt-2">Starred GitHub repositories</p>
</li>
</ul>
</div>
{# Inspiration section #}
<div class="mt-8 p-4 bg-surface-100 dark:bg-surface-800 rounded-lg">
<div class="mt-8 p-4 bg-surface-100 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
<h2 class="text-lg font-semibold text-surface-800 dark:text-surface-200 mb-2">Want more slash pages?</h2>
<p class="text-surface-600 dark:text-surface-400 text-sm">
Check out <a href="https://slashpages.net" class="text-accent-600 dark:text-accent-400 hover:underline" target="_blank" rel="noopener">slashpages.net</a>

View File

@@ -24,7 +24,7 @@ eleventyExcludeFromCollections: true
<span>
<span x-text="totalCount"></span> repos starred on GitHub.
<template x-if="lastSync">
<span>Last synced <span x-text="formatDate(lastSync)"></span>.</span>
<span>Last synced <span class="font-mono" x-text="formatDate(lastSync)"></span>.</span>
</template>
</span>
</template>
@@ -37,8 +37,8 @@ eleventyExcludeFromCollections: true
{# Loading state #}
<template x-if="loading">
<div class="text-center py-12">
<div class="inline-block w-8 h-8 border-4 border-accent-200 border-t-accent-600 rounded-full animate-spin"></div>
<p class="mt-4 text-surface-500">Loading starred repositories&hellip;</p>
<div class="inline-block w-8 h-8 border-4 border-emerald-200 border-t-emerald-600 rounded-full animate-spin"></div>
<p class="mt-4 text-surface-600 dark:text-surface-400">Loading starred repositories&hellip;</p>
</div>
</template>
@@ -53,13 +53,15 @@ eleventyExcludeFromCollections: true
{# ===== TAB BAR ===== #}
<div class="mb-4 -mx-4 px-4 overflow-x-auto scrollbar-thin">
<div class="flex gap-1 min-w-max border-b border-surface-200 dark:border-surface-700">
<div class="flex gap-1 min-w-max border-b border-surface-200 dark:border-surface-700" role="tablist" aria-label="Starred repository lists">
{# All tab #}
<button
@click="activeTab = 'all'; resetView()"
:class="activeTab === 'all'
? 'border-accent-600 text-accent-700 dark:text-accent-400'
: 'border-transparent text-surface-500 hover:text-surface-700 dark:hover:text-surface-300 hover:border-surface-300'"
? 'border-emerald-600 text-emerald-700 dark:text-emerald-400'
: 'border-transparent text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300 hover:border-surface-300'"
:aria-selected="(activeTab === 'all').toString()"
role="tab"
class="px-3 py-2 text-sm font-medium border-b-2 whitespace-nowrap transition-colors"
>
All
@@ -71,8 +73,10 @@ eleventyExcludeFromCollections: true
<button
@click="activeTab = list.slug; resetView()"
:class="activeTab === list.slug
? 'border-accent-600 text-accent-700 dark:text-accent-400'
: 'border-transparent text-surface-500 hover:text-surface-700 dark:hover:text-surface-300 hover:border-surface-300'"
? 'border-emerald-600 text-emerald-700 dark:text-emerald-400'
: 'border-transparent text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300 hover:border-surface-300'"
:aria-selected="(activeTab === list.slug).toString()"
role="tab"
class="px-3 py-2 text-sm font-medium border-b-2 whitespace-nowrap transition-colors"
>
<span x-text="list.name"></span>
@@ -84,8 +88,10 @@ eleventyExcludeFromCollections: true
<button
@click="activeTab = 'uncategorized'; resetView()"
:class="activeTab === 'uncategorized'
? 'border-accent-600 text-accent-700 dark:text-accent-400'
: 'border-transparent text-surface-500 hover:text-surface-700 dark:hover:text-surface-300 hover:border-surface-300'"
? 'border-emerald-600 text-emerald-700 dark:text-emerald-400'
: 'border-transparent text-surface-600 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-300 hover:border-surface-300'"
:aria-selected="(activeTab === 'uncategorized').toString()"
role="tab"
class="px-3 py-2 text-sm font-medium border-b-2 whitespace-nowrap transition-colors"
>
Uncategorized
@@ -108,7 +114,7 @@ eleventyExcludeFromCollections: true
x-model="searchQuery"
@input="resetView()"
placeholder="Search by name, description, topic, or language..."
class="w-full pl-10 pr-10 py-2 rounded-lg border border-surface-300 dark:border-surface-600 bg-surface-50 dark:bg-surface-800 text-surface-900 dark:text-surface-100 placeholder-surface-400 text-sm focus:outline-none focus:ring-2 focus:ring-accent-500 focus:border-transparent"
class="w-full pl-10 pr-10 py-2 rounded-lg border border-surface-300 dark:border-surface-600 bg-surface-50 dark:bg-surface-800 text-surface-900 dark:text-surface-100 placeholder-surface-400 text-sm"
>
<template x-if="searchQuery">
<button @click="searchQuery = ''; resetView()" class="absolute right-3 top-1/2 -translate-y-1/2 text-surface-400 hover:text-surface-600">
@@ -123,7 +129,7 @@ eleventyExcludeFromCollections: true
<select
x-model="sortBy"
@change="resetView()"
class="px-3 py-2 rounded-lg border border-surface-300 dark:border-surface-600 bg-surface-50 dark:bg-surface-800 text-surface-900 dark:text-surface-100 text-sm focus:outline-none focus:ring-2 focus:ring-accent-500"
class="px-3 py-2 rounded-lg border border-surface-300 dark:border-surface-600 bg-surface-50 dark:bg-surface-800 text-surface-900 dark:text-surface-100 text-sm"
>
<option value="stars">Sort: Stars</option>
<option value="starredAt">Sort: Recently Starred</option>
@@ -138,7 +144,7 @@ eleventyExcludeFromCollections: true
<select
x-model="languageFilter"
@change="resetView()"
class="px-2 py-1.5 rounded border border-surface-300 dark:border-surface-600 bg-surface-50 dark:bg-surface-800 text-surface-900 dark:text-surface-100 text-xs focus:outline-none focus:ring-2 focus:ring-accent-500"
class="px-2 py-1.5 rounded border border-surface-300 dark:border-surface-600 bg-surface-50 dark:bg-surface-800 text-surface-900 dark:text-surface-100 text-xs"
>
<option value="">All Languages</option>
<template x-for="lang in availableLanguages" :key="lang">
@@ -152,7 +158,7 @@ eleventyExcludeFromCollections: true
<button
@click="starCountMin = opt.value; resetView()"
:class="starCountMin === opt.value
? 'bg-accent-600 text-white'
? 'bg-emerald-600 text-white'
: 'bg-surface-50 dark:bg-surface-800 text-surface-600 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-700'"
class="px-2.5 py-1 text-xs font-medium transition-colors"
x-text="opt.label"
@@ -162,7 +168,7 @@ eleventyExcludeFromCollections: true
{# Archived toggle #}
<label class="flex items-center gap-1.5 text-xs text-surface-600 dark:text-surface-400 cursor-pointer">
<input type="checkbox" x-model="showArchived" @change="resetView()" class="rounded border-surface-300 text-accent-600 focus:ring-accent-500">
<input type="checkbox" x-model="showArchived" @change="resetView()" class="rounded border-surface-300 text-emerald-600">
Show archived
</label>
@@ -170,21 +176,21 @@ eleventyExcludeFromCollections: true
<template x-if="hasActiveFilters">
<button
@click="clearFilters()"
class="text-xs text-accent-600 dark:text-accent-400 hover:underline"
class="text-xs text-emerald-600 dark:text-emerald-400 hover:underline"
>Clear filters</button>
</template>
</div>
</div>
{# ===== RESULTS SUMMARY ===== #}
<div class="mb-4 text-sm text-surface-500">
<div class="mb-4 text-sm text-surface-600 dark:text-surface-400">
<span x-text="resultSummary"></span>
</div>
{# ===== REPO GRID ===== #}
<div class="grid gap-3 sm:gap-4 md:grid-cols-2" id="starred-grid">
<template x-for="repo in visibleStars" :key="repo.fullName">
<article class="starred-card p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-surface-400 dark:hover:border-surface-500 transition-colors">
<article class="starred-card p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 hover:border-emerald-400 dark:hover:border-emerald-600 transition-colors shadow-sm">
<div class="flex items-center gap-2 mb-1">
<template x-if="repo.ownerAvatar">
<img :src="repo.ownerAvatar" :alt="repo.ownerLogin" class="w-5 h-5 rounded-full" loading="lazy">
@@ -207,12 +213,12 @@ eleventyExcludeFromCollections: true
<span class="text-xs px-2 py-0.5 bg-surface-100 dark:bg-surface-700 text-surface-700 dark:text-surface-300 rounded" x-text="topic"></span>
</template>
<template x-if="repo.topics.length > 5">
<span class="text-xs px-2 py-0.5 bg-surface-100 dark:bg-surface-700 text-surface-500 rounded" x-text="'+' + (repo.topics.length - 5)"></span>
<span class="text-xs px-2 py-0.5 bg-surface-100 dark:bg-surface-700 text-surface-600 dark:text-surface-400 rounded" x-text="'+' + (repo.topics.length - 5)"></span>
</template>
</div>
</template>
<div class="flex flex-wrap items-center gap-3 text-xs text-surface-500">
<div class="flex flex-wrap items-center gap-3 text-xs text-surface-600 dark:text-surface-400">
<template x-if="repo.language">
<span class="flex items-center gap-1">
<span class="w-2.5 h-2.5 rounded-full" :style="'background:' + languageColor(repo.language)"></span>
@@ -233,7 +239,7 @@ eleventyExcludeFromCollections: true
<span x-text="repo.license"></span>
</template>
<template x-if="repo.starredAt">
<span x-text="'Starred ' + formatDate(repo.starredAt)"></span>
<span class="font-mono" x-text="'Starred ' + formatDate(repo.starredAt)"></span>
</template>
</div>
</article>
@@ -243,8 +249,8 @@ eleventyExcludeFromCollections: true
{# ===== EMPTY FILTERED STATE ===== #}
<template x-if="sortedStars.length === 0">
<div class="text-center py-12">
<p class="text-surface-500">No repos match your current filters.</p>
<button @click="clearFilters()" class="mt-2 text-sm text-accent-600 dark:text-accent-400 hover:underline">Clear all filters</button>
<p class="text-surface-600 dark:text-surface-400">No repos match your current filters.</p>
<button @click="clearFilters()" class="mt-2 text-sm text-emerald-600 dark:text-emerald-400 hover:underline">Clear all filters</button>
</div>
</template>
@@ -253,10 +259,10 @@ eleventyExcludeFromCollections: true
<div class="mt-6 text-center">
<button
@click="visibleCount = Math.min(visibleCount + 50, sortedStars.length)"
class="px-6 py-2.5 bg-accent-600 hover:bg-accent-700 text-white rounded-lg transition-colors"
class="px-6 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg transition-colors"
>
Load More
<span class="text-accent-200" x-text="'(' + (sortedStars.length - visibleCount) + ' remaining)'"></span>
<span class="text-emerald-200" x-text="'(' + (sortedStars.length - visibleCount) + ' remaining)'"></span>
</button>
</div>
</template>

View File

@@ -41,20 +41,6 @@ export default {
900: "#78350f",
950: "#451a03",
},
// Legacy — kept for compatibility, not used in templates
primary: {
50: "#eff6ff",
100: "#dbeafe",
200: "#bfdbfe",
300: "#93c5fd",
400: "#60a5fa",
500: "#3b82f6",
600: "#2563eb",
700: "#1d4ed8",
800: "#1e40af",
900: "#1e3a8a",
950: "#172554",
},
},
fontFamily: {
sans: [

View File

@@ -52,13 +52,13 @@ pagefindIgnore: true
{{ post.url }}
</a>
</td>
<td class="p-2 text-xs text-surface-500 font-mono">
<td class="p-2 text-xs text-surface-600 dark:text-surface-400 font-mono">
{% if legacyUrls.length %}
{% for legacyUrl in legacyUrls %}
<div>{{ legacyUrl }}</div>
{% endfor %}
{% else %}
<span class="text-surface-400">-</span>
<span class="text-surface-600 dark:text-surface-400">-</span>
{% endif %}
</td>
<td class="p-2 text-right">
@@ -67,7 +67,7 @@ pagefindIgnore: true
{{ allMentions.length }}
</span>
{% else %}
<span class="text-surface-400">0</span>
<span class="text-surface-600 dark:text-surface-400">0</span>
{% endif %}
</td>
</tr>
@@ -94,7 +94,7 @@ pagefindIgnore: true
{% for newUrl, oldUrls in aliasEntries | head(20) %}
<tr>
<td class="p-2 text-xs break-all">{{ newUrl }}</td>
<td class="p-2 text-xs break-all text-surface-500">
<td class="p-2 text-xs break-all text-surface-600 dark:text-surface-400">
{% for oldUrl in oldUrls %}
<div>{{ oldUrl }}</div>
{% endfor %}
@@ -115,7 +115,7 @@ pagefindIgnore: true
<ul class="space-y-1 font-mono text-xs">
{% for wm in webmentions | head(30) %}
<li class="p-2 bg-surface-50 dark:bg-surface-800/50 rounded">
<span class="text-surface-500">{{ wm["wm-property"] }}:</span>
<span class="text-surface-600 dark:text-surface-400">{{ wm["wm-property"] }}:</span>
<span class="break-all">{{ wm["wm-target"] }}</span>
</li>
{% endfor %}

View File

@@ -15,11 +15,13 @@ withSidebar: true
{# Multi-channel tabs #}
{% if youtubeChannel.isMultiChannel and youtubeChannel.channels.length > 1 %}
<div class="mb-6">
<div class="flex flex-wrap gap-2 border-b border-surface-200 dark:border-surface-700">
<div class="flex flex-wrap gap-2 border-b border-surface-200 dark:border-surface-700" role="tablist" aria-label="YouTube channels">
{% for channel in youtubeChannel.channels %}
<button
@click="activeChannel = {{ loop.index0 }}"
:class="activeChannel === {{ loop.index0 }} ? 'border-red-500 text-red-600 dark:text-red-400' : 'border-transparent text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-surface-100'"
:aria-selected="(activeChannel === {{ loop.index0 }}).toString()"
role="tab" id="yt-tab-{{ loop.index0 }}" aria-controls="yt-panel-{{ loop.index0 }}"
class="px-4 py-2 font-medium border-b-2 -mb-px transition-colors"
>
{{ channel.configName or channel.title }}
@@ -31,7 +33,7 @@ withSidebar: true
{# Channel sections #}
{% for channel in youtubeChannel.channels %}
<div x-show="activeChannel === {{ loop.index0 }}" {% if not loop.first %}x-cloak{% endif %}>
<div x-show="activeChannel === {{ loop.index0 }}" {% if not loop.first %}x-cloak{% endif %} role="tabpanel" id="yt-panel-{{ loop.index0 }}" aria-labelledby="yt-tab-{{ loop.index0 }}">
{# Channel Header #}
<section class="mb-8">
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-4 sm:gap-5 p-4 sm:p-6 bg-gradient-to-br from-red-500/10 to-red-600/5 dark:from-red-900/20 dark:to-red-800/10 rounded-xl sm:rounded-2xl border border-red-500/20">
@@ -57,7 +59,7 @@ withSidebar: true
</a>
</h2>
{% if channel.customUrl %}
<p class="text-sm text-surface-500">{{ channel.customUrl }}</p>
<p class="text-sm text-surface-600 dark:text-surface-400">{{ channel.customUrl }}</p>
{% endif %}
<div class="flex flex-wrap items-center gap-4 mt-2 text-sm text-surface-600 dark:text-surface-400">
<span class="flex items-center gap-1">
@@ -173,7 +175,7 @@ withSidebar: true
{% if channelVideos and channelVideos.length %}
<div class="grid gap-4 sm:gap-6 sm:grid-cols-2 lg:grid-cols-3">
{% for video in channelVideos | head(9) %}
<article class="group bg-surface-50 dark:bg-surface-800 rounded-xl overflow-hidden border border-surface-200 dark:border-surface-700 hover:border-red-400 dark:hover:border-red-600 transition-colors">
<article class="group bg-surface-50 dark:bg-surface-800 rounded-xl overflow-hidden border border-surface-200 dark:border-surface-700 hover:border-red-400 dark:hover:border-red-600 transition-colors shadow-sm">
<a href="{{ video.url }}" class="block" target="_blank" rel="noopener">
<div class="relative aspect-video">
{% if video.thumbnail %}
@@ -220,7 +222,7 @@ withSidebar: true
{{ video.title }}
</a>
</h3>
<div class="flex items-center gap-3 text-sm text-surface-500">
<div class="flex items-center gap-3 text-sm text-surface-600 dark:text-surface-400">
<span class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>