Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
94ee084
chore(nix): make chrome-devtools shell evaluate on Darwin
srid May 3, 2026
830abcb
feat(ui): attach panels to a single card; right-margin TOC + backlinks
srid May 3, 2026
218d8a3
docs(agency): seed code-police project rules from #672 follow-ups
srid May 3, 2026
4f5b07e
feat(ui): unify wikilink + title + backlinks under a single primary p…
srid May 3, 2026
383a6ea
feat(ui): tone external links + label and anchor footnote popups
srid May 3, 2026
4491c0c
feat(ui): swap Space Grotesk for Mona Sans; widen sans-font selector
srid May 3, 2026
666ad46
feat(ui): emerald theme; deemphasize note-link chips; calmer heading …
srid May 4, 2026
df3d283
feat(ui): timeline backlinks render as a year heatmap in the right panel
srid May 4, 2026
4360666
feat(ui): timeline heatmap also renders attached at bottom on <lg
srid May 4, 2026
aac7e36
feat(ui): timeline cells get the same hover-context flyout as backlinks
srid May 4, 2026
232a679
fix(ui): drop native title= on timeline cells; rich flyout is enough
srid May 4, 2026
9e077e0
fix(ui): fold footer into the card; update e2e selectors to new chrome
srid May 4, 2026
04e227d
fix(ui): timeline cells fill bottom-strip width at <lg
srid May 4, 2026
ad36e83
fix(e2e): scope #backlinks-bottom to the regular-backlinks section
srid May 4, 2026
a6c2dfe
back to blue
srid May 4, 2026
507388e
feat(ui): special pages adopt the card chrome; in-prose task alignmen…
srid May 4, 2026
1343c26
docs: CHANGELOG entry for the #699 default theme refresh
srid May 4, 2026
96f13a6
docs + chore: bump 1.6 → 2.0; refresh html-template docs; tighten cod…
srid May 4, 2026
2b16d20
feat(ui): coordinated syntax palette + framed prose images + sidebar …
srid May 4, 2026
7a5ab6e
fix(ui): syntax highlighting falls back to hex; var() refs were unres…
srid May 4, 2026
9372c1a
fix(ui): backlinks-bottom rows separated; context indented under its …
srid May 4, 2026
33a835c
fix(ui): apply the row-separated layout to the no-sidebar backlinks c…
srid May 4, 2026
3e794dd
fix(ui): drop sidebar depth rails + replace leaf file icon with a dot
srid May 4, 2026
6065452
fix(ui): override heist-extra's heavy table cell borders
srid May 4, 2026
562b837
update changelog
srid May 4, 2026
184ab7a
..
srid May 4, 2026
0623205
feat(ui): metadata tag chips adopt the primary chip palette
srid May 4, 2026
9a76ffa
fix(ui): refine task checkbox + scope marker-slot rule + drop tips/js.md
srid May 4, 2026
ac7fcec
docs: refresh fonts.md + daily-notes.md for the #699 visual language
srid May 4, 2026
cf81590
fix mcp link
srid May 4, 2026
c59c606
fix(ui): special-page chip language + invalid Tailwind in DT/error ba…
srid May 4, 2026
dab4caf
fix(ui): drop bg-fill from /-/all chip rest state
srid May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .agency/code-police.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# /code-police — project rules

Project-specific additions to the built-in rule set in `.claude/skills/code-police/SKILL.md`. Treat each rule below as an extra row in Pass 1's checklist.

