Skip to content

Install and adopt

Install

pip install abilian-ds        # or: uv add abilian-ds

The package carries the stylesheet, the Jinja macros, the linter and the token reference. It has no front-end toolchain and does not want one.

Vendor the stylesheet

abilian-ds sync

That writes two files into static/css/ads/:

static/css/ads/
├── ads.css          # the one a page links — knobs, the reset and every part, in order
└── situ.css         # only if you use situ, and only after that kit's own stylesheet

Then link it:

<link rel="stylesheet" href="/static/css/ads/ads.css">

One file, one request. The package authors the stylesheet as a tree — knobs.css, derive.css, base.css and a dozen files under parts/ — and sync splices them in ads.css's own import order. That order is load-bearing (layout.css and then forced-colors.css last), and it stays declared in exactly one place.

Why one file instead of @import?

@import is a render-blocking waterfall: the browser cannot discover parts/*.css until ads.css has arrived and been parsed. A fifteen-file tree therefore costs two serial round trips before anything paints, and fifteen cache entries afterwards — on every navigation, if the server answers a conditional request with a fresh 200, as several do.

The parts are how the stylesheet is authored. They are not how it is served.

Why vendored rather than served from the package?

A browser needs a URL.

Vendoring normally opens an escape hatch — someone edits the local copy and the project quietly forks. abilian-ds lint closes it by hashing every vendored file against what the package would produce, so an edit is a finding — and so is forgetting to re-sync after an upgrade, which is the more common way to get there:

static/css/ads/ads.css:0: asset-drift: differs from the installed package
    (not re-synced after an upgrade, or edited locally); run `abilian-ds sync`

sync also deletes any other .css under the target: the directory belongs to the command, and a stale parts/button.css left over from an older version is exactly the file someone edits and wonders why nothing changes.

Configure the project

Paths live in pyproject.toml, so the commands take no flags:

[tool.abilian-ds]
templates = ["templates"]          # what to lint
theme = "static/css/ads"           # where sync put the stylesheet
components = "templates/ads"       # where sync put the macros
allow = ".abilian-ds-allow"        # project allowlist, optional
app = "myapp.main:app"             # optional, enables the endpoint check

Configure Jinja

from jinja2 import StrictUndefined

TemplateConfig(
    directory=[HERE / "templates"],
    engine=JinjaTemplateEngine,
    engine_callback=lambda engine: setattr(
        engine.engine, "undefined", StrictUndefined
    ),
)

StrictUndefined is not optional

It is what turns a misspelt component argument into an error instead of an unstyled element. {{ button("Save", variant="pirmary") }} raises; without it, it renders a button with no classes and nobody notices until a screenshot.

Run the linter

abilian-ds lint
templates/page.html:3: unknown-class: bg-gray-100: not in the design system
1 finding(s)

Green means on-system. Wire it into CI and into your pre-commit hook; it is the contract, and everything else in these docs is a description of it.

Next