Card designs with [needs.card_layouts]¶
Added in version 0.31.2.
A need card is what ubCode renders a need directive as, in the built site and in the editor preview alike. Its shape is a card design: a named, declarative description of which regions the card has and what each one holds.
ubCode ships ten designs, and a project can define its own.
Both are the same kind of thing, written in the same vocabulary and interpreted
by the same code — so a design you write is not a second-class citizen of the
renderer, and complete is not privileged over review.
A need chooses its design with :layout:, or a project chooses one for it with
[needs.fields.layout]
(default and predicates).
An unknown name renders the default design and reports
build.need_layout_unknown —
it never ends the build.
The regions¶
A card has five regions, and every design is a statement about them:
Region |
What it holds |
|---|---|
|
The type badge, the title and the id, on one line.
A design either has it ( |
|
The collapsible table of the need’s fields and link rows.
A design chooses which tier of fields it shows and can filter them;
|
|
The need’s body. Always present. |
|
An ordered list of elements below the content. |
|
An ordered list of elements in a column beside the card, on the left or the right. |
Both footer and side range over the same element set
(each element can also be written as an object —
see The object form):
Element |
What it draws |
|---|---|
|
The need’s id as an in-page permalink — what keeps a header-less card linkable. |
|
The need’s title, drawn visibly where you put it — the design’s title
carrier. Legal only in a design with |
|
The need’s type name. |
|
|
|
|
|
|
|
An image whose source is the value of one named need field (see Images on a card). |
Every element is omit-when-empty, and so is every region:
an element with nothing to draw draws nothing,
and a region whose elements all draw nothing emits no markup at all.
That rule is what makes the side column of a design like clean_l free for a
need that has no image.
A prefixed element’s payload is always a field name.
image:diagram draws the image the need’s diagram field points to —
the payload is never itself a literal path or URL,
and a payload that names no defined field is a configuration diagnostic.
The object form¶
Every element string is canonical sugar for an object (a TOML inline table). The two spellings are equivalent — the same resolved design, the same rendered bytes, and the same configuration hash:
"image:diagram"≡{ type = "image", field = "diagram" }"field:owner"≡{ type = "field", field = "owner" }"id"≡{ type = "id" }, and the same fortitle,type,layout_echoandstyle_echo
The string forms stay valid forever and remain the default way to write an element. The object form exists for the things a bare string cannot say: an element’s options.
[needs.card_layouts.spec_card]
footer = [{ type = "field", field = "owner", label = "Owned by" }]
[needs.card_layouts.spec_card.side]
elements = [{ type = "image", field = "diagram", height = "40px" }]
type is required, and names one of the seven element kinds.
field is required for field and image elements and not valid on any
other kind — and it is always a field name, exactly as the prefixed strings’
payload is (see the invariant stated above), never a literal path or URL.
The options, only these and only where stated:
Option |
On |
Value |
|---|---|---|
|
|
The drawn image’s height, emitted as the |
|
|
The drawn image’s width. Same grammar (and same string spelling)
as |
|
|
Replaces the field name in the rendered |
Anything else — an unknown key, a missing or misplaced field, an option on
the wrong element kind, a value failing its grammar — is a configuration
diagnostic, and the element is ignored exactly as an unknown element string is.
The grammars are deliberately narrow, and shared verbatim with the sphinx-needs
needs_card_layouts companion: a label cannot carry a markup-active
character, so one value renders the same pair in both engines.
The built-in designs¶
Name |
What it renders |
|---|---|
|
The default: header, a meta section of the fields the need actually
carries, then the content. Emits no layout class at all, so a
|
|
|
|
Content only — no header, no meta section — plus a slim footer carrying the card’s permalink. |
|
|
|
|
|
The same column, beside the header and meta section only; the content takes the card’s full width below it. |
|
|
Two further sphinx-needs names are accepted as identities:
test renders clean, and focus_f renders focus.
Both are stated as such in the editor’s completion list.
Note
A built-in design cannot be redefined.
A [needs.card_layouts] table claiming one of the twelve names above is
refused with a configuration diagnostic, and the built-in keeps its meaning —
so :layout: complete means the same thing in every project.
Defining a design¶
A design is a table under [needs.card_layouts], named with a CSS identifier
([A-Za-z_][A-Za-z0-9_-]* — the name becomes a class on the card):
# `complete`'s rows and echoes, with the meta section pinned closed and the
# owner echoed in the footer.
[needs.card_layouts.review]
extends = "complete"
collapse = "closed"
footer = ["layout_echo", "field:owner"]
extends names any design — one of the twelve built-in names (the ten designs
and the two sphinx-needs identities) or another of your own — and every key you
omit is inherited from it, the side column included:
# `clean_l`'s image column, with the meta section pinned closed.
[needs.card_layouts.illustrated]
extends = "clean_l"
collapse = "closed"
Without extends, an omitted key takes clean’s value.
Two keys can be switched off rather than merely left unstated, for a design whose base has one and does not want it:
[needs.card_layouts.plain]
extends = "clean_l"
meta = false # no meta section
side = false # no side column, despite the base having one
side = { elements = [] } means the same thing as side = false: a column
with no elements is not a column, so it resolves to no side region at all rather
than to an empty one. Both spellings are accepted, in ubCode and in a
Sphinx-Needs project using needs_card_layouts.
meta and side are sub-tables:
[needs.card_layouts.spec_card]
header = true
footer = ["id"]
[needs.card_layouts.spec_card.meta]
# "stored" (the fields the need carries), "effective" (plus defaults),
# or "all" (plus internal fields — the `debug` tier).
fields = "effective"
# Show only these rows, in this order. Empty means "no restriction".
include = ["status", "priority", "owner"]
# Never show these. `exclude` beats `include`.
exclude = ["layout"]
# Show a row whose value is empty.
empties = false
links = true
links_back = true
[needs.card_layouts.spec_card.side]
elements = ["image:diagram"]
position = "right" # or "left"
span = "full" # or "partial" — beside the header and meta only
The full key list¶
Key |
Default |
Meaning |
|---|---|---|
|
(none) |
The design whose values every omitted key inherits. |
|
|
Whether the card has a header line. |
|
clean’s meta |
The meta section’s selection, or |
|
|
Present for completeness. |
|
|
Elements below the content, in order. |
|
(inherited) |
The side column: |
|
|
|
|
|
|
|
|
An allow-list: non-empty restricts the rows to exactly these names, in this order. An included name is shown even if it is an internal field. |
|
|
Names never shown. Beats |
|
|
Whether a field with an empty value gets a row. |
|
|
Whether outgoing / incoming link rows are shown. |
|
|
The column’s elements, in order. |
|
|
|
|
|
|
Images on a card¶
image:<field> draws an image whose source is the value of a need field.
The value must be one of exactly two shapes:
an https URL, emitted verbatim. ubCode never fetches it, at build time or any other time.
a project-relative path — no scheme, no
//prefix, no backslash and no..segment. It resolves against the project’s source directory, whichever page the card is drawn on, and the file is published under_images/exactly as an.. image::argument is.
Anything else is refused and reported as build.need_card_image_invalid, and
the element draws nothing.
That is deliberately stricter than an authored .. image::: a card image’s
value is a need field, and a need can arrive from an imported needs.json,
i.e. from outside your project.
A path is resolved from the project’s source directory, not from the document
drawing the card — a card is a property of the need, and the same need’s card is
drawn by every page that shows it, so a document-relative path would mean
different files on different pages.
(A Sphinx-Needs project’s image("field:…") resolves the same way, so one
value works in both.)
Spelling a design out in full — the field, the design, and a need that uses it:
[needs.fields.diagram]
nullable = true
[needs.card_layouts.diagrammed]
extends = "clean"
[needs.card_layouts.diagrammed.side]
elements = ["image:diagram"]
position = "left"
span = "full"
.. spec:: Session handshake
:id: SP_1
:diagram: assets/handshake.svg
:layout: diagrammed
The client opens the session with …
A need with no value for diagram renders a card byte-identical to clean.
This project configures a shorter version of the same thing, illustrated,
which inherits clean_l’s column instead of restating it — see
Trying it in this project.
How designs compose¶
The layout class. A card carries
ub-need-layout-<design>— except forclean(whose absence of a class is its identity) and except for a design whose only difference fromcleanis a side column that drew nothing. Layout classes compose freely with the:style:classes, the per-type accent and the hidden-card state.``:collapse:``.
collapse = "honour"lets the need’s own value decide;"open"and"closed"pin it. A design with no meta section has no disclosure, so:collapse:is inert on it.``:hide:``. A hidden need renders its whole card — every region, this design included — inside a closed disclosure whose summary is the card’s header line. That summary sits outside the side column’s row, because a disclosure’s control has to span the card rather than sit in a narrow cell — so on a hidden card
span = "full"runs beside the meta section, content and footer, andspan = "partial"beside the meta section alone. A hidden card also emits its title once, in that summary, so atitleelement and the automatic span both stand down.Nested needs. A need directive inside another need’s body renders a full card inside the outer card’s content, with its own design.
Before the index has seen the need. The editor preview renders a card in the default
cleanshape until the background index has indexed it — the raw:layout:you typed is never read, only the resolved one. So a design you have just written shows up a moment after the index catches up, not instantly.
One title per card¶
The built site’s search index takes a card’s title from the card’s title span, anywhere inside the card, and appends every run it finds. So a card must contain exactly one.
Three rules follow, and ubCode enforces all three rather than documenting them and hoping:
a design with
header = falseand notitleelement gets a title span automatically, so a header-less card still yields a full search record. Both stylesheets hide that automatic span visually — for every design, including one you defined — while leaving it in the accessibility tree, where a bare id is a poor label.a
titleelement replaces that automatic span. It is the design’s title carrier: it draws where you put it, in your own element order, and it is visible. (This is what upstream’smeta("title")does in aneeds_layoutsfooter.)a
titleelement is legal only in a header-less design, and only once across all of a design’s regions. Anything else is refused at configuration time and the offending elements are dropped, because a doubled title corrupts that need’s search record silently — the search index appends every title run it finds inside a card.
A hidden need (:hide:) is the exception to the first two: its card’s
<summary> is a header and already carries the title, so both the automatic
span and an explicit title element stand down.
Differences from a Sphinx-Needs build¶
The vocabulary is shared, the mechanism is not. A Sphinx-Needs project can write the same designs with that project’s
needs_card_layouts, which compiles them down toneeds_layoutsentries. ubCode does not implement needs_layouts itself — see Differences from a Sphinx build for why.``focus_f`` ≡ ``focus``, by design. Upstream’s bare
focusdrops even the id, which would leave a card with nothing to link to; ubCode’sfocuscarries the id footer, so it is upstream’sfocus_fexactly, and upstream’s barefocusis the name with no ubCode equivalent.``debug`` opens. Upstream’s
debuglayout renders collapsed unconditionally; ubCode forces it open, because a debug view that opens closed shows nothing.The vertical id strip differs in two named ways. Upstream’s
focus_l/focus_rstylesheet useswriting-mode: sideways-lr/sideways-rlwithtext-orientation: upright. ubCode declares the widely-supportedwriting-mode: vertical-rlfirst andsideways-lrafter it, so a browser withoutsideways-*still gets a vertical strip rather than an overlapping one; and it usestext-orientation: mixedrather thanupright, which would stack a need id one character per line.An unknown name warns. Upstream raises
SphinxNeedLayoutException, aBaseExceptionsubclass, so one typo ends the build.
Trying it in this project¶
These documents configure card designs of their own, in this repository’s
docs/ubproject.toml — the illustrated, review and plain tables are
literally the examples above, not imitations of them.
Open the repository in ubCode and they are there to try: write :layout:
illustrated on a need in the editor preview, or run ubc build html over
docs/.
Where they do and do not render, plainly. [needs.card_layouts] is a
ubCode table. These published pages are built by Sphinx with a released
sphinx-needs, which does not know the key yet and ignores it silently — so the
project’s own designs are inert here, and live in ubCode. They become live in
this build too once the docs’ sphinx-needs pin includes the companion
needs_card_layouts feature.
What both engines already share is the permutation names, so the live cards below use those. They are real needs on this page, rendered by whichever engine built what you are reading.
clean_l — clean plus a left image column, drawing the need’s image
field (registered in docs/ubproject.toml, and the field upstream’s own
clean_* layouts read):
The column holds the |
The same design on a need that has no value for that field. The column draws
nothing, so there is no column, no wrapper — and, because a side column is this
design’s only difference from clean, not even a layout class to catch a
stylesheet on:
This card is byte-identical to a |
focus_r — content only, with the id in a narrow vertical strip on the right
instead of a footer:
No header and no meta section. The id beside this text is the card’s permalink, and the need’s title is still in the search index even though nothing here shows it. |
Upgrade cost¶
Adding [needs.card_layouts] to a project changes its configuration hash, and
ubCode keys both its index cache and its build manifest on that hash — so the
first build after adding or editing a design re-indexes and re-renders once.
That is the intended trade: it is also what makes an edit to a design’s body
re-render every page that draws it.