The patterns here came out of [#672](https://github.com/srid/emanote/pull/672) (Stork ES-module migration), where seven follow-up commits — three from `/code-police`, four from `/hickey` + `/lowy` — landed on top of the primary feature. Capture: same shape of mistake, same kind of fix, made into a recurring check.

## emanote-tailwind-first

When styling Heist templates or JS-generated DOM, prefer **inline Tailwind utility classes** over CSS rules in `styles.tpl`. The grandfathered CSS in that file lives there because the rule it expresses doesn't fit the utility-class shape (e.g. complex `@keyframes`, `:popover-open` pseudo-states, or selectors that target Pandoc-emitted HTML with no class hook). Everything else belongs as `class="…"` in the template.

A new CSS rule in `styles.tpl` is only acceptable when:

- The selector targets an element Pandoc / a vendor library emits without classes (`main table`, `pre`, `kbd`, `:popover-open` modifiers). Add a one-line comment explaining the no-class-hook constraint.
- The rule needs `@keyframes` or other at-rule constructs Tailwind doesn't express inline.
- The same property repeats across a CSS-property family that Tailwind would force you to spell as one arbitrary value per case (e.g. complex `font-feature-settings` runs).

If you find yourself writing `.foo { color: …; padding: …; }` in CSS where `<div class="text-… p-…">` would do the job, the template is the right home — utility classes are scanned by Tailwind's JIT and tree-shaken with the rest of the build, while raw CSS in `styles.tpl` ships unconditionally.

> _Rationale_: keeping styling in templates means a reader reviewing one component sees its full styling at the call site rather than having to cross-reference a separate `styles.tpl` `data-category` block. It also makes the surface visible to `/code-police`'s rule-of-three reviews — three sites of the same Tailwind class string become a candidate for `<bind>` or extraction; three CSS rules in `styles.tpl` are invisible.

## emanote-no-one-field-options-bag

A function that takes a single-key options object — `fn(x, { onlyKey: value })` — is an options-bag larp dressed as future-proofing. Replace with a positional parameter (`fn(x, onlyKey = default)`). Threshold: **if at most one of N call sites actually passes anything, the bag has no users to justify it.**

Future-proofing argument ("we might add more keys later") is rejected — add the bag *when* the second key arrives, not before.

> _From #672_: `registerIndex({ forceOverwrite: true })` had two callers; only one passed anything → replaced with `registerIndex(forceOverwrite = false)`.

## emanote-dom-over-mirror

When UI state is already represented in the DOM (a body class, an `aria-*` attribute, an element's presence), **query the DOM** rather than maintaining a parallel JS variable that "should track" it. Mirror flags drift when anything outside the owning module mutates the DOM (devtools, utility scripts, future code), leaving handlers reacting to stale state — usually as a silent no-op that's invisible in review.

The `classList.contains` cost on a hot path is irrelevant; correctness wins. Cache only if a profile shows a real bottleneck.

> _From #672_: `searchShown` mirrored `body.stork-overflow-hidden-important`. Esc-handler silently no-op'd if the class was toggled by anything outside the module. Replaced with `isSearchShown()` querying the class directly.

## emanote-vendor-global-guard

An ES module that depends on a vendor global defined by a separate `<script src>` (CDN, inline `<script>` in a template, etc.) **must guard at module evaluation**:

```js
if (typeof vendorGlobal === 'undefined') {
throw new Error(`vendorGlobal is missing — expected from <some-template>.tpl`);
}
```

Without the guard, a future template tweak that reorders or removes the dependency surfaces as a cryptic `X is not defined` on the user's first interaction — far from the actual cause. The error message must name the template/script that should provide it, so the failure mode is self-documenting.

> _From #672_: `stork.js` assumed `window.stork` from `stork-search-head.tpl`'s vendor `<script src>`. Added a guard pointing at that template.

## Calibration for built-in rules

These are not new rules — they're concrete examples for built-in rules that come up often in this codebase. Use them when judging borderline cases.

### `no-silent-error-swallowing` — URL/parse failures must log

The bare `catch {}` form is already disallowed, but a *named* catch that returns a fallback value is just as silent if it doesn't log. URL parsing, `JSON.parse`, attribute decoding — these all have valid fallbacks (root path, empty object, "off") *and* a real diagnostic story (`console.warn` with the offending input + the consequence). Both are required.

> _From #672_: `getBaseUrl()` caught a malformed `document.baseURI` and degraded to `/` — silently. Fix: `console.warn` with the bad URL + "search path resolution falling back to /".

### `dry-rule-of-three` — string literals across hot-path methods

The threshold is three; `'stork-overflow-hidden-important'` appearing across `toggleSearch` / `clearSearch` / `isSearchShown` is exactly that case. Extract to a `const MODAL_HIDDEN_CLASS = '...';` at module top so a typo in one of the three sites can't silently desync the open-state machinery.
4 changes: 2 additions & 2 deletions docs/_redirects
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
# Tips
/tips/adding-images /adding-images 301
/tips/js /js 301
/tips/js/math /math 301
/tips/js/mermaid /mermaid 301
/tips/js/math /tips/math 301
/tips/js/mermaid /tips/mermaid 301
/tips/js/syntax-highlighting /syntax-highlighting 301
/tips/sync /sync 301
32 changes: 0 additions & 32 deletions docs/architecture.md

This file was deleted.

9 changes: 5 additions & 4 deletions docs/guide/daily-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ Emanote has special support for [Obsidian style daily notes](https://help.obsidi

If you create notes named `YYYY-MM-DD.md`, Emanote will treat them as daily notes. This has a couple of effects:

1. The backlinks panel will render daily notes separate from regular notes. The daily notes will be rendered as a "timeline" in reverse chronological order.
1. The backlinks panel will render daily notes separate from regular notes. Daily notes render as a year-stacked **timeline heatmap** (see [[backlinks#timeline-backlinks]]); regular notes render as a "Linked from" chip list.
2. Each daily note automatically gets a hierarchical tag (eg: `#calendar/2025/03`) allowing you to browse them by calendar navigation in the tag index.

## Timeline backlinks demo

The sample notes below all link back to this page. Notes whose filenames begin
with `YYYY-MM-DD` show up in the Timeline panel at the end of this page, while
the ordinary backlink from the [[obsmd|Obsidian]] note shows up in the regular
"Links to this page" panel.
with `YYYY-MM-DD` show up in the timeline heatmap (in the [[right-panel]] at
`lg:`+ widths, or the bottom strip at narrower widths), while the ordinary
backlink from the [[obsmd|Obsidian]] note shows up in the "Linked from" chip
list alongside it.

```query
path:./*
Expand Down
33 changes: 33 additions & 0 deletions docs/guide/html-template/backlinks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
slug: backlinks
---

# Backlinks

In [[html-template|the default template]], backlinks split into two flavors based on whether the linking note's filename matches an `YYYY-MM-DD[-…]` daily-note pattern:

- **Regular backlinks** — non-daily notes that reference the current page.
- **Timeline backlinks** — daily notes (filenames like `2025-03-14.md`) that reference the current page.

The split is computed in `Calendar.parseRouteDay` and exposed to templates as the `<ema:note:backlinks:nodaily>` / `<ema:note:backlinks:daily>` Heist splices respectively.

## Regular backlinks

Render as a slim labelled list of "tiny title chips" — same chip language as wiki-links — in the right-panel column at `lg:` and above (`#backlinks-margin`), or stacked at the bottom of the card at narrower widths (`#backlinks-bottom`).

Each chip is just the linking note's title; **hovering or focusing** a chip opens a flyout to the left over the prose column with the rendered context paragraphs (the prose around where this note is referenced). The flyout shares typography with the timeline heatmap's cell-hover popup, so all "side material" reads as one family.

On `<lg` (when the right-panel is hidden), the bottom strip carries the same data. Touch devices skip the flyout entirely and see the contexts inlined under the chip.

## Timeline backlinks

Render as a **year-stacked heatmap**: 12 rows per year (one per month), each row 31 cells wide. Cells with a daily backlink are filled in the primary palette and clickable; empty cells are gray. Years stack newest-first.

Hovering a filled cell opens a context flyout (header `YYYY-MM-DD — title`, then the rendered backlink context paragraphs). Same flyout chrome as regular-backlink chips. The heatmap renders in two homes:

- Right-panel at `lg:`+ (compact 4×4 cells in the narrow column)
- Bottom strip at `<lg` (cells stretch as horizontal bars to fill the wider row)

Dates are parsed out of each linked note's title via a `YYYY-MM-DD` regex; entries without a parseable date are silently skipped (with the screen-reader fallback list still readable).

See [[daily-notes]] for how the daily/non-daily split feeds these two surfaces.
4 changes: 2 additions & 2 deletions docs/guide/html-template/fonts.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ slug: fonts
Emanote ships with three self-hosted fonts served out of the bundled static layer (no third-party fetch at page load):

- [Lora](https://fonts.google.com/specimen/Lora) — prose
- [Space Grotesk](https://fonts.google.com/specimen/Space+Grotesk) — UI chrome (sidebar, breadcrumbs, TOC, backlinks) and headings
- [Mona Sans](https://github.com/github/mona-sans) — UI chrome (sidebar, right-panel, breadcrumbs, backlinks) and headings
- [Space Mono](https://fonts.google.com/specimen/Space+Mono) — inline code and code blocks

The woff2 files plus a generated `fonts.css` live under `_emanote-static/fonts/`, and `templates/styles.tpl` links them via `<emanoteStaticUrl path="fonts/fonts.css">…</emanoteStaticUrl>`. Generated static sites therefore work fully offline.

The theme colour (set via `template.theme` — see [[yaml-config|YAML configuration]]) shows up in the note title, wikilinks, TOC accents, and backlink cards rather than in full-bleed body backgrounds.
The theme colour (set via `template.theme` — see [[yaml-config|YAML configuration]]) drives the unified chip language used by the page title, wikilinks, [[backlinks]], query results, the [[right-panel|timeline heatmap]], and tags — every "linked note" surface reads as one family. The [[toc|table of contents]] sits deliberately outside this palette in neutral gray, since TOC entries map to plain-prose headings rather than other notes.

## Changing the Font Family

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/html-template/neuron-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ In the absence of [[sidebar]], you may use the [[query|folgezettel children quer

```query
children:.
```
```
23 changes: 23 additions & 0 deletions docs/guide/html-template/right-panel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
slug: right-panel
---

# Right panel

In [[html-template|the default template]], the right-panel is the column attached to the right edge of the main card at `lg:` (≥1024px) and above. It mirrors the [[sidebar]]'s chrome (gray background, sticky-scrolling, matching width tiers — `lg:w-52` / `xl:w-72`) so the two read as a symmetric pair.

The panel hosts three kinds of "side material", stacked top-to-bottom:

1. [[toc|Table of contents]] — neutral-gray hierarchy of the current page's headings, with scroll-spy highlighting the section in view.
2. **Timeline heatmap** — year-stacked grid of cells where filled days are [[backlinks|daily-note backlinks]] to the current page (see [[backlinks#timeline-backlinks]]).
3. **Linked from** — slim chip list of regular [[backlinks|backlinks]] (non-daily notes that reference the current page); each chip opens a context flyout on hover.

Below `lg:`, the right-panel hides and a footer-attached strip at the bottom of the card carries the timeline heatmap + regular backlinks list (the TOC is omitted at narrow widths, since the prose dominates).

## Why this column exists

Pre-#699 the default theme rendered TOC as a side column nested inside the prose body and pushed backlinks below the article as a separate floating card. The right-panel consolidates both into one attached column inside the same `#container` card as the sidebar and prose, so the page reads as a single composed unit instead of stacked stripes.

## Disabling

The TOC and backlinks each have their own enable knobs (see [[toc]] and [[backlinks]]). The right-panel renders only when there's content for at least one of them — pages with no TOC and no backlinks omit it entirely (no empty gray column).
10 changes: 8 additions & 2 deletions docs/guide/html-template/toc.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ slug: toc

# Table of contents

In [[html-template|the default template]], the table of contents is rendered on the right side. TOC is rendered only if the page has more than one heading.
In [[html-template|the default template]], the table of contents lives in the right-panel column at `lg:` (≥1024px) breakpoint and above, alongside [[backlinks]] and the timeline heatmap. Below that breakpoint the right-panel hides entirely and the TOC is omitted to keep the prose centered.

A TOC is rendered only when a page has more than one heading.

## Visual treatment

TOC entries map directly to headings in the prose, which render in plain text — so the TOC stays in a **neutral gray palette** rather than the primary chip language. The currently-visible heading (tracked via `IntersectionObserver` in `_emanote-static/js/toc-spy.js`) is highlighted with a subtle gray background and bold weight. Reserving the primary palette for "this links to another note" affordances keeps the visual semantics tight: primary = navigation between notes, gray = navigation within a note.

## Disabling the ToC

Expand All @@ -14,4 +20,4 @@ In [[yaml-config]],
template:
toc:
enable: false
```
```
2 changes: 1 addition & 1 deletion docs/guide/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ slug: mcp
> [!warning] Work in progress
> MCP support is rolling out in phases ([#645](https://github.com/srid/emanote/issues/645)). The current release ships only the HTTP transport and the lifecycle handshake — resources, tools, and subscriptions arrive in later PRs. Expect the surface to grow and the wire details to shift until this notice is removed.

Emanote can expose an [MCP (Model Context Protocol)](https://modelcontextprotocol.io) endpoint beside its [[live-server|live server]], so that [Claude Code](https://claude.com/claude-code), [Codex](https://github.com/openai/codex), or any other MCP-aware client can query your notebook directly from the same process that renders it.
Emanote can expose an [MCP (Model Context Protocol)](https://modelcontextprotocol.io) endpoint beside its [live server](https://ema.srid.ca/topics/live-server), so that [Claude Code](https://claude.com/claude-code), [Codex](https://github.com/openai/codex), or any other MCP-aware client can query your notebook directly from the same process that renders it.

Enable it by passing `--mcp-port PORT` to `emanote run`:

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/orgmode.org
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ See [[file:../tips/syntax-highlighting.md][Syntax Highlighting]] for general inf

*** LaTeX

See [[file:../tips/js/math.md][Math]] for general information.
See [[file:../tips/math.md][Math]] for general information.

The radius of the sun is R_sun = 6.96 x 10^8 m. On the other hand,
the radius of Alpha Centauri is R_{Alpha Centauri} = 1.28 x R_{sun}.
Expand Down
12 changes: 0 additions & 12 deletions docs/tips/js.md

This file was deleted.

File renamed without changes.
File renamed without changes.
Loading
Loading