The following packages are alpha software, and thus subject to API breakages, bugs, and unlikely behavior. Use them at your own risk!
In order to take this risk, you need to update your Rheo project to use rookery’s dev branch:
[packages.rookery]
repo = "https://github.com/freecomputinglab/rookery"
branch = "dev".bib file and mints one idea per reference, keyed and titled from the entry itself, so a work is a note like any other and anything that cites it says so with a backlink.…Reads a .bib file and mints one idea per reference, keyed and titled from the entry itself, so a work is a note like any other and anything that cites it says so with a backlink.
Write a citation by hand where you have something to say about the work, and sweep the rest of the bibliography with all().
#import "@rookery/core:0.1.0": rookery
#import "@rookery/bibtex:0.1.0": bibtex
#show: rookery
/* Typst cannot call a dictionary key directly, so bind the factory's
functions once rather than writing #(refs.citation)(..) at every call. */
#let refs = bibtex(read("references.bib"))
#let citation = refs.citation
#let citations-as-ideas = refs.all
/* The BibTeX key names the note, written as a ref, a bare label or a string.
The title comes off the entry as "Badiou, Ethics (2002)", and the note is
tagged `citation` alongside whatever tags: you add. */
#citation(<badiou2002>, tags: "essay")[
Read against Handelman, since it treats the same refusal of mathematics as
a question of ethics rather than of method.
/* The entry's own fields, as a definition list. */
#refs.fields("badiou2002")
]
/* Every entry no hand-written citation has claimed, in key order. Call this
once, from one vertebra — a register page the bar never lists, which the
pages a reader browses transclude by tag. */
#citations-as-ideas()Zotero writes rows that belong to the library rather than to the work, and a library holds far more than a project cites. Narrow and trim at the factory, once.
#let refs = bibtex(
read("references.bib"),
/* Parse these keys alone, so a fourteen-hundred-entry export costs what
the project actually cites and mints as many notes. */
only: ("badiou2002", "handelman2019"),
/* Turn Zotero's own keywords into rookery tags, then hide the raw row
they came from. */
keywords: "all",
show-fields: ("keywords": false, "urldate": false, "file": false),
)Models a venue and the calls it puts out. Each call is a single idea carrying its deadline, its portal, and what happened when it was answered.
The venue is the thing that recurs and the call is the round, so a programme you apply to three years running is one #venue and three #cfps pointing at it by label.
#import "@rookery/core:0.1.0": rookery
#import "@rookery/cfps:0.1.0": cfps
#show: rookery
#let TODAY = datetime(year: 2026, month: 6, day: 1)
/* Every call names one of these kinds, so a typo fails at the call rather
than minting a round no ladder governs. */
#let (venue, cfp, cfp-state, panel) = cfps(kinds: (
postdoc: (sort: "job", ladder: (transit: ("submitted",), terminal: ("offered", "rejected"))),
))
#venue("acme", title: [Acme University])[A programme that runs every year.]
/* State comes from the dates rather than a field. A future deadline with
nothing sent is open. */
#cfp(
"acme-postdoc-26",
venue: <acme>,
kind: "postdoc",
deadline: datetime(year: 2026, month: 1, day: 1),
today: TODAY,
)[A round that lapsed with nothing sent.]
/* A submitted stage and no terminal rung is in flight, and a terminal rung
settles the round. Stage names are checked against the kind's ladder. */
#cfp(
"acme-postdoc-25",
venue: <acme>,
kind: "postdoc",
deadline: datetime(year: 2025, month: 1, day: 1),
timeline: (
submitted: datetime(year: 2024, month: 12, day: 1),
offered: datetime(year: 2025, month: 2, day: 1),
),
today: TODAY,
)[A round that was answered, and settled.]
/* Panels filter on exactly those words. Pass today: everywhere, as it has no
fallback. */
#panel(state: "settled", today: TODAY)Mints a meeting as an idea, recording who was in the room, when it happened, and what was said.
A meeting names the people it was with by pointing at the ideas that already stand for them, so the record and the person are the same kind of thing.
#import "@rookery/core:0.1.0": idea, rookery
#import "@rookery/meetings:0.1.0": meeting
#show: rookery
#idea("doshi-velez-finale", title: [Finale Doshi-Velez])[A person.]
/* The title is synthesized from the participants and the date, coming out as
"Meeting with Finale Doshi-Velez on 10.9.26". A reference date has to come
from somewhere, whether today: here, today: on the factory below, or the
document's own #set document(date: ..). There is no wall-clock fallback. */
#meeting(
<doshi-velez-26-9-10>,
with: <doshi-velez-finale>,
on: datetime(year: 2026, month: 9, day: 10),
today: datetime(year: 2026, month: 9, day: 10),
)[
What was said.
]Where a whole site’s meetings share a page tag, bind the constructor once and write the tag nowhere else.
#import "@rookery/meetings:0.1.0": meetings
#let meeting = meetings("digital-theory-lab", today: TODAY)
#meeting(<blix-27-8-26>, with: <hagen-blix>, on: d)[..]Arranges ideas as draggable cards on a board that remembers where each one was put, after the structural method of John McPhee, which lays out the components of a piece until a sequence appears among them.
A board with no arguments is every note in the rookery, as cards.
#import "@rookery/core:0.1.0": idea, rookery
#import "@rookery/pinboard:0.1.0": pinboard
#show: rookery
#idea("outline", title: [Outline])[..]
#idea("interview", title: [The interview])[..]
/* Cards are windows rendered without backlinks, so a board showing the whole
corpus is a view of the notes rather than forty references to them. */
#pinboard()Pinboard has no query of its own, because rookery already has one. Narrow with ideas(..) and hand the rows in.
#import "@rookery/core:0.1.0": ideas
/* Naming a board gives it a memory. Positions live in the reader's own
browser under rookery-pinboard:<id>, keyed on each note's registry name, so
a layout belongs to the reader rather than the document and renaming a
board starts it empty. */
#pinboard(id: "outline-board", notes: ideas(tagged: "outline"), folded: false)
/* layout: places a card the board has never seen before, and only such a
card. Once something has been dragged, neither the layout nor the order of
ideas(..) moves it again. */
#pinboard(id: "flow-board", layout: "flow")Provides a full-text search interface for rookery ideas, as seen in rookery’s docsite, or one of these sites.
Ideas are ranked by name, by title, and by full text. This index can power a search modal or an inline filter panel.
#import "@rookery/core:0.1.0": rookery
#import "@rookery/search:0.1.0": search-bar, filter-panel
#show: rookery
/* Modal-based search bar with a limited number of ideas in the base view. */
#search-bar(placeholder: "Find a note", limit: 12)
/* Inline search panel with clickable pills as preset searches for certain tags. */
#filter-panel(tag: "todo", pills: ("ready", "blocked", "epic-jobs"))You can specify tags: when using the UX elements to narrow the search space at compile time:
#search-bar(tags: "phd", elem-id: "phd-index", placeholder: "phd notes")
#search-bar(tags: "trip", elem-id: "trip-index", placeholder: "trip notes")Renders a set of ideas as an endlessly scrolling presentation, with a camera that moves between slips rather than cutting from one slide to the next.
A slip is an idea, so a deck is a query over ideas.
#import "@rookery/core:0.1.0": rookery
#import "@rookery/slipshow:0.1.0": slip, slipshow
#show: rookery
#slip("opening")[Welcome. This deck has three slips and no options.]
#slip("middle")[Each `#slip` is a note the deck below queries by tag.]
#slip("closing")[The end.]
/* Queried decks sort on the slip-order tag, and take order: "created" or an
explicit list of names instead. Give a deck a query or a list of slips, as
a deck given both, or neither, panics rather than guessing. */
#slipshow(tags: "slip")The other route hands the deck its slips directly, in the order they should be read. These are already ordered, so passing order: alongside them is an error rather than a redundancy.
#slipshow(slips: (
slip("intro", title: [Welcome], fullscreen: true)[The opening slip.],
slip("closing")[The last one.],
))Name what the camera does arriving at a slip, per deck or per slip. The camera is HTML, so on a paged target every slip prints in order, without reveal, rows or backgrounds.
/* reveal: false renders the whole deck up front instead of one slip at a
time, and background: takes a Typst value rather than a path string. */
#slipshow(tags: "slip", enter: "scroll", reveal: false)
#slip("cover", enter: "jump", background: image("cover.png"))[..]Keeps a dated lifecycle log on the tags of an idea, from when it was created to whatever stages a ladder of its own names, and draws the rail that orders them.
A lifecycle is a dictionary of stage names to dates, folded into the note’s own tags, so it travels with the note and any view that can read tags can read it.
#import "@rookery/core:0.1.0": idea, rookery
#import "@rookery/timeline:0.1.0": timeline-tags, timeline-view
#show: rookery
#let NOW = datetime(year: 2027, month: 1, day: 5)
/* Three stage names are reserved: scheduled, deadline and closed. Every other
rung is vocabulary you invent for the thing you are tracking. A stage may
also be a dictionary rather than a bare date, needing a timestamp: and
taking an optional note:. */
#let wolf = timeline-tags(
deadline: datetime(year: 2026, month: 11, day: 1),
timeline: (
submitted: datetime(year: 2026, month: 10, day: 28),
longlisted: datetime(year: 2026, month: 12, day: 15),
"first-interview": datetime(year: 2027, month: 1, day: 20),
),
)
#idea("wolf", tags: wolf)[A round in flight.]
#timeline-view((created: datetime(year: 2026, month: 10, day: 1)), wolf, today: NOW)The package also ships a skin, so the stages can be arguments instead of a fragment you merge yourself.
#import "@rookery/timeline:0.1.0": idea, rookery, window
/* Writing the same stage twice, once as an argument and again inside
timeline:, is an error rather than a last-one-wins. Number a stage that
genuinely happens more than once (review-1, review-2) and match it with a
review-* family rung. */
#idea("ship", deadline: d, timeline: (submitted: d2))[Cut the release.]Writes todos and epics as ideas with a dependency graph over them, so a rookery can say for itself what is ready to work, what is blocked, and what has gone stale.
A todo is an idea with a priority, a state and a list of the todos it waits on. Nothing computes what to do next, as the graph already knows.
#import "@rookery/core:0.1.0": rookery
#import "@rookery/todos:0.1.0": todo, todos-ready, todo-graph-view
#show: rookery
#let TODAY = datetime(year: 2026, month: 8, day: 25)
/* Name any todo something else depends on. An auto-named #todo[..] takes a
sequence id that shifts the moment a note is inserted earlier in the file,
silently repointing every dependency on it. */
#todo("fetch", title: [Fetch the source], priority: 0, done: datetime(year: 2026, month: 8, day: 1))[..]
/* Priorities run the way birds' do, a higher number being more important with
no ceiling, so escalating something never means renumbering everything
below it. */
#todo("parse", title: [Parse it], priority: 9, type: "bug", deps: ("fetch",))[..]
/* parse waits on fetch, fetch is closed, so parse is what surfaces here.
Every view needing a now takes today: explicitly and panics without one. */
#todos-ready(today: TODAY)
#todo-graph-view(today: TODAY)An epic is a tag with a constructor bound to it rather than a container. Two todos in one epic stay unrelated until one names the other in deps:, as the epic groups them without ordering them.
#let launch = epic("launch")
#launch("plan", priority: 5)[Kick-off.]
#launch("post", deps: ("plan",))[Follows the plan.]