Migrating to Karni 0.4¶
Every change below is recorded in Karni's machine-readable changelog
(packages/karni/karni/changelog/0.4.0.toml). None needs a code rewrite, so
karni upgrade reports each one with a link here and changes no files.
Nothing here breaks a 0.3 project's build. Some names in generated code and
some test expectations change.
Upgrading¶
pipx upgrade karni-cli # the CLI carries the changelog
karni upgrade --dry-run # lists the notes below and writes nothing
# then pin karni to 0.4 in pyproject.toml, reinstall, and run:
karni rules sync # rewrite the agent files in the new wording
karni types # regenerate the TypeScript client
karni check # and the tests
Modules move with core: karni-payments, karni-approvals and
karni-impersonation 0.4 require karni>=0.4,<0.5, and the module UIs peer
on @karni/ui ^0.4.0.
Changed in 0.4¶
Generated API clients: operation IDs¶
A route that ends in a path parameter now has _by_<param> in its OpenAPI
operation ID, so two routes no longer share one ID. Before, GET
<resource>/import/ and GET <resource>/import/{job}/ both became
..._import_retrieve, and a generated client named the second one
..._retrieve_2. Now the second is ..._import_by_job_retrieve. The
standard retrieve, update, partial_update and destroy routes keep
their names. Run karni types and rename the calls the compiler reports.
Enum names in the OpenAPI schema¶
When two choice fields share a name but not their choices (two status
fields, say), drf-spectacular named their enums with a hash, such as
Status469Enum. Karni now names them after the model and the field:
RefundStatusEnum, BookingStatusEnum. A field name used by one choice set
keeps its plain name, so most schemas do not change. Run karni types, then
accept the project's OpenAPI snapshot (KARNI_UPDATE_SNAPSHOTS=1 karni
test). To keep drf-spectacular's names, put
"drf_spectacular.hooks.postprocess_schema_enums" back in
SPECTACULAR_SETTINGS["POSTPROCESSING_HOOKS"]. ENUM_NAME_OVERRIDES still
wins over both. See enum names.
The agent files: run karni rules sync¶
The rules, the procedures and the header of the generated AGENTS.md,
CLAUDE.md and the other adapter files are reworded in plain sentences. The
facts are the same. Until you run karni rules sync, karni rules sync
--check (and make check) reports the files as stale.
Messages and log lines are reworded¶
What Karni prints no longer uses semicolons or em dashes. Two formats that a
log search may match changed. The development query log now reads
GET /path 200: 3 queries, 2 duplicated
where it used to read
GET /path 200 — 3 queries, 2 duplicated.
A budget overrun now reads
ran 28 queries against a budget of 8
where it used to read
ran 28 queries; budget is 8.
karni explain - reads both. Empty values show Django's - in the admin and
EmptyValue in @karni/ui. Update tests that match the old wording.
ResourceTable is a grid¶
ResourceTable is now a WAI-ARIA grid with one Tab stop: Tab enters and
leaves it, and arrow keys move inside. Its accessible role is grid, so a
test that finds it with getByRole("table") must use getByRole("grid").
The element is still a <table>. Pass keyboardNavigation="native" for the
previous behaviour, where every control in a row is its own Tab stop.
Focus moves after navigation¶
Layout moves focus to the new page's heading after a client-side route
change and announces the page title. A test that expects focus to stay on
the clicked link after navigating needs updating. Pass routeFocus={false}
to turn it off.
Deprecated in 0.4¶
The 0.3 colour token names¶
Every visual value in @karni/ui now resolves from a token in one of the
families --karni-color-*, --karni-space-*, --karni-radius-*,
--karni-font-* and --karni-shadow-* (see the
styling section
of the @karni/ui README).
Colours are now plain CSS colours: --karni-color-primary: oklch(0.55 0.22 285)
or var(--your-primary). The 0.3 names held HSL triplets
(--karni-primary: 262 83% 58%). Each --karni-color-* defaults to hsl() of
its 0.3 name, so a 0.3 theme keeps working unchanged. The 0.3 names are
deprecated in 0.4 and removed in 1.0.
To move a theme, rename each property and wrap the triplet in hsl():
/* 0.3 */
:root { --karni-primary: 262 83% 58%; }
/* 0.4 */
:root { --karni-color-primary: hsl(262 83% 58%); }
The renamed properties are background, foreground, card,
card-foreground, popover, popover-foreground, primary,
primary-foreground, secondary, secondary-foreground, muted,
muted-foreground, accent, accent-foreground, destructive,
destructive-foreground, border, input, ring and chart-1 to chart-5:
--karni-<name> becomes --karni-color-<name>. --karni-radius keeps its
name, and --karni-radius-sm, -md, -lg and -xl derive from it.
Opacity modifiers on Karni colours in your own code (bg-primary/50) keep
working under Tailwind 3 and 4. Inside @karni/ui they are replaced by named
shades (bg-primary-hover, bg-muted-subtle, bg-destructive-subtle), which
you can theme on their own.
Changed defaults in 0.4¶
The admin wears @karni/ui's brand¶
The admin's default UNFOLD setting now loads @karni/ui's design tokens and
reads its colours from them (admin theming): the
primary is @karni/ui's (near-black in light mode, near-white in dark)
instead of indigo, the neutral greys and the dark background are the tokens'
gray, the radius is 6px instead of 8px and the font is the system stack
instead of Inter. Nothing breaks. The admin only looks different.
A project that set UNFOLD["COLORS"]["primary"]["600"] alone now gets that
step over Karni's primary scale. Either set the whole scale ("50" to
"950"), or put the colour in a token override instead, which also reaches
buttons' text colour and Karni's own admin pages:
/* static/css/admin-theme.css, appended to UNFOLD["STYLES"] */
:root,
.dark {
--karni-color-primary: oklch(0.55 0.2 250);
--karni-color-primary-foreground: white;
}
To keep unfold's own look, set UNFOLD["STYLES"] = [] and delete
UNFOLD["COLORS"] after karni_settings().
AuthGuard will explain a refusal by default¶
An AuthGuard can now say why it refuses. With explain, a visitor who is
not signed in sees a sign-in prompt (a LoginForm, and signing in reveals the
content in place), and a signed-in user whose role lacks access sees
PermissionDenied. In 0.4 a guard with no fallback and no explain still
renders nothing, as in 0.3, and logs one warning in development. 1.0 makes
explain the default. Before 1.0, decide for each guard:
- A guard that protects a page: add
explain(oranonymous/deniedfor your own nodes). - A guard that hides a piece of UI (a nav link, a button): pass
fallback={null}to keep rendering nothing, or useusePermission().hasRole(...)instead.