diff --git a/.cursor/plans/decouple-ewjdev-anyclick-react-styling-and-ship-default-style-providers.md b/.cursor/plans/decouple-ewjdev-anyclick-react-styling-and-ship-default-style-providers.md
new file mode 100644
index 0000000..c45eb83
--- /dev/null
+++ b/.cursor/plans/decouple-ewjdev-anyclick-react-styling-and-ship-default-style-providers.md
@@ -0,0 +1,399 @@
+# Decouple `@ewjdev/anyclick-react` Styling and Ship Default Style Providers
+
+## Summary
+
+Refactor `@ewjdev/anyclick-react` into a headless-by-default UI package that renders Anyclick behavior and structure without importing Tailwind or bundling a styling framework. Move styling into a formal adapter layer, then ship three companion packages for the default styling systems:
+
+- `@ewjdev/anyclick-react-tailwind`
+- `@ewjdev/anyclick-react-shadcn`
+- `@ewjdev/anyclick-react-mui`
+
+This will be a major-version change for `@ewjdev/anyclick-react`. The current implicit Tailwind import in [`packages/anyclick-react/src/index.ts`](/Users/ericjohnson/Desktop/projects/anyclick/packages/anyclick-react/src/index.ts) is removed, and the current mixed styling model in [`packages/anyclick-react/src/styles.ts`](/Users/ericjohnson/Desktop/projects/anyclick/packages/anyclick-react/src/styles.ts), [`packages/anyclick-react/src/ContextMenu.tsx`](/Users/ericjohnson/Desktop/projects/anyclick/packages/anyclick-react/src/ContextMenu.tsx), [`packages/anyclick-react/src/ScreenshotPreview.tsx`](/Users/ericjohnson/Desktop/projects/anyclick/packages/anyclick-react/src/ScreenshotPreview.tsx), and [`packages/anyclick-react/src/ui/button.tsx`](/Users/ericjohnson/Desktop/projects/anyclick/packages/anyclick-react/src/ui/button.tsx) is replaced by a slot-based styling contract plus optional component overrides.
+
+## Context Summary
+
+Current coupling points:
+
+- `@ewjdev/anyclick-react` auto-imports Tailwind CSS from package entry.
+- The package uses a hybrid of:
+ - inline style maps for menu/screenshot surfaces
+ - Tailwind-prefixed utility classes for buttons, inputs, icons, and some layout
+ - CSS variables from `tokens.css`
+- Existing public styling hooks are too narrow:
+ - `menuClassName`
+ - `menuStyle`
+ - `AnyclickTheme.menuClassName`
+ - `AnyclickTheme.menuStyle`
+- Styling inheritance today is merged through provider theme logic, not a reusable styling system.
+
+Target outcome:
+
+- Consumers can style Anyclick using the same mechanism as the host app.
+- Tailwind, shadcn-style, and MUI defaults are first-class and shipped by us.
+- Core runtime no longer depends on Tailwind or any other UI framework.
+
+## Architecture Decisions and Rationale
+
+- `@ewjdev/anyclick-react` becomes headless and framework-agnostic for styling.
+Reason: this removes hard coupling, avoids shipping unwanted CSS, and gives a clean base for all style systems.
+
+- Styling defaults ship as companion packages, not subpath exports inside the core package.
+Reason: MUI brings heavy peer dependencies, and companion packages keep dependency graphs clean and predictable.
+
+- The new styling contract is slot-based with optional component overrides.
+Reason: Tailwind and shadcn work well with class-slot recipes, while MUI needs actual component wrappers. A contract that supports both avoids maintaining multiple incompatible rendering paths.
+
+- Shadcn support ships in two layers.
+Reason: you chose “both.”
+ - Default shadcn-style class/token adapter for plain DOM rendering.
+ - Optional consumer-supplied component overrides for teams that want to inject local shadcn/ui components.
+
+- This is a major-version migration now, not a transitional minor.
+Reason: you chose “major now,” so we should not preserve implicit Tailwind as the default path.
+
+- Existing `menuClassName` and `menuStyle` remain as deprecated compatibility shims for one major cycle.
+Reason: even in a major, this materially reduces migration pain and lets us map old props onto the new slot model without blocking the cleaner architecture.
+
+## Public API and Interface Changes
+
+### Core package: `@ewjdev/anyclick-react`
+
+Add:
+
+- `AnyclickStyleAdapter`
+- `AnyclickStyleProvider`
+- `useAnyclickStyle`
+- `AnyclickStyleTokens`
+- `AnyclickStyleSlot`
+- `AnyclickSlotState`
+- `AnyclickSlotProps`
+- `AnyclickComponentOverrides`
+- `slotClassNames?: Partial>` on `AnyclickProviderProps`
+- `slotStyles?: Partial>` on `AnyclickProviderProps`
+- `components?: Partial` on `AnyclickProviderProps`
+- `styleAdapter?: AnyclickStyleAdapter` on `AnyclickProviderProps`
+
+Deprecate but keep working in this major:
+
+- `menuClassName`
+- `menuStyle`
+- `AnyclickTheme.menuClassName`
+- `AnyclickTheme.menuStyle`
+
+Remove from core package behavior:
+
+- implicit `import "./styles/tailwind.css"` from package entry
+- Tailwind as a required styling dependency for rendering
+
+### New core styling contract
+
+Define a fixed slot taxonomy covering all user-visible surfaces shipped by `@ewjdev/anyclick-react`:
+
+- `menu.overlay`
+- `menu.surface`
+- `menu.header`
+- `menu.headerAction`
+- `menu.list`
+- `menu.item`
+- `menu.itemIcon`
+- `menu.itemLabel`
+- `menu.itemBadge`
+- `menu.submenuIndicator`
+- `menu.backButton`
+- `menu.dragHandle`
+- `comment.section`
+- `comment.textarea`
+- `comment.primaryAction`
+- `comment.secondaryAction`
+- `screenshot.surface`
+- `screenshot.header`
+- `screenshot.tab`
+- `screenshot.tabActive`
+- `screenshot.preview`
+- `screenshot.empty`
+- `screenshot.error`
+- `screenshot.meta`
+- `screenshot.action`
+- `quickChat.surface`
+- `quickChat.header`
+- `quickChat.messageList`
+- `quickChat.input`
+- `quickChat.submit`
+- `inspect.surface`
+- `inspect.header`
+- `inspect.content`
+- `inspect.action`
+- `shared.button`
+- `shared.input`
+- `shared.textarea`
+- `shared.badge`
+
+Define slot resolver behavior as:
+
+- input: slot name + semantic state + resolved tokens + consumer overrides
+- output: `{ className?, style?, attrs? }`
+
+Define `AnyclickComponentOverrides` for the few places where DOM replacement is required:
+
+- `Surface`
+- `Button`
+- `IconButton`
+- `Input`
+- `Textarea`
+- `Tabs`
+- `Tab`
+- `Badge`
+
+Do not make icons part of v1 adapter scope.
+Reason: style portability is the primary problem. Icon replacement can be added later if needed.
+
+### Companion packages
+
+#### `@ewjdev/anyclick-react-tailwind`
+
+Exports:
+
+- `createTailwindAnyclickStyleAdapter(options?)`
+- `TailwindAnyclickStyleProvider`
+- `tailwind.css`
+
+Behavior:
+
+- provides class-slot recipes using Tailwind utilities
+- ships the current default visual look, adjusted to match today’s UX closely
+- uses package-level CSS only inside the companion package, not core
+
+#### `@ewjdev/anyclick-react-shadcn`
+
+Exports:
+
+- `createShadcnAnyclickStyleAdapter(options?)`
+- `ShadcnAnyclickStyleProvider`
+- `createShadcnComponentOverrides(overrides)`
+- optional `shadcn.css` token bridge
+
+Behavior:
+
+- default adapter renders plain DOM with class recipes aligned to shadcn-style tokens and utility conventions
+- optional component override factory accepts consumer components for:
+ - `Button`
+ - `Input`
+ - `Textarea`
+ - `Tabs`
+ - `Tab`
+ - `Badge`
+ - `Surface`/`Card`
+- no runtime dependency on a generated shadcn package
+
+#### `@ewjdev/anyclick-react-mui`
+
+Exports:
+
+- `createMuiAnyclickStyleAdapter(options?)`
+- `MuiAnyclickStyleProvider`
+
+Peer dependencies:
+
+- `@mui/material`
+- `@emotion/react`
+- `@emotion/styled`
+
+Behavior:
+
+- uses MUI primitives for surfaces and controls
+- maps semantic tokens to `sx` and MUI theme values
+- default components:
+ - `Paper` for surfaces
+ - `Button`/`IconButton`
+ - `TextField` or multiline input wrapper
+ - `Tabs`/`Tab`
+ - `Chip` for badges
+ - `CircularProgress` for loading affordances
+
+## Step-by-Step Plan
+
+1. Inventory and freeze the render surface.
+- Enumerate every rendered DOM/control surface in `ContextMenu`, `ScreenshotPreview`, `QuickChat`, `InspectSimple`, shared button/input primitives, and provider wrappers.
+- Create a slot map document in the repo that ties each rendered node to one slot name and one semantic state model.
+- Explicitly mark non-stylable internals out of scope for v1: event plumbing, highlight math, adapter submission flow.
+
+2. Introduce a dedicated styling context in core.
+- Add `AnyclickStyleContext`.
+- Add `AnyclickStyleProvider`.
+- Add merge rules:
+ - base unstyled adapter
+ - inherited adapter from nearest style provider
+ - local `styleAdapter` on `AnyclickProvider`
+ - local `slotClassNames`, `slotStyles`, `components`
+ - deprecated `menuClassName` and `menuStyle` mapped last onto `menu.surface`
+- Keep styling inheritance separate from existing behavioral `theme` inheritance.
+
+3. Define and implement the core slot resolver API.
+- Add canonical slot names and state types.
+- Add helper utilities:
+ - `mergeStyleAdapters`
+ - `resolveSlotProps`
+ - `composeClassNames`
+ - `composeInlineStyles`
+- Normalize state shape so all adapters receive consistent state keys:
+ - `active`
+ - `disabled`
+ - `hovered`
+ - `pressed`
+ - `expanded`
+ - `selected`
+ - `error`
+ - `loading`
+ - `tone`
+ - `size`
+
+4. Refactor the current React components to consume slot props instead of hardcoded styles/classes.
+- Replace direct reads from `menuStyles` for structural rendering with slot resolution.
+- Replace Tailwind utility strings inside core components with slot-provided classes.
+- Leave the current semantic markup and behavior intact unless required by MUI componentization.
+- Keep the DOM structure stable where possible to avoid behavioral regressions.
+
+5. Convert `styles.ts` into semantic token defaults, not UI implementation.
+- Retain only neutral semantic defaults needed for an unstyled fallback and behavior-safe layout.
+- Move current appearance-specific rules into the Tailwind companion package.
+- Remove Tailwind-specific CSS files from core package entry and build outputs.
+
+6. Build the minimal unstyled core fallback.
+- Core should still render accessibly if no style provider is present.
+- The fallback should be intentionally plain but usable:
+ - readable typography
+ - visible borders
+ - keyboard focus states
+ - accessible button/input affordances
+- Emit a development warning when no non-fallback style adapter is configured.
+
+7. Build `@ewjdev/anyclick-react-tailwind`.
+- Port the current look into slot recipes and CSS owned by the Tailwind adapter package.
+- Export both a provider and a raw adapter factory.
+- Ensure this package is the documented replacement for current out-of-the-box usage.
+
+8. Build `@ewjdev/anyclick-react-shadcn`.
+- Implement class/token adapter first.
+- Add optional consumer-component override factory second.
+- Define the override interface precisely so a consumer can pass their own local shadcn components without wrapping every slot manually.
+- Publish examples showing:
+ - class/token only
+ - component override mode
+
+9. Build `@ewjdev/anyclick-react-mui`.
+- Implement MUI-specific component overrides rather than forcing class recipes.
+- Require the consumer app’s existing MUI `ThemeProvider`.
+- Expose optional adapter options for:
+ - density
+ - variant mapping
+ - elevation
+ - use of `TextField` vs custom multiline field
+- Validate portals/z-index interactions against Anyclick’s overlay behavior.
+
+10. Update documentation, examples, and migration materials.
+- Replace all docs that imply `@ewjdev/anyclick-react` self-styles via Tailwind.
+- Add three first-class setup guides:
+ - Tailwind
+ - shadcn
+ - MUI
+- Add a migration guide:
+ - old usage
+ - new provider/adaptor setup
+ - deprecated prop mapping
+ - common migration pitfalls
+
+11. Release strategy.
+- Ship a major for `@ewjdev/anyclick-react`.
+- Ship first stable versions of the companion style packages in the same release train.
+- Keep deprecated prop shims for one major line, then remove them in the next major after adoption.
+
+## Testing and Validation
+
+### Unit and integration coverage
+
+- `AnyclickProvider` resolves style adapters from provider context, local props, and deprecated shims in the correct precedence order.
+- `resolveSlotProps` merges classes and styles deterministically.
+- Deprecated `menuClassName` and `menuStyle` still affect `menu.surface`.
+- Rendering without a style provider falls back to accessible minimal styling.
+- Tailwind adapter applies expected slot classes for default menu, comment form, screenshot preview, quick chat, and inspect surfaces.
+- Shadcn class/token adapter renders without consumer component injection.
+- Shadcn component override mode correctly uses consumer-supplied components.
+- MUI adapter renders with MUI components and respects the host theme.
+
+### Behavioral regression coverage
+
+- Right-click menu open/close behavior unchanged.
+- Touch press-and-hold behavior unchanged.
+- Keyboard navigation and focus trapping unchanged.
+- Screenshot preview flow unchanged.
+- QuickChat flow unchanged.
+- Nested provider theme inheritance still works for non-styling behavior fields.
+- Scoped providers still isolate event handling correctly.
+
+### Visual validation
+
+- Storybook or equivalent component gallery for:
+ - unstyled fallback
+ - Tailwind default
+ - shadcn class/token
+ - shadcn component override
+ - MUI
+- Visual snapshots for:
+ - default menu
+ - submenu
+ - comment form
+ - screenshot preview
+ - loading/error states
+ - quick chat
+ - tiny inspect dialog
+
+### Acceptance criteria
+
+- `@ewjdev/anyclick-react` has no Tailwind import and no Tailwind dependency.
+- A consumer can style all shipped surfaces without forking core components.
+- Tailwind, shadcn, and MUI setup each take one provider/adaptor import plus normal host-app theme setup.
+- MUI package does not leak MUI dependencies into users who do not install it.
+- Shadcn package works both without and with consumer component injection.
+
+## Risks and Mitigations
+
+- Risk: slot taxonomy misses a surface and forces ad hoc styling later.
+Mitigation: complete inventory first and require every rendered node to map to a named slot before refactor starts.
+
+- Risk: MUI component overrides change DOM structure enough to break measurements, focus, or portal behavior.
+Mitigation: keep overlay positioning and event logic in core; MUI adapter only swaps leaf/surface primitives, not behavior containers.
+
+- Risk: shadcn is not a true runtime dependency model.
+Mitigation: treat shadcn default support as class/token recipes first, then optional consumer component injection.
+
+- Risk: too many override axes create an unmaintainable API.
+Mitigation: separate concerns clearly:
+ - tokens for design semantics
+ - slots for class/style recipes
+ - components only for control/surface replacement
+
+- Risk: migration pain for current users.
+Mitigation: keep deprecated `menuClassName` and `menuStyle` shims, provide Tailwind adapter that recreates the current experience, and publish a direct migration guide.
+
+## Rollout and Monitoring
+
+1. Land the core style-adapter refactor behind the new public API in the major branch.
+2. Land the Tailwind adapter package and migrate first-party examples to it.
+3. Land shadcn and MUI packages.
+4. Update docs site and README examples to never show bare `AnyclickProvider` without a style provider unless intentionally demonstrating the unstyled fallback.
+5. Monitor:
+- issue volume for styling regressions
+- install/download split between the three companion packages
+- reports of missing slots or unstyleable surfaces
+- DOM/focus regressions in MUI mode
+
+## Assumptions and Defaults Chosen
+
+- Packaging strategy: companion packages.
+- Migration posture: major now.
+- Shadcn model: both class/token adapter and optional consumer component overrides.
+- Icons remain Lucide-based in v1 of the styling refactor.
+- Core continues to own behavior, event handling, focus, and positioning.
+- Style adapters own appearance and primitive/component selection.
+- Deprecated `menuClassName` and `menuStyle` are retained for one major to ease migration, even though the package is otherwise headless by default.
+- No additional styling systems beyond Tailwind, shadcn, and MUI are included in this release.
diff --git a/.cursor/plans/homepage_scenario_workflow_dialogs_20260216.plan.md b/.cursor/plans/homepage_scenario_workflow_dialogs_20260216.plan.md
new file mode 100644
index 0000000..cd16f0c
--- /dev/null
+++ b/.cursor/plans/homepage_scenario_workflow_dialogs_20260216.plan.md
@@ -0,0 +1,89 @@
+# Homepage Scenario Workflow Dialogs
+
+## Summary
+Implement a shared workflow-drawer system on the homepage immersive workstreams so each scenario has exactly 3 right-click actions, and each action opens a relevant multi-step workflow dialog.
+
+Locked choices:
+- GitHub submission is real only for software "Report this bug" GitHub path.
+- All other workflow submissions are demo-only.
+- Keep software code editor visual, but keep menu actions fixed.
+- Use heuristic AI-style title generation (no model call).
+
+## Assumptions and Defaults
+1. Use right-side panel UI for all workflows.
+2. Every workflow shows Anyclick context: target selector, container selector, container screenshot, page URL/title.
+3. Screenshot failures are non-blocking and show fallback.
+4. GitHub failures/non-configured state offer "Complete as demo" fallback.
+5. Changes remain in `apps/web` only (no `packages/*` API changes).
+
+## Public API / Interface / Type Changes
+1. Add `POST /api/feedback/github` at `apps/web/src/app/api/feedback/github/route.ts`.
+2. Endpoint body is `AnyclickPayload`; supports:
+ - `metadata.github.title` as preferred GitHub issue title.
+ - `metadata.routing.adapter = "github"`.
+3. Internal immersive changes:
+ - Remove menu definitions from theme ownership.
+ - Add workflow types under `apps/web/src/components/immersive-workstream/workflows/types.ts`.
+
+## Workflow Catalog
+- Software Development
+ - Report this bug: Ticket system -> Issue details -> Review & submit (GitHub real, Jira demo)
+ - Send to Cursor: Fix scope -> Prompt & constraints -> Review (demo)
+ - Copy selector: Selector format -> Copy bundle (demo)
+- E-commerce & Logistics
+ - Item missing: Incident details -> Resolution path -> Confirm (demo)
+ - Shipping issue: Shipment details -> Customer impact -> Confirm (demo)
+ - Escalate order: Escalation target -> Priority/SLA -> Confirm (demo)
+- Healthcare
+ - Check-in issue: Issue type -> Patient context -> Confirm (demo)
+ - Vital alert: Vital entry -> Clinical action -> Confirm (demo)
+ - Flag urgent: Urgency classification -> Notification route -> Confirm (demo)
+- Social Media
+ - Save asset: Destination -> Metadata/tags -> Confirm (demo)
+ - Flag content: Policy category -> Enforcement action -> Confirm (demo)
+ - Quick reply: Draft reply -> Tone/approval -> Confirm (demo)
+
+## Architecture Decisions
+1. Use `ContextMenuItem.onClick` for all 12 actions; bypass default submit flow.
+2. Build shared workflow launcher + drawer; action definitions live in registry.
+3. Keep software editor visible and add fixed-workflow indicator.
+4. Capture context with Anyclick core utilities:
+ - `buildElementContext`
+ - `captureScreenshot(..., "container")`
+5. Isolate GitHub submit via dedicated `/api/feedback/github` endpoint.
+
+## Implementation Steps
+1. Add workflow foundation files:
+ - `workflows/types.ts`
+ - `workflows/registry.ts`
+ - `workflows/titleHeuristics.ts`
+ - `workflows/contextCapture.ts`
+2. Add shared launcher + UI:
+ - `workflows/WorkflowDrawer.tsx`
+ - `workflows/useWorkflowLauncher.tsx`
+ - `workflows/WorkflowFlowRenderer.tsx`
+3. Integrate sections:
+ - `sections/SoftwareDevelopmentSection.tsx`
+ - `sections/DefaultWorkstreamSection.tsx`
+ - `sections/HealthcareSection.tsx`
+4. Lock software editor behavior and copy:
+ - `cards/SoftwareEditorCard.tsx`
+ - `softwareCode.ts`
+5. Move menu ownership out of theme model:
+ - `types.ts`
+ - `themes.tsx`
+6. Add GitHub-only endpoint:
+ - `app/api/feedback/github/route.ts`
+7. Wire software GitHub submit path in workflow renderer.
+
+## Validation
+1. Each scenario shows exactly 3 right-click menu items.
+2. All 12 actions open relevant dialogs with multi-step forms.
+3. Context panel shows selector/container selector/screenshot or fallback.
+4. Software bug flow first step has Jira + GitHub choices.
+5. GitHub title prefilled heuristically; editable/regeneratable.
+6. GitHub submit succeeds when configured.
+7. GitHub failure offers demo completion.
+8. All non-software flows complete without backend dependency.
+9. Escape/close behavior works and body scroll restores.
+10. Run `yarn --cwd /Users/ericjohnson/Desktop/projects/anyclick/apps/web lint`.
diff --git a/.cursor/plans/swift_native_anyclick_implementation_69a4d5c4.plan.md b/.cursor/plans/swift_native_anyclick_implementation_69a4d5c4.plan.md
new file mode 100644
index 0000000..d9309a2
--- /dev/null
+++ b/.cursor/plans/swift_native_anyclick_implementation_69a4d5c4.plan.md
@@ -0,0 +1,158 @@
+---
+name: Swift Native AnyClick Implementation
+overview: Create a native macOS application using Swift that intercepts right-clicks, resolves context (including deep browser context via an extension bridge), and displays a dynamic, graph-based native menu.
+todos:
+ - id: scaffold-swift-project
+ content: Scaffold Swift Project in `apps/anyclick-native`
+ status: pending
+ - id: implement-input-monitor
+ content: Implement InputMonitor and MenuRenderer (Proof of Concept)
+ status: pending
+ dependencies:
+ - scaffold-swift-project
+ - id: implement-graph-engine
+ content: Design and Implement Graph Data Structure in Swift
+ status: pending
+ dependencies:
+ - scaffold-swift-project
+ - id: update-extension-manifest
+ content: Update Extension Manifest for Native Messaging
+ status: pending
+ - id: implement-browser-bridge
+ content: Implement Browser Bridge in Swift and Extension
+ status: pending
+ dependencies:
+ - update-extension-manifest
+ - scaffold-swift-project
+isProject: false
+---
+
+# AnyClick Native macOS Implementation Plan
+
+This plan outlines the creation of a high-performance native macOS application that replaces/augments the web-based AnyClick flows. It uses system-level hooks for event tracking and a bridge to the browser for DOM interactions.
+
+## Architecture
+
+```mermaid
+graph TD
+ User((User)) -->|Right Click| InputMonitor[Input Monitor (CGEventTap)]
+ InputMonitor -- Suppress OS Menu --> ContextEngine[Context Engine]
+
+ subgraph Native App [Swift macOS App]
+ InputMonitor
+ ContextEngine
+ GraphEngine[Graph Engine]
+ MenuRenderer[Menu Renderer]
+ ActionExecutor[Action Executor]
+ end
+
+ subgraph Browser [Chrome/Arc/Safari]
+ Extension[AnyClick Extension]
+ DOM[DOM Context]
+ end
+
+ ContextEngine -->|Get Active App| OS[macOS Accessibility API]
+ ContextEngine -.->|Native Messaging| Extension
+
+ Extension -->|DOM Data| ContextEngine
+
+ ContextEngine -->|Context + AppID| GraphEngine
+ GraphEngine -->|Menu Items| MenuRenderer
+ MenuRenderer -->|Show| User
+
+ User -->|Select Action| ActionExecutor
+ ActionExecutor -->|Perform| OS
+ ActionExecutor -->|Execute Script| Extension
+```
+
+
+
+## Core Components
+
+### 1. Native Application Structure (`apps/anyclick-native`)
+
+- **Technology**: Swift 6, AppKit (for low-level events/menus), SwiftUI (for Settings UI).
+- **Permissions**:
+ - `Accessibility`: To read selected text and UI elements.
+ - `Input Monitoring`: To intercept global mouse clicks.
+ - `Native Messaging`: To communicate with the browser extension.
+
+### 2. The Graph Engine
+
+A JSON-based configuration engine that resolves menu items based on `(Application, Context)`.
+
+**Schema Definition:**
+
+```json
+{
+ "rules": [
+ {
+ "appId": "com.microsoft.VSCode",
+ "context": { "hasSelection": true },
+ "actions": ["copy", "save-to-context", "ask-ai"]
+ },
+ {
+ "appId": "com.google.Chrome",
+ "context": { "domType": "image" },
+ "actions": ["save-image", "upload-thing"]
+ }
+ ]
+}
+```
+
+### 3. Context & Event Loop
+
+1. **Hook**: `CGEvent.tapCreate` listens for right-mouse-down.
+2. **Filter**: If a modifier key (e.g., `Cmd`) is held (optional config), passthrough to OS. Otherwise, suppress.
+3. **Resolve**:
+ - Get `NSWorkspace.shared.frontmostApplication`.
+ - Use `AXUIElement` to get selected text.
+ - If Browser, send "Ping" to Extension via Native Messaging to get DOM element under cursor.
+
+### 4. Extension Bridge
+
+- **Update** `packages/anyclick-extension` to support Native Messaging.
+- **Protocol**:
+ - `Request: { type: "GET_CONTEXT", x: 100, y: 200 }`
+ - `Response: { element: "img", src: "...", id: "..." }`
+ - `Command: { type: "EXECUTE", action: "remove-element", targetId: "..." }`
+
+## Implementation Steps
+
+### Phase 1: Native Foundation
+
+- Initialize Xcode project in `apps/anyclick-native`.
+- Implement `InputMonitor` class using `CoreGraphics`.
+- Create `PermissionManager` to request/check Accessibility/Input permissions.
+- Implement basic `MenuRenderer` using `NSMenu`.
+
+### Phase 2: Graph & Context
+
+- Define `GraphConfig` structs and load default rules.
+- Implement `ContextResolver` (getting frontmost app bundle ID and selected text).
+- Connect `InputMonitor` -> `ContextResolver` -> `Graph` -> `Menu`.
+
+### Phase 3: Browser Integration
+
+- Add `nativeMessaging` permission to `packages/anyclick-extension/manifest.json`.
+- Implement Native Messaging Host registration script.
+- Create `BrowserBridge` in Swift to handle stdio communication with Chrome.
+- Update Extension content script to answer "Context Requests".
+
+## File Structure
+
+- `apps/anyclick-native/`
+ - `Sources/`
+ - `Core/` (Graph, EventLoop)
+ - `Mac/` (Accessibility, InputTap)
+ - `Bridge/` (NativeMessaging)
+ - `UI/` (Settings, Menu)
+- `packages/anyclick-extension/src/native/` (New bridge logic)
+
+## Key Challenges & Mitigations
+
+- **Speed**: Waiting for Chrome extension response might delay menu.
+ - *Mitigation*: Show menu immediately with "Loading..." or generic actions, update when response arrives (or strict 50ms timeout).
+- **Sandboxing**: App Store distribution is hard with Accessibility/NativeMessaging.
+ - *Mitigation*: Distribute as signed Developer ID app (outside App Store).
+
diff --git a/.cursor/plans/swiftui-anyclick-macos_ff4c2c5e.plan.md b/.cursor/plans/swiftui-anyclick-macos_ff4c2c5e.plan.md
new file mode 100644
index 0000000..5b7a91a
--- /dev/null
+++ b/.cursor/plans/swiftui-anyclick-macos_ff4c2c5e.plan.md
@@ -0,0 +1,83 @@
+---
+name: swiftui-anyclick-macos
+overview: Assess feasibility and outline a staged implementation of a SwiftUI macOS Anyclick menu that is driven by a local graph, integrated app‑specifically with Chrome and Cursor, and reuses existing Anyclick types and context capture patterns.
+todos:
+ - id: define-graph-model
+ content: Define menu graph schema + local persistence + JSON import/export
+ status: pending
+ - id: native-menu
+ content: Build SwiftUI/NSMenu renderer + action dispatch
+ status: pending
+ - id: chrome-bridge
+ content: Send Chrome right-click context to Mac app
+ status: pending
+ - id: cursor-bridge
+ content: Send Cursor right-click context to Mac app
+ status: pending
+ - id: action-adapters
+ content: Implement Anyclick/Cursor/3rd-party action adapters
+ status: pending
+isProject: false
+---
+
+# SwiftUI Anyclick Menu Plan
+
+## Feasibility Notes
+
+- This is feasible with app‑specific integrations. The repo already has menu modeling (`ContextMenuItem`) and extension‑side right‑click interception that can be reused as the semantic model and context capture pattern for a native SwiftUI menu.
+- A global system‑wide menu would require Accessibility/event taps; with your app‑specific choice we can avoid those and integrate per app (Chrome extension + Cursor plugin) while still presenting a native macOS menu.
+
+## Key Existing References (reuse/adapt)
+
+- Menu item modeling: `ContextMenuItem` in `packages/anyclick-react/src/types.ts` and `ExtensionMenuItem` in `packages/anyclick-extension/src/contextMenu.ts`.
+- Right‑click interception in Chrome extension: `packages/anyclick-extension/src/content.ts`.
+- Intent/protocol semantics to map actions: `packages/anyclick-protocol/src/spec.ts`.
+
+## Plan
+
+1. **Define the local menu graph & action schema**
+ - Create a Swift model for a graph that maps `(AppContext, SelectionContext, PageContext)` → ordered menu items + actions.
+ - Use a local‑only store (SwiftData/CoreData or SQLite) plus JSON import/export for easy editing and future sync.
+ - Add action “targets” for: Anyclick web chat/context, Cursor agent, and third‑party APIs.
+2. **Build the SwiftUI macOS app shell + native menu renderer**
+ - Implement a background/status‑bar app that can:
+ - Receive a context event, resolve the menu graph, and render a native `NSMenu` at the cursor location.
+ - Execute action handlers (copy to clipboard, send to Anyclick web endpoints, invoke Cursor local adapter, call 3rd‑party API).
+ - Add a simple UI to edit graph rules and reorder menu items.
+3. **Chrome integration (app‑specific)**
+ - Extend `packages/anyclick-extension` to send a right‑click context payload (selection text, URL, element metadata) to the Mac app.
+ - Use either Native Messaging or a local HTTP bridge to deliver events to the Swift app.
+ - The extension defers menu rendering to the Swift app (native menu), but still uses existing capture logic for context consistency.
+4. **Cursor integration (app‑specific)**
+ - Create a small Cursor extension/plugin that captures selection + editor context on right‑click and forwards it to the Swift app.
+ - Reuse `@ewjdev/anyclick-cursor` / `@ewjdev/anyclick-cursor-local` conventions for action execution where appropriate.
+5. **Action execution + telemetry (local‑only MVP)**
+ - Implement action adapters in the macOS app:
+ - **Anyclick web chat/context**: call configured endpoints with selection + page context.
+ - **Cursor agent**: use local adapter protocol (or CLI) for “add to chat” actions.
+ - **Third‑party**: generic HTTP webhook action.
+ - Capture local logs for debugging; no cloud sync in MVP.
+
+```mermaid
+flowchart LR
+ ChromeExt --> LocalBridge
+ CursorExt --> LocalBridge
+ LocalBridge --> MacApp
+ MacApp --> MenuGraph
+ MenuGraph --> NativeMenu
+ NativeMenu --> ActionAdapters
+ ActionAdapters --> AnyclickWeb
+ ActionAdapters --> CursorAgent
+ ActionAdapters --> ThirdPartyAPI
+```
+
+
+
+## Initial Implementation Todos
+
+- `define-graph-model`: Swift models + local persistence + JSON import/export.
+- `native-menu`: SwiftUI/NSMenu rendering and action dispatch.
+- `chrome-bridge`: Extension event payload + local bridge to Mac app.
+- `cursor-bridge`: Cursor extension + local bridge to Mac app.
+- `action-adapters`: Anyclick web, Cursor agent, third‑party webhooks.
+
diff --git a/.cursor/worktrees.json b/.cursor/worktrees.json
new file mode 100644
index 0000000..50c4373
--- /dev/null
+++ b/.cursor/worktrees.json
@@ -0,0 +1,3 @@
+{
+ "setup-worktree": ["yarn install"]
+}
diff --git a/.gitignore b/.gitignore
index 5224b6d..63cca7c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,3 +37,5 @@ yarn-error.log*
# turbo
.turbo
.vercel
+
+.myh/
\ No newline at end of file
diff --git a/README.md b/README.md
index acd214e..f677b85 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ Right-click any element in your app to capture feedback with full DOM context, s
### Installation
```bash
-yarn install @ewjdev/anyclick-react @ewjdev/anyclick-github
+yarn install @ewjdev/anyclick-core @ewjdev/anyclick-react @ewjdev/anyclick-github
```
### Usage
@@ -54,7 +54,7 @@ export async function POST(request: Request) {
"use client";
import { FeedbackProvider } from "@ewjdev/anyclick-react";
-import { createHttpAdapter } from "@ewjdev/anyclick-github";
+import { createHttpAdapter } from "@ewjdev/anyclick-core";
const adapter = createHttpAdapter({ endpoint: "/api/feedback" });
diff --git a/apps/web/package.json b/apps/web/package.json
index 09887ed..4a58dbc 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -27,6 +27,7 @@
"@ewjdev/anyclick-github": "*",
"@ewjdev/anyclick-jira": "*",
"@ewjdev/anyclick-react": "*",
+ "@ewjdev/anyclick-react-tailwind": "*",
"@ewjdev/anyclick-uploadthing": "*",
"@headlessui/react": "^2.2.8",
"@heroicons/react": "^2.1.3",
diff --git a/apps/web/src/app/api/feedback/github/route.d.ts b/apps/web/src/app/api/feedback/github/route.d.ts
new file mode 100644
index 0000000..e7dfb1f
--- /dev/null
+++ b/apps/web/src/app/api/feedback/github/route.d.ts
@@ -0,0 +1 @@
+export declare function POST(req: Request): Promise;
diff --git a/apps/web/src/app/api/feedback/github/route.js b/apps/web/src/app/api/feedback/github/route.js
new file mode 100644
index 0000000..c9f555e
--- /dev/null
+++ b/apps/web/src/app/api/feedback/github/route.js
@@ -0,0 +1,80 @@
+import { createGitHubAdapter, defaultFormatTitle, } from "@ewjdev/anyclick-github/server";
+function isRecord(value) {
+ return typeof value === "object" && value !== null;
+}
+function parseGitHubRepo(repoValue) {
+ const [owner, repo] = repoValue.split("/");
+ if (!owner || !repo) {
+ throw new Error(`GITHUB_REPO is not in valid format. Expected \"owner/repo\", got \"${repoValue}\"`);
+ }
+ return { owner, repo };
+}
+function readPreferredTitle(metadata) {
+ if (!isRecord(metadata))
+ return undefined;
+ const github = metadata.github;
+ if (!isRecord(github))
+ return undefined;
+ const preferredTitle = github.title;
+ if (typeof preferredTitle !== "string")
+ return undefined;
+ const trimmed = preferredTitle.trim();
+ return trimmed || undefined;
+}
+function withRoutingMetadata(metadata) {
+ const baseMetadata = isRecord(metadata) ? metadata : {};
+ const routing = isRecord(baseMetadata.routing) ? baseMetadata.routing : {};
+ return {
+ ...baseMetadata,
+ routing: {
+ ...routing,
+ adapter: "github",
+ },
+ };
+}
+export async function POST(req) {
+ let payload;
+ try {
+ payload = (await req.json());
+ }
+ catch {
+ return Response.json({
+ success: false,
+ error: "Invalid JSON payload",
+ }, { status: 400 });
+ }
+ const token = process.env.GITHUB_TOKEN;
+ const repoValue = process.env.GITHUB_REPO ?? "ewjdev/anyclick";
+ if (!token) {
+ return Response.json({
+ success: false,
+ error: "GITHUB_TOKEN is not configured. Set GITHUB_TOKEN in your environment.",
+ }, { status: 500 });
+ }
+ try {
+ const { owner, repo } = parseGitHubRepo(repoValue);
+ const githubAdapter = createGitHubAdapter({
+ owner,
+ repo,
+ token,
+ formatTitle: (issuePayload) => readPreferredTitle(issuePayload.metadata) || defaultFormatTitle(issuePayload),
+ });
+ const normalizedPayload = {
+ ...payload,
+ metadata: withRoutingMetadata(payload.metadata),
+ };
+ const issue = await githubAdapter.createIssue(normalizedPayload);
+ return Response.json({
+ success: true,
+ issue,
+ });
+ }
+ catch (error) {
+ const errorMessage = error instanceof Error ? error.message : "Failed to create GitHub issue";
+ return Response.json({
+ success: false,
+ error: errorMessage,
+ }, { status: 500 });
+ }
+}
+//# sourceMappingURL=route.js.map
\ No newline at end of file
diff --git a/apps/web/src/app/api/feedback/github/route.js.map b/apps/web/src/app/api/feedback/github/route.js.map
new file mode 100644
index 0000000..eaf6a5c
--- /dev/null
+++ b/apps/web/src/app/api/feedback/github/route.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"route.js","sourceRoot":"","sources":["route.ts"],"names":[],"mappings":"AACA,OAAO,EACL,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,gCAAgC,CAAC;AAExC,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;AACrD,CAAC;AAED,SAAS,eAAe,CAAC,SAAiB;IACxC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAE3C,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CACb,sEAAsE,SAAS,IAAI,CACpF,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AACzB,CAAC;AAED,SAAS,kBAAkB,CAAC,QAAiB;IAC3C,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,OAAO,SAAS,CAAC;IAC1C,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;IAC/B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,SAAS,CAAC;IAExC,MAAM,cAAc,GAAG,MAAM,CAAC,KAAK,CAAC;IACpC,IAAI,OAAO,cAAc,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAEzD,MAAM,OAAO,GAAG,cAAc,CAAC,IAAI,EAAE,CAAC;IACtC,OAAO,OAAO,IAAI,SAAS,CAAC;AAC9B,CAAC;AAED,SAAS,mBAAmB,CAAC,QAAiB;IAC5C,MAAM,YAAY,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;IACxD,MAAM,OAAO,GAAG,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAE3E,OAAO;QACL,GAAG,YAAY;QACf,OAAO,EAAE;YACP,GAAG,OAAO;YACV,OAAO,EAAE,QAAQ;SAClB;KACF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,GAAY;IACrC,IAAI,OAAwB,CAAC;IAE7B,IAAI,CAAC;QACH,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAoB,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,CAAC,IAAI,CAClB;YACE,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,sBAAsB;SAC9B,EACD,EAAE,MAAM,EAAE,GAAG,EAAE,CAChB,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;IACvC,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,iBAAiB,CAAC;IAE/D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,QAAQ,CAAC,IAAI,CAClB;YACE,OAAO,EAAE,KAAK;YACd,KAAK,EACH,uEAAuE;SAC1E,EACD,EAAE,MAAM,EAAE,GAAG,EAAE,CAChB,CAAC;IACJ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC;QAEnD,MAAM,aAAa,GAAG,mBAAmB,CAAC;YACxC,KAAK;YACL,IAAI;YACJ,KAAK;YACL,WAAW,EAAE,CAAC,YAAY,EAAE,EAAE,CAC5B,kBAAkB,CAAC,YAAY,CAAC,QAAQ,CAAC,IAAI,kBAAkB,CAAC,YAAY,CAAC;SAChF,CAAC,CAAC;QAEH,MAAM,iBAAiB,GAAoB;YACzC,GAAG,OAAO;YACV,QAAQ,EAAE,mBAAmB,CAAC,OAAO,CAAC,QAAQ,CAAC;SAChD,CAAC;QAEF,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,WAAW,CAAC,iBAAiB,CAAC,CAAC;QAEjE,OAAO,QAAQ,CAAC,IAAI,CAAC;YACnB,OAAO,EAAE,IAAI;YACb,KAAK;SACN,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,YAAY,GAChB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,+BAA+B,CAAC;QAE3E,OAAO,QAAQ,CAAC,IAAI,CAClB;YACE,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,YAAY;SACpB,EACD,EAAE,MAAM,EAAE,GAAG,EAAE,CAChB,CAAC;IACJ,CAAC;AACH,CAAC"}
\ No newline at end of file
diff --git a/apps/web/src/app/api/feedback/github/route.ts b/apps/web/src/app/api/feedback/github/route.ts
new file mode 100644
index 0000000..c9b4032
--- /dev/null
+++ b/apps/web/src/app/api/feedback/github/route.ts
@@ -0,0 +1,111 @@
+import type { AnyclickPayload } from "@ewjdev/anyclick-core";
+import {
+ createGitHubAdapter,
+ defaultFormatTitle,
+} from "@ewjdev/anyclick-github/server";
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === "object" && value !== null;
+}
+
+function parseGitHubRepo(repoValue: string): { owner: string; repo: string } {
+ const [owner, repo] = repoValue.split("/");
+
+ if (!owner || !repo) {
+ throw new Error(
+ `GITHUB_REPO is not in valid format. Expected \"owner/repo\", got \"${repoValue}\"`,
+ );
+ }
+
+ return { owner, repo };
+}
+
+function readPreferredTitle(metadata: unknown): string | undefined {
+ if (!isRecord(metadata)) return undefined;
+ const github = metadata.github;
+ if (!isRecord(github)) return undefined;
+
+ const preferredTitle = github.title;
+ if (typeof preferredTitle !== "string") return undefined;
+
+ const trimmed = preferredTitle.trim();
+ return trimmed || undefined;
+}
+
+function withRoutingMetadata(metadata: unknown): Record {
+ const baseMetadata = isRecord(metadata) ? metadata : {};
+ const routing = isRecord(baseMetadata.routing) ? baseMetadata.routing : {};
+
+ return {
+ ...baseMetadata,
+ routing: {
+ ...routing,
+ adapter: "github",
+ },
+ };
+}
+
+export async function POST(req: Request) {
+ let payload: AnyclickPayload;
+
+ try {
+ payload = (await req.json()) as AnyclickPayload;
+ } catch {
+ return Response.json(
+ {
+ success: false,
+ error: "Invalid JSON payload",
+ },
+ { status: 400 },
+ );
+ }
+
+ const token = process.env.GITHUB_TOKEN;
+ const repoValue = process.env.GITHUB_REPO ?? "ewjdev/anyclick";
+
+ if (!token) {
+ return Response.json(
+ {
+ success: false,
+ error:
+ "GITHUB_TOKEN is not configured. Set GITHUB_TOKEN in your environment.",
+ },
+ { status: 500 },
+ );
+ }
+
+ try {
+ const { owner, repo } = parseGitHubRepo(repoValue);
+
+ const githubAdapter = createGitHubAdapter({
+ owner,
+ repo,
+ token,
+ formatTitle: (issuePayload) =>
+ readPreferredTitle(issuePayload.metadata) || defaultFormatTitle(issuePayload),
+ });
+
+ const normalizedPayload: AnyclickPayload = {
+ ...payload,
+ metadata: withRoutingMetadata(payload.metadata),
+ };
+
+ const issue = await githubAdapter.createIssue(normalizedPayload);
+
+ return Response.json({
+ success: true,
+ issue,
+ });
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Failed to create GitHub issue";
+
+ return Response.json(
+ {
+ success: false,
+ error: errorMessage,
+ },
+ { status: 500 },
+ );
+ }
+}
diff --git a/apps/web/src/app/docs/adapters/page.tsx b/apps/web/src/app/docs/adapters/page.tsx
index 01086cf..d139e58 100644
--- a/apps/web/src/app/docs/adapters/page.tsx
+++ b/apps/web/src/app/docs/adapters/page.tsx
@@ -98,16 +98,19 @@ export default function AdaptersDocsPage() {
Installation
- {`npm install @ewjdev/anyclick-github`}
+ {`npm install @ewjdev/anyclick-core @ewjdev/anyclick-github`}
Browser-Side Setup
- Use the HTTP adapter to send feedback to your API endpoint:
+ Use the generic HTTP adapter from @ewjdev/anyclick-core{" "}
+ to send feedback to your API endpoint:
{`'use client';
-import { FeedbackProvider } from '@ewjdev/anyclick-react';
-import { createHttpAdapter } from '@ewjdev/anyclick-github';
+import { AnyclickProvider } from '@ewjdev/anyclick-react';
+import { TailwindAnyclickStyleProvider } from '@ewjdev/anyclick-react-tailwind';
+import '@ewjdev/anyclick-react-tailwind/tailwind.css';
+import { createHttpAdapter } from '@ewjdev/anyclick-core';
const adapter = createHttpAdapter({
endpoint: '/api/feedback',
@@ -119,9 +122,11 @@ const adapter = createHttpAdapter({
export function Providers({ children }) {
return (
-
- {children}
-
+
+
+ {children}
+
+
);
}`}
@@ -379,7 +384,7 @@ export async function POST(request: Request) {
{`npm install @ewjdev/anyclick-cursor`}
Usage
- {`import { FeedbackProvider } from '@ewjdev/anyclick-react';
+ {`import { AnyclickProvider } from '@ewjdev/anyclick-react';
import { createCursorAdapter } from '@ewjdev/anyclick-cursor';
const cursorAdapter = createCursorAdapter({
@@ -399,12 +404,12 @@ const menuItems = [
},
];
-
{children}
- `}
+`}
Format for Cursor Agent
@@ -445,7 +450,7 @@ const agentPrompt = formatForCursorAgent(payload);
npm run feedback-server`}
Browser-Side Setup
- {`import { FeedbackProvider } from '@ewjdev/anyclick-react';
+ {`import { AnyclickProvider } from '@ewjdev/anyclick-react';
import { createLocalAdapter } from '@ewjdev/anyclick-cursor-local';
// Only use in development
@@ -459,9 +464,9 @@ const adapter = process.env.NODE_ENV === 'development'
? localAdapter
: productionAdapter;
-
+
{children}
- `}
+`}
Server Configuration
diff --git a/apps/web/src/app/docs/getting-started/page.tsx b/apps/web/src/app/docs/getting-started/page.tsx
index ec539e9..6fa673b 100644
--- a/apps/web/src/app/docs/getting-started/page.tsx
+++ b/apps/web/src/app/docs/getting-started/page.tsx
@@ -46,7 +46,7 @@ export default function GettingStartedPage() {
Install the core packages you need. For a typical React + GitHub
setup:
-
+
@@ -56,15 +56,21 @@ export default function GettingStartedPage() {
@ewjdev/anyclick-react –
- React provider and UI components
+ Headless React provider and UI behavior
- @ewjdev/anyclick-github –
- GitHub Issues adapter
+
+ @ewjdev/anyclick-react-tailwind
+ {" "}
+ – Default Tailwind style adapter and provider
@ewjdev/anyclick-core –
- Automatically included as a dependency
+ Core types and the browser HTTP adapter
+
+
+ @ewjdev/anyclick-github –
+ GitHub Issues adapter
@@ -140,15 +146,18 @@ export async function POST(request: Request) {
Step 4: Add the Provider
Wrap your application with the{" "}
- FeedbackProvider:
+ AnyclickProvider and a style
+ provider:
- {children}
-
+
+
+ {children}
+
+
);
}`}
/>
@@ -213,7 +224,7 @@ export default function RootLayout({
Configuration Options
- The FeedbackProvider accepts
+ The AnyclickProvider accepts
several configuration options:
diff --git a/apps/web/src/app/docs/react/page.tsx b/apps/web/src/app/docs/react/page.tsx
index ecaf347..cea50f5 100644
--- a/apps/web/src/app/docs/react/page.tsx
+++ b/apps/web/src/app/docs/react/page.tsx
@@ -61,11 +61,14 @@ export default function ReactDocsPage() {
{/* Installation */}
Installation
- {`npm install @ewjdev/anyclick-react`}
+ {`npm install @ewjdev/anyclick-react @ewjdev/anyclick-react-tailwind`}
Requires React 19+ and includes{" "}
@ewjdev/anyclick-core as a
- dependency.
+ dependency. The React package is headless by default, so pair it with
+ a style package such as{" "}
+ @ewjdev/anyclick-react-tailwind
+ .
@@ -75,7 +78,9 @@ export default function ReactDocsPage() {
{`'use client';
import { AnyclickProvider } from '@ewjdev/anyclick-react';
-import { createHttpAdapter } from '@ewjdev/anyclick-github';
+import { TailwindAnyclickStyleProvider } from '@ewjdev/anyclick-react-tailwind';
+import '@ewjdev/anyclick-react-tailwind/tailwind.css';
+import { createHttpAdapter } from '@ewjdev/anyclick-core';
const adapter = createHttpAdapter({
endpoint: '/api/feedback',
@@ -83,14 +88,17 @@ const adapter = createHttpAdapter({
export function Providers({ children }: { children: React.ReactNode }) {
return (
-
- {children}
-
+
+
+ {children}
+
+
);
}`}
- Note: {" "}
- FeedbackProvider is still
+ Note: use a companion style provider or your own{" "}
+ AnyclickStyleProvider.
+ AnyclickProvider is still
exported for backward compatibility but is deprecated.
@@ -188,15 +196,35 @@ export function Providers({ children }: { children: React.ReactNode }) {
type="(error: Error, payload: FeedbackPayload) => void"
description="Callback fired after failed submission."
/>
+
+
+
+
- {children}
-
+
+
+ {children}
+
+
);
}`}
/>
diff --git a/apps/web/src/app/examples/cursor-local/page.tsx b/apps/web/src/app/examples/cursor-local/page.tsx
index dda16dc..7a897e0 100644
--- a/apps/web/src/app/examples/cursor-local/page.tsx
+++ b/apps/web/src/app/examples/cursor-local/page.tsx
@@ -109,9 +109,9 @@ export default function CursorLocalPage() {
{`'use client';
-import { FeedbackProvider, filterMenuItemsByRole } from '@ewjdev/anyclick-react';
+import { AnyclickProvider, filterMenuItemsByRole } from '@ewjdev/anyclick-react';
import type { FeedbackMenuItem } from '@ewjdev/anyclick-react';
-import { createHttpAdapter } from '@ewjdev/anyclick-github';
+import { createHttpAdapter } from '@ewjdev/anyclick-core';
import { createLocalAdapter } from '@ewjdev/anyclick-cursor-local';
import { Monitor, Cloud, Bug, Lightbulb } from 'lucide-react';
@@ -157,12 +157,12 @@ const menuItems: FeedbackMenuItem[] = [
export function Providers({ children }: { children: React.ReactNode }) {
return (
-
{children}
-
+
);
}`}
@@ -200,9 +200,9 @@ const adapter = createRoutingAdapter({
github: githubAdapter,
});
-
+
{children}
- `}
+`}
{/* Server Configuration */}
diff --git a/apps/web/src/app/examples/custom-menu/page.tsx b/apps/web/src/app/examples/custom-menu/page.tsx
index 8d58ac7..a698c84 100644
--- a/apps/web/src/app/examples/custom-menu/page.tsx
+++ b/apps/web/src/app/examples/custom-menu/page.tsx
@@ -82,7 +82,7 @@ export default function CustomMenuExamplePage() {
icon prop. Use any React
component (Lucide, Heroicons, custom SVGs, etc.):
- {`import { FeedbackProvider } from '@ewjdev/anyclick-react';
+ {`import { AnyclickProvider } from '@ewjdev/anyclick-react';
import { Bug, Lightbulb, Heart } from 'lucide-react';
const menuItems = [
@@ -106,9 +106,9 @@ const menuItems = [
},
];
-
+
{children}
- `}
+`}
{/* Submenus */}
@@ -154,7 +154,7 @@ const menuItems = [
Show or hide menu items based on user roles using{" "}
requiredRoles:
- {`import { FeedbackProvider, filterMenuItemsByRole } from '@ewjdev/anyclick-react';
+ {`import { AnyclickProvider, filterMenuItemsByRole } from '@ewjdev/anyclick-react';
import type { FeedbackMenuItem, FeedbackUserContext } from '@ewjdev/anyclick-react';
// Define all menu items with role requirements
@@ -194,13 +194,13 @@ function Providers({ children, currentUser }) {
const menuItems = filterMenuItemsByRole(allMenuItems, userContext);
return (
-
{children}
-
+
);
}`}
@@ -211,7 +211,7 @@ function Providers({ children, currentUser }) {
Apply custom styles or classes to the context menu:
- {`{`
{children}
- `}
+`}
{`/* Custom menu styling */
.my-custom-menu {
@@ -247,7 +247,7 @@ function Providers({ children, currentUser }) {
Customize the colors used to highlight target and container elements:
- {`{`
{children}
- `}
+`}
{/* Complete Example */}
@@ -279,7 +279,7 @@ function Providers({ children, currentUser }) {
Complete Example
{`'use client';
-import { FeedbackProvider, filterMenuItemsByRole } from '@ewjdev/anyclick-react';
+import { AnyclickProvider, filterMenuItemsByRole } from '@ewjdev/anyclick-react';
import type { FeedbackMenuItem, FeedbackUserContext } from '@ewjdev/anyclick-react';
import { createHttpAdapter } from '@ewjdev/anyclick-';
import { Bug, Lightbulb, Heart, Code, Monitor, Cloud, Shield } from 'lucide-react';
@@ -347,7 +347,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
const menuItems = filterMenuItemsByRole(allMenuItems, userContext);
return (
-
{children}
-
+
);
}`}
diff --git a/apps/web/src/app/examples/custom-pointer/page.tsx b/apps/web/src/app/examples/custom-pointer/page.tsx
index 2faa878..320513a 100644
--- a/apps/web/src/app/examples/custom-pointer/page.tsx
+++ b/apps/web/src/app/examples/custom-pointer/page.tsx
@@ -562,25 +562,25 @@ function ThemeSwitcher() {
}`}
- {/* Integration with FeedbackProvider */}
+ {/* Integration with AnyclickProvider */}
- Integration with FeedbackProvider
+ Integration with AnyclickProvider
Combine with the feedback system for a complete experience:
{`'use client';
-import { FeedbackProvider } from '@ewjdev/anyclick-react';
+import { AnyclickProvider } from '@ewjdev/anyclick-react';
import { PointerProvider } from '@ewjdev/anyclick-pointer';
-import { createHttpAdapter } from '@ewjdev/anyclick-github';
+import { createHttpAdapter } from '@ewjdev/anyclick-core';
const adapter = createHttpAdapter({ endpoint: '/api/feedback' });
export function Providers({ children }: { children: React.ReactNode }) {
return (
-
+
{children}
-
+
);
}`}
diff --git a/apps/web/src/app/examples/github-integration/page.tsx b/apps/web/src/app/examples/github-integration/page.tsx
index da63c5b..1075682 100644
--- a/apps/web/src/app/examples/github-integration/page.tsx
+++ b/apps/web/src/app/examples/github-integration/page.tsx
@@ -98,7 +98,7 @@ GITHUB_REPO=your-username/your-repository-name`}
{`'use client';
-import { FeedbackProvider } from '@ewjdev/anyclick-react';
+import { AnyclickProvider } from '@ewjdev/anyclick-react';
import { createHttpAdapter } from '@ewjdev/anyclick-';
const adapter = createHttpAdapter({
@@ -107,7 +107,7 @@ const adapter = createHttpAdapter({
export function Providers({ children }: { children: React.ReactNode }) {
return (
-
{children}
-
+
);
}`}
diff --git a/apps/web/src/app/examples/jira-integration/page.tsx b/apps/web/src/app/examples/jira-integration/page.tsx
index 5fd5474..a2ec148 100644
--- a/apps/web/src/app/examples/jira-integration/page.tsx
+++ b/apps/web/src/app/examples/jira-integration/page.tsx
@@ -2,7 +2,7 @@
import { CodeBlock } from "@/components/CodePreview";
import { useEffect, useState } from "react";
-import { createHttpAdapter } from "@ewjdev/anyclick-github";
+import { createHttpAdapter } from "@ewjdev/anyclick-core";
import { AnyclickProvider, type ContextMenuItem } from "@ewjdev/anyclick-react";
import {
AlertCircle,
diff --git a/apps/web/src/app/examples/modes/page.tsx b/apps/web/src/app/examples/modes/page.tsx
index 60d83a5..92d477d 100644
--- a/apps/web/src/app/examples/modes/page.tsx
+++ b/apps/web/src/app/examples/modes/page.tsx
@@ -3,7 +3,7 @@
import { CodeBlock } from "@/components/CodePreview";
import { useEffect, useMemo, useRef, useState } from "react";
import { createGameModeAdapter } from "@ewjdev/anyclick-adapters";
-import { createHttpAdapter } from "@ewjdev/anyclick-github";
+import { createHttpAdapter } from "@ewjdev/anyclick-core";
import { usePointer } from "@ewjdev/anyclick-pointer";
import { AnyclickProvider, type ContextMenuItem } from "@ewjdev/anyclick-react";
import { ArrowRight, Car, Keyboard, Sparkles } from "lucide-react";
@@ -238,7 +238,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
{`import { useEffect, useMemo, useRef, useState } from 'react';
import { AnyclickProvider } from '@ewjdev/anyclick-react';
-import { createHttpAdapter } from '@ewjdev/anyclick-github';
+import { createHttpAdapter } from '@ewjdev/anyclick-core';
import { createGameModeAdapter } from '@ewjdev/anyclick-adapters';
import { usePointer } from '@ewjdev/anyclick-pointer';
diff --git a/apps/web/src/app/examples/quick-chat/QuickChatProvider.tsx b/apps/web/src/app/examples/quick-chat/QuickChatProvider.tsx
index 21a9de7..43feba9 100644
--- a/apps/web/src/app/examples/quick-chat/QuickChatProvider.tsx
+++ b/apps/web/src/app/examples/quick-chat/QuickChatProvider.tsx
@@ -1,5 +1,5 @@
"use client";
-import { createHttpAdapter } from "@ewjdev/anyclick-github";
+import { createHttpAdapter } from "@ewjdev/anyclick-core";
import {
AnyclickProvider,
DEFAULT_QUICK_CHAT_CONFIG,
diff --git a/apps/web/src/app/examples/quick-chat/page.tsx b/apps/web/src/app/examples/quick-chat/page.tsx
index b6b2f3b..453b2f5 100644
--- a/apps/web/src/app/examples/quick-chat/page.tsx
+++ b/apps/web/src/app/examples/quick-chat/page.tsx
@@ -183,7 +183,7 @@ export default function QuickChatExamplePage() {
code={`'use client';
import { AnyclickProvider } from '@ewjdev/anyclick-react';
-import { createHttpAdapter } from '@ewjdev/anyclick-github';
+import { createHttpAdapter } from '@ewjdev/anyclick-core';
const adapter = createHttpAdapter({
endpoint: '/api/feedback',
diff --git a/apps/web/src/app/examples/role-presets/RolePresetsClient.tsx b/apps/web/src/app/examples/role-presets/RolePresetsClient.tsx
index 8dc7dc3..1aaa69d 100644
--- a/apps/web/src/app/examples/role-presets/RolePresetsClient.tsx
+++ b/apps/web/src/app/examples/role-presets/RolePresetsClient.tsx
@@ -23,7 +23,7 @@ import Link from "next/link";
const presetSnippet = `'use client';
import { AnyclickProvider, createPresetMenu } from '@ewjdev/anyclick-react';
-import { createHttpAdapter } from '@ewjdev/anyclick-github';
+import { createHttpAdapter } from '@ewjdev/anyclick-core';
const adapter = createHttpAdapter({ endpoint: '/api/feedback' });
diff --git a/apps/web/src/app/examples/scoped-providers/ScopedProvidersDemo.tsx b/apps/web/src/app/examples/scoped-providers/ScopedProvidersDemo.tsx
index 6bca216..f632528 100644
--- a/apps/web/src/app/examples/scoped-providers/ScopedProvidersDemo.tsx
+++ b/apps/web/src/app/examples/scoped-providers/ScopedProvidersDemo.tsx
@@ -3,7 +3,7 @@
import type { CSSProperties } from "react";
import { AnyclickProvider } from "@ewjdev/anyclick-react";
import { PointerProvider } from "@ewjdev/anyclick-pointer";
-import { createHttpAdapter } from "@ewjdev/anyclick-github";
+import { createHttpAdapter } from "@ewjdev/anyclick-core";
import {
Layers,
Palette,
diff --git a/apps/web/src/app/examples/scoped-providers/page.tsx b/apps/web/src/app/examples/scoped-providers/page.tsx
index c6bd5c0..a506e0d 100644
--- a/apps/web/src/app/examples/scoped-providers/page.tsx
+++ b/apps/web/src/app/examples/scoped-providers/page.tsx
@@ -79,16 +79,20 @@ export default function ScopedProvidersExamplePage() {
{`'use client';
import { AnyclickProvider } from '@ewjdev/anyclick-react';
+import { TailwindAnyclickStyleProvider } from '@ewjdev/anyclick-react-tailwind';
+import '@ewjdev/anyclick-react-tailwind/tailwind.css';
export function Dashboard({ children }) {
return (
-
- {children}
-
+
+
+ {children}
+
+
);
}`}
@@ -218,7 +222,7 @@ export function PaymentForm({ children }) {
{`'use client';
import { AnyclickProvider } from '@ewjdev/anyclick-react';
-import { createHttpAdapter } from '@ewjdev/anyclick-github';
+import { createHttpAdapter } from '@ewjdev/anyclick-core';
// Main app feedback goes to GitHub
const githubAdapter = createHttpAdapter({
diff --git a/apps/web/src/app/examples/sensitive-masking/page.tsx b/apps/web/src/app/examples/sensitive-masking/page.tsx
index 704a624..7697e4a 100644
--- a/apps/web/src/app/examples/sensitive-masking/page.tsx
+++ b/apps/web/src/app/examples/sensitive-masking/page.tsx
@@ -245,8 +245,8 @@ ${DEFAULT_SENSITIVE_SELECTORS.map((s) => `'${s}'`).join(",\n")}`}
{`'use client';
-import { FeedbackProvider } from '@ewjdev/anyclick-react';
-import { createHttpAdapter } from '@ewjdev/anyclick-github';
+import { AnyclickProvider } from '@ewjdev/anyclick-react';
+import { createHttpAdapter } from '@ewjdev/anyclick-core';
import { DEFAULT_SENSITIVE_SELECTORS } from '@ewjdev/anyclick-core';
const adapter = createHttpAdapter({
@@ -255,7 +255,7 @@ const adapter = createHttpAdapter({
export function Providers({ children }: { children: React.ReactNode }) {
return (
-
{children}
-
+
);
}`}
@@ -275,8 +275,8 @@ export function Providers({ children }: { children: React.ReactNode }) {
{`'use client';
-import { FeedbackProvider } from '@ewjdev/anyclick-react';
-import { createHttpAdapter } from '@ewjdev/anyclick-github';
+import { AnyclickProvider } from '@ewjdev/anyclick-react';
+import { createHttpAdapter } from '@ewjdev/anyclick-core';
import { DEFAULT_SENSITIVE_SELECTORS } from '@ewjdev/anyclick-core';
const adapter = createHttpAdapter({
@@ -285,7 +285,7 @@ const adapter = createHttpAdapter({
export function Providers({ children }: { children: React.ReactNode }) {
return (
-
{children}
-
+
);
}`}
diff --git a/apps/web/src/app/examples/t3chat-integration/T3ChatProvider.tsx b/apps/web/src/app/examples/t3chat-integration/T3ChatProvider.tsx
index 145fc4b..d8d4dfd 100644
--- a/apps/web/src/app/examples/t3chat-integration/T3ChatProvider.tsx
+++ b/apps/web/src/app/examples/t3chat-integration/T3ChatProvider.tsx
@@ -1,7 +1,7 @@
"use client";
import type { ReactNode } from "react";
-import { createHttpAdapter } from "@ewjdev/anyclick-github";
+import { createHttpAdapter } from "@ewjdev/anyclick-core";
import { AnyclickProvider, createPresetMenu } from "@ewjdev/anyclick-react";
const chromePreset = createPresetMenu("chrome");
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index 89b83f0..a69723c 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -3,6 +3,7 @@ import { cn } from "@/lib/utils";
import type { ReactNode } from "react";
import type { Metadata, Viewport } from "next";
import { Inter, Lato, Montserrat } from "next/font/google";
+import "@ewjdev/anyclick-react-tailwind/tailwind.css";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index 1e5ed92..adc55f0 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -1,14 +1,34 @@
+ "use client";
+
import { AnyclickLogo } from "@/components/AnyclickLogo";
+import { AnimatedWord } from "@/components/AnimatedWord";
import { HeroCodeBlock } from "@/components/CodePreview";
import FeaturesSection from "@/components/FeaturesSection";
import { ImmersiveWorkstreamShowcase } from "@/components/ImmersiveWorkstreamShowcase";
import PackagesSection from "@/components/PackagesSection";
import QuickStartSection from "@/components/QuickStartSection";
import RoadmapSummary from "@/components/RoadmapSummary";
+import { useFirstTimeInteraction } from "@/lib/hooks/useFirstTimeInteraction";
import { ArrowRight } from "lucide-react";
+import { motion } from "motion/react";
import Link from "next/link";
+import { useEffect, useState } from "react";
export default function Home() {
+ const { hasInteracted, markComplete } = useFirstTimeInteraction();
+ const [showButton, setShowButton] = useState(hasInteracted);
+
+ useEffect(() => {
+ if (hasInteracted) {
+ setShowButton(true);
+ }
+ }, [hasInteracted]);
+
+ const handleCycleComplete = () => {
+ markComplete();
+ setShowButton(true);
+ };
+
return (
{/* Animated gradient background */}
@@ -79,9 +99,19 @@ export default function Home() {
UX done right
-
- Why not click on everything?
-
+
+ Why not click on
+
+ ?
+
{/* Subheadline */}
@@ -92,7 +122,15 @@ export default function Home() {
{/* CTA Buttons */}
-
+
-
+
{/* Code Preview */}
@@ -109,7 +147,9 @@ export default function Home() {
filename="app/layout.tsx"
language="tsx"
code={`import { AnyclickProvider } from '@ewjdev/anyclick-react';
-import { createHttpAdapter } from '@ewjdev/anyclick-github';
+import { TailwindAnyclickStyleProvider } from '@ewjdev/anyclick-react-tailwind';
+import '@ewjdev/anyclick-react-tailwind/tailwind.css';
+import { createHttpAdapter } from '@ewjdev/anyclick-core';
const adapter = createHttpAdapter({
endpoint: '/api/feedback'
@@ -117,9 +157,11 @@ const adapter = createHttpAdapter({
export default function RootLayout({ children }) {
return (
-
- {children}
-
+
+
+ {children}
+
+
);
}`}
/>
diff --git a/apps/web/src/components/AnimatedWord.tsx b/apps/web/src/components/AnimatedWord.tsx
new file mode 100644
index 0000000..4ac3ff1
--- /dev/null
+++ b/apps/web/src/components/AnimatedWord.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+/**
+ * AnimatedWord - Component that cycles through words on click with smooth layout animations.
+ *
+ * @module components/AnimatedWord
+ */
+import { useState } from "react";
+import { AnimatePresence, motion } from "motion/react";
+
+interface AnimatedWordProps {
+ words: string[];
+ onCycleComplete?: () => void;
+ className?: string;
+}
+
+/**
+ * AnimatedWord component that cycles through an array of words on click.
+ * When cycling back to the first word (index 0), calls onCycleComplete callback.
+ *
+ * @param words - Array of words to cycle through
+ * @param onCycleComplete - Callback fired when cycling back to the first word
+ * @param className - Additional CSS classes
+ */
+export function AnimatedWord({
+ words,
+ onCycleComplete,
+ className = "",
+}: AnimatedWordProps) {
+ const [currentIndex, setCurrentIndex] = useState(0);
+ const [hasStarted, setHasStarted] = useState(false);
+
+ const handleClick = () => {
+ if (!hasStarted) {
+ // First click: animate out "everything" and show first word
+ setHasStarted(true);
+ setCurrentIndex(0);
+ } else {
+ // Subsequent clicks: cycle through words
+ const nextIndex = (currentIndex + 1) % words.length;
+ setCurrentIndex(nextIndex);
+
+ // If we've cycled back to index 0, trigger completion callback
+ if (nextIndex === 0 && onCycleComplete) {
+ onCycleComplete();
+ }
+ }
+ };
+
+ const currentWord = hasStarted ? words[currentIndex] : "everything";
+
+ return (
+ {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ handleClick();
+ }
+ }}
+ aria-label="Click to cycle through words"
+ >
+
+
+ {currentWord}
+
+
+
+ );
+}
diff --git a/apps/web/src/components/AnyclickProviderWrapper.tsx b/apps/web/src/components/AnyclickProviderWrapper.tsx
index 502acb8..f838d52 100644
--- a/apps/web/src/components/AnyclickProviderWrapper.tsx
+++ b/apps/web/src/components/AnyclickProviderWrapper.tsx
@@ -3,8 +3,9 @@
import type { ReactNode } from "react";
import { useEffect, useMemo } from "react";
import { InspectDialogManager } from "@ewjdev/anyclick-devtools";
-import { createHttpAdapter } from "@ewjdev/anyclick-github";
+import { createHttpAdapter } from "@ewjdev/anyclick-core";
import { AnyclickProvider, createPresetMenu } from "@ewjdev/anyclick-react";
+import { TailwindAnyclickStyleProvider } from "@ewjdev/anyclick-react-tailwind";
const adapter = createHttpAdapter({
endpoint: "/api/feedback",
@@ -14,8 +15,6 @@ export function AnyclickProviderWrapper({ children }: { children: ReactNode }) {
// Use chrome preset for developer-focused menu with inspect, copy, etc.
const chromePreset = useMemo(() => createPresetMenu("chrome"), []);
- console.count("AnyclickProviderWrapper");
-
// Clean up any stale cursor hiding styles when PointerProvider is not used
// This ensures the default cursor is restored if PointerProvider was removed
useEffect(() => {
@@ -47,34 +46,35 @@ export function AnyclickProviderWrapper({ children }: { children: ReactNode }) {
// useHideCursor(true);
return (
- >}
- quickChatConfig={{
- endpoint: "/api/anyclick/chat",
- model: "gpt-5-nano",
- maxResponseLength: 500,
- showRedactionUI: true,
- showSuggestions: true,
- placeholder: "Ask about this element...",
- title: "Quick Ask",
- }}
- theme={{
- highlightConfig: {
- enabled: false,
- },
- }}
- >
- {children}
-
+ >}
+ quickChatConfig={{
+ endpoint: "/api/anyclick/chat",
+ model: "gpt-5-nano",
+ maxResponseLength: 500,
+ showRedactionUI: true,
+ showSuggestions: true,
+ placeholder: "Ask about this element...",
+ title: "Quick Ask",
}}
- />
- {/*
+ {children}
+
+ {/*
-
+
*/}
-
+
+
);
}
diff --git a/apps/web/src/components/CodePreview.tsx b/apps/web/src/components/CodePreview.tsx
index f3523fc..8150863 100644
--- a/apps/web/src/components/CodePreview.tsx
+++ b/apps/web/src/components/CodePreview.tsx
@@ -3,7 +3,7 @@
import { cn } from "@/lib/utils";
import * as React from "react";
import { CSSProperties, useMemo } from "react";
-import { createHttpAdapter } from "@ewjdev/anyclick-github";
+import { createHttpAdapter } from "@ewjdev/anyclick-core";
import { AnyclickProvider, type ContextMenuItem } from "@ewjdev/anyclick-react";
import { Check, Copy, FileCode, Terminal } from "lucide-react";
diff --git a/apps/web/src/components/QuickStartSection.tsx b/apps/web/src/components/QuickStartSection.tsx
index 95a1fde..9b2b855 100644
--- a/apps/web/src/components/QuickStartSection.tsx
+++ b/apps/web/src/components/QuickStartSection.tsx
@@ -19,7 +19,7 @@ const QuickStartSection = () => {
Install the packages
@@ -63,15 +63,19 @@ export async function POST(req) {
filename="app/layout.tsx"
language="tsx"
code={`import { AnyclickProvider } from '@ewjdev/anyclick-react';
-import { createHttpAdapter } from '@ewjdev/anyclick-github';
+import { TailwindAnyclickStyleProvider } from '@ewjdev/anyclick-react-tailwind';
+import '@ewjdev/anyclick-react-tailwind/tailwind.css';
+import { createHttpAdapter } from '@ewjdev/anyclick-core';
const adapter = createHttpAdapter({ endpoint: '/api/feedback' });
export default function Layout({ children }) {
return (
-
- {children}
-
+
+
+ {children}
+
+
);
}`}
/>
diff --git a/apps/web/src/components/WorkstreamShowcase.tsx b/apps/web/src/components/WorkstreamShowcase.tsx
index dbb4a52..38928ba 100644
--- a/apps/web/src/components/WorkstreamShowcase.tsx
+++ b/apps/web/src/components/WorkstreamShowcase.tsx
@@ -3,7 +3,7 @@
import { cn } from "@/lib/utils";
import type { CSSProperties, ReactNode } from "react";
import { useRef } from "react";
-import { createHttpAdapter } from "@ewjdev/anyclick-github";
+import { createHttpAdapter } from "@ewjdev/anyclick-core";
import { PointerProvider } from "@ewjdev/anyclick-pointer";
import { AnyclickProvider, type ContextMenuItem } from "@ewjdev/anyclick-react";
import {
diff --git a/apps/web/src/components/immersive-workstream/adapter.ts b/apps/web/src/components/immersive-workstream/adapter.ts
index 4437180..c37a89c 100644
--- a/apps/web/src/components/immersive-workstream/adapter.ts
+++ b/apps/web/src/components/immersive-workstream/adapter.ts
@@ -1,3 +1,3 @@
-import { createHttpAdapter } from "@ewjdev/anyclick-github";
+import { createHttpAdapter } from "@ewjdev/anyclick-core";
export const adapter = createHttpAdapter({ endpoint: "/api/feedback" });
diff --git a/apps/web/src/components/immersive-workstream/cards/EcommerceCard.tsx b/apps/web/src/components/immersive-workstream/cards/EcommerceCard.tsx
index 1f5df40..ad56a5b 100644
--- a/apps/web/src/components/immersive-workstream/cards/EcommerceCard.tsx
+++ b/apps/web/src/components/immersive-workstream/cards/EcommerceCard.tsx
@@ -1,72 +1,320 @@
-export function EcommerceCard() {
+import type { CSSProperties } from "react";
+import { PointerProvider } from "@ewjdev/anyclick-pointer";
+import { AnyclickProvider, type ContextMenuItem } from "@ewjdev/anyclick-react";
+import { adapter } from "../adapter";
+
+interface EcommerceCardProps {
+ menuStyle: CSSProperties;
+ orderMenuItems: ContextMenuItem[];
+ pointerCircleColor: string;
+ pointerColor: string;
+ pointerIcon: React.ReactNode;
+ pricingMenuItems: ContextMenuItem[];
+ primaryColor: string;
+ productMenuItems: ContextMenuItem[];
+ stockMenuItems: ContextMenuItem[];
+}
+
+interface ExampleOrder {
+ customer: string;
+ discount: string;
+ fulfillment: string;
+ id: string;
+ originalPrice: string;
+ price: string;
+ productDescription: string;
+ productName: string;
+ shippingStatus: string;
+ stockLabel: string;
+}
+
+const EXAMPLE_ORDERS: ExampleOrder[] = [
+ {
+ id: "ORD-20481",
+ customer: "Alex P.",
+ productName: "Trail Running Jacket",
+ productDescription: "Weatherproof shell, matte black, size M",
+ price: "$49.99",
+ originalPrice: "$59.99",
+ discount: "17% off",
+ stockLabel: "In Stock",
+ fulfillment: "Packed in WH-West",
+ shippingStatus: "Label printed",
+ },
+ {
+ id: "ORD-20482",
+ customer: "Jordan R.",
+ productName: "Performance Cargo Pack",
+ productDescription: "34L day pack, ember orange",
+ price: "$89.00",
+ originalPrice: "$89.00",
+ discount: "No active discount",
+ stockLabel: "Low Stock",
+ fulfillment: "Pick in progress",
+ shippingStatus: "Carrier pending",
+ },
+ {
+ id: "ORD-20483",
+ customer: "Casey L.",
+ productName: "Hydro Grip Bottle",
+ productDescription: "24oz steel bottle, glacier blue",
+ price: "$28.50",
+ originalPrice: "$35.00",
+ discount: "Promo bundle",
+ stockLabel: "Backorder",
+ fulfillment: "Supplier transfer",
+ shippingStatus: "ETA Feb 23",
+ },
+];
+
+function stockToneClass(stockLabel: string): string {
+ if (stockLabel === "In Stock") {
+ return "border-green-400/40 bg-green-500/15 text-green-300";
+ }
+
+ if (stockLabel === "Low Stock") {
+ return "border-amber-400/40 bg-amber-500/15 text-amber-200";
+ }
+
+ return "border-rose-400/40 bg-rose-500/15 text-rose-200";
+}
+
+export function EcommerceCard({
+ menuStyle,
+ orderMenuItems,
+ pointerCircleColor,
+ pointerColor,
+ pointerIcon,
+ pricingMenuItems,
+ primaryColor,
+ productMenuItems,
+ stockMenuItems,
+}: EcommerceCardProps) {
+ const providerTheme = {
+ menuStyle,
+ highlightConfig: {
+ enabled: true,
+ colors: {
+ targetColor: primaryColor,
+ containerColor: `${primaryColor}40`,
+ },
+ },
+ };
+
return (
-
+
-
-
-
-
-
-
-
- $49.99
-
-
- In Stock
-
-
+
+
+
+ Live Order Board
+
+
+ Product, price, and stock workflows
+
+
+
+ 3 active orders
-
-
-
+
+ {EXAMPLE_ORDERS.map((order) => (
-
-
-
-
-
-
+ key={order.id}
+ className="rounded-xl border border-amber-200/15 bg-black/25 p-3"
+ data-order-id={order.id}
+ >
+
+
>}
+ scoped
+ >
+
+
+
+
+
+ {order.productName}
+
+
+ {order.productDescription}
+
+
+ Right-click to edit details or upload media
+
+
+
+
+
+
+
+
>}
+ scoped
+ >
+
+
+
+ {order.price}
+
+
+ {order.originalPrice}
+
+
+ {order.discount}
+
+
+
+
+
+
>}
+ scoped
+ >
+
+
+
+ {order.stockLabel}
+
+
+
+
+
+
+
+
>}
+ scoped
+ >
+
+
+
+
+
+
Customer
+
{order.customer}
+
+
+
Status
+
{order.fulfillment}
+
+
+
+
+
+
+ {order.shippingStatus}
+
+
+
+
+
+ ))}
diff --git a/apps/web/src/components/immersive-workstream/cards/HealthcareCard.tsx b/apps/web/src/components/immersive-workstream/cards/HealthcareCard.tsx
index a902347..fd8fbe1 100644
--- a/apps/web/src/components/immersive-workstream/cards/HealthcareCard.tsx
+++ b/apps/web/src/components/immersive-workstream/cards/HealthcareCard.tsx
@@ -1,9 +1,76 @@
-import { cn } from "@/lib/utils";
+import type { CSSProperties, ReactNode } from "react";
+import { PointerProvider } from "@ewjdev/anyclick-pointer";
+import { AnyclickProvider, type ContextMenuItem } from "@ewjdev/anyclick-react";
import { HeartPulse } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { adapter } from "../adapter";
+
+interface HealthcareCardProps {
+ menuStyle: CSSProperties;
+ pointerCircleColor: string;
+ pointerColor: string;
+ pointerIcon: ReactNode;
+ primaryColor: string;
+ statusMenuItems: ContextMenuItem[];
+ summaryMenuItems: ContextMenuItem[];
+ vitalsMenuItems: ContextMenuItem[];
+}
+
+const VITALS = [
+ {
+ bg: "bg-rose-50",
+ color: "text-rose-500",
+ label: "HR",
+ unit: "bpm",
+ value: "72",
+ },
+ {
+ bg: "bg-emerald-50",
+ color: "text-emerald-600",
+ label: "BP",
+ unit: "",
+ value: "120/80",
+ },
+ {
+ bg: "bg-cyan-50",
+ color: "text-cyan-600",
+ label: "Temp",
+ unit: "deg F",
+ value: "98.6",
+ },
+];
+
+export function HealthcareCard({
+ menuStyle,
+ pointerCircleColor,
+ pointerColor,
+ pointerIcon,
+ primaryColor,
+ statusMenuItems,
+ summaryMenuItems,
+ vitalsMenuItems,
+}: HealthcareCardProps) {
+ const providerTheme = {
+ menuStyle,
+ highlightConfig: {
+ enabled: true,
+ colors: {
+ targetColor: primaryColor,
+ containerColor: `${primaryColor}30`,
+ },
+ },
+ };
+
+ const pointerTheme = {
+ colors: {
+ pointerColor,
+ circleColor: pointerCircleColor,
+ },
+ pointerIcon,
+ };
-export function HealthcareCard() {
return (
-
+
-
+
>}
+ scoped
+ >
+
+
+
+
+
+ Patient Summary
+
+
+ Token PT-80321
+
+
+ Bed B-12 | Intake queue routed
+
+
+
+
+ Active
+
+
+
+
+ Right-click to run check-in and identity workflows.
+
+
+
+
-
- {[
- {
- label: "HR",
- value: "72",
- unit: "bpm",
- color: "text-rose-500",
- bg: "bg-rose-50",
- },
- {
- label: "BP",
- value: "120/80",
- unit: "",
- color: "text-emerald-600",
- bg: "bg-emerald-50",
- },
- {
- label: "Temp",
- value: "98.6",
- unit: "°F",
- color: "text-cyan-600",
- bg: "bg-cyan-50",
- },
- ].map((vital) => (
+
>}
+ scoped
+ >
+
-
- {vital.label}
+
+ {VITALS.map((vital) => (
+
+
+ {vital.label}
+
+
+ {vital.value}
+
+ {vital.unit}
+
+
+
+ ))}
-
- {vital.value}
-
- {vital.unit}
-
+
+
+
+
+ Right-click vitals to alert, recheck, or open trend review.
+
- ))}
-
+
+
+
+
>}
+ scoped
+ >
+
+
+
+
+
+ Care Status
+
+
Monitoring stable
+
+
+
Team
+
Primary RN + On-call
+
+
+
Route
+
Unit board task queue
+
+
-
+
+
+
+ Right-click status to flag urgent, notify care team, or escalate
+ handoff.
+
+
+
+
void;
-}
-
-export function SoftwareEditorCard({
- onMenuItemsChange,
-}: SoftwareEditorCardProps) {
- const [code, setCode] = useState(DEFAULT_SOFTWARE_CODE);
- const [parseError, setParseError] = useState(false);
-
- const handleCodeChange = useCallback(
- (value: string) => {
- setCode(value);
- const parsed = parseMenuItems(value);
- if (parsed) {
- setParseError(false);
- onMenuItemsChange(parsed);
- } else {
- setParseError(true);
- }
- },
- [onMenuItemsChange],
- );
-
- useEffect(() => {
- const parsed = parseMenuItems(DEFAULT_SOFTWARE_CODE);
- if (parsed) {
- onMenuItemsChange(parsed);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+export function SoftwareEditorCard() {
+ const [code] = useState(DEFAULT_SOFTWARE_CODE);
return (
-
+
-
- {parseError ? "Syntax error" : "Config valid"}
+
+ Workflow actions fixed for this demo
- Live Preview Active
+ Right-click to run prototype flows
diff --git a/apps/web/src/components/immersive-workstream/sections/DefaultWorkstreamSection.tsx b/apps/web/src/components/immersive-workstream/sections/DefaultWorkstreamSection.tsx
index d0dc02d..f10a53c 100644
--- a/apps/web/src/components/immersive-workstream/sections/DefaultWorkstreamSection.tsx
+++ b/apps/web/src/components/immersive-workstream/sections/DefaultWorkstreamSection.tsx
@@ -5,6 +5,9 @@ import { type MotionValue, motion } from "motion/react";
import { FloatingIcon } from "../FloatingIcon";
import { adapter } from "../adapter";
import type { ImmersiveTheme } from "../types";
+import { WorkflowDrawer } from "../workflows/WorkflowDrawer";
+import { isWorkflowWorkstreamId } from "../workflows/registry";
+import { useWorkflowLauncher } from "../workflows/useWorkflowLauncher";
interface DefaultWorkstreamSectionProps {
theme: ImmersiveTheme;
@@ -19,6 +22,13 @@ export function DefaultWorkstreamSection({
isInView,
bgY,
}: DefaultWorkstreamSectionProps) {
+ const workflowWorkstreamId = isWorkflowWorkstreamId(theme.id)
+ ? theme.id
+ : "software";
+ const { activeWorkflow, closeWorkflow, menuItems } = useWorkflowLauncher(
+ workflowWorkstreamId,
+ );
+
return (
<>
+
+
>
);
}
diff --git a/apps/web/src/components/immersive-workstream/sections/EcommerceSection.tsx b/apps/web/src/components/immersive-workstream/sections/EcommerceSection.tsx
new file mode 100644
index 0000000..19fd031
--- /dev/null
+++ b/apps/web/src/components/immersive-workstream/sections/EcommerceSection.tsx
@@ -0,0 +1,143 @@
+import { useMemo } from "react";
+import { MousePointer2 } from "lucide-react";
+import { type MotionValue, motion } from "motion/react";
+import { FloatingIcon } from "../FloatingIcon";
+import { EcommerceCard } from "../cards/EcommerceCard";
+import type { ImmersiveTheme } from "../types";
+import { WorkflowDrawer } from "../workflows/WorkflowDrawer";
+import { useWorkflowLauncher } from "../workflows/useWorkflowLauncher";
+
+interface EcommerceSectionProps {
+ bgY: MotionValue;
+ isInView: boolean;
+ scrollYProgress: MotionValue;
+ theme: ImmersiveTheme;
+}
+
+export function EcommerceSection({
+ bgY,
+ isInView,
+ scrollYProgress,
+ theme,
+}: EcommerceSectionProps) {
+ const { activeWorkflow, closeWorkflow, getMenuItemsForActionIds } =
+ useWorkflowLauncher("ecommerce");
+
+ const orderMenuItems = useMemo(
+ () =>
+ getMenuItemsForActionIds([
+ "ecommerce.item_missing",
+ "ecommerce.shipping_issue",
+ "ecommerce.escalate_order",
+ ]),
+ [getMenuItemsForActionIds],
+ );
+
+ const stockMenuItems = useMemo(
+ () =>
+ getMenuItemsForActionIds([
+ "ecommerce.view_stock_details",
+ "ecommerce.adjust_stock",
+ "ecommerce.set_restock_alert",
+ ]),
+ [getMenuItemsForActionIds],
+ );
+
+ const pricingMenuItems = useMemo(
+ () =>
+ getMenuItemsForActionIds([
+ "ecommerce.view_discounts",
+ "ecommerce.change_price",
+ "ecommerce.schedule_promotion",
+ ]),
+ [getMenuItemsForActionIds],
+ );
+
+ const productMenuItems = useMemo(
+ () =>
+ getMenuItemsForActionIds([
+ "ecommerce.edit_product",
+ "ecommerce.upload_product_media",
+ "ecommerce.update_product_copy",
+ ]),
+ [getMenuItemsForActionIds],
+ );
+
+ return (
+ <>
+
+
+ {theme.gridPattern && (
+
+ )}
+
+ {theme.floatingElements.map((element, index) => (
+
+ ))}
+
+
+
+
+ {theme.title}
+
+ {theme.tagline}
+
+
+
+
+
+
+
+
+ Right-click stock, pricing, product details, or order rows
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/web/src/components/immersive-workstream/sections/HealthcareSection.tsx b/apps/web/src/components/immersive-workstream/sections/HealthcareSection.tsx
index b58903d..acb29c4 100644
--- a/apps/web/src/components/immersive-workstream/sections/HealthcareSection.tsx
+++ b/apps/web/src/components/immersive-workstream/sections/HealthcareSection.tsx
@@ -1,12 +1,12 @@
-import { PointerProvider } from "@ewjdev/anyclick-pointer";
-import { AnyclickProvider } from "@ewjdev/anyclick-react";
+import { useMemo } from "react";
import { MousePointer2 } from "lucide-react";
import { type MotionValue, motion } from "motion/react";
import { FloatingIcon } from "../FloatingIcon";
-import { adapter } from "../adapter";
import { HealthcareMedicalBackground } from "../backgrounds/HealthcareMedicalBackground";
import { HealthcareCard } from "../cards/HealthcareCard";
import type { ImmersiveTheme } from "../types";
+import { WorkflowDrawer } from "../workflows/WorkflowDrawer";
+import { useWorkflowLauncher } from "../workflows/useWorkflowLauncher";
interface HealthcareSectionProps {
theme: ImmersiveTheme;
@@ -21,6 +21,39 @@ export function HealthcareSection({
isInView,
bgY,
}: HealthcareSectionProps) {
+ const { activeWorkflow, closeWorkflow, getMenuItemsForActionIds } =
+ useWorkflowLauncher("healthcare");
+
+ const summaryMenuItems = useMemo(
+ () =>
+ getMenuItemsForActionIds([
+ "healthcare.check_in_issue",
+ "healthcare.verify_identity_token",
+ "healthcare.coverage_exception",
+ ]),
+ [getMenuItemsForActionIds],
+ );
+
+ const vitalsMenuItems = useMemo(
+ () =>
+ getMenuItemsForActionIds([
+ "healthcare.vital_alert",
+ "healthcare.request_vital_recheck",
+ "healthcare.open_trend_review",
+ ]),
+ [getMenuItemsForActionIds],
+ );
+
+ const statusMenuItems = useMemo(
+ () =>
+ getMenuItemsForActionIds([
+ "healthcare.flag_urgent",
+ "healthcare.notify_care_team",
+ "healthcare.escalate_handoff",
+ ]),
+ [getMenuItemsForActionIds],
+ );
+
return (
<>
- >}
- scoped
- >
-
+
+
-
-
-
-
- Right-click to interact
-
-
-
-
+
+
+ Right-click patient summary, vitals, or care status to interact
+
+
+
@@ -110,6 +126,8 @@ export function HealthcareSection({
background: `linear-gradient(to top, rgba(16, 185, 129, 0.1), transparent)`,
}}
/>
+
+
>
);
}
diff --git a/apps/web/src/components/immersive-workstream/sections/SoftwareDevelopmentSection.tsx b/apps/web/src/components/immersive-workstream/sections/SoftwareDevelopmentSection.tsx
index ac7df9b..74618be 100644
--- a/apps/web/src/components/immersive-workstream/sections/SoftwareDevelopmentSection.tsx
+++ b/apps/web/src/components/immersive-workstream/sections/SoftwareDevelopmentSection.tsx
@@ -1,13 +1,14 @@
-import { useCallback, useState } from "react";
import { PointerProvider } from "@ewjdev/anyclick-pointer";
import { AnyclickProvider } from "@ewjdev/anyclick-react";
-import type { ContextMenuItem } from "@ewjdev/anyclick-react";
import { MousePointer2 } from "lucide-react";
import { type MotionValue, motion } from "motion/react";
import { FloatingIcon } from "../FloatingIcon";
import { adapter } from "../adapter";
import { SoftwareEditorCard } from "../cards/SoftwareEditorCard";
import type { ImmersiveTheme } from "../types";
+import { WorkflowDrawer } from "../workflows/WorkflowDrawer";
+import { isWorkflowWorkstreamId } from "../workflows/registry";
+import { useWorkflowLauncher } from "../workflows/useWorkflowLauncher";
interface SoftwareDevelopmentSectionProps {
theme: ImmersiveTheme;
@@ -22,12 +23,11 @@ export function SoftwareDevelopmentSection({
isInView,
bgY,
}: SoftwareDevelopmentSectionProps) {
- const [menuItems, setMenuItems] = useState(
- theme.menuItems,
- );
- const handleMenuItemsChange = useCallback(
- (items: ContextMenuItem[]) => setMenuItems(items),
- [],
+ const workflowWorkstreamId = isWorkflowWorkstreamId(theme.id)
+ ? theme.id
+ : "software";
+ const { activeWorkflow, closeWorkflow, menuItems } = useWorkflowLauncher(
+ workflowWorkstreamId,
);
return (
@@ -115,7 +115,7 @@ export function SoftwareDevelopmentSection({
}}
>
-
+
- Right-click to see your custom menu
+ Right-click to launch workflow demos
@@ -139,6 +139,8 @@ export function SoftwareDevelopmentSection({
opacity: 0.3,
}}
/>
+
+
>
);
}
diff --git a/apps/web/src/components/immersive-workstream/sections/WorkstreamSection.tsx b/apps/web/src/components/immersive-workstream/sections/WorkstreamSection.tsx
index 80b74fb..a888810 100644
--- a/apps/web/src/components/immersive-workstream/sections/WorkstreamSection.tsx
+++ b/apps/web/src/components/immersive-workstream/sections/WorkstreamSection.tsx
@@ -2,6 +2,7 @@ import { useRef } from "react";
import { useInView, useScroll, useTransform } from "motion/react";
import type { ImmersiveTheme } from "../types";
import { DefaultWorkstreamSection } from "./DefaultWorkstreamSection";
+import { EcommerceSection } from "./EcommerceSection";
import { HealthcareSection } from "./HealthcareSection";
import { SoftwareDevelopmentSection } from "./SoftwareDevelopmentSection";
@@ -50,6 +51,21 @@ export function WorkstreamSection({ theme }: WorkstreamSectionProps) {
/>
);
+ case "ecommerce":
+ return (
+
+ );
default:
return (
Issue Details -> Review" },
+ { label: "Send to Cursor", flow: "Fix Scope -> Prompt & Constraints -> Review" },
+ { label: "Copy selector", flow: "Selector Format -> Copy Bundle" },
];
-// Right-click anywhere to see your menu in action!
+// This editor remains visible to show how the workflow configuration might look.
`;
diff --git a/apps/web/src/components/immersive-workstream/themes.tsx b/apps/web/src/components/immersive-workstream/themes.tsx
index 572bcd6..74b0e8a 100644
--- a/apps/web/src/components/immersive-workstream/themes.tsx
+++ b/apps/web/src/components/immersive-workstream/themes.tsx
@@ -13,8 +13,6 @@ import {
Truck,
Users,
} from "lucide-react";
-import { EcommerceCard } from "./cards/EcommerceCard";
-import { HealthcareCard } from "./cards/HealthcareCard";
import { SocialCard } from "./cards/SocialCard";
import type { ImmersiveTheme } from "./types";
@@ -22,7 +20,7 @@ export const getImmersiveThemes = (): ImmersiveTheme[] => [
{
id: "software",
title: "Software Development",
- tagline: "Edit the code below to customize your context menu in real-time",
+ tagline: "Right-click to run full development workflow prototypes",
primaryColor: "#00ff41",
secondaryColor: "#22c55e",
glowColor: "rgba(0, 255, 65, 0.4)",
@@ -77,11 +75,6 @@ export const getImmersiveThemes = (): ImmersiveTheme[] => [
},
],
cardContent: null,
- menuItems: [
- { label: "Report bug", type: "bug", showComment: true },
- { label: "Send to Cursor", type: "cursor", showComment: false },
- { label: "Copy selector", type: "selector", showComment: false },
- ],
menuStyle: {
"--anyclick-menu-bg": "#0a0a0a",
"--anyclick-menu-border": "#00ff41",
@@ -146,12 +139,7 @@ export const getImmersiveThemes = (): ImmersiveTheme[] => [
opacity: 0.4,
},
],
- cardContent: ,
- menuItems: [
- { label: "Item missing", type: "missing", showComment: true },
- { label: "Shipping issue", type: "shipping", showComment: true },
- { label: "Escalate", type: "escalate", showComment: false },
- ],
+ cardContent: null,
menuStyle: {
"--anyclick-menu-bg": "#1c1917",
"--anyclick-menu-border": "#f59e0b",
@@ -208,12 +196,7 @@ export const getImmersiveThemes = (): ImmersiveTheme[] => [
opacity: 0.2,
},
],
- cardContent: ,
- menuItems: [
- { label: "Check-in issue", type: "checkin", showComment: true },
- { label: "Vital alert", type: "vital", showComment: true },
- { label: "Flag urgent", type: "urgent", showComment: false },
- ],
+ cardContent: null,
menuStyle: {
"--anyclick-menu-bg": "#ffffff",
"--anyclick-menu-border": "#10b981",
@@ -280,11 +263,6 @@ export const getImmersiveThemes = (): ImmersiveTheme[] => [
},
],
cardContent: ,
- menuItems: [
- { label: "Save asset", type: "save", showComment: false },
- { label: "Flag content", type: "flag", showComment: true },
- { label: "Quick reply", type: "reply", showComment: true },
- ],
menuStyle: {
"--anyclick-menu-bg": "rgba(236, 72, 153, 0.1)",
"--anyclick-menu-border": "rgba(236, 72, 153, 0.3)",
diff --git a/apps/web/src/components/immersive-workstream/types.ts b/apps/web/src/components/immersive-workstream/types.ts
index 0b37842..78ea10e 100644
--- a/apps/web/src/components/immersive-workstream/types.ts
+++ b/apps/web/src/components/immersive-workstream/types.ts
@@ -1,5 +1,4 @@
import type { CSSProperties, ReactNode } from "react";
-import type { ContextMenuItem } from "@ewjdev/anyclick-react";
export interface ImmersiveTheme {
id: string;
@@ -14,7 +13,6 @@ export interface ImmersiveTheme {
backgroundImage?: string;
floatingElements: FloatingElement[];
cardContent: ReactNode;
- menuItems: ContextMenuItem[];
menuStyle: CSSProperties;
pointerIcon: ReactNode;
}
diff --git a/apps/web/src/components/immersive-workstream/workflows/WorkflowContextSummary.tsx b/apps/web/src/components/immersive-workstream/workflows/WorkflowContextSummary.tsx
new file mode 100644
index 0000000..78df4c0
--- /dev/null
+++ b/apps/web/src/components/immersive-workstream/workflows/WorkflowContextSummary.tsx
@@ -0,0 +1,131 @@
+"use client";
+
+import { useState } from "react";
+import { Loader2, RefreshCw } from "lucide-react";
+import type { WorkflowCaptureState } from "./types";
+
+interface WorkflowContextSummaryProps {
+ captureState: WorkflowCaptureState;
+ onRetryCapture: () => Promise;
+}
+
+export function WorkflowContextSummary({
+ captureState,
+ onRetryCapture,
+}: WorkflowContextSummaryProps) {
+ const [isExpanded, setIsExpanded] = useState(false);
+ const data = captureState.data;
+
+ return (
+
+
+
+ Captured Context
+
+
+ setIsExpanded((current) => !current)}
+ className="rounded border border-white/20 px-2 py-1 text-[11px] font-medium text-gray-200 transition-colors hover:bg-white/10"
+ >
+ {isExpanded ? "Hide details" : "Show details"}
+
+
+
+
+ This workflow auto-captures selectors, page metadata, and screenshot
+ evidence so teams can act on precise context instead of manually
+ re-entering details and risking handoff mistakes.
+
+
+ {!isExpanded ? null : captureState.status === "capturing" ? (
+
+
+
+ Capturing target and container context...
+
+
+ ) : !data ? (
+
+
+ Context capture is unavailable.
+ {
+ void onRetryCapture();
+ }}
+ className="inline-flex items-center gap-1 rounded border border-amber-200/40 px-2 py-1 text-[11px] font-medium text-amber-100 hover:bg-amber-200/10"
+ >
+
+ Retry
+
+
+ {captureState.error && (
+
+ {captureState.error}
+
+ )}
+
+ ) : (
+ <>
+
+
+
+ Target:
+ {" "}
+
+ {data.targetSelector}
+
+
+
+
+ Container:
+ {" "}
+
+ {data.containerSelector}
+
+
+
+
+
+
+
+ Page
+
+ {data.pageContext.title}
+
+ {data.pageContext.url}
+
+
+
+
+ {data.containerScreenshot ? (
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+
+ ) : (
+
+ Screenshot unavailable.
+ {data.screenshotError ? ` ${data.screenshotError}` : ""}
+
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/apps/web/src/components/immersive-workstream/workflows/WorkflowDrawer.tsx b/apps/web/src/components/immersive-workstream/workflows/WorkflowDrawer.tsx
new file mode 100644
index 0000000..f631066
--- /dev/null
+++ b/apps/web/src/components/immersive-workstream/workflows/WorkflowDrawer.tsx
@@ -0,0 +1,243 @@
+"use client";
+
+import type { CSSProperties } from "react";
+import { useCallback, useEffect, useState } from "react";
+import { X } from "lucide-react";
+import { captureWorkflowContext } from "./contextCapture";
+import { WorkflowFlowRenderer } from "./WorkflowFlowRenderer";
+import type {
+ WorkflowCaptureState,
+ WorkflowLaunchState,
+ WorkflowWorkstreamId,
+} from "./types";
+
+interface WorkflowDrawerProps {
+ activeWorkflow: WorkflowLaunchState | null;
+ onClose: () => void;
+}
+
+interface WorkflowPalette {
+ accent: string;
+ accentSoftBg: string;
+ accentSoftBorder: string;
+ accentStrongBg: string;
+ accentText: string;
+ accentTextMuted: string;
+ closeHoverBg: string;
+}
+
+const WORKSTREAM_PALETTES: Record = {
+ software: {
+ accent: "#00ff41",
+ accentSoftBg: "rgba(0, 255, 65, 0.12)",
+ accentSoftBorder: "rgba(0, 255, 65, 0.35)",
+ accentStrongBg: "rgba(0, 255, 65, 0.95)",
+ accentText: "#c7ffd8",
+ accentTextMuted: "rgba(151, 255, 185, 0.8)",
+ closeHoverBg: "rgba(0, 255, 65, 0.16)",
+ },
+ ecommerce: {
+ accent: "#f59e0b",
+ accentSoftBg: "rgba(245, 158, 11, 0.14)",
+ accentSoftBorder: "rgba(245, 158, 11, 0.38)",
+ accentStrongBg: "rgba(245, 158, 11, 0.96)",
+ accentText: "#ffe8b8",
+ accentTextMuted: "rgba(255, 212, 128, 0.82)",
+ closeHoverBg: "rgba(245, 158, 11, 0.18)",
+ },
+ healthcare: {
+ accent: "#10b981",
+ accentSoftBg: "rgba(16, 185, 129, 0.13)",
+ accentSoftBorder: "rgba(16, 185, 129, 0.34)",
+ accentStrongBg: "rgba(16, 185, 129, 0.95)",
+ accentText: "#c6ffec",
+ accentTextMuted: "rgba(152, 255, 224, 0.82)",
+ closeHoverBg: "rgba(16, 185, 129, 0.16)",
+ },
+ social: {
+ accent: "#ec4899",
+ accentSoftBg: "rgba(236, 72, 153, 0.14)",
+ accentSoftBorder: "rgba(236, 72, 153, 0.36)",
+ accentStrongBg: "rgba(236, 72, 153, 0.95)",
+ accentText: "#ffd7ea",
+ accentTextMuted: "rgba(255, 186, 223, 0.84)",
+ closeHoverBg: "rgba(236, 72, 153, 0.17)",
+ },
+};
+
+function getDrawerStyle(workstream: WorkflowWorkstreamId): CSSProperties {
+ const palette = WORKSTREAM_PALETTES[workstream];
+
+ return {
+ "--workflow-accent": palette.accent,
+ "--workflow-accent-soft-bg": palette.accentSoftBg,
+ "--workflow-accent-soft-border": palette.accentSoftBorder,
+ "--workflow-accent-strong-bg": palette.accentStrongBg,
+ "--workflow-accent-text": palette.accentText,
+ "--workflow-accent-text-muted": palette.accentTextMuted,
+ "--workflow-close-hover-bg": palette.closeHoverBg,
+ } as CSSProperties;
+}
+
+const INITIAL_CAPTURE_STATE: WorkflowCaptureState = {
+ data: null,
+ status: "idle",
+};
+
+export function WorkflowDrawer({
+ activeWorkflow,
+ onClose,
+}: WorkflowDrawerProps) {
+ const [captureState, setCaptureState] =
+ useState(INITIAL_CAPTURE_STATE);
+
+ const drawerStyle = getDrawerStyle(
+ activeWorkflow?.action.workstream ?? "software",
+ );
+
+ const refreshCapture = useCallback(async () => {
+ if (!activeWorkflow) return;
+
+ setCaptureState({ data: null, status: "capturing" });
+
+ try {
+ const data = await captureWorkflowContext(
+ activeWorkflow.targetElement,
+ activeWorkflow.containerElement,
+ );
+
+ setCaptureState({
+ data,
+ status: "ready",
+ });
+ } catch (error) {
+ setCaptureState({
+ data: null,
+ error:
+ error instanceof Error
+ ? error.message
+ : "Failed to capture workflow context",
+ status: "ready",
+ });
+ }
+ }, [activeWorkflow]);
+
+ useEffect(() => {
+ if (!activeWorkflow) {
+ setCaptureState(INITIAL_CAPTURE_STATE);
+ return;
+ }
+
+ let isActive = true;
+
+ setCaptureState({ data: null, status: "capturing" });
+
+ captureWorkflowContext(
+ activeWorkflow.targetElement,
+ activeWorkflow.containerElement,
+ )
+ .then((data) => {
+ if (!isActive) return;
+ setCaptureState({ data, status: "ready" });
+ })
+ .catch((error) => {
+ if (!isActive) return;
+ setCaptureState({
+ data: null,
+ error:
+ error instanceof Error
+ ? error.message
+ : "Failed to capture workflow context",
+ status: "ready",
+ });
+ });
+
+ return () => {
+ isActive = false;
+ };
+ }, [activeWorkflow]);
+
+ useEffect(() => {
+ if (!activeWorkflow) return;
+
+ const previousOverflow = document.body.style.overflow;
+ document.body.style.overflow = "hidden";
+
+ return () => {
+ document.body.style.overflow = previousOverflow;
+ };
+ }, [activeWorkflow]);
+
+ useEffect(() => {
+ if (!activeWorkflow) return;
+
+ const onEscape = (event: KeyboardEvent) => {
+ if (event.key === "Escape") {
+ onClose();
+ }
+ };
+
+ window.addEventListener("keydown", onEscape);
+ return () => window.removeEventListener("keydown", onEscape);
+ }, [activeWorkflow, onClose]);
+
+ if (!activeWorkflow) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+ Anyclick workflow preview
+
+
+ {activeWorkflow.action.menuLabel}
+
+
+
+
{
+ event.currentTarget.style.backgroundColor =
+ "var(--workflow-close-hover-bg)";
+ }}
+ onMouseLeave={(event) => {
+ event.currentTarget.style.backgroundColor = "transparent";
+ }}
+ aria-label="Close workflow drawer"
+ >
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/immersive-workstream/workflows/WorkflowFlowRenderer.tsx b/apps/web/src/components/immersive-workstream/workflows/WorkflowFlowRenderer.tsx
new file mode 100644
index 0000000..f8cd683
--- /dev/null
+++ b/apps/web/src/components/immersive-workstream/workflows/WorkflowFlowRenderer.tsx
@@ -0,0 +1,2679 @@
+"use client";
+
+import { useEffect, useMemo, useState } from "react";
+import {
+ buildAnyclickPayload,
+ type AnyclickPayload,
+ type ScreenshotData,
+} from "@ewjdev/anyclick-core";
+import {
+ AlertCircle,
+ CheckCircle2,
+ Clipboard,
+ ExternalLink,
+} from "lucide-react";
+import { buildSelectorBundle, generateHeuristicIssueTitle } from "./titleHeuristics";
+import { WorkflowContextSummary } from "./WorkflowContextSummary";
+import type {
+ WorkflowActionId,
+ WorkflowCaptureData,
+ WorkflowCaptureState,
+ WorkflowLaunchState,
+} from "./types";
+
+type GenericActionId = Exclude<
+ WorkflowActionId,
+ "software.report_bug" | "software.copy_selector"
+>;
+
+type FieldKind = "select" | "text" | "textarea";
+
+interface FieldOption {
+ label: string;
+ value: string;
+}
+
+interface FieldConfig {
+ key: string;
+ kind: FieldKind;
+ label: string;
+ options?: FieldOption[];
+ placeholder?: string;
+ required?: boolean;
+}
+
+interface GenericStepConfig {
+ description: string;
+ fields?: FieldConfig[];
+ kind?: "form" | "review";
+ title: string;
+}
+
+interface GenericWorkflowConfig {
+ successMessage: string;
+ successTitle: string;
+ steps: GenericStepConfig[];
+}
+
+interface WorkflowFlowRendererProps {
+ captureState: WorkflowCaptureState;
+ launch: WorkflowLaunchState;
+ onClose: () => void;
+ onRetryCapture: () => Promise;
+}
+
+interface WorkflowSuccessState {
+ demo: boolean;
+ issueUrl?: string;
+ message: string;
+ title: string;
+}
+
+const GENERIC_WORKFLOWS: Record = {
+ "ecommerce.escalate_order": {
+ successTitle: "Escalation workflow simulated",
+ successMessage:
+ "Order escalation payload was assembled with context and ready for adapter routing.",
+ steps: [
+ {
+ title: "Escalation Target",
+ description: "Route this incident to the right team.",
+ kind: "form",
+ fields: [
+ {
+ key: "targetTeam",
+ kind: "select",
+ label: "Target Team",
+ required: true,
+ options: [
+ { value: "fulfillment", label: "Fulfillment" },
+ { value: "support", label: "Customer Support" },
+ { value: "operations", label: "Operations" },
+ ],
+ },
+ {
+ key: "escalationReason",
+ kind: "textarea",
+ label: "Escalation Reason",
+ placeholder: "Describe why this order should be escalated.",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Priority & SLA",
+ description: "Set urgency and expected response window.",
+ kind: "form",
+ fields: [
+ {
+ key: "priority",
+ kind: "select",
+ label: "Priority",
+ required: true,
+ options: [
+ { value: "high", label: "High" },
+ { value: "medium", label: "Medium" },
+ { value: "low", label: "Low" },
+ ],
+ },
+ {
+ key: "slaWindow",
+ kind: "select",
+ label: "Target SLA",
+ required: true,
+ options: [
+ { value: "30m", label: "30 minutes" },
+ { value: "2h", label: "2 hours" },
+ { value: "24h", label: "24 hours" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review the escalation request.",
+ kind: "review",
+ },
+ ],
+ },
+ "ecommerce.item_missing": {
+ successTitle: "Missing item workflow simulated",
+ successMessage:
+ "Incident details are packaged with selectors and screenshot context for routing.",
+ steps: [
+ {
+ title: "Incident Details",
+ description: "Capture missing item details.",
+ kind: "form",
+ fields: [
+ {
+ key: "orderId",
+ kind: "text",
+ label: "Order ID",
+ placeholder: "ORD-102944",
+ required: true,
+ },
+ {
+ key: "sku",
+ kind: "text",
+ label: "SKU",
+ placeholder: "SKU-3482",
+ required: true,
+ },
+ {
+ key: "quantityMissing",
+ kind: "text",
+ label: "Quantity Missing",
+ placeholder: "1",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Resolution Path",
+ description: "Define next action and owner.",
+ kind: "form",
+ fields: [
+ {
+ key: "resolutionPath",
+ kind: "select",
+ label: "Resolution Path",
+ required: true,
+ options: [
+ { value: "reship", label: "Reship item" },
+ { value: "refund", label: "Refund line item" },
+ { value: "investigate", label: "Investigate warehouse pick" },
+ ],
+ },
+ {
+ key: "owner",
+ kind: "select",
+ label: "Owner",
+ required: true,
+ options: [
+ { value: "warehouse", label: "Warehouse Lead" },
+ { value: "support", label: "Support Manager" },
+ { value: "ops", label: "Operations Duty" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review details and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "ecommerce.shipping_issue": {
+ successTitle: "Shipping issue workflow simulated",
+ successMessage:
+ "Shipping disruption data was captured with page and DOM context.",
+ steps: [
+ {
+ title: "Shipment Details",
+ description: "Capture shipment-level context.",
+ kind: "form",
+ fields: [
+ {
+ key: "carrier",
+ kind: "select",
+ label: "Carrier",
+ required: true,
+ options: [
+ { value: "ups", label: "UPS" },
+ { value: "fedex", label: "FedEx" },
+ { value: "usps", label: "USPS" },
+ { value: "other", label: "Other" },
+ ],
+ },
+ {
+ key: "trackingId",
+ kind: "text",
+ label: "Tracking ID",
+ placeholder: "1Z999AA10123456784",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Customer Impact",
+ description: "Capture impact and communication strategy.",
+ kind: "form",
+ fields: [
+ {
+ key: "impactLevel",
+ kind: "select",
+ label: "Impact Level",
+ required: true,
+ options: [
+ { value: "low", label: "Low" },
+ { value: "medium", label: "Medium" },
+ { value: "high", label: "High" },
+ ],
+ },
+ {
+ key: "customerMessage",
+ kind: "textarea",
+ label: "Customer Message",
+ placeholder: "Draft a customer-facing update.",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review details and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "ecommerce.view_stock_details": {
+ successTitle: "Stock details workflow simulated",
+ successMessage:
+ "Inventory and warehouse context were assembled for operational review.",
+ steps: [
+ {
+ title: "Inventory Snapshot",
+ description: "Inspect the current stock position.",
+ kind: "form",
+ fields: [
+ {
+ key: "sku",
+ kind: "text",
+ label: "SKU",
+ placeholder: "SKU-4821",
+ required: true,
+ },
+ {
+ key: "availableUnits",
+ kind: "text",
+ label: "Available Units",
+ placeholder: "128",
+ required: true,
+ },
+ {
+ key: "reservedUnits",
+ kind: "text",
+ label: "Reserved Units",
+ placeholder: "27",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Source Location",
+ description: "Review warehouse and replenishment source.",
+ kind: "form",
+ fields: [
+ {
+ key: "warehouse",
+ kind: "select",
+ label: "Primary Warehouse",
+ required: true,
+ options: [
+ { value: "wh-west", label: "WH-West" },
+ { value: "wh-central", label: "WH-Central" },
+ { value: "wh-east", label: "WH-East" },
+ ],
+ },
+ {
+ key: "restockEta",
+ kind: "text",
+ label: "Restock ETA",
+ placeholder: "2026-02-22 14:00",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review stock context and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "ecommerce.adjust_stock": {
+ successTitle: "Stock adjustment workflow simulated",
+ successMessage:
+ "Inventory adjustment instructions were prepared with captured UI context.",
+ steps: [
+ {
+ title: "Adjustment Type",
+ description: "Choose how inventory should be adjusted.",
+ kind: "form",
+ fields: [
+ {
+ key: "adjustmentType",
+ kind: "select",
+ label: "Adjustment Type",
+ required: true,
+ options: [
+ { value: "increase", label: "Increase stock" },
+ { value: "decrease", label: "Decrease stock" },
+ { value: "release", label: "Release reserved units" },
+ ],
+ },
+ {
+ key: "channel",
+ kind: "select",
+ label: "Inventory Channel",
+ required: true,
+ options: [
+ { value: "online", label: "Online store" },
+ { value: "marketplace", label: "Marketplace" },
+ { value: "all", label: "All channels" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Quantity & Reason",
+ description: "Specify quantity change and audit reason.",
+ kind: "form",
+ fields: [
+ {
+ key: "delta",
+ kind: "text",
+ label: "Quantity Delta",
+ placeholder: "+24",
+ required: true,
+ },
+ {
+ key: "adjustReason",
+ kind: "textarea",
+ label: "Reason",
+ placeholder: "Cycle count reconciliation after receiving batch.",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review adjustment details and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "ecommerce.set_restock_alert": {
+ successTitle: "Restock alert workflow simulated",
+ successMessage:
+ "Threshold and notification routing were captured for alert automation.",
+ steps: [
+ {
+ title: "Threshold",
+ description: "Define when low-stock alerts should fire.",
+ kind: "form",
+ fields: [
+ {
+ key: "thresholdUnits",
+ kind: "text",
+ label: "Threshold Units",
+ placeholder: "20",
+ required: true,
+ },
+ {
+ key: "lookbackWindow",
+ kind: "select",
+ label: "Sales Velocity Window",
+ required: true,
+ options: [
+ { value: "7d", label: "Last 7 days" },
+ { value: "14d", label: "Last 14 days" },
+ { value: "30d", label: "Last 30 days" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Notification Route",
+ description: "Select where alerts should be sent.",
+ kind: "form",
+ fields: [
+ {
+ key: "notifyTarget",
+ kind: "select",
+ label: "Notify Target",
+ required: true,
+ options: [
+ { value: "ops-slack", label: "Ops Slack" },
+ { value: "inventory-email", label: "Inventory Email" },
+ { value: "pager", label: "On-call Pager" },
+ ],
+ },
+ {
+ key: "escalationDelay",
+ kind: "select",
+ label: "Escalation Delay",
+ required: true,
+ options: [
+ { value: "15m", label: "15 minutes" },
+ { value: "60m", label: "1 hour" },
+ { value: "4h", label: "4 hours" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review alert configuration and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "ecommerce.view_discounts": {
+ successTitle: "Discount review workflow simulated",
+ successMessage:
+ "Discount and margin context were prepared for pricing review.",
+ steps: [
+ {
+ title: "Active Promotions",
+ description: "Inspect currently applied discount programs.",
+ kind: "form",
+ fields: [
+ {
+ key: "promoSource",
+ kind: "select",
+ label: "Promotion Source",
+ required: true,
+ options: [
+ { value: "catalog", label: "Catalog pricing rules" },
+ { value: "coupon", label: "Coupon campaign" },
+ { value: "bundle", label: "Bundle offer" },
+ ],
+ },
+ {
+ key: "activeDiscount",
+ kind: "text",
+ label: "Current Discount",
+ placeholder: "15%",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Margin Impact",
+ description: "Evaluate margin effect before changes.",
+ kind: "form",
+ fields: [
+ {
+ key: "grossMargin",
+ kind: "text",
+ label: "Gross Margin",
+ placeholder: "32%",
+ required: true,
+ },
+ {
+ key: "impactNotes",
+ kind: "textarea",
+ label: "Impact Notes",
+ placeholder: "Describe expected revenue and conversion impact.",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review pricing signals and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "ecommerce.change_price": {
+ successTitle: "Price change workflow simulated",
+ successMessage:
+ "Price update details were captured with guardrails and approval routing.",
+ steps: [
+ {
+ title: "New Price",
+ description: "Set the new price and effective window.",
+ kind: "form",
+ fields: [
+ {
+ key: "currentPrice",
+ kind: "text",
+ label: "Current Price",
+ placeholder: "49.99",
+ required: true,
+ },
+ {
+ key: "newPrice",
+ kind: "text",
+ label: "New Price",
+ placeholder: "44.99",
+ required: true,
+ },
+ {
+ key: "effectiveAt",
+ kind: "text",
+ label: "Effective At",
+ placeholder: "2026-02-20 09:00",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Guardrails",
+ description: "Validate floor and approval requirements.",
+ kind: "form",
+ fields: [
+ {
+ key: "floorPrice",
+ kind: "text",
+ label: "Floor Price",
+ placeholder: "39.99",
+ required: true,
+ },
+ {
+ key: "approvalRoute",
+ kind: "select",
+ label: "Approval Route",
+ required: true,
+ options: [
+ { value: "pricing-manager", label: "Pricing manager" },
+ { value: "merch-lead", label: "Merchandising lead" },
+ { value: "auto", label: "Auto-approved under threshold" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review the price update and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "ecommerce.schedule_promotion": {
+ successTitle: "Promotion scheduling workflow simulated",
+ successMessage:
+ "Promotion timing and audience parameters were prepared for launch.",
+ steps: [
+ {
+ title: "Promotion Details",
+ description: "Define promotion mechanics.",
+ kind: "form",
+ fields: [
+ {
+ key: "promotionType",
+ kind: "select",
+ label: "Promotion Type",
+ required: true,
+ options: [
+ { value: "percent", label: "Percent off" },
+ { value: "fixed", label: "Fixed amount off" },
+ { value: "bogo", label: "BOGO" },
+ ],
+ },
+ {
+ key: "promotionValue",
+ kind: "text",
+ label: "Promotion Value",
+ placeholder: "20%",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Timing & Audience",
+ description: "Configure activation window and target segment.",
+ kind: "form",
+ fields: [
+ {
+ key: "window",
+ kind: "text",
+ label: "Active Window",
+ placeholder: "2026-02-21 to 2026-02-28",
+ required: true,
+ },
+ {
+ key: "audience",
+ kind: "select",
+ label: "Audience",
+ required: true,
+ options: [
+ { value: "all", label: "All customers" },
+ { value: "new", label: "New customers" },
+ { value: "vip", label: "VIP segment" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review the promotion setup and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "ecommerce.edit_product": {
+ successTitle: "Product edit workflow simulated",
+ successMessage:
+ "Product metadata and merchandising changes were staged with context.",
+ steps: [
+ {
+ title: "Core Fields",
+ description: "Update product metadata.",
+ kind: "form",
+ fields: [
+ {
+ key: "productName",
+ kind: "text",
+ label: "Product Name",
+ placeholder: "Trail Running Jacket",
+ required: true,
+ },
+ {
+ key: "category",
+ kind: "select",
+ label: "Category",
+ required: true,
+ options: [
+ { value: "outerwear", label: "Outerwear" },
+ { value: "footwear", label: "Footwear" },
+ { value: "accessories", label: "Accessories" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Description & Highlights",
+ description: "Tune copy and merchandising bullets.",
+ kind: "form",
+ fields: [
+ {
+ key: "shortDescription",
+ kind: "textarea",
+ label: "Short Description",
+ placeholder: "Lightweight shell with weatherproof finish.",
+ required: true,
+ },
+ {
+ key: "highlightBullet",
+ kind: "text",
+ label: "Highlight Bullet",
+ placeholder: "Breathable 3-layer fabric",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review product edits and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "ecommerce.upload_product_media": {
+ successTitle: "Product media workflow simulated",
+ successMessage:
+ "Media upload details were captured for image pipeline handoff.",
+ steps: [
+ {
+ title: "Media Source",
+ description: "Select media input details.",
+ kind: "form",
+ fields: [
+ {
+ key: "mediaType",
+ kind: "select",
+ label: "Media Type",
+ required: true,
+ options: [
+ { value: "image", label: "Image" },
+ { value: "video", label: "Video" },
+ { value: "360", label: "360 view" },
+ ],
+ },
+ {
+ key: "sourcePath",
+ kind: "text",
+ label: "Source URL / Path",
+ placeholder: "https://cdn.example.com/product/sku-4821/front.jpg",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Variants & Alt Text",
+ description: "Map to variants and accessibility copy.",
+ kind: "form",
+ fields: [
+ {
+ key: "variantMap",
+ kind: "text",
+ label: "Variant Mapping",
+ placeholder: "color:black,size:m",
+ required: true,
+ },
+ {
+ key: "altText",
+ kind: "textarea",
+ label: "Alt Text",
+ placeholder: "Front view of black trail running jacket on white background.",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review media updates and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "ecommerce.update_product_copy": {
+ successTitle: "Product copy workflow simulated",
+ successMessage:
+ "Content edits were captured with SEO and tone guidance.",
+ steps: [
+ {
+ title: "Copy Focus",
+ description: "Choose which copy section to revise.",
+ kind: "form",
+ fields: [
+ {
+ key: "copySection",
+ kind: "select",
+ label: "Copy Section",
+ required: true,
+ options: [
+ { value: "headline", label: "Headline" },
+ { value: "description", label: "Description" },
+ { value: "bullets", label: "Feature bullets" },
+ ],
+ },
+ {
+ key: "draftCopy",
+ kind: "textarea",
+ label: "Draft Copy",
+ placeholder: "Write the updated product copy.",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Tone & SEO",
+ description: "Set tone and search target phrase.",
+ kind: "form",
+ fields: [
+ {
+ key: "copyTone",
+ kind: "select",
+ label: "Tone",
+ required: true,
+ options: [
+ { value: "premium", label: "Premium" },
+ { value: "practical", label: "Practical" },
+ { value: "energetic", label: "Energetic" },
+ ],
+ },
+ {
+ key: "seoPhrase",
+ kind: "text",
+ label: "SEO Phrase",
+ placeholder: "lightweight waterproof running jacket",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review content changes and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "healthcare.check_in_issue": {
+ successTitle: "Check-in issue workflow simulated",
+ successMessage:
+ "Non-PHI intake issue details are prepared for front-desk routing.",
+ steps: [
+ {
+ title: "Issue Type",
+ description: "Classify the check-in issue.",
+ kind: "form",
+ fields: [
+ {
+ key: "checkinIssueType",
+ kind: "select",
+ label: "Issue Type",
+ required: true,
+ options: [
+ { value: "insurance", label: "Insurance mismatch" },
+ { value: "identity", label: "Identity verification" },
+ { value: "appointment", label: "Appointment mismatch" },
+ ],
+ },
+ {
+ key: "deskQueue",
+ kind: "select",
+ label: "Front Desk Queue",
+ required: true,
+ options: [
+ { value: "intake", label: "Intake Queue" },
+ { value: "billing", label: "Billing Queue" },
+ { value: "triage", label: "Triage Queue" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Patient Context",
+ description: "Capture non-PHI context fields.",
+ kind: "form",
+ fields: [
+ {
+ key: "patientToken",
+ kind: "text",
+ label: "Patient Token",
+ placeholder: "PT-80321",
+ required: true,
+ },
+ {
+ key: "contextNotes",
+ kind: "textarea",
+ label: "Context Notes",
+ placeholder: "No names or DOB. Include only operational context.",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review details and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "healthcare.flag_urgent": {
+ successTitle: "Urgent flag workflow simulated",
+ successMessage:
+ "Urgency classification and notifications are prepared for routing.",
+ steps: [
+ {
+ title: "Urgency Classification",
+ description: "Define urgency and risk posture.",
+ kind: "form",
+ fields: [
+ {
+ key: "urgencyLevel",
+ kind: "select",
+ label: "Urgency Level",
+ required: true,
+ options: [
+ { value: "stat", label: "STAT" },
+ { value: "high", label: "High" },
+ { value: "moderate", label: "Moderate" },
+ ],
+ },
+ {
+ key: "riskCategory",
+ kind: "select",
+ label: "Risk Category",
+ required: true,
+ options: [
+ { value: "safety", label: "Patient safety" },
+ { value: "compliance", label: "Compliance" },
+ { value: "operational", label: "Operational" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Notification Route",
+ description: "Choose whom to notify.",
+ kind: "form",
+ fields: [
+ {
+ key: "notifyGroup",
+ kind: "select",
+ label: "Notify Group",
+ required: true,
+ options: [
+ { value: "oncall", label: "On-call clinician" },
+ { value: "charge", label: "Charge nurse" },
+ { value: "ops", label: "Clinical operations" },
+ ],
+ },
+ {
+ key: "responseWindow",
+ kind: "select",
+ label: "Response Window",
+ required: true,
+ options: [
+ { value: "5m", label: "5 minutes" },
+ { value: "15m", label: "15 minutes" },
+ { value: "30m", label: "30 minutes" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review details and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "healthcare.vital_alert": {
+ successTitle: "Vital alert workflow simulated",
+ successMessage:
+ "Vital threshold and action details were captured for triage.",
+ steps: [
+ {
+ title: "Vital Entry",
+ description: "Document observed vital values.",
+ kind: "form",
+ fields: [
+ {
+ key: "vitalType",
+ kind: "select",
+ label: "Vital Type",
+ required: true,
+ options: [
+ { value: "hr", label: "Heart rate" },
+ { value: "bp", label: "Blood pressure" },
+ { value: "spo2", label: "SpO2" },
+ { value: "temp", label: "Temperature" },
+ ],
+ },
+ {
+ key: "vitalValue",
+ kind: "text",
+ label: "Observed Value",
+ placeholder: "e.g. 88/54",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Clinical Action",
+ description: "Define immediate clinical response.",
+ kind: "form",
+ fields: [
+ {
+ key: "clinicalAction",
+ kind: "select",
+ label: "Clinical Action",
+ required: true,
+ options: [
+ { value: "notify", label: "Notify provider" },
+ { value: "repeat", label: "Repeat measurement" },
+ { value: "rapid", label: "Call rapid response" },
+ ],
+ },
+ {
+ key: "actionNotes",
+ kind: "textarea",
+ label: "Action Notes",
+ placeholder: "Document immediate next steps.",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review details and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "healthcare.verify_identity_token": {
+ successTitle: "Identity verification workflow simulated",
+ successMessage:
+ "Verification and mismatch details were prepared using non-PHI context.",
+ steps: [
+ {
+ title: "Verification Source",
+ description: "Capture token verification source.",
+ kind: "form",
+ fields: [
+ {
+ key: "verificationSource",
+ kind: "select",
+ label: "Source",
+ required: true,
+ options: [
+ { value: "kiosk", label: "Check-in kiosk" },
+ { value: "frontdesk", label: "Front desk scan" },
+ { value: "portal", label: "Patient portal token" },
+ ],
+ },
+ {
+ key: "verificationMode",
+ kind: "select",
+ label: "Verification Mode",
+ required: true,
+ options: [
+ { value: "qr", label: "QR token" },
+ { value: "wristband", label: "Wristband barcode" },
+ { value: "manual", label: "Manual confirmation token" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Mismatch Details",
+ description: "Document mismatch details (non-PHI only).",
+ kind: "form",
+ fields: [
+ {
+ key: "patientTokenRef",
+ kind: "text",
+ label: "Patient Token Reference",
+ placeholder: "PT-TOKEN-49211",
+ required: true,
+ },
+ {
+ key: "mismatchDetails",
+ kind: "textarea",
+ label: "Mismatch Details",
+ placeholder:
+ "Use operational details only. Do not include patient name, DOB, or MRN.",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "healthcare.coverage_exception": {
+ successTitle: "Coverage exception workflow simulated",
+ successMessage:
+ "Coverage and authorization routing details were staged with context.",
+ steps: [
+ {
+ title: "Payer & Trigger",
+ description: "Capture coverage trigger details.",
+ kind: "form",
+ fields: [
+ {
+ key: "payerType",
+ kind: "select",
+ label: "Payer",
+ required: true,
+ options: [
+ { value: "commercial", label: "Commercial" },
+ { value: "medicare", label: "Medicare" },
+ { value: "medicaid", label: "Medicaid" },
+ ],
+ },
+ {
+ key: "exceptionTrigger",
+ kind: "select",
+ label: "Eligibility Trigger",
+ required: true,
+ options: [
+ { value: "inactive", label: "Inactive coverage" },
+ { value: "auth", label: "Authorization required" },
+ { value: "benefit", label: "Benefit limitation" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Authorization Path",
+ description: "Choose routing and follow-up path.",
+ kind: "form",
+ fields: [
+ {
+ key: "authorizationQueue",
+ kind: "select",
+ label: "Authorization Queue",
+ required: true,
+ options: [
+ { value: "billing-auth", label: "Billing authorization" },
+ { value: "clinical-auth", label: "Clinical authorization" },
+ { value: "supervisor", label: "Supervisor review" },
+ ],
+ },
+ {
+ key: "coverageNotes",
+ kind: "textarea",
+ label: "Operational Notes",
+ placeholder:
+ "Use token references and workflow context only. Exclude PHI.",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "healthcare.request_vital_recheck": {
+ successTitle: "Vital recheck workflow simulated",
+ successMessage:
+ "Recheck timing and ownership were captured for bedside follow-up.",
+ steps: [
+ {
+ title: "Vital & Threshold",
+ description: "Define concerning vital threshold.",
+ kind: "form",
+ fields: [
+ {
+ key: "recheckVitalType",
+ kind: "select",
+ label: "Vital Type",
+ required: true,
+ options: [
+ { value: "hr", label: "Heart rate" },
+ { value: "bp", label: "Blood pressure" },
+ { value: "spo2", label: "SpO2" },
+ { value: "temp", label: "Temperature" },
+ ],
+ },
+ {
+ key: "thresholdRule",
+ kind: "text",
+ label: "Threshold Rule",
+ placeholder: "HR > 120 for 5 min",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Recheck Timing & Owner",
+ description: "Assign recheck timing and owner.",
+ kind: "form",
+ fields: [
+ {
+ key: "recheckWindow",
+ kind: "select",
+ label: "Recheck Window",
+ required: true,
+ options: [
+ { value: "5m", label: "5 minutes" },
+ { value: "10m", label: "10 minutes" },
+ { value: "15m", label: "15 minutes" },
+ ],
+ },
+ {
+ key: "recheckOwner",
+ kind: "select",
+ label: "Owner",
+ required: true,
+ options: [
+ { value: "primary-rn", label: "Primary RN" },
+ { value: "charge-rn", label: "Charge RN" },
+ { value: "rapid-team", label: "Rapid response team" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "healthcare.open_trend_review": {
+ successTitle: "Trend review workflow simulated",
+ successMessage:
+ "Trend review request and clinical context were prepared for analysis.",
+ steps: [
+ {
+ title: "Trend Window",
+ description: "Select time range for trend review.",
+ kind: "form",
+ fields: [
+ {
+ key: "trendWindow",
+ kind: "select",
+ label: "Trend Window",
+ required: true,
+ options: [
+ { value: "30m", label: "Last 30 minutes" },
+ { value: "2h", label: "Last 2 hours" },
+ { value: "24h", label: "Last 24 hours" },
+ ],
+ },
+ {
+ key: "trendFocus",
+ kind: "select",
+ label: "Focus",
+ required: true,
+ options: [
+ { value: "instability", label: "Instability pattern" },
+ { value: "response", label: "Treatment response" },
+ { value: "artifact", label: "Potential monitor artifact" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Clinical Context",
+ description: "Capture non-PHI context notes.",
+ kind: "form",
+ fields: [
+ {
+ key: "clinicalContextNotes",
+ kind: "textarea",
+ label: "Clinical Context Notes",
+ placeholder:
+ "Operational context only. Use unit/bed tokens and no patient identifiers.",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "healthcare.notify_care_team": {
+ successTitle: "Care team notification workflow simulated",
+ successMessage:
+ "Care team notification and urgency context were prepared for routing.",
+ steps: [
+ {
+ title: "Team Selection",
+ description: "Select notification recipients.",
+ kind: "form",
+ fields: [
+ {
+ key: "careTeamTarget",
+ kind: "select",
+ label: "Care Team",
+ required: true,
+ options: [
+ { value: "primary-team", label: "Primary care team" },
+ { value: "consult-team", label: "Consult team" },
+ { value: "oncall-team", label: "On-call coverage" },
+ ],
+ },
+ {
+ key: "notifyChannel",
+ kind: "select",
+ label: "Channel",
+ required: true,
+ options: [
+ { value: "ehr-inbox", label: "EHR inbox (demo)" },
+ { value: "secure-chat", label: "Secure chat (demo)" },
+ { value: "unit-board", label: "Unit board task" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Message & Priority",
+ description: "Set priority and write handoff message.",
+ kind: "form",
+ fields: [
+ {
+ key: "notifyPriority",
+ kind: "select",
+ label: "Priority",
+ required: true,
+ options: [
+ { value: "routine", label: "Routine" },
+ { value: "high", label: "High" },
+ { value: "urgent", label: "Urgent" },
+ ],
+ },
+ {
+ key: "notifyMessage",
+ kind: "textarea",
+ label: "Message",
+ placeholder:
+ "Use operational context and token references only (no PHI).",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "healthcare.escalate_handoff": {
+ successTitle: "Handoff escalation workflow simulated",
+ successMessage:
+ "Handoff escalation target and SLA requirements were captured.",
+ steps: [
+ {
+ title: "Handoff Target",
+ description: "Choose escalation destination.",
+ kind: "form",
+ fields: [
+ {
+ key: "handoffTarget",
+ kind: "select",
+ label: "Target Team",
+ required: true,
+ options: [
+ { value: "charge-nurse", label: "Charge nurse" },
+ { value: "hospitalist", label: "Hospitalist coverage" },
+ { value: "rapid-response", label: "Rapid response coordinator" },
+ ],
+ },
+ {
+ key: "handoffReason",
+ kind: "textarea",
+ label: "Escalation Reason",
+ placeholder:
+ "Summarize operational urgency and context with non-PHI details.",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Risk & SLA",
+ description: "Define risk posture and response timeline.",
+ kind: "form",
+ fields: [
+ {
+ key: "handoffRisk",
+ kind: "select",
+ label: "Risk Level",
+ required: true,
+ options: [
+ { value: "moderate", label: "Moderate" },
+ { value: "high", label: "High" },
+ { value: "critical", label: "Critical" },
+ ],
+ },
+ {
+ key: "handoffSla",
+ kind: "select",
+ label: "SLA Target",
+ required: true,
+ options: [
+ { value: "10m", label: "10 minutes" },
+ { value: "30m", label: "30 minutes" },
+ { value: "60m", label: "60 minutes" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "social.flag_content": {
+ successTitle: "Content flag workflow simulated",
+ successMessage:
+ "Moderation policy and enforcement intent were captured with context.",
+ steps: [
+ {
+ title: "Policy Category",
+ description: "Choose applicable policy category.",
+ kind: "form",
+ fields: [
+ {
+ key: "policyCategory",
+ kind: "select",
+ label: "Policy Category",
+ required: true,
+ options: [
+ { value: "harassment", label: "Harassment" },
+ { value: "misinfo", label: "Misinformation" },
+ { value: "spam", label: "Spam" },
+ { value: "other", label: "Other" },
+ ],
+ },
+ {
+ key: "flagNotes",
+ kind: "textarea",
+ label: "Flag Notes",
+ placeholder: "Add reviewer context.",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Enforcement Action",
+ description: "Choose moderation action.",
+ kind: "form",
+ fields: [
+ {
+ key: "enforcementAction",
+ kind: "select",
+ label: "Action",
+ required: true,
+ options: [
+ { value: "hide", label: "Hide content" },
+ { value: "warn", label: "Warn account" },
+ { value: "escalate", label: "Escalate for review" },
+ ],
+ },
+ {
+ key: "reviewQueue",
+ kind: "select",
+ label: "Review Queue",
+ required: true,
+ options: [
+ { value: "safety", label: "Safety queue" },
+ { value: "trust", label: "Trust queue" },
+ { value: "legal", label: "Legal queue" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review details and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "social.quick_reply": {
+ successTitle: "Quick reply workflow simulated",
+ successMessage:
+ "Reply draft and moderation guardrails were captured.",
+ steps: [
+ {
+ title: "Draft Reply",
+ description: "Compose contextual response.",
+ kind: "form",
+ fields: [
+ {
+ key: "draftReply",
+ kind: "textarea",
+ label: "Draft Reply",
+ placeholder: "Write the reply that should be suggested.",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Tone & Approval",
+ description: "Set tone and approval requirements.",
+ kind: "form",
+ fields: [
+ {
+ key: "replyTone",
+ kind: "select",
+ label: "Tone",
+ required: true,
+ options: [
+ { value: "friendly", label: "Friendly" },
+ { value: "formal", label: "Formal" },
+ { value: "empathetic", label: "Empathetic" },
+ ],
+ },
+ {
+ key: "approvalLevel",
+ kind: "select",
+ label: "Approval",
+ required: true,
+ options: [
+ { value: "auto", label: "Auto-send" },
+ { value: "manager", label: "Manager approval" },
+ { value: "legal", label: "Legal approval" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review details and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "social.save_asset": {
+ successTitle: "Save asset workflow simulated",
+ successMessage:
+ "Asset destination and metadata tagging were captured for handoff.",
+ steps: [
+ {
+ title: "Destination",
+ description: "Pick where the asset should be stored.",
+ kind: "form",
+ fields: [
+ {
+ key: "assetDestination",
+ kind: "select",
+ label: "Destination",
+ required: true,
+ options: [
+ { value: "campaign", label: "Campaign library" },
+ { value: "brand", label: "Brand vault" },
+ { value: "draft", label: "Draft workspace" },
+ ],
+ },
+ {
+ key: "assetName",
+ kind: "text",
+ label: "Asset Name",
+ placeholder: "holiday-launch-variant-b",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Metadata & Tags",
+ description: "Attach categorization metadata.",
+ kind: "form",
+ fields: [
+ {
+ key: "assetTags",
+ kind: "text",
+ label: "Tags",
+ placeholder: "holiday,retargeting,ugc",
+ required: true,
+ },
+ {
+ key: "usageRights",
+ kind: "select",
+ label: "Usage Rights",
+ required: true,
+ options: [
+ { value: "internal", label: "Internal" },
+ { value: "paid", label: "Paid media" },
+ { value: "global", label: "Global unrestricted" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Confirm",
+ description: "Review details and complete.",
+ kind: "review",
+ },
+ ],
+ },
+ "software.send_to_cursor": {
+ successTitle: "Cursor handoff workflow simulated",
+ successMessage:
+ "Fix scope, prompt, and constraints are staged for Cursor integration.",
+ steps: [
+ {
+ title: "Fix Scope",
+ description: "Select scope and expected output.",
+ kind: "form",
+ fields: [
+ {
+ key: "fixScope",
+ kind: "select",
+ label: "Fix Scope",
+ required: true,
+ options: [
+ { value: "element", label: "Selected element only" },
+ { value: "container", label: "Container component" },
+ { value: "page", label: "Whole page" },
+ ],
+ },
+ {
+ key: "impact",
+ kind: "select",
+ label: "Impact",
+ required: true,
+ options: [
+ { value: "blocker", label: "Blocker" },
+ { value: "high", label: "High" },
+ { value: "medium", label: "Medium" },
+ ],
+ },
+ ],
+ },
+ {
+ title: "Prompt & Constraints",
+ description: "Specify agent prompt and boundaries.",
+ kind: "form",
+ fields: [
+ {
+ key: "cursorPrompt",
+ kind: "textarea",
+ label: "Prompt",
+ placeholder: "Describe the fix that should be generated.",
+ required: true,
+ },
+ {
+ key: "branchName",
+ kind: "text",
+ label: "Branch",
+ placeholder: "codex/fix-login-button",
+ required: true,
+ },
+ {
+ key: "constraints",
+ kind: "textarea",
+ label: "Constraints",
+ placeholder: "Do not modify API contracts. Keep visual styles stable.",
+ required: false,
+ },
+ ],
+ },
+ {
+ title: "Review",
+ description: "Review payload and context before send.",
+ kind: "review",
+ },
+ ],
+ },
+};
+
+const INITIAL_FORM_VALUES: Partial>> =
+ {
+ "software.report_bug": {
+ ticketSystem: "github",
+ severity: "medium",
+ labels: "bug,homepage",
+ },
+ "software.copy_selector": {
+ selectorFormat: "css",
+ },
+ "software.send_to_cursor": {
+ branchName: "codex/fix-ui-feedback",
+ },
+ };
+
+const DEFAULT_BUG_SUMMARY_PLACEHOLDER =
+ "Observed UI behavior does not match expected interaction.";
+
+function isGenericAction(actionId: WorkflowActionId): actionId is GenericActionId {
+ return actionId !== "software.report_bug" && actionId !== "software.copy_selector";
+}
+
+function defaultSummaryFromCapture(capture: WorkflowCaptureData | null): string {
+ if (!capture) {
+ return "Observed UI behavior does not match expected interaction.";
+ }
+
+ const text = capture.targetContext?.innerText?.trim();
+ if (text) {
+ return `Observed unexpected behavior on \"${text.slice(0, 80)}\" when interacting in ${capture.pageContext.title}.`;
+ }
+
+ return `Observed unexpected behavior on ${capture.targetSelector} in ${capture.pageContext.title}.`;
+}
+
+function formatFormValue(value: string): string {
+ if (!value) return "Not provided";
+ return value;
+}
+
+function parseLabels(csvValue: string): string[] {
+ return csvValue
+ .split(",")
+ .map((label) => label.trim())
+ .filter(Boolean);
+}
+
+async function writeClipboard(text: string): Promise {
+ if (!navigator.clipboard) {
+ throw new Error("Clipboard API not available");
+ }
+ await navigator.clipboard.writeText(text);
+}
+
+function buildMetadata(
+ launch: WorkflowLaunchState,
+ formState: Record,
+ capture: WorkflowCaptureData | null,
+) {
+ return {
+ routing: {
+ adapter: "github",
+ },
+ workflow: {
+ actionId: launch.action.id,
+ actionLabel: launch.action.menuLabel,
+ source: "homepage-immersive-workstream",
+ ticketSystem: formState.ticketSystem,
+ workstream: launch.action.workstream,
+ severity: formState.severity,
+ labels: parseLabels(formState.labels || ""),
+ },
+ context: {
+ containerSelector: capture?.containerSelector || "unknown",
+ targetSelector: capture?.targetSelector || "unknown",
+ pageTitle: capture?.pageContext.title || "",
+ },
+ github: {
+ title: formState.bugTitle,
+ },
+ };
+}
+
+function buildScreenshots(capture: WorkflowCaptureData | null): ScreenshotData | undefined {
+ if (!capture) return undefined;
+
+ const screenshots: ScreenshotData = {
+ capturedAt: capture.capturedAt,
+ };
+
+ if (capture.containerScreenshot) {
+ screenshots.container = capture.containerScreenshot;
+ }
+
+ if (capture.screenshotError) {
+ screenshots.errors = {
+ container: {
+ message: capture.screenshotError,
+ name: "SCREENSHOT_CAPTURE_ERROR",
+ },
+ };
+ }
+
+ if (!screenshots.container && !screenshots.errors) {
+ return undefined;
+ }
+
+ return screenshots;
+}
+
+function FieldControl({
+ field,
+ value,
+ onChange,
+}: {
+ field: FieldConfig;
+ onChange: (value: string) => void;
+ value: string;
+}) {
+ return (
+
+
+ {field.label}
+
+ {field.kind === "textarea" ? (
+
+ );
+}
+
+export function WorkflowFlowRenderer({
+ captureState,
+ launch,
+ onClose,
+ onRetryCapture,
+}: WorkflowFlowRendererProps) {
+ const [stepIndex, setStepIndex] = useState(0);
+ const [formState, setFormState] = useState>({});
+ const [stepError, setStepError] = useState(null);
+ const [submitError, setSubmitError] = useState(null);
+ const [successState, setSuccessState] = useState(
+ null,
+ );
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [copyStatus, setCopyStatus] = useState("");
+
+ const action = launch.action;
+ const captureData = captureState.data;
+
+ const selectorBundle = useMemo(() => {
+ if (!captureData) return null;
+ return buildSelectorBundle(captureData);
+ }, [captureData]);
+
+ const genericWorkflow = isGenericAction(action.id)
+ ? GENERIC_WORKFLOWS[action.id]
+ : null;
+
+ useEffect(() => {
+ const defaults = {
+ ...(INITIAL_FORM_VALUES[action.id] || {}),
+ };
+
+ if (action.id === "software.report_bug") {
+ defaults.bugSummary = DEFAULT_BUG_SUMMARY_PLACEHOLDER;
+ }
+
+ setFormState(defaults);
+ setStepIndex(0);
+ setStepError(null);
+ setSubmitError(null);
+ setSuccessState(null);
+ setIsSubmitting(false);
+ setCopyStatus("");
+ }, [action.id, launch.openedAt]);
+
+ useEffect(() => {
+ if (action.id !== "software.report_bug") return;
+ if (!captureData) return;
+
+ setFormState((current) => {
+ if (current.bugTitle) {
+ return current;
+ }
+
+ return {
+ ...current,
+ bugTitle: generateHeuristicIssueTitle(captureData),
+ bugSummary:
+ !current.bugSummary ||
+ current.bugSummary === DEFAULT_BUG_SUMMARY_PLACEHOLDER
+ ? defaultSummaryFromCapture(captureData)
+ : current.bugSummary,
+ };
+ });
+ }, [action.id, captureData]);
+
+ const setField = (key: string, value: string) => {
+ setFormState((current) => ({
+ ...current,
+ [key]: value,
+ }));
+ };
+
+ const isLastStep = stepIndex === action.steps.length - 1;
+
+ const selectedCopyValue = useMemo(() => {
+ if (!selectorBundle) return "";
+ const selectorFormat = formState.selectorFormat || "css";
+
+ if (selectorFormat === "xpath") return selectorBundle.xpath;
+ if (selectorFormat === "testid") return selectorBundle.testId;
+ return selectorBundle.css;
+ }, [formState.selectorFormat, selectorBundle]);
+
+ const validateCurrentStep = (): string | null => {
+ if (action.id === "software.report_bug") {
+ if (stepIndex === 0 && !formState.ticketSystem) {
+ return "Select a ticket system.";
+ }
+
+ if (stepIndex === 1) {
+ if (formState.ticketSystem === "github") {
+ if (!formState.bugTitle?.trim()) return "Issue title is required.";
+ if (!formState.bugSummary?.trim()) {
+ return "Issue summary is required.";
+ }
+ } else {
+ if (!formState.jiraProject?.trim()) return "Project key is required.";
+ if (!formState.jiraIssueType?.trim()) {
+ return "Issue type is required.";
+ }
+ }
+ }
+
+ return null;
+ }
+
+ if (action.id === "software.copy_selector") {
+ return null;
+ }
+
+ if (!genericWorkflow) {
+ return null;
+ }
+
+ const step = genericWorkflow.steps[stepIndex];
+ if (!step || step.kind === "review") {
+ return null;
+ }
+
+ for (const field of step.fields || []) {
+ if (!field.required) continue;
+ const value = formState[field.key] || "";
+ if (!value.trim()) {
+ return `${field.label} is required.`;
+ }
+ }
+
+ return null;
+ };
+
+ const completeAsDemo = (title?: string, message?: string) => {
+ if (isGenericAction(action.id) && !title && !message) {
+ setSuccessState({
+ demo: true,
+ title: GENERIC_WORKFLOWS[action.id].successTitle,
+ message: GENERIC_WORKFLOWS[action.id].successMessage,
+ });
+ return;
+ }
+
+ if (action.id === "software.copy_selector" && !title && !message) {
+ setSuccessState({
+ demo: true,
+ title: "Selector bundle ready",
+ message:
+ "Selector output and snippet are prepared. In production this would route to adapter actions.",
+ });
+ return;
+ }
+
+ setSuccessState({
+ demo: true,
+ title: title || "Workflow simulation complete",
+ message:
+ message ||
+ "This workflow is configured as a demo and is ready for adapter integration.",
+ });
+ };
+
+ const submitSoftwareBugToGitHub = async () => {
+ if (!launch.targetElement) {
+ setSubmitError("No target element available for GitHub issue creation.");
+ return;
+ }
+
+ setIsSubmitting(true);
+ setSubmitError(null);
+
+ try {
+ const payload: AnyclickPayload = buildAnyclickPayload(
+ launch.targetElement,
+ "issue",
+ {
+ comment: formState.bugSummary,
+ metadata: buildMetadata(launch, formState, captureData),
+ },
+ );
+
+ const screenshots = buildScreenshots(captureData);
+ if (screenshots) {
+ payload.screenshots = screenshots;
+ }
+
+ const response = await fetch("/api/feedback/github", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(payload),
+ });
+
+ const data = (await response.json().catch(() => null)) as
+ | {
+ error?: string;
+ issue?: { htmlUrl?: string; url?: string };
+ success?: boolean;
+ }
+ | null;
+
+ if (!response.ok || !data?.success) {
+ throw new Error(
+ data?.error || `GitHub submission failed with status ${response.status}`,
+ );
+ }
+
+ setSuccessState({
+ demo: false,
+ issueUrl: data.issue?.htmlUrl || data.issue?.url,
+ message:
+ "GitHub issue was created successfully with Anyclick element and container context.",
+ title: "GitHub issue created",
+ });
+ } catch (error) {
+ setSubmitError(
+ error instanceof Error
+ ? error.message
+ : "Failed to create GitHub issue. You can still complete as demo.",
+ );
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const completeWorkflow = async () => {
+ if (action.id === "software.report_bug") {
+ if (formState.ticketSystem === "github") {
+ await submitSoftwareBugToGitHub();
+ } else {
+ completeAsDemo(
+ "Jira workflow simulated",
+ "Jira is marked as needs integration. Workflow steps are captured for adapter wiring.",
+ );
+ }
+ return;
+ }
+
+ completeAsDemo();
+ };
+
+ const handleNext = async () => {
+ setStepError(null);
+
+ const validationError = validateCurrentStep();
+ if (validationError) {
+ setStepError(validationError);
+ return;
+ }
+
+ if (!isLastStep) {
+ setStepIndex((current) => current + 1);
+ return;
+ }
+
+ await completeWorkflow();
+ };
+
+ const handleBack = () => {
+ setStepError(null);
+ setSubmitError(null);
+
+ if (stepIndex === 0) return;
+ setStepIndex((current) => current - 1);
+ };
+
+ const regenerateTitle = () => {
+ if (!captureData) return;
+ setField("bugTitle", generateHeuristicIssueTitle(captureData));
+ };
+
+ const copySelectedOutput = async () => {
+ if (!selectedCopyValue) return;
+
+ try {
+ await writeClipboard(selectedCopyValue);
+ setCopyStatus("Copied selected selector.");
+ } catch {
+ setCopyStatus("Clipboard permission denied.");
+ }
+ };
+
+ const copyBundleSnippet = async () => {
+ if (!selectorBundle) return;
+
+ try {
+ await writeClipboard(selectorBundle.snippet);
+ setCopyStatus("Copied selector bundle snippet.");
+ } catch {
+ setCopyStatus("Clipboard permission denied.");
+ }
+ };
+
+ const renderSoftwareBug = () => {
+ if (stepIndex === 0) {
+ const selectedSystem = formState.ticketSystem || "";
+
+ return (
+
+
+ Choose the ticket system for this bug report workflow.
+
+
+
+ {[{ value: "jira", label: "Jira (needs integration)" }, {
+ value: "github",
+ label: "GitHub",
+ }].map((option) => {
+ const active = selectedSystem === option.value;
+ return (
+ setField("ticketSystem", option.value)}
+ className={`rounded-md border px-3 py-3 text-left text-sm transition-colors ${
+ active
+ ? "text-white"
+ : "border-white/15 bg-white/5 text-gray-200 hover:border-white/30"
+ }`}
+ style={
+ active
+ ? {
+ backgroundColor: "var(--workflow-accent-soft-bg)",
+ borderColor: "var(--workflow-accent-soft-border)",
+ color: "var(--workflow-accent-text)",
+ }
+ : undefined
+ }
+ >
+ {option.label}
+
+ );
+ })}
+
+
+ );
+ }
+
+ if (stepIndex === 1) {
+ if (formState.ticketSystem === "github") {
+ return (
+
+
+ GitHub form auto-populates title from selector and page context.
+
+
+
+
+ AI-generated title
+
+
+ setField("bugTitle", event.target.value)}
+ className="w-full rounded-md border border-white/15 bg-black/25 px-3 py-2 text-sm text-white focus:border-[var(--workflow-accent)] focus:outline-none"
+ />
+
+ Regenerate
+
+
+
+
+
+
+ Summary
+
+
+
+
+ setField("severity", value)}
+ />
+ setField("labels", value)}
+ />
+
+
+ );
+ }
+
+ return (
+
+
+ Jira flow is UI-only in this demo. This step shows the proposed form
+ shape before adapter integration.
+
+
+
setField("jiraProject", value)}
+ />
+
+ setField("jiraIssueType", value)}
+ />
+
+ setField("jiraAssignee", value)}
+ />
+
+ );
+ }
+
+ return (
+
+
+
+ Review
+
+
+
+
Ticket system
+ {formatFormValue(formState.ticketSystem || "")}
+
+
+
Severity
+ {formatFormValue(formState.severity || "")}
+
+
+
Summary
+
+ {formatFormValue(formState.bugSummary || "")}
+
+
+
+
+
+ {submitError && (
+
+
+
+
+
GitHub submission failed
+
{submitError}
+
+
+
+ {formState.ticketSystem === "github" && (
+
+ completeAsDemo(
+ "Completed as demo",
+ "GitHub was unavailable. Workflow completed as demo fallback.",
+ )
+ }
+ className="mt-3 rounded border border-rose-200/50 px-2.5 py-1.5 text-xs font-medium text-rose-100 hover:bg-rose-200/10"
+ >
+ Complete as demo
+
+ )}
+
+ )}
+
+ );
+ };
+
+ const renderSoftwareCopySelector = () => {
+ if (stepIndex === 0) {
+ return (
+
+
+ Select which selector format should be generated for the workflow
+ bundle.
+
+
+
setField("selectorFormat", value)}
+ />
+
+
+ {selectedCopyValue || "Waiting for captured selector..."}
+
+
+ );
+ }
+
+ return (
+
+
+ Copy selector output and integration snippet.
+
+
+
+
+
+ Selected output
+
+
+ {selectedCopyValue || "No selector available"}
+
+
+
+
+
+ Bundle snippet
+
+
+ {selectorBundle?.snippet || "No snippet available"}
+
+
+
+
+
+ {
+ void copySelectedOutput();
+ }}
+ className="inline-flex items-center gap-1 rounded border border-white/20 px-2.5 py-1.5 text-xs text-gray-100 hover:bg-white/10"
+ >
+ Copy selector
+
+ {
+ void copyBundleSnippet();
+ }}
+ className="inline-flex items-center gap-1 rounded border border-white/20 px-2.5 py-1.5 text-xs text-gray-100 hover:bg-white/10"
+ >
+ Copy bundle
+
+
+
+ {copyStatus && (
+
+ {copyStatus}
+
+ )}
+
+ );
+ };
+
+ const renderGenericWorkflow = () => {
+ if (!genericWorkflow) {
+ return null;
+ }
+
+ const step = genericWorkflow.steps[stepIndex];
+ if (!step) {
+ return null;
+ }
+
+ if (step.kind === "review") {
+ const fields = genericWorkflow.steps
+ .flatMap((item) => item.fields || [])
+ .filter((field, index, list) =>
+ list.findIndex((candidate) => candidate.key === field.key) === index,
+ );
+
+ return (
+
+
+ Review
+
+
+ {fields.map((field) => (
+
+
{field.label}
+
+ {formatFormValue(formState[field.key] || "")}
+
+
+ ))}
+
+
+ );
+ }
+
+ return (
+
+ {(step.fields || []).map((field) => (
+ setField(field.key, value)}
+ />
+ ))}
+
+ );
+ };
+
+ const renderStepContent = () => {
+ if (action.id === "software.report_bug") {
+ return renderSoftwareBug();
+ }
+
+ if (action.id === "software.copy_selector") {
+ return renderSoftwareCopySelector();
+ }
+
+ return renderGenericWorkflow();
+ };
+
+ const currentStepLabel = action.steps[stepIndex];
+
+ const primaryLabel = (() => {
+ if (!isLastStep) {
+ return "Next";
+ }
+
+ if (action.id === "software.report_bug") {
+ return formState.ticketSystem === "github"
+ ? isSubmitting
+ ? "Creating issue..."
+ : "Create GitHub issue"
+ : "Complete as demo";
+ }
+
+ return "Complete demo";
+ })();
+
+ if (successState) {
+ return (
+
+
+
+
+
+
+
{successState.title}
+
+ {successState.message}
+
+
+ {successState.demo ? "Demo mode" : "Live GitHub submission"}
+
+
+ {successState.issueUrl && (
+
+ View issue
+
+ )}
+
+
+
+
+
+
+
+
+
+ Close
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ Step {stepIndex + 1} of {action.steps.length}
+
+ {currentStepLabel?.title}
+
+
+
+
+
+
+
+
+ {currentStepLabel?.title}
+
+
+ {currentStepLabel?.description}
+
+
+
+ {renderStepContent()}
+
+ {stepError && (
+
+ {stepError}
+
+ )}
+
+
+
+
+
+
+ Back
+
+
+ {
+ void handleNext();
+ }}
+ disabled={isSubmitting}
+ className="rounded-md px-3 py-2 text-sm font-medium text-[#061018] disabled:cursor-not-allowed disabled:opacity-60"
+ style={{ backgroundColor: "var(--workflow-accent-strong-bg)" }}
+ >
+ {primaryLabel}
+
+
+
+ );
+}
diff --git a/apps/web/src/components/immersive-workstream/workflows/contextCapture.ts b/apps/web/src/components/immersive-workstream/workflows/contextCapture.ts
new file mode 100644
index 0000000..b3d3252
--- /dev/null
+++ b/apps/web/src/components/immersive-workstream/workflows/contextCapture.ts
@@ -0,0 +1,73 @@
+import {
+ buildElementContext,
+ buildPageContext,
+ captureScreenshot,
+} from "@ewjdev/anyclick-core";
+import type { WorkflowCaptureData } from "./types";
+
+const FALLBACK_SELECTOR = "unknown";
+
+export async function captureWorkflowContext(
+ targetElement: Element | null,
+ containerElement: Element | null,
+): Promise {
+ const pageContext = buildPageContext();
+
+ const targetContext = targetElement
+ ? buildElementContext(targetElement, {
+ maxAncestors: 8,
+ maxInnerTextLength: 600,
+ maxOuterHTMLLength: 2500,
+ })
+ : null;
+
+ const containerContext = containerElement
+ ? buildElementContext(containerElement, {
+ maxAncestors: 8,
+ maxInnerTextLength: 600,
+ maxOuterHTMLLength: 2500,
+ })
+ : null;
+
+ let screenshotError: string | undefined;
+ let containerScreenshot: WorkflowCaptureData["containerScreenshot"];
+
+ if (targetElement) {
+ try {
+ const result = await captureScreenshot(
+ targetElement,
+ containerElement,
+ "container",
+ {
+ maxSizeBytes: 700 * 1024,
+ quality: 0.8,
+ showPreview: false,
+ },
+ );
+
+ if (result.capture) {
+ containerScreenshot = result.capture;
+ }
+
+ if (result.error) {
+ screenshotError = result.error.message;
+ }
+ } catch (error) {
+ screenshotError =
+ error instanceof Error ? error.message : "Failed to capture screenshot";
+ }
+ } else {
+ screenshotError = "No right-click target available.";
+ }
+
+ return {
+ capturedAt: new Date().toISOString(),
+ containerContext,
+ containerSelector: containerContext?.selector || FALLBACK_SELECTOR,
+ containerScreenshot,
+ pageContext,
+ screenshotError,
+ targetContext,
+ targetSelector: targetContext?.selector || FALLBACK_SELECTOR,
+ };
+}
diff --git a/apps/web/src/components/immersive-workstream/workflows/registry.ts b/apps/web/src/components/immersive-workstream/workflows/registry.ts
new file mode 100644
index 0000000..e274c7a
--- /dev/null
+++ b/apps/web/src/components/immersive-workstream/workflows/registry.ts
@@ -0,0 +1,682 @@
+import type {
+ WorkflowActionDefinition,
+ WorkflowActionId,
+ WorkflowWorkstreamId,
+} from "./types";
+
+const WORKFLOW_ACTIONS: WorkflowActionDefinition[] = [
+ {
+ id: "software.report_bug",
+ workstream: "software",
+ menuLabel: "Report this bug",
+ menuType: "software_report_bug",
+ submitMode: "github-or-demo",
+ steps: [
+ {
+ id: "ticket-system",
+ title: "Ticket System",
+ description: "Pick where the bug should be routed.",
+ },
+ {
+ id: "issue-details",
+ title: "Issue Details",
+ description: "Capture title, summary, and routing metadata.",
+ },
+ {
+ id: "review-submit",
+ title: "Review & Submit",
+ description: "Review captured context and submit.",
+ },
+ ],
+ },
+ {
+ id: "software.send_to_cursor",
+ workstream: "software",
+ menuLabel: "Send to Cursor",
+ menuType: "software_send_to_cursor",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "fix-scope",
+ title: "Fix Scope",
+ description: "Select what should be fixed.",
+ },
+ {
+ id: "prompt-constraints",
+ title: "Prompt & Constraints",
+ description: "Tune prompt details for agent handoff.",
+ },
+ {
+ id: "review",
+ title: "Review",
+ description: "Review payload before sending.",
+ },
+ ],
+ },
+ {
+ id: "software.copy_selector",
+ workstream: "software",
+ menuLabel: "Copy selector",
+ menuType: "software_copy_selector",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "selector-format",
+ title: "Selector Format",
+ description: "Choose the selector output format.",
+ },
+ {
+ id: "copy-bundle",
+ title: "Copy Bundle",
+ description: "Copy selector, container, and snippet.",
+ },
+ ],
+ },
+ {
+ id: "ecommerce.item_missing",
+ workstream: "ecommerce",
+ menuLabel: "Item missing",
+ menuType: "ecommerce_item_missing",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "incident-details",
+ title: "Incident Details",
+ description: "Capture order and inventory details.",
+ },
+ {
+ id: "resolution-path",
+ title: "Resolution Path",
+ description: "Select remediation and owner.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review and complete the workflow.",
+ },
+ ],
+ },
+ {
+ id: "ecommerce.shipping_issue",
+ workstream: "ecommerce",
+ menuLabel: "Shipping issue",
+ menuType: "ecommerce_shipping_issue",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "shipment-details",
+ title: "Shipment Details",
+ description: "Capture carrier and order impact.",
+ },
+ {
+ id: "customer-impact",
+ title: "Customer Impact",
+ description: "Define urgency and communication plan.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review and complete the workflow.",
+ },
+ ],
+ },
+ {
+ id: "ecommerce.escalate_order",
+ workstream: "ecommerce",
+ menuLabel: "Escalate order",
+ menuType: "ecommerce_escalate_order",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "escalation-target",
+ title: "Escalation Target",
+ description: "Pick team and escalation reason.",
+ },
+ {
+ id: "priority-sla",
+ title: "Priority & SLA",
+ description: "Set priority and resolution target.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review and complete the workflow.",
+ },
+ ],
+ },
+ {
+ id: "ecommerce.view_stock_details",
+ workstream: "ecommerce",
+ menuLabel: "View stock details",
+ menuType: "ecommerce_view_stock_details",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "inventory-snapshot",
+ title: "Inventory Snapshot",
+ description: "Review stock, reserve, and fulfillment data.",
+ },
+ {
+ id: "source-location",
+ title: "Source Location",
+ description: "Inspect which warehouse feeds this product.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review context and complete.",
+ },
+ ],
+ },
+ {
+ id: "ecommerce.adjust_stock",
+ workstream: "ecommerce",
+ menuLabel: "Adjust stock",
+ menuType: "ecommerce_adjust_stock",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "adjustment-type",
+ title: "Adjustment Type",
+ description: "Select increase, decrease, or reserve release.",
+ },
+ {
+ id: "quantity-reason",
+ title: "Quantity & Reason",
+ description: "Enter quantity delta and operational reason.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review update and complete.",
+ },
+ ],
+ },
+ {
+ id: "ecommerce.set_restock_alert",
+ workstream: "ecommerce",
+ menuLabel: "Set restock alert",
+ menuType: "ecommerce_set_restock_alert",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "threshold",
+ title: "Threshold",
+ description: "Set low-stock threshold for this SKU.",
+ },
+ {
+ id: "notification-route",
+ title: "Notification Route",
+ description: "Pick destination channels and escalation delay.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review alert setup and complete.",
+ },
+ ],
+ },
+ {
+ id: "ecommerce.view_discounts",
+ workstream: "ecommerce",
+ menuLabel: "View discounts",
+ menuType: "ecommerce_view_discounts",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "active-promotions",
+ title: "Active Promotions",
+ description: "Inspect current coupons and promo campaigns.",
+ },
+ {
+ id: "margin-impact",
+ title: "Margin Impact",
+ description: "Review effective margin and revenue impact.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review findings and complete.",
+ },
+ ],
+ },
+ {
+ id: "ecommerce.change_price",
+ workstream: "ecommerce",
+ menuLabel: "Change price",
+ menuType: "ecommerce_change_price",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "new-price",
+ title: "New Price",
+ description: "Set new list price and effective window.",
+ },
+ {
+ id: "guardrails",
+ title: "Guardrails",
+ description: "Validate floor price and approval requirements.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review price update and complete.",
+ },
+ ],
+ },
+ {
+ id: "ecommerce.schedule_promotion",
+ workstream: "ecommerce",
+ menuLabel: "Schedule promotion",
+ menuType: "ecommerce_schedule_promotion",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "promotion-details",
+ title: "Promotion Details",
+ description: "Define campaign type and discount value.",
+ },
+ {
+ id: "timing-audience",
+ title: "Timing & Audience",
+ description: "Set campaign window and customer segment.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review promotion setup and complete.",
+ },
+ ],
+ },
+ {
+ id: "ecommerce.edit_product",
+ workstream: "ecommerce",
+ menuLabel: "Edit product details",
+ menuType: "ecommerce_edit_product",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "core-fields",
+ title: "Core Fields",
+ description: "Update title, brand, and category metadata.",
+ },
+ {
+ id: "description-highlights",
+ title: "Description & Highlights",
+ description: "Refine customer-facing copy and key bullets.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review product edits and complete.",
+ },
+ ],
+ },
+ {
+ id: "ecommerce.upload_product_media",
+ workstream: "ecommerce",
+ menuLabel: "Upload product media",
+ menuType: "ecommerce_upload_product_media",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "media-source",
+ title: "Media Source",
+ description: "Select upload source and media type.",
+ },
+ {
+ id: "variants-alt-text",
+ title: "Variants & Alt Text",
+ description: "Assign variant mapping and accessibility text.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review media update and complete.",
+ },
+ ],
+ },
+ {
+ id: "ecommerce.update_product_copy",
+ workstream: "ecommerce",
+ menuLabel: "Update product copy",
+ menuType: "ecommerce_update_product_copy",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "copy-focus",
+ title: "Copy Focus",
+ description: "Select headline, description, or bullets to update.",
+ },
+ {
+ id: "tone-seo",
+ title: "Tone & SEO",
+ description: "Apply tone direction and SEO target phrase.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review copy updates and complete.",
+ },
+ ],
+ },
+ {
+ id: "healthcare.check_in_issue",
+ workstream: "healthcare",
+ menuLabel: "Check-in issue",
+ menuType: "healthcare_check_in_issue",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "issue-type",
+ title: "Issue Type",
+ description: "Classify the front-desk issue.",
+ },
+ {
+ id: "patient-context",
+ title: "Patient Context",
+ description: "Capture non-PHI context for routing.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review and complete the workflow.",
+ },
+ ],
+ },
+ {
+ id: "healthcare.vital_alert",
+ workstream: "healthcare",
+ menuLabel: "Vital alert",
+ menuType: "healthcare_vital_alert",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "vital-entry",
+ title: "Vital Entry",
+ description: "Record out-of-range vital details.",
+ },
+ {
+ id: "clinical-action",
+ title: "Clinical Action",
+ description: "Select escalation and care response.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review and complete the workflow.",
+ },
+ ],
+ },
+ {
+ id: "healthcare.flag_urgent",
+ workstream: "healthcare",
+ menuLabel: "Flag urgent",
+ menuType: "healthcare_flag_urgent",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "urgency-classification",
+ title: "Urgency Classification",
+ description: "Classify urgency and risk level.",
+ },
+ {
+ id: "notification-route",
+ title: "Notification Route",
+ description: "Select recipients and response window.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review and complete the workflow.",
+ },
+ ],
+ },
+ {
+ id: "healthcare.verify_identity_token",
+ workstream: "healthcare",
+ menuLabel: "Verify identity token",
+ menuType: "healthcare_verify_identity_token",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "verification-source",
+ title: "Verification Source",
+ description: "Choose where verification context was pulled from.",
+ },
+ {
+ id: "mismatch-details",
+ title: "Mismatch Details",
+ description: "Capture non-PHI mismatch details for routing.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review and complete the workflow.",
+ },
+ ],
+ },
+ {
+ id: "healthcare.coverage_exception",
+ workstream: "healthcare",
+ menuLabel: "Coverage exception",
+ menuType: "healthcare_coverage_exception",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "payer-trigger",
+ title: "Payer & Trigger",
+ description: "Record payer and eligibility trigger reason.",
+ },
+ {
+ id: "authorization-path",
+ title: "Authorization Path",
+ description: "Route to the appropriate authorization queue.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review and complete the workflow.",
+ },
+ ],
+ },
+ {
+ id: "healthcare.request_vital_recheck",
+ workstream: "healthcare",
+ menuLabel: "Request vital recheck",
+ menuType: "healthcare_request_vital_recheck",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "vital-threshold",
+ title: "Vital & Threshold",
+ description: "Select the vital and threshold concern.",
+ },
+ {
+ id: "recheck-owner",
+ title: "Recheck Timing & Owner",
+ description: "Set recheck timing and assigned owner.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review and complete the workflow.",
+ },
+ ],
+ },
+ {
+ id: "healthcare.open_trend_review",
+ workstream: "healthcare",
+ menuLabel: "Open trend review",
+ menuType: "healthcare_open_trend_review",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "trend-window",
+ title: "Trend Window",
+ description: "Choose trend time horizon for review.",
+ },
+ {
+ id: "clinical-context",
+ title: "Clinical Context",
+ description: "Add non-PHI clinical context notes.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review and complete the workflow.",
+ },
+ ],
+ },
+ {
+ id: "healthcare.notify_care_team",
+ workstream: "healthcare",
+ menuLabel: "Notify care team",
+ menuType: "healthcare_notify_care_team",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "team-selection",
+ title: "Team Selection",
+ description: "Choose care team recipients.",
+ },
+ {
+ id: "message-priority",
+ title: "Message & Priority",
+ description: "Compose summary and priority level.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review and complete the workflow.",
+ },
+ ],
+ },
+ {
+ id: "healthcare.escalate_handoff",
+ workstream: "healthcare",
+ menuLabel: "Escalate handoff",
+ menuType: "healthcare_escalate_handoff",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "handoff-target",
+ title: "Handoff Target",
+ description: "Select destination team for escalation.",
+ },
+ {
+ id: "risk-sla",
+ title: "Risk & SLA",
+ description: "Define risk posture and response target.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review and complete the workflow.",
+ },
+ ],
+ },
+ {
+ id: "social.save_asset",
+ workstream: "social",
+ menuLabel: "Save asset",
+ menuType: "social_save_asset",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "destination",
+ title: "Destination",
+ description: "Select where the asset should be stored.",
+ },
+ {
+ id: "metadata-tags",
+ title: "Metadata & Tags",
+ description: "Add campaign metadata and tags.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review and complete the workflow.",
+ },
+ ],
+ },
+ {
+ id: "social.flag_content",
+ workstream: "social",
+ menuLabel: "Flag content",
+ menuType: "social_flag_content",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "policy-category",
+ title: "Policy Category",
+ description: "Choose policy violation type.",
+ },
+ {
+ id: "enforcement-action",
+ title: "Enforcement Action",
+ description: "Choose moderation response and queue.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review and complete the workflow.",
+ },
+ ],
+ },
+ {
+ id: "social.quick_reply",
+ workstream: "social",
+ menuLabel: "Quick reply",
+ menuType: "social_quick_reply",
+ submitMode: "demo",
+ steps: [
+ {
+ id: "draft-reply",
+ title: "Draft Reply",
+ description: "Create a contextual response.",
+ },
+ {
+ id: "tone-approval",
+ title: "Tone & Approval",
+ description: "Set tone, risk level, and approvals.",
+ },
+ {
+ id: "confirm",
+ title: "Confirm",
+ description: "Review and complete the workflow.",
+ },
+ ],
+ },
+];
+
+const WORKFLOW_BY_ID = new Map(
+ WORKFLOW_ACTIONS.map((action) => [action.id, action]),
+);
+
+export function isWorkflowWorkstreamId(
+ value: string,
+): value is WorkflowWorkstreamId {
+ return (
+ value === "software" ||
+ value === "ecommerce" ||
+ value === "healthcare" ||
+ value === "social"
+ );
+}
+
+export function getWorkflowActionsForWorkstream(
+ workstream: WorkflowWorkstreamId,
+): WorkflowActionDefinition[] {
+ return WORKFLOW_ACTIONS.filter((action) => action.workstream === workstream);
+}
+
+export function getWorkflowActionById(
+ actionId: WorkflowActionId,
+): WorkflowActionDefinition {
+ const action = WORKFLOW_BY_ID.get(actionId);
+ if (!action) {
+ throw new Error(`Unknown workflow action: ${actionId}`);
+ }
+ return action;
+}
diff --git a/apps/web/src/components/immersive-workstream/workflows/titleHeuristics.ts b/apps/web/src/components/immersive-workstream/workflows/titleHeuristics.ts
new file mode 100644
index 0000000..4163aca
--- /dev/null
+++ b/apps/web/src/components/immersive-workstream/workflows/titleHeuristics.ts
@@ -0,0 +1,149 @@
+import type { WorkflowCaptureData } from "./types";
+
+export interface SelectorBundle {
+ css: string;
+ snippet: string;
+ testId: string;
+ xpath: string;
+}
+
+function compactWhitespace(value: string): string {
+ return value.replace(/\s+/g, " ").trim();
+}
+
+function truncate(value: string, maxLength: number): string {
+ if (value.length <= maxLength) return value;
+ return `${value.slice(0, maxLength - 3)}...`;
+}
+
+function selectorToken(selector: string): string {
+ if (!selector) return "selected element";
+ const clean = selector
+ .replace(/:nth-of-type\(\d+\)/g, "")
+ .replace(/\s*>\s*/g, " ")
+ .trim();
+ const parts = clean.split(" ").filter(Boolean);
+ return truncate(parts[parts.length - 1] || clean, 32);
+}
+
+function extractTargetText(capture: WorkflowCaptureData): string {
+ const targetText = compactWhitespace(capture.targetContext?.innerText || "");
+ if (!targetText) return "";
+ return truncate(targetText, 42);
+}
+
+function extractContainerLabel(capture: WorkflowCaptureData): string {
+ if (capture.containerContext?.id) {
+ return `#${capture.containerContext.id}`;
+ }
+
+ const classes = capture.containerContext?.classes || [];
+ if (classes.length > 0) {
+ return `.${classes[0]}`;
+ }
+
+ return selectorToken(capture.containerSelector);
+}
+
+export function generateHeuristicIssueTitle(
+ capture: WorkflowCaptureData,
+): string {
+ const targetText = extractTargetText(capture);
+ const targetLabel = targetText
+ ? `"${targetText}"`
+ : selectorToken(capture.targetSelector);
+ const containerLabel = extractContainerLabel(capture);
+ const pageLabel = truncate(capture.pageContext.title || "Untitled page", 32);
+
+ return `Bug: ${targetLabel} fails inside ${containerLabel} on ${pageLabel}`;
+}
+
+function selectorSegmentToXPath(segment: string): string {
+ const cleaned = segment.trim();
+ if (!cleaned) return "*";
+
+ const tagMatch = cleaned.match(/^[a-zA-Z][a-zA-Z0-9-]*/);
+ const tag = tagMatch ? tagMatch[0] : "*";
+
+ const idMatch = cleaned.match(/#([a-zA-Z0-9_-]+)/);
+ if (idMatch) {
+ return `${tag}[@id="${idMatch[1]}"]`;
+ }
+
+ const classMatch = cleaned.match(/\.([a-zA-Z0-9_-]+)/);
+ const nthMatch = cleaned.match(/:nth-of-type\((\d+)\)/);
+
+ if (classMatch && nthMatch) {
+ return `${tag}[contains(@class,"${classMatch[1]}")][${nthMatch[1]}]`;
+ }
+
+ if (classMatch) {
+ return `${tag}[contains(@class,"${classMatch[1]}")]`;
+ }
+
+ if (nthMatch) {
+ return `${tag}[${nthMatch[1]}]`;
+ }
+
+ return tag;
+}
+
+function cssToXPath(selector: string): string {
+ const parts = selector
+ .split(">")
+ .map((part) => part.trim())
+ .filter(Boolean)
+ .map(selectorSegmentToXPath);
+
+ if (parts.length === 0) {
+ return "//*";
+ }
+
+ return `//${parts.join("/")}`;
+}
+
+function inferTestIdSelector(capture: WorkflowCaptureData): string {
+ const dataAttrs = capture.targetContext?.dataAttributes || {};
+ const candidateKeys = ["testid", "testId", "qa", "cy", "test"];
+
+ for (const key of candidateKeys) {
+ const value = dataAttrs[key];
+ if (value) {
+ if (key === "testid" || key === "testId") {
+ return `[data-testid="${value}"]`;
+ }
+ return `[data-${key}="${value}"]`;
+ }
+ }
+
+ const primaryClass = capture.targetContext?.classes?.[0];
+ const tag = capture.targetContext?.tag || "element";
+ const fallback = primaryClass ? `${tag}-${primaryClass}` : `${tag}-target`;
+
+ return `[data-testid="${fallback}"]`;
+}
+
+export function buildSelectorBundle(
+ capture: WorkflowCaptureData,
+): SelectorBundle {
+ const css = capture.targetSelector || "unknown";
+ const xpath = cssToXPath(css);
+ const testId = inferTestIdSelector(capture);
+
+ const snippet = [
+ `// Anyclick selector bundle`,
+ `const targetCss = \"${css}\";`,
+ `const targetXpath = \"${xpath}\";`,
+ `const targetTestId = \"${testId}\";`,
+ capture.containerSelector
+ ? `const containerCss = \"${capture.containerSelector}\";`
+ : "const containerCss = \"\";",
+ ].join("\n");
+
+ return {
+ css,
+ snippet,
+ testId,
+ xpath,
+ };
+}
diff --git a/apps/web/src/components/immersive-workstream/workflows/types.ts b/apps/web/src/components/immersive-workstream/workflows/types.ts
new file mode 100644
index 0000000..e458cdc
--- /dev/null
+++ b/apps/web/src/components/immersive-workstream/workflows/types.ts
@@ -0,0 +1,81 @@
+import type {
+ ElementContext,
+ PageContext,
+ ScreenshotCapture,
+} from "@ewjdev/anyclick-core";
+
+export type WorkflowWorkstreamId =
+ | "software"
+ | "ecommerce"
+ | "healthcare"
+ | "social";
+
+export type WorkflowActionId =
+ | "software.report_bug"
+ | "software.send_to_cursor"
+ | "software.copy_selector"
+ | "ecommerce.item_missing"
+ | "ecommerce.shipping_issue"
+ | "ecommerce.escalate_order"
+ | "ecommerce.view_stock_details"
+ | "ecommerce.adjust_stock"
+ | "ecommerce.set_restock_alert"
+ | "ecommerce.view_discounts"
+ | "ecommerce.change_price"
+ | "ecommerce.schedule_promotion"
+ | "ecommerce.edit_product"
+ | "ecommerce.upload_product_media"
+ | "ecommerce.update_product_copy"
+ | "healthcare.check_in_issue"
+ | "healthcare.vital_alert"
+ | "healthcare.flag_urgent"
+ | "healthcare.verify_identity_token"
+ | "healthcare.coverage_exception"
+ | "healthcare.request_vital_recheck"
+ | "healthcare.open_trend_review"
+ | "healthcare.notify_care_team"
+ | "healthcare.escalate_handoff"
+ | "social.save_asset"
+ | "social.flag_content"
+ | "social.quick_reply";
+
+export type WorkflowSubmitMode = "demo" | "github-or-demo";
+
+export interface WorkflowStepDefinition {
+ id: string;
+ title: string;
+ description: string;
+}
+
+export interface WorkflowActionDefinition {
+ id: WorkflowActionId;
+ workstream: WorkflowWorkstreamId;
+ menuLabel: string;
+ menuType: string;
+ submitMode: WorkflowSubmitMode;
+ steps: WorkflowStepDefinition[];
+}
+
+export interface WorkflowLaunchState {
+ action: WorkflowActionDefinition;
+ containerElement: Element | null;
+ openedAt: string;
+ targetElement: Element | null;
+}
+
+export interface WorkflowCaptureData {
+ capturedAt: string;
+ containerContext: ElementContext | null;
+ containerSelector: string;
+ containerScreenshot?: ScreenshotCapture;
+ pageContext: PageContext;
+ screenshotError?: string;
+ targetContext: ElementContext | null;
+ targetSelector: string;
+}
+
+export interface WorkflowCaptureState {
+ data: WorkflowCaptureData | null;
+ error?: string;
+ status: "capturing" | "idle" | "ready";
+}
diff --git a/apps/web/src/components/immersive-workstream/workflows/useWorkflowLauncher.tsx b/apps/web/src/components/immersive-workstream/workflows/useWorkflowLauncher.tsx
new file mode 100644
index 0000000..c201c22
--- /dev/null
+++ b/apps/web/src/components/immersive-workstream/workflows/useWorkflowLauncher.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+import { useCallback, useMemo, useState } from "react";
+import type { ContextMenuItem } from "@ewjdev/anyclick-react";
+import {
+ getWorkflowActionById,
+ getWorkflowActionsForWorkstream,
+} from "./registry";
+import type {
+ WorkflowActionDefinition,
+ WorkflowActionId,
+ WorkflowLaunchState,
+ WorkflowWorkstreamId,
+} from "./types";
+
+interface WorkflowLauncherResult {
+ activeWorkflow: WorkflowLaunchState | null;
+ closeWorkflow: () => void;
+ getMenuItemsForActionIds: (actionIds: WorkflowActionId[]) => ContextMenuItem[];
+ menuItems: ContextMenuItem[];
+}
+
+export function useWorkflowLauncher(
+ workstream: WorkflowWorkstreamId,
+): WorkflowLauncherResult {
+ const [activeWorkflow, setActiveWorkflow] = useState(
+ null,
+ );
+
+ const openWorkflow = useCallback(
+ (nextState: Omit) => {
+ setActiveWorkflow({
+ ...nextState,
+ openedAt: new Date().toISOString(),
+ });
+ },
+ [],
+ );
+
+ const closeWorkflow = useCallback(() => {
+ setActiveWorkflow(null);
+ }, []);
+
+ const buildMenuItems = useCallback(
+ (actions: WorkflowActionDefinition[]): ContextMenuItem[] => {
+ return actions.map((action) => ({
+ label: action.menuLabel,
+ showComment: false,
+ type: action.menuType,
+ onClick: ({ closeMenu, containerElement, targetElement }) => {
+ openWorkflow({
+ action,
+ containerElement,
+ targetElement,
+ });
+
+ closeMenu();
+ return false;
+ },
+ }));
+ },
+ [openWorkflow],
+ );
+
+ const menuItems = useMemo(() => {
+ const actions = getWorkflowActionsForWorkstream(workstream);
+ return buildMenuItems(actions);
+ }, [buildMenuItems, workstream]);
+
+ const getMenuItemsForActionIds = useCallback(
+ (actionIds: WorkflowActionId[]): ContextMenuItem[] => {
+ const actions = actionIds.map((actionId) => getWorkflowActionById(actionId));
+ return buildMenuItems(actions);
+ },
+ [buildMenuItems],
+ );
+
+ return {
+ activeWorkflow,
+ closeWorkflow,
+ getMenuItemsForActionIds,
+ menuItems,
+ };
+}
diff --git a/apps/web/src/data/releases.json b/apps/web/src/data/releases.json
index 93275b8..e216264 100644
--- a/apps/web/src/data/releases.json
+++ b/apps/web/src/data/releases.json
@@ -185,3 +185,5 @@
+
+
diff --git a/apps/web/src/lib/hooks/useFirstTimeInteraction.ts b/apps/web/src/lib/hooks/useFirstTimeInteraction.ts
new file mode 100644
index 0000000..1f98051
--- /dev/null
+++ b/apps/web/src/lib/hooks/useFirstTimeInteraction.ts
@@ -0,0 +1,35 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+const STORAGE_KEY = "anyclick_hero_interaction_complete";
+
+export function useFirstTimeInteraction() {
+ const [hasInteracted, setHasInteracted] = useState(false);
+ const [isClient, setIsClient] = useState(false);
+
+ useEffect(() => {
+ setIsClient(true);
+
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ const stored = localStorage.getItem(STORAGE_KEY);
+ setHasInteracted(stored === "true");
+ }, []);
+
+ const markComplete = () => {
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ localStorage.setItem(STORAGE_KEY, "true");
+ setHasInteracted(true);
+ };
+
+ return {
+ hasInteracted: isClient ? hasInteracted : false,
+ markComplete,
+ };
+}
diff --git a/package.json b/package.json
index db840e9..25be2c2 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
"build": "turbo run build",
"changeset": "changeset",
"clean": "turbo run clean && rm -rf node_modules",
- "dev": "turbo run dev --concurrency=15",
+ "dev": "turbo run dev --concurrency=20",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\" --ignore-path .gitignore",
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\" --ignore-path .gitignore",
"release": "turbo run build && changeset publish",
diff --git a/packages/anyclick-core/CHANGELOG.md b/packages/anyclick-core/CHANGELOG.md
index 3df0875..c6765fb 100644
--- a/packages/anyclick-core/CHANGELOG.md
+++ b/packages/anyclick-core/CHANGELOG.md
@@ -1,5 +1,11 @@
# @ewjdev/anyclick-core
+## 1.5.0
+
+### Minor Changes
+
+- Move `createHttpAdapter` into `@ewjdev/anyclick-core` as the canonical browser-side transport and keep the `@ewjdev/anyclick-github` export as a deprecated compatibility alias.
+
## 1.4.0
### Minor Changes
diff --git a/packages/anyclick-core/README.md b/packages/anyclick-core/README.md
index 8754410..c7d8579 100644
--- a/packages/anyclick-core/README.md
+++ b/packages/anyclick-core/README.md
@@ -49,6 +49,16 @@ await client.submitAnalytic(element, "issue", {
client.detach();
```
+## HTTP Adapter
+
+```typescript
+import { createHttpAdapter } from "@ewjdev/anyclick-core";
+
+const adapter = createHttpAdapter({
+ endpoint: "/api/feedback",
+});
+```
+
## API Reference
### DOM Utilities
@@ -163,7 +173,7 @@ For full documentation, visit [anyclick.ewj.dev/docs/core](https://anyclick.ewj.
## Related Packages
- [`@ewjdev/anyclick-react`](https://www.npmjs.com/package/@ewjdev/anyclick-react) - React provider and UI
-- [`@ewjdev/anyclick-`](https://www.npmjs.com/package/@ewjdev/anyclick-github) - GitHub Issues integration
+- [`@ewjdev/anyclick-github`](https://www.npmjs.com/package/@ewjdev/anyclick-github) - GitHub Issues integration
- [`@ewjdev/anyclick-cursor`](https://www.npmjs.com/package/@ewjdev/anyclick-cursor) - Cursor AI integration
- [`@ewjdev/anyclick-cursor-local`](https://www.npmjs.com/package/@ewjdev/anyclick-cursor-local) - Local Cursor development
diff --git a/packages/anyclick-core/package.json b/packages/anyclick-core/package.json
index e7b0abd..3bc76b4 100644
--- a/packages/anyclick-core/package.json
+++ b/packages/anyclick-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@ewjdev/anyclick-core",
- "version": "1.4.0",
+ "version": "1.5.0",
"private": false,
"description": "Framework-agnostic core library for UI / UX events capture with DOM context",
"keywords": [
diff --git a/packages/anyclick-core/src/httpAdapter.ts b/packages/anyclick-core/src/httpAdapter.ts
new file mode 100644
index 0000000..0432276
--- /dev/null
+++ b/packages/anyclick-core/src/httpAdapter.ts
@@ -0,0 +1,65 @@
+import type { AnyclickAdapter, AnyclickPayload, HttpAdapterOptions } from "./types";
+
+/**
+ * HTTP adapter for browser-side usage.
+ * Sends anyclick payloads to an arbitrary backend endpoint.
+ */
+export class HttpAdapter implements AnyclickAdapter {
+ private endpoint: string;
+ private headers: Record;
+ private method: "POST" | "PUT";
+ private transformPayload?: (
+ payload: AnyclickPayload,
+ ) => Record;
+ private timeout: number;
+
+ constructor(options: HttpAdapterOptions) {
+ this.endpoint = options.endpoint;
+ this.headers = {
+ "Content-Type": "application/json",
+ ...options.headers,
+ };
+ this.method = options.method ?? "POST";
+ this.transformPayload = options.transformPayload;
+ this.timeout = options.timeout ?? 10000;
+ }
+
+ async submitAnyclick(payload: AnyclickPayload): Promise {
+ const body = this.transformPayload
+ ? this.transformPayload(payload)
+ : payload;
+
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
+
+ try {
+ const response = await fetch(this.endpoint, {
+ method: this.method,
+ headers: this.headers,
+ body: JSON.stringify(body),
+ signal: controller.signal,
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text().catch(() => "Unknown error");
+ throw new Error(
+ `Feedback submission failed: ${response.status} ${response.statusText} - ${errorText}`,
+ );
+ }
+ } catch (error) {
+ if (error instanceof Error && error.name === "AbortError") {
+ throw new Error("Feedback submission timed out");
+ }
+ throw error;
+ } finally {
+ clearTimeout(timeoutId);
+ }
+ }
+}
+
+/**
+ * Create an HTTP adapter instance.
+ */
+export function createHttpAdapter(options: HttpAdapterOptions): HttpAdapter {
+ return new HttpAdapter(options);
+}
diff --git a/packages/anyclick-core/src/index.ts b/packages/anyclick-core/src/index.ts
index acbffb6..be77250 100644
--- a/packages/anyclick-core/src/index.ts
+++ b/packages/anyclick-core/src/index.ts
@@ -16,6 +16,7 @@ export type {
ScreenshotConfig,
ScreenshotResult,
ScreenshotError,
+ HttpAdapterOptions,
AnyclickTriggerEvent,
AnyclickMenuEvent,
} from "./types";
@@ -55,6 +56,9 @@ export {
// Client
export { AnyclickClient, createAnyclickClient } from "./client";
+// Generic adapters
+export { HttpAdapter, createHttpAdapter } from "./httpAdapter";
+
// Inspect utilities
export {
CURATED_STYLE_PROPERTIES,
diff --git a/packages/anyclick-core/src/types.ts b/packages/anyclick-core/src/types.ts
index 92ffab0..6078760 100644
--- a/packages/anyclick-core/src/types.ts
+++ b/packages/anyclick-core/src/types.ts
@@ -190,6 +190,22 @@ export interface AnyclickPayload {
screenshots?: ScreenshotData;
}
+/**
+ * Options for the browser-side HTTP adapter
+ */
+export interface HttpAdapterOptions {
+ /** URL of your backend endpoint that handles feedback */
+ endpoint: string;
+ /** Additional headers to include with requests */
+ headers?: Record;
+ /** HTTP method to use (defaults to POST) */
+ method?: "POST" | "PUT";
+ /** Transform the payload before sending (e.g., add auth tokens) */
+ transformPayload?: (payload: AnyclickPayload) => Record;
+ /** Request timeout in milliseconds */
+ timeout?: number;
+}
+
/**
* Adapter interface for submitting anyclick to any backend
*/
diff --git a/packages/anyclick-cursor-local/README.md b/packages/anyclick-cursor-local/README.md
index 026ba9b..b0c7fe1 100644
--- a/packages/anyclick-cursor-local/README.md
+++ b/packages/anyclick-cursor-local/README.md
@@ -90,7 +90,7 @@ Route different feedback types to different adapters:
```typescript
import { createLocalAdapter } from "@ewjdev/anyclick-cursor-local";
-import { createHttpAdapter } from "@ewjdev/anyclick-github";
+import { createHttpAdapter } from "@ewjdev/anyclick-core";
function createRoutingAdapter(config) {
return {
diff --git a/packages/anyclick-github/CHANGELOG.md b/packages/anyclick-github/CHANGELOG.md
index 41d82d5..ad86a02 100644
--- a/packages/anyclick-github/CHANGELOG.md
+++ b/packages/anyclick-github/CHANGELOG.md
@@ -1,5 +1,16 @@
# @ewjdev/anyclick-github
+## 3.1.0
+
+### Minor Changes
+
+- Move `createHttpAdapter` into `@ewjdev/anyclick-core` as the canonical browser-side transport and keep the `@ewjdev/anyclick-github` export as a deprecated compatibility alias.
+
+### Patch Changes
+
+- Updated dependencies
+ - @ewjdev/anyclick-core@1.5.0
+
## 3.0.0
### Patch Changes
diff --git a/packages/anyclick-github/README.md b/packages/anyclick-github/README.md
index 9f12df2..bd1e251 100644
--- a/packages/anyclick-github/README.md
+++ b/packages/anyclick-github/README.md
@@ -7,12 +7,12 @@
## Overview
-`@ewjdev/anyclick-github` provides adapters for integrating anyclick with GitHub Issues. Includes a browser-side HTTP adapter and a server-side GitHub API client.
+`@ewjdev/anyclick-github` provides the server-side GitHub integration for anyclick. The browser-side HTTP adapter now lives in `@ewjdev/anyclick-core`; the old export from `@ewjdev/anyclick-github` remains as a deprecated compatibility alias.
## Installation
```bash
-npm install @ewjdev/anyclick-github
+npm install @ewjdev/anyclick-core @ewjdev/anyclick-github
```
## Setup
@@ -57,11 +57,13 @@ const exists = await github.mediaBranchExists();
### Browser Side
+Use the generic HTTP adapter from `@ewjdev/anyclick-core`:
+
```tsx
"use client";
import { FeedbackProvider } from "@ewjdev/anyclick-react";
-import { createHttpAdapter } from "@ewjdev/anyclick-github";
+import { createHttpAdapter } from "@ewjdev/anyclick-core";
const adapter = createHttpAdapter({
endpoint: "/api/feedback",
@@ -104,7 +106,11 @@ export async function POST(request: Request) {
### HTTP Adapter (Browser)
+Import this from `@ewjdev/anyclick-core`. The legacy `@ewjdev/anyclick-github` export is deprecated.
+
```typescript
+import { createHttpAdapter } from "@ewjdev/anyclick-core";
+
const adapter = createHttpAdapter({
endpoint: "/api/feedback",
headers: {
diff --git a/packages/anyclick-github/package.json b/packages/anyclick-github/package.json
index 3c5ec12..2fc87f0 100644
--- a/packages/anyclick-github/package.json
+++ b/packages/anyclick-github/package.json
@@ -1,8 +1,8 @@
{
"name": "@ewjdev/anyclick-github",
- "version": "3.0.0",
+ "version": "3.1.0",
"private": false,
- "description": "GitHub adapter for UI feedback - includes browser HTTP adapter and Node-side GitHub integration",
+ "description": "GitHub adapter for UI feedback with server-side issue creation and screenshot asset management",
"keywords": [
"feedback",
"ui",
@@ -57,7 +57,7 @@
"setup": "node bin/setup-wrapper.js"
},
"dependencies": {
- "@ewjdev/anyclick-core": "^1.4.0"
+ "@ewjdev/anyclick-core": "^1.5.0"
},
"devDependencies": {
"dotenv": "^16.4.5",
diff --git a/packages/anyclick-github/src/httpAdapter.ts b/packages/anyclick-github/src/httpAdapter.ts
index da75bb9..479fcc6 100644
--- a/packages/anyclick-github/src/httpAdapter.ts
+++ b/packages/anyclick-github/src/httpAdapter.ts
@@ -1,65 +1,15 @@
-import type { AnyclickAdapter, AnyclickPayload } from "@ewjdev/anyclick-core";
+import {
+ HttpAdapter as CoreHttpAdapter,
+} from "@ewjdev/anyclick-core";
import type { HttpAdapterOptions } from "./types";
/**
- * HTTP adapter for browser-side usage
- * Sends anyclick payloads to your backend endpoint
+ * @deprecated Import HttpAdapter from "@ewjdev/anyclick-core" instead.
*/
-export class HttpAdapter implements AnyclickAdapter {
- private endpoint: string;
- private headers: Record;
- private method: "POST" | "PUT";
- private transformPayload?: (
- payload: AnyclickPayload,
- ) => Record;
- private timeout: number;
-
- constructor(options: HttpAdapterOptions) {
- this.endpoint = options.endpoint;
- this.headers = {
- "Content-Type": "application/json",
- ...options.headers,
- };
- this.method = options.method ?? "POST";
- this.transformPayload = options.transformPayload;
- this.timeout = options.timeout ?? 10000;
- }
-
- async submitAnyclick(payload: AnyclickPayload): Promise {
- const body = this.transformPayload
- ? this.transformPayload(payload)
- : payload;
-
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
-
- try {
- const response = await fetch(this.endpoint, {
- method: this.method,
- headers: this.headers,
- body: JSON.stringify(body),
- signal: controller.signal,
- });
-
- if (!response.ok) {
- const errorText = await response.text().catch(() => "Unknown error");
- throw new Error(
- `Feedback submission failed: ${response.status} ${response.statusText} - ${errorText}`,
- );
- }
- } catch (error) {
- if (error instanceof Error && error.name === "AbortError") {
- throw new Error("Feedback submission timed out");
- }
- throw error;
- } finally {
- clearTimeout(timeoutId);
- }
- }
-}
+export class HttpAdapter extends CoreHttpAdapter {}
/**
- * Create an HTTP adapter instance
+ * @deprecated Import createHttpAdapter from "@ewjdev/anyclick-core" instead.
*/
export function createHttpAdapter(options: HttpAdapterOptions): HttpAdapter {
return new HttpAdapter(options);
diff --git a/packages/anyclick-github/src/types.ts b/packages/anyclick-github/src/types.ts
index 779c1a9..6085a54 100644
--- a/packages/anyclick-github/src/types.ts
+++ b/packages/anyclick-github/src/types.ts
@@ -1,7 +1,7 @@
import type { AnyclickPayload } from "@ewjdev/anyclick-core";
/**
- * Options for the HTTP adapter (browser-side)
+ * @deprecated Import HttpAdapterOptions from "@ewjdev/anyclick-core" instead.
*/
export interface HttpAdapterOptions {
/** URL of your backend endpoint that handles feedback */
diff --git a/packages/anyclick-jira/CHANGELOG.md b/packages/anyclick-jira/CHANGELOG.md
index 551f82a..d6f1336 100644
--- a/packages/anyclick-jira/CHANGELOG.md
+++ b/packages/anyclick-jira/CHANGELOG.md
@@ -37,4 +37,3 @@
- Node.js >= 18.18.0
- Jira Cloud instance
- Valid Jira API token with issue creation permissions
-
diff --git a/packages/anyclick-jira/package.json b/packages/anyclick-jira/package.json
index 0e184cb..0ee723e 100644
--- a/packages/anyclick-jira/package.json
+++ b/packages/anyclick-jira/package.json
@@ -61,4 +61,3 @@
"node": ">=18.18.0"
}
}
-
diff --git a/packages/anyclick-pointer/README.md b/packages/anyclick-pointer/README.md
index 738e124..ce9360f 100644
--- a/packages/anyclick-pointer/README.md
+++ b/packages/anyclick-pointer/README.md
@@ -280,7 +280,7 @@ Use alongside `@ewjdev/anyclick-react` for a complete feedback experience:
```tsx
import { FeedbackProvider } from "@ewjdev/anyclick-react";
import { PointerProvider } from "@ewjdev/anyclick-pointer";
-import { createHttpAdapter } from "@ewjdev/anyclick-github";
+import { createHttpAdapter } from "@ewjdev/anyclick-core";
const adapter = createHttpAdapter({ endpoint: "/api/feedback" });
diff --git a/packages/anyclick-pointer/src/PointerProvider.tsx b/packages/anyclick-pointer/src/PointerProvider.tsx
index 210f892..a9b04df 100644
--- a/packages/anyclick-pointer/src/PointerProvider.tsx
+++ b/packages/anyclick-pointer/src/PointerProvider.tsx
@@ -8,6 +8,7 @@ import {
useRef,
useState,
} from "react";
+import { createPortal } from "react-dom";
import { CustomPointer } from "./CustomPointer";
import { POINTER_CONTAINER_ATTR, usePointerStore } from "./pointerStore";
import type {
@@ -75,6 +76,7 @@ export function PointerProvider({
const [dynamicConfig, setDynamicConfig] = useState(
initialConfig,
);
+ const [portalRoot, setPortalRoot] = useState(null);
// Merge theme and config with defaults
const mergedTheme = useMemo(
@@ -197,6 +199,33 @@ export function PointerProvider({
[style, isThisContainerActive],
);
+ useEffect(() => {
+ if (typeof document === "undefined") return;
+ setPortalRoot(document.body);
+ }, []);
+
+ const pointerNode =
+ shouldShowPointer && portalRoot
+ ? createPortal(
+ ,
+ portalRoot,
+ )
+ : shouldShowPointer
+ ? (
+
+ )
+ : null;
+
return (
{children}
- {shouldShowPointer && (
-
- )}
+ {pointerNode}
);
}
diff --git a/packages/anyclick-react-mui/README.md b/packages/anyclick-react-mui/README.md
new file mode 100644
index 0000000..e9e0778
--- /dev/null
+++ b/packages/anyclick-react-mui/README.md
@@ -0,0 +1,20 @@
+# @ewjdev/anyclick-react-mui
+
+MUI companion styling package for `@ewjdev/anyclick-react`.
+
+```tsx
+import { AnyclickProvider } from "@ewjdev/anyclick-react";
+import { MuiAnyclickStyleProvider } from "@ewjdev/anyclick-react-mui";
+import { ThemeProvider, createTheme } from "@mui/material/styles";
+```
+
+Exports:
+
+- `createMuiAnyclickStyleAdapter(options?)`
+- `MuiAnyclickStyleProvider`
+
+Peer dependencies:
+
+- `@mui/material`
+- `@emotion/react`
+- `@emotion/styled`
diff --git a/packages/anyclick-react-mui/package.json b/packages/anyclick-react-mui/package.json
new file mode 100644
index 0000000..2c2c33f
--- /dev/null
+++ b/packages/anyclick-react-mui/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "@ewjdev/anyclick-react-mui",
+ "version": "1.0.0",
+ "private": false,
+ "description": "MUI adapter package for @ewjdev/anyclick-react",
+ "license": "MIT",
+ "author": "ewjdev",
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://registry.npmjs.org/"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/ewjdev/anyclick.git",
+ "directory": "packages/anyclick-react-mui"
+ },
+ "main": "dist/index.js",
+ "module": "dist/index.mjs",
+ "types": "src/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./src/index.d.ts",
+ "require": "./dist/index.js",
+ "import": "./dist/index.mjs"
+ }
+ },
+ "files": [
+ "dist",
+ "src/index.d.ts"
+ ],
+ "scripts": {
+ "build": "tsup src/index.tsx --format esm,cjs --sourcemap --external react,react-dom,@mui/material,@emotion/react,@emotion/styled",
+ "clean": "rimraf dist",
+ "dev": "tsup src/index.tsx --format esm,cjs --sourcemap --external react,react-dom,@mui/material,@emotion/react,@emotion/styled --watch"
+ },
+ "dependencies": {
+ "@ewjdev/anyclick-react": "^5.0.0"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.14.0",
+ "@emotion/styled": "^11.14.0",
+ "@mui/material": "^7.0.0",
+ "react": ">=19.2.1",
+ "react-dom": ">=19.2.1"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.1",
+ "@types/react-dom": "^19.2.1",
+ "rimraf": "^6.0.1",
+ "tsup": "^8.3.5",
+ "typescript": "5.8.3"
+ }
+}
diff --git a/packages/anyclick-react-mui/src/index.d.ts b/packages/anyclick-react-mui/src/index.d.ts
new file mode 100644
index 0000000..8ef0789
--- /dev/null
+++ b/packages/anyclick-react-mui/src/index.d.ts
@@ -0,0 +1,28 @@
+import type { ReactElement } from "react";
+import type {
+ AnyclickStyleAdapter,
+ AnyclickStyleProviderProps,
+} from "@ewjdev/anyclick-react";
+
+export interface MuiAnyclickStyleOptions {
+ density?: "comfortable" | "compact";
+ elevation?: number;
+ useTextField?: boolean;
+ variantMapping?: {
+ button?: "contained" | "outlined" | "text";
+ field?: "filled" | "outlined" | "standard";
+ };
+}
+
+export declare function createMuiAnyclickStyleAdapter(
+ options?: MuiAnyclickStyleOptions,
+): AnyclickStyleAdapter;
+
+export interface MuiAnyclickStyleProviderProps
+ extends AnyclickStyleProviderProps {
+ options?: MuiAnyclickStyleOptions;
+}
+
+export declare function MuiAnyclickStyleProvider(
+ props: MuiAnyclickStyleProviderProps,
+): ReactElement;
diff --git a/packages/anyclick-react-mui/src/index.tsx b/packages/anyclick-react-mui/src/index.tsx
new file mode 100644
index 0000000..b108204
--- /dev/null
+++ b/packages/anyclick-react-mui/src/index.tsx
@@ -0,0 +1,242 @@
+import React, { forwardRef, useMemo } from "react";
+import {
+ AnyclickStyleProvider,
+ type AnyclickBadgeProps,
+ type AnyclickButtonProps,
+ type AnyclickComponentOverrides,
+ type AnyclickInputProps,
+ type AnyclickStyleAdapter,
+ type AnyclickStyleProviderProps,
+ type AnyclickSurfaceProps,
+ type AnyclickTabProps,
+ type AnyclickTabsProps,
+ type AnyclickTextareaProps,
+} from "@ewjdev/anyclick-react";
+import {
+ Box,
+ Button,
+ Chip,
+ CircularProgress,
+ IconButton,
+ Paper,
+ Tab,
+ TextField,
+} from "@mui/material";
+
+export interface MuiAnyclickStyleOptions {
+ density?: "comfortable" | "compact";
+ elevation?: number;
+ useTextField?: boolean;
+ variantMapping?: {
+ button?: "contained" | "outlined" | "text";
+ field?: "filled" | "outlined" | "standard";
+ };
+}
+
+function toneToColor(tone: AnyclickButtonProps["slotState"] extends infer T
+ ? T extends { tone?: infer Tone }
+ ? Tone
+ : never
+ : never) {
+ if (tone === "danger") return "error";
+ if (tone === "success") return "success";
+ if (tone === "warning") return "warning";
+ return "primary";
+}
+
+function createMuiComponentOverrides(
+ options: MuiAnyclickStyleOptions = {},
+): Partial {
+ const density = options.density ?? "comfortable";
+ const elevation = options.elevation ?? 3;
+ const buttonVariant = options.variantMapping?.button ?? "contained";
+ const fieldVariant = options.variantMapping?.field ?? "outlined";
+ const size = density === "compact" ? "small" : "medium";
+
+ const Surface = forwardRef(
+ ({ children, className, slotState, style, ...props }, ref) => (
+
+ {children}
+
+ ),
+ );
+
+ const ActionButton = forwardRef(
+ ({ children, className, slotState, style, ...props }, ref) => (
+ : undefined
+ }
+ style={style}
+ variant={slotState?.tone === "neutral" ? "outlined" : buttonVariant}
+ {...props}
+ >
+ {children}
+
+ ),
+ );
+
+ const ActionIconButton = forwardRef(
+ ({ children, className, slotState, style, ...props }, ref) => (
+
+ {slotState?.loading ? (
+
+ ) : (
+ children
+ )}
+
+ ),
+ );
+
+ const Input = forwardRef(
+ ({ className, slotState, style, ...props }, ref) => (
+
+ ),
+ );
+
+ const Textarea = forwardRef(
+ ({ className, slotState, style, ...props }, ref) => (
+
+ ),
+ );
+
+ const Tabs = forwardRef(
+ ({ children, className, style, ...props }, ref) => (
+
+ {children}
+
+ ),
+ );
+
+ const TabButton = forwardRef(
+ ({ children, className, slotState, style, ...props }, ref) => (
+
+ ),
+ );
+
+ const Badge = forwardRef(
+ ({ children, className, style, ...props }, ref) => (
+
+ ),
+ );
+
+ return {
+ Badge,
+ Button: ActionButton,
+ IconButton: ActionIconButton,
+ Input,
+ Surface,
+ Tab: TabButton,
+ Tabs,
+ Textarea,
+ };
+}
+
+export function createMuiAnyclickStyleAdapter(
+ options: MuiAnyclickStyleOptions = {},
+): AnyclickStyleAdapter {
+ return {
+ components: createMuiComponentOverrides(options),
+ name: "mui-companion",
+ tokens: {
+ accent: "#1976d2",
+ accentMuted: "#e3f2fd",
+ border: "#d0d7de",
+ danger: "#d32f2f",
+ fontFamily:
+ '"Roboto","Helvetica","Arial",sans-serif',
+ radiusMd: "12px",
+ radiusSm: "8px",
+ shadowLg: "0 18px 40px rgba(25, 118, 210, 0.16)",
+ shadowMd: "0 10px 24px rgba(15, 23, 42, 0.12)",
+ surface: "#ffffff",
+ surfaceMuted: "#f7f9fc",
+ text: "#111827",
+ textMuted: "#6b7280",
+ },
+ };
+}
+
+export interface MuiAnyclickStyleProviderProps
+ extends AnyclickStyleProviderProps {
+ options?: MuiAnyclickStyleOptions;
+}
+
+export function MuiAnyclickStyleProvider({
+ children,
+ options,
+ ...rest
+}: MuiAnyclickStyleProviderProps) {
+ const adapter = useMemo(() => createMuiAnyclickStyleAdapter(options), [
+ options?.density,
+ options?.elevation,
+ options?.useTextField,
+ options?.variantMapping?.button,
+ options?.variantMapping?.field,
+ ]);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/anyclick-react-mui/tsconfig.json b/packages/anyclick-react-mui/tsconfig.json
new file mode 100644
index 0000000..f2940a8
--- /dev/null
+++ b/packages/anyclick-react-mui/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "jsx": "react-jsx",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "declaration": true,
+ "declarationMap": true,
+ "outDir": "dist",
+ "rootDir": "src",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/anyclick-react-shadcn/README.md b/packages/anyclick-react-shadcn/README.md
new file mode 100644
index 0000000..f5f6461
--- /dev/null
+++ b/packages/anyclick-react-shadcn/README.md
@@ -0,0 +1,19 @@
+# @ewjdev/anyclick-react-shadcn
+
+shadcn-style companion styling package for `@ewjdev/anyclick-react`.
+
+```tsx
+import { AnyclickProvider } from "@ewjdev/anyclick-react";
+import {
+ ShadcnAnyclickStyleProvider,
+ createShadcnComponentOverrides,
+} from "@ewjdev/anyclick-react-shadcn";
+import "@ewjdev/anyclick-react-shadcn/shadcn.css";
+```
+
+Exports:
+
+- `createShadcnAnyclickStyleAdapter(options?)`
+- `ShadcnAnyclickStyleProvider`
+- `createShadcnComponentOverrides(overrides)`
+- `shadcn.css`
diff --git a/packages/anyclick-react-shadcn/package.json b/packages/anyclick-react-shadcn/package.json
new file mode 100644
index 0000000..e813b0d
--- /dev/null
+++ b/packages/anyclick-react-shadcn/package.json
@@ -0,0 +1,55 @@
+{
+ "name": "@ewjdev/anyclick-react-shadcn",
+ "version": "1.0.0",
+ "private": false,
+ "description": "shadcn-style adapter package for @ewjdev/anyclick-react",
+ "license": "MIT",
+ "author": "ewjdev",
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://registry.npmjs.org/"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/ewjdev/anyclick.git",
+ "directory": "packages/anyclick-react-shadcn"
+ },
+ "sideEffects": [
+ "./src/shadcn.css"
+ ],
+ "main": "dist/index.js",
+ "module": "dist/index.mjs",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "require": "./dist/index.js",
+ "import": "./dist/index.mjs"
+ },
+ "./shadcn.css": "./src/shadcn.css"
+ },
+ "files": [
+ "dist",
+ "src/shadcn.css"
+ ],
+ "scripts": {
+ "build": "tsup src/index.tsx --format esm,cjs --dts --sourcemap --external react",
+ "clean": "rimraf dist",
+ "dev": "tsup src/index.tsx --format esm,cjs --dts --sourcemap --external react --watch"
+ },
+ "dependencies": {
+ "@ewjdev/anyclick-react": "^5.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=19.2.1",
+ "react-dom": ">=19.2.1",
+ "tailwindcss": ">=4.1.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.1",
+ "@types/react-dom": "^19.2.1",
+ "rimraf": "^6.0.1",
+ "tsup": "^8.3.5",
+ "typescript": "5.8.3"
+ }
+}
diff --git a/packages/anyclick-react-shadcn/src/index.tsx b/packages/anyclick-react-shadcn/src/index.tsx
new file mode 100644
index 0000000..97dbc2b
--- /dev/null
+++ b/packages/anyclick-react-shadcn/src/index.tsx
@@ -0,0 +1,97 @@
+import React, { useMemo } from "react";
+import {
+ AnyclickStyleProvider,
+ type AnyclickComponentOverrides,
+ type AnyclickStyleAdapter,
+ type AnyclickStyleProviderProps,
+ type AnyclickStyleSlot,
+} from "@ewjdev/anyclick-react";
+
+export interface ShadcnAnyclickStyleOptions {
+ classNamePrefix?: string;
+}
+
+function classNameMap(prefix: string): Record {
+ return {
+ "comment.primaryAction": `${prefix}-action ${prefix}-action-primary`,
+ "comment.secondaryAction": `${prefix}-action`,
+ "comment.section": `${prefix}-section`,
+ "comment.textarea": `${prefix}-field ${prefix}-textarea`,
+ "inspect.action": `${prefix}-action`,
+ "inspect.content": `${prefix}-section`,
+ "inspect.header": `${prefix}-header`,
+ "inspect.surface": `${prefix}-surface`,
+ "menu.backButton": `${prefix}-menu-item ${prefix}-menu-back`,
+ "menu.dragHandle": `${prefix}-icon-button`,
+ "menu.header": `${prefix}-header`,
+ "menu.headerAction": `${prefix}-icon-button`,
+ "menu.item": `${prefix}-menu-item`,
+ "menu.itemBadge": `${prefix}-badge`,
+ "menu.itemIcon": `${prefix}-menu-icon`,
+ "menu.itemLabel": `${prefix}-menu-label`,
+ "menu.list": `${prefix}-list`,
+ "menu.overlay": `${prefix}-overlay`,
+ "menu.submenuIndicator": `${prefix}-submenu-indicator`,
+ "menu.surface": `${prefix}-surface`,
+ "quickChat.header": `${prefix}-header`,
+ "quickChat.input": `${prefix}-field ${prefix}-textarea`,
+ "quickChat.messageList": `${prefix}-section`,
+ "quickChat.submit": `${prefix}-icon-button`,
+ "quickChat.surface": `${prefix}-surface`,
+ "screenshot.action": `${prefix}-action`,
+ "screenshot.empty": `${prefix}-empty`,
+ "screenshot.error": `${prefix}-empty ${prefix}-error`,
+ "screenshot.header": `${prefix}-header`,
+ "screenshot.meta": `${prefix}-meta`,
+ "screenshot.preview": `${prefix}-preview`,
+ "screenshot.surface": `${prefix}-surface`,
+ "screenshot.tab": `${prefix}-tab`,
+ "screenshot.tabActive": `${prefix}-tab ${prefix}-tab-active`,
+ "shared.badge": `${prefix}-badge`,
+ "shared.button": `${prefix}-action`,
+ "shared.input": `${prefix}-field`,
+ "shared.textarea": `${prefix}-field ${prefix}-textarea`,
+ };
+}
+
+export function createShadcnAnyclickStyleAdapter(
+ options: ShadcnAnyclickStyleOptions = {},
+): AnyclickStyleAdapter {
+ const prefix = options.classNamePrefix ?? "ac-anyclick-shadcn";
+ const classes = classNameMap(prefix);
+
+ return {
+ name: "shadcn-companion",
+ resolveSlot: ({ slot }) => ({
+ className: classes[slot],
+ }),
+ };
+}
+
+export function createShadcnComponentOverrides(
+ overrides: Partial,
+): Partial {
+ return overrides;
+}
+
+export interface ShadcnAnyclickStyleProviderProps
+ extends AnyclickStyleProviderProps {
+ options?: ShadcnAnyclickStyleOptions;
+}
+
+export function ShadcnAnyclickStyleProvider({
+ children,
+ options,
+ ...rest
+}: ShadcnAnyclickStyleProviderProps) {
+ const adapter = useMemo(
+ () => createShadcnAnyclickStyleAdapter(options),
+ [options?.classNamePrefix],
+ );
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/anyclick-react-shadcn/src/shadcn.css b/packages/anyclick-react-shadcn/src/shadcn.css
new file mode 100644
index 0000000..adc1f9d
--- /dev/null
+++ b/packages/anyclick-react-shadcn/src/shadcn.css
@@ -0,0 +1,88 @@
+@import "tailwindcss";
+
+@layer components {
+ .ac-anyclick-shadcn-overlay {
+ @apply fixed inset-0 bg-transparent;
+ }
+
+ .ac-anyclick-shadcn-surface {
+ @apply rounded-xl border border-border bg-background text-foreground shadow-sm;
+ }
+
+ .ac-anyclick-shadcn-header {
+ @apply flex items-center justify-between gap-2 border-b border-border px-3 py-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground;
+ }
+
+ .ac-anyclick-shadcn-list,
+ .ac-anyclick-shadcn-section {
+ @apply flex flex-col gap-2 p-3;
+ }
+
+ .ac-anyclick-shadcn-menu-item {
+ @apply flex min-h-10 w-full items-center gap-3 rounded-md border-0 bg-transparent px-4 py-2 text-left text-sm text-foreground transition hover:bg-accent/10 disabled:cursor-not-allowed disabled:opacity-60;
+ }
+
+ .ac-anyclick-shadcn-menu-back {
+ @apply border-b border-border;
+ }
+
+ .ac-anyclick-shadcn-menu-icon {
+ @apply inline-flex h-5 w-5 items-center justify-center;
+ }
+
+ .ac-anyclick-shadcn-menu-label {
+ @apply inline-flex flex-1 items-center gap-2;
+ }
+
+ .ac-anyclick-shadcn-submenu-indicator {
+ @apply ml-auto opacity-60;
+ }
+
+ .ac-anyclick-shadcn-action {
+ @apply inline-flex min-h-8 items-center justify-center gap-2 rounded-md border border-input bg-background px-3 text-sm font-medium text-foreground transition hover:bg-accent/10 disabled:pointer-events-none disabled:opacity-50;
+ }
+
+ .ac-anyclick-shadcn-action-primary {
+ @apply border-primary bg-primary text-primary-foreground hover:bg-primary/90;
+ }
+
+ .ac-anyclick-shadcn-icon-button {
+ @apply inline-flex h-8 w-8 items-center justify-center rounded-md border border-transparent bg-transparent text-muted-foreground transition hover:bg-accent/10;
+ }
+
+ .ac-anyclick-shadcn-field {
+ @apply w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground outline-none transition placeholder:text-muted-foreground focus-visible:ring-1 focus-visible:ring-ring;
+ }
+
+ .ac-anyclick-shadcn-textarea {
+ @apply min-h-[88px] resize-y;
+ }
+
+ .ac-anyclick-shadcn-badge {
+ @apply inline-flex items-center rounded-full border border-border bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground;
+ }
+
+ .ac-anyclick-shadcn-tab {
+ @apply inline-flex min-h-7 items-center gap-1 rounded-md border border-transparent bg-transparent px-2 py-1 text-xs font-medium text-muted-foreground transition hover:bg-accent/10;
+ }
+
+ .ac-anyclick-shadcn-tab-active {
+ @apply border-border bg-accent/10 text-foreground;
+ }
+
+ .ac-anyclick-shadcn-preview {
+ @apply flex min-h-40 items-center justify-center rounded-lg border border-border bg-muted/40 p-2;
+ }
+
+ .ac-anyclick-shadcn-empty {
+ @apply flex flex-col items-center justify-center gap-2 p-6 text-center text-sm text-muted-foreground;
+ }
+
+ .ac-anyclick-shadcn-error {
+ @apply border border-destructive/20 bg-destructive/10 text-destructive;
+ }
+
+ .ac-anyclick-shadcn-meta {
+ @apply text-center text-[11px] text-muted-foreground;
+ }
+}
diff --git a/packages/anyclick-react-shadcn/tsconfig.json b/packages/anyclick-react-shadcn/tsconfig.json
new file mode 100644
index 0000000..f2940a8
--- /dev/null
+++ b/packages/anyclick-react-shadcn/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "jsx": "react-jsx",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "declaration": true,
+ "declarationMap": true,
+ "outDir": "dist",
+ "rootDir": "src",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/anyclick-react-tailwind/README.md b/packages/anyclick-react-tailwind/README.md
new file mode 100644
index 0000000..d34e914
--- /dev/null
+++ b/packages/anyclick-react-tailwind/README.md
@@ -0,0 +1,17 @@
+# @ewjdev/anyclick-react-tailwind
+
+Tailwind companion styling package for `@ewjdev/anyclick-react`.
+
+```tsx
+import { AnyclickProvider } from "@ewjdev/anyclick-react";
+import {
+ TailwindAnyclickStyleProvider,
+} from "@ewjdev/anyclick-react-tailwind";
+import "@ewjdev/anyclick-react-tailwind/tailwind.css";
+```
+
+Exports:
+
+- `createTailwindAnyclickStyleAdapter(options?)`
+- `TailwindAnyclickStyleProvider`
+- `tailwind.css`
diff --git a/packages/anyclick-react-tailwind/package.json b/packages/anyclick-react-tailwind/package.json
new file mode 100644
index 0000000..80eec70
--- /dev/null
+++ b/packages/anyclick-react-tailwind/package.json
@@ -0,0 +1,55 @@
+{
+ "name": "@ewjdev/anyclick-react-tailwind",
+ "version": "1.0.0",
+ "private": false,
+ "description": "Tailwind style adapter package for @ewjdev/anyclick-react",
+ "license": "MIT",
+ "author": "ewjdev",
+ "publishConfig": {
+ "access": "public",
+ "registry": "https://registry.npmjs.org/"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/ewjdev/anyclick.git",
+ "directory": "packages/anyclick-react-tailwind"
+ },
+ "sideEffects": [
+ "./src/tailwind.css"
+ ],
+ "main": "dist/index.js",
+ "module": "dist/index.mjs",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "require": "./dist/index.js",
+ "import": "./dist/index.mjs"
+ },
+ "./tailwind.css": "./src/tailwind.css"
+ },
+ "files": [
+ "dist",
+ "src/tailwind.css"
+ ],
+ "scripts": {
+ "build": "tsup src/index.tsx --format esm,cjs --dts --sourcemap --external react",
+ "clean": "rimraf dist",
+ "dev": "tsup src/index.tsx --format esm,cjs --dts --sourcemap --external react --watch"
+ },
+ "dependencies": {
+ "@ewjdev/anyclick-react": "^5.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=19.2.1",
+ "react-dom": ">=19.2.1",
+ "tailwindcss": ">=4.1.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.1",
+ "@types/react-dom": "^19.2.1",
+ "rimraf": "^6.0.1",
+ "tsup": "^8.3.5",
+ "typescript": "5.8.3"
+ }
+}
diff --git a/packages/anyclick-react-tailwind/src/index.tsx b/packages/anyclick-react-tailwind/src/index.tsx
new file mode 100644
index 0000000..10c5550
--- /dev/null
+++ b/packages/anyclick-react-tailwind/src/index.tsx
@@ -0,0 +1,90 @@
+import React, { useMemo } from "react";
+import {
+ AnyclickStyleProvider,
+ type AnyclickStyleAdapter,
+ type AnyclickStyleProviderProps,
+ type AnyclickStyleSlot,
+} from "@ewjdev/anyclick-react";
+
+export interface TailwindAnyclickStyleOptions {
+ classNamePrefix?: string;
+}
+
+function classNameMap(prefix: string): Record {
+ return {
+ "comment.primaryAction": `${prefix}-action ${prefix}-action-primary`,
+ "comment.secondaryAction": `${prefix}-action`,
+ "comment.section": `${prefix}-comment-section`,
+ "comment.textarea": `${prefix}-field ${prefix}-textarea`,
+ "inspect.action": `${prefix}-action`,
+ "inspect.content": `${prefix}-inspect-content`,
+ "inspect.header": `${prefix}-header`,
+ "inspect.surface": `${prefix}-surface`,
+ "menu.backButton": `${prefix}-menu-item ${prefix}-menu-back`,
+ "menu.dragHandle": `${prefix}-icon-button`,
+ "menu.header": `${prefix}-header`,
+ "menu.headerAction": `${prefix}-icon-button`,
+ "menu.item": `${prefix}-menu-item`,
+ "menu.itemBadge": `${prefix}-badge`,
+ "menu.itemIcon": `${prefix}-menu-icon`,
+ "menu.itemLabel": `${prefix}-menu-label`,
+ "menu.list": `${prefix}-menu-list`,
+ "menu.overlay": `${prefix}-overlay`,
+ "menu.submenuIndicator": `${prefix}-submenu-indicator`,
+ "menu.surface": `${prefix}-surface ${prefix}-menu-surface`,
+ "quickChat.header": `${prefix}-header`,
+ "quickChat.input": `${prefix}-field ${prefix}-textarea`,
+ "quickChat.messageList": `${prefix}-message-list`,
+ "quickChat.submit": `${prefix}-icon-button`,
+ "quickChat.surface": `${prefix}-surface`,
+ "screenshot.action": `${prefix}-action`,
+ "screenshot.empty": `${prefix}-empty`,
+ "screenshot.error": `${prefix}-empty ${prefix}-error`,
+ "screenshot.header": `${prefix}-header ${prefix}-screenshot-header`,
+ "screenshot.meta": `${prefix}-meta`,
+ "screenshot.preview": `${prefix}-preview`,
+ "screenshot.surface": `${prefix}-surface`,
+ "screenshot.tab": `${prefix}-tab`,
+ "screenshot.tabActive": `${prefix}-tab ${prefix}-tab-active`,
+ "shared.badge": `${prefix}-badge`,
+ "shared.button": `${prefix}-action`,
+ "shared.input": `${prefix}-field`,
+ "shared.textarea": `${prefix}-field ${prefix}-textarea`,
+ };
+}
+
+export function createTailwindAnyclickStyleAdapter(
+ options: TailwindAnyclickStyleOptions = {},
+): AnyclickStyleAdapter {
+ const prefix = options.classNamePrefix ?? "ac-anyclick-tw";
+ const classes = classNameMap(prefix);
+
+ return {
+ name: "tailwind-companion",
+ resolveSlot: ({ slot }) => ({
+ className: classes[slot],
+ }),
+ };
+}
+
+export interface TailwindAnyclickStyleProviderProps
+ extends AnyclickStyleProviderProps {
+ options?: TailwindAnyclickStyleOptions;
+}
+
+export function TailwindAnyclickStyleProvider({
+ children,
+ options,
+ ...rest
+}: TailwindAnyclickStyleProviderProps) {
+ const adapter = useMemo(
+ () => createTailwindAnyclickStyleAdapter(options),
+ [options?.classNamePrefix],
+ );
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/anyclick-react-tailwind/src/tailwind.css b/packages/anyclick-react-tailwind/src/tailwind.css
new file mode 100644
index 0000000..42b1b46
--- /dev/null
+++ b/packages/anyclick-react-tailwind/src/tailwind.css
@@ -0,0 +1,158 @@
+@import "tailwindcss";
+
+@layer components {
+ .ac-anyclick-tw-overlay {
+ @apply fixed inset-0 bg-transparent;
+ }
+
+ .ac-anyclick-tw-surface {
+ @apply rounded-xl shadow-2xl;
+ background: var(--anyclick-menu-bg, #ffffff);
+ border: 1px solid var(--anyclick-menu-border, #e2e8f0);
+ color: var(--anyclick-menu-text, #0f172a);
+ }
+
+ .ac-anyclick-tw-menu-surface {
+ @apply min-w-[220px] overflow-hidden;
+ }
+
+ .ac-anyclick-tw-header {
+ @apply flex items-center justify-between gap-2 px-3 py-2 text-[11px] font-bold uppercase tracking-[0.08em];
+ border-bottom: 1px solid var(--anyclick-menu-border, #e2e8f0);
+ color: var(--anyclick-menu-text-muted, var(--anyclick-menu-text, #475569));
+ }
+
+ .ac-anyclick-tw-menu-list {
+ @apply flex flex-col;
+ }
+
+ .ac-anyclick-tw-menu-item {
+ @apply flex min-h-10 w-full items-center gap-3 border-0 bg-transparent px-4 py-2 text-left text-sm transition disabled:cursor-not-allowed disabled:opacity-60;
+ color: var(--anyclick-menu-text, #0f172a);
+ }
+
+ .ac-anyclick-tw-menu-item:hover,
+ .ac-anyclick-tw-menu-item:focus-visible {
+ background: var(--anyclick-menu-hover, #f1f5f9);
+ color: var(--anyclick-menu-text, #0f172a);
+ }
+
+ .ac-anyclick-tw-menu-back {
+ border-bottom: 1px solid var(--anyclick-menu-border, #e2e8f0);
+ }
+
+ .ac-anyclick-tw-menu-icon {
+ @apply inline-flex h-5 w-5 items-center justify-center;
+ color: currentColor;
+ }
+
+ .ac-anyclick-tw-menu-label {
+ @apply inline-flex flex-1 items-center gap-2;
+ color: inherit;
+ }
+
+ .ac-anyclick-tw-submenu-indicator {
+ @apply ml-auto opacity-60;
+ color: inherit;
+ }
+
+ .ac-anyclick-tw-comment-section,
+ .ac-anyclick-tw-inspect-content,
+ .ac-anyclick-tw-message-list {
+ @apply flex flex-col gap-2 p-3;
+ }
+
+ .ac-anyclick-tw-action {
+ @apply inline-flex min-h-8 items-center justify-center gap-2 rounded-md px-3 text-sm font-semibold transition disabled:cursor-not-allowed disabled:opacity-60;
+ background: var(--anyclick-menu-cancel-bg, color-mix(in srgb, var(--anyclick-menu-bg, #ffffff) 88%, #ffffff));
+ border: 1px solid var(--anyclick-menu-border, #e2e8f0);
+ color: var(--anyclick-menu-cancel-text, var(--anyclick-menu-text, #0f172a));
+ }
+
+ .ac-anyclick-tw-action-primary {
+ background: var(--anyclick-menu-accent, #0284c7);
+ border-color: transparent;
+ color: var(--anyclick-menu-accent-text, #ffffff);
+ }
+
+ .ac-anyclick-tw-icon-button {
+ @apply inline-flex h-8 w-8 items-center justify-center rounded-md border border-transparent bg-transparent transition;
+ color: var(--anyclick-menu-text-muted, var(--anyclick-menu-text, #475569));
+ }
+
+ .ac-anyclick-tw-action:hover,
+ .ac-anyclick-tw-action:focus-visible,
+ .ac-anyclick-tw-icon-button:hover,
+ .ac-anyclick-tw-icon-button:focus-visible {
+ background: var(--anyclick-menu-hover, #f1f5f9);
+ }
+
+ .ac-anyclick-tw-action-primary:hover,
+ .ac-anyclick-tw-action-primary:focus-visible {
+ filter: brightness(1.05);
+ }
+
+ .ac-anyclick-tw-field {
+ @apply w-full rounded-md px-3 py-2 text-sm outline-none ring-0 transition;
+ background: var(--anyclick-menu-input-bg, #ffffff);
+ border: 1px solid var(--anyclick-menu-input-border, var(--anyclick-menu-border, #e2e8f0));
+ color: var(--anyclick-menu-text, #0f172a);
+ }
+
+ .ac-anyclick-tw-textarea {
+ @apply min-h-[88px] resize-y;
+ }
+
+ .ac-anyclick-tw-field::placeholder {
+ color: color-mix(in srgb, var(--anyclick-menu-text, #0f172a) 55%, transparent);
+ }
+
+ .ac-anyclick-tw-field:focus-visible {
+ border-color: var(--anyclick-menu-accent, #0284c7);
+ }
+
+ .ac-anyclick-tw-badge {
+ @apply inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-semibold;
+ background: color-mix(in srgb, var(--anyclick-menu-hover, #f1f5f9) 85%, transparent);
+ border: 1px solid var(--anyclick-menu-border, #e2e8f0);
+ color: var(--anyclick-menu-text-muted, var(--anyclick-menu-text, #475569));
+ }
+
+ .ac-anyclick-tw-screenshot-header {
+ @apply border-b-0;
+ }
+
+ .ac-anyclick-tw-tab {
+ @apply inline-flex min-h-7 items-center gap-1 rounded-md border border-transparent bg-transparent px-2 py-1 text-xs font-medium transition;
+ color: var(--anyclick-menu-text-muted, var(--anyclick-menu-text, #475569));
+ }
+
+ .ac-anyclick-tw-tab-active {
+ background: color-mix(in srgb, var(--anyclick-menu-accent, #0284c7) 14%, transparent);
+ border-color: color-mix(in srgb, var(--anyclick-menu-accent, #0284c7) 35%, transparent);
+ color: var(--anyclick-menu-accent, #0284c7);
+ }
+
+ .ac-anyclick-tw-preview {
+ @apply flex min-h-40 items-center justify-center rounded-lg p-2;
+ background: color-mix(in srgb, var(--anyclick-menu-hover, #f8fafc) 70%, transparent);
+ border: 1px solid var(--anyclick-menu-border, #e2e8f0);
+ }
+
+ .ac-anyclick-tw-empty {
+ @apply flex flex-col items-center justify-center gap-2 p-6 text-center text-sm;
+ color: var(--anyclick-menu-text-muted, var(--anyclick-menu-text, #475569));
+ }
+
+ .ac-anyclick-tw-error {
+ @apply border;
+ background: color-mix(in srgb, #fb7185 12%, transparent);
+ border-color: color-mix(in srgb, #fb7185 45%, transparent);
+ color: #be123c;
+ }
+
+ .ac-anyclick-tw-meta {
+ @apply text-center text-[11px];
+ color: var(--anyclick-menu-text-muted, var(--anyclick-menu-text, #475569));
+ }
+}
diff --git a/packages/anyclick-react-tailwind/tsconfig.json b/packages/anyclick-react-tailwind/tsconfig.json
new file mode 100644
index 0000000..f2940a8
--- /dev/null
+++ b/packages/anyclick-react-tailwind/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "jsx": "react-jsx",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "declaration": true,
+ "declarationMap": true,
+ "outDir": "dist",
+ "rootDir": "src",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/anyclick-react/README.md b/packages/anyclick-react/README.md
index 3896cbb..2621f78 100644
--- a/packages/anyclick-react/README.md
+++ b/packages/anyclick-react/README.md
@@ -7,41 +7,95 @@
## Overview
-`@ewjdev/anyclick-react` provides a drop-in React component that adds feedback capture to your application. Right-click any element to open a customizable context menu for submitting feedback.
+`@ewjdev/anyclick-react` is now headless-by-default for styling. It provides the Anyclick behavior, structure, and accessibility model, while styling is supplied through a slot-based adapter layer. Right-click any element to open a customizable context menu for submitting feedback.
## Installation
```bash
-npm install @ewjdev/anyclick-react
+npm install @ewjdev/anyclick-core @ewjdev/anyclick-react
# or
-yarn add @ewjdev/anyclick-react
+yarn add @ewjdev/anyclick-core @ewjdev/anyclick-react
# or
-pnpm add @ewjdev/anyclick-react
+pnpm add @ewjdev/anyclick-core @ewjdev/anyclick-react
+```
+
+### Companion style packages
+
+```bash
+npm install @ewjdev/anyclick-react-tailwind
+# or
+npm install @ewjdev/anyclick-react-shadcn
+# or
+npm install @ewjdev/anyclick-react-mui
```
## Requirements
- React 19+
-- @ewjdev/anyclick-core (included as dependency)
+- @ewjdev/anyclick-core
## Quick Start
```tsx
"use client";
-import { createHttpAdapter } from "@ewjdev/anyclick-github";
+import { createHttpAdapter } from "@ewjdev/anyclick-core";
import { AnyclickProvider } from "@ewjdev/anyclick-react";
+import {
+ TailwindAnyclickStyleProvider,
+} from "@ewjdev/anyclick-react-tailwind";
+import "@ewjdev/anyclick-react-tailwind/tailwind.css";
const adapter = createHttpAdapter({
endpoint: "/api/feedback",
});
export function Providers({ children }: { children: React.ReactNode }) {
- return {children} ;
+ return (
+
+ {children}
+
+ );
}
```
-That's it! Users can now right-click any element to submit feedback.
+For a plain unstyled fallback, render `AnyclickProvider` without a style provider. That path is usable for development and debugging, but the intended production setup is one of the companion packages or a custom `AnyclickStyleProvider`.
+
+### Style adapter setup
+
+Tailwind:
+
+```tsx
+import { AnyclickProvider } from "@ewjdev/anyclick-react";
+import {
+ TailwindAnyclickStyleProvider,
+} from "@ewjdev/anyclick-react-tailwind";
+import "@ewjdev/anyclick-react-tailwind/tailwind.css";
+```
+
+shadcn-style classes:
+
+```tsx
+import { AnyclickProvider } from "@ewjdev/anyclick-react";
+import {
+ ShadcnAnyclickStyleProvider,
+} from "@ewjdev/anyclick-react-shadcn";
+import "@ewjdev/anyclick-react-shadcn/shadcn.css";
+```
+
+MUI:
+
+```tsx
+import { AnyclickProvider } from "@ewjdev/anyclick-react";
+import { MuiAnyclickStyleProvider } from "@ewjdev/anyclick-react-mui";
+import { ThemeProvider, createTheme } from "@mui/material/styles";
+```
+
+### Migration notes
+
+- `menuClassName` and `menuStyle` still work in v5, but they are now deprecated shims that map to `menu.surface`.
+- Use `slotClassNames`, `slotStyles`, `styleAdapter`, or `components` for new styling work.
+- `@ewjdev/anyclick-react` no longer imports Tailwind CSS for you.
### Advanced inspector (DevTools UI)
diff --git a/packages/anyclick-react/docs/style-slots.md b/packages/anyclick-react/docs/style-slots.md
new file mode 100644
index 0000000..ebfb8ae
--- /dev/null
+++ b/packages/anyclick-react/docs/style-slots.md
@@ -0,0 +1,75 @@
+# Anyclick React Style Slots
+
+This document freezes the v5 render surface for `@ewjdev/anyclick-react` styling.
+Each user-visible node maps to one public slot name. Behavior, positioning math,
+highlight calculations, event routing, and adapter submission flow are intentionally
+out of scope.
+
+## Context Menu
+
+| Surface | Slot | Notes |
+| --- | --- | --- |
+| Backdrop portal overlay | `menu.overlay` | Click-away surface only |
+| Floating menu shell | `menu.surface` | Receives deprecated `menuClassName`/`menuStyle` shims |
+| Header row | `menu.header` | Title and top-right controls |
+| Header control buttons | `menu.headerAction` | Drag handle and quick-chat toggle |
+| Scrollable/root list wrapper | `menu.list` | Wraps items or submenu |
+| Item button row | `menu.item` | Active, disabled, selected states |
+| Item icon wrapper | `menu.itemIcon` | Icon container only |
+| Item label wrapper | `menu.itemLabel` | Text plus badge |
+| Badge pill | `menu.itemBadge` | Tone-aware |
+| Submenu chevron wrapper | `menu.submenuIndicator` | Indicator only |
+| Submenu back button | `menu.backButton` | Special-case menu item |
+| Drag handle button | `menu.dragHandle` | Dynamic-position mode only |
+
+## Comment Form
+
+| Surface | Slot | Notes |
+| --- | --- | --- |
+| Comment section wrapper | `comment.section` | Mounted inside menu surface |
+| Comment textarea | `comment.textarea` | Uses shared textarea semantics too |
+| Primary send button | `comment.primaryAction` | Loading-aware |
+| Secondary cancel button | `comment.secondaryAction` | Neutral action |
+
+## Screenshot Preview
+
+| Surface | Slot | Notes |
+| --- | --- | --- |
+| Screenshot panel shell | `screenshot.surface` | Inline or expanded modal state |
+| Header row | `screenshot.header` | Title, size, expand/collapse |
+| Tab button | `screenshot.tab` | Default tab state |
+| Active tab button | `screenshot.tabActive` | Selected tab styling |
+| Preview viewport | `screenshot.preview` | Image frame |
+| Empty state container | `screenshot.empty` | No capture available |
+| Error state container | `screenshot.error` | Failed capture copy |
+| Metadata row | `screenshot.meta` | Size and dimensions |
+| Action buttons | `screenshot.action` | Retake, cancel, send, continue |
+
+## QuickChat
+
+| Surface | Slot | Notes |
+| --- | --- | --- |
+| QuickChat shell | `quickChat.surface` | Inline or pinned drawer state |
+| Header row | `quickChat.header` | Context controls and actions |
+| Messages viewport | `quickChat.messageList` | Message stack wrapper |
+| Composer textarea | `quickChat.input` | Auto-resizing textarea |
+| Submit and outbound actions | `quickChat.submit` | Send and external-chat buttons |
+
+## Inspect Simple
+
+| Surface | Slot | Notes |
+| --- | --- | --- |
+| Dialog shell | `inspect.surface` | Mobile sheet or floating panel |
+| Header row | `inspect.header` | Identity pill and close/open buttons |
+| Body content wrapper | `inspect.content` | Selector, status, action row |
+| Action buttons | `inspect.action` | Copy/save/open actions |
+
+## Shared Primitives
+
+| Surface | Slot | Notes |
+| --- | --- | --- |
+| Generic button primitive | `shared.button` | Neutral by default, tone-aware |
+| Generic input primitive | `shared.input` | Single-line controls |
+| Generic textarea primitive | `shared.textarea` | Multi-line controls |
+| Generic badge primitive | `shared.badge` | Token-driven pill styling |
+
diff --git a/packages/anyclick-react/package.json b/packages/anyclick-react/package.json
index 2d47ca2..74934d8 100644
--- a/packages/anyclick-react/package.json
+++ b/packages/anyclick-react/package.json
@@ -1,6 +1,6 @@
{
"name": "@ewjdev/anyclick-react",
- "version": "4.0.0",
+ "version": "5.0.0",
"private": false,
"description": "React provider and context menu UI for UI feedback capture",
"keywords": [
@@ -58,8 +58,7 @@
"@ewjdev/anyclick-devtools": "^4.0.0",
"ai": "^5.0.112",
"lucide-react": "^0.544.0",
- "zustand": "^5.0.2",
- "tailwindcss": "4.1.17"
+ "zustand": "^5.0.2"
},
"peerDependencies": {
"react": ">=19.2.1",
@@ -70,13 +69,10 @@
"@types/react": "^19.2.1",
"@types/react-dom": "^19.2.1",
"@vitejs/plugin-react": "^4.3.0",
- "autoprefixer": "^10.4.19",
- "postcss": "^8.4.38",
"tsup": "^8.3.5",
"typescript": "5.8.3",
"rimraf": "^6.0.1",
- "vite": "^5.2.0",
- "@tailwindcss/vite": "4.1.17"
+ "vite": "^5.2.0"
},
"engines": {
"node": ">=18.18.0"
diff --git a/packages/anyclick-react/postcss.config.cjs b/packages/anyclick-react/postcss.config.cjs
deleted file mode 100644
index 436ec2c..0000000
--- a/packages/anyclick-react/postcss.config.cjs
+++ /dev/null
@@ -1,7 +0,0 @@
-// tailwindcss@4 is ESM-only; use .default when requiring from CJS.
-const tailwindcss = require("tailwindcss").default;
-const autoprefixer = require("autoprefixer").default ?? require("autoprefixer");
-
-module.exports = {
- plugins: [tailwindcss, autoprefixer],
-};
diff --git a/packages/anyclick-react/src/AnyclickProvider.tsx b/packages/anyclick-react/src/AnyclickProvider.tsx
index c577b07..34f3504 100644
--- a/packages/anyclick-react/src/AnyclickProvider.tsx
+++ b/packages/anyclick-react/src/AnyclickProvider.tsx
@@ -29,6 +29,11 @@ import { ContextMenu } from "./ContextMenu";
import { AnyclickContext, useAnyclick } from "./context";
import { findContainerParent } from "./highlight";
import { type ProviderInstance, useProviderStore } from "./store";
+import {
+ AnyclickStyleProvider,
+ composeClassNames,
+ useAnyclickStyle,
+} from "./styling";
import type {
AnyclickContextValue,
AnyclickProviderProps,
@@ -46,6 +51,28 @@ const defaultMenuItems: ContextMenuItem[] = [
];
const OFFSCREEN_POSITION = { x: -9999, y: -9999 };
+const warnedFallbackProviders = new Set();
+
+function StyleFallbackWarning({ providerId }: { providerId: string }) {
+ const style = useAnyclickStyle();
+
+ useEffect(() => {
+ if (
+ process.env.NODE_ENV === "production" ||
+ !style.isFallback ||
+ warnedFallbackProviders.has(providerId)
+ ) {
+ return;
+ }
+
+ warnedFallbackProviders.add(providerId);
+ console.warn(
+ "[anyclick-react] Rendering with the unstyled fallback adapter. Add AnyclickStyleProvider or a companion style package for production styling.",
+ );
+ }, [providerId, style.isFallback]);
+
+ return null;
+}
/**
* AnyclickProvider component - wraps your app to enable feedback capture.
@@ -80,6 +107,7 @@ export function AnyclickProvider({
maxOuterHTMLLength,
menuClassName,
menuItems = defaultMenuItems,
+ menuPositionMode,
menuStyle,
metadata,
onSubmitError,
@@ -87,11 +115,15 @@ export function AnyclickProvider({
quickChatConfig,
scoped = false,
screenshotConfig,
+ slotClassNames,
+ slotStyles,
+ styleAdapter,
stripAttributes,
targetFilter,
theme,
touchHoldDurationMs,
touchMoveThreshold,
+ components,
}: AnyclickProviderProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const [menuVisible, setMenuVisible] = useState(false);
@@ -412,13 +444,30 @@ export function AnyclickProvider({
[inheritedTheme, localTheme],
);
- // Apply merged theme styles
- const effectiveMenuStyle = mergedTheme.menuStyle ?? menuStyle;
- const effectiveMenuClassName = mergedTheme.menuClassName ?? menuClassName;
const effectiveHighlightConfig =
mergedTheme.highlightConfig ?? highlightConfig;
const effectiveScreenshotConfig =
mergedTheme.screenshotConfig ?? screenshotConfig;
+ const effectiveSlotClassNames = useMemo(() => {
+ const next = { ...(slotClassNames ?? {}) };
+ if (mergedTheme.menuClassName) {
+ next["menu.surface"] = composeClassNames(
+ next["menu.surface"],
+ mergedTheme.menuClassName,
+ );
+ }
+ return next;
+ }, [mergedTheme.menuClassName, slotClassNames]);
+ const effectiveSlotStyles = useMemo(() => {
+ const next = { ...(slotStyles ?? {}) };
+ if (mergedTheme.menuStyle) {
+ next["menu.surface"] = {
+ ...(next["menu.surface"] ?? {}),
+ ...mergedTheme.menuStyle,
+ };
+ }
+ return next;
+ }, [mergedTheme.menuStyle, slotStyles]);
// Context value
const contextValue: AnyclickContextValue = useMemo(
@@ -459,27 +508,34 @@ export function AnyclickProvider({
);
return (
-
-
- {content}
-
-
-
+
+
+
+
+ {content}
+
+
+
+
);
}
diff --git a/packages/anyclick-react/src/ContextMenu.tsx b/packages/anyclick-react/src/ContextMenu.tsx
index 8e6737d..135077c 100644
--- a/packages/anyclick-react/src/ContextMenu.tsx
+++ b/packages/anyclick-react/src/ContextMenu.tsx
@@ -1,6 +1,7 @@
"use client";
import React, { useCallback, useEffect, useRef, useState } from "react";
+import { createPortal } from "react-dom";
import type { AnyclickType, ScreenshotData } from "@ewjdev/anyclick-core";
import {
DEFAULT_SCREENSHOT_CONFIG,
@@ -20,7 +21,15 @@ import {
import { QuickChat } from "./QuickChat";
import { ScreenshotPreview } from "./ScreenshotPreview";
import { applyHighlights, clearHighlights } from "./highlight";
-import { getBadgeStyle, menuStyles } from "./styles";
+import {
+ AnyclickBadge,
+ AnyclickButton,
+ AnyclickIconButton,
+ AnyclickSurface,
+ AnyclickTextarea,
+ resolveSlotProps,
+ useAnyclickStyle,
+} from "./styling";
import type { ContextMenuItem, ContextMenuProps } from "./types";
/** Padding from viewport edges in pixels */
@@ -43,16 +52,22 @@ const OFFSCREEN_POSITION = { x: -9999, y: -9999 };
const DefaultHeader = ({
children,
className,
- styles,
+ style,
title = "Send Feedback",
}: {
children?: React.ReactNode;
className?: string;
- styles?: React.CSSProperties;
+ style?: React.CSSProperties;
title?: string;
}) => {
+ const adapter = useAnyclickStyle();
+ const headerProps = resolveSlotProps(adapter, "menu.header", {}, {
+ className,
+ style,
+ });
+
return (
-
+
{title}
{children}
@@ -78,13 +93,14 @@ const MenuItem = React.memo(function MenuItem({
const isComingSoon = item.status === "comingSoon";
const badgeLabel = item.badge?.label ?? (isComingSoon ? "Coming soon" : null);
const badgeTone = item.badge?.tone ?? (isComingSoon ? "neutral" : "neutral");
-
- const badgeStyle = badgeLabel ? getBadgeStyle(badgeTone) : undefined;
const iconNode = item.icon ?? defaultIcons[item.type];
+ const adapter = useAnyclickStyle();
+ const itemIconProps = resolveSlotProps(adapter, "menu.itemIcon");
+ const itemLabelProps = resolveSlotProps(adapter, "menu.itemLabel");
+ const submenuProps = resolveSlotProps(adapter, "menu.submenuIndicator");
return (
-
setIsHovered(true)}
@@ -95,22 +111,47 @@ const MenuItem = React.memo(function MenuItem({
onTouchCancel={() => setIsPressed(false)}
onTouchEnd={() => setIsPressed(false)}
onTouchStart={() => setIsPressed(true)}
- style={{
- ...menuStyles.touchFriendly,
- ...menuStyles.item,
- ...(isHovered || isPressed ? menuStyles.itemHover : {}),
- ...(disabled ? menuStyles.itemDisabled : {}),
+ slotName="menu.item"
+ slotState={{
+ disabled: disabled || isComingSoon,
+ hovered: isHovered,
+ pressed: isPressed,
}}
>
- {iconNode ? {iconNode} : null}
-
+ {iconNode ? (
+
+ {iconNode}
+
+ ) : null}
+
{item.label}
- {badgeLabel && {badgeLabel} }
+ {badgeLabel && (
+
+ {badgeLabel}
+
+ )}
{hasChildren && (
-
+
+
+
)}
-
+
);
});
@@ -124,10 +165,11 @@ const BackButton = React.memo(function BackButton({
}) {
const [isHovered, setIsHovered] = useState(false);
const [isPressed, setIsPressed] = useState(false);
+ const adapter = useAnyclickStyle();
+ const itemLabelProps = resolveSlotProps(adapter, "menu.itemLabel");
return (
-
setIsHovered(true)}
onMouseLeave={() => {
@@ -137,16 +179,18 @@ const BackButton = React.memo(function BackButton({
onTouchCancel={() => setIsPressed(false)}
onTouchEnd={() => setIsPressed(false)}
onTouchStart={() => setIsPressed(true)}
- style={{
- ...menuStyles.item,
- ...menuStyles.backButton,
- ...menuStyles.touchFriendly,
- ...(isHovered || isPressed ? menuStyles.itemHover : {}),
- }}
+ slotName="menu.backButton"
+ slotState={{ hovered: isHovered, pressed: isPressed }}
>
- Back
-
+
+ Back
+
+
);
});
@@ -164,6 +208,8 @@ const CommentForm = React.memo(function CommentForm({
}) {
const [comment, setComment] = useState("");
const inputRef = useRef
(null);
+ const adapter = useAnyclickStyle();
+ const sectionProps = resolveSlotProps(adapter, "comment.section");
useEffect(() => {
inputRef.current?.focus();
@@ -185,37 +231,38 @@ const CommentForm = React.memo(function CommentForm({
);
return (
-
);
@@ -287,7 +334,7 @@ export function ContextMenu({
positionMode = "inView",
quickChatConfig,
screenshotConfig,
- style,
+ style: surfaceStyle,
targetElement,
visible,
}: ContextMenuProps) {
@@ -307,6 +354,7 @@ export function ContextMenu({
}
});
const menuRef = useRef(null);
+ const [portalRoot, setPortalRoot] = useState(null);
// Position state for different modes
const [adjustedPosition, setAdjustedPosition] = useState<{
@@ -500,6 +548,11 @@ export function ContextMenu({
// Track initial input for type-to-chat feature
const [initialChatInput, setInitialChatInput] = useState("");
+ useEffect(() => {
+ if (typeof document === "undefined") return;
+ setPortalRoot(document.body);
+ }, []);
+
// Handle keyboard input - escape and type-to-chat
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -706,10 +759,138 @@ export function ContextMenu({
// Show pinned QuickChat drawer (separate from menu)
const showPinnedDrawer = isQuickChatPinned && quickChatConfig;
const showMenu = visible && targetElement;
+ const style = useAnyclickStyle();
+ const overlayProps = resolveSlotProps(style, "menu.overlay");
+ const listProps = resolveSlotProps(style, "menu.list");
+ const menuNode = showMenu ? (
+ <>
+
+
+ {!header &&
+ currentView !== "screenshot-preview" &&
+ currentView !== "quick-chat" && (
+
+ {positionMode === "dynamic" && (
+ {
+ (e.currentTarget as HTMLElement).style.opacity = "1";
+ }}
+ onMouseLeave={(e) => {
+ (e.currentTarget as HTMLElement).style.opacity = "0.5";
+ }}
+ onPointerDown={handleDragStart}
+ slotName="menu.dragHandle"
+ slotState={{ active: isDragging }}
+ style={{ cursor: isDragging ? "grabbing" : "grab", opacity: 0.5 }}
+ title="Drag to move"
+ >
+
+
+ )}
+ {showPreview && (
+
+
+
+ )}
+ {quickChatConfig && (
+
+
+
+ )}
+
+ )}
+ {!!header && header}
+
+ {currentView === "menu" && (
+
+ {submenuStack.length > 0 && }
+ {currentItems.map((item) => (
+ 0}
+ item={item}
+ onClick={() => handleItemClick(item)}
+ />
+ ))}
+
+ )}
+
+ {currentView === "comment" && (
+
+ )}
+
+ {currentView === "screenshot-preview" && (
+
+ )}
+
+ {currentView === "quick-chat" && quickChatConfig && !isQuickChatPinned && (
+ setInitialChatInput("")}
+ />
+ )}
+ {footer}
+
+ >
+ ) : null;
return (
<>
- {/* Pinned QuickChat drawer - anchored to right side, independent of menu */}
{showPinnedDrawer && (
)}
-
- {/* Context menu - only visible when menu is open */}
- {showMenu && (
-
- {!header &&
- currentView !== "screenshot-preview" &&
- currentView !== "quick-chat" && (
-
- {positionMode === "dynamic" && (
- {
- (e.currentTarget as HTMLElement).style.opacity = "1";
- }}
- onMouseLeave={(e) => {
- (e.currentTarget as HTMLElement).style.opacity = "0.5";
- }}
- onPointerDown={handleDragStart}
- style={{
- ...menuStyles.dragHandle,
- cursor: isDragging ? "grabbing" : "grab",
- }}
- title="Drag to move"
- >
-
-
- )}
- {showPreview && (
-
-
-
- )}
- {quickChatConfig && (
-
-
-
- )}
-
- )}
- {!!header && header}
-
- {currentView === "menu" && (
-
- {submenuStack.length > 0 && }
- {currentItems.map((item) => (
- 0}
- item={item}
- onClick={() => handleItemClick(item)}
- />
- ))}
-
- )}
-
- {currentView === "comment" && (
-
- )}
-
- {currentView === "screenshot-preview" && (
-
- )}
-
- {/* Inline QuickChat - inside menu when not pinned */}
- {currentView === "quick-chat" &&
- quickChatConfig &&
- !isQuickChatPinned && (
-
setInitialChatInput("")}
- />
- )}
-
- )}
+ {portalRoot && menuNode ? createPortal(menuNode, portalRoot) : menuNode}
>
);
}
diff --git a/packages/anyclick-react/src/InspectDialog/InspectSimple.tsx b/packages/anyclick-react/src/InspectDialog/InspectSimple.tsx
index 4972c49..c61036c 100644
--- a/packages/anyclick-react/src/InspectDialog/InspectSimple.tsx
+++ b/packages/anyclick-react/src/InspectDialog/InspectSimple.tsx
@@ -13,6 +13,12 @@ import {
highlightContainer,
highlightTarget,
} from "../highlight";
+import {
+ AnyclickIconButton,
+ AnyclickSurface,
+ resolveSlotProps,
+ useAnyclickStyle,
+} from "../styling";
import {
type IDEConfig,
type SourceLocation,
@@ -175,6 +181,8 @@ export function InspectSimple({
className,
highlightColors,
}: InspectSimpleProps) {
+ const adapter = useAnyclickStyle();
+ const { tokens } = adapter;
const [info, setInfo] = useState(null);
const [sourceLocation, setSourceLocation] = useState(
null,
@@ -302,15 +310,14 @@ export function InspectSimple({
left: 0,
right: 0,
bottom: 0,
- zIndex: 9999,
+ zIndex: tokens.zIndexSurface,
width: "100%",
maxWidth: "100%",
- background: "#0f172a",
- color: "#e2e8f0",
- borderTop: "1px solid #1e293b",
+ color: tokens.text,
+ borderTop: `1px solid ${tokens.border}`,
borderRadius: "16px 16px 0 0",
- boxShadow: "0 -8px 32px rgba(0,0,0,0.4)",
- fontFamily: "Inter, system-ui, -apple-system, sans-serif",
+ boxShadow: tokens.shadowLg,
+ fontFamily: tokens.fontFamily,
fontSize: 13,
overflow: "hidden",
...style,
@@ -319,19 +326,21 @@ export function InspectSimple({
position: "fixed",
right: 16,
bottom: 16,
- zIndex: 9999,
+ zIndex: tokens.zIndexSurface,
width: 320,
maxWidth: "90vw",
- background: "#0f172a",
- color: "#e2e8f0",
- border: "1px solid #1e293b",
+ color: tokens.text,
+ border: `1px solid ${tokens.border}`,
borderRadius: 12,
- boxShadow: "0 18px 48px rgba(0,0,0,0.45)",
- fontFamily: "Inter, system-ui, -apple-system, sans-serif",
+ boxShadow: tokens.shadowLg,
+ fontFamily: tokens.fontFamily,
fontSize: 13,
overflow: "hidden",
...style,
};
+ const headerProps = resolveSlotProps(adapter, "inspect.header");
+ const contentProps = resolveSlotProps(adapter, "inspect.content");
+ const actionState = { size: "sm" as const };
return (
<>
@@ -349,20 +358,19 @@ export function InspectSimple({
role="presentation"
/>
-
{/* Header with close button */}
{/* Mobile drawer handle */}
@@ -376,7 +384,7 @@ export function InspectSimple({
width: 36,
height: 4,
borderRadius: 2,
- background: "#475569",
+ background: tokens.textMuted,
}}
/>
)}
@@ -386,44 +394,44 @@ export function InspectSimple({
style={{
padding: "3px 8px",
borderRadius: 6,
- background: "#1e293b",
- color: "#e2e8f0",
+ background: tokens.surfaceMuted,
+ color: tokens.text,
fontSize: 12,
fontWeight: 600,
- fontFamily: "monospace",
+ fontFamily: tokens.fontMono,
}}
>
{identityLabel}
{sourceLocation && (
-
-
+
)}
-
-
+
{/* Compact content */}
{/* Selector row */}
@@ -431,8 +439,8 @@ export function InspectSimple({
-
-
-
+
-
-
+
-
-
+
-
+
-
+
>
);
}
-
-const iconBtnStyle: React.CSSProperties = {
- display: "inline-flex",
- alignItems: "center",
- justifyContent: "center",
- width: 28,
- height: 28,
- borderRadius: 6,
- border: "1px solid #1e293b",
- background: "#0b1220",
- color: "#94a3b8",
- cursor: "pointer",
- padding: 0,
-};
-
-const iconActionStyle: React.CSSProperties = {
- display: "inline-flex",
- alignItems: "center",
- justifyContent: "center",
- width: 32,
- height: 32,
- borderRadius: 8,
- border: "1px solid #1e293b",
- background: "#0b1220",
- color: "#94a3b8",
- cursor: "pointer",
- padding: 0,
- transition: "background 0.15s, color 0.15s, border-color 0.15s",
-};
diff --git a/packages/anyclick-react/src/QuickChat/QuickChat.tsx b/packages/anyclick-react/src/QuickChat/QuickChat.tsx
index fc30b16..916a227 100644
--- a/packages/anyclick-react/src/QuickChat/QuickChat.tsx
+++ b/packages/anyclick-react/src/QuickChat/QuickChat.tsx
@@ -31,7 +31,15 @@ import {
Send,
X,
} from "lucide-react";
-import { quickChatKeyframes, quickChatStyles } from "./styles";
+import {
+ AnyclickButton,
+ AnyclickIconButton,
+ AnyclickSurface,
+ AnyclickTextarea,
+ resolveSlotProps,
+ useAnyclickStyle,
+} from "../styling";
+import { quickChatKeyframes } from "./styles";
import type { QuickChatProps } from "./types";
import { useQuickChat } from "./useQuickChat";
@@ -51,14 +59,19 @@ function injectStyles() {
* Loading dots component.
*/
const LoadingDots = React.memo(function LoadingDots() {
+ const { tokens } = useAnyclickStyle();
return (
-
+
{[0, 1, 2].map((i) => (
))}
@@ -82,6 +95,8 @@ export function QuickChat({
initialInput,
onInitialInputConsumed,
}: QuickChatProps) {
+ const adapter = useAnyclickStyle();
+ const { tokens } = adapter;
const inputRef = useRef
(null);
const messagesEndRef = useRef(null);
const [inputFocused, setInputFocused] = useState(false);
@@ -301,48 +316,65 @@ export function QuickChat({
// Use different styles based on pinned state
const containerStyles = isPinned
? {
- ...quickChatStyles.pinnedContainer,
animation: "slideInFromRight 0.25s ease-out",
+ borderLeft: `1px solid ${tokens.border}`,
+ bottom: 0,
+ boxShadow: "-4px 0 24px rgba(15, 23, 42, 0.16)",
+ position: "fixed" as const,
+ right: 0,
+ top: 0,
+ width: "340px",
+ zIndex: tokens.zIndexPinned,
...style,
}
: {
- ...quickChatStyles.container,
animation: "fadeIn 0.15s ease-out",
+ maxHeight: "360px",
...style,
};
+ const headerProps = resolveSlotProps(adapter, "quickChat.header");
+ const messageListProps = resolveSlotProps(adapter, "quickChat.messageList", {
+ expanded: isPinned,
+ });
+ const inputSlotProps = resolveSlotProps(adapter, "quickChat.input");
+
return (
-
+
{/* Header with context badge */}
{!isPinned && (
-
-
+
)}
{/* Context badge */}
{mergedConfig.showRedactionUI && contextChunks.length > 0 && (
<>
-
setShowContext(!showContext)}
- style={{
- ...quickChatStyles.contextBadge,
- ...(showContext ? quickChatStyles.contextBadgeActive : {}),
+ slotName="shared.button"
+ slotState={{
+ selected: showContext,
+ size: "sm",
}}
title="Edit context"
>
@@ -354,82 +386,91 @@ export function QuickChat({
) : (
)}
-
+
{/* All/None toggles when dropdown is open */}
{showContext && (
-
toggleAllChunks(true)}
- style={quickChatStyles.contextToggleSmall}
+ slotName="shared.button"
+ slotState={{ size: "sm" }}
title="Include all"
>
All
-
-
+ toggleAllChunks(false)}
- style={quickChatStyles.contextToggleSmall}
+ slotName="shared.button"
+ slotState={{ size: "sm" }}
title="Exclude all"
>
None
-
+
)}
>
)}
-
+
{messages.length > 0 && (
-
-
+
)}
-
{isPinned ? : }
-
-
+
-
+
{/* Context dropdown - compact list */}
{showContext && contextChunks.length > 0 && (
-
+
{contextChunks.map((chunk) => (
toggleChunk(chunk.id)}
- style={quickChatStyles.contextCheckbox}
/>
- {chunk.label}
+ {chunk.label}
))}
@@ -439,26 +480,36 @@ export function QuickChat({
{mergedConfig.showSuggestions &&
messages.length === 0 &&
suggestedPrompts.length > 0 && (
-
+
{isLoadingSuggestions ? (
) : (
suggestedPrompts.map((prompt) => (
- selectSuggestion(prompt)}
onMouseEnter={() => setHoveredSuggestion(prompt.id)}
onMouseLeave={() => setHoveredSuggestion(null)}
+ slotName="shared.button"
+ slotState={{
+ hovered: hoveredSuggestion === prompt.id,
+ size: "sm",
+ }}
style={{
- ...quickChatStyles.suggestionChip,
- ...(hoveredSuggestion === prompt.id
- ? quickChatStyles.suggestionChipHover
- : {}),
+ justifyContent: "flex-start",
+ whiteSpace: "nowrap",
}}
>
{prompt.text}
-
+
))
)}
@@ -466,25 +517,35 @@ export function QuickChat({
{/* Messages area */}
{/* Keep generic errors visible, but rate-limit uses a sticky banner below */}
{error && !rateLimitNotice && (
-
-
-
{error}
-
+
+ {error}
+ sendMessage()}
- style={quickChatStyles.errorRetry}
+ slotName="quickChat.submit"
+ slotState={{ tone: "danger", size: "sm" }}
>
Retry
-
+
)}
{debugInfo && (
@@ -536,49 +597,75 @@ export function QuickChat({
{message.content}
{message.isStreaming && (
-
+
)}
{message.role === "assistant" &&
!message.isStreaming &&
message.content.endsWith("...") && (
- (truncated)
+
+ (truncated)
+
)}
{message.role === "assistant" &&
!message.isStreaming &&
message.content && (
-
-
+ handleCopy(message.content)}
- style={quickChatStyles.actionButton}
+ slotName="shared.button"
+ slotState={{ size: "sm" }}
>
Copy
-
+
{message.actions?.map((action) => (
-
{action.icon}
{action.label}
-
+
))}
)}
@@ -745,8 +832,15 @@ export function QuickChat({
)}
{/* Input area */}
-
-
+
);
}
diff --git a/packages/anyclick-react/src/ScreenshotPreview.tsx b/packages/anyclick-react/src/ScreenshotPreview.tsx
index 1970a56..6256bb6 100644
--- a/packages/anyclick-react/src/ScreenshotPreview.tsx
+++ b/packages/anyclick-react/src/ScreenshotPreview.tsx
@@ -13,7 +13,15 @@ import {
ShrinkIcon,
XIcon,
} from "lucide-react";
-import { menuStyles, screenshotPreviewStyles as styles } from "./styles";
+import {
+ AnyclickButton,
+ AnyclickIconButton,
+ AnyclickSurface,
+ AnyclickTab,
+ AnyclickTabs,
+ resolveSlotProps,
+ useAnyclickStyle,
+} from "./styling";
import type { ScreenshotPreviewProps } from "./types";
/** Screenshot preview tab types */
@@ -38,6 +46,16 @@ export const ScreenshotPreview = React.memo(function ScreenshotPreview({
}: ScreenshotPreviewProps) {
const [activeTab, setActiveTab] = useState
("element");
const [isExpanded, setIsExpanded] = useState(false);
+ const adapter = useAnyclickStyle();
+ const headerProps = resolveSlotProps(adapter, "screenshot.header");
+ const previewProps = resolveSlotProps(adapter, "screenshot.preview", {
+ expanded: isExpanded,
+ });
+ const emptyProps = resolveSlotProps(adapter, "screenshot.empty");
+ const errorProps = resolveSlotProps(adapter, "screenshot.error", {
+ error: true,
+ });
+ const metaProps = resolveSlotProps(adapter, "screenshot.meta");
// Get error for a specific tab
const getError = (key: TabType): ScreenshotError | undefined => {
@@ -86,51 +104,59 @@ export const ScreenshotPreview = React.memo(function ScreenshotPreview({
if (isLoading) {
return (
-
-
+
+
- Capturing screenshots...
+ Capturing screenshots...
-
+
);
}
if (!screenshots) {
return (
-
-
+
+
-
Screenshots unavailable
-
+ Screenshots unavailable
+
Some elements can't be captured (e.g., gradient text)
-
-
+
Try Again
-
-
+
onConfirm({ capturedAt: new Date().toISOString() })
}
- style={styles.continueButton}
+ slotName="screenshot.action"
+ slotState={{ disabled: isSubmitting, tone: "accent" }}
>
Continue Without
-
+
-
+
);
}
@@ -144,21 +170,43 @@ export const ScreenshotPreview = React.memo(function ScreenshotPreview({
const activeError = getError(activeTab);
return (
-
-
-
Review Screenshots
-
-
{formatBytes(totalSize)}
-
+ Review Screenshots
+
+
+ {formatBytes(totalSize)}
+
+
setIsExpanded(!isExpanded)}
- style={styles.iconButton}
+ slotName="screenshot.action"
title={isExpanded ? "Collapse" : "Expand"}
>
{isExpanded ? (
@@ -166,21 +214,24 @@ export const ScreenshotPreview = React.memo(function ScreenshotPreview({
) : (
)}
-
+
{/* Tabs */}
-
+
{tabs.map((tab) => (
- setActiveTab(tab.key)}
- style={{
- ...styles.tab,
- ...(activeTab === tab.key ? styles.tabActive : {}),
- ...(tab.error && !tab.data ? styles.tabError : {}),
+ slotName={activeTab === tab.key ? "screenshot.tabActive" : "screenshot.tab"}
+ slotState={{
+ error: Boolean(tab.error && !tab.data),
+ selected: activeTab === tab.key,
+ size: "sm",
}}
>
{tab.error && !tab.data && (
@@ -191,35 +242,42 @@ export const ScreenshotPreview = React.memo(function ScreenshotPreview({
)}
{tab.label}
{tab.data && (
-
+
{formatBytes(tab.data.sizeBytes)}
)}
-
+
))}
-
+
{/* Preview image */}
{activeScreenshot ? (
) : activeError ? (
-
+
-
Capture Failed
-
{activeError.message}
+
Capture Failed
+
{activeError.message}
) : (
-
+
No {activeTab} screenshot
@@ -228,41 +286,52 @@ export const ScreenshotPreview = React.memo(function ScreenshotPreview({
{/* Dimensions info */}
{activeScreenshot && (
-
+
{activeScreenshot.width} × {activeScreenshot.height}px
)}
{/* Actions */}
-
-
+
Retake
-
-
-
+
+
Cancel
-
-
+ onConfirm(screenshots)}
- style={{
- ...menuStyles.button,
- ...menuStyles.submitButton,
- ...(isSubmitting ? menuStyles.submitButtonDisabled : {}),
+ slotName="screenshot.action"
+ slotState={{
+ disabled: isSubmitting,
+ loading: isSubmitting,
+ tone: "accent",
}}
>
{isSubmitting ? (
@@ -276,9 +345,9 @@ export const ScreenshotPreview = React.memo(function ScreenshotPreview({
Send
>
)}
-
+
-
+
);
});
diff --git a/packages/anyclick-react/src/index.ts b/packages/anyclick-react/src/index.ts
index 101f639..a69629f 100644
--- a/packages/anyclick-react/src/index.ts
+++ b/packages/anyclick-react/src/index.ts
@@ -12,7 +12,7 @@
* @example
* ```tsx
* import { AnyclickProvider } from "@ewjdev/anyclick-react";
- * import { createHttpAdapter } from "@ewjdev/anyclick-github";
+ * import { createHttpAdapter } from "@ewjdev/anyclick-core";
*
* const adapter = createHttpAdapter({ endpoint: "/api/feedback" });
*
@@ -26,8 +26,6 @@
* ```
*/
"use client";
-// Shared styles (prefixed, no preflight) for anyclick react surfaces.
-import "./styles/tailwind.css";
// ============================================================================
// Core Components
@@ -121,7 +119,29 @@ export { generateProviderId } from "./store";
export { dispatchContextMenuEvent } from "./store";
// ============================================================================
-// UI primitives (shadcn-inspired, prefixed Tailwind)
+// Styling
+// ============================================================================
+export {
+ ANYCLICK_STYLE_SLOTS,
+ AnyclickBadge,
+ AnyclickButton,
+ AnyclickIconButton,
+ AnyclickInput,
+ AnyclickStyleProvider,
+ AnyclickSurface,
+ AnyclickTab,
+ AnyclickTabs,
+ AnyclickTextarea,
+ composeClassNames,
+ composeInlineStyles,
+ fallbackAnyclickStyleAdapter,
+ mergeStyleAdapters,
+ resolveSlotProps,
+ useAnyclickStyle,
+} from "./styling";
+
+// ============================================================================
+// UI primitives
// ============================================================================
export * from "./ui";
@@ -163,6 +183,39 @@ export type {
FeedbackMenuStatus,
} from "./types";
+export type {
+ /** Style adapter contract for slot-based styling */
+ AnyclickStyleAdapter,
+ /** Props for badge primitive overrides */
+ AnyclickBadgeProps,
+ /** Props for button primitive overrides */
+ AnyclickButtonProps,
+ /** Primitive component override registry */
+ AnyclickComponentOverrides,
+ /** Props for input primitive overrides */
+ AnyclickInputProps,
+ /** Props for AnyclickStyleProvider */
+ AnyclickStyleProviderProps,
+ /** Style slot union */
+ AnyclickStyleSlot,
+ /** Slot state passed to adapters */
+ AnyclickSlotState,
+ /** Resolved slot props */
+ AnyclickSlotProps,
+ /** Resolved style adapter after inheritance/merging */
+ AnyclickResolvedStyleAdapter,
+ /** Props for surface primitive overrides */
+ AnyclickSurfaceProps,
+ /** Props for tab primitive overrides */
+ AnyclickTabProps,
+ /** Props for tabs primitive overrides */
+ AnyclickTabsProps,
+ /** Props for textarea primitive overrides */
+ AnyclickTextareaProps,
+ /** Semantic style tokens */
+ AnyclickStyleTokens,
+} from "./styling";
+
// ============================================================================
// Presets
// ============================================================================
diff --git a/packages/anyclick-react/src/styles/tailwind.css b/packages/anyclick-react/src/styles/tailwind.css
deleted file mode 100644
index 93a9787..0000000
--- a/packages/anyclick-react/src/styles/tailwind.css
+++ /dev/null
@@ -1,2 +0,0 @@
-@import "./tokens.css";
-@import "tailwindcss";
diff --git a/packages/anyclick-react/src/styles/tokens.css b/packages/anyclick-react/src/styles/tokens.css
deleted file mode 100644
index 13fe1df..0000000
--- a/packages/anyclick-react/src/styles/tokens.css
+++ /dev/null
@@ -1,24 +0,0 @@
-[data-anyclick-root] {
- /* Base surface and text */
- --ac-surface: #0f172a;
- --ac-surface-muted: #111827;
- --ac-border: #1f2937;
- --ac-text: #e5e7eb;
- --ac-text-muted: #9ca3af;
-
- /* Accents */
- --ac-accent: #22d3ee;
- --ac-accent-muted: #0ea5e9;
- --ac-accent-foreground: #0b1020;
- --ac-destructive: #ef4444;
-
- /* Layout */
- --ac-gap: 0.5rem;
- --ac-pad: 0.75rem;
- --ac-shadow-menu: 0 10px 40px rgba(0, 0, 0, 0.35);
-
- /* Radius */
- --ac-radius-md: 8px;
- --ac-radius-lg: 12px;
- --ac-radius-full: 9999px;
-}
diff --git a/packages/anyclick-react/src/styling/context.tsx b/packages/anyclick-react/src/styling/context.tsx
new file mode 100644
index 0000000..3f23cc4
--- /dev/null
+++ b/packages/anyclick-react/src/styling/context.tsx
@@ -0,0 +1,176 @@
+"use client";
+
+import React, { createContext, useContext, useMemo } from "react";
+import { fallbackAnyclickStyleAdapter } from "./defaults";
+import type {
+ AnyclickResolvedStyleAdapter,
+ AnyclickSlotProps,
+ AnyclickSlotState,
+ AnyclickStyleAdapter,
+ AnyclickStyleProviderProps,
+ AnyclickStyleSlot,
+} from "./types";
+
+function toStyleAdapter(
+ adapter: AnyclickResolvedStyleAdapter,
+): AnyclickStyleAdapter {
+ return {
+ components: adapter.components,
+ isFallback: adapter.isFallback,
+ name: adapter.name,
+ resolveSlot: ({ slot, state }) => adapter.resolveSlot(slot, state),
+ tokens: adapter.tokens,
+ };
+}
+
+export function composeClassNames(
+ ...values: Array
+): string | undefined {
+ const resolved = values
+ .flatMap((value) => (typeof value === "string" ? value.split(/\s+/) : []))
+ .map((value) => value.trim())
+ .filter(Boolean);
+
+ return resolved.length > 0 ? resolved.join(" ") : undefined;
+}
+
+export function composeInlineStyles(
+ ...values: Array
+): React.CSSProperties | undefined {
+ const merged = Object.assign({}, ...values.filter(Boolean));
+ return Object.keys(merged).length > 0 ? merged : undefined;
+}
+
+function createSlotOverrideAdapter(
+ props: Pick<
+ AnyclickStyleProviderProps,
+ "components" | "slotClassNames" | "slotStyles"
+ >,
+): AnyclickStyleAdapter | null {
+ const hasClassNames =
+ props.slotClassNames && Object.keys(props.slotClassNames).length > 0;
+ const hasStyles = props.slotStyles && Object.keys(props.slotStyles).length > 0;
+ const hasComponents =
+ props.components && Object.keys(props.components).length > 0;
+
+ if (!hasClassNames && !hasStyles && !hasComponents) {
+ return null;
+ }
+
+ return {
+ components: props.components,
+ name: "slot-overrides",
+ resolveSlot: ({ slot }) => ({
+ className: props.slotClassNames?.[slot],
+ style: props.slotStyles?.[slot],
+ }),
+ };
+}
+
+export function mergeStyleAdapters(
+ ...adapters: Array
+): AnyclickResolvedStyleAdapter {
+ const activeAdapters = adapters.filter(Boolean) as AnyclickStyleAdapter[];
+ const tokens = Object.assign(
+ {},
+ fallbackAnyclickStyleAdapter.tokens,
+ ...activeAdapters.map((adapter) => adapter.tokens ?? {}),
+ );
+ const components = Object.assign(
+ {},
+ ...activeAdapters.map((adapter) => adapter.components ?? {}),
+ );
+
+ const resolvers = activeAdapters
+ .map((adapter) => adapter.resolveSlot)
+ .filter(Boolean) as NonNullable[];
+
+ const name =
+ activeAdapters.map((adapter) => adapter.name).filter(Boolean).join(" + ") ||
+ "unstyled-fallback";
+
+ return {
+ components,
+ isFallback: activeAdapters.every((adapter) => adapter.isFallback),
+ name,
+ resolveSlot: (
+ slot: AnyclickStyleSlot,
+ state: AnyclickSlotState = {},
+ overrides?: Pick,
+ ) => {
+ const merged: AnyclickSlotProps = {
+ attrs: { "data-anyclick-slot": slot },
+ };
+
+ for (const resolve of resolvers) {
+ const next = resolve({ slot, state, tokens });
+ if (!next) continue;
+ merged.attrs = { ...merged.attrs, ...next.attrs };
+ merged.className = composeClassNames(merged.className, next.className);
+ merged.style = composeInlineStyles(merged.style, next.style);
+ }
+
+ return {
+ attrs: {
+ ...merged.attrs,
+ ...(state.active ? { "data-active": true } : {}),
+ ...(state.disabled ? { "data-disabled": true } : {}),
+ ...(state.error ? { "data-error": true } : {}),
+ ...(state.expanded ? { "data-expanded": true } : {}),
+ ...(state.loading ? { "data-loading": true } : {}),
+ ...(state.pressed ? { "data-pressed": true } : {}),
+ ...(state.selected ? { "data-selected": true } : {}),
+ ...(state.size ? { "data-size": state.size } : {}),
+ ...(state.tone ? { "data-tone": state.tone } : {}),
+ },
+ className: composeClassNames(merged.className, overrides?.className),
+ style: composeInlineStyles(merged.style, overrides?.style),
+ };
+ },
+ tokens,
+ };
+}
+
+const AnyclickStyleContext = createContext(
+ null,
+);
+
+export function resolveSlotProps(
+ adapter: AnyclickResolvedStyleAdapter,
+ slot: AnyclickStyleSlot,
+ state: AnyclickSlotState = {},
+ overrides?: Pick,
+): AnyclickSlotProps {
+ return adapter.resolveSlot(slot, state, overrides);
+}
+
+export function useAnyclickStyle(): AnyclickResolvedStyleAdapter {
+ const context = useContext(AnyclickStyleContext);
+ return context ?? mergeStyleAdapters(fallbackAnyclickStyleAdapter);
+}
+
+export function AnyclickStyleProvider({
+ children,
+ components,
+ slotClassNames,
+ slotStyles,
+ styleAdapter,
+}: AnyclickStyleProviderProps) {
+ const parentStyle = useContext(AnyclickStyleContext);
+
+ const value = useMemo(
+ () =>
+ mergeStyleAdapters(
+ parentStyle ? toStyleAdapter(parentStyle) : fallbackAnyclickStyleAdapter,
+ styleAdapter,
+ createSlotOverrideAdapter({ components, slotClassNames, slotStyles }),
+ ),
+ [components, parentStyle, slotClassNames, slotStyles, styleAdapter],
+ );
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/anyclick-react/src/styling/defaults.ts b/packages/anyclick-react/src/styling/defaults.ts
new file mode 100644
index 0000000..20c1aeb
--- /dev/null
+++ b/packages/anyclick-react/src/styling/defaults.ts
@@ -0,0 +1,406 @@
+import type { CSSProperties } from "react";
+import type {
+ AnyclickResolveSlotInput,
+ AnyclickStyleAdapter,
+ AnyclickStyleTokens,
+} from "./types";
+
+export const defaultAnyclickStyleTokens: AnyclickStyleTokens = {
+ background: "#f8fafc",
+ surface: "#ffffff",
+ surfaceMuted: "#f1f5f9",
+ surfaceSubtle: "#e2e8f0",
+ border: "#cbd5e1",
+ text: "#0f172a",
+ textMuted: "#475569",
+ accent: "#2563eb",
+ accentMuted: "#dbeafe",
+ accentText: "#ffffff",
+ danger: "#dc2626",
+ dangerMuted: "#fee2e2",
+ success: "#16a34a",
+ warning: "#d97706",
+ focusRing: "rgba(37, 99, 235, 0.35)",
+ radiusSm: "6px",
+ radiusMd: "10px",
+ radiusLg: "14px",
+ radiusFull: "9999px",
+ shadowSm: "0 1px 2px rgba(15, 23, 42, 0.08)",
+ shadowMd: "0 10px 24px rgba(15, 23, 42, 0.16)",
+ shadowLg: "0 20px 48px rgba(15, 23, 42, 0.2)",
+ fontFamily:
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
+ fontMono:
+ 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
+ fontSizeXs: "11px",
+ fontSizeSm: "12px",
+ fontSizeMd: "14px",
+ fontSizeLg: "16px",
+ lineHeightTight: 1.2,
+ lineHeightBase: 1.45,
+ spacing2xs: "2px",
+ spacingXs: "4px",
+ spacingSm: "8px",
+ spacingMd: "12px",
+ spacingLg: "16px",
+ spacingXl: "24px",
+ minTouchTarget: "38px",
+ zIndexOverlay: 9998,
+ zIndexSurface: 9999,
+ zIndexPinned: 9998,
+};
+
+function menuVar(variable: string, fallback: string): string {
+ return `var(${variable}, ${fallback})`;
+}
+
+function withInteractiveState(
+ style: CSSProperties,
+ input: AnyclickResolveSlotInput,
+): CSSProperties {
+ const { state, tokens } = input;
+ return {
+ ...style,
+ opacity: state.disabled ? 0.6 : style.opacity,
+ cursor: state.disabled ? "not-allowed" : style.cursor,
+ backgroundColor:
+ state.error &&
+ (style.backgroundColor === tokens.surface ||
+ style.backgroundColor === menuVar("--anyclick-menu-bg", tokens.surface))
+ ? tokens.dangerMuted
+ : state.selected || state.active || state.hovered || state.pressed
+ ? menuVar("--anyclick-menu-hover", tokens.surfaceMuted)
+ : style.backgroundColor,
+ borderColor: state.error
+ ? tokens.danger
+ : style.borderColor,
+ };
+}
+
+function baseButtonStyle(input: AnyclickResolveSlotInput): CSSProperties {
+ const { state, tokens } = input;
+ const tone = state.tone ?? "neutral";
+ const emphasized = tone === "accent";
+ const destructive = tone === "danger";
+
+ return withInteractiveState(
+ {
+ alignItems: "center",
+ appearance: "none",
+ backgroundColor: destructive
+ ? tokens.danger
+ : emphasized
+ ? menuVar("--anyclick-menu-accent", tokens.accent)
+ : menuVar(
+ "--anyclick-menu-cancel-bg",
+ menuVar("--anyclick-menu-bg", tokens.surface),
+ ),
+ border: `1px solid ${
+ destructive || emphasized
+ ? "transparent"
+ : menuVar("--anyclick-menu-border", tokens.border)
+ }`,
+ borderRadius: tokens.radiusSm,
+ color: destructive
+ ? tokens.accentText
+ : emphasized
+ ? menuVar("--anyclick-menu-accent-text", tokens.accentText)
+ : menuVar(
+ "--anyclick-menu-cancel-text",
+ menuVar("--anyclick-menu-text", tokens.text),
+ ),
+ cursor: "pointer",
+ display: "inline-flex",
+ fontFamily: tokens.fontFamily,
+ fontSize: state.size === "sm" ? tokens.fontSizeXs : tokens.fontSizeSm,
+ fontWeight: 600,
+ gap: tokens.spacingXs,
+ justifyContent: "center",
+ minHeight:
+ state.size === "sm" ? "28px" : state.size === "lg" ? "42px" : "34px",
+ padding:
+ state.size === "sm"
+ ? `0 ${tokens.spacingSm}`
+ : `0 ${tokens.spacingMd}`,
+ transition:
+ "background-color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease",
+ },
+ input,
+ );
+}
+
+function baseFieldStyle(input: AnyclickResolveSlotInput): CSSProperties {
+ const { state, tokens } = input;
+ return withInteractiveState(
+ {
+ appearance: "none",
+ backgroundColor: state.disabled
+ ? menuVar("--anyclick-menu-hover", tokens.surfaceMuted)
+ : menuVar("--anyclick-menu-input-bg", tokens.surface),
+ border: `1px solid ${
+ state.error
+ ? tokens.danger
+ : menuVar("--anyclick-menu-input-border", tokens.border)
+ }`,
+ borderRadius: tokens.radiusSm,
+ boxSizing: "border-box",
+ color: menuVar("--anyclick-menu-text", tokens.text),
+ fontFamily: tokens.fontFamily,
+ fontSize: tokens.fontSizeMd,
+ lineHeight: tokens.lineHeightBase,
+ outline: "none",
+ padding: `${tokens.spacingSm} ${tokens.spacingMd}`,
+ width: "100%",
+ },
+ input,
+ );
+}
+
+function slotStyle(input: AnyclickResolveSlotInput): CSSProperties {
+ const { slot, state, tokens } = input;
+
+ switch (slot) {
+ case "menu.overlay":
+ return {
+ inset: 0,
+ position: "fixed",
+ zIndex: tokens.zIndexOverlay,
+ };
+ case "menu.surface":
+ return {
+ backgroundColor: menuVar("--anyclick-menu-bg", tokens.surface),
+ border: `1px solid ${menuVar("--anyclick-menu-border", tokens.border)}`,
+ borderRadius: tokens.radiusMd,
+ boxShadow: tokens.shadowMd,
+ color: menuVar("--anyclick-menu-text", tokens.text),
+ fontFamily: tokens.fontFamily,
+ fontSize: tokens.fontSizeMd,
+ minWidth: "220px",
+ overflow: "hidden",
+ position: "fixed",
+ zIndex: tokens.zIndexSurface,
+ };
+ case "menu.header":
+ case "quickChat.header":
+ case "screenshot.header":
+ case "inspect.header":
+ return {
+ alignItems: "center",
+ borderBottom:
+ slot === "quickChat.header"
+ ? undefined
+ : `1px solid ${menuVar("--anyclick-menu-border", tokens.border)}`,
+ color: menuVar("--anyclick-menu-text-muted", tokens.textMuted),
+ display: "flex",
+ fontSize: tokens.fontSizeXs,
+ fontWeight: 700,
+ gap: tokens.spacingSm,
+ justifyContent: "space-between",
+ letterSpacing: "0.08em",
+ padding: `${tokens.spacingSm} ${tokens.spacingMd}`,
+ textTransform: "uppercase",
+ };
+ case "menu.headerAction":
+ case "menu.dragHandle":
+ case "screenshot.action":
+ case "quickChat.submit":
+ case "inspect.action":
+ return baseButtonStyle(input);
+ case "menu.list":
+ return {
+ display: "flex",
+ flexDirection: "column",
+ padding: 0,
+ };
+ case "menu.item":
+ case "menu.backButton":
+ return withInteractiveState(
+ {
+ alignItems: "center",
+ appearance: "none",
+ backgroundColor: "transparent",
+ border: "none",
+ color: menuVar("--anyclick-menu-text", tokens.text),
+ cursor: "pointer",
+ display: "flex",
+ fontFamily: tokens.fontFamily,
+ fontSize: tokens.fontSizeMd,
+ gap: tokens.spacingMd,
+ minHeight: tokens.minTouchTarget,
+ padding: `${tokens.spacingSm} ${tokens.spacingLg}`,
+ textAlign: "left",
+ width: "100%",
+ },
+ input,
+ );
+ case "menu.itemIcon":
+ return {
+ alignItems: "center",
+ display: "inline-flex",
+ height: "20px",
+ justifyContent: "center",
+ width: "20px",
+ };
+ case "menu.itemLabel":
+ return {
+ alignItems: "center",
+ display: "inline-flex",
+ flex: 1,
+ gap: tokens.spacingSm,
+ };
+ case "menu.itemBadge":
+ case "shared.badge":
+ return {
+ alignItems: "center",
+ backgroundColor:
+ state.tone === "success"
+ ? "rgba(22, 163, 74, 0.12)"
+ : state.tone === "warning"
+ ? "rgba(217, 119, 6, 0.12)"
+ : state.tone === "info" || state.tone === "accent"
+ ? menuVar("--anyclick-menu-hover", tokens.accentMuted)
+ : menuVar("--anyclick-menu-hover", tokens.surfaceMuted),
+ border: `1px solid ${menuVar("--anyclick-menu-border", tokens.border)}`,
+ borderRadius: tokens.radiusFull,
+ color:
+ state.tone === "success"
+ ? tokens.success
+ : state.tone === "warning"
+ ? tokens.warning
+ : state.tone === "info" || state.tone === "accent"
+ ? menuVar("--anyclick-menu-accent", tokens.accent)
+ : menuVar("--anyclick-menu-text-muted", tokens.textMuted),
+ display: "inline-flex",
+ fontSize: tokens.fontSizeXs,
+ fontWeight: 600,
+ gap: tokens.spacing2xs,
+ lineHeight: tokens.lineHeightTight,
+ padding: `2px ${tokens.spacingSm}`,
+ };
+ case "menu.submenuIndicator":
+ return {
+ marginLeft: "auto",
+ opacity: 0.6,
+ };
+ case "comment.section":
+ return {
+ borderTop: `1px solid ${menuVar("--anyclick-menu-border", tokens.border)}`,
+ display: "flex",
+ flexDirection: "column",
+ gap: tokens.spacingSm,
+ padding: `${tokens.spacingMd} ${tokens.spacingLg}`,
+ };
+ case "comment.textarea":
+ case "shared.textarea":
+ case "quickChat.input":
+ return {
+ ...baseFieldStyle(input),
+ minHeight: slot === "quickChat.input" ? "40px" : "88px",
+ resize: slot === "quickChat.input" ? "none" : "vertical",
+ };
+ case "comment.primaryAction":
+ return baseButtonStyle({
+ ...input,
+ state: { ...state, tone: "accent" },
+ });
+ case "comment.secondaryAction":
+ return baseButtonStyle(input);
+ case "screenshot.surface":
+ case "quickChat.surface":
+ case "inspect.surface":
+ return {
+ backgroundColor: menuVar("--anyclick-menu-bg", tokens.surface),
+ border:
+ slot === "quickChat.surface" && state.expanded
+ ? `1px solid ${menuVar("--anyclick-menu-border", tokens.border)}`
+ : `1px solid ${menuVar("--anyclick-menu-border", tokens.border)}`,
+ borderRadius: state.expanded ? "0" : tokens.radiusMd,
+ boxShadow: state.expanded ? tokens.shadowLg : tokens.shadowSm,
+ color: menuVar("--anyclick-menu-text", tokens.text),
+ display: "flex",
+ flexDirection: "column",
+ fontFamily: tokens.fontFamily,
+ fontSize: tokens.fontSizeMd,
+ overflow: "hidden",
+ width: "100%",
+ };
+ case "screenshot.tab":
+ case "screenshot.tabActive":
+ return baseButtonStyle({
+ ...input,
+ state: {
+ ...state,
+ tone: slot === "screenshot.tabActive" || state.selected
+ ? "accent"
+ : "neutral",
+ size: "sm",
+ },
+ });
+ case "screenshot.preview":
+ return {
+ alignItems: "center",
+ backgroundColor: menuVar("--anyclick-menu-hover", tokens.surfaceMuted),
+ border: `1px solid ${menuVar("--anyclick-menu-border", tokens.border)}`,
+ borderRadius: tokens.radiusSm,
+ display: "flex",
+ justifyContent: "center",
+ minHeight: state.expanded ? "60vh" : "160px",
+ overflow: "hidden",
+ padding: tokens.spacingSm,
+ };
+ case "screenshot.empty":
+ case "screenshot.error":
+ return {
+ alignItems: "center",
+ color:
+ slot === "screenshot.error"
+ ? tokens.danger
+ : menuVar("--anyclick-menu-text-muted", tokens.textMuted),
+ display: "flex",
+ flexDirection: "column",
+ gap: tokens.spacingSm,
+ justifyContent: "center",
+ padding: tokens.spacingXl,
+ textAlign: "center",
+ };
+ case "screenshot.meta":
+ return {
+ color: menuVar("--anyclick-menu-text-muted", tokens.textMuted),
+ fontSize: tokens.fontSizeXs,
+ textAlign: "center",
+ };
+ case "quickChat.messageList":
+ return {
+ display: "flex",
+ flex: 1,
+ flexDirection: "column",
+ gap: tokens.spacingSm,
+ maxHeight: state.expanded ? undefined : "180px",
+ minHeight: "48px",
+ overflowY: "auto",
+ padding: state.expanded ? tokens.spacingLg : `${tokens.spacingSm} ${tokens.spacingMd}`,
+ };
+ case "inspect.content":
+ return {
+ display: "flex",
+ flexDirection: "column",
+ gap: tokens.spacingSm,
+ padding: tokens.spacingMd,
+ };
+ case "shared.button":
+ return baseButtonStyle(input);
+ case "shared.input":
+ return baseFieldStyle(input);
+ default:
+ return {};
+ }
+}
+
+export const fallbackAnyclickStyleAdapter: AnyclickStyleAdapter = {
+ isFallback: true,
+ name: "unstyled-fallback",
+ resolveSlot: (input) => ({
+ style: slotStyle(input),
+ }),
+ tokens: defaultAnyclickStyleTokens,
+};
diff --git a/packages/anyclick-react/src/styling/index.ts b/packages/anyclick-react/src/styling/index.ts
new file mode 100644
index 0000000..0960df5
--- /dev/null
+++ b/packages/anyclick-react/src/styling/index.ts
@@ -0,0 +1,38 @@
+export { fallbackAnyclickStyleAdapter } from "./defaults";
+export {
+ AnyclickStyleProvider,
+ composeClassNames,
+ composeInlineStyles,
+ mergeStyleAdapters,
+ resolveSlotProps,
+ useAnyclickStyle,
+} from "./context";
+export {
+ AnyclickBadge,
+ AnyclickButton,
+ AnyclickIconButton,
+ AnyclickInput,
+ AnyclickSurface,
+ AnyclickTab,
+ AnyclickTabs,
+ AnyclickTextarea,
+} from "./primitives";
+export type {
+ AnyclickBadgeProps,
+ AnyclickButtonProps,
+ AnyclickComponentOverrides,
+ AnyclickInputProps,
+ AnyclickResolvedStyleAdapter,
+ AnyclickSlotComponentProps,
+ AnyclickSlotProps,
+ AnyclickSlotState,
+ AnyclickStyleAdapter,
+ AnyclickStyleProviderProps,
+ AnyclickStyleSlot,
+ AnyclickStyleTokens,
+ AnyclickSurfaceProps,
+ AnyclickTabProps,
+ AnyclickTabsProps,
+ AnyclickTextareaProps,
+} from "./types";
+export { ANYCLICK_STYLE_SLOTS } from "./types";
diff --git a/packages/anyclick-react/src/styling/primitives.tsx b/packages/anyclick-react/src/styling/primitives.tsx
new file mode 100644
index 0000000..ef2f1ba
--- /dev/null
+++ b/packages/anyclick-react/src/styling/primitives.tsx
@@ -0,0 +1,228 @@
+"use client";
+
+import React, { forwardRef } from "react";
+import { resolveSlotProps, useAnyclickStyle } from "./context";
+import type {
+ AnyclickBadgeProps,
+ AnyclickButtonProps,
+ AnyclickInputProps,
+ AnyclickSurfaceProps,
+ AnyclickTabProps,
+ AnyclickTabsProps,
+ AnyclickTextareaProps,
+} from "./types";
+
+const DefaultSurface = forwardRef(
+ (
+ { className, slotName = "menu.surface", slotState, style, ...props },
+ ref,
+ ) => {
+ const adapter = useAnyclickStyle();
+ const resolved = resolveSlotProps(adapter, slotName, slotState, {
+ className,
+ style,
+ });
+
+ return
;
+ },
+);
+
+DefaultSurface.displayName = "DefaultAnyclickSurface";
+
+const DefaultButton = forwardRef(
+ (
+ { className, slotName = "shared.button", slotState, style, ...props },
+ ref,
+ ) => {
+ const adapter = useAnyclickStyle();
+ const resolved = resolveSlotProps(adapter, slotName, slotState, {
+ className,
+ style,
+ });
+
+ return ;
+ },
+);
+
+DefaultButton.displayName = "DefaultAnyclickButton";
+
+const DefaultInput = forwardRef(
+ (
+ { className, slotName = "shared.input", slotState, style, ...props },
+ ref,
+ ) => {
+ const adapter = useAnyclickStyle();
+ const resolved = resolveSlotProps(adapter, slotName, slotState, {
+ className,
+ style,
+ });
+
+ return ;
+ },
+);
+
+DefaultInput.displayName = "DefaultAnyclickInput";
+
+const DefaultTextarea = forwardRef(
+ (
+ { className, slotName = "shared.textarea", slotState, style, ...props },
+ ref,
+ ) => {
+ const adapter = useAnyclickStyle();
+ const resolved = resolveSlotProps(adapter, slotName, slotState, {
+ className,
+ style,
+ });
+
+ return ;
+ },
+);
+
+DefaultTextarea.displayName = "DefaultAnyclickTextarea";
+
+const DefaultTabs = forwardRef(
+ (
+ {
+ className,
+ slotName = "screenshot.header",
+ slotState,
+ style,
+ ...props
+ },
+ ref,
+ ) => {
+ const adapter = useAnyclickStyle();
+ const resolved = resolveSlotProps(adapter, slotName, slotState, {
+ className,
+ style,
+ });
+
+ return
;
+ },
+);
+
+DefaultTabs.displayName = "DefaultAnyclickTabs";
+
+const DefaultTab = forwardRef(
+ (
+ { className, slotName = "screenshot.tab", slotState, style, ...props },
+ ref,
+ ) => {
+ const adapter = useAnyclickStyle();
+ const resolved = resolveSlotProps(adapter, slotName, slotState, {
+ className,
+ style,
+ });
+
+ return ;
+ },
+);
+
+DefaultTab.displayName = "DefaultAnyclickTab";
+
+const DefaultBadge = forwardRef(
+ (
+ { className, slotName = "shared.badge", slotState, style, ...props },
+ ref,
+ ) => {
+ const adapter = useAnyclickStyle();
+ const resolved = resolveSlotProps(adapter, slotName, slotState, {
+ className,
+ style,
+ });
+
+ return ;
+ },
+);
+
+DefaultBadge.displayName = "DefaultAnyclickBadge";
+
+export const AnyclickSurface = forwardRef(
+ (props, ref) => {
+ const { components } = useAnyclickStyle();
+ const Component = components.Surface ?? DefaultSurface;
+
+ return ;
+ },
+);
+
+AnyclickSurface.displayName = "AnyclickSurface";
+
+export const AnyclickButton = forwardRef(
+ (props, ref) => {
+ const { components } = useAnyclickStyle();
+ const Component = components.Button ?? DefaultButton;
+
+ return ;
+ },
+);
+
+AnyclickButton.displayName = "AnyclickButton";
+
+export const AnyclickIconButton = forwardRef<
+ HTMLButtonElement,
+ AnyclickButtonProps
+>((props, ref) => {
+ const { components } = useAnyclickStyle();
+ const Component = components.IconButton ?? DefaultButton;
+
+ return ;
+});
+
+AnyclickIconButton.displayName = "AnyclickIconButton";
+
+export const AnyclickInput = forwardRef(
+ (props, ref) => {
+ const { components } = useAnyclickStyle();
+ const Component = components.Input ?? DefaultInput;
+
+ return ;
+ },
+);
+
+AnyclickInput.displayName = "AnyclickInput";
+
+export const AnyclickTextarea = forwardRef<
+ HTMLTextAreaElement,
+ AnyclickTextareaProps
+>((props, ref) => {
+ const { components } = useAnyclickStyle();
+ const Component = components.Textarea ?? DefaultTextarea;
+
+ return ;
+});
+
+AnyclickTextarea.displayName = "AnyclickTextarea";
+
+export const AnyclickTabs = forwardRef(
+ (props, ref) => {
+ const { components } = useAnyclickStyle();
+ const Component = components.Tabs ?? DefaultTabs;
+
+ return ;
+ },
+);
+
+AnyclickTabs.displayName = "AnyclickTabs";
+
+export const AnyclickTab = forwardRef(
+ (props, ref) => {
+ const { components } = useAnyclickStyle();
+ const Component = components.Tab ?? DefaultTab;
+
+ return ;
+ },
+);
+
+AnyclickTab.displayName = "AnyclickTab";
+
+export const AnyclickBadge = forwardRef(
+ (props, ref) => {
+ const { components } = useAnyclickStyle();
+ const Component = components.Badge ?? DefaultBadge;
+
+ return ;
+ },
+);
+
+AnyclickBadge.displayName = "AnyclickBadge";
diff --git a/packages/anyclick-react/src/styling/types.ts b/packages/anyclick-react/src/styling/types.ts
new file mode 100644
index 0000000..205d5ef
--- /dev/null
+++ b/packages/anyclick-react/src/styling/types.ts
@@ -0,0 +1,197 @@
+import type {
+ ButtonHTMLAttributes,
+ CSSProperties,
+ ComponentType,
+ HTMLAttributes,
+ InputHTMLAttributes,
+ ReactNode,
+ TextareaHTMLAttributes,
+} from "react";
+
+export const ANYCLICK_STYLE_SLOTS = [
+ "menu.overlay",
+ "menu.surface",
+ "menu.header",
+ "menu.headerAction",
+ "menu.list",
+ "menu.item",
+ "menu.itemIcon",
+ "menu.itemLabel",
+ "menu.itemBadge",
+ "menu.submenuIndicator",
+ "menu.backButton",
+ "menu.dragHandle",
+ "comment.section",
+ "comment.textarea",
+ "comment.primaryAction",
+ "comment.secondaryAction",
+ "screenshot.surface",
+ "screenshot.header",
+ "screenshot.tab",
+ "screenshot.tabActive",
+ "screenshot.preview",
+ "screenshot.empty",
+ "screenshot.error",
+ "screenshot.meta",
+ "screenshot.action",
+ "quickChat.surface",
+ "quickChat.header",
+ "quickChat.messageList",
+ "quickChat.input",
+ "quickChat.submit",
+ "inspect.surface",
+ "inspect.header",
+ "inspect.content",
+ "inspect.action",
+ "shared.button",
+ "shared.input",
+ "shared.textarea",
+ "shared.badge",
+] as const;
+
+export type AnyclickStyleSlot = (typeof ANYCLICK_STYLE_SLOTS)[number];
+
+export interface AnyclickStyleTokens {
+ background: string;
+ surface: string;
+ surfaceMuted: string;
+ surfaceSubtle: string;
+ border: string;
+ text: string;
+ textMuted: string;
+ accent: string;
+ accentMuted: string;
+ accentText: string;
+ danger: string;
+ dangerMuted: string;
+ success: string;
+ warning: string;
+ focusRing: string;
+ radiusSm: string;
+ radiusMd: string;
+ radiusLg: string;
+ radiusFull: string;
+ shadowSm: string;
+ shadowMd: string;
+ shadowLg: string;
+ fontFamily: string;
+ fontMono: string;
+ fontSizeXs: string;
+ fontSizeSm: string;
+ fontSizeMd: string;
+ fontSizeLg: string;
+ lineHeightTight: number;
+ lineHeightBase: number;
+ spacing2xs: string;
+ spacingXs: string;
+ spacingSm: string;
+ spacingMd: string;
+ spacingLg: string;
+ spacingXl: string;
+ minTouchTarget: string;
+ zIndexOverlay: number;
+ zIndexSurface: number;
+ zIndexPinned: number;
+}
+
+export interface AnyclickSlotState {
+ active?: boolean;
+ disabled?: boolean;
+ hovered?: boolean;
+ pressed?: boolean;
+ expanded?: boolean;
+ selected?: boolean;
+ error?: boolean;
+ loading?: boolean;
+ tone?:
+ | "accent"
+ | "danger"
+ | "info"
+ | "neutral"
+ | "success"
+ | "warning"
+ | (string & {});
+ size?: "sm" | "md" | "lg" | (string & {});
+}
+
+export interface AnyclickSlotProps {
+ attrs?: Record;
+ className?: string;
+ style?: CSSProperties;
+}
+
+export interface AnyclickResolveSlotInput {
+ slot: AnyclickStyleSlot;
+ state: AnyclickSlotState;
+ tokens: AnyclickStyleTokens;
+}
+
+export interface AnyclickSlotComponentProps {
+ children?: ReactNode;
+ className?: string;
+ slotName?: AnyclickStyleSlot;
+ slotState?: AnyclickSlotState;
+ style?: CSSProperties;
+}
+
+export type AnyclickSurfaceProps = HTMLAttributes &
+ AnyclickSlotComponentProps;
+
+export type AnyclickButtonProps = ButtonHTMLAttributes &
+ AnyclickSlotComponentProps;
+
+export type AnyclickInputProps = InputHTMLAttributes &
+ AnyclickSlotComponentProps;
+
+export type AnyclickTextareaProps =
+ TextareaHTMLAttributes & AnyclickSlotComponentProps;
+
+export type AnyclickTabsProps = HTMLAttributes &
+ AnyclickSlotComponentProps;
+
+export type AnyclickTabProps = ButtonHTMLAttributes &
+ AnyclickSlotComponentProps;
+
+export type AnyclickBadgeProps = HTMLAttributes &
+ AnyclickSlotComponentProps;
+
+export interface AnyclickComponentOverrides {
+ Surface: ComponentType;
+ Button: ComponentType;
+ IconButton: ComponentType;
+ Input: ComponentType;
+ Textarea: ComponentType;
+ Tabs: ComponentType;
+ Tab: ComponentType;
+ Badge: ComponentType;
+}
+
+export interface AnyclickStyleAdapter {
+ components?: Partial;
+ isFallback?: boolean;
+ name?: string;
+ resolveSlot?: (
+ input: AnyclickResolveSlotInput,
+ ) => AnyclickSlotProps | null | undefined;
+ tokens?: Partial;
+}
+
+export interface AnyclickStyleProviderProps {
+ children: ReactNode;
+ components?: Partial;
+ slotClassNames?: Partial>;
+ slotStyles?: Partial>;
+ styleAdapter?: AnyclickStyleAdapter;
+}
+
+export interface AnyclickResolvedStyleAdapter {
+ components: Partial;
+ isFallback: boolean;
+ name: string;
+ resolveSlot: (
+ slot: AnyclickStyleSlot,
+ state?: AnyclickSlotState,
+ overrides?: Pick,
+ ) => AnyclickSlotProps;
+ tokens: AnyclickStyleTokens;
+}
diff --git a/packages/anyclick-react/src/types.ts b/packages/anyclick-react/src/types.ts
index 4a9a449..90284ab 100644
--- a/packages/anyclick-react/src/types.ts
+++ b/packages/anyclick-react/src/types.ts
@@ -18,6 +18,11 @@ import type {
} from "@ewjdev/anyclick-core";
import { CompactModeConfig } from "./InspectDialog/InspectSimple";
import type { QuickChatConfig } from "./QuickChat/types";
+import type {
+ AnyclickComponentOverrides,
+ AnyclickStyleAdapter,
+ AnyclickStyleSlot,
+} from "./styling/types";
// ============================================================================
// Theme Configuration
@@ -55,9 +60,9 @@ export interface AnyclickTheme {
funMode?: boolean | FunModeThemeConfig;
/** Configuration for element highlighting */
highlightConfig?: HighlightConfig;
- /** Custom class name for the context menu */
+ /** @deprecated Use `slotClassNames["menu.surface"]` or a style adapter instead. */
menuClassName?: string;
- /** Custom styles for the context menu */
+ /** @deprecated Use `slotStyles["menu.surface"]` or a style adapter instead. */
menuStyle?: CSSProperties;
/** Configuration for screenshot capture */
screenshotConfig?: ScreenshotConfig;
@@ -346,13 +351,13 @@ export interface AnyclickProviderProps {
maxInnerTextLength?: number;
/** Maximum length for outerHTML capture */
maxOuterHTMLLength?: number;
- /** Custom class name for the context menu */
+ /** @deprecated Use `slotClassNames["menu.surface"]` or a style adapter instead. */
menuClassName?: string;
/** Custom menu items (defaults to Issue, Feature, Like) */
menuItems?: ContextMenuItem[];
/** Menu positioning mode (default: 'inView') */
menuPositionMode?: MenuPositionMode;
- /** Custom styles for the context menu */
+ /** @deprecated Use `slotStyles["menu.surface"]` or a style adapter instead. */
menuStyle?: CSSProperties;
/** Additional metadata to include with every submission */
metadata?: Record;
@@ -387,6 +392,14 @@ export interface AnyclickProviderProps {
* Set to null or { disabled: true } to disable anyclick in this subtree.
*/
theme?: AnyclickTheme | null;
+ /** Slot-level class overrides merged after inherited style providers. */
+ slotClassNames?: Partial>;
+ /** Slot-level inline style overrides merged after inherited style providers. */
+ slotStyles?: Partial>;
+ /** Primitive component overrides for DOM replacement scenarios. */
+ components?: Partial;
+ /** Local style adapter merged on top of the nearest AnyclickStyleProvider. */
+ styleAdapter?: AnyclickStyleAdapter;
/** Duration in ms to hold touch before triggering context menu (default: 500) */
touchHoldDurationMs?: number;
/** Maximum movement in px before touch hold is cancelled (default: 10) */
diff --git a/packages/anyclick-react/src/ui/button.tsx b/packages/anyclick-react/src/ui/button.tsx
index 9f85ba8..67e07a9 100644
--- a/packages/anyclick-react/src/ui/button.tsx
+++ b/packages/anyclick-react/src/ui/button.tsx
@@ -1,38 +1,26 @@
-import { ButtonHTMLAttributes, forwardRef } from "react";
-import { cn } from "../utils/cn";
+import type { ButtonHTMLAttributes } from "react";
+import { forwardRef } from "react";
+import { AnyclickButton } from "../styling";
type ButtonProps = ButtonHTMLAttributes & {
variant?: "default" | "ghost" | "outline" | "destructive";
size?: "sm" | "md" | "lg";
};
-const variantClasses: Record, string> = {
- default:
- "ac-bg-accent ac-text-accent-foreground hover:ac-bg-accent-muted ac-border ac-border-border",
- ghost: "ac-bg-transparent hover:ac-bg-surface-muted ac-text-text",
- outline:
- "ac-bg-transparent ac-text-text ac-border ac-border-border hover:ac-bg-surface-muted",
- destructive:
- "ac-bg-destructive ac-text-accent-foreground hover:ac-bg-destructive/80",
-};
-
-const sizeClasses: Record, string> = {
- sm: "ac-h-8 ac-px-3 ac-text-sm",
- md: "ac-h-10 ac-px-4 ac-text-sm",
- lg: "ac-h-11 ac-px-5 ac-text-base",
-};
-
export const Button = forwardRef(
- ({ className, variant = "default", size = "md", ...props }, ref) => {
+ ({ variant = "default", size = "md", ...props }, ref) => {
+ const tone =
+ variant === "default"
+ ? "accent"
+ : variant === "destructive"
+ ? "danger"
+ : "neutral";
+
return (
-
);
diff --git a/packages/anyclick-react/src/ui/input.tsx b/packages/anyclick-react/src/ui/input.tsx
index 641c4bc..521c8c9 100644
--- a/packages/anyclick-react/src/ui/input.tsx
+++ b/packages/anyclick-react/src/ui/input.tsx
@@ -1,21 +1,11 @@
-import { InputHTMLAttributes, forwardRef } from "react";
-import { cn } from "../utils/cn";
+import type { InputHTMLAttributes } from "react";
+import { forwardRef } from "react";
+import { AnyclickInput } from "../styling";
type InputProps = InputHTMLAttributes;
-export const Input = forwardRef(
- ({ className, ...props }, ref) => {
- return (
-
- );
- },
-);
+export const Input = forwardRef((props, ref) => {
+ return ;
+});
Input.displayName = "Input";
diff --git a/packages/anyclick-react/tsup.config.ts b/packages/anyclick-react/tsup.config.ts
index 8e4b98c..4a231c7 100644
--- a/packages/anyclick-react/tsup.config.ts
+++ b/packages/anyclick-react/tsup.config.ts
@@ -10,9 +10,4 @@ export default defineConfig({
"process.env.NODE_ENV": JSON.stringify("production"),
process: "{}",
},
- esbuildOptions(options) {
- // Allow tailwindcss v4 style export to resolve
- options.conditions = ["style", "import", "module", "default"];
- options.banner ??= {};
- },
});
diff --git a/packages/anyclick-react/vite.config.ts b/packages/anyclick-react/vite.config.ts
index 40633f7..009a5c9 100644
--- a/packages/anyclick-react/vite.config.ts
+++ b/packages/anyclick-react/vite.config.ts
@@ -1,18 +1,12 @@
-import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
-import path from "path";
import { defineConfig } from "vite";
// Library-friendly Vite config: externals react/react-dom, keeps tsup for dts.
export default defineConfig({
- plugins: [react(), tailwindcss()],
- css: {
- // Let Tailwind/PostCSS handle styling; classes are prefixed via config.
- postcss: path.resolve(__dirname, "postcss.config.cjs"),
- },
+ plugins: [react()],
build: {
lib: {
- entry: path.resolve(__dirname, "src/index.ts"),
+ entry: new URL("./src/index.ts", import.meta.url).pathname,
name: "AnyclickReact",
fileName: (fmt) => `index.${fmt}.js`,
formats: ["es", "cjs"],
diff --git a/scripts/update-releases-json.mjs b/scripts/update-releases-json.mjs
index 42fbdb1..b18d0d4 100644
--- a/scripts/update-releases-json.mjs
+++ b/scripts/update-releases-json.mjs
@@ -258,3 +258,5 @@ updateReleasesJson();
+
+