stdout:
---
name: control-in-app-browser
description: "Control the in-app Browser for opening, navigating, inspecting visible or interactive page state, clicking, typing, screenshots, and local web testing. It can have existing signed-in sessions. For semantic operations on linked resources, prefer a purpose-built connector, API, or CLI when available."
---

# Browser
## Stop: choose the right surface before any browser action
Explicit browser intent wins: if the user names the in-app browser or Chrome, or asks to open, show, or navigate to a page; inspect its visual or interactive state; or interact with its UI, continue with Browser and do not substitute a connector.

Otherwise, treat a URL or open browser tab as context, not browser intent. Earlier Browser use does not make later semantic work browser-first. Before each semantic operation on a linked resource, you MUST query available and deferred tools for an applicable connector, API, or CLI. Reading this skill or scanning visible tools does not count. Do not initialize Browser for that operation until the query is complete. Use the non-browser tool when available. If it handles the current operation, continue the larger workflow without Browser for that operation. Use Browser when no such tool exists, the tool cannot access the resource or lacks a required capability, or UI work remains; use available browser context before asking the user to repeat it.

Use this skill for browser automation tasks such as inspecting pages, navigating, testing local apps, clicking, typing, taking screenshots, and reading visible page state.

If this plugin is listed as available in the session, treat that as mandatory reading before browser work. Open and follow this skill before saying that Browser is unavailable and before falling back to standalone Playwright or Computer Use.

Do not skip this skill just because Computer Use MCP tool calls are directly visible or appear easier to invoke. The presence of Computer Use tools is not evidence that Computer Use is the preferred browser surface.

## Setup Documentation
Use `await agent.documentation.get("<name>")` when one of these setup topics applies:
- `bootstrap-troubleshooting`: read when browser setup succeeds but discovery or selection fails
- `chrome-troubleshooting`: read when Chrome extension setup, installation, or communication fails

## Bootstrap
These setup details are internal. User-facing progress updates should be less technical in nature. Never mention `Node REPL`, `node_repl`, `REPL`, JavaScript sessions, module exports, reading documentation, or loading instructions unless a user is asking for that exact information. If setup or recovery is needed, describe it naturally as connecting to the browser or retrying the browser connection.

The `browser-client` module is the core entry point for browser use, and is available under `scripts/browser-client.mjs` in this plugin's root directory. ALWAYS import it using an absolute path. IMPORTANT: If this path cannot be found, stop and report that this plugin is missing `scripts/browser-client.mjs`. NEVER use the built in `browser-client` library.

Run browser setup code through the Node REPL `js` tool. In this environment the callable tool id typically appears as `mcp__node_repl__js`. If it is not already available, use tool discovery for `node_repl js` without setting a result limit. You need the `js` execution tool: `js_reset` only clears state, and `js_add_node_module_dir` only changes package resolution. Do not call either helper while trying to expose `js`. If `js` is still not available, search again for `node_repl js` with `limit: 10`.

Initialize the runtime once per fresh Node session. If `agent.browsers` already exists, reuse it; do not import or initialize another browser runtime.

```js
if (globalThis.agent?.browsers == null) {
  const { setupBrowserRuntime } = await import("<plugin root>/scripts/browser-client.mjs");
  await setupBrowserRuntime({ globals: globalThis });
}
```

Once a browser connection is established, reuse its existing browser binding across later turns and do not reread this skill. Once you have read a browser's complete documentation, do not read it again unless you select a different browser.

A tab binding is separate from its browser binding. If a later turn reports that a tab is missing, stale, closed, or not part of the current browser session, discard that tab binding and obtain or create a fresh tab from the existing browser binding. An empty `browser.tabs.list()` or `browser.user.openTabs()` result is normal after tab cleanup and does not invalidate the browser binding. Never call `agent.browsers.get*` to recover a tab; only an explicit browser-disconnected error invalidates the binding.

## Browser selection
The scenarios below are for the initial browser selection only. Before calling any `agent.browsers.get*` method, reuse an existing `globalThis.browser`, `globalThis.iab`, or `globalThis.chrome` binding that already serves the task. A new user turn does not invalidate a browser binding or require another selection or documentation call.

Select the initial browser with exactly one of these scenarios, in the order
shown. An explicit request for the in-app browser or Chrome always wins over URL
selection. Never call `getForUrl()` when the user names a browser.

App-provided in-app-browser context is ambient UI state, not a user instruction to select or switch browsers. Only the text of the user's request can explicitly choose a browser.

Do not inspect browser cookies, local storage, profiles, passwords, or session stores. Browser discovery must remain read-only.

When authentication blocks requested browser navigation, do not replace it with web search, a search engine, another site, or another source merely to bypass sign-in.

### The user explicitly requests a browser
A plugin mention in the user's request explicitly names its browser.
`[@Browser](plugin://browser@openai-bundled)` names the in-app browser.
`[@Chrome](plugin://chrome@openai-bundled)`,
`[@chrome-internal](plugin://chrome-internal@openai-bundled)`, and
`[@chrome-dev](plugin://chrome-dev@openai-bundled)` name Chrome. Follow the
corresponding explicit-browser scenario below.

The in-app browser is available only when the Browser skill is listed for the session. If the user explicitly requests the in-app browser and that skill is available, use a distinct persistent binding and immediately read its complete documentation:

```js
if (globalThis.iab == null) {
  globalThis.iab = await agent.browsers.get("iab");
  nodeRepl.write(await iab.documentation());
}
```

If the user explicitly requests the in-app browser but the Browser skill is not available, report that the in-app browser is unavailable instead of substituting another browser.

Chrome is available only when the Chrome skill is listed for the session. If the user explicitly requests Chrome and that skill is available, use a separate persistent binding and immediately read its complete documentation:

```js
if (globalThis.chrome == null) {
  globalThis.chrome = await agent.browsers.get("extension");
  nodeRepl.write(await chrome.documentation());
}
```

If the user explicitly requests Chrome but the Chrome skill is not available, report that Chrome is unavailable instead of substituting another browser.

An explicit browser choice remains in force for the task. If authentication blocks the task in an explicitly selected browser, your next response must explicitly ask the user to sign in in that browser and tell you when it is ready, unless that browser's documentation provides a supported authentication flow to try first. Merely reporting that sign-in is required is not sufficient. Do not switch to another browser unless the user asks or approves the switch.

### The task requires browser interaction, the user does not specify a browser, and the task has a target URL
When the user supplies a URL or the intended URL can be reasonably inferred from the request, replace the example below with that URL and let browser-client choose the browser best suited to it:

```js
if (globalThis.browser == null) {
  globalThis.browser = await agent.browsers.getForUrl("https://example.com/");
  nodeRepl.write(await browser.documentation());
}
```

### The user specifies neither a browser nor a target URL
Use the runtime default, which prefers the in-app browser when it is available and otherwise uses Chrome. Do not list browsers first:

```js
if (globalThis.browser == null) {
  globalThis.browser = await agent.browsers.getDefault();
  nodeRepl.write(await browser.documentation());
}
```

## After setup
If setup succeeds but browser discovery or selection fails, read `await agent.documentation.get("bootstrap-troubleshooting")` before resetting the JavaScript session or trying another browser-control mechanism.

If the failure is specific to Chrome extension setup, installation, or communication, read `await agent.documentation.get("chrome-troubleshooting")` before retrying or taking another recovery action.

When the user did not explicitly choose a browser, a browser selected by the runtime is not a user constraint. Do not switch browsers based only on an assumption about authentication. If navigation shows that the selected browser lacks the required authentication, select another available browser before asking the user to sign in. You may select it without resetting the Node session. Preserve existing `iab`, `chrome`, and `browser` bindings when they are still useful. Existing tabs remain bound to the browser that created them. After selecting a different browser, obtain a tab from that browser before continuing and read its complete documentation.

The ability to interact directly with browsers is exposed through the `browser-client` runtime via the `agent.browsers.*` API. Before trying to interact with a selected browser for the first time, you MUST emit and read the complete documentation returned by its `documentation()` call in one go. For the initial documentation read, run the exact direct `nodeRepl.write(await <browser>.documentation());` call shown in the applicable scenario above. Do not assign the documentation to a variable, inspect its length, slice it, truncate it, summarize it, or emit only an excerpt. Do not proactively split the documentation into pages or chunks. Only if the tool output itself explicitly reports that it was truncated may you emit and read smaller chunks until you have read the documentation in its entirety.

Only the Node REPL `js` tool (`mcp__node_repl__js`) can be used to control the selected browser. Do not use external MCP browser-control tools, separate browser automation servers, or other browser skills for this surface. References to Playwright mean the in-skill `tab.playwright` API after browser-client setup.

<!-- BROWSER_SKILL_EOF: This is the complete Browser skill. Do not request additional lines. -->


text:
Script completed
Wall time 0.1 seconds
Output:

text:
---
name: control-in-app-browser
description: "Control the in-app Browser for opening, navigating, inspecting visible or interactive page state, clicking, typing, screenshots, and local web testing. It can have existing signed-in sessions. For semantic operations on linked resources, prefer a purpose-built connector, API, or CLI when available."
---

# Browser
## Stop: choose the right surface before any browser action
Explicit browser intent wins: if the user names the in-app browser or Chrome, or asks to open, show, or navigate to a page; inspect its visual or interactive state; or interact with its UI, continue with Browser and do not substitute a connector.

Otherwise, treat a URL or open browser tab as context, not browser intent. Earlier Browser use does not make later semantic work browser-first. Before each semantic operation on a linked resource, you MUST query available and deferred tools for an applicable connector, API, or CLI. Reading this skill or scanning visible tools does not count. Do not initialize Browser for that operation until the query is complete. Use the non-browser tool when available. If it handles the current operation, continue the larger workflow without Browser for that operation. Use Browser when no such tool exists, the tool cannot access the resource or lacks a required capability, or UI work remains; use available browser context before asking the user to repeat it.

Use this skill for browser automation tasks such as inspecting pages, navigating, testing local apps, clicking, typing, taking screenshots, and reading visible page state.

If this plugin is listed as available in the session, treat that as mandatory reading before browser work. Open and follow this skill before saying that Browser is unavailable and before falling back to standalone Playwright or Computer Use.

Do not skip this skill just because Computer Use MCP tool calls are directly visible or appear easier to invoke. The presence of Computer Use tools is not evidence that Computer Use is the preferred browser surface.

## Setup Documentation
Use `await agent.documentation.get("<name>")` when one of these setup topics applies:
- `bootstrap-troubleshooting`: read when browser setup succeeds but discovery or selection fails
- `chrome-troubleshooting`: read when Chrome extension setup, installation, or communication fails

## Bootstrap
These setup details are internal. User-facing progress updates should be less technical in nature. Never mention `Node REPL`, `node_repl`, `REPL`, JavaScript sessions, module exports, reading documentation, or loading instructions unless a user is asking for that exact information. If setup or recovery is needed, describe it naturally as connecting to the browser or retrying the browser connection.

The `browser-client` module is the core entry point for browser use, and is available under `scripts/browser-client.mjs` in this plugin's root directory. ALWAYS import it using an absolute path. IMPORTANT: If this path cannot be found, stop and report that this plugin is missing `scripts/browser-client.mjs`. NEVER use the built in `browser-client` library.

Run browser setup code through the Node REPL `js` tool. In this environment the callable tool id typically appears as `mcp__node_repl__js`. If it is not already available, use tool discovery for `node_repl js` without setting a result limit. You need the `js` execution tool: `js_reset` only clears state, and `js_add_node_module_dir` only changes package resolution. Do not call either helper while trying to expose `js`. If `js` is still not available, search again for `node_repl js` with `limit: 10`.

Initialize the runtime once per fresh Node session. If `agent.browsers` already exists, reuse it; do not import or initialize another browser runtime.

```js
if (globalThis.agent?.browsers == null) {
  const { setupBrowserRuntime } = await import("<plugin root>/scripts/browser-client.mjs");
  await setupBrowserRuntime({ globals: globalThis });
}
```

Once a browser connection is established, reuse its existing browser binding across later turns and do not reread this skill. Once you have read a browser's complete documentation, do not read it again unless you select a different browser.

A tab binding is separate from its browser binding. If a later turn reports that a tab is missing, stale, closed, or not part of the current browser session, discard that tab binding and obtain or create a fresh tab from the existing browser binding. An empty `browser.tabs.list()` or `browser.user.openTabs()` result is normal after tab cleanup and does not invalidate the browser binding. Never call `agent.browsers.get*` to recover a tab; only an explicit browser-disconnected error invalidates the binding.

## Browser selection
The scenarios below are for the initial browser selection only. Before calling any `agent.browsers.get*` method, reuse an existing `globalThis.browser`, `globalThis.iab`, or `globalThis.chrome` binding that already serves the task. A new user turn does not invalidate a browser binding or require another selection or documentation call.

Select the initial browser with exactly one of these scenarios, in the order
shown. An explicit request for the in-app browser or Chrome always wins over URL
selection. Never call `getForUrl()` when the user names a browser.

App-provided in-app-browser context is ambient UI state, not a user instruction to select or switch browsers. Only the text of the user's request can explicitly choose a browser.

Do not inspect browser cookies, local storage, profiles, passwords, or session stores. Browser discovery must remain read-only.

When authentication blocks requested browser navigation, do not replace it with web search, a search engine, another site, or another source merely to bypass sign-in.

### The user explicitly requests a browser
A plugin mention in the user's request explicitly names its browser.
`[@Browser](plugin://browser@openai-bundled)` names the in-app browser.
`[@Chrome](plugin://chrome@openai-bundled)`,
`[@chrome-internal](plugin://chrome-internal@openai-bundled)`, and
`[@chrome-dev](plugin://chrome-dev@openai-bundled)` name Chrome. Follow the
corresponding explicit-browser scenario below.

The in-app browser is available only when the Browser skill is listed for the session. If the user explicitly requests the in-app browser and that skill is available, use a distinct persistent binding and immediately read its complete documentation:

```js
if (globalThis.iab == null) {
  globalThis.iab = await agent.browsers.get("iab");
  nodeRepl.write(await iab.documentation());
}
```

If the user explicitly requests the in-app browser but the Browser skill is not available, report that the in-app browser is unavailable instead of substituting another browser.

Chrome is available only when the Chrome skill is listed for the session. If the user explicitly requests Chrome and that skill is available, use a separate persistent binding and immediately read its complete documentation:

```js
if (globalThis.chrome == null) {
  globalThis.chrome = await agent.browsers.get("extension");
  nodeRepl.write(await chrome.documentation());
}
```

If the user explicitly requests Chrome but the Chrome skill is not available, report that Chrome is unavailable instead of substituting another browser.

An explicit browser choice remains in force for the task. If authentication blocks the task in an explicitly selected browser, your next response must explicitly ask the user to sign in in that browser and tell you when it is ready, unless that browser's documentation provides a supported authentication flow to try first. Merely reporting that sign-in is required is not sufficient. Do not switch to another browser unless the user asks or approves the switch.

### The task requires browser interaction, the user does not specify a browser, and the task has a target URL
When the user supplies a URL or the intended URL can be reasonably inferred from the request, replace the example below with that URL and let browser-client choose the browser best suited to it:

```js
if (globalThis.browser == null) {
  globalThis.browser = await agent.browsers.getForUrl("https://example.com/");
  nodeRepl.write(await browser.documentation());
}
```

### The user specifies neither a browser nor a target URL
Use the runtime default, which prefers the in-app browser when it is available and otherwise uses Chrome. Do not list browsers first:

```js
if (globalThis.browser == null) {
  globalThis.browser = await agent.browsers.getDefault();
  nodeRepl.write(await browser.documentation());
}
```

## After setup
If setup succeeds but browser discovery or selection fails, read `await agent.documentation.get("bootstrap-troubleshooting")` before resetting the JavaScript session or trying another browser-control mechanism.

If the failure is specific to Chrome extension setup, installation, or communication, read `await agent.documentation.get("chrome-troubleshooting")` before retrying or taking another recovery action.

When the user did not explicitly choose a browser, a browser selected by the runtime is not a user constraint. Do not switch browsers based only on an assumption about authentication. If navigation shows that the selected browser lacks the required authentication, select another available browser before asking the user to sign in. You may select it without resetting the Node session. Preserve existing `iab`, `chrome`, and `browser` bindings when they are still useful. Existing tabs remain bound to the browser that created them. After selecting a different browser, obtain a tab from that browser before continuing and read its complete documentation.

The ability to interact directly with browsers is exposed through the `browser-client` runtime via the `agent.browsers.*` API. Before trying to interact with a selected browser for the first time, you MUST emit and read the complete documentation returned by its `documentation()` call in one go. For the initial documentation read, run the exact direct `nodeRepl.write(await <browser>.documentation());` call shown in the applicable scenario above. Do not assign the documentation to a variable, inspect its length, slice it, truncate it, summarize it, or emit only an excerpt. Do not proactively split the documentation into pages or chunks. Only if the tool output itself explicitly reports that it was truncated may you emit and read smaller chunks until you have read the documentation in its entirety.

Only the Node REPL `js` tool (`mcp__node_repl__js`) can be used to control the selected browser. Do not use external MCP browser-control tools, separate browser automation servers, or other browser skills for this surface. References to Playwright mean the in-skill `tab.playwright` API after browser-client setup.

<!-- BROWSER_SKILL_EOF: This is the complete Browser skill. Do not request additional lines. -->


exec

status: completed

input:
const r = await tools.exec_command({cmd:"rg -n \"fluxScripts|livewireScripts\" vendor/livewire/flux/stubs vendor/livewire/livewire/src/Features/SupportPageComponents vendor/livewire/flux -g '*.blade.php' | head -80; sed -n '1,70p' vendor/livewire/flux/stubs/resources/views/layouts/app.blade.php 2>/dev/null || true; rg -n \"function fluxScripts|class.*Flux\" vendor/livewire/flux/src vendor/livewire/flux -g '*.php' | head -80","workdir":"/Users/wesner/Herd/shop","yield_time_ms":10000,"max_output_tokens":12000});
text(r.output);


type: CommandExecution

command: rg -n "fluxScripts|livewireScripts" vendor/livewire/flux/stubs vendor/livewire/livewire/src/Features/SupportPageComponents vendor/livewire/flux -g '*.blade.php' | head -80; sed -n '1,70p' vendor/livewire/flux/stubs/resources/views/layouts/app.blade.php 2>/dev/null || true; rg -n "function fluxScripts|class.*Flux" vendor/livewire/flux/src vendor/livewire/flux -g '*.php' | head -80

type: unknown

status: completed

