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:

Install

Add joby_kit to your deps:

def deps do
  [
    {:joby_kit, "~> 0.1"}
  ]
end

JobyKit ships function components built on phoenix_live_view ~> 1.0 and assumes daisyUI is installed in your Tailwind config (the default for Phoenix 1.7+ apps generated with mix phx.new). Heroicons via the heroicons Tailwind 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
end

2. 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
end

3. 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
end

4. 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:

  1. Domain composite? Use it. Lives in a domain-scoped component module in this app. Surfaces on the custom-designs page.
  2. Generic composite? Use it. Multi-primitive pattern reused across domains in this app. Surfaces on the custom-designs page.
  3. Core wrapper? Use it. One wrapper per daisyUI primitive, defined in core_components. Surfaces on the kit page.
  4. daisyUI primitive? Wrap it as a core component first, then use the wrapper. The daisyUI catalogue at the bottom of /design lists every primitive.
  5. 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:

  1. Declare every prop with attr.
  2. Carry data-component on the root element.
  3. Accept :rest, :global.
  4. Internals compose tokens + daisyUI primitives only.
  5. 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.