Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
Agents
emaia/laravel-hotwire
claude-code
cursor
hotwire
turbo
stimulus
blade-components
artisan-commands
presets

The complete Hotwire stack for Laravel — Turbo Drive, Turbo Streams, Stimulus controllers and Blade components.

Install
php artisan boost:add-skill emaia/laravel-hotwire

Save this content to: AGENTS.md

# Laravel Hotwire

The complete Hotwire stack for Laravel — Turbo Drive, Turbo Streams, Stimulus controllers and Blade components.

## Package Structure

- `src/` — PHP source (commands, components, service provider)
- `src/Components/Concerns/` — shared component traits (e.g. `StripsNullProps` for omitting null props from rendered
  attributes)
- `src/Support/` — framework-agnostic helpers (`FieldKey` for id/errorKey derivation, `MaskPresets`, `ControllerImports`
  for resolving a controller's shared JS deps, doc search/render/paging, installer)
- `src/Registry/catalog.php` — single source of truth mapping every component and controller to its class/source, view,
  docs, category and dependencies
- `src/Registry/styles.php` — preset-neutral visual module ownership, dependencies and canonical source order
- `resources/js/controllers/` — Stimulus controllers shipped with the package (published to user's app)
- `resources/views/component-views/` — Blade component views
- `stubs/resources/` — Scaffolding files copied by `hotwire:install`
- `config/hotwire.php` — Package configuration (component prefix)
- `tests/` — Pest PHP tests with Orchestra Testbench
- `tests/Controllers/` — Bun JS tests for Stimulus controllers
- `docs/` — Documentation

## Artisan Commands

| Command                   | Description                                                                                                            |
|---------------------------|------------------------------------------------------------------------------------------------------------------------|
| `hotwire:install`         | Scaffold JS/CSS setup, add npm deps to package.json                                                                    |
| `hotwire:make-controller` | Create a new Stimulus controller (interactive scaffolding)                                                             |
| `hotwire:make-preset`     | Generate a complete custom CSS preset scaffold or clone a shipped preset                                               |
| `hotwire:bundle-preset`   | Generate or regenerate a selective preset bundle from explicit component selections                                   |
| `hotwire:controllers`     | Publish package Stimulus controllers to the app for customization (`--outdated` to update only published+changed ones) |
| `hotwire:components`      | List available Blade components and their controller dependencies                                                      |
| `hotwire:check`           | Verify npm/controller drift, generated CSS and complete application preset contracts (CI-friendly)                      |
| `hotwire:docs`            | Browse and read controller/component docs in the terminal                                                              |
| `hotwire:ide-json`        | Generate Laravel Idea metadata for package and application Stimulus controllers                                        |

## Conventions

### Stimulus Controllers

- **Flat layout** at the top level. File naming: `{name}_controller.js` (snake_case). The user can generate `.ts`
  controllers via `hotwire:make-controller --ts`; the package ships `.js` only.
- **Identifier** matches the file name converted to kebab-case: `auto_submit_controller.js` →
  `data-controller="auto-submit"`.
- **Substrate folders** (`turbo/`, `optimistic/`, `dev/`) group controllers tied to a specific technical layer. Files
  inside keep Stimulus' `--` separator in the identifier: `turbo/progress_controller.js` →
  `data-controller="turbo--progress"`.
- **No UI-role folders** (no `form/`, `modal/`, `utils/`, `lib/`, `media/`, `notification/`). Names themselves describe
  intent — prefer compound names (`copy-to-clipboard`, `lazy-image`, `input-mask`) over namespace buckets.
- **Internal helpers** prefixed with `_` (e.g. `_focus_trap.js`, `_presence.js`, `_form_errors.js`) are shared utility
  modules imported by controllers. They are **not** Stimulus controllers and are never registered via
  `data-controller`.
- **Package marker.** Every controller, helper, and shared dependency shipped by the package begins with
  `// @hotwire-package` on its first non-empty line (or `/* @hotwire-package */` for `.css`). The marker lets
  `hotwire:controllers` (with `--force` or `--outdated --force`) and `hotwire:check --fix` distinguish files that came
  from the package from files written by the user — without the marker, those commands refuse to overwrite.
  `hotwire:make-controller` deliberately does **not** emit the marker, so generated user controllers stay protected.
  Drop the marker when reading `Support\PackageMarker`.
- Loaded via `@emaia/stimulus-lazy-loader` with Vite's `import.meta.glob`.
- **Controllers must be mutually compatible.** Blade components stack several controllers on the same element
  (`<hw:form>` → `auto-submit unsaved-changes clean-query-params`; `<hw:file>` →
  `file-preserve reset-files` plus the user's own `data-controller`). A controller must therefore never assume it owns
  the element exclusively:
    - Scope DOM reads/writes to `this.element` (and prefer `this.targets`); don't clobber attributes other controllers
      set, especially shared `data-controller`.
    - Multiple controllers commonly listen to the same Turbo events (`turbo:submit-end`, `turbo:morph`,
      `turbo:before-render`/`turbo:render`). Coordinate on shared semantics (e.g. submit `success`) and make each
      handler idempotent and order-independent rather than mutating state another controller relies on.
    - Clean up every listener and timer in `disconnect()` so re-renders/morphs don't leave duplicates behind.
    - When a controller is activated by a component prop, expose it as an explicit prop and let the component merge the
      identifier — see the Blade Components `data-*` filtering rule below.

### Blade Components

- Registered with configurable prefix (default: `hw`)
- See all available components with `php artisan hotwire:components`
- Components that encapsulate a Stimulus controller merge user-provided `data-controller` with internal controllers on
  the element. User-provided `data-{identifier}-*` for internal controllers active via props is filtered to prevent
  conflicts; for other controllers these pass through freely. Expose supported controller configuration as explicit
  Blade props instead of relying on user-provided `data-*` attributes.
- **Exposing a `controller` prop.** A component exposes `controller` — letting the app swap the Stimulus identifier for
  its own subclass — only when the controller encapsulates configuration the package cannot proxy through props: ECharts
  `option`, Leaflet layers, Tiptap extensions, Embla plugins, the File Upload pipeline. Where the controller's surface is
  closed (overlays sharing `_overlay.js`, the Floating UI positioners, satellites like `char-counter`, `clear-input` or
  `timeago`), the extension path is composition — the merge rule above already lets the app stack its own
  `data-controller` on the element and react to the component's events. Every `controller` prop is permanent public API,
  one more entry point to validate, and one more identifier flowing through `@aware`, so open one on demand rather than
  for symmetry: reach for an event or a specific prop first.
- Components that derive field ids/error keys (Input, File, Select, etc.) use `Support\FieldKey`; components that omit
  null props from the rendered tag use the `StripsNullProps` concern. Reuse these instead of reimplementing.

### Registry

`src/Registry/catalog.php` is the public component and controller contract — `hotwire:components`,
`hotwire:controllers`, `hotwire:check` and `hotwire:docs` all read from it. `src/Registry/styles.php` separately owns
visual CSS modules, their dependencies and source order. **Any new component or controller must be registered in the
catalog, and any visual ownership must also be registered in styles.php**, or the commands won't see it.

- `components` entries: `class`, `view`, `docs`, `category`, `description`, and `controllers` (the list of Stimulus
  identifiers the component depends on — keep it in sync with what the Blade view actually mounts, since
  `hotwire:check` verifies these are published). A family's root component declares its slots once in a public `SLOTS`
  constant as local key => public name + `visual|structural`; child classes and views reference those local keys. The
  catalog projects the declaration through `styling.slots` class references, optionally narrowed with `only`.
- `controllers` entries: `source` (path to the `.js`/`.ts` file), `docs`, `category`, `description`, and optional
  `npm` (a `package => version` map for third-party deps like `@floating-ui/dom`, `maska`, `echarts`). Controllers that
  create `data-slot` values in JavaScript declare them in `slots` too.
- Add every new package-emitted `data-slot` to the owning family declaration and reference that declaration from the
  catalog. Do not recover this metadata by parsing Blade, PHP or CSS at runtime; the projected registry is the contract
  used by preset generation and coverage tests.
- Identifiers follow the Stimulus naming rules above — substrate-folder controllers use the `--` separator
  (`turbo--progress`, `optimistic--form`, `dev--log`).
- Every registered component/controller should ship a matching doc file under `docs/` at the path given in the entry.
- **`description` is public presentation.** It reaches an app developer through `hotwire:docs` and
  `hotwire:components` who has never read the source and is deciding whether the entry solves their problem, so it
  says what the entry gives them and where it applies — [`docs/registry.md`](docs/registry.md#descriptions) covers
  the form:
    - Name the thing, then the capabilities that decide adoption: modes, Laravel and Turbo integration,
      accessibility, limits.
    - Name a library the app itself adopts (Embla, ECharts, Tiptap, Leaflet, Maska, Lucide); keep the package's own
      plumbing out (Presence, outlets, Floating UI, header names, morph internals) and describe its effect instead —
      `X-Turbo-Frame-Src` became "Sends the Turbo Frame source URL", `IntersectionObserver` became "enters view".
    - Drop in-house shorthand — "items shortcut", "sticky sugar", "composed subcomponents", "state-driven motion" —
      and empty restatements like "Displays a button or a component that looks like a button".
    - A description that outlived its implementation is wrong, not merely dated; revisit it whenever a prop or option
      changes what the entry does.

### Turbo

- Provided by `emaia/laravel-hotwire-turbo` dependency
- Fluent stream builder: `turbo_stream()->append(...)` (the builder is `Responsable` — return it directly from the
  controller; use `->withResponse(...)` when you need a custom status or headers)
- Request detection: `request()->wantsTurboStream()`, `request()->wasFromTurboFrame()`, `request()->turboFrameId()`,
  `request()->turboFrameSource()`
- DOM helpers: `dom_id($model)`, `dom_class($model)`

## Collaboration Rules

- Do not expose local absolute paths in user-facing messages.
- Before committing, pushing, opening a PR, creating a tag or publishing a release, get the user's explicit confirmation of the action. One confirmation may cover a clearly specified sequence; ask again if its scope changes. Approval of the exact wording is only required if the user requests it.

## Release Workflow

### Commits

- **Subject**: Imperative mood, no period at end (e.g. `Add progress bar and counter targets to carousel`)
- **Body**: Optional. Explain rationale, tradeoffs or technical details when they help understand the change; keep it clear and avoid narrating what the diff already shows. Use `-` bullets only for distinct, relevant points, not to meet a quota
- **PR reference**: Appended as `(#N)` in the subject when applicable
- Always signed (GPG)

### Pull Requests

- Push feature branch, open PR on GitHub
- Branch naming: descriptive kebab-case (e.g. `radio-group`, `alert-dialog`)
- PR title matches commit subject convention; body summarizes the outcome and why it matters
- Review required; merge from the GitHub UI (not the CLI)
- Remote merge (GitHub UI):
    - **Default: squash-merge** — for PRs where iterative review/fixup commits should collapse into a single clean
      commit reflecting the deliverable
    - **Merge commit (no squash)** — for PRs that bundle multiple isolated changes that each deserve their own commit in
      history
- **PR body template** — `## Summary` (a short paragraph or bullets for distinct principal changes) + `## Test plan`.
  Include only applicable automated checks and manual smokes for what tests cannot verify (visual, browser-specific
  behavior, real interaction). Use checkboxes for actual verification steps; do not add filler to reach a bullet count.
- Write the PR Summary in concise, natural language for a reviewer: focus on the deliverable, relevant decisions and
  limitations. Add technical details only when they clarify behavior, impact or review; don't narrate files or obvious
  diff details. Omit supporting implementation details such as catalog registration, IDE metadata, docs, examples, and
  test coverage unless they are themselves the purpose of the PR.

Example (include only checks that apply):

  ```markdown
  ## Test plan

  - [x] `composer test` — N/N passing
  - [ ] Manual smoke: <exercise the changed interaction and expected result, when relevant>
  ```

  Skip the manual lines that don't apply (a pure internal refactor with full test coverage may legitimately have only
  the automated lines). For Stimulus controllers and Blade components, default to manual smokes for browser-observable
  behavior, interaction, accessibility, and structural CSS. Include a pure visual smoke only when the change
  intentionally affects appearance.

### Tag and Release

- Tags are created on `main` after the PR is merged remotely
- Versioning follows `X.Y.Z` semver:
    - Patch (`X.Y.Z+1`): bugfixes
    - Minor (`X.Y+1.Z`): new features
- Annotated tag: `git tag -a X.Y.Z -m "X.Y.Z"`
- Release created via `gh release create X.Y.Z --title "X.Y.Z" --notes-file /tmp/release-notes.md`
- Release notes format (following the `X.Y.Z` template):
    - Markdown title with feature name (e.g. `## Carousel progress bar and slide counter`)
    - One-sentence summary
    - Section per feature and referer docs for examples
    - `**Full Changelog**: https://github.com/emaia/laravel-hotwire/compare/<prev>...<version>` at the end
- CHANGELOG.md is updated automatically by the release workflow; do not edit manually

## Development

```bash
composer test          # Run Pest tests
composer analyse       # Run PHPStan
bun run test           # Run JS unit tests (Bun + happy-dom). Use `bun run test`, not `bun test` — the npm script wires up --isolate so mocks don't leak across files
bun run test:browser   # Run browser tests (Playwright)
composer format        # Run Pint code formatter
```

### Testing (TDD)

Follow test-driven development for testable behavior and contracts: write tests first, then implement. Purely visual CSS
changes follow the CSS test scope below.

There are three test suites:

- **PHP** (`composer test`) — Pest + Orchestra Testbench. Covers commands, components, registry.
- **JS** (`bun run test`) — Bun test runner + happy-dom. Covers Stimulus controllers in `tests/Controllers/*.test.js`.
- **Browser JS** (`bun run test:browser`) — Playwright. Covers browser-dependent Stimulus behavior in
  `tests/Browser/*.pw.js`.

#### CSS test scope

Automate CSS coverage when a rule provides a structural, behavioral, accessibility, compilation, or documented public
contract. Examples include required visibility, scrolling, clipping, positioning, interaction state, focus visibility,
contrast, reduced motion, forced-colors support, and public values emitted through CSS custom properties.

Do not add source snapshots or exact computed-style assertions solely to freeze visual choices such as colors, shadows,
border radii, typography, decorative motion, or non-functional spacing. Validate intentional appearance changes with a
focused manual smoke; they do not require a failing automated test. Stylesheet imports, generated output, slot and axis
coverage, catalog ownership, and structural browser behavior remain subject to TDD.

TDD flow:

1. **Write the failing test** in the appropriate suite
2. **Run only that test** to confirm it fails:
    - PHP: `vendor/bin/pest --filter='test name'`
    - JS: `bun test --isolate tests/Controllers/<name>_controller.test.js`
    - Browser JS: `bun run test:browser -- tests/Browser/<name>.pw.js`
3. **Implement** the minimum code to make it pass
4. **Run the test again** to confirm it passes
5. **Repeat** for the next behavior

PHP conventions:

- Test files mirror `src/` structure: `src/Commands/FooCommand.php` → `tests/Commands/FooCommandTest.php`
- Use Pest syntax (`it()`, `test()`, `expect()`) — no PHPUnit classes
- Group related tests with comment headers: `// --- Section name ---`
- Use `beforeEach`/`afterEach` for shared setup and cleanup (temp files, directories)
- For artisan commands: use `$this->artisan('command')->assertSuccessful()` and `expectsQuestion`/`expectsChoice`/
  `expectsOutput` for interactive flows
- Always run `composer test` at the end to ensure nothing else broke

JS conventions:

- One test file per controller: `tests/Controllers/<name>_controller.test.js`
- Use `mountController` from `resources/js/helpers/test_stimulus.js` to set up the DOM and Stimulus
- Always call `mounted?.cleanup()` in `afterEach`
- Always run `bun run test` at the end to ensure nothing else broke (the script applies `--isolate`)
- The suite runs with `bun test --isolate` (Bun ≥1.3.10): each file gets its own JSGlobalObject, so
  `mock.module` registrations don't leak between files. Drop the flag once Bun 1.4 makes isolation the default.
- **Do not add `--parallel`.** Bare, it defaults to the CPU core count (32 on the maintainer's machine), which drove a
  single worker to 11.8 GB and OOM-killed the entire WSL VM. Pinned low it is still a bad trade: measured on 32 cores,
  `--parallel=4` failed 4 runs in 8 (~7s) and `--parallel=2` 2 in 4 (~14s), against ~3 in 5 serial (~31s) — and under
  any worker count the flaky tests below stop responding entirely rather than merely running late.
- **Behaviour that hinges on MutationObserver delivery belongs in Playwright, not here.** happy-dom drains those
  callbacks unreliably once many files share a process: failure probability scaled with how many files ran first
  (5 files → 0 of 6 runs, 15 → 2 of 6, 40 → 3 of 6), and in a failing run the expected transition never arrived at all,
  so widening poll budgets did not help — Bun caps a single test at 5s regardless. Two mitigations are in place, and
  the Bun suite has been green 10 runs running since:
    - `resources/js/helpers/test_stimulus.js` drops its `globalThis` bindings in an `afterAll`, so each file releases
      its happy-dom `Window` instead of pinning one for the life of the process. It must stay in `afterAll`, not in
      `cleanup()` — many `afterEach` blocks still touch `document` after cleaning up.
    - The cases that turned on target removal or an external value change moved to
      `tests/Browser/read_more_controller.pw.js` and `tests/Browser/side_panel_controller.pw.js`.
- Use Playwright (`tests/Browser/*.pw.js`) for controller behavior that depends on real browser semantics:
  `MutationObserver`, focus, `requestAnimationFrame`, layout required by a behavioral, accessibility, or documented
  public contract, Turbo frame-like DOM changes, or other complex event timing. Do not use Playwright solely to assert
  decorative spacing or appearance.
- Keep Playwright tests focused and few; prefer `bun run test` for deterministic controller unit behavior.
- Run `bun run test:browser` after changing browser-dependent behavior.

## Dependencies

- PHP: `emaia/laravel-hotwire-turbo`, `spatie/laravel-package-tools`
- JS: `@hotwired/stimulus`, `@hotwired/turbo`, `@emaia/stimulus-lazy-loader`
- Optional JS: third-party libs required by specific controllers — the `npm` maps in `src/Registry/catalog.php` are the
  source of truth (don't list them here)

## Docblock convention

**Selective documentation** — docblocks must add information; if they don't, they're noise. Default to skipping;
document deliberately.

### Decision tree

```
1. Is this method part of the public API that app code calls?
   (Facades, fluent builders, model accessors/scopes, trait methods exposed to consumers,
    interface contracts, public methods on PHP classes)
   → YES  → docblock required (single-line summary at minimum)
   → NO   → step 2

2. Is the purpose non-obvious from name + parameter types + return type?
   (Cross-class interaction, hidden constraint, unusual side effect, surprising return semantics)
   → YES  → docblock focused on WHY, not what
   → NO   → skip — let the code speak
```

### What to write

- **Imperative mood**, single sentence ending in a period: `Persist the upload and return the new Media.`
- **Multi-line only** when 2-3 sentences are required to capture a non-obvious constraint or rationale. If you need
  more, the docblock is masking a design smell — refactor or move the explanation to `docs/*.md`.
- **WHY over what** when explaining: "Extracted so the per-iteration try/catch covers every step" — not "Encodes and
  persists a conversion" (that's the name).

### Native types first

Always declare native PHP types on properties, parameters, and return values when possible. Drop redundant `@var` /
`@param` / `@return` once the native type carries the contract. Untyped signatures combined with a docblock that names
the type are an anti-pattern (the type system can't enforce what the docblock says).

Constructors don't need a return type. PHP's `resource` pseudo-type has no native equivalent — use
`@param resource $stream` / `@return resource` there.

### When to add `@param` / `@return` / `@throws`

- **`@param` / `@return`**: only when they carry information **beyond** the native type (`array<string, callable>`,
  `string[]`, `Collection<int, Media>`, `array{conversion: string, exception: \Throwable}`, semantic constraint like
  "empty array returns all"). Never add when they merely repeat the type.
- **`@throws`**: list exceptions that are part of the method's contract (caller is expected to handle them). Skip
  generic `RuntimeException` of the "if it breaks it broke" variety.
- **Property `@var`**: only when the native type loses information
  (`array<int, array{conversion: string, exception: \Throwable}>`, `MediaChannel[]`,
  `array<string, Collection<int, Media>>`). Plain typed properties don't need it.

### What to always remove

- Auto-generated IDE docblocks: `/** Get the X. */` on `getX()` — pure noise
- `@author`, `@version`, `@since`, `@package` — git/Composer resolve these
- Multi-paragraph essays — move to `docs/*.md` and link
- `@param` / `@return` that just repeat the type-hint
- Comments inside method bodies explaining WHAT the next line does (rename a variable instead); keep only WHY when
  non-obvious
- Stale TODO/FIXME — convert to an issue or delete

## Roadmap

Operational roadmap and execution notes are maintained outside this repository. Treat local planning exports as
temporary context that may be stale.

## CSS / Theming

- **Semantic tokens only.** Component views use `bg-background`, `text-foreground`, `border-border`, etc. Never hardcode
  raw colors (`bg-white`, `text-gray-700`, `bg-zinc-900`). Tokens are defined in
  `resources/css/tokens.css` via `@theme inline` with OKLCH values for light and dark mode.
- **Dark mode via `data-theme`** on `<html>`, not `class="dark"`. Default is `:root` (light);
  `[data-theme="dark"]` activates the dark palette.
- **Tailwind v4 scanner constraint.** Imported presets are processed directly and must not be scanned as content with
  `@source`. Do not add Tailwind utility defaults back to package Blade/PHP unless they are also represented by preset
  CSS or the `@source inline(...)` safelist in
  `resources/css/structural.css`, which every preset imports.
- **Structural vs visual CSS.** Mechanics live in `resources/css/structural.css` — the rules whose absence leaves a
  component broken rather than restyled (the carousel track, the Accordion's `::details-content` collapse). Every preset
  imports that file, so they compile into the application stylesheet and hold on the first paint instead of landing once
  the bundle runs, and no new preset has to rediscover them. Visual styling lives in presets and uses `data-slot`. Mark
  presentation-free or controller-owned slots `structural` in the catalog; `hotwire:make-preset` scaffolds only visual
  slots.
- **`Variants` helper is for application code, not for package components.** `Support\Variants` (CVA-equivalent in PHP)
  stays exported so apps can build their own variant matrices; the Tailwind scanner reaches it there because it scans
  the app's own PHP. Package components must not use it — they emit no classes at all, only `data-slot`,
  `data-variant` and `data-size`, and the preset does the styling. `Support\PresetAxes` reports the attributes a
  stylesheet differentiates — `data-*`, `aria-*` and native attributes alike — as lexical diagnostics only. Presets
  may cover values with base rules or equivalent selectors; conformance must not require identical extracted axes.
- Theming docs for app developers: `docs/theming.md` — token reference, override instructions, colour space notes.
  Upgrade notes for existing apps: `docs/upgrade.md` (deliverable of `0.32.0`).

## PR Checklist

Every PR that introduces a new component or controller must include:

- [ ] **Manual smoke matrix.** Component verified: (a) in isolation, (b) inside a Modal, (c) inside a Dropdown, (d)
  inside a Turbo Frame. Overlay components additionally verified nested inside each other.
- [ ] **Listeners and timers** verified cleaned up in `disconnect()` — Turbo morph re-connects controllers; duplicate
  listeners are a recurring bug source.
- [ ] **Integration opt-in evaluation.** For each new component, assess whether exposing a prop that activates an
  existing controller turns a common user setup into a one-liner. Model: Button + `hotkey`
  (`hotkey="cmd+s"`). Document the decision (adopted or discarded, with rationale) in the PR body.

## New internal helpers

- `_overlay.js` — shared Presence-driven overlay lifecycle (`data-state`, `hidden`, `inert`, FocusTrap, body scroll
  lock, Escape key, focus return and cancelable CSS motion). Consumed by Modal, Alert Dialog, Drawer, Sheet and Sidebar.
  Exports `createOverlay(controller, options)` with animated and immediate open/close lifecycle methods.
- `_overlay_host.js` — create and retire per-opening instances from reusable hosts; correlate Frame requests and
  parent/child ownership. Never resolve a missing origin through the top overlay or let a late response affect a new
  opening.
- `_overlay_return.js` — capture the exact initiating form/region and apply returned HTML; fail safely if that
  destination changes. The origin identifying the child to close and the region receiving its return are separate
  identities.
- `_overlay_presentation.js` — apply size/side per opening, preserve only managed presentation during morph and restore
  host defaults without overwriting unrelated styles.

## Controller auto-loading

All package controllers are loaded directly from the vendor directory via `import.meta.glob` in
`resources/js/controllers/index.js`. No `hotwire:controllers` step is required — controllers work out of the box.

The loader index is a static file copied from `stubs/resources/js/controllers/index.js` by
`hotwire:install`. There is no dynamic generation or version tracking — the stub is the single source of truth.

`hotwire:check` verifies npm dependencies (declared in the catalog `npm` key) and reports outdated/diverged published
controllers. It no longer flags "not published" as a problem since controllers auto-load from vendor.
`hotwire:controllers` remains available for users who want to publish a controller to customize it.

## Workflow

### Teamwork

- Project: Laravel Hotwire
- Operational notebook: `task-workflow.md` (always read before changing Teamwork tasks; contains project-specific rules)

Laravel Hotwire

The complete Hotwire stack for Laravel — Turbo Drive, Turbo Streams, Stimulus controllers and Blade components.

Package Structure

  • src/ — PHP source (commands, components, service provider)
  • src/Components/Concerns/ — shared component traits (e.g. StripsNullProps for omitting null props from rendered attributes)
  • src/Support/ — framework-agnostic helpers (FieldKey for id/errorKey derivation, MaskPresets, ControllerImports for resolving a controller's shared JS deps, doc search/render/paging, installer)
  • src/Registry/catalog.php — single source of truth mapping every component and controller to its class/source, view, docs, category and dependencies
  • src/Registry/styles.php — preset-neutral visual module ownership, dependencies and canonical source order
  • resources/js/controllers/ — Stimulus controllers shipped with the package (published to user's app)
  • resources/views/component-views/ — Blade component views
  • stubs/resources/ — Scaffolding files copied by hotwire:install
  • config/hotwire.php — Package configuration (component prefix)
  • tests/ — Pest PHP tests with Orchestra Testbench
  • tests/Controllers/ — Bun JS tests for Stimulus controllers
  • docs/ — Documentation

Artisan Commands

Command Description
hotwire:install Scaffold JS/CSS setup, add npm deps to package.json
hotwire:make-controller Create a new Stimulus controller (interactive scaffolding)
hotwire:make-preset Generate a complete custom CSS preset scaffold or clone a shipped preset
hotwire:bundle-preset Generate or regenerate a selective preset bundle from explicit component selections
hotwire:controllers Publish package Stimulus controllers to the app for customization (--outdated to update only published+changed ones)
hotwire:components List available Blade components and their controller dependencies
hotwire:check Verify npm/controller drift, generated CSS and complete application preset contracts (CI-friendly)
hotwire:docs Browse and read controller/component docs in the terminal
hotwire:ide-json Generate Laravel Idea metadata for package and application Stimulus controllers

Conventions

Stimulus Controllers

  • Flat layout at the top level. File naming: {name}_controller.js (snake_case). The user can generate .ts controllers via hotwire:make-controller --ts; the package ships .js only.
  • Identifier matches the file name converted to kebab-case: auto_submit_controller.js → data-controller="auto-submit".
  • Substrate folders (turbo/, optimistic/, dev/) group controllers tied to a specific technical layer. Files inside keep Stimulus' -- separator in the identifier: turbo/progress_controller.js → data-controller="turbo--progress".
  • No UI-role folders (no form/, modal/, utils/, lib/, media/, notification/). Names themselves describe intent — prefer compound names (copy-to-clipboard, lazy-image, input-mask) over namespace buckets.
  • Internal helpers prefixed with _ (e.g. _focus_trap.js, _presence.js, _form_errors.js) are shared utility modules imported by controllers. They are not Stimulus controllers and are never registered via data-controller.
  • Package marker. Every controller, helper, and shared dependency shipped by the package begins with // @hotwire-package on its first non-empty line (or /* @hotwire-package */ for .css). The marker lets hotwire:controllers (with --force or --outdated --force) and hotwire:check --fix distinguish files that came from the package from files written by the user — without the marker, those commands refuse to overwrite. hotwire:make-controller deliberately does not emit the marker, so generated user controllers stay protected. Drop the marker when reading Support\PackageMarker.
  • Loaded via @emaia/stimulus-lazy-loader with Vite's import.meta.glob.
  • Controllers must be mutually compatible. Blade components stack several controllers on the same element (<hw:form> → auto-submit unsaved-changes clean-query-params; <hw:file> → file-preserve reset-files plus the user's own data-controller). A controller must therefore never assume it owns the element exclusively:
    • Scope DOM reads/writes to this.element (and prefer this.targets); don't clobber attributes other controllers set, especially shared data-controller.
    • Multiple controllers commonly listen to the same Turbo events (turbo:submit-end, turbo:morph, turbo:before-render/turbo:render). Coordinate on shared semantics (e.g. submit success) and make each handler idempotent and order-independent rather than mutating state another controller relies on.
    • Clean up every listener and timer in disconnect() so re-renders/morphs don't leave duplicates behind.
    • When a controller is activated by a component prop, expose it as an explicit prop and let the component merge the identifier — see the Blade Components data-* filtering rule below.

Blade Components

  • Registered with configurable prefix (default: hw)
  • See all available components with php artisan hotwire:components
  • Components that encapsulate a Stimulus controller merge user-provided data-controller with internal controllers on the element. User-provided data-{identifier}-* for internal controllers active via props is filtered to prevent conflicts; for other controllers these pass through freely. Expose supported controller configuration as explicit Blade props instead of relying on user-provided data-* attributes.
  • Exposing a controller prop. A component exposes controller — letting the app swap the Stimulus identifier for its own subclass — only when the controller encapsulates configuration the package cannot proxy through props: ECharts option, Leaflet layers, Tiptap extensions, Embla plugins, the File Upload pipeline. Where the controller's surface is closed (overlays sharing _overlay.js, the Floating UI positioners, satellites like char-counter, clear-input or timeago), the extension path is composition — the merge rule above already lets the app stack its own data-controller on the element and react to the component's events. Every controller prop is permanent public API, one more entry point to validate, and one more identifier flowing through @aware, so open one on demand rather than for symmetry: reach for an event or a specific prop first.
  • Components that derive field ids/error keys (Input, File, Select, etc.) use Support\FieldKey; components that omit null props from the rendered tag use the StripsNullProps concern. Reuse these instead of reimplementing.

Registry

src/Registry/catalog.php is the public component and controller contract — hotwire:components, hotwire:controllers, hotwire:check and hotwire:docs all read from it. src/Registry/styles.php separately owns visual CSS modules, their dependencies and source order. Any new component or controller must be registered in the catalog, and any visual ownership must also be registered in styles.php, or the commands won't see it.

  • components entries: class, view, docs, category, description, and controllers (the list of Stimulus identifiers the component depends on — keep it in sync with what the Blade view actually mounts, since hotwire:check verifies these are published). A family's root component declares its slots once in a public SLOTS constant as local key => public name + visual|structural; child classes and views reference those local keys. The catalog projects the declaration through styling.slots class references, optionally narrowed with only.
  • controllers entries: source (path to the .js/.ts file), docs, category, description, and optional npm (a package => version map for third-party deps like @floating-ui/dom, maska, echarts). Controllers that create data-slot values in JavaScript declare them in slots too.
  • Add every new package-emitted data-slot to the owning family declaration and reference that declaration from the catalog. Do not recover this metadata by parsing Blade, PHP or CSS at runtime; the projected registry is the contract used by preset generation and coverage tests.
  • Identifiers follow the Stimulus naming rules above — substrate-folder controllers use the -- separator (turbo--progress, optimistic--form, dev--log).
  • Every registered component/controller should ship a matching doc file under docs/ at the path given in the entry.
  • description is public presentation. It reaches an app developer through hotwire:docs and hotwire:components who has never read the source and is deciding whether the entry solves their problem, so it says what the entry gives them and where it applies — docs/registry.md covers the form:
    • Name the thing, then the capabilities that decide adoption: modes, Laravel and Turbo integration, accessibility, limits.
    • Name a library the app itself adopts (Embla, ECharts, Tiptap, Leaflet, Maska, Lucide); keep the package's own plumbing out (Presence, outlets, Floating UI, header names, morph internals) and describe its effect instead — X-Turbo-Frame-Src became "Sends the Turbo Frame source URL", IntersectionObserver became "enters view".
    • Drop in-house shorthand — "items shortcut", "sticky sugar", "composed subcomponents", "state-driven motion" — and empty restatements like "Displays a button or a component that looks like a button".
    • A description that outlived its implementation is wrong, not merely dated; revisit it whenever a prop or option changes what the entry does.

Turbo

  • Provided by emaia/laravel-hotwire-turbo dependency
  • Fluent stream builder: turbo_stream()->append(...) (the builder is Responsable — return it directly from the controller; use ->withResponse(...) when you need a custom status or headers)
  • Request detection: request()->wantsTurboStream(), request()->wasFromTurboFrame(), request()->turboFrameId(), request()->turboFrameSource()
  • DOM helpers: dom_id($model), dom_class($model)

Collaboration Rules

  • Do not expose local absolute paths in user-facing messages.
  • Before committing, pushing, opening a PR, creating a tag or publishing a release, get the user's explicit confirmation of the action. One confirmation may cover a clearly specified sequence; ask again if its scope changes. Approval of the exact wording is only required if the user requests it.

Release Workflow

Commits

  • Subject: Imperative mood, no period at end (e.g. Add progress bar and counter targets to carousel)
  • Body: Optional. Explain rationale, tradeoffs or technical details when they help understand the change; keep it clear and avoid narrating what the diff already shows. Use - bullets only for distinct, relevant points, not to meet a quota
  • PR reference: Appended as (#N) in the subject when applicable
  • Always signed (GPG)

Pull Requests

  • Push feature branch, open PR on GitHub
  • Branch naming: descriptive kebab-case (e.g. radio-group, alert-dialog)
  • PR title matches commit subject convention; body summarizes the outcome and why it matters
  • Review required; merge from the GitHub UI (not the CLI)
  • Remote merge (GitHub UI):
    • Default: squash-merge — for PRs where iterative review/fixup commits should collapse into a single clean commit reflecting the deliverable
    • Merge commit (no squash) — for PRs that bundle multiple isolated changes that each deserve their own commit in history
  • PR body template — ## Summary (a short paragraph or bullets for distinct principal changes) + ## Test plan. Include only applicable automated checks and manual smokes for what tests cannot verify (visual, browser-specific behavior, real interaction). Use checkboxes for actual verification steps; do not add filler to reach a bullet count.
  • Write the PR Summary in concise, natural language for a reviewer: focus on the deliverable, relevant decisions and limitations. Add technical details only when they clarify behavior, impact or review; don't narrate files or obvious diff details. Omit supporting implementation details such as catalog registration, IDE metadata, docs, examples, and test coverage unless they are themselves the purpose of the PR.

Example (include only checks that apply):

## Test plan

- [x] `composer test` — N/N passing
- [ ] Manual smoke: <exercise the changed interaction and expected result, when relevant>

Skip the manual lines that don't apply (a pure internal refactor with full test coverage may legitimately have only the automated lines). For Stimulus controllers and Blade components, default to manual smokes for browser-observable behavior, interaction, accessibility, and structural CSS. Include a pure visual smoke only when the change intentionally affects appearance.

Tag and Release

  • Tags are created on main after the PR is merged remotely
  • Versioning follows X.Y.Z semver:
    • Patch (X.Y.Z+1): bugfixes
    • Minor (X.Y+1.Z): new features
  • Annotated tag: git tag -a X.Y.Z -m "X.Y.Z"
  • Release created via gh release create X.Y.Z --title "X.Y.Z" --notes-file /tmp/release-notes.md
  • Release notes format (following the X.Y.Z template):
    • Markdown title with feature name (e.g. ## Carousel progress bar and slide counter)
    • One-sentence summary
    • Section per feature and referer docs for examples
    • **Full Changelog**: https://github.com/emaia/laravel-hotwire/compare/<prev>...<version> at the end
  • CHANGELOG.md is updated automatically by the release workflow; do not edit manually

Development

composer test          # Run Pest tests
composer analyse       # Run PHPStan
bun run test           # Run JS unit tests (Bun + happy-dom). Use `bun run test`, not `bun test` — the npm script wires up --isolate so mocks don't leak across files
bun run test:browser   # Run browser tests (Playwright)
composer format        # Run Pint code formatter

Testing (TDD)

Follow test-driven development for testable behavior and contracts: write tests first, then implement. Purely visual CSS changes follow the CSS test scope below.

There are three test suites:

  • PHP (composer test) — Pest + Orchestra Testbench. Covers commands, components, registry.
  • JS (bun run test) — Bun test runner + happy-dom. Covers Stimulus controllers in tests/Controllers/*.test.js.
  • Browser JS (bun run test:browser) — Playwright. Covers browser-dependent Stimulus behavior in tests/Browser/*.pw.js.

CSS test scope

Automate CSS coverage when a rule provides a structural, behavioral, accessibility, compilation, or documented public contract. Examples include required visibility, scrolling, clipping, positioning, interaction state, focus visibility, contrast, reduced motion, forced-colors support, and public values emitted through CSS custom properties.

Do not add source snapshots or exact computed-style assertions solely to freeze visual choices such as colors, shadows, border radii, typography, decorative motion, or non-functional spacing. Validate intentional appearance changes with a focused manual smoke; they do not require a failing automated test. Stylesheet imports, generated output, slot and axis coverage, catalog ownership, and structural browser behavior remain subject to TDD.

TDD flow:

  1. Write the failing test in the appropriate suite
  2. Run only that test to confirm it fails:
    • PHP: vendor/bin/pest --filter='test name'
    • JS: bun test --isolate tests/Controllers/<name>_controller.test.js
    • Browser JS: bun run test:browser -- tests/Browser/<name>.pw.js
  3. Implement the minimum code to make it pass
  4. Run the test again to confirm it passes
  5. Repeat for the next behavior

PHP conventions:

  • Test files mirror src/ structure: src/Commands/FooCommand.php → tests/Commands/FooCommandTest.php
  • Use Pest syntax (it(), test(), expect()) — no PHPUnit classes
  • Group related tests with comment headers: // --- Section name ---
  • Use beforeEach/afterEach for shared setup and cleanup (temp files, directories)
  • For artisan commands: use $this->artisan('command')->assertSuccessful() and expectsQuestion/expectsChoice/ expectsOutput for interactive flows
  • Always run composer test at the end to ensure nothing else broke

JS conventions:

  • One test file per controller: tests/Controllers/<name>_controller.test.js
  • Use mountController from resources/js/helpers/test_stimulus.js to set up the DOM and Stimulus
  • Always call mounted?.cleanup() in afterEach
  • Always run bun run test at the end to ensure nothing else broke (the script applies --isolate)
  • The suite runs with bun test --isolate (Bun ≥1.3.10): each file gets its own JSGlobalObject, so mock.module registrations don't leak between files. Drop the flag once Bun 1.4 makes isolation the default.
  • Do not add --parallel. Bare, it defaults to the CPU core count (32 on the maintainer's machine), which drove a single worker to 11.8 GB and OOM-killed the entire WSL VM. Pinned low it is still a bad trade: measured on 32 cores, --parallel=4 failed 4 runs in 8 (~7s) and --parallel=2 2 in 4 (~14s), against ~3 in 5 serial (~31s) — and under any worker count the flaky tests below stop responding entirely rather than merely running late.
  • Behaviour that hinges on MutationObserver delivery belongs in Playwright, not here. happy-dom drains those callbacks unreliably once many files share a process: failure probability scaled with how many files ran first (5 files → 0 of 6 runs, 15 → 2 of 6, 40 → 3 of 6), and in a failing run the expected transition never arrived at all, so widening poll budgets did not help — Bun caps a single test at 5s regardless. Two mitigations are in place, and the Bun suite has been green 10 runs running since:
    • resources/js/helpers/test_stimulus.js drops its globalThis bindings in an afterAll, so each file releases its happy-dom Window instead of pinning one for the life of the process. It must stay in afterAll, not in cleanup() — many afterEach blocks still touch document after cleaning up.
    • The cases that turned on target removal or an external value change moved to tests/Browser/read_more_controller.pw.js and tests/Browser/side_panel_controller.pw.js.
  • Use Playwright (tests/Browser/*.pw.js) for controller behavior that depends on real browser semantics: MutationObserver, focus, requestAnimationFrame, layout required by a behavioral, accessibility, or documented public contract, Turbo frame-like DOM changes, or other complex event timing. Do not use Playwright solely to assert decorative spacing or appearance.
  • Keep Playwright tests focused and few; prefer bun run test for deterministic controller unit behavior.
  • Run bun run test:browser after changing browser-dependent behavior.

Dependencies

  • PHP: emaia/laravel-hotwire-turbo, spatie/laravel-package-tools
  • JS: @hotwired/stimulus, @hotwired/turbo, @emaia/stimulus-lazy-loader
  • Optional JS: third-party libs required by specific controllers — the npm maps in src/Registry/catalog.php are the source of truth (don't list them here)

Docblock convention

Selective documentation — docblocks must add information; if they don't, they're noise. Default to skipping; document deliberately.

Decision tree

1. Is this method part of the public API that app code calls?
   (Facades, fluent builders, model accessors/scopes, trait methods exposed to consumers,
    interface contracts, public methods on PHP classes)
   → YES  → docblock required (single-line summary at minimum)
   → NO   → step 2

2. Is the purpose non-obvious from name + parameter types + return type?
   (Cross-class interaction, hidden constraint, unusual side effect, surprising return semantics)
   → YES  → docblock focused on WHY, not what
   → NO   → skip — let the code speak

What to write

  • Imperative mood, single sentence ending in a period: Persist the upload and return the new Media.
  • Multi-line only when 2-3 sentences are required to capture a non-obvious constraint or rationale. If you need more, the docblock is masking a design smell — refactor or move the explanation to docs/*.md.
  • WHY over what when explaining: "Extracted so the per-iteration try/catch covers every step" — not "Encodes and persists a conversion" (that's the name).

Native types first

Always declare native PHP types on properties, parameters, and return values when possible. Drop redundant @var / @param / @return once the native type carries the contract. Untyped signatures combined with a docblock that names the type are an anti-pattern (the type system can't enforce what the docblock says).

Constructors don't need a return type. PHP's resource pseudo-type has no native equivalent — use @param resource $stream / @return resource there.

When to add @param / @return / @throws

  • @param / @return: only when they carry information beyond the native type (array<string, callable>, string[], Collection<int, Media>, array{conversion: string, exception: \Throwable}, semantic constraint like "empty array returns all"). Never add when they merely repeat the type.
  • @throws: list exceptions that are part of the method's contract (caller is expected to handle them). Skip generic RuntimeException of the "if it breaks it broke" variety.
  • Property @var: only when the native type loses information (array<int, array{conversion: string, exception: \Throwable}>, MediaChannel[], array<string, Collection<int, Media>>). Plain typed properties don't need it.

What to always remove

  • Auto-generated IDE docblocks: /** Get the X. */ on getX() — pure noise
  • @author, @version, @since, @package — git/Composer resolve these
  • Multi-paragraph essays — move to docs/*.md and link
  • @param / @return that just repeat the type-hint
  • Comments inside method bodies explaining WHAT the next line does (rename a variable instead); keep only WHY when non-obvious
  • Stale TODO/FIXME — convert to an issue or delete

Roadmap

Operational roadmap and execution notes are maintained outside this repository. Treat local planning exports as temporary context that may be stale.

CSS / Theming

  • Semantic tokens only. Component views use bg-background, text-foreground, border-border, etc. Never hardcode raw colors (bg-white, text-gray-700, bg-zinc-900). Tokens are defined in resources/css/tokens.css via @theme inline with OKLCH values for light and dark mode.
  • Dark mode via data-theme on <html>, not class="dark". Default is :root (light); [data-theme="dark"] activates the dark palette.
  • Tailwind v4 scanner constraint. Imported presets are processed directly and must not be scanned as content with @source. Do not add Tailwind utility defaults back to package Blade/PHP unless they are also represented by preset CSS or the @source inline(...) safelist in resources/css/structural.css, which every preset imports.
  • Structural vs visual CSS. Mechanics live in resources/css/structural.css — the rules whose absence leaves a component broken rather than restyled (the carousel track, the Accordion's ::details-content collapse). Every preset imports that file, so they compile into the application stylesheet and hold on the first paint instead of landing once the bundle runs, and no new preset has to rediscover them. Visual styling lives in presets and uses data-slot. Mark presentation-free or controller-owned slots structural in the catalog; hotwire:make-preset scaffolds only visual slots.
  • Variants helper is for application code, not for package components. Support\Variants (CVA-equivalent in PHP) stays exported so apps can build their own variant matrices; the Tailwind scanner reaches it there because it scans the app's own PHP. Package components must not use it — they emit no classes at all, only data-slot, data-variant and data-size, and the preset does the styling. Support\PresetAxes reports the attributes a stylesheet differentiates — data-*, aria-* and native attributes alike — as lexical diagnostics only. Presets may cover values with base rules or equivalent selectors; conformance must not require identical extracted axes.
  • Theming docs for app developers: docs/theming.md — token reference, override instructions, colour space notes. Upgrade notes for existing apps: docs/upgrade.md (deliverable of 0.32.0).

PR Checklist

Every PR that introduces a new component or controller must include:

  • Manual smoke matrix. Component verified: (a) in isolation, (b) inside a Modal, (c) inside a Dropdown, (d) inside a Turbo Frame. Overlay components additionally verified nested inside each other.
  • Listeners and timers verified cleaned up in disconnect() — Turbo morph re-connects controllers; duplicate listeners are a recurring bug source.
  • Integration opt-in evaluation. For each new component, assess whether exposing a prop that activates an existing controller turns a common user setup into a one-liner. Model: Button + hotkey (hotkey="cmd+s"). Document the decision (adopted or discarded, with rationale) in the PR body.

New internal helpers

  • _overlay.js — shared Presence-driven overlay lifecycle (data-state, hidden, inert, FocusTrap, body scroll lock, Escape key, focus return and cancelable CSS motion). Consumed by Modal, Alert Dialog, Drawer, Sheet and Sidebar. Exports createOverlay(controller, options) with animated and immediate open/close lifecycle methods.
  • _overlay_host.js — create and retire per-opening instances from reusable hosts; correlate Frame requests and parent/child ownership. Never resolve a missing origin through the top overlay or let a late response affect a new opening.
  • _overlay_return.js — capture the exact initiating form/region and apply returned HTML; fail safely if that destination changes. The origin identifying the child to close and the region receiving its return are separate identities.
  • _overlay_presentation.js — apply size/side per opening, preserve only managed presentation during morph and restore host defaults without overwriting unrelated styles.

Controller auto-loading

All package controllers are loaded directly from the vendor directory via import.meta.glob in resources/js/controllers/index.js. No hotwire:controllers step is required — controllers work out of the box.

The loader index is a static file copied from stubs/resources/js/controllers/index.js by hotwire:install. There is no dynamic generation or version tracking — the stub is the single source of truth.

hotwire:check verifies npm dependencies (declared in the catalog npm key) and reports outdated/diverged published controllers. It no longer flags "not published" as a problem since controllers auto-load from vendor. hotwire:controllers remains available for users who want to publish a controller to customize it.

Workflow

Teamwork

  • Project: Laravel Hotwire
  • Operational notebook: task-workflow.md (always read before changing Teamwork tasks; contains project-specific rules)
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings