Convention for core-component fixes: if a change touches a component a host might have forked (
import JobyKit.CoreComponents, except: [...]), say so explicitly — "if you've forkedtable/1, apply this to your copy". Forks don't receive upstream fixes, and the 0.2.1table/1fix reached a component one app never rendered while its fork kept the identical bug.mix joby_kit.lintnow reports:forked_wrapper, and/design.jsoncarriesforked_from_kit.
v0.3.2
Two fixes to the same flaw, found while migrating the first app: the
/design page was host-specific when its entire promise is being
identical across every JobyKit consumer.
Nothing here requires host action, and nothing fails to compile. The
changes affect the two documentation surfaces — /design and
/custom-designs — not the components an app renders.
One thing to know before upgrading: if you customised the previews
or summaries for kit components in your own DesignPreviews /
DesignManifest, those no longer appear on /design — the kit's
canonical versions do. That is the point of the change, but it means a
local tweak you liked will quietly stop showing. Previews for your own
components are untouched.
/design is the kit's page, structurally
The kit now owns its own page end to end. page_component/1 renders
JobyKit.KitManifest — the kit's own registrations, with previews from
JobyKit.Previews — instead of filtering whatever the host declared.
Three things used to drift, and all three were found in the fleet:
- Which components appear. Generated files are written once and never updated, so an app installed at 0.1 still advertised the 0.1 inventory. One app showed 8 of the 14 components its kit version shipped, with nothing indicating the other six existed.
- What each preview renders. Previews for kit components lived in the
host's
DesignPreviewsand were edited locally, so two apps on the same kit version demonstrated the same<.button>with different examples — one missing the icon-button case entirely. - What the summary says. Same component, different prose per app.
A page that differs per app cannot be the thing an agent learns once.
Generated apps no longer register kit components. The install
template registers only the host's own, and its daisy_overrides/0
starts empty — the kit declares the primitives it wraps, so the
catalogue is right without the host restating it. /design.json merges
the kit's entries with the host's and drops duplicates, so it remains
the single source of truth.
Existing apps need no change. Kit registrations left in a host
manifest are ignored for /design (the kit's list wins) and de-duplicated
in /design.json. Deleting them is tidy-up, not migration — and worth
doing, since they otherwise pin a snapshot of an old inventory.
Which page a component lands on is now decided by who owns the module, not by the category the host declared.
category is a free atom the host picks, so keying the two pages off it
made the split a convention rather than a rule — and conventions get
broken. Found during the first fleet upgrade: an app had registered two
of its own components under category: :core, so they rendered on
/design, the page whose entire promise is "identical across every
JobyKit consumer". To anyone reading that page — or any agent scraping
it — they looked like components JobyKit ships.
page_component/1 now renders only entries whose module the kit owns
(JobyKit.PageComponent.kit_component_modules/0); custom_page_component/1
renders everything else. Category still groups entries within whichever
page owns them.
Deliberately an explicit module list rather than a JobyKit. prefix
check: "what the kit provides" is finite and knowable, and prefix
matching would hand the kit page to anything a host chose to namespace
under JobyKit.
No host action needed. A misfiled component moves to
/custom-designs on upgrade with no manifest change. Recategorising it
to :composite is still worth doing so the heading reads right, but
nothing is broken if you don't.
v0.3.1
Clears the audit's remaining polish, hardens the linter, and removes an internal codename from public surfaces. No breaking changes to component APIs; the linter does gain rules, so expect new findings — they are real, and each is explained below.
New lint rules
:unmarked_component(warning) — a public function component renders markup but carries nodata-componentat all.:unregistered_wrapperkeys off that attribute, so it could only ever see components that had already done half the registration; a component with no marker — the most common way to skip the contract — was invisible to it. Excluded by convention:render/1(a LiveView callback),*_preview/1(the manifest's preview harness), and<App>Web.Layoutsfunctions. Those exclusions exist because the first cut produced eighteen findings on the kit's own generated app, every one structural.:assign_new_default(error) —assign_new(assigns, :x, ...)on an attr declared with adefault:. The key is always present, so the fallback is dead code. This is not hypothetical: it shipped as the flash nil-id bug in 0.2.0, where every toast rendered without an id and the dismiss handler becameJS.hide(to: "#").:missing_data_componentno longer false-positives on a wrapper that builds its marker dynamically (data-component={@dc}), and is now scoped to the component's owndef— including every clause of a multi-clause component. Checking the whole file let one compliant wrapper vouch for every other function in it; scoping it naively to the first clause then reportedinput/1, which has six, as non-compliant. Both caught by a new end-to-end test that runs the linter against JobyKit's own components.
Fixes
Bardois gone from public API. The design page's scraper-facing DOM ids are nowdesign-system-indexanddesign-category-<x>. No fleet app referenced the old ones, and the kit promises these are stable, so this was the last cheap moment.flash/1accepts:successand:warning. The enum was[:info, :error], so apps with a success or warning message could not use the kit's flash at all and hand-rolled alerts instead.- The md patchers no longer duplicate their block. A file that kept the start marker but lost the end one got a second block appended — and then, with both markers present, looked done forever, making the duplicate permanent.
<.table>gainstable_id, sinceidis required for stream containers and lands on<tbody>;attrdocs now cover the enums that agents read out of/design.json.
Housekeeping
- A
test_coveragethreshold of 85% is configured, somix test --coverin CI has a real floor. Current coverage is 90.9%. - The kit's spacing-and-typography principle — own your box, nothing
outside it — is now in the
AGENTS.mdandCLAUDE.mdblocks that every install writes, so agents building new wrappers get the rule that 0.3.0 was built around. - Stale release tarballs, an
erl_crash.dump, and the resolvedNOTES-flash-nil-id.mdscratch file removed from the repo.
Polish pass over the items the audit had left open, plus removal of an internal codename from public surfaces.
Bardois gone from public API. The design page's scraper-facing DOM ids weredesign-system-bardo-indexanddesign-bardo-category-<x>, and the manifest docs said "One Bardo wrapper per daisyUI primitive". Nowdesign-system-indexanddesign-category-<x>. Renaming was safe because no app in the fleet referenced them; the kit promises these ids are stable, so this was the last moment to change them cheaply.app_cssderives the Tailwind@sourcepath fromMix.Project.deps_paths()instead of hardcoding../../deps/joby_kit/lib. The hardcoded form failed silently twice over: umbrella apps keep deps at the root so it never resolved, and an app switched from a hex dep to a path dep keeps the olddeps/joby_kit/directory, so the@sourcekept resolving — to a stale copy of the previous version. Observed live: Tailwind generating CSS from 0.2.1 while the app compiled against 0.2.3, so new classes silently never reached the stylesheet.NavPatcherno longer writes into the wrong element. The</ul>search ran to end-of-file, so a header without its own list would adopt the first list anywhere later in the document — a footer, a sidebar — and still report success. It is now bounded to the enclosing nav/header; a nav we can't place links inside returns:no_nav_foundand the install task prints manual instructions.- Undeclared categories are a compile error. An entry registered
under a typo'd
category:was dropped byby_category/0and vanished from/designand/custom-designswhile still appearing inentries/0and/design.json. Silent invisibility is the failure this kit exists to prevent. The error names the component and lists the categories that do exist. Anonymouspreview:functions are rejected the same way, instead of dying insideMacro.escapewith a message that names nothing. daisy_overrides/0is a declared optional callback, so misspelling it is a compile warning rather than a silent no-op that leaves every wrapped primitive showing as unwrapped.category_label/1andcategory_description/1agree. One raised on an unknown category and the other returned"", so a caller got either a crash or silently blank prose depending on which it happened to call.- One broken preview no longer takes down
/design. Previews render inline, so aKeyErrorfrom a preview that reads an assign 500'd every other component's card with it. Now contained, with the error rendered in place of that one preview. ManifestControllerreturns its JSON error for a typo'd module. Any atom passed the guard, soDesignManifstraisedUndefinedFunctionErrordeep inside the payload and surfaced as a generic HTML 500 — instead of the helpful JSON response written for exactly this case.carduses daisy'scard-borderrather than hand-rolled border classes;<.input type="file" multiple>actually accepts multiple files (the attr was only rendered in the select branch); and an unknown<.icon>name explains itself instead of raising a bareFunctionClauseErrorinside the kit.
Closed as won't-do: list stays a ul. daisy's list requires
ul/li.list-row, and title_class in 0.3.0 addressed the actual
complaint.
v0.3.0
Breaking, on purpose. See MIGRATING-0.3.md for the upgrade, including the list of compensating hacks to delete.
Removes the spacing and typography opinions host apps were routing
around, and fills in the variants they were hand-rolling. Driven by
usage data from three consumer apps: adoption turned out to be inversely
proportional to baked-in opinion — button/input (shape and behaviour,
nothing to undo) got hundreds of uses, while card/header/list
(typography and margin opinions) got bypassed. One app carried 127
hand-rolled panels against 10 <.card> uses; another had zero
<.header> uses and the same header class string pasted ten times.
input/1:classmoves to the root. It landed on the control, so the field group's own box was unreachable — callers resorted to nudging neighbouring elements (class="mb-0.5"on an adjacent button that didn't even work).classand layout now target the root<fieldset>, matching every other wrapper; the newinput_classstyles the control. Global attributes still reach the control, sinceplaceholder/required/etc. would be meaningless on a fieldset.input/1: no outer margin. The hardcodedmb-2is gone — containers own spacing viaspace-y-*/gap-*. Width now works through the root (class="w-32"); the control is alwaysw-full. The root is a semantic<fieldset>, and errors are associated with the control viaaria-invalidandaria-describedby, so screen readers get more than a red border.card/1: body typography is opt-in. The forcedtext-sm text-base-content/70wrapper is gone, and body content renders as direct children ofcard-body— so the card's owngapapplies instead of being dead weight behind a single wrapper div.proseopts into the old treatment;body_classsets your own.card-actionslost its hardcodedmt-3.header/1: usable at last.pb-4removed; new:eyebrowslot (both header-consuming apps hand-rolled one),levelso a section header stops emitting a second<h1>,size(page/section), andtitle_classfor apps with their own display face.button/1: tones,xs, and shapes.varianttakessoft | primary | neutral | ghost | danger. Destructive actions can finally read as destructive — previously Revoke, Delete and Purge rendered identically to Refresh. The default is unchanged and is now nameable assoftfor computed callers.shape(circle/square) covers icon-only buttons, replacing stacks likebtn btn-primary btn-soft btn-sm btn-ghost btn-circlewhere four competing classes were resolved only by stylesheet order.table/1: additive hooks. An:emptyslot (five pages in one app repeated the:if={@rows != []}+<.empty_state>pair),zebraopt-out, asizedensity enum instead of leakingtable-xsthroughclass, anddata-table-actionson the action cell so overrides can target it precisely — one app's:last-childworkaround was hitting a data column on tables with no:actionslot.
Linter
The :raw_html_primitive rule is the kit's headline check, and it was
both over- and under-firing. Expect new findings after upgrading —
they are real, previously hidden.
- The exemption is now scoped to the enclosing
def, not the file. Any file containingdata-component=anywhere — including in a comment or a docstring — silenced the rule for the whole file. One small wrapper in a 500-line LiveView zeroed coverage for the entire render, and a# data-component=comment disabled the check wholesale without the audit trail the documented escape hatch leaves. Adefwhose body carriesdata-component=is still treated as wrapper territory; everything else in the file is checked. The kit's own fixtures gained a finding the moment this landed. - Prose is no longer mistaken for markup.
@doc/@moduledocheredocs and HEEx/HTML comments are blanked (newlines preserved) before scanning. The kit's owncomposite_components.extemplate spells outdata-component="<App>Web.CompositeComponents.<name>"and says "never raw<button>/<input>/<textarea>" — so every freshly installed host opened with four phantom warnings, one of them quoting the sentence telling them not to do it. A clean install now lints clean. - Every primitive on a line is reported, not just the first
(
Regex.scan, notRegex.run) — fixing one used to reveal the next. - Capitalised remote components are no longer flagged. The tag
regex was case-insensitive, so
<Input.autocomplete />read as a raw<input>. HEEx reserves lowercase for HTML and capitalised for components; the regex now matches accordingly. ~H'''heredocs are scanned (the sigil pattern missed the single-quote delimiter, so those files were skipped entirely).- The violation message no longer suggests
# jobykit:allow-raw-htmlinside a template — in a~Hblock a#line is literal text that renders into the page.
Two new rules, both from demonstrated harm in consumer apps:
:forked_wrapper(warning) — flags components excluded fromimport JobyKit.CoreComponentsand replaced with a host copy. One app forkedbutton/1andtable/1; the 0.2.1table/1fix shipped into a component it never renders, while its fork carried the identical bug. Nothing surfaced that. Entries now also carryforked_from_kitin/design.json, so "did that fix reach us?" is a lookup instead of an archaeology exercise.:duplicated_class_string(warning) — the same ≥25-characterclassstring appearing three or more times. The kit's own CLAUDE.md names this as the symptom that markup wants lifting into a wrapper, but nothing checked it: one app pasted the same header class string ten times and linted clean.
mix joby_kit.new moved to its own package
Action required if you install the archive:
mix archive.uninstall joby_kit
mix archive.install hex joby_kit_new
The generator now lives at
joby_kit_new. Nothing else
changes — joby_kit.install, bootstrap, gen.wrapper and lint stay
in this package, which is the point.
joby_kit.new has to run before a project exists, so it can only be a
Mix archive — and an archive puts everything it contains on the global
code path. With the in-project tasks in the same package, the archive's
copy of them shadowed each project's own dependency. Concretely: with a
project pinned to 0.2.3 and a 0.2.0 archive installed,
mix joby_kit.install generated 0.2.0 scaffolding, and re-running
changed nothing. An app's mix.exs did not control which JobyKit
generated its files — whichever archive was on the machine did. That
also explains generated-file drift across a fleet.
Only joby_kit.lint escaped, by accident: it declares
@requirements ["compile"], which loads the project's dependencies
first, so the dependency's copy won.
Splitting removes the possibility instead of detecting it, and follows
Phoenix's arrangement — phx_new is a separate hex package from
phoenix. A version guard was considered and rejected: Mix gives no way
to prefer the dependency's task over an archive's, so a warning would
have left users stuck rather than fixed.
New components
Three of the four components consumers had built for themselves. The
fourth, an icon button, is covered by button/1's new shape.
badge/1— status chip with a semantic tone (neutral | ok | warn | danger | info). One app maintained five separate tone-to-class functions mapping the same states to border+bg+text triples, and had copied one of them verbatim into a second file — precisely the drift the kit's guidance warns about.neutralanddangerdeliberately mean the same thing here as onbutton/1.eyebrow/1— the small uppercase label. This was the single most-duplicated string in the fleet: 326 hand-typed instances in one app, with letter-spacing drifting across nine values and six font sizes.card/1andheader/1now render their:eyebrowslots through it, so the kit stops carrying three copies of the string itself.modal/1— server-driven dialog. Two apps built one and both hit the same three problems, so those are what it solves: visibility is a plain assign rather than client state, so it can't disagree with the LiveView that owns it; oneon_cancelcovers the close button, the backdrop, and Escape, instead of the separate close/dismiss handlers apps ended up writing; andstaticrenders the box in flow for design pages, since.modalisposition: fixedand would otherwise cover the page it's being previewed on.list/1loosened rather than demoted. It had zero uses in one app and was bypassed in another because the forcedfont-boldtitle couldn't be overridden;title_classreplaces it. It stays aulbecause daisy'slistrequires that shape. The starter page's build order now uses it.
Registering these surfaced a small proof the new linter works: the
shipped previews template repeated one layout string four times and
tripped :duplicated_class_string on a fresh install. Extracted to a
private helper — a fresh install lints clean again.
Starter app and theming
The generated app is the kit's own worked example, so it has to be
exemplary. It wasn't: it hand-rolled markup the linter would flag, and
it dropped the theme switching every mix phx.new app ships with.
- New
theme_toggle/1— a segmented system / light / dark control, registered and previewed like any other wrapper.mix joby_kit.newrestores the theme script Phoenix puts inroot.html.heex(which applies the stored choice before first paint, so there's no flash) and wires the control into the layout. Phoenix builds its version from raw<button>elements; this one composes<.button shape="square">, so the kit's own chrome satisfies the contract the kit enforces. simple_nav/1no longer paints its own surface, and accepts globals. It carriedbg-base-100 border-b, which produced a visible seam wherever a layout wrapped it in its own sticky bar: the bar's translucent background showed at the edges while the nav painted an opaque strip only as wide as its max-width container. Surface belongs to the container. It also gains an:actionsslot for trailing controls,aria-current="page"on the active link, and theattr :rest, :globalit was missing — the kit's own component had been violating the contract.simple_navandtheme_toggleare registered in the shipped manifest, with previews, so both appear on/designand in/design.json. The daisy catalogue'snavbarandtheme-controllerentries now read as wrapped.- The landing page is a worked example rather than a welcome page.
It hand-rolled a header, nested a second
<main>inside the layout's, and styled sections with one-off classes. It's now built entirely from wrappers —<.header>with the new:eyebrow,<.card>,<.list>for the build order (a real sequence, so the numbering carries information) — and each section names the component that renders it. - Signature cards: long attr defaults no longer collide with the attr
name. The row is a two-column grid whose second track sized to
max-content, so a default like
simple_nav's link list overflowed across the label. Long defaults now take the full-width row thatvalues:already used.
Generators
mix joby_kit.new --no-dashboard/--no-mailerproduced apps that don't compile. Both flags are advertised as forwarded tophx.new, but the router template referencedPhoenix.LiveDashboard.RouterandPlug.Swoosh.MailboxPreviewunconditionally. The template is now conditional, and drops the whole/devscope when neither is present. All four flag combinations are parse-checked in the suite.mix joby_kit.newno longer silently half-succeeds. If thedefp deps do [regex didn't match the generatedmix.exs,Regex.replace/4returned the source unchanged — the task printed "* updating mix.exs" and the failure surfaced much later as an unrelated-looking error about undefinedJobyKitmodules. It now verifies the dep landed and raises with the line to add by hand.- The generated web module keeps Gettext wired. It dropped
phx.new's
use Gettext, backend: …while the app still shipped the backend, so anygettext(...)call in a template failed to compile. Now included, and omitted only for--no-gettext. mix joby_kit.bootstrap's home page no longer models the anti-pattern the kit polices. It hand-rolledcard card-bordered/btn btn-primarymarkup instead of using the wrappers — andcard-borderedhas been dead since daisyUI 5 (the v5 name iscard-border), so the "bordered" card wasn't even bordered. Now uses<JobyKit.CoreComponents.card>/.button, fully qualified because bootstrap runs against an existing app whose ownCoreComponentsmay still be imported.mix joby_kit.gen.wrapper --category compositescaffolded a module that couldn't hold a real composite. A freshcomposite_components.exgot a bareuse Phoenix.Component, so the first<.icon>or~p"/..."in the new composite failed to compile — and the kit's own worked example uses both. It now matches the install template:use <App>Web, :html.- Tests: generated output is parsed (
Code.string_to_quoted) across install, bootstrap, and gen.wrapper. Nothing had ever checked that the code these tasks write is syntactically valid; the suite asserted on strings only. Install also asserts the expected file count, so a template added without a test can't slip by.
daisyUI catalogue
Reconciles DaisyCatalogue with daisyUI 5.7.16, verified against the
published package rather than the docs prose.
- Two demos taught removed daisyUI 4 classes. The card demo used
card-compact(v5 replaced the single compact modifier with thecard-xs/sm/md/lg/xlscale) and the label demo usedlabel-text(v5 dropped theform-control/label-textpairing entirely). Both are absent from 5.7.16's CSS — confirmed by grepping the shippedcomponents/*.css. The page that exists to be the reference was teaching a dead API. - Nine docs links 404'd.
docs_url/1derived the URL from the display name, which breaks wherever our label differs from daisy's page name — "Chat bubble" →/components/chat-bubble/, "Text Input" →/components/text-input/, all four mockups reversed (browser-mockupvsmockup-browser), and so on. Entries now carry an optional:docs_slug, anddocs_url/1accepts a catalogue entry (the bare-name form still works). Every one of the 68 links was checked against daisyui.com and now resolves. - Six primitives were missing, all added after 5.0:
hover-gallery(5.1),hover-3dandtext-rotate(5.5),aura,megamenu, andotp(5.6). Each carries a:sincekey and says so in its note, because hosts vendor their ownassets/vendor/daisyui.js— a host on an older bundle simply does not have those classes.daisy_version/0now reports which daisy release the catalogue was verified against. merged/1silently dropped every daisy override for a manifest module that happened not to be loaded yet.function_exported?/3answers false for an unloaded module, so wrapped primitives showed as unwrapped depending on load order. Now guarded withCode.ensure_loaded?/1.- Tests: a
:external-tagged case verifies every docs link over the network (excluded by default;mix test --include external), plus guards that no demo reintroduces a removed v4 class and that every:sinceentry names its version in its note. Also fixed a seed-dependent flake in the new lint-task tests, where Mix's own==> appbanner interleaved with captured task output.
v0.2.3
Fixes a crash that takes down any form with an array-typed field, plus
two CoreComponents corrections and the missing half of the shipped
manifest.
translate_error/1: no longer raises on non-String.Charserror opts. It stringified every opt eagerly instead of deferring toString.replace/4's function form, so the opts Ecto attaches to a cast error on an{:array, _}field —[type: {:array, :string}, validation: :cast]— raisedProtocol.UndefinedErroron the tuple. Every<.input field={...}>routes errors through this, so the first invalid submit on any form with an array or composite-typed field returned a 500. Same class of crash forvalidate_subset/validate_inclusionopts carrying atom lists. If you worked around this by overridingtranslate_error/1, you can drop the override.button/1:typenow passes through. It was neither a declared attr nor in the:globalinclude list, so<.button type="button">emitted an "undefined attribute" warning and the attribute was dropped — every button inside a form submitted it, with no way to opt out. Omittingtypestill leaves the attribute off, so existing submit buttons are unaffected.formpasses through too.flash/1+flash_group/1: one toast container per page instead of one per notice. Eachflash/1rendered its own fixed-positiontoast toast-top toast-endcontainer, so simultaneous notices — an:infoand an:errorfrom the same action, or a flash plus a disconnect toast — stacked at the identical fixed position and occluded each other. Thetoastcontainer now lives onflash_group/1and every notice stacks inside it. Behavior change:flash/1rendered on its own is now an inlinealertand no longer positions itself. If you callflash/1outsideflash_group/1and relied on it floating, wrap it in your own positioned container. Callingflash_group/1from your root layout — the documented path — needs no change. The container ispointer-events-none(notices re-enable it), so the now-always-present fixed element can't intercept clicks in an empty corner.:infonotices arerole="status"rather than the interruptingrole="alert"; errors stay assertive. Both components now declareattr :class, so a caller class merges into the root instead of colliding with the identity classes.- The install manifest registers all nine shipped wrappers. It
listed only
button,card,icon,input, andflash, while the kit also shipsheader,list,table, andflash_group— shipped, documented, and invisible on/designand in/design.json, so agents following the build order re-wrapped or hand-rolled them.daisy_overrides/0likewise now reports every primitive the kit actually wraps (alert, toast, list, table, and the four input types) instead of just button and card. Existing apps: re-runmix joby_kit.install --forceto pick up the new registrations, or copy the entries into yourDesignManifestby hand. (flash_groupis registered without a preview on purpose — it's a fixed-position container, so an inline preview would float over the design page rather than sit in its card.)
Note for hosts that forked a core component (import JobyKit.CoreComponents, except: [...]): the button/1 and flash/1
fixes above land in the kit's copy, not yours. Apply them to your fork.
v0.2.2
Docs-only release. No functional change — skip it if 0.2.1 is working for you.
- ExDoc no longer tries to link the internal patcher modules
(
JobyKit.AgentsMd,JobyKit.ClaudeMd,JobyKit.NavPatcher,JobyKit.AppCss), which are@moduledoc falseon purpose, orPhoenix.Component.__components__/0, which is hidden upstream. They render as plain code viaskip_code_autolink_to, so the published docs build warning-free.
v0.2.1
Two rendering fixes, both in CoreComponents:
flash/1: every toast gets a usableidagain.attr :idalready puts:idin assigns, so theassign_new/3default never fired and any flash rendered without an explicit id — both toastsflash_group/1shows, i.e. the ones users actually see — rendered with noidattribute and a dismiss handler ofJS.hide(to: "#").#is not a valid selector, so clicking a flash threwFailed to execute 'querySelectorAll' on 'Document'and the toast never faded out. Thelv:clear-flashpush runs first, so the flash still cleared — which is how this stayed hidden behind a working-looking dismiss.table/1: action cell gainswhitespace-nowrap. Wrappable button text let the cell under-report min-content under thew-0width hack, so action buttons painted past the table edge on full-width tables. Host apps carrying a scoped nowrap override for[data-component="JobyKit.CoreComponents.table"]can drop it after upgrading.
v0.2.0
Wrapper-contract enforcement: agent-experience fixes for the failure
mode where an agent skips the manifest, drops raw HTML primitives into
.heex, and gets a green lint check anyway. This release closes that
loop with a real lint rule, an auto-loaded CLAUDE.md, and a worked
composite example. Existing consumers may see new warnings on first
mix joby_kit.lint run after upgrading — that's the rule firing on
pre-existing violations; silence per-line with
<%!-- jobykit:allow-raw-html --%> or lift the markup into a wrapper.
Linter
- New
:raw_html_primitiverule (warning). Scans.heexand~Hblocks in.exfor raw<button>,<input>,<textarea>, and<select>outside of wrapper definitions. Files containingdata-component=are treated as wrapper territory and skipped. Per-line opt-out via<%!-- jobykit:allow-raw-html --%>(heex) or# jobykit:allow-raw-htmlon the preceding.exline. Inside Elixir string literals (heuristically detected) doesn't fire. mix joby_kit.lintdefault--pathsnow includeslib/**/*.heexalongsidelib/**/*.ex.
CLAUDE.md
mix joby_kit.installnow writes aCLAUDE.mdblock at the project root (auto-loaded by Claude Code) that inlines the highest-priority wrapper-contract diagnostics — including "Symptoms you skipped step 1" and the per-line opt-out syntax. Idempotent and marker-bracketed likeAGENTS.md.- New
JobyKit.ClaudeMdpatcher mirrorsJobyKit.AgentsMd.
Worked composite example
mix joby_kit.installnow scaffolds<App>Web.CompositeComponentswith a workingempty_state/1composite (icon + title + supporting text + optional action slot). Pre-registered in the generatedDesignManifestand previewed on/custom-designs. Pattern-match on it when adding your own composites.
Bootstrap stdout
mix joby_kit.new,mix joby_kit.install, andmix joby_kit.bootstrapend-of-run summaries now inline the "Symptoms you skipped step 1" diagnostic (raw<button>/<input>, private function components styled as primitives, components missingdata-component/:rest/ manifest entry). Calling out the failure modes in stdout means an agent doesn't have to know to openAGENTS.mdto find them.
v0.1.1
Documentation + first-run UX fixes for hex consumers:
mix joby_kit.newnow defaults to a hex dep ({:joby_kit, "~> X.Y"}) when no--joby-kit-pathis given. The flag stays for kit development; the README and moduledoc lead with the hex install path (mix archive.install hex joby_kit).README.mdandmix joby_kit.newmoduledoc rewritten to drop references to local checkouts and.ezbuild steps.mix joby_kit.newskips the path-depapp.cssrewrite when running in hex mode (the install task's default@sourcealready resolves for hex deps).
No runtime API changes.
v0.1.0
Initial release. JobyKit ships:
Manifest + design pages
JobyKit.Manifest— behaviour +__using__macro for declaring a host component manifest.category/2andcomponent/3macros register entries;@before_compilegeneratesentries/0,by_category/0,categories/0,category_label/1,category_description/1,fetch/2callbacks. The runtimeenrich/1helper introspects each component's attrs/slots viaPhoenix.Component.__components__/0so prop signatures never drift from source.JobyKit.Contract— universal contract content (5-step build order, 5-rule wrapper checklist, 3-layer module taxonomy) as plain data.JobyKit.DaisyCatalogue— canonical list of every daisyUI primitive (62 entries across 7 daisy categories), with stable atom IDs, default statuses, amerged/1overlay that applies host overrides, and ademo/1function component dispatched per primitive.JobyKit.SignatureComponent— per-component signature card renderer.JobyKit.PageComponent— two function components for the design surfaces:page_component/1— the kit's curated/designpage (filters to:coreonly). Optional:custom_pathattr renders an agent-redirect callout pointing new components to the host's custom-designs page.custom_page_component/1— host's/custom-designspage (renders only non-:coreentries) with a breadcrumb back to the kit page.
JobyKit.ManifestController— JSON endpoint serving the combined manifest (kit core + composites + domain) at/design.json. Reads the manifest module fromconn.private[:joby_kit_manifest].
Core components
JobyKit.CoreComponents— kit-shipped wrappers that hosts import:button/1,card/1,header/1,icon/1,input/1,flash/1,flash_group/1,list/1,table/1. Each carriesdata-component="JobyKit.CoreComponents.<name>", declares attrs withvalues:enums, acceptsattr :rest, :global, and treatsclassas additive on top of the daisy primitive class set. Plusshow/2/hide/2JS helpers and a Gettext-freetranslate_error/1.JobyKit.NavComponent—simple_nav/1, a daisyUI navbar primitive with active-link highlighting. Used in the kit-flavoredLayouts.appgenerated bymix joby_kit.new.
Patchers (idempotent host-file editors)
JobyKit.AgentsMd— patches the host'sAGENTS.mdto (a) append the JobyKit guidelines section and (b) walk a list of rule-rewrites that replace stale Phoenix-default rules now superseded by the kit. Four rewrites ship: anti-daisy line,<.input>source rule,<.input>class-override rule,<.icon>source rule.JobyKit.AppCss— adds@source "../../deps/joby_kit/lib";to the host'sassets/css/app.cssso Tailwind v4 scans the kit for class names.JobyKit.NavPatcher— locates the first<nav>or<header>+</ul>in the host'sapp.html.heex(orlayouts.exas a fallback) and inserts kit nav links wrapped in<%!-- jobykit:nav-* --%>markers. Idempotent.
Linting
JobyKit.Lint— engine that verifies the wrapper contract by introspecting a manifest module and scanning the host's source. Four rules::manifest_drift(entry points at non-existent function),:missing_data_component(registered wrapper missing the attribute),:missing_rest_global(registered wrapper missingattr :rest, :global),:unregistered_wrapper(function emitsdata-componentbut isn't in the manifest).
Mix tasks
mix joby_kit.install— installs into an existing Phoenix project: generatesdesign_manifest.ex,design_previews.ex, the two design LiveViews, patchesAGENTS.md, patchesassets/css/app.css, and patches the host's nav with/design//custom-designslinks.mix joby_kit.bootstrap— composesinstallwith greenfield steps for an already-generatedphx.newproject: replaces the default HomeLive, rewiresrouter.ex, and removes the unused PageController/PageHTML.mix joby_kit.new <app_name>— wrapsmix phx.newto generate a new Phoenix app with JobyKit baked in: replaces<app>_web.ex, layouts, and router with kit-flavored variants; deletes the redundant Phoenix scaffolding; runsmix joby_kit.install; runsmix assets.setupandmix assets.buildsomix phx.serverworks on the first try.mix joby_kit.gen.wrapper <name>— scaffolds a new wrapper component end-to-end: function skeleton with the contract baked in, manifest entry registered, preview function added.mix joby_kit.lint— CLI for the lint engine. Auto-detects the host's manifest, supports--format jsonfor agent consumption, and--strictto fail on warnings.