stdout:
vendor/livewire/flux/src/FluxManager.php:12:class FluxManager
vendor/livewire/flux/src/FluxManager.php:12:class FluxManager
vendor/livewire/flux/src/FluxServiceProvider.php:10:class FluxServiceProvider extends ServiceProvider
vendor/livewire/flux/src/FluxServiceProvider.php:10:class FluxServiceProvider extends ServiceProvider
vendor/livewire/flux/src/Console/IconCommand.php:111:        $classes = Flux::classes('shrink-0')
vendor/livewire/flux/src/Console/IconCommand.php:111:        $classes = Flux::classes('shrink-0')
vendor/livewire/flux/src/Flux.php:10:class Flux extends Facade
vendor/livewire/flux/src/Flux.php:10:class Flux extends Facade
vendor/livewire/flux/src/FluxTagCompiler.php:7:class FluxTagCompiler extends ComponentTagCompiler
vendor/livewire/flux/src/FluxTagCompiler.php:7:class FluxTagCompiler extends ComponentTagCompiler
vendor/livewire/flux/stubs/resources/views/flux/badge/close.blade.php:14:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/badge/index.blade.php:30:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/textarea.blade.php:11:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/breadcrumbs/item.blade.php:13:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/separator.blade.php:14:$classes = Flux::classes('border-0 [print-color-adjust:exact]')
vendor/livewire/flux/stubs/resources/views/flux/modal/index.blade.php:22:    $classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/modal/index.blade.php:42:    $classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/navlist/badge.blade.php:9:$class = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/navlist/item.blade.php:24:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/navlist/index.blade.php:8:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/navmenu/item.blade.php:31:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/navmenu/index.blade.php:4:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/document-plus.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/battery-100.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/arrow-trending-down.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/archive-box.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/map-pin.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/aside.blade.php:8:$classes = Flux::classes('[grid-area:aside]');
vendor/livewire/flux/stubs/resources/views/flux/icon/document-currency-pound.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/profile.blade.php:23:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/currency-yen.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/arrow-uturn-left.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/avatar/group.blade.php:4:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/button/group.blade.php:4:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/information-circle.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/rocket-launch.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/strikethrough.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/calculator.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/button/index.blade.php:64:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/avatar/index.blade.php:59:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/bars-3-bottom-left.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/field.blade.php:8:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/h3.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/envelope.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/fieldset.blade.php:9:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/heading.blade.php:10:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/chevron-double-up.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/footer.blade.php:4:$classes = Flux::classes('[grid-area:footer]')
vendor/livewire/flux/stubs/resources/views/flux/icon/film.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/face-smile.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/header.blade.php:9:$classes = Flux::classes('[grid-area:header]')
vendor/livewire/flux/stubs/resources/views/flux/icon/chevron-right.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/receipt-percent.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/arrow-down-on-square-stack.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/text.blade.php:11:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/callout/heading.blade.php:11:    $classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/hand-thumb-down.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/menu/item.blade.php:31:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/gift-top.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/shield-exclamation.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/book-open.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/building-office.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/callout/link.blade.php:8:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/arrow-path-rounded-square.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/card/index.blade.php:8:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/navbar/badge.blade.php:9:$class = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/circle-stack.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/menu/heading.blade.php:4:$classes = Flux::classes([
vendor/livewire/flux/stubs/resources/views/flux/icon/phone-arrow-down-left.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/chat-bubble-oval-left.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/stop.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/callout/index.blade.php:29:    $classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/gift.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/menu/group.blade.php:8:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/navbar/item.blade.php:25:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/arrow-uturn-down.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/chat-bubble-oval-left-ellipsis.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/clipboard-document.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/chevron-up.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/radio/indicator.blade.php:4:$classes = Flux::classes()


text:
Script completed
Wall time 0.1 seconds
Output:

text:
vendor/livewire/flux/src/FluxManager.php:12:class FluxManager
vendor/livewire/flux/src/FluxManager.php:12:class FluxManager
vendor/livewire/flux/src/FluxServiceProvider.php:10:class FluxServiceProvider extends ServiceProvider
vendor/livewire/flux/src/FluxServiceProvider.php:10:class FluxServiceProvider extends ServiceProvider
vendor/livewire/flux/src/Console/IconCommand.php:111:        $classes = Flux::classes('shrink-0')
vendor/livewire/flux/src/Console/IconCommand.php:111:        $classes = Flux::classes('shrink-0')
vendor/livewire/flux/src/Flux.php:10:class Flux extends Facade
vendor/livewire/flux/src/Flux.php:10:class Flux extends Facade
vendor/livewire/flux/src/FluxTagCompiler.php:7:class FluxTagCompiler extends ComponentTagCompiler
vendor/livewire/flux/src/FluxTagCompiler.php:7:class FluxTagCompiler extends ComponentTagCompiler
vendor/livewire/flux/stubs/resources/views/flux/badge/close.blade.php:14:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/badge/index.blade.php:30:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/textarea.blade.php:11:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/breadcrumbs/item.blade.php:13:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/separator.blade.php:14:$classes = Flux::classes('border-0 [print-color-adjust:exact]')
vendor/livewire/flux/stubs/resources/views/flux/modal/index.blade.php:22:    $classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/modal/index.blade.php:42:    $classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/navlist/badge.blade.php:9:$class = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/navlist/item.blade.php:24:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/navlist/index.blade.php:8:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/navmenu/item.blade.php:31:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/navmenu/index.blade.php:4:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/document-plus.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/battery-100.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/arrow-trending-down.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/archive-box.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/map-pin.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/aside.blade.php:8:$classes = Flux::classes('[grid-area:aside]');
vendor/livewire/flux/stubs/resources/views/flux/icon/document-currency-pound.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/profile.blade.php:23:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/currency-yen.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/arrow-uturn-left.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/avatar/group.blade.php:4:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/button/group.blade.php:4:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/information-circle.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/rocket-launch.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/strikethrough.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/calculator.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/button/index.blade.php:64:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/avatar/index.blade.php:59:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/bars-3-bottom-left.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/field.blade.php:8:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/h3.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/envelope.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/fieldset.blade.php:9:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/heading.blade.php:10:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/chevron-double-up.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/footer.blade.php:4:$classes = Flux::classes('[grid-area:footer]')
vendor/livewire/flux/stubs/resources/views/flux/icon/film.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/face-smile.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/header.blade.php:9:$classes = Flux::classes('[grid-area:header]')
vendor/livewire/flux/stubs/resources/views/flux/icon/chevron-right.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/receipt-percent.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/arrow-down-on-square-stack.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/text.blade.php:11:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/callout/heading.blade.php:11:    $classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/hand-thumb-down.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/menu/item.blade.php:31:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/gift-top.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/shield-exclamation.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/book-open.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/building-office.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/callout/link.blade.php:8:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/arrow-path-rounded-square.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/card/index.blade.php:8:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/navbar/badge.blade.php:9:$class = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/circle-stack.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/menu/heading.blade.php:4:$classes = Flux::classes([
vendor/livewire/flux/stubs/resources/views/flux/icon/phone-arrow-down-left.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/chat-bubble-oval-left.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/stop.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/callout/index.blade.php:29:    $classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/gift.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/menu/group.blade.php:8:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/navbar/item.blade.php:25:$classes = Flux::classes()
vendor/livewire/flux/stubs/resources/views/flux/icon/arrow-uturn-down.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/chat-bubble-oval-left-ellipsis.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/clipboard-document.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/icon/chevron-up.blade.php:10:$classes = Flux::classes('shrink-0')
vendor/livewire/flux/stubs/resources/views/flux/radio/indicator.blade.php:4:$classes = Flux::classes()


status: completed

input:
const r = await tools.exec_command({cmd:"rg -n \"fluxScripts|@livewireScripts\" vendor/livewire/flux README.md vendor/livewire/livewire -g '*.md' -g '*.blade.php' | head -100; rg -n \"Flux::scripts|flux/flux.js|flux.js\" vendor/livewire/flux/src vendor/livewire/flux -g '*.php' | head -100","workdir":"/Users/wesner/Herd/shop","yield_time_ms":10000,"max_output_tokens":12000});
text(r.output);


command: rg -n "fluxScripts|@livewireScripts" vendor/livewire/flux README.md vendor/livewire/livewire -g '*.md' -g '*.blade.php' | head -100; rg -n "Flux::scripts|flux/flux.js|flux.js" vendor/livewire/flux/src vendor/livewire/flux -g '*.php' | head -100

type: search

cmd: rg -n 'fluxScripts|@livewireScripts' vendor/livewire/flux README.md vendor/livewire/livewire -g '*.md' -g '*.blade.php'

query: fluxScripts|@livewireScripts

path: flux

type: search

cmd: rg -n 'Flux::scripts|flux/flux.js|flux.js' vendor/livewire/flux/src vendor/livewire/flux -g '*.php'

query: Flux::scripts|flux/flux.js|flux.js

path: flux

stdout:
vendor/livewire/flux/README.md:27:Add the `@fluxAppearance` and `@fluxScripts` Blade directives to your layout file:
vendor/livewire/flux/README.md:37:    @fluxScripts
vendor/livewire/flux/src/AssetManager.php:37:        Route::get('/flux/flux.js', [static::class, 'fluxJs']);
vendor/livewire/flux/src/AssetManager.php:46:            ? $this->pretendResponseIsFile(__DIR__.'/../../flux-pro/dist/flux.js', 'text/javascript')
vendor/livewire/flux/src/AssetManager.php:80:        $versionHash = $manifest['/flux.js'];
vendor/livewire/flux/src/AssetManager.php:85:            return '<script src="'. url('/flux/flux.js?id='. $versionHash) . '" data-navigate-once' . $nonce . '></script>';
vendor/livewire/flux/src/AssetManager.php:37:        Route::get('/flux/flux.js', [static::class, 'fluxJs']);
vendor/livewire/flux/src/AssetManager.php:46:            ? $this->pretendResponseIsFile(__DIR__.'/../../flux-pro/dist/flux.js', 'text/javascript')
vendor/livewire/flux/src/AssetManager.php:80:        $versionHash = $manifest['/flux.js'];
vendor/livewire/flux/src/AssetManager.php:85:            return '<script src="'. url('/flux/flux.js?id='. $versionHash) . '" data-navigate-once' . $nonce . '></script>';


text:
Script completed
Wall time 0.1 seconds
Output:

text:
vendor/livewire/flux/README.md:27:Add the `@fluxAppearance` and `@fluxScripts` Blade directives to your layout file:
vendor/livewire/flux/README.md:37:    @fluxScripts
vendor/livewire/flux/src/AssetManager.php:37:        Route::get('/flux/flux.js', [static::class, 'fluxJs']);
vendor/livewire/flux/src/AssetManager.php:46:            ? $this->pretendResponseIsFile(__DIR__.'/../../flux-pro/dist/flux.js', 'text/javascript')
vendor/livewire/flux/src/AssetManager.php:80:        $versionHash = $manifest['/flux.js'];
vendor/livewire/flux/src/AssetManager.php:85:            return '<script src="'. url('/flux/flux.js?id='. $versionHash) . '" data-navigate-once' . $nonce . '></script>';
vendor/livewire/flux/src/AssetManager.php:37:        Route::get('/flux/flux.js', [static::class, 'fluxJs']);
vendor/livewire/flux/src/AssetManager.php:46:            ? $this->pretendResponseIsFile(__DIR__.'/../../flux-pro/dist/flux.js', 'text/javascript')
vendor/livewire/flux/src/AssetManager.php:80:        $versionHash = $manifest['/flux.js'];
vendor/livewire/flux/src/AssetManager.php:85:            return '<script src="'. url('/flux/flux.js?id='. $versionHash) . '" data-navigate-once' . $nonce . '></script>';


status: completed

input:
const r = await tools.exec_command({cmd:"sed -n '1,80p' vendor/livewire/flux/README.md; sed -n '1,115p' vendor/livewire/flux/src/AssetManager.php; sed -n '80,130p' vendor/livewire/flux/stubs/resources/views/flux/modal/index.blade.php; sed -n '1,45p' vendor/livewire/flux/stubs/resources/views/flux/input/viewable.blade.php","workdir":"/Users/wesner/Herd/shop","yield_time_ms":10000,"max_output_tokens":12000});
text(r.output);


command: sed -n '1,80p' vendor/livewire/flux/README.md; sed -n '1,115p' vendor/livewire/flux/src/AssetManager.php; sed -n '80,130p' vendor/livewire/flux/stubs/resources/views/flux/modal/index.blade.php; sed -n '1,45p' vendor/livewire/flux/stubs/resources/views/flux/input/viewable.blade.php

README.md

type: read

viewable.blade.php

stdout:
# Flux

Flux is a robust, hand-crafted UI component library for [Livewire](https://livewire.laravel.com) applications. It's built using [Tailwind CSS](https://tailwindcss.com) and provides a set of components that are easy to use and customize.

You can view the components in action at [https://fluxui.dev](https://fluxui.dev).

## Prerequisites

Flux requires the following before installing:

* Laravel v10.0+
* Livewire v3.5.19+
* Tailwind CSS v4.0+

## Installation

Run the following command from your project's root directory:

```bash
composer require livewire/flux
```

## Getting Started

### 1. Include Flux Assets

Add the `@fluxAppearance` and `@fluxScripts` Blade directives to your layout file:

```html
<head>
    ...
    @fluxAppearance
</head>

<body>
    ...
    @fluxScripts
</body>
```

### 2. Set up Tailwind CSS

Flux uses Tailwind CSS for its default styling. Add the following configuration to your `resources/css/app.css` file:

```css
@import 'tailwindcss';
@import '../../vendor/livewire/flux/dist/flux.css';

@custom-variant dark (&:where(.dark, .dark *));
```

If you don't have Tailwind installed, you can learn how to install it on the [Tailwind website](https://tailwindcss.com/docs/guides/laravel).

### 3. Use the Inter Font Family (Optional)

We recommend using the [Inter font family](https://rsms.me/inter). Add the following to your layout file:

```html
<head>
    ...
    <link rel="preconnect" href="https://fonts.bunny.net">
    <link href="https://fonts.bunny.net/css?family=inter:400,500,600&display=swap" rel="stylesheet" />
</head>
```

Configure Tailwind to use this font in your `resources/css/app.css`:

```css
@import 'tailwindcss';
@import '../../vendor/livewire/flux/dist/flux.css';

...

@theme {
    --font-sans: Inter, sans-serif;
}
```

## What's included?

<?php

namespace Flux;

use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Blade;
use Illuminate\Http\Request;

class AssetManager
{
    static function boot()
    {
        $instance = new static;

        $instance->registerAssetDirective();
        $instance->registerAssetRoutes();
    }

    public function registerAssetDirective()
    {
        Blade::directive('fluxScripts', function ($expression) {
            return <<<PHP
            <?php app('livewire')->forceAssetInjection(); ?>
            {!! app('flux')->scripts($expression) !!}
            PHP;
        });

        Blade::directive('fluxAppearance', function ($expression) {
            return <<<PHP
            {!! app('flux')->fluxAppearance($expression) !!}
            PHP;
        });
    }

    public function registerAssetRoutes()
    {
        Route::get('/flux/flux.js', [static::class, 'fluxJs']);
        Route::get('/flux/flux.min.js', [static::class, 'fluxMinJs']);
        Route::get('/flux/editor.css', [static::class, 'editorCss']);
        Route::get('/flux/editor.js', [static::class, 'editorJs']);
        Route::get('/flux/editor.min.js', [static::class, 'editorMinJs']);
    }

    public function fluxJs() {
        return Flux::pro()
            ? $this->pretendResponseIsFile(__DIR__.'/../../flux-pro/dist/flux.js', 'text/javascript')
            : $this->pretendResponseIsFile(__DIR__.'/../../flux/dist/flux-lite.min.js', 'text/javascript');
    }

    public function fluxMinJs() {
        return Flux::pro()
            ? $this->pretendResponseIsFile(__DIR__.'/../../flux-pro/dist/flux.min.js', 'text/javascript')
            : $this->pretendResponseIsFile(__DIR__.'/../../flux/dist/flux-lite.min.js', 'text/javascript');
    }

    public function editorCss() {
        if (! Flux::pro()) throw new \Exception('Flux Pro is required to use the Flux editor.');

        return $this->pretendResponseIsFile(__DIR__.'/../../flux-pro/dist/editor.css', 'text/css');
    }

    public function editorJs() {
        if (! Flux::pro()) throw new \Exception('Flux Pro is required to use the Flux editor.');

        return $this->pretendResponseIsFile(__DIR__.'/../../flux-pro/dist/editor.js', 'text/javascript');
    }

    public function editorMinJs() {
        if (! Flux::pro()) throw new \Exception('Flux Pro is required to use the Flux editor.');

        return $this->pretendResponseIsFile(__DIR__.'/../../flux-pro/dist/editor.min.js', 'text/javascript');
    }

    public static function scripts($options = [])
    {
        $manifest = Flux::pro()
            ? json_decode(file_get_contents(__DIR__.'/../../flux-pro/dist/manifest.json'), true)
            : json_decode(file_get_contents(__DIR__.'/../../flux/dist/manifest.json'), true);

        $versionHash = $manifest['/flux.js'];

        $nonce = isset($options) && isset($options['nonce']) ? ' nonce="' . $options['nonce'] . '"' : '';

        if (config('app.debug')) {
            return '<script src="'. url('/flux/flux.js?id='. $versionHash) . '" data-navigate-once' . $nonce . '></script>';
        } else {
            return '<script src="'. url('/flux/flux.min.js?id='. $versionHash) . '" data-navigate-once' . $nonce . '></script>';
        }
    }

    public static function fluxAppearance($options = [])
    {
        $nonce = isset($options) && isset($options['nonce']) ? ' nonce="' . $options['nonce'] . '"' : '';

        // Make scrollbars dark in dark mode...
        return <<<HTML
<style$nonce>
    :root.dark {
        color-scheme: dark;
    }
</style>
<script$nonce>
    window.Flux = {
        applyAppearance (appearance) {
            let applyDark = () => document.documentElement.classList.add('dark')
            let applyLight = () => document.documentElement.classList.remove('dark')

            if (appearance === 'system') {
                let media = window.matchMedia('(prefers-color-scheme: dark)')

                window.localStorage.removeItem('flux.appearance')

                media.matches ? applyDark() : applyLight()
            } else if (appearance === 'dark') {
                window.localStorage.setItem('flux.appearance', 'dark')
[ $styleAttributes, $attributes ] = Flux::splitAttributes($attributes, ['autofocus', 'class', 'style', 'wire:close', 'x-on:close', 'wire:cancel', 'x-on:cancel']);
@endphp

<ui-modal {{ $attributes }} data-flux-modal>
    <?php if ($trigger): ?>
        {{ $trigger }}
    <?php endif; ?>

    <dialog
        wire:ignore.self {{-- This needs to be here because the dialog element adds a "close" attribute that isn't durable... --}}
        {{ $styleAttributes->class($classes) }}
        @if ($name) data-modal="{{ $name }}" @endif
        @if ($flyout) data-flux-flyout @endif
        x-data="fluxModal(@js($name), @js(isset($__livewire) ? $__livewire->getId() : null))"
        x-on:modal-show.document="handleShow($event)"
        x-on:modal-close.document="handleClose($event)"
    >
        {{ $slot }}

        <?php if ($closable): ?>
            <div class="absolute top-0 end-0 mt-4 me-4">
                <flux:modal.close>
                    <flux:button variant="ghost" icon="x-mark" size="sm" aria-label="Close modal" class="text-zinc-400! hover:text-zinc-800! dark:text-zinc-500! dark:hover:text-white!"></flux:button>
                </flux:modal.close>
            </div>
        <?php endif; ?>
    </dialog>
</ui-modal>
@blaze

@php
$attributes = $attributes->merge([
    'variant' => 'subtle',
    'class' => '-me-1',
    'square' => true,
    'size' => null,
]);
@endphp

<flux:button
    :$attributes
    :size="$size === 'sm' || $size === 'xs' ? 'xs' : 'sm'"
    x-data="fluxInputViewable"
    x-on:click="toggle()"
    x-bind:data-viewable-open="open"
    aria-label="{{ __('Toggle password visibility') }}"
>
    <flux:icon.eye-slash variant="micro" class="hidden [[data-viewable-open]>&]:block" />
    <flux:icon.eye variant="micro" class="block [[data-viewable-open]>&]:hidden" />
</flux:button>


text:
Script completed
Wall time 0.1 seconds
Output:

text:
# Flux

Flux is a robust, hand-crafted UI component library for [Livewire](https://livewire.laravel.com) applications. It's built using [Tailwind CSS](https://tailwindcss.com) and provides a set of components that are easy to use and customize.

You can view the components in action at [https://fluxui.dev](https://fluxui.dev).

## Prerequisites

Flux requires the following before installing:

* Laravel v10.0+
* Livewire v3.5.19+
* Tailwind CSS v4.0+

## Installation

Run the following command from your project's root directory:

```bash
composer require livewire/flux
```

## Getting Started

### 1. Include Flux Assets

Add the `@fluxAppearance` and `@fluxScripts` Blade directives to your layout file:

```html
<head>
    ...
    @fluxAppearance
</head>

<body>
    ...
    @fluxScripts
</body>
```

### 2. Set up Tailwind CSS

Flux uses Tailwind CSS for its default styling. Add the following configuration to your `resources/css/app.css` file:

```css
@import 'tailwindcss';
@import '../../vendor/livewire/flux/dist/flux.css';

@custom-variant dark (&:where(.dark, .dark *));
```

If you don't have Tailwind installed, you can learn how to install it on the [Tailwind website](https://tailwindcss.com/docs/guides/laravel).

### 3. Use the Inter Font Family (Optional)

We recommend using the [Inter font family](https://rsms.me/inter). Add the following to your layout file:

```html
<head>
    ...
    <link rel="preconnect" href="https://fonts.bunny.net">
    <link href="https://fonts.bunny.net/css?family=inter:400,500,600&display=swap" rel="stylesheet" />
</head>
```

Configure Tailwind to use this font in your `resources/css/app.css`:

```css
@import 'tailwindcss';
@import '../../vendor/livewire/flux/dist/flux.css';

...

@theme {
    --font-sans: Inter, sans-serif;
}
```

## What's included?

<?php

namespace Flux;

use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Blade;
use Illuminate\Http\Request;

class AssetManager
{
    static function boot()
    {
        $instance = new static;

        $instance->registerAssetDirective();
        $instance->registerAssetRoutes();
    }

    public function registerAssetDirective()
    {
        Blade::directive('fluxScripts', function ($expression) {
            return <<<PHP
            <?php app('livewire')->forceAssetInjection(); ?>
            {!! app('flux')->scripts($expression) !!}
            PHP;
        });

        Blade::directive('fluxAppearance', function ($expression) {
            return <<<PHP
            {!! app('flux')->fluxAppearance($expression) !!}
            PHP;
        });
    }

    public function registerAssetRoutes()
    {
        Route::get('/flux/flux.js', [static::class, 'fluxJs']);
        Route::get('/flux/flux.min.js', [static::class, 'fluxMinJs']);
        Route::get('/flux/editor.css', [static::class, 'editorCss']);
        Route::get('/flux/editor.js', [static::class, 'editorJs']);
        Route::get('/flux/editor.min.js', [static::class, 'editorMinJs']);
    }

    public function fluxJs() {
        return Flux::pro()
            ? $this->pretendResponseIsFile(__DIR__.'/../../flux-pro/dist/flux.js', 'text/javascript')
            : $this->pretendResponseIsFile(__DIR__.'/../../flux/dist/flux-lite.min.js', 'text/javascript');
    }

    public function fluxMinJs() {
        return Flux::pro()
            ? $this->pretendResponseIsFile(__DIR__.'/../../flux-pro/dist/flux.min.js', 'text/javascript')
            : $this->pretendResponseIsFile(__DIR__.'/../../flux/dist/flux-lite.min.js', 'text/javascript');
    }

    public function editorCss() {
        if (! Flux::pro()) throw new \Exception('Flux Pro is required to use the Flux editor.');

        return $this->pretendResponseIsFile(__DIR__.'/../../flux-pro/dist/editor.css', 'text/css');
    }

    public function editorJs() {
        if (! Flux::pro()) throw new \Exception('Flux Pro is required to use the Flux editor.');

        return $this->pretendResponseIsFile(__DIR__.'/../../flux-pro/dist/editor.js', 'text/javascript');
    }

    public function editorMinJs() {
        if (! Flux::pro()) throw new \Exception('Flux Pro is required to use the Flux editor.');

        return $this->pretendResponseIsFile(__DIR__.'/../../flux-pro/dist/editor.min.js', 'text/javascript');
    }

    public static function scripts($options = [])
    {
        $manifest = Flux::pro()
            ? json_decode(file_get_contents(__DIR__.'/../../flux-pro/dist/manifest.json'), true)
            : json_decode(file_get_contents(__DIR__.'/../../flux/dist/manifest.json'), true);

        $versionHash = $manifest['/flux.js'];

        $nonce = isset($options) && isset($options['nonce']) ? ' nonce="' . $options['nonce'] . '"' : '';

        if (config('app.debug')) {
            return '<script src="'. url('/flux/flux.js?id='. $versionHash) . '" data-navigate-once' . $nonce . '></script>';
        } else {
            return '<script src="'. url('/flux/flux.min.js?id='. $versionHash) . '" data-navigate-once' . $nonce . '></script>';
        }
    }

    public static function fluxAppearance($options = [])
    {
        $nonce = isset($options) && isset($options['nonce']) ? ' nonce="' . $options['nonce'] . '"' : '';

        // Make scrollbars dark in dark mode...
        return <<<HTML
<style$nonce>
    :root.dark {
        color-scheme: dark;
    }
</style>
<script$nonce>
    window.Flux = {
        applyAppearance (appearance) {
            let applyDark = () => document.documentElement.classList.add('dark')
            let applyLight = () => document.documentElement.classList.remove('dark')

            if (appearance === 'system') {
                let media = window.matchMedia('(prefers-color-scheme: dark)')

                window.localStorage.removeItem('flux.appearance')

                media.matches ? applyDark() : applyLight()
            } else if (appearance === 'dark') {
                window.localStorage.setItem('flux.appearance', 'dark')
[ $styleAttributes, $attributes ] = Flux::splitAttributes($attributes, ['autofocus', 'class', 'style', 'wire:close', 'x-on:close', 'wire:cancel', 'x-on:cancel']);
@endphp

<ui-modal {{ $attributes }} data-flux-modal>
    <?php if ($trigger): ?>
        {{ $trigger }}
    <?php endif; ?>

    <dialog
        wire:ignore.self {{-- This needs to be here because the dialog element adds a "close" attribute that isn't durable... --}}
        {{ $styleAttributes->class($classes) }}
        @if ($name) data-modal="{{ $name }}" @endif
        @if ($flyout) data-flux-flyout @endif
        x-data="fluxModal(@js($name), @js(isset($__livewire) ? $__livewire->getId() : null))"
        x-on:modal-show.document="handleShow($event)"
        x-on:modal-close.document="handleClose($event)"
    >
        {{ $slot }}

        <?php if ($closable): ?>
            <div class="absolute top-0 end-0 mt-4 me-4">
                <flux:modal.close>
                    <flux:button variant="ghost" icon="x-mark" size="sm" aria-label="Close modal" class="text-zinc-400! hover:text-zinc-800! dark:text-zinc-500! dark:hover:text-white!"></flux:button>
                </flux:modal.close>
            </div>
        <?php endif; ?>
    </dialog>
</ui-modal>
@blaze

@php
$attributes = $attributes->merge([
    'variant' => 'subtle',
    'class' => '-me-1',
    'square' => true,
    'size' => null,
]);
@endphp

<flux:button
    :$attributes
    :size="$size === 'sm' || $size === 'xs' ? 'xs' : 'sm'"
    x-data="fluxInputViewable"
    x-on:click="toggle()"
    x-bind:data-viewable-open="open"
    aria-label="{{ __('Toggle password visibility') }}"
>
    <flux:icon.eye-slash variant="micro" class="hidden [[data-viewable-open]>&]:block" />
    <flux:icon.eye variant="micro" class="block [[data-viewable-open]>&]:hidden" />
</flux:button>


exec

status: completed

input:
const r = await tools.mcp__playwright__browser_tabs({action:"list"});
for (const c of (r?.content ?? [])) if (c.type === "text") text(c.text);


type: McpToolCall

server: playwright

action: list

type: text

text:
### Result
- 0: (current) [Admin · Acme Fashion](http://shop.test:8000/admin/settings/domains)


text:
Script completed
Wall time 0.0 seconds
Output:

text:
### Result
- 0: (current) [Admin · Acme Fashion](http://shop.test:8000/admin/settings/domains)


status: completed

input:
const r = await tools.mcp__playwright__browser_navigate({url:"http://shop.test:8000/login"});
for (const c of (r?.content ?? [])) if (c.type === "text") text(c.text);


text:
### Ran Playwright code
```js
await page.goto('http://shop.test:8000/login');
```
### Page
- Page URL: http://shop.test:8000/dashboard
- Page Title: Dashboard
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-08-21T00-27-17-556Z.yml)
### Events
- New console entries: .playwright-mcp/console-2026-08-21T00-27-17-078Z.log#L1


text:
Script completed
Wall time 0.5 seconds
Output:

text:
### Ran Playwright code
```js
await page.goto('http://shop.test:8000/login');
```
### Page
- Page URL: http://shop.test:8000/dashboard
- Page Title: Dashboard
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-08-21T00-27-17-556Z.yml)
### Events
- New console entries: .playwright-mcp/console-2026-08-21T00-27-17-078Z.log#L1


status: completed

input:
const r = await tools.mcp__playwright__browser_console_messages({level:"error"});
for (const c of (r?.content ?? [])) if (c.type === "text") text(c.text);


level: error

text:
### Result
Total messages: 1 (Errors: 0, Warnings: 0)
Returning 0 messages for level "error"


text:
Script completed
Wall time 0.0 seconds
Output:

text:
### Result
Total messages: 1 (Errors: 0, Warnings: 0)
Returning 0 messages for level "error"


status: completed

input:
const r = await tools.mcp__playwright__browser_navigate({url:"http://shop.test:8000/admin/settings/domains"});
for (const c of (r?.content ?? [])) if (c.type === "text") text(c.text);


text:
### Ran Playwright code
```js
await page.goto('http://shop.test:8000/admin/settings/domains');
```
### Page
- Page URL: http://shop.test:8000/admin/settings/domains
- Page Title: Admin · Acme Fashion
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-08-21T00-27-22-900Z.yml)
### Events
- New console entries: .playwright-mcp/console-2026-08-21T00-27-22-836Z.log#L1


text:
Script completed
Wall time 0.1 seconds
Output:

text:
### Ran Playwright code
```js
await page.goto('http://shop.test:8000/admin/settings/domains');
```
### Page
- Page URL: http://shop.test:8000/admin/settings/domains
- Page Title: Admin · Acme Fashion
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-08-21T00-27-22-900Z.yml)
### Events
- New console entries: .playwright-mcp/console-2026-08-21T00-27-22-836Z.log#L1


status: completed

input:
const r = await tools.mcp__playwright__browser_console_messages({level:"error"});
for (const c of (r?.content ?? [])) if (c.type === "text") text(c.text);


text:
Script completed
Wall time 0.0 seconds
Output:

text:
### Result
Total messages: 1 (Errors: 0, Warnings: 0)
Returning 0 messages for level "error"


status: completed

input:
const r = await tools.mcp__playwright__browser_snapshot({depth:3});
for (const c of (r?.content ?? [])) if (c.type === "text") text(c.text);


text:
### Page
- Page URL: http://shop.test:8000/admin/settings/domains
- Page Title: Admin · Acme Fashion
### Snapshot
```yaml
- generic [ref=f68e3]:
  - banner [ref=f68e4]:
    - generic [ref=f68e5]:
      - button "Open navigation" [ref=f68e6]: ☰
      - generic [ref=f68e7]: Store Admin
      - button "Dark" [ref=f68e10]
  - main [ref=f68e11]:
    - generic [ref=f68e12]:
      - generic [ref=f68e13]
      - table [ref=f68e22]
```


text:
Script completed
Wall time 0.0 seconds
Output:

text:
### Page
- Page URL: http://shop.test:8000/admin/settings/domains
- Page Title: Admin · Acme Fashion
### Snapshot
```yaml
- generic [ref=f68e3]:
  - banner [ref=f68e4]:
    - generic [ref=f68e5]:
      - button "Open navigation" [ref=f68e6]: ☰
      - generic [ref=f68e7]: Store Admin
      - button "Dark" [ref=f68e10]
  - main [ref=f68e11]:
    - generic [ref=f68e12]:
      - generic [ref=f68e13]
      - table [ref=f68e22]
```


status: completed

input:
const r = await tools.mcp__playwright__browser_find({text:"Add domain"});
for (const c of (r?.content ?? [])) if (c.type === "text") text(c.text);


text: Add domain

text:
### Result
Found 1 match for "Add domain":

- generic [ref=f68e3]:
  - main [ref=f68e11]:
    - generic [ref=f68e12]:
      - generic [ref=f68e13]:
        - generic [ref=f68e14]: Connected domains
        - button "Add domain" [ref=f68e15]
      - table [ref=f68e22]:
        - rowgroup [ref=f68e23]:
          - row [ref=f68e24]:


text:
Script completed
Wall time 0.0 seconds
Output:

text:
### Result
Found 1 match for "Add domain":

- generic [ref=f68e3]:
  - main [ref=f68e11]:
    - generic [ref=f68e12]:
      - generic [ref=f68e13]:
        - generic [ref=f68e14]: Connected domains
        - button "Add domain" [ref=f68e15]
      - table [ref=f68e22]:
        - rowgroup [ref=f68e23]:
          - row [ref=f68e24]:


status: completed

input:
const r = await tools.mcp__playwright__browser_click({target:"f68e15",element:"Add domain button"});
for (const c of (r?.content ?? [])) if (c.type === "text") text(c.text);


element: Add domain button

text:
### Ran Playwright code
```js
await page.getByRole('button', { name: 'Add domain' }).click();
```
### Page
- Page URL: http://shop.test:8000/admin/settings/domains
- Page Title: Admin · Acme Fashion
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-08-21T00-27-33-372Z.yml)


text:
Script completed
Wall time 0.6 seconds
Output:

text:
### Ran Playwright code
```js
await page.getByRole('button', { name: 'Add domain' }).click();
```
### Page
- Page URL: http://shop.test:8000/admin/settings/domains
- Page Title: Admin · Acme Fashion
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-08-21T00-27-33-372Z.yml)


status: completed

input:
const r = await tools.mcp__playwright__browser_console_messages({level:"error"});
for (const c of (r?.content ?? [])) if (c.type === "text") text(c.text);


text:
Script completed
Wall time 0.0 seconds
Output:

text:
### Result
Total messages: 1 (Errors: 0, Warnings: 0)
Returning 0 messages for level "error"


status: completed

input:
const result = await tools.multi_agent_v1__wait_agent({
  targets: [
    "01a021ae-4641-7d70-97fa-a13af2f3ed44",
    "01a021ae-46e6-7403-b288-76b3738f772b"
  ],
  timeout_ms: 10000
});
text(result);


text:
Script completed
Wall time 10.0 seconds
Output:


status: completed

input:
const r = await tools.multi_agent_v1__send_input({target:"01a021ae-4641-7d70-97fa-a13af2f3ed44",message:"Please return your concrete findings now, even if browser verification was unavailable."});
text(r);


type: CollabAgentToolCall

status: completed

agent_nickname: Lovelace

prompt: Please return your concrete findings now, even if browser verification was unavailable.

01a021ae-4641-7d70-97fa-a13af2f3ed44: running


text:
Script completed
Wall time 0.0 seconds
Output:

text: {"submission_id":"01a021b7-8825-7b91-a546-24c043c18b80"}


status: completed

input:
const r = await tools.multi_agent_v1__send_input({target:"01a021ae-46e6-7403-b288-76b3738f772b","message":"Please return your concrete findings now, even if browser verification was unavailable."});
text(r);


type: CollabAgentToolCall

status: completed

agent_nickname: Ramanujan

prompt: Please return your concrete findings now, even if browser verification was unavailable.

01a021ae-46e6-7403-b288-76b3738f772b: running


text:
Script completed
Wall time 0.0 seconds
Output:

text: {"submission_id":"01a021b7-911b-7fb3-bc84-2c22f74f5f23"}


status: completed

input:
const r = await tools.exec_command({cmd:"find app/Livewire/Storefront -type f -maxdepth 4 -print; for f in app/Livewire/Storefront/*.php app/Livewire/Storefront/*/*.php; do echo ---$f; sed -n '1,180p' $f; done","workdir":"/Users/wesner/Herd/shop","yield_time_ms":10000,"max_output_tokens":40000});
text(r.output);


command: find app/Livewire/Storefront -type f -maxdepth 4 -print; for f in app/Livewire/Storefront/*.php app/Livewire/Storefront/*/*.php; do echo ---$f; sed -n '1,180p' $f; done

stdout:
app/Livewire/Storefront/Products/Show.php
app/Livewire/Storefront/Home.php
app/Livewire/Storefront/Checkout/Show.php
app/Livewire/Storefront/Checkout/Confirmation.php
app/Livewire/Storefront/Search/Index.php
app/Livewire/Storefront/Search/Modal.php
app/Livewire/Storefront/CartDrawer.php
app/Livewire/Storefront/Cart/Show.php
app/Livewire/Storefront/Account/Dashboard.php
app/Livewire/Storefront/Account/Auth/Login.php
app/Livewire/Storefront/Account/Auth/Register.php
app/Livewire/Storefront/Account/Auth/ResetPassword.php
app/Livewire/Storefront/Account/Auth/ForgotPassword.php
app/Livewire/Storefront/Account/Addresses/Index.php
app/Livewire/Storefront/Account/Orders/Index.php
app/Livewire/Storefront/Account/Orders/Show.php
app/Livewire/Storefront/Collections/Index.php
app/Livewire/Storefront/Collections/Show.php
app/Livewire/Storefront/Pages/Show.php
---app/Livewire/Storefront/CartDrawer.php
<?php

namespace App\Livewire\Storefront;

use App\Models\Cart;
use App\Services\CartService;
use Livewire\Attributes\On;
use Livewire\Component;

class CartDrawer extends Component
{
    public bool $open = false;

    public Cart $cart;

    public function mount(CartService $carts): void
    {
        $this->cart = $carts->getOrCreateForSession(app('current_store'), auth('customer')->user());
    }

    #[On('open-cart-drawer')]
    public function open(): void
    {
        $this->refreshCart();
        $this->open = true;
    }

    #[On('cart-updated')]
    public function refreshCart(): void
    {
        $this->cart = app(CartService::class)->getOrCreateForSession(app('current_store'), auth('customer')->user());
        $this->open = true;
    }

    public function close(): void
    {
        $this->open = false;
    }

    public function render(): mixed
    {
        return view('livewire.storefront.cart-drawer');
    }
}
---app/Livewire/Storefront/Home.php
<?php

namespace App\Livewire\Storefront;

use App\Models\Collection;
use App\Models\Product;
use App\Models\Store;
use Livewire\Component;

class Home extends Component
{
    public ?Store $store = null;

    public function mount(): void
    {
        $this->store = app()->bound('current_store') ? app('current_store') : null;
    }

    public function render(): mixed
    {
        if ($this->store === null) {
            return view('livewire.storefront.home-fallback')->layout('layouts.empty');
        }

        return view('livewire.storefront.home', ['collections' => Collection::query()->where('status', 'active')->withCount('products')->latest()->take(4)->get(), 'products' => Product::query()->published()->with(['variants.inventory', 'media'])->latest('published_at')->take(8)->get()])->layout('layouts.storefront');
    }
}
---app/Livewire/Storefront/Account/Dashboard.php
<?php

namespace App\Livewire\Storefront\Account;

use Livewire\Component;

class Dashboard extends Component
{
    public function render(): mixed
    {
        $customer = auth('customer')->user()->loadCount('orders');

        return view('livewire.storefront.account.dashboard', compact('customer'))->layout('layouts.storefront');
    }
}
---app/Livewire/Storefront/Cart/Show.php
<?php

namespace App\Livewire\Storefront\Cart;

use App\Exceptions\InvalidDiscountException;
use App\Models\Cart;
use App\Services\CartService;
use App\Services\CheckoutService;
use App\Services\DiscountService;
use Livewire\Component;

class Show extends Component
{
    public Cart $cart;

    public string $discountCode = '';

    public string $message = '';

    public int $discountAmount = 0;

    public int $totalAmount = 0;

    public function mount(CartService $carts, DiscountService $discounts): void
    {
        $this->cart = $carts->getOrCreateForSession(app('current_store'), auth('customer')->user());
        $this->discountCode = (string) ($this->cart->discount_code ?? '');
        $this->refreshCart($discounts);
    }

    public function increase(int $lineId, CartService $carts, DiscountService $discounts): void
    {
        $line = $this->cart->lines->firstWhere('id', $lineId);

        if ($line === null) {
            return;
        }

        $carts->updateLineQuantity($this->cart, $lineId, $line->quantity + 1);
        $this->refreshCart($discounts);
    }

    public function decrease(int $lineId, CartService $carts, DiscountService $discounts): void
    {
        $line = $this->cart->lines->firstWhere('id', $lineId);

        if ($line === null) {
            return;
        }

        if ($line->quantity > 1) {
            $carts->updateLineQuantity($this->cart, $lineId, $line->quantity - 1);
        }

        $this->refreshCart($discounts);
    }

    public function remove(int $lineId, CartService $carts, DiscountService $discounts): void
    {
        if (! $this->cart->lines->contains('id', $lineId)) {
            return;
        }

        $carts->removeLine($this->cart, $lineId);
        $this->message = 'Item removed from your cart.';
        $this->refreshCart($discounts);
    }

    public function checkout(CheckoutService $checkouts): void
    {
        if ($this->cart->lines->isEmpty()) {
            $this->addError('cart', 'Your cart is empty.');

            return;
        }

        $checkout = $checkouts->create($this->cart, auth('customer')->user()?->email ?? 'guest@example.com', auth('customer')->user());
        $this->redirect(route('checkout.show', $checkout), navigate: true);
    }

    public function applyDiscount(DiscountService $discounts): void
    {
        $this->validate(['discountCode' => ['required', 'string', 'max:64']]);

        try {
            $discount = $discounts->validate(trim($this->discountCode), app('current_store'), $this->cart);
            $this->cart->update(['discount_code' => $discount->code]);
            $this->discountCode = $discount->code;
            $this->message = 'Discount applied.';
            $this->resetValidation('discountCode');
            $this->refreshCart($discounts);
        } catch (InvalidDiscountException $exception) {
            $this->addError('discountCode', $exception->getMessage());
        }
    }

    public function removeDiscount(DiscountService $discounts): void
    {
        $this->cart->update(['discount_code' => null]);
        $this->discountCode = '';
        $this->message = 'Discount removed.';
        $this->resetValidation('discountCode');
        $this->refreshCart($discounts);
    }

    public function formatMoney(int|float $amount): string
    {
        return number_format((float) $amount / 100, 2, '.', ',').' '.($this->cart->currency ?: 'EUR');
    }

    private function refreshCart(DiscountService $discounts): void
    {
        $this->cart = $this->cart->refresh()->load(['lines.variant.product.media', 'lines.variant.product.collections', 'lines.variant.inventory']);
        $this->discountAmount = $this->calculateDiscountAmount($discounts);
        $subtotal = (int) $this->cart->lines->sum('line_subtotal_amount');
        $this->totalAmount = max(0, $subtotal - $this->discountAmount);
    }

    private function calculateDiscountAmount(DiscountService $discounts): int
    {
        if ($this->cart->discount_code === null || $this->cart->lines->isEmpty()) {
            return 0;
        }

        try {
            $discount = $discounts->validate($this->cart->discount_code, app('current_store'), $this->cart);
        } catch (InvalidDiscountException) {
            return 0;
        }

        $subtotal = (int) $this->cart->lines->sum('line_subtotal_amount');
        $result = $discounts->calculate($discount, $subtotal, $this->cart->lines->map(fn ($line): array => [
            'line_id' => $line->id,
            'amount' => $line->line_subtotal_amount,
            'product_id' => $line->variant->product_id,
            'collection_ids' => $line->variant->product->collections->modelKeys(),
        ])->all());

        return $result->amount;
    }

    public function render(): mixed
    {
        return view('livewire.storefront.cart.show')->layout('layouts.storefront');
    }
}
---app/Livewire/Storefront/Checkout/Confirmation.php
<?php

namespace App\Livewire\Storefront\Checkout;

use App\Models\Order;
use Illuminate\Support\Str;
use Livewire\Component;

class Confirmation extends Component
{
    public Order $order;

    public function mount(string $checkoutId): void
    {
        $this->order = Order::query()
            ->with(['lines.variant.product.media', 'customer', 'checkout', 'payments'])
            ->where('checkout_id', $checkoutId)
            ->first()
            ?? Order::query()
                ->with(['lines.variant.product.media', 'customer', 'checkout', 'payments'])
                ->where('id', $checkoutId)
                ->firstOrFail();

        $customerId = auth('customer')->id();
        $sessionCartIds = array_filter([
            session('cart_id'),
            session('cart_id_'.app('current_store')->getKey()),
        ]);

        abort_unless(
            ($customerId !== null && (int) $this->order->customer_id === (int) $customerId)
                || ($customerId === null && in_array($this->order->checkout?->cart_id, $sessionCartIds, true)),
            404,
        );
    }

    public function formatMoney(int|float $amount): string
    {
        return number_format((float) $amount / 100, 2, '.', ',').' '.($this->order->currency ?: 'EUR');
    }

    public function paymentLabel(): string
    {
        return match ((string) $this->order->payment_method) {
            'credit_card' => 'Credit card',
            'paypal' => 'PayPal',
            'bank_transfer' => 'Bank transfer',
            default => Str::headline((string) $this->order->payment_method),
        };
    }

    public function isBankTransfer(): bool
    {
        return $this->order->payment_method === 'bank_transfer';
    }

    public function canViewAccountOrder(): bool
    {
        return auth('customer')->check() && $this->order->customer_id !== null;
    }

    public function render(): mixed
    {
        return view('livewire.storefront.checkout.confirmation')->layout('layouts.storefront');
    }
}
---app/Livewire/Storefront/Checkout/Show.php
<?php

namespace App\Livewire\Storefront\Checkout;

use App\Enums\CheckoutStatus;
use App\Enums\PaymentMethod;
use App\Exceptions\InvalidDiscountException;
use App\Models\Checkout as CheckoutModel;
use App\Models\CustomerAddress;
use App\Services\CheckoutService;
use App\Services\DiscountService;
use App\Services\PaymentService;
use App\Services\PricingEngine;
use App\Services\ShippingCalculator;
use Carbon\Carbon;
use Livewire\Component;
use Throwable;

class Show extends Component
{
    public CheckoutModel $checkout;

    public string $email = '';

    /** @var array<string, string> */
    public array $shippingAddress = [
        'first_name' => '',
        'last_name' => '',
        'address1' => '',
        'address2' => '',
        'city' => '',
        'state' => '',
        'country_code' => 'DE',
        'postal_code' => '',
        'phone' => '',
    ];

    /** @var array<string, string> */
    public array $billingAddress = [
        'first_name' => '',
        'last_name' => '',
        'address1' => '',
        'address2' => '',
        'city' => '',
        'state' => '',
        'country_code' => 'DE',
        'postal_code' => '',
        'phone' => '',
    ];

    public bool $billingSameAsShipping = true;

    public ?int $savedAddressId = null;

    public ?int $shippingRateId = null;

    public string $paymentMethod = PaymentMethod::CreditCard->value;

    public string $cardNumber = '4242424242424242';

    public string $cardholderName = '';

    public string $cardExpiry = '';

    public string $cardCvc = '';

    public string $discountCode = '';

    public string $message = '';

    public bool $processing = false;

    public int $activeStep = 1;

    public bool $showOrderSummary = false;

    public function mount(int $checkoutId): void
    {
        $this->checkout = CheckoutModel::query()
            ->with(['cart.lines.variant.product.media', 'shippingRate'])
            ->findOrFail($checkoutId);

        $customerId = auth('customer')->id();
        $sessionCartIds = array_filter([
            session('cart_id'),
            session('cart_id_'.app('current_store')->getKey()),
        ]);

        abort_unless(
            ($customerId !== null && (int) $this->checkout->customer_id === (int) $customerId)
                || ($customerId === null && in_array($this->checkout->cart_id, $sessionCartIds, true)),
            404,
        );

        $this->email = (string) $this->checkout->email;
        $this->discountCode = (string) ($this->checkout->discount_code ?? '');
        $this->shippingRateId = $this->checkout->shipping_rate_id;
        $this->paymentMethod = (string) ($this->checkout->payment_method ?? PaymentMethod::CreditCard->value);

        if ($this->checkout->shipping_address_json !== null) {
            $this->shippingAddress = array_merge($this->shippingAddress, $this->checkout->shipping_address_json);
        }

        if ($this->checkout->billing_address_json !== null) {
            $this->billingAddress = array_merge($this->billingAddress, $this->checkout->billing_address_json);
            $this->billingSameAsShipping = $this->billingAddress === $this->shippingAddress;
        }

        $this->activeStep = match ($this->checkout->status) {
            CheckoutStatus::Started => 1,
            CheckoutStatus::Addressed => 3,
            CheckoutStatus::ShippingSelected, CheckoutStatus::PaymentSelected, CheckoutStatus::PaymentPending, CheckoutStatus::Completed => 4,
            default => 1,
        };
    }

    public function saveContact(): void
    {
        $this->validate([
            'email' => ['required', 'email', 'max:255'],
        ], [
            'email.required' => 'Enter an email address to receive your order confirmation.',
        ]);

        $this->checkout->update(['email' => $this->email]);
        $this->checkout->refresh();
        $this->message = 'Contact information saved.';
        $this->activeStep = 2;
    }

    public function saveAddress(CheckoutService $checkouts, PricingEngine $pricing): void
    {
        $this->normalizeAddresses();

        $rules = [
            'email' => ['required', 'email', 'max:255'],
            ...$this->addressRules('shippingAddress'),
        ];

        if (! $this->billingSameAsShipping) {
            $rules = [...$rules, ...$this->addressRules('billingAddress')];
        }

        $this->validate($rules);

        $this->checkout->update(['email' => $this->email]);
        $this->checkout = $checkouts->setAddress(
            $this->checkout->refresh(),
            $this->shippingAddress,
            $this->billingAddress,
            $this->billingSameAsShipping,
        );

        if ($this->requiresShipping()) {
            $this->checkout->update([
                'shipping_rate_id' => null,
                'shipping_method_id' => null,
                'status' => CheckoutStatus::Addressed,
            ]);
            $this->checkout = $this->checkout->refresh();
            $pricing->calculate($this->checkout);
            $this->checkout = $this->checkout->refresh();
            $this->shippingRateId = null;
            $this->activeStep = 3;
        } else {
            $this->checkout = $checkouts->setShippingMethod($this->checkout, 0);
            $this->shippingRateId = null;
            $this->activeStep = 4;
        }

        $this->message = 'Address saved.';
    }

    public function selectSavedAddress(int|string|null $addressId): void
    {
        $customerId = auth('customer')->id();

        if ($customerId === null || $addressId === null || $addressId === '') {
            $this->savedAddressId = null;

---app/Livewire/Storefront/Collections/Index.php
<?php

namespace App\Livewire\Storefront\Collections;

use App\Models\Collection as ProductCollection;
use Livewire\Component;

class Index extends Component
{
    public function render(): mixed
    {
        return view('livewire.storefront.collections.index', ['collections' => ProductCollection::query()->where('status', 'active')->latest()->get()])->layout('layouts.storefront');
    }
}
---app/Livewire/Storefront/Collections/Show.php
<?php

namespace App\Livewire\Storefront\Collections;

use App\Models\Collection as ProductCollection;
use Livewire\Component;

class Show extends Component
{
    public ProductCollection $collection;

    public string $sort = 'featured';

    public bool $inStock = false;

    public function mount(string $handle): void
    {
        $this->collection = ProductCollection::query()->where('handle', $handle)->with(['products.variants.inventory', 'products.media'])->firstOrFail();
    }

    public function render(): mixed
    {
        $products = $this->collection->products->filter(fn ($product): bool => $product->status->value === 'active' && (! $this->inStock || $product->variants->contains(fn ($variant): bool => $variant->availableQuantity() > 0)));

        if ($this->sort === 'price_asc') {
            $products = $products->sortBy(fn ($product): int => $product->defaultVariant()?->price_amount ?? 0);
        } elseif ($this->sort === 'price_desc') {
            $products = $products->sortByDesc(fn ($product): int => $product->defaultVariant()?->price_amount ?? 0);
        } elseif ($this->sort === 'newest') {
            $products = $products->sortByDesc('created_at');
        }

        return view('livewire.storefront.collections.show', ['products' => $products])->layout('layouts.storefront');
    }

    public function clearFilters(): void
    {
        $this->inStock = false;
        $this->sort = 'featured';
    }
}
---app/Livewire/Storefront/Pages/Show.php
<?php

namespace App\Livewire\Storefront\Pages;

use App\Models\Page;
use Livewire\Component;

class Show extends Component
{
    public Page $page;

    public function mount(string $handle): void
    {
        $this->page = Page::query()->where('handle', $handle)->where('status', 'published')->firstOrFail();
    }

    public function render(): mixed
    {
        return view('livewire.storefront.pages.show')->layout('layouts.storefront');
    }
}
---app/Livewire/Storefront/Products/Show.php
<?php

namespace App\Livewire\Storefront\Products;

use App\Exceptions\InsufficientInventoryException;
use App\Models\Product;
use App\Services\CartService;
use Livewire\Component;

class Show extends Component
{
    public Product $product;

    public int $selectedVariantId;

    /** @var array<int, int> */
    public array $selectedOptions = [];

    public int $quantity = 1;

    public string $message = '';

    public function mount(string $handle): void
    {
        $this->product = Product::query()->with(['variants.inventory', 'variants.optionValues', 'media', 'options.values'])->where('handle', $handle)->firstOrFail();
        abort_unless($this->product->status->value === 'active', 404);
        $this->selectedVariantId = $this->product->defaultVariant()?->getKey() ?? 0;
        $default = $this->product->defaultVariant();
        $this->selectedOptions = $default?->optionValues->mapWithKeys(fn ($value): array => [$value->product_option_id => $value->getKey()])->all() ?? [];
    }

    public function addToCart(CartService $carts): void
    {
        $cart = $carts->getOrCreateForSession(app('current_store'), auth('customer')->user());
        try {
            $carts->addLine($cart, $this->selectedVariantId, $this->quantity);
        } catch (InsufficientInventoryException $exception) {
            $this->addError('quantity', $exception->getMessage());

            return;
        }
        $this->message = 'Added to cart';
        $this->dispatch('cart-updated');
    }

    public function selectVariant(int $variantId): void
    {
        abort_unless($this->product->variants->contains('id', $variantId), 404);

        $this->selectedVariantId = $variantId;
    }

    public function selectOption(int $optionId, int $valueId): void
    {
        abort_unless($this->product->options->firstWhere('id', $optionId)?->values->contains('id', $valueId), 404);
        $this->selectedOptions[$optionId] = $valueId;
        $selectedValueIds = array_values($this->selectedOptions);
        $variant = $this->product->variants->first(function ($candidate) use ($selectedValueIds): bool {
            $candidateValueIds = $candidate->optionValues->modelKeys();

            return count($selectedValueIds) === count($candidateValueIds)
                && count(array_intersect($selectedValueIds, $candidateValueIds)) === count($selectedValueIds);
        });

        if ($variant !== null) {
            $this->selectedVariantId = $variant->getKey();
        }
    }

    public function render(): mixed
    {
        $this->product->loadMissing(['variants.inventory', 'variants.optionValues', 'media', 'options.values']);

        return view('livewire.storefront.products.show')->layout('layouts.storefront');
    }
}
---app/Livewire/Storefront/Search/Index.php
<?php

namespace App\Livewire\Storefront\Search;

use App\Services\SearchService;
use Livewire\Component;

class Index extends Component
{
    public string $query = '';

    public function mount(?string $q = null): void
    {
        $this->query = $q ?? request()->string('q')->toString();
    }

    public function render(SearchService $search): mixed
    {
        $products = $search->search(app('current_store'), $this->query, [], 12);

        return view('livewire.storefront.search.index', compact('products'))->layout('layouts.storefront');
    }
}
---app/Livewire/Storefront/Search/Modal.php
<?php

namespace App\Livewire\Storefront\Search;

use App\Services\SearchService;
use Livewire\Component;

class Modal extends Component
{
    public string $query = '';

    public bool $open = false;

    public function updatedQuery(SearchService $search): void
    {
        if (! $this->open) {
            return;
        }

        $this->suggestions = $search->autocomplete(app('current_store'), $this->query)->map(fn ($product): array => ['id' => $product->getKey(), 'title' => $product->title, 'handle' => $product->handle])->all();
    }

    /** @var array<int, array{id: int, title: string, handle: string}> */
    public array $suggestions = [];

    public function render(): mixed
    {
        return view('livewire.storefront.search.modal');
    }
}