JobyKit
An opinionated, agentic-first design-system kit for Phoenix + daisyUI apps.
JobyKit gives your AI coding agents a structured, machine-readable inventory of every UI component your app exposes — with prop signatures, daisyUI basis, rendered previews, and a stable contract — so design and prototype phases don't accumulate a hodgepodge of UI markup that's hard to reason about.
Why this exists
Most Phoenix apps end up with a sprawl of inline Tailwind classes, ad-hoc component shapes, and undocumented variants. AI coding agents working on those apps have to grep through HEEx to discover what's available and often just write new markup from scratch — making the sprawl worse.
JobyKit pushes the codebase in the opposite direction. Your app declares a manifest of its components, and JobyKit serves them at two surfaces:
/design— a curated, kit-uniform page showing your core wrappers (one per daisyUI primitive), the daisyUI catalogue, the wrapper contract, and the build-order decision tree. Same shape across every JobyKit consumer./custom-designs— your app's composites and domain components, separated from the kit's curated inventory so the kit page stays clean./design.json— a single combined JSON endpoint that an agent can fetch to get every component, attr, slot, and source path without parsing rendered HEEx.
Install
Add joby_kit to your deps:
def deps do
[
{:joby_kit, "~> 0.1"}
]
endJobyKit ships function components built on
phoenix_live_view ~> 1.0and assumes daisyUI is installed in your Tailwind config (the default for Phoenix 1.7+ apps generated withmix phx.new). Heroicons via theheroiconsTailwind plugin is also expected for the chevron in the signature card disclosure.
Generators
Two mix tasks scaffold the manifest, previews, and LiveViews so you don't have to write the boilerplate by hand:
# Existing project: generates four files under lib/<your_app>_web/ and prints
# the routes you need to add to router.ex. Idempotent — skips files that
# already exist (use --force to overwrite).
mix joby_kit.install
# Inside an already-generated phx.new project: composes joby_kit.install
# with three extra steps — replaces the default `get "/", PageController,
# :home` route with `live "/", DesignSystemLive, :index`, adds the
# /custom-designs and /design.json routes inline, and deletes the unused
# PageController and PageHTML modules. Use --keep-page-controller to
# leave them in place.
mix joby_kit.bootstrap
# To generate a brand-new app with JobyKit baked in from scratch.
# Wraps `mix phx.new`, swaps out the default Phoenix HTML scaffolding
# for kit-flavored layouts, and pre-registers JobyKit.CoreComponents
# in the manifest.
mix joby_kit.new my_app
# To run mix joby_kit.new from any directory (no Mix project required),
# install the kit as a Mix archive:
cd /path/to/joby_kit
mix archive.build
mix archive.install ./joby_kit-0.1.0.ez
# Then from anywhere:
mix joby_kit.new my_app --joby-kit-path /path/to/joby_kit
After either task, restart mix phx.server, visit /design and
/custom-designs, and curl /design.json to see the manifest.
The rest of this README walks through the same steps manually for projects that prefer a hand-rolled wiring.
1. Declare your manifest
defmodule MyAppWeb.DesignManifest do
use JobyKit.Manifest
alias MyAppWeb.{CoreComponents, DesignPreviews}
category :core,
label: "Core wrappers",
description: "One wrapper per daisyUI primitive."
category :composite,
label: "Composites",
description: "Multi-primitive patterns reused across domains."
category :domain,
label: "Domain composites",
description: "Composites tied to a product area."
component CoreComponents, :button,
category: :core,
daisy_basis: "btn",
summary: "Standard text button.",
preview: &DesignPreviews.button_preview/1
component CoreComponents, :badge,
category: :core,
daisy_basis: "badge",
summary: "Inline status label.",
preview: &DesignPreviews.badge_preview/1
# ...one component/3 call per Bardo wrapper
@doc """
Tells JobyKit which daisyUI primitives are wrapped, so the catalogue
rendering flips them to :wrapped and links to the matching signature card.
"""
def daisy_overrides do
%{
button: %{wrapper: "<.button>", anchor: "#jobykit-component-myappweb-corecomponents-button"},
badge: %{wrapper: "<.badge>", anchor: "#jobykit-component-myappweb-corecomponents-badge"}
}
end
end2. Write preview functions
defmodule MyAppWeb.DesignPreviews do
use Phoenix.Component
# import your CoreComponents wrappers as needed
def button_preview(assigns) do
~H"""
<button class="btn btn-primary">Click me</button>
"""
end
# ...one *_preview/1 per registered component
end3. Wire the routes
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :authenticated_json do
plug :accepts, ["json"]
plug :fetch_session
plug :put_secure_browser_headers
# plug your auth pipeline; the controller does not enforce auth
end
scope "/", MyAppWeb do
pipe_through [:browser, :require_authenticated_user]
live "/design", DesignSystemLive, :index
live "/custom-designs", CustomDesignsLive, :index
end
scope "/" do
pipe_through :authenticated_json
get "/design.json", JobyKit.ManifestController, :show,
private: %{joby_kit_manifest: MyAppWeb.DesignManifest}
end
end4. Wrap the page components
defmodule MyAppWeb.DesignSystemLive do
use MyAppWeb, :live_view
def mount(_, _, socket), do: {:ok, assign(socket, page_title: "Design System")}
def handle_event(_, _, socket), do: {:noreply, socket}
def render(assigns) do
~H"""
<Layouts.app flash={@flash}>
<JobyKit.PageComponent.page_component
manifest={MyAppWeb.DesignManifest}
custom_path={~p"/custom-designs"}
/>
</Layouts.app>
"""
end
end
defmodule MyAppWeb.CustomDesignsLive do
use MyAppWeb, :live_view
def mount(_, _, socket), do: {:ok, assign(socket, page_title: "Custom Designs")}
def handle_event(_, _, socket), do: {:noreply, socket}
def render(assigns) do
~H"""
<Layouts.app flash={@flash}>
<JobyKit.PageComponent.custom_page_component
manifest={MyAppWeb.DesignManifest}
back_to={~p"/design"}
/>
</Layouts.app>
"""
end
end
That's it. Visit /design for the kit-curated catalogue; visit
/custom-designs for your app's composites; curl /design.json to fetch the
combined inventory for your AI agent.
The contract
JobyKit ships a five-step build order every consumer's /design page
displays:
- Domain composite? Use it. Lives in a domain-scoped component module in this app. Surfaces on the custom-designs page.
- Generic composite? Use it. Multi-primitive pattern reused across domains in this app. Surfaces on the custom-designs page.
- Core wrapper? Use it. One wrapper per daisyUI primitive, defined
in
core_components. Surfaces on the kit page. - daisyUI primitive? Wrap it as a core component first, then use the
wrapper. The daisyUI catalogue at the bottom of
/designlists every primitive. - Build from tokens. Tailwind + theme tokens only. Expose the result as a core wrapper or composite and register it in the manifest.
And a five-rule wrapper contract every Bardo component must satisfy:
-
Declare every prop with
attr. -
Carry
data-componenton the root element. -
Accept
:rest, :global. - Internals compose tokens + daisyUI primitives only.
- Register every component in the host manifest.
The agent surface (/design.json) is a single source of truth even when the
two pages render different subsets — kit core, generic composites, and
domain composites are all returned together with category labels.
Status
v0.1.0 is the initial extraction from the Bardo project. The runtime
shape is stable; generators (mix joby_kit.install,
mix joby_kit.gen.wrapper, mix joby_kit.lint) are slated for v0.2.0.
License
MIT. See LICENSE.