Skip to content

Repository files navigation

Preview Panel

A Grav 2.x plugin providing theme-agnostic Twig functions:

  • preview_panel() — a source preview panel for any page with prose links to wikipedia.org/wiki/…. Clicking a link loads the article's lead section into the panel without leaving the page; wikilinks inside the loaded text chain further.
  • vis_timeline(events, options) — a vis.js timeline driven by page frontmatter, whose item clicks populate the same panel.

The theme decides where the markup goes; the plugin owns what it does.

Status

0.3.0 — both functions work. The .pp-timeline--wide full-bleed CSS is a per-theme integration step, not part of this plugin (see "Full-viewport timeline" below). See CLAUDE.md for the full implementation plan and CHANGELOG.md for what has actually landed.

Installation

Not on GPM. Clone into a Grav install's user/plugins/:

git clone <this-repo> user/plugins/preview-panel

Note for the artificiallystupid.com deployment specifically: Git-Sync's folders: list does not include plugins, so this never reaches the VPS automatically the way page and theme edits do. Deploying is a manual clone/pull on that install.

Usage

Call the function from a template wherever the panel should sit:

{% if page.header.preview_panel %}{{ preview_panel() }}{% endif %}

That single call also enqueues the plugin's CSS and JS — no separate asset wiring. Pages that never call it pay nothing.

preview_panel() accepts an optional options hash:

Key Meaning
class Extra classes appended to pp-panel, passed through verbatim.
id Overrides the configured panel_id for this one call.

A bare string is taken as class, so {{ preview_panel('my-class') }} and {{ preview_panel({class: 'my-class'}) }} are equivalent.

Calling it twice on one page renders one panel. The second call returns an empty string rather than a duplicate — the panel carries a DOM id and the script binds to the first .pp-panel it finds, so a second one would be an element the theme placed that never fills. This is also what lets a future vis_timeline() auto-emit a panel without having to know whether the template already emitted one.

Theme requirements

  1. The theme's asset block must be deferred. This plugin registers its stylesheet from inside a Twig function, i.e. while the page body is rendering — after a non-deferred {{ assets.css() }} in <head> has already run. A theme that renders assets without Twig's deferred modifier will silently drop the panel CSS. Quark 2 is fine (partials/base.html.twig: {% block assets deferred %}). JS goes to the bottom group, which is rendered at the end of the document and needs no deferral.
  2. Placement is the theme's job. The plugin sets no width, float, grid column or position. Wrap the call however the layout needs.

Theming

The plugin never declares its public custom properties — it only reads them with a fallback to a private default. So a theme can set them at equal specificity and win, regardless of stylesheet order (which matters: Grav's Assets manager renders a theme's custom.css before a plugin's stylesheet).

Property Default Used for
--pp-bg transparent Panel background
--pp-fg inherit Panel text
--pp-border rgba(0,0,0,.15) / lighter in dark mode Panel border
--pp-muted rgba(0,0,0,.55) / lighter in dark mode Placeholder + "no preview" text
--pp-accent currentColor Underline on panel-loading links
--pp-radius 6px Panel corner radius
/* in the theme's own stylesheet */
.pp-panel {
  --pp-bg: var(--my-card-bg);
  --pp-border: var(--my-border);
}

Without any mapping the panel still renders correctly, including a dark-mode variant driven by prefers-color-scheme and by :root[data-theme="light"|"dark"].

Markup contract

These names are stable across versions:

<aside id="preview" class="pp-panel" >
  <div class="pp-panel__extract"></div>   <!-- JS writes here -->
</aside>

Both levels are load-bearing — the JS writes into .pp-panel__extract and does nothing if it is missing. Links the plugin claims are tagged a.pp-source-link.

Timeline usage

{{ vis_timeline(page.header.events, {class: 'pp-timeline--wide'}) }}

events is page.header.events — a YAML list in the page's own frontmatter, not a fetched file (user/ blocks direct .json/.yaml access, so a client-side fetch() of page data never works here). options accepts the same class/id shape as preview_panel() (bare string = class). id overrides the canvas element's id, useful only if a page calls vis_timeline() more than once.

Auto-emits a panel if the page hasn't already called preview_panel() — a page calling only vis_timeline() still gets a working panel, in either call order. Calling both is normal and produces exactly one panel (see "Calling it twice" above).

Event schema

events:
  - id: prise-de-la-bastille       # required, stable, unique on the page
    start: 1789-07-14               # required — YYYY, YYYY-MM, or YYYY-MM-DD, quoted or not
    end: 1789-07-15                 # optional — presence makes it a range, not a point
    content: Prise de la Bastille   # required, short — rendered inside the item box
    detail: >                       # optional prose, shown in the panel + hover tooltip
      L'attaque de la forteresse-prison, symbole de l'arbitraire royal.
    group: militaire                # optional swimlane id — plugin never hardcodes a vocabulary,
                                     # whatever `group` values a page's events carry become that
                                     # page's lanes; omit on every event for a single-lane timeline
    citation: 'Michelet, 1847'      # optional short source citation
    className: milestone            # optional, per-item CSS hook for color
    wikipedia: https://fr.wikipedia.org/wiki/Prise_de_la_Bastille   # shorthand for {url: ...}

A missing id/start/content throws with the offending event's id (or index) in the message rather than silently dropping the event. wikipedia.title is optional — derived from the URL when omitted. Unrecognized/absent source keys render the event's content/detail/citation with no extract underneath, never an error.

Markup contract

<div class="pp-timeline {{ options.class }}">
  <div class="pp-timeline__canvas" id=""></div>
  <script type="application/json" id="…-data"></script>
  <aside class="pp-panel" id="preview"></aside> <!-- only when auto-emitted -->
</div>

.pp-timeline sets no width, float, or grid column — the plugin publishes stable class names and a caller-supplied modifier class (options.class); the host theme decides whether and how to escape its own content column. This site's own breakout rule (.pp-timeline--wide) lives in themes/quark2/css/custom.css, not here — see that file's comment for the two nested max-width constraints it clears.

Configuration

Admin2 → Plugins → Preview Panel, or user/config/plugins/preview-panel.yaml:

Setting Default Meaning
enabled true Plugin on/off.
panel_id preview DOM id the panel renders with and the timeline targets.
autoload_first_link true Fill the panel from the first previewable link on load, so prose pages don't open empty.
sources.wikipedia.enabled true Fetch article leads from the MediaWiki API.
sources.wikipedia.lang en Language subdomain used to build the API host.

Other source keys (e.g. courtlistener:) are accepted and render as plain outbound links until an adapter for them exists — authoring them now needs no later migration.

Behaviour notes

  • A page with no panel is left completely alone. The assets are not enqueued at all, and even if a theme loads the script anyway it bails before tagging anything. Wikipedia links on such a page navigate normally.
  • Non-article namespaces (File:, Category:, Special:, …) are never claimed; those links stay outbound.
  • Leads are cached for the page's lifetime, keyed by requested title, so revisiting a link is instant and costs no second request.

Content sanitization

sanitizeLeadHtml() is a paragraph filter, not a sanitizer. It keeps top-level <p> children and strips sup.reference/<style>, but anything else inside a kept paragraph survives into innerHTML. The trust assumption is "whatever Wikipedia serves, we run" — an inline <img onerror=…> in a vandalized article would execute. That is a conscious trade-off for en.wikipedia.org. If a future adapter targets a less-curated source, that assumption stops holding and a real allow-list sanitizer becomes mandatory.

License

MIT.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages