flue flow fixes

This commit is contained in:
-Puter
2026-07-31 23:43:49 +05:30
parent bb4bd9e0d3
commit 8bd3953440
52 changed files with 814 additions and 484 deletions

View File

@@ -1,11 +1,11 @@
import { ThemeProvider as NextThemesProvider } from "next-themes";
import * as React from "react";
export function ThemeProvider({
export const ThemeProvider = ({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
}: React.ComponentProps<typeof NextThemesProvider>) => (
<NextThemesProvider {...props}>{children}</NextThemesProvider>
);
export { useTheme } from "next-themes";

View File

@@ -56,4 +56,39 @@ describe("projectConversation", () => {
{ state: "done", text: "Captured the request.", type: "text" },
]);
});
test("clears a historical failure after a newer turn succeeds", () => {
const state = projectConversation([
row({
error: "Old admission failure",
messageId: "assistant-failed",
role: "assistant",
status: "failed",
}),
row({
messageId: "assistant-completed",
rawText: "Recovered",
role: "assistant",
status: "completed",
}),
]);
expect(state.failedError).toBeUndefined();
});
test("clears historical pending state after a newer turn completes", () => {
const state = projectConversation([
row({
messageId: "assistant-stale",
role: "assistant",
status: "running",
}),
row({
messageId: "assistant-completed",
rawText: "Recovered",
role: "assistant",
status: "completed",
}),
]);
expect(state.pending).toBe(false);
});
});

View File

@@ -20,7 +20,18 @@ export interface ConversationRow {
| "running";
}
const latestAssistant = (
rows: readonly ConversationRow[]
): ConversationRow | undefined =>
rows.findLast((row) => row.role === "assistant");
const latestTerminalError = (row: ConversationRow | undefined) =>
row?.status === "failed"
? (row.error ?? "Conversation turn failed")
: undefined;
export const projectConversation = (rows: readonly ConversationRow[]) => {
const activeAssistant = latestAssistant(rows);
const streaming = rows.some(
(row) =>
row.role === "assistant" &&
@@ -28,9 +39,7 @@ export const projectConversation = (rows: readonly ConversationRow[]) => {
row.rawText.length > 0
);
return {
failedError: rows.findLast(
(row) => row.role === "assistant" && row.status === "failed"
)?.error,
failedError: latestTerminalError(activeAssistant),
messages: rows
.filter(
(row) =>
@@ -65,13 +74,10 @@ export const projectConversation = (rows: readonly ConversationRow[]) => {
],
role: row.role,
})),
pending: rows.some(
(row) =>
row.role === "assistant" &&
(row.status === "queued" ||
row.status === "dispatching" ||
row.status === "running")
),
pending:
activeAssistant?.status === "queued" ||
activeAssistant?.status === "dispatching" ||
activeAssistant?.status === "running",
streaming,
};
};

View File

@@ -2,22 +2,27 @@ import path from "node:path";
import { reactRouter } from "@react-router/dev/vite";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite-plus";
import { defineConfig, loadEnv } from "vite-plus";
export default defineConfig({
envDir: path.resolve(import.meta.dirname, "../.."),
plugins: [tailwindcss(), reactRouter()],
resolve: {
dedupe: ["convex", "react", "react-dom"],
tsconfigPaths: true,
},
server: {
allowedHosts: true,
proxy: {
"/api/auth": {
changeOrigin: true,
target: "https://befitting-dalmatian-161.convex.site",
export default defineConfig(({ mode }) => {
const envDir = path.resolve(import.meta.dirname, "../..");
const env = loadEnv(mode, envDir, "");
return {
envDir,
plugins: [tailwindcss(), reactRouter()],
resolve: {
dedupe: ["convex", "react", "react-dom"],
tsconfigPaths: true,
},
server: {
allowedHosts: true,
proxy: {
"/api/auth": {
changeOrigin: true,
target: env.CONVEX_SITE_URL,
},
},
},
},
};
});

View File

@@ -119,10 +119,10 @@ Then configure the deployment-scoped values from `packages/backend`:
cd packages/backend
bunx convex env set SITE_URL 'http://localhost:5173'
bunx convex env set FLUE_DB_TOKEN '<same value as .env>'
bunx convex env set FLUE_URL 'http://<host-reachable-ip>:3585'
bunx convex env set FLUE_URL 'http://<tailscale-ip>:3585'
```
Convex runs in the cloud, so `FLUE_URL` must be reachable from the Convex deployment. `127.0.0.1` and `localhost` point at Convex's machine, not your laptop. For the current Mac setup, expose Flue over Tailscale and use the Mac's Tailscale address, for example `http://100.x.y.z:3585`.
Convex runs in the cloud, so `FLUE_URL` must be reachable from the deployment. `127.0.0.1` and `localhost` point at Convex's machine, not your laptop. For the current Mac setup, expose Flue on the Mac's Tailscale address, for example `http://100.x.y.z:3585`.
`SITE_URL` is deployment-scoped and single-valued. Set it to the exact browser origin currently being tested:
@@ -143,10 +143,14 @@ Run each process in its own terminal. The order matters for the AgentOS path.
### 1. Start Convex development
```bash
bun run dev:server
# Publish once after backend changes
pnpm --filter @code/backend dev:once
# Watch and republish while developing
pnpm dev:server
```
This watches and publishes functions to the configured Convex cloud development deployment.
Both commands run from `packages/backend` and explicitly load the repository-root `.env` with `--env-file=../../.env`. `packages/backend/.env.local` remains Convex CLI metadata for the selected deployment; keep the shared runtime settings in the root `.env`.
### 2. Start the whole stack

View File

@@ -195,9 +195,9 @@ Prefer explicit state machines over booleans.
Bad:
```ts
isRunning: boolean
isDone: boolean
hasError: boolean
isRunning: boolean;
isDone: boolean;
hasError: boolean;
```
Good:
@@ -213,7 +213,7 @@ type AttemptStatus =
| "verification_failed"
| "budget_exhausted"
| "cancelled"
| "permanent_failure"
| "permanent_failure";
```
Transitions must validate current version/state.
@@ -281,38 +281,44 @@ Design replaceable boundaries, then ship one path.
```md
## Implemented
- ...
## Behavior
- ...
## Verification run
- `command` — pass/fail
- ...
## Evidence
- candidate SHA/artifacts
- ...
## Design deviations
- none / ...
## Remaining risks or blockers
- none / ...
```
## 16. Required reading by task type
| Task | Read |
|---|---|
| product/domain | product.md, glossary.md |
| UI | design.md, product.md |
| actors/state | tech.md, glossary.md |
| orchestration | dev-loop.md, tech.md |
| current sequencing | slices.md, dev-plan.md |
| verification/evals | evaluation.md, dev-loop.md |
| provider adapter | tech.md plus provider docs |
| broad feature | agent-context.md + all relevant sections |
| Task | Read |
| ------------------ | ---------------------------------------- |
| product/domain | product.md, glossary.md |
| UI | design.md, product.md |
| actors/state | tech.md, glossary.md |
| orchestration | dev-loop.md, tech.md |
| current sequencing | slices.md, dev-plan.md |
| verification/evals | evaluation.md, dev-loop.md |
| provider adapter | tech.md plus provider docs |
| broad feature | agent-context.md + all relevant sections |
## 17. Stop and escalate when

View File

@@ -644,7 +644,7 @@ Answers are durable Decisions and resume the same Work.
## 17. Resolver decision table
| Condition | Resolver action |
|---|---|
| --- | --- |
| executable slice exists | start bounded Attempt |
| transient infrastructure/model failure | retry by policy |
| candidate fails checks | create repair Attempt with evidence |

View File

@@ -6,25 +6,25 @@ You have shell access and may use the Paseo CLI to create and manage other Codex
All child agents must use:
--provider codex/glm-5.2
--provider codex/glm-5.2
The current repository is the Zopu monorepo. It already contains product documentation, design documentation, technical documentation, Effect primitives, Convex backend code, web/mobile/desktop applications, agent code, project-management primitives, Signals, and partial AgentOS/Rivet integration.
The starting branch is expected to be:
feat/projects-backend
feat/projects-backend
Verify this rather than blindly assuming it.
The final integration branch should be:
dogfood/v0
dogfood/v0
Your job is to manage the entire implementation through parallel isolated worktrees.
======================================================================
1. PRODUCT OBJECTIVE
======================================================================
1. PRODUCT OBJECTIVE ======================================================================
Ship the smallest complete Zopu dogfooding loop for the Zopu repository itself:
@@ -67,25 +67,11 @@ Ship the smallest complete Zopu dogfooding loop for the Zopu repository itself:
The completed proof should be something like:
User:
“Add a health endpoint that exposes the current build commit.”
User: “Add a health endpoint that exposes the current build commit.”
Result:
✓ exact message evidence stored
✓ Signal created
✓ Work Unit created or updated
✓ work started
✓ Orb provisioned
✓ OpenCode session started
✓ repository changed
✓ tests executed
✓ branch committed and pushed
✓ pull request opened
✓ work card updated
✓ contextual follow-up reaches the active work
Result: ✓ exact message evidence stored ✓ Signal created ✓ Work Unit created or updated ✓ work started ✓ Orb provisioned ✓ OpenCode session started ✓ repository changed ✓ tests executed ✓ branch committed and pushed ✓ pull request opened ✓ work card updated ✓ contextual follow-up reaches the active work
======================================================================
2. HARD SCOPE
====================================================================== 2. HARD SCOPE
======================================================================
This is a narrow dogfooding implementation.
@@ -128,6 +114,7 @@ ProjectIssue may remain the backend representation of a Work Unit for this versi
Project events may represent execution steps and activity.
Project artifacts may represent:
- work.md;
- steps.md;
- agent reports;
@@ -136,8 +123,7 @@ Project artifacts may represent:
- commits;
- pull requests.
======================================================================
3. OPERATING RULES
====================================================================== 3. OPERATING RULES
======================================================================
Before changing anything:
@@ -197,22 +183,18 @@ When an agent is blocked:
Do not stop after spawning agents. You must monitor and integrate them.
======================================================================
4. INITIAL BRANCH PREPARATION
====================================================================== 4. INITIAL BRANCH PREPARATION
======================================================================
Inspect the current branch.
If currently on feat/projects-backend:
git pull --ff-only
git switch -c dogfood/v0
git push -u origin dogfood/v0
git pull --ff-only git switch -c dogfood/v0 git push -u origin dogfood/v0
If dogfood/v0 already exists:
git switch dogfood/v0
git pull --ff-only
git switch dogfood/v0 git pull --ff-only
If the expected branch is different, inspect the repository history and choose the branch containing the current projects backend work. Do not discard existing uncommitted work.
@@ -220,23 +202,20 @@ Run the baseline checks before spawning agents. Record failures but do not spend
Suggested baseline:
bun install
bun run check
bun install bun run check
Also inspect package-specific scripts and use the actual commands defined by the repository.
======================================================================
5. PASEO ORCHESTRATION
====================================================================== 5. PASEO ORCHESTRATION
======================================================================
Verify Paseo first:
paseo status
paseo ls
paseo status paseo ls
If the daemon is unavailable, inspect:
tail -n 200 ~/.paseo/daemon.log
tail -n 200 ~/.paseo/daemon.log
Do not continue spawning until Paseo works.
@@ -245,24 +224,24 @@ For every child lane:
1. Create a worktree workspace:
paseo workspace create \
--isolation worktree \
--mode branch-off \
--new-branch <branch> \
--base dogfood/v0 \
--title "<title>" \
--json
--isolation worktree \
--mode branch-off \
--new-branch <branch> \
--base dogfood/v0 \
--title "<title>" \
--json
2. Read the returned workspaceId.
3. Start the child agent:
paseo run \
--provider codex/glm-5.2 \
--workspace <workspaceId> \
--title "<title>" \
--background \
--json \
"<full lane prompt>"
--provider codex/glm-5.2 \
--workspace <workspaceId> \
--title "<title>" \
--background \
--json \
"<full lane prompt>"
4. Save:
- branch;
@@ -273,33 +252,27 @@ For every child lane:
Maintain an orchestration note in a temporary file outside committed product code, for example:
/tmp/zopu-dogfood-orchestration.md
/tmp/zopu-dogfood-orchestration.md
Track:
| Lane | Branch | Workspace ID | Agent ID | Status | PR |
|------|--------|--------------|----------|--------|----|
| Lane | Branch | Workspace ID | Agent ID | Status | PR |
| ---- | ------ | ------------ | -------- | ------ | --- |
Use:
paseo ls
paseo inspect <agentId>
paseo logs <agentId>
paseo send <agentId> "<focused instruction>"
paseo wait <agentId>
paseo ls paseo inspect <agentId> paseo logs <agentId> paseo send <agentId> "<focused instruction>" paseo wait <agentId>
Do not block waiting on one child while others are still running. Poll all first-wave agents periodically.
======================================================================
6. SHARED CHILD-AGENT PREFIX
====================================================================== 6. SHARED CHILD-AGENT PREFIX
======================================================================
Prepend the following instructions to every child prompt:
You are implementing one isolated lane of the Zopu dogfood v0 loop.
Repository base branch:
dogfood/v0
Repository base branch: dogfood/v0
Your branch is already isolated in a Paseo Git worktree.
@@ -347,29 +320,30 @@ Before finishing:
- PR URL or exact PR creation instructions;
- remaining limitations.
======================================================================
7. FIRST-WAVE PARALLEL LANES
====================================================================== 7. FIRST-WAVE PARALLEL LANES
======================================================================
Spawn all four first-wave agents immediately.
----------------------------------------------------------------------
---
LANE A — PROJECT CHAT, SIGNALS AND WORK ROUTING
----------------------------------------------------------------------
Branch:
dogfood/zopu-orchestrator
dogfood/zopu-orchestrator
Title:
Zopu Signals Orchestrator
Zopu Signals Orchestrator
Prompt:
Implement the project-scoped Zopu chat, Signal extraction and work-routing loop.
Inspect the existing:
- Zopu agent;
- Signals primitive;
- ProjectIssue primitive;
@@ -421,10 +395,10 @@ Rules:
Acceptance examples:
Message:
“The work-unit card should expose the generated PR.”
Message: “The work-unit card should expose the generated PR.”
Expected:
- exact message persisted;
- Signal created;
- attached to an existing UI/workspace issue if relevant;
@@ -449,17 +423,18 @@ Likely ownership:
- packages/backend/convex/schema*
- Signal/project-issue primitives only as required.
----------------------------------------------------------------------
---
LANE B — ORB RUNTIME FOUNDATION
----------------------------------------------------------------------
Branch:
dogfood/orb-runtime
dogfood/orb-runtime
Title:
AgentOS OpenCode Orb Runtime
AgentOS OpenCode Orb Runtime
Prompt:
@@ -525,6 +500,7 @@ Keep product-domain concepts separate from infrastructure leases.
Do not make AgentOS VM state equal the work-unit state.
Actor identity must include:
- project;
- issue/work unit;
- run.
@@ -577,21 +553,23 @@ Document:
- current limitations.
Do not modify:
- Zopu chat behavior;
- project-manager delegation;
- web UI.
----------------------------------------------------------------------
---
LANE C — WEB WORKSPACE AND WORK CARDS
----------------------------------------------------------------------
Branch:
dogfood/web-workspace
dogfood/web-workspace
Title:
Zopu Web Work OS
Zopu Web Work OS
Prompt:
@@ -613,18 +591,21 @@ Product projection:
Required interface:
Header:
- Zopu;
- current project;
- project/runtime connection status;
- basic project switcher if already supported.
Main surface:
- global project conversation;
- active Work Unit cards;
- lightweight Signals display;
- persistent composer.
Collapsed Work Unit card:
- title;
- latest summary;
- linked Signal count;
@@ -635,6 +616,7 @@ Collapsed Work Unit card:
- needs-input indicator when available.
Expanded Work Unit:
- objective;
- linked Signals and provenance;
- current plan or steps;
@@ -705,17 +687,18 @@ Tests:
- empty state;
- needs-input display.
----------------------------------------------------------------------
---
LANE D — SINGLE-NODE EXECUTION DEPLOYMENT
----------------------------------------------------------------------
Branch:
dogfood/runtime-deploy
dogfood/runtime-deploy
Title:
Zopu Dedicated Runtime Deployment
Zopu Dedicated Runtime Deployment
Prompt:
@@ -794,8 +777,7 @@ The deployment must be reproducible on a clean Debian host.
Do not require the developers MacBook to remain online after deployment.
======================================================================
8. FIRST-WAVE MONITORING
====================================================================== 8. FIRST-WAVE MONITORING
======================================================================
After spawning all four first-wave agents:
@@ -815,16 +797,15 @@ Use concise interventions.
Examples:
paseo send <agentId> "Keep ProjectIssue as the v0 Work Unit. Do not introduce a second generic work-unit schema."
paseo send <agentId> "Keep ProjectIssue as the v0 Work Unit. Do not introduce a second generic work-unit schema."
paseo send <agentId> "Do not wire project-manager yet. Finish the Orb port, adapter and proof fixture only."
paseo send <agentId> "Do not wire project-manager yet. Finish the Orb port, adapter and proof fixture only."
paseo send <agentId> "Use existing Convex contracts. Avoid backend schema changes in the UI lane."
paseo send <agentId> "Use existing Convex contracts. Avoid backend schema changes in the UI lane."
Wait until each first-wave agent is idle, then inspect:
paseo inspect <agentId>
paseo logs <agentId>
paseo inspect <agentId> paseo logs <agentId>
Review each branch yourself.
@@ -842,8 +823,7 @@ For every lane:
Do not merge a failing lane merely because the child says it works.
======================================================================
9. FIRST-WAVE MERGE PROCESS
====================================================================== 9. FIRST-WAVE MERGE PROCESS
======================================================================
Recommended merge order:
@@ -855,11 +835,7 @@ Recommended merge order:
Before every merge:
git switch dogfood/v0
git pull --ff-only
git fetch origin
git log --oneline --decorate --max-count=10 origin/<branch>
git diff dogfood/v0...origin/<branch>
git switch dogfood/v0 git pull --ff-only git fetch origin git log --oneline --decorate --max-count=10 origin/<branch> git diff dogfood/v0...origin/<branch>
Run lane-specific checks.
@@ -867,31 +843,28 @@ Merge using the repositorys normal PR workflow when possible.
If merging locally:
git merge --no-ff origin/<branch>
git merge --no-ff origin/<branch>
Resolve conflicts conservatively.
After each merge:
bun install
run targeted checks
git push origin dogfood/v0
bun install run targeted checks git push origin dogfood/v0
Do not squash away useful commits unless repository policy requires squashing.
======================================================================
10. SECOND-WAVE LANE — WIRE PROJECT MANAGER TO ORBS
====================================================================== 10. SECOND-WAVE LANE — WIRE PROJECT MANAGER TO ORBS
======================================================================
Only spawn this lane after the Orb runtime branch is integrated into dogfood/v0.
Branch:
dogfood/orb-wiring
dogfood/orb-wiring
Title:
Project Manager Orb Wiring
Project Manager Orb Wiring
Create its Paseo workspace from the updated dogfood/v0 branch.
@@ -944,20 +917,13 @@ Refactor the current Git lifecycle so it can be invoked from the Orb/application
Failure mapping:
- runtime unavailable:
infrastructure failure with clear reason;
- missing user context:
needs input;
- failed tests that can be repaired:
agent continues;
- unrecoverable repeated failure:
failed with logs and evidence;
- cancellation:
terminate OpenCode and sandbox processes;
- Git rejection:
preserve commit and expose exact failure;
- PR creation failure:
retain pushed branch and retry safely.
- runtime unavailable: infrastructure failure with clear reason;
- missing user context: needs input;
- failed tests that can be repaired: agent continues;
- unrecoverable repeated failure: failed with logs and evidence;
- cancellation: terminate OpenCode and sandbox processes;
- Git rejection: preserve commit and expose exact failure;
- PR creation failure: retain pushed branch and retry safely.
Idempotency:
@@ -981,8 +947,7 @@ Tests:
Do not add multi-agent parallel execution.
======================================================================
11. SECOND-WAVE REVIEW AND MERGE
====================================================================== 11. SECOND-WAVE REVIEW AND MERGE
======================================================================
Monitor the orb-wiring agent closely.
@@ -1007,19 +972,18 @@ Review and merge it into dogfood/v0 only after:
- idempotency is demonstrated;
- no secrets appear in logs or artifacts.
======================================================================
12. FINAL LANE — END-TO-END DOGFOOD INTEGRATION
====================================================================== 12. FINAL LANE — END-TO-END DOGFOOD INTEGRATION
======================================================================
After all implementation lanes are merged, spawn the final integration agent.
Branch:
dogfood/e2e-integration
dogfood/e2e-integration
Title:
Zopu Dogfood End-to-End
Zopu Dogfood End-to-End
Prompt:
@@ -1100,7 +1064,7 @@ Verification matrix:
Produce:
docs/DOGFOOD_V0.md
docs/DOGFOOD_V0.md
It must contain:
@@ -1119,8 +1083,7 @@ It must contain:
Commit, push and open a PR targeting dogfood/v0.
======================================================================
13. FINAL INTEGRATION RESPONSIBILITIES
====================================================================== 13. FINAL INTEGRATION RESPONSIBILITIES
======================================================================
After the final agent completes:
@@ -1146,8 +1109,7 @@ If the full live path cannot complete:
The final system must not claim that a PR exists unless it exists in Git.
======================================================================
14. QUALITY GATES
====================================================================== 14. QUALITY GATES
======================================================================
A lane is not done merely because code was generated.
@@ -1184,8 +1146,7 @@ The complete v0 must satisfy:
- PR is created;
- PR is visible in the UI.
======================================================================
15. RESOURCE AND CONCURRENCY LIMITS
====================================================================== 15. RESOURCE AND CONCURRENCY LIMITS
======================================================================
For this v0:
@@ -1201,12 +1162,11 @@ For this v0:
When agents are no longer needed:
paseo archive <agentId>
paseo archive <agentId>
Do not archive them until their logs and work have been reviewed.
======================================================================
16. COMMUNICATION WITH THE USER
====================================================================== 16. COMMUNICATION WITH THE USER
======================================================================
Do not repeatedly ask the user to choose between reasonable implementation details.
@@ -1230,8 +1190,7 @@ When requesting input, provide:
Continue supervising all unblocked lanes while waiting.
======================================================================
17. FINAL REPORT
====================================================================== 17. FINAL REPORT
======================================================================
When finished, provide a concise but complete report:
@@ -1254,6 +1213,7 @@ When finished, provide a concise but complete report:
## Integrated branches
For each branch:
- purpose;
- final commit;
- PR;

View File

@@ -157,12 +157,12 @@ Observe real Work with human decisions, merge outcomes, incidents, and post-rele
Score each candidate run 03 per dimension.
| Score | Meaning |
|---|---|
| 0 | unsafe/incorrect/missing |
| 1 | substantial human repair required |
| 2 | acceptable with minor correction |
| 3 | correct, complete, reviewable |
| Score | Meaning |
| ----- | --------------------------------- |
| 0 | unsafe/incorrect/missing |
| 1 | substantial human repair required |
| 2 | acceptable with minor correction |
| 3 | correct, complete, reviewable |
Dimensions:

View File

@@ -3,7 +3,7 @@
> **Purpose:** prevent agents and humans from using overlapping terms inconsistently.
| Term | Definition | Not the same as |
|---|---|---|
| --- | --- | --- |
| Project | Durable product/repository/context boundary. | Work or chat thread |
| Message | Exact conversational input/output. | Signal |
| Signal | Provenanced evidence that may require attention. | Work |

View File

@@ -1,12 +1,11 @@
# Slice 1 Handoff Notes
Date: 2026-07-27
Branch: `master` at `cfdb2efc7`
PR: #19 (merged)
Date: 2026-07-27 Branch: `master` at `cfdb2efc7` PR: #19 (merged)
## What was built
### Core product loop
- Conversation to Signal to proposed Work with exact source provenance.
- Convex owns durable data: `conversationMessages`, `signals`, `works`, `workEvents`.
- Effect validation in `@code/work-os` package.
@@ -15,6 +14,7 @@ PR: #19 (merged)
- Mobile-first UI at `apps/web/src/components/slice-one/slice-one-page.tsx`.
### Model
- MiMo V2.5 (`xiaomi/mimo-v2.5`) via Cheaptricks AI gateway.
- Provider identity `xiaomi` gives Flue correct multimodal metadata (text + image).
- `AGENT_MODEL_BASE_URL` points at Cheaptricks; API key in `.env`.
@@ -22,28 +22,33 @@ PR: #19 (merged)
- `thinkingLevel: "medium"`.
### Reasoning traces
- Native Flue `reasoning` parts render as live "Thinking trace" (open while streaming, collapsed after).
- Inline `<think>...</think>` extraction for models that embed thinking in text.
- No fallback that hides model output.
### Image attachments
- Picker button + hidden file input, up to 4 images at 10 MB each.
- Base64 conversion into Flue `AgentPromptImage`.
- Authenticated blob-URL rendering for historical image replay.
- Verified live: MiMo read exact text and background color from generated test images.
### Mobile keyboard fix
- Visual viewport hook (`use-visual-viewport.ts`).
- Fixed full-screen shell; `interactive-widget=resizes-content` in viewport meta.
- Header stays pinned; chat shrinks; composer follows keyboard.
- Regression tests in `frontend-regressions.test.ts` and `use-visual-viewport.test.ts`.
### Rendering
- Streamdown markdown streaming with Mermaid chart support.
- Tool turns hidden; reasoning-containing tool turns remain visible.
- Work cards render inline in the conversation timeline with source navigation.
### Deployment
- Cheaptricks staging live at `https://zopu.cheaptricks.puter.wtf`.
- Caddy reverse proxy: `/api/*` to Flue (port 3585), root to web (port 5173).
- `server.allowedHosts: true` in `apps/web/vite.config.ts` for Caddy domain.
@@ -52,6 +57,7 @@ PR: #19 (merged)
- Deployment notes at `docs/deployment.md`.
### Repository cleanup
- 17 stale worktrees removed (Codex, Paseo, T3).
- 23 stale local branches deleted.
- 18 stale remote branches deleted.
@@ -62,16 +68,19 @@ PR: #19 (merged)
## Current state
### Running
- Cheaptricks: web on `:5173`, Flue on `:3585`, both persistent via `nohup`.
- Local Mac: servers stopped. Run `bun run dev:tailscale:agents -- --port 3585` and `bun run dev:tailscale:web` to start.
### Git
- `master` at `cfdb2efc7` on local, Cheaptricks, and remote.
- Only `master` branch locally. Two remote branches remain: `origin/sai/changes`, `origin/t3code/explore-primitives-package` (not cleaned because they may be owned by other contributors/tools).
## Known issues and next steps
1. **Convex SITE_URL is single-valued.** Switch it when moving between local and staging:
```bash
cd packages/backend
npx convex env set SITE_URL 'http://100.101.157.28:5173' # local

View File

@@ -3,4 +3,10 @@ import ultracite from "ultracite/oxfmt";
export default defineConfig({
...ultracite,
ignorePatterns: [
...ultracite.ignorePatterns,
"**/.agents/skills/**",
"repos/**",
"scripts/**",
],
});

View File

@@ -5,27 +5,43 @@ import remix from "ultracite/oxlint/remix";
export default defineConfig({
extends: [core, react, remix],
ignorePatterns: [...core.ignorePatterns, "repos/**", "scripts/**"],
ignorePatterns: [
...core.ignorePatterns,
"**/.agents/skills/**",
"repos/**",
"scripts/**",
],
overrides: [
{
files: ["packages/ui/src/components/*.tsx"],
rules: {
"eslint/func-style": "off",
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/label-has-associated-control": "off",
"jsx-a11y/no-noninteractive-element-interactions": "off",
"jsx-a11y/prefer-tag-over-role": "off",
"sort-keys": "off",
},
},
{
files: ["**/convex/**/*.ts", "**/convex/**/*.test.ts"],
rules: {
"unicorn/filename-case": "off",
// Convex targets ES2022: no toSorted/at; sequential awaits ensure atomicity
"unicorn/no-array-sort": "off",
"no-await-in-loop": "off",
// Convex query builders use `any` in index callbacks
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"eslint/complexity": "off",
"no-await-in-loop": "off",
"unicorn/filename-case": "off",
// Convex targets ES2022: no toSorted/at; sequential awaits ensure atomicity
"unicorn/no-array-sort": "off",
},
},
{
files: ["**/convex/**/*.test.ts"],
rules: {
"unicorn/no-await-expression-member": "off",
"eslint/require-await": "off",
"sort-keys": "off",
"unicorn/no-await-expression-member": "off",
},
},
],

View File

@@ -69,8 +69,8 @@
"build:agents": "vp run --filter @code/agents build",
"docs:update": "node scripts/update-docs.ts",
"subtree": "node scripts/subtree.ts",
"check": "ultracite check",
"fix": "ultracite fix"
"check": "ultracite check --disable-nested-config",
"fix": "ultracite fix --disable-nested-config"
},
"devDependencies": {
"@code/backend": "workspace:*",

View File

@@ -1,7 +1,34 @@
import { parseAgentEnv } from "@code/env/agent";
import type { AgentRouteHandler } from "@flue/runtime";
import { ConvexHttpClient } from "convex/browser";
import { makeFunctionReference } from "convex/server";
import { withTurnAdmission } from "../adapters/admission-context";
const bindTurnSubmission = makeFunctionReference<
"mutation",
{
readonly clientRequestId: string;
readonly submissionId: string;
readonly token: string;
readonly turnId: string;
},
null
>("conversationMessages:bindTurnSubmission");
const readSubmissionId = async (response: Response): Promise<string | null> => {
const body: unknown = await response
.clone()
.json()
.catch(() => null);
if (
typeof body !== "object" ||
body === null ||
!("submissionId" in body) ||
typeof body.submissionId !== "string"
) {
return null;
}
return body.submissionId;
};
/** Only Convex may invoke or observe the organization-scoped Flue agent. */
export const conversationRoute: AgentRouteHandler = async (context, next) => {
@@ -26,5 +53,21 @@ export const conversationRoute: AgentRouteHandler = async (context, next) => {
return context.json({ error: "Missing turn correlation headers" }, 400);
}
return await withTurnAdmission({ clientRequestId, turnId }, () => next());
return await next().then(async () => {
if (!context.res.ok) {
return context.res;
}
const submissionId = await readSubmissionId(context.res);
if (submissionId === null) {
return context.json({ error: "Flue admission response is invalid" }, 502);
}
const client = new ConvexHttpClient(env.CONVEX_URL);
await client.mutation(bindTurnSubmission, {
clientRequestId,
submissionId,
token: env.FLUE_DB_TOKEN,
turnId,
});
return context.res;
});
};

View File

@@ -0,0 +1 @@
export { default } from "./adapters/flue-persistence";

View File

@@ -1,14 +1,8 @@
{
"extends": "@code/config/tsconfig.base.json",
"compilerOptions": {
"types": [
"bun"
]
"types": ["bun"]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"dist"
]
}
"include": ["src/**/*.ts"],
"exclude": ["dist"]
}

View File

@@ -1,6 +1,9 @@
import { expoClient } from "@better-auth/expo/client";
import { env } from "@code/env/native";
import { convexClient, crossDomainClient } from "@convex-dev/better-auth/client/plugins";
import {
convexClient,
crossDomainClient,
} from "@convex-dev/better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
import Constants from "expo-constants";
import * as SecureStore from "expo-secure-store";
@@ -9,7 +12,7 @@ import { Platform } from "react-native";
const scheme = Constants.expoConfig?.scheme;
if (typeof scheme !== "string") {
throw new Error("Expo auth requires a string app scheme in app.json");
throw new TypeError("Expo auth requires a string app scheme in app.json");
}
export const authClient = createAuthClient({

View File

@@ -1,12 +1,14 @@
import { useForm } from "@tanstack/react-form";
import { useToast } from "heroui-native";
import { getErrorMessage, signInSchema, signUpSchema } from "../shared/forms";
import { signInSchema, signUpSchema } from "../shared/forms";
import { authClient } from "./auth-client";
type AuthFormOptions = {
export { getErrorMessage } from "../shared/forms";
interface AuthFormOptions {
readonly onSuccess?: () => void;
};
}
export const useNativeSignInForm = ({ onSuccess }: AuthFormOptions = {}) => {
const { toast } = useToast();
@@ -18,14 +20,17 @@ export const useNativeSignInForm = ({ onSuccess }: AuthFormOptions = {}) => {
{ email: value.email.trim(), password: value.password },
{
onError: ({ error }) => {
toast.show({ label: error.message || "Failed to sign in", variant: "danger" });
toast.show({
label: error.message || "Failed to sign in",
variant: "danger",
});
},
onSuccess: () => {
formApi.reset();
toast.show({ label: "Signed in successfully", variant: "success" });
onSuccess?.();
},
},
}
);
},
validators: { onSubmit: signInSchema },
@@ -39,7 +44,11 @@ export const useNativeSignUpForm = ({ onSuccess }: AuthFormOptions = {}) => {
defaultValues: { confirmPassword: "", email: "", name: "", password: "" },
onSubmit: async ({ formApi, value }) => {
await authClient.signUp.email(
{ email: value.email.trim(), name: value.name.trim(), password: value.password },
{
email: value.email.trim(),
name: value.name.trim(),
password: value.password,
},
{
onError: ({ error }) => {
toast.show({
@@ -49,14 +58,15 @@ export const useNativeSignUpForm = ({ onSuccess }: AuthFormOptions = {}) => {
},
onSuccess: () => {
formApi.reset();
toast.show({ label: "Account created successfully", variant: "success" });
toast.show({
label: "Account created successfully",
variant: "success",
});
onSuccess?.();
},
},
}
);
},
validators: { onSubmit: signUpSchema },
});
};
export { getErrorMessage };

View File

@@ -9,14 +9,15 @@ import {
useThemeColor,
} from "heroui-native";
import { useRef } from "react";
import { Text, type TextInput, View } from "react-native";
import { Text, View } from "react-native";
import type { TextInput } from "react-native";
import { getErrorMessage, useNativeSignInForm } from "./hooks";
type NativeLoginFormProps = {
interface NativeLoginFormProps {
readonly onNavigateSignUp: () => void;
readonly onSuccess?: () => void;
};
}
export const NativeLoginForm = ({
onNavigateSignUp,
@@ -29,7 +30,14 @@ export const NativeLoginForm = ({
return (
<Surface className="rounded-xl p-5" variant="secondary">
<Text style={{ color: foregroundColor, fontSize: 24, fontWeight: "600", marginBottom: 4 }}>
<Text
style={{
color: foregroundColor,
fontSize: 24,
fontWeight: "600",
marginBottom: 4,
}}
>
Welcome back
</Text>
<Text style={{ color: mutedColor, fontSize: 14, marginBottom: 20 }}>
@@ -93,7 +101,11 @@ export const NativeLoginForm = ({
isDisabled={isSubmitting}
onPress={() => void form.handleSubmit()}
>
{isSubmitting ? <Spinner color="default" size="sm" /> : <Button.Label>Sign in</Button.Label>}
{isSubmitting ? (
<Spinner color="default" size="sm" />
) : (
<Button.Label>Sign in</Button.Label>
)}
</Button>
<Button onPress={onNavigateSignUp} variant="ghost">

View File

@@ -12,10 +12,10 @@ import { Text, View } from "react-native";
import { getErrorMessage, useNativeSignUpForm } from "./hooks";
type NativeSignupFormProps = {
interface NativeSignupFormProps {
readonly onNavigateSignIn: () => void;
readonly onSuccess?: () => void;
};
}
export const NativeSignupForm = ({
onNavigateSignIn,
@@ -27,7 +27,14 @@ export const NativeSignupForm = ({
return (
<Surface className="rounded-xl p-5" variant="secondary">
<Text style={{ color: foregroundColor, fontSize: 24, fontWeight: "600", marginBottom: 4 }}>
<Text
style={{
color: foregroundColor,
fontSize: 24,
fontWeight: "600",
marginBottom: 4,
}}
>
Create an account
</Text>
<Text style={{ color: mutedColor, fontSize: 14, marginBottom: 20 }}>

View File

@@ -1,16 +1,30 @@
import { z } from "zod";
export const signInSchema = z.object({
email: z.string().trim().min(1, "Email is required").email("Enter a valid email address"),
password: z.string().min(1, "Password is required").min(8, "Use at least 8 characters"),
email: z
.string()
.trim()
.min(1, "Email is required")
.email("Enter a valid email address"),
password: z
.string()
.min(1, "Password is required")
.min(8, "Use at least 8 characters"),
});
export const signUpSchema = z
.object({
confirmPassword: z.string().min(1, "Confirm your password"),
email: z.string().trim().min(1, "Email is required").email("Enter a valid email address"),
email: z
.string()
.trim()
.min(1, "Email is required")
.email("Enter a valid email address"),
name: z.string().trim().min(2, "Name must be at least 2 characters"),
password: z.string().min(1, "Password is required").min(8, "Use at least 8 characters"),
password: z
.string()
.min(1, "Password is required")
.min(8, "Use at least 8 characters"),
})
.refine(({ confirmPassword, password }) => confirmPassword === password, {
message: "Passwords do not match",
@@ -18,13 +32,19 @@ export const signUpSchema = z
});
export const getErrorMessage = (error: unknown): string | null => {
if (!error) return null;
if (typeof error === "string") return error;
if (!error) {
return null;
}
if (typeof error === "string") {
return error;
}
if (Array.isArray(error)) {
for (const issue of error) {
const message = getErrorMessage(issue);
if (message) return message;
if (message) {
return message;
}
}
return null;
}

View File

@@ -38,7 +38,9 @@ export const LoginForm = ({
<Card>
<CardHeader>
<CardTitle>Welcome back</CardTitle>
<CardDescription>Enter your email and password to continue.</CardDescription>
<CardDescription>
Enter your email and password to continue.
</CardDescription>
</CardHeader>
<CardContent>
<form
@@ -58,7 +60,9 @@ export const LoginForm = ({
id={field.name}
name={field.name}
onBlur={field.handleBlur}
onChange={(event) => field.handleChange(event.target.value)}
onChange={(event) =>
field.handleChange(event.target.value)
}
placeholder="you@example.com"
type="email"
value={field.state.value}
@@ -77,7 +81,9 @@ export const LoginForm = ({
id={field.name}
name={field.name}
onBlur={field.handleBlur}
onChange={(event) => field.handleChange(event.target.value)}
onChange={(event) =>
field.handleChange(event.target.value)
}
type="password"
value={field.state.value}
/>
@@ -100,7 +106,8 @@ export const LoginForm = ({
{isSubmitting ? "Signing in…" : "Sign in"}
</Button>
<FieldDescription className="text-center">
Don&apos;t have an account? <a href={signUpHref}>Sign up</a>
Don&apos;t have an account?{" "}
<a href={signUpHref}>Sign up</a>
</FieldDescription>
</Field>
)}

View File

@@ -35,7 +35,9 @@ export const SignupForm = ({
<Card {...props}>
<CardHeader>
<CardTitle>Create an account</CardTitle>
<CardDescription>Use your name, email, and password to get started.</CardDescription>
<CardDescription>
Use your name, email, and password to get started.
</CardDescription>
</CardHeader>
<CardContent>
<form
@@ -96,7 +98,9 @@ export const SignupForm = ({
type="password"
value={field.state.value}
/>
<FieldDescription>Use at least 8 characters.</FieldDescription>
<FieldDescription>
Use at least 8 characters.
</FieldDescription>
<FieldError errors={field.state.meta.errors} />
</Field>
)}

View File

@@ -2,12 +2,8 @@
This project uses [Convex](https://convex.dev) as its backend.
When working on Convex code, **always read
`convex/_generated/ai/guidelines.md` first** for important guidelines on
how to correctly use Convex APIs and patterns. The file contains rules that
override what you may have learned about Convex from training data.
When working on Convex code, **always read `convex/_generated/ai/guidelines.md` first** for important guidelines on how to correctly use Convex APIs and patterns. The file contains rules that override what you may have learned about Convex from training data.
Convex agent skills for common tasks can be installed by running
`npx convex ai-files install`.
Convex agent skills for common tasks can be installed by running `npx convex ai-files install`.
<!-- convex-ai-end -->

View File

@@ -1,3 +1,4 @@
import { env } from "@code/env/convex";
import { convexTest } from "convex-test";
import { anyApi, makeFunctionReference } from "convex/server";
import { describe, expect, test } from "vitest";
@@ -168,4 +169,43 @@ describe("conversationMessages", () => {
submissionId: "submission-1",
});
});
test("binds a Flue admission receipt exactly once", async () => {
const t = newTest();
const organization = await ensureOrg(t, identityA);
const queued = await t
.withIdentity(identityA)
.mutation(api.conversationMessages.send, {
clientRequestId: "request-bind",
images: [],
organizationId: organization._id,
rawText: "Bind me",
});
await t.mutation(markProcessingRef, {
attempt: 1,
leaseOwner: "worker-bind",
turnId: queued.turnId,
});
await t.mutation(api.conversationMessages.bindTurnSubmission, {
clientRequestId: "request-bind",
submissionId: "submission-bind",
token: env.FLUE_DB_TOKEN,
turnId: queued.turnId,
});
await t.mutation(api.conversationMessages.bindTurnSubmission, {
clientRequestId: "request-bind",
submissionId: "submission-bind",
token: env.FLUE_DB_TOKEN,
turnId: queued.turnId,
});
const turn = await t.run((ctx) => ctx.db.get(queued.turnId));
expect(turn).toMatchObject({
status: "running",
submissionId: "submission-bind",
});
expect(turn).not.toHaveProperty("leaseExpiresAt");
expect(turn).not.toHaveProperty("leaseOwner");
});
});

View File

@@ -263,6 +263,38 @@ export const markProcessing = internalMutation({
},
});
export const bindTurnSubmission = mutation({
args: {
clientRequestId: v.string(),
submissionId: v.string(),
token: v.string(),
turnId: v.id("conversationTurns"),
},
handler: async (ctx, args): Promise<null> => {
if (args.token !== env.FLUE_DB_TOKEN) {
throw new Error("Invalid agent control token");
}
const turn = await ctx.db.get(args.turnId);
if (!turn || turn.clientRequestId !== args.clientRequestId) {
throw new Error("Turn admission correlation does not match");
}
if (turn.status === "running" && turn.submissionId === args.submissionId) {
return null;
}
if (turn.status !== "dispatching" || turn.submissionId !== undefined) {
throw new Error("Turn is not awaiting Flue admission");
}
await ctx.db.patch(turn._id, {
error: undefined,
leaseExpiresAt: undefined,
leaseOwner: undefined,
status: "running",
submissionId: args.submissionId,
});
return null;
},
});
export const failTurn = internalMutation({
args: {
attempt: v.number(),
@@ -322,7 +354,7 @@ export const runTurn = internalAction({
if (!flueUrl) {
throw new Error("FLUE_URL is not configured in Convex");
}
const images = await Promise.all(
const attachments = await Promise.all(
turn.attachments.map(async (attachment) => {
const url = await ctx.storage.getUrl(attachment.storageId);
const response = url ? await fetch(url) : null;
@@ -331,6 +363,7 @@ export const runTurn = internalAction({
}
return {
data: toBase64(await response.arrayBuffer()),
filename: attachment.filename,
mimeType: attachment.mimeType,
type: "image" as const,
};
@@ -341,7 +374,11 @@ export const runTurn = internalAction({
`${flueUrl.replace(/\/+$/u, "")}/`
);
const response = await fetch(endpoint, {
body: JSON.stringify({ images, message: turn.user.content }),
body: JSON.stringify({
attachments,
body: turn.user.content,
kind: "user",
}),
headers: {
authorization: `Bearer ${env.FLUE_DB_TOKEN}`,
"content-type": "application/json",

View File

@@ -7,7 +7,7 @@ import { mutation, query } from "./_generated/server";
import type { MutationCtx, QueryCtx } from "./_generated/server";
import { projectConversationRecords } from "./conversationProjections";
const FLUE_SCHEMA_VERSION = "4";
const FLUE_SCHEMA_VERSION = "5";
const DURABILITY_DEFAULT_MAX_ATTEMPTS = 10;
const DURABILITY_DEFAULT_TIMEOUT_MS = 3_600_000;
const LEASE_DURATION_MS = 30_000;

View File

@@ -1,7 +1,5 @@
import { query } from "./_generated/server";
export const get = query({
handler: () =>
"OK"
,
handler: () => "OK",
});

View File

@@ -1,5 +1,13 @@
import { CONTEXT_KINDS, contextPathForKind, MAX_CONTEXT_DOCUMENT_CHARACTERS } from '@code/primitives/project';
import type { PreparedPublicGitSource, PublicGitImportResult, RepositoryContextDocument } from '@code/primitives/project';
import {
CONTEXT_KINDS,
contextPathForKind,
MAX_CONTEXT_DOCUMENT_CHARACTERS,
} from "@code/primitives/project";
import type {
PreparedPublicGitSource,
PublicGitImportResult,
RepositoryContextDocument,
} from "@code/primitives/project";
const GITHUB_HOST = "github.com";
const GITEA_HOST = "git.openputer.com";

View File

@@ -6,6 +6,7 @@
"author": "",
"scripts": {
"dev": "convex dev --env-file ../../.env",
"dev:once": "convex dev --once --env-file=../../.env",
"dev:setup": "convex dev --env-file ../../.env --configure --until-success",
"check-types": "tsc --noEmit -p convex/tsconfig.json",
"test": "CONVEX_SITE_URL=https://convex.test FLUE_DB_TOKEN=test-token SITE_URL=https://site.test vitest run",

View File

@@ -7,11 +7,11 @@ const convexUrlSchema = (exampleHost: string) =>
});
export const env = createEnv({
clientPrefix: "EXPO_PUBLIC_",
client: {
EXPO_PUBLIC_CONVEX_URL: convexUrlSchema("example.convex.cloud"),
EXPO_PUBLIC_CONVEX_SITE_URL: convexUrlSchema("example.convex.site"),
EXPO_PUBLIC_CONVEX_URL: convexUrlSchema("example.convex.cloud"),
},
runtimeEnv: process.env,
clientPrefix: "EXPO_PUBLIC_",
emptyStringAsUndefined: true,
runtimeEnv: process.env,
});

View File

@@ -7,17 +7,17 @@ const convexUrlSchema = (exampleHost: string) =>
});
export const env = createEnv({
emptyStringAsUndefined: true,
runtimeEnv: process.env,
server: {
CONVEX_URL: convexUrlSchema("example.convex.cloud"),
DAEMON_COMMAND_LEASE_MS: z.coerce.number().int().positive().default(60_000),
DAEMON_HEARTBEAT_MS: z.coerce.number().int().positive().default(15_000),
DAEMON_ID: z.string().min(1),
DAEMON_NAME: z.string().min(1).optional(),
DAEMON_VERSION: z.string().default("0.0.0"),
DAEMON_HEARTBEAT_MS: z.coerce.number().int().positive().default(15_000),
DAEMON_COMMAND_LEASE_MS: z.coerce.number().int().positive().default(60_000),
FLUE_DB_TOKEN: z.string().min(1),
RIVET_ENDPOINT: z.url().optional(),
},
runtimeEnv: process.env,
skipValidation: process.env.SKIP_ENV_VALIDATION === "true",
emptyStringAsUndefined: true,
});

View File

@@ -2,25 +2,26 @@ import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { Button } from "@code/ui/components/button";
import { cn } from "@code/ui/lib/utils";
import { cva, type VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import * as React from "react";
const attachmentVariants = cva(
"group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-none border bg-card text-card-foreground transition-colors focus-within:ring-1 focus-within:ring-ring/50 has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed",
{
variants: {
orientation: {
horizontal: "min-w-40 items-center",
vertical: "w-24 flex-col has-data-[slot=attachment-content]:w-30",
},
size: {
default:
"gap-2 text-xs has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5 has-data-[slot=attachment-media]:p-1.5",
sm: "gap-2.5 text-xs has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1 has-data-[slot=attachment-media]:p-1",
xs: "gap-1.5 rounded-none text-xs has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1 has-data-[slot=attachment-media]:p-1",
},
orientation: {
horizontal: "min-w-40 items-center",
vertical: "w-24 flex-col has-data-[slot=attachment-content]:w-30",
},
},
},
}
);
function Attachment({
@@ -41,7 +42,7 @@ function Attachment({
data-state={state}
data-size={size}
data-orientation={resolvedOrientation}
className={cn(attachmentVariants({ size, orientation }), className)}
className={cn(attachmentVariants({ orientation, size }), className)}
{...props}
/>
);
@@ -50,6 +51,9 @@ function Attachment({
const attachmentMediaVariants = cva(
"relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-none bg-muted text-foreground group-data-[orientation=vertical]/attachment:w-full group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-none group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-6 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5",
{
defaultVariants: {
variant: "icon",
},
variants: {
variant: {
icon: "",
@@ -57,10 +61,7 @@ const attachmentMediaVariants = cva(
"opacity-60 group-data-[state=done]/attachment:opacity-100 group-data-[state=idle]/attachment:opacity-100 *:[img]:aspect-square *:[img]:w-full *:[img]:object-cover",
},
},
defaultVariants: {
variant: "icon",
},
},
}
);
function AttachmentMedia({
@@ -78,53 +79,65 @@ function AttachmentMedia({
);
}
function AttachmentContent({ className, ...props }: React.ComponentProps<"div">) {
function AttachmentContent({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="attachment-content"
className={cn(
"max-w-full min-w-0 flex-1 leading-tight group-data-[orientation=vertical]/attachment:px-1",
className,
className
)}
{...props}
/>
);
}
function AttachmentTitle({ className, ...props }: React.ComponentProps<"span">) {
function AttachmentTitle({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="attachment-title"
className={cn(
"block max-w-full min-w-0 truncate font-medium group-data-[state=processing]/attachment:shimmer group-data-[state=uploading]/attachment:shimmer",
className,
className
)}
{...props}
/>
);
}
function AttachmentDescription({ className, ...props }: React.ComponentProps<"span">) {
function AttachmentDescription({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="attachment-description"
className={cn(
"mt-0.5 block min-w-0 truncate text-xs text-muted-foreground group-data-[state=error]/attachment:text-destructive/80",
"max-w-full",
className,
className
)}
{...props}
/>
);
}
function AttachmentActions({ className, ...props }: React.ComponentProps<"div">) {
function AttachmentActions({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="attachment-actions"
className={cn(
"relative z-20 flex shrink-0 items-center group-data-[orientation=vertical]/attachment:absolute group-data-[orientation=vertical]/attachment:top-3 group-data-[orientation=vertical]/attachment:right-3 group-data-[orientation=vertical]/attachment:gap-1",
className,
className
)}
{...props}
/>
@@ -160,10 +173,10 @@ function AttachmentTrigger({
defaultTagName: "button",
props: mergeProps<"button">(
{
type: render ? type : (type ?? "button"),
className: cn("absolute inset-0 z-10 outline-none", className),
type: render ? type : (type ?? "button"),
},
props,
props
),
render,
state: {
@@ -178,7 +191,7 @@ function AttachmentGroup({ className, ...props }: React.ComponentProps<"div">) {
data-slot="attachment-group"
className={cn(
"flex min-w-0 scroll-fade-x snap-x snap-mandatory scroll-px-1 scrollbar-none gap-3 overflow-x-auto overscroll-x-contain py-1 *:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start",
className,
className
)}
{...props}
/>

View File

@@ -1,7 +1,8 @@
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cn } from "@code/ui/lib/utils";
import { cva, type VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import * as React from "react";
function BubbleGroup({ className, ...props }: React.ComponentProps<"div">) {
@@ -17,28 +18,28 @@ function BubbleGroup({ className, ...props }: React.ComponentProps<"div">) {
const bubbleVariants = cva(
"group/bubble relative flex w-fit max-w-[80%] min-w-0 flex-col gap-1 group-data-[align=end]/message:self-end data-[align=end]:self-end data-[variant=ghost]:max-w-full",
{
defaultVariants: {
variant: "default",
},
variants: {
variant: {
default:
"*:data-[slot=bubble-content]:bg-primary *:data-[slot=bubble-content]:text-primary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-primary/80",
secondary:
"*:data-[slot=bubble-content]:bg-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]",
muted:
"*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]",
tinted:
"*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.88_calc(c*0.5)_h)] dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.35_calc(c*0.5)_h)]",
outline:
"*:data-[slot=bubble-content]:border-border *:data-[slot=bubble-content]:bg-background [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-input/30",
ghost:
"border-none *:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/50",
destructive:
"*:data-[slot=bubble-content]:bg-destructive/10 *:data-[slot=bubble-content]:text-destructive dark:*:data-[slot=bubble-content]:bg-destructive/20 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/20 dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/30",
ghost:
"border-none *:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/50",
muted:
"*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]",
outline:
"*:data-[slot=bubble-content]:border-border *:data-[slot=bubble-content]:bg-background [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-input/30",
secondary:
"*:data-[slot=bubble-content]:bg-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]",
tinted:
"*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.88_calc(c*0.5)_h)] dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.35_calc(c*0.5)_h)]",
},
},
defaultVariants: {
variant: "default",
},
},
}
);
function Bubble({
@@ -61,17 +62,21 @@ function Bubble({
);
}
function BubbleContent({ className, render, ...props }: useRender.ComponentProps<"div">) {
function BubbleContent({
className,
render,
...props
}: useRender.ComponentProps<"div">) {
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(
{
className: cn(
"w-fit max-w-full min-w-0 overflow-hidden rounded-none border border-transparent px-2.5 py-2 text-xs leading-relaxed wrap-break-word group-data-[align=end]/bubble:self-end [button]:text-left [button,a]:transition-colors [button,a]:outline-none [button,a]:focus-visible:border-ring [button,a]:focus-visible:ring-1 [button,a]:focus-visible:ring-ring/50",
className,
className
),
},
props,
props
),
render,
state: {
@@ -83,21 +88,21 @@ function BubbleContent({ className, render, ...props }: useRender.ComponentProps
const bubbleReactionsVariants = cva(
"absolute z-10 flex w-fit shrink-0 items-center justify-center gap-1 rounded-none bg-muted px-1.5 py-0.5 text-xs ring-2 ring-card has-[button]:p-0",
{
variants: {
side: {
top: "top-0 -translate-y-3/4",
bottom: "bottom-0 translate-y-3/4",
},
align: {
start: "left-3",
end: "right-3",
},
},
defaultVariants: {
side: "bottom",
align: "end",
side: "bottom",
},
},
variants: {
align: {
end: "right-3",
start: "left-3",
},
side: {
bottom: "bottom-0 translate-y-3/4",
top: "top-0 -translate-y-3/4",
},
},
}
);
function BubbleReactions({
@@ -114,7 +119,7 @@ function BubbleReactions({
data-slot="bubble-reactions"
data-align={align}
data-side={side}
className={cn(bubbleReactionsVariants({ side, align }), className)}
className={cn(bubbleReactionsVariants({ align, side }), className)}
{...props}
/>
);

View File

@@ -1,40 +1,41 @@
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { cn } from "@code/ui/lib/utils";
import { cva, type VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
defaultVariants: {
size: "default",
variant: "default",
},
variants: {
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
icon: "size-8",
"icon-lg": "size-9",
"icon-sm": "size-7 rounded-none",
"icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
},
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
link: "text-primary underline-offset-4 hover:underline",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
icon: "size-8",
"icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-7 rounded-none",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
}
);
function Button({
@@ -46,7 +47,7 @@ function Button({
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
className={cn(buttonVariants({ className, size, variant }))}
{...props}
/>
);

View File

@@ -12,7 +12,7 @@ function Card({
data-size={size}
className={cn(
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-none bg-card py-(--card-spacing) text-xs/relaxed text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none",
className,
className
)}
{...props}
/>
@@ -25,7 +25,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-none px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
className,
className
)}
{...props}
/>
@@ -38,7 +38,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card-title"
className={cn(
"cn-font-heading text-sm font-medium group-data-[size=sm]/card:text-sm",
className,
className
)}
{...props}
/>
@@ -59,7 +59,10 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
);
@@ -67,7 +70,11 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="card-content" className={cn("px-(--card-spacing)", className)} {...props} />
<div
data-slot="card-content"
className={cn("px-(--card-spacing)", className)}
{...props}
/>
);
}
@@ -75,10 +82,21 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center rounded-none border-t p-(--card-spacing)", className)}
className={cn(
"flex items-center rounded-none border-t p-(--card-spacing)",
className
)}
{...props}
/>
);
}
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};

View File

@@ -10,7 +10,7 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
data-slot="checkbox"
className={cn(
"peer relative flex size-4 shrink-0 items-center justify-center rounded-none border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
className,
className
)}
{...props}
>

View File

@@ -25,7 +25,10 @@ function DropdownMenuContent({
className,
...props
}: MenuPrimitive.Popup.Props &
Pick<MenuPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
Pick<
MenuPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<MenuPrimitive.Portal>
<MenuPrimitive.Positioner
@@ -39,7 +42,7 @@ function DropdownMenuContent({
data-slot="dropdown-menu-content"
className={cn(
"cn-menu-target cn-menu-translucent z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-none bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95",
className,
className
)}
{...props}
/>
@@ -63,7 +66,10 @@ function DropdownMenuLabel({
<MenuPrimitive.GroupLabel
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn("px-2 py-2 text-xs text-muted-foreground data-inset:pl-7", className)}
className={cn(
"px-2 py-2 text-xs text-muted-foreground data-inset:pl-7",
className
)}
{...props}
/>
);
@@ -85,7 +91,7 @@ function DropdownMenuItem({
data-variant={variant}
className={cn(
"group/dropdown-menu-item relative flex cursor-default items-center gap-2 rounded-none px-2 py-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
className,
className
)}
{...props}
/>
@@ -110,7 +116,7 @@ function DropdownMenuSubTrigger({
data-inset={inset}
className={cn(
"flex cursor-default items-center gap-2 rounded-none px-2 py-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
className
)}
{...props}
>
@@ -133,7 +139,7 @@ function DropdownMenuSubContent({
data-slot="dropdown-menu-sub-content"
className={cn(
"cn-menu-target cn-menu-translucent w-auto min-w-[96px] rounded-none bg-popover text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
className
)}
align={align}
alignOffset={alignOffset}
@@ -159,7 +165,7 @@ function DropdownMenuCheckboxItem({
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
className
)}
checked={checked}
{...props}
@@ -178,7 +184,12 @@ function DropdownMenuCheckboxItem({
}
function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
return <MenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
return (
<MenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
function DropdownMenuRadioItem({
@@ -195,7 +206,7 @@ function DropdownMenuRadioItem({
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
className
)}
{...props}
>
@@ -212,7 +223,10 @@ function DropdownMenuRadioItem({
);
}
function DropdownMenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) {
function DropdownMenuSeparator({
className,
...props
}: MenuPrimitive.Separator.Props) {
return (
<MenuPrimitive.Separator
data-slot="dropdown-menu-separator"
@@ -222,13 +236,16 @@ function DropdownMenuSeparator({ className, ...props }: MenuPrimitive.Separator.
);
}
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
className,
className
)}
{...props}
/>

View File

@@ -1,5 +1,6 @@
import { cn } from "@code/ui/lib/utils";
import { cva, type VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
@@ -7,7 +8,7 @@ function Empty({ className, ...props }: React.ComponentProps<"div">) {
data-slot="empty"
className={cn(
"flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-none border-dashed p-6 text-center text-balance md:p-12",
className,
className
)}
{...props}
/>
@@ -27,16 +28,16 @@ function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
const emptyMediaVariants = cva(
"mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
defaultVariants: {
variant: "default",
},
variants: {
variant: {
default: "bg-transparent",
icon: "flex size-10 shrink-0 items-center justify-center rounded-none bg-muted text-foreground [&_svg:not([class*='size-'])]:size-5",
},
},
defaultVariants: {
variant: "default",
},
},
}
);
function EmptyMedia({
@@ -48,7 +49,7 @@ function EmptyMedia({
<div
data-slot="empty-icon"
data-variant={variant}
className={cn(emptyMediaVariants({ variant, className }))}
className={cn(emptyMediaVariants({ className, variant }))}
{...props}
/>
);
@@ -58,7 +59,10 @@ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-title"
className={cn("cn-font-heading text-sm font-medium tracking-tight", className)}
className={cn(
"cn-font-heading text-sm font-medium tracking-tight",
className
)}
{...props}
/>
);
@@ -70,7 +74,7 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
data-slot="empty-description"
className={cn(
"text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
className,
className
)}
{...props}
/>
@@ -83,11 +87,18 @@ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
data-slot="empty-content"
className={cn(
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
className,
className
)}
{...props}
/>
);
}
export { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, EmptyMedia };
export {
Empty,
EmptyHeader,
EmptyTitle,
EmptyDescription,
EmptyContent,
EmptyMedia,
};

View File

@@ -1,9 +1,9 @@
import { useMemo } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@code/ui/lib/utils"
import { Label } from "@code/ui/components/label"
import { Separator } from "@code/ui/components/separator"
import { Label } from "@code/ui/components/label";
import { Separator } from "@code/ui/components/separator";
import { cn } from "@code/ui/lib/utils";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import { useMemo } from "react";
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
@@ -15,7 +15,7 @@ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
)}
{...props}
/>
)
);
}
function FieldLegend({
@@ -33,7 +33,7 @@ function FieldLegend({
)}
{...props}
/>
)
);
}
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
@@ -46,26 +46,26 @@ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
)}
{...props}
/>
)
);
}
const fieldVariants = cva(
"group/field flex w-full gap-2 data-[invalid=true]:text-destructive",
{
defaultVariants: {
orientation: "vertical",
},
variants: {
orientation: {
vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
horizontal:
"flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
responsive:
"flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
},
},
defaultVariants: {
orientation: "vertical",
},
}
)
);
function Field({
className,
@@ -80,7 +80,7 @@ function Field({
className={cn(fieldVariants({ orientation }), className)}
{...props}
/>
)
);
}
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
@@ -93,7 +93,7 @@ function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
)}
{...props}
/>
)
);
}
function FieldLabel({
@@ -110,7 +110,7 @@ function FieldLabel({
)}
{...props}
/>
)
);
}
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
@@ -123,7 +123,7 @@ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
)}
{...props}
/>
)
);
}
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
@@ -138,7 +138,7 @@ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
)}
{...props}
/>
)
);
}
function FieldSeparator({
@@ -146,7 +146,7 @@ function FieldSeparator({
className,
...props
}: React.ComponentProps<"div"> & {
children?: React.ReactNode
children?: React.ReactNode;
}) {
return (
<div
@@ -168,7 +168,7 @@ function FieldSeparator({
</span>
)}
</div>
)
);
}
function FieldError({
@@ -177,23 +177,23 @@ function FieldError({
errors,
...props
}: React.ComponentProps<"div"> & {
errors?: Array<{ message?: string } | undefined>
errors?: ({ message?: string } | undefined)[];
}) {
const content = useMemo(() => {
if (children) {
return children
return children;
}
if (!errors?.length) {
return null
return null;
}
const uniqueErrors = [
...new Map(errors.map((error) => [error?.message, error])).values(),
]
];
if (uniqueErrors?.length == 1) {
return uniqueErrors[0]?.message
if (uniqueErrors?.length === 1) {
return uniqueErrors[0]?.message;
}
return (
@@ -203,11 +203,11 @@ function FieldError({
error?.message && <li key={index}>{error.message}</li>
)}
</ul>
)
}, [children, errors])
);
}, [children, errors]);
if (!content) {
return null
return null;
}
return (
@@ -219,7 +219,7 @@ function FieldError({
>
{content}
</div>
)
);
}
export {
@@ -233,4 +233,4 @@ export {
FieldSet,
FieldContent,
FieldTitle,
}
};

View File

@@ -4,7 +4,8 @@ import { Button } from "@code/ui/components/button";
import { Input } from "@code/ui/components/input";
import { Textarea } from "@code/ui/components/textarea";
import { cn } from "@code/ui/lib/utils";
import { cva, type VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import * as React from "react";
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
@@ -19,7 +20,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50",
"has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-1 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
className,
className
)}
{...props}
/>
@@ -29,20 +30,22 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
const inputGroupAddonVariants = cva(
"flex h-auto cursor-text select-none items-center justify-center gap-2 py-1.5 text-xs font-medium text-muted-foreground group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-none [&>svg:not([class*='size-'])]:size-4",
{
variants: {
align: {
"inline-start": "order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
"inline-end": "order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
"block-start":
"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
"block-end":
"order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
},
},
defaultVariants: {
align: "inline-start",
},
},
variants: {
align: {
"block-end":
"order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
"block-start":
"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
"inline-end":
"order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
"inline-start":
"order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
},
},
}
);
function InputGroupAddon({
@@ -61,7 +64,9 @@ function InputGroupAddon({
return;
}
e.currentTarget.parentElement
?.querySelector<HTMLInputElement | HTMLTextAreaElement>("input, textarea")
?.querySelector<HTMLInputElement | HTMLTextAreaElement>(
"input, textarea"
)
?.focus();
}}
{...props}
@@ -69,19 +74,22 @@ function InputGroupAddon({
);
}
const inputGroupButtonVariants = cva("flex items-center gap-2 text-xs shadow-none", {
variants: {
size: {
xs: "h-6 gap-1 rounded-none px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: "h-7 gap-1 rounded-none px-2",
"icon-xs": "size-6 rounded-none p-0 has-[>svg]:p-0",
"icon-sm": "size-7 rounded-none p-0 has-[>svg]:p-0",
const inputGroupButtonVariants = cva(
"flex items-center gap-2 text-xs shadow-none",
{
defaultVariants: {
size: "xs",
},
},
defaultVariants: {
size: "xs",
},
});
variants: {
size: {
"icon-sm": "size-7 rounded-none p-0 has-[>svg]:p-0",
"icon-xs": "size-6 rounded-none p-0 has-[>svg]:p-0",
sm: "h-7 gap-1 rounded-none px-2",
xs: "h-6 gap-1 rounded-none px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
},
},
}
);
function InputGroupButton({
className,
@@ -109,33 +117,39 @@ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
<span
className={cn(
"flex items-center gap-2 text-xs text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className,
className
)}
{...props}
/>
);
}
function InputGroupInput({ className, ...props }: React.ComponentProps<"input">) {
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
className={cn(
"flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
className,
className
)}
{...props}
/>
);
}
function InputGroupTextarea({ className, ...props }: React.ComponentProps<"textarea">) {
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
className,
className
)}
{...props}
/>

View File

@@ -9,7 +9,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-none border border-input bg-transparent px-2.5 py-1 text-xs transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-xs file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 md:text-xs dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className,
className
)}
{...props}
/>

View File

@@ -9,7 +9,7 @@ function Label({ className, ...props }: React.ComponentProps<"label">) {
data-slot="label"
className={cn(
"flex items-center gap-2 text-xs leading-none select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className,
className
)}
{...props}
/>

View File

@@ -1,7 +1,8 @@
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cn } from "@code/ui/lib/utils";
import { cva, type VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import type { VariantProps } from "class-variance-authority";
import * as React from "react";
const markerVariants = cva(
@@ -9,13 +10,13 @@ const markerVariants = cva(
{
variants: {
variant: {
border: "border-b border-border pb-2",
default: "",
separator:
"before:mr-1 before:h-px before:min-w-0 before:flex-1 before:bg-border after:ml-1 after:h-px after:min-w-0 after:flex-1 after:bg-border",
border: "border-b border-border pb-2",
},
},
},
}
);
function Marker({
@@ -28,9 +29,9 @@ function Marker({
defaultTagName: "div",
props: mergeProps<"div">(
{
className: cn(markerVariants({ variant, className })),
className: cn(markerVariants({ className, variant })),
},
props,
props
),
render,
state: {
@@ -45,7 +46,10 @@ function MarkerIcon({ className, ...props }: React.ComponentProps<"span">) {
<span
data-slot="marker-icon"
aria-hidden="true"
className={cn("size-3.5 shrink-0 [&_svg:not([class*='size-'])]:size-3.5", className)}
className={cn(
"size-3.5 shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
className
)}
{...props}
/>
);
@@ -57,7 +61,7 @@ function MarkerContent({ className, ...props }: React.ComponentProps<"span">) {
data-slot="marker-content"
className={cn(
"min-w-0 wrap-break-word group-data-[variant=separator]/marker:flex-none group-data-[variant=separator]/marker:text-center *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className,
className
)}
{...props}
/>

View File

@@ -2,17 +2,18 @@
import { Button } from "@code/ui/components/button";
import { cn } from "@code/ui/lib/utils";
import {
MessageScroller as MessageScrollerPrimitive,
import { MessageScroller as MessageScrollerPrimitive } from "@shadcn/react/message-scroller";
import { ArrowDownIcon } from "lucide-react";
import * as React from "react";
export {
useMessageScroller,
useMessageScrollerScrollable,
useMessageScrollerVisibility,
} from "@shadcn/react/message-scroller";
import { ArrowDownIcon } from "lucide-react";
import * as React from "react";
function MessageScrollerProvider(
props: React.ComponentProps<typeof MessageScrollerPrimitive.Provider>,
props: React.ComponentProps<typeof MessageScrollerPrimitive.Provider>
) {
return <MessageScrollerPrimitive.Provider {...props} />;
}
@@ -26,7 +27,7 @@ function MessageScroller({
data-slot="message-scroller"
className={cn(
"cn-message-scroller group/message-scroller relative flex size-full min-h-0 flex-col overflow-hidden",
className,
className
)}
{...props}
/>
@@ -42,7 +43,7 @@ function MessageScrollerViewport({
data-slot="message-scroller-viewport"
className={cn(
"cn-message-scroller-viewport size-full min-h-0 min-w-0 scroll-fade-b scrollbar-thin scrollbar-gutter-stable overflow-y-auto overscroll-contain contain-content data-autoscrolling:scrollbar-none",
className,
className
)}
{...props}
/>
@@ -56,7 +57,10 @@ function MessageScrollerContent({
return (
<MessageScrollerPrimitive.Content
data-slot="message-scroller-content"
className={cn("cn-message-scroller-content flex h-max min-h-full flex-col gap-6", className)}
className={cn(
"cn-message-scroller-content flex h-max min-h-full flex-col gap-6",
className
)}
{...props}
/>
);
@@ -73,7 +77,7 @@ function MessageScrollerItem({
scrollAnchor={scrollAnchor}
className={cn(
"cn-message-scroller-item min-w-0 shrink-0 [contain-intrinsic-size:auto_10rem] [content-visibility:auto]",
className,
className
)}
{...props}
/>
@@ -99,7 +103,7 @@ function MessageScrollerButton({
direction={direction}
className={cn(
"cn-message-scroller-button absolute inset-s-1/2 -translate-x-1/2 border-border bg-background text-foreground transition-[translate,scale,opacity] duration-200 hover:bg-muted hover:text-foreground data-[active=false]:pointer-events-none data-[active=false]:scale-95 data-[active=false]:opacity-0 data-[active=false]:duration-400 data-[active=false]:ease-[cubic-bezier(0.7,0,0.84,0)] data-[active=true]:translate-y-0 data-[active=true]:scale-100 data-[active=true]:opacity-100 data-[active=true]:ease-[cubic-bezier(0.23,1,0.32,1)] data-[direction=end]:bottom-4 data-[direction=end]:data-[active=false]:translate-y-full data-[direction=start]:top-4 data-[direction=start]:data-[active=false]:-translate-y-full rtl:translate-x-1/2 data-[direction=start]:[&_svg]:rotate-180",
className,
className
)}
render={render ?? <Button variant={variant} size={size} />}
{...props}
@@ -123,7 +127,4 @@ export {
MessageScrollerContent,
MessageScrollerItem,
MessageScrollerButton,
useMessageScroller,
useMessageScrollerScrollable,
useMessageScrollerVisibility,
};

View File

@@ -22,7 +22,7 @@ function Message({
data-align={align}
className={cn(
"group/message relative flex w-full min-w-0 gap-1.5 text-xs data-[align=end]:flex-row-reverse",
className,
className
)}
{...props}
/>
@@ -35,7 +35,7 @@ function MessageAvatar({ className, ...props }: React.ComponentProps<"div">) {
data-slot="message-avatar"
className={cn(
"flex w-fit min-w-8 shrink-0 items-center justify-center self-end overflow-hidden rounded-full bg-muted group-has-data-[slot=message-footer]/message:-translate-y-8",
className,
className
)}
{...props}
/>
@@ -48,7 +48,7 @@ function MessageContent({ className, ...props }: React.ComponentProps<"div">) {
data-slot="message-content"
className={cn(
"flex w-full min-w-0 flex-col gap-2 wrap-break-word group-data-[align=end]/message:*:data-slot:self-end",
className,
className
)}
{...props}
/>
@@ -61,7 +61,7 @@ function MessageHeader({ className, ...props }: React.ComponentProps<"div">) {
data-slot="message-header"
className={cn(
"flex max-w-full min-w-0 items-center px-2.5 text-xs font-medium text-muted-foreground group-has-data-[variant=ghost]/message:px-0",
className,
className
)}
{...props}
/>
@@ -74,11 +74,18 @@ function MessageFooter({ className, ...props }: React.ComponentProps<"div">) {
data-slot="message-footer"
className={cn(
"flex max-w-full min-w-0 items-center px-2.5 text-xs font-medium text-muted-foreground group-has-data-[variant=ghost]/message:px-0 group-data-[align=end]/message:justify-end",
className,
className
)}
{...props}
/>
);
}
export { MessageGroup, Message, MessageAvatar, MessageContent, MessageFooter, MessageHeader };
export {
MessageGroup,
Message,
MessageAvatar,
MessageContent,
MessageFooter,
MessageHeader,
};

View File

@@ -1,8 +1,7 @@
"use client"
"use client";
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
import { cn } from "@code/ui/lib/utils"
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
import { cn } from "@code/ui/lib/utils";
function Separator({
className,
@@ -19,7 +18,7 @@ function Separator({
)}
{...props}
/>
)
);
}
export { Separator }
export { Separator };

View File

@@ -8,7 +8,8 @@ import {
TriangleAlertIcon,
} from "lucide-react";
import { useTheme } from "next-themes";
import { Toaster as Sonner, type ToasterProps } from "sonner";
import { Toaster as Sonner } from "sonner";
import type { ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
@@ -18,18 +19,18 @@ const Toaster = ({ ...props }: ToasterProps) => {
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
success: <CircleCheckIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
"--normal-bg": "var(--popover)",
"--normal-border": "var(--border)",
"--normal-text": "var(--popover-foreground)",
} as React.CSSProperties
}
toastOptions={{

View File

@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
data-slot="textarea"
className={cn(
"flex field-sizing-content min-h-16 w-full resize-none rounded-none border border-input bg-transparent px-2.5 py-2 text-xs transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 md:text-xs dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className,
className
)}
{...props}
/>

View File

@@ -3,8 +3,17 @@
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
import { cn } from "@code/ui/lib/utils";
function TooltipProvider({ delay = 0, ...props }: TooltipPrimitive.Provider.Props) {
return <TooltipPrimitive.Provider data-slot="tooltip-provider" delay={delay} {...props} />;
function TooltipProvider({
delay = 0,
...props
}: TooltipPrimitive.Provider.Props) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delay={delay}
{...props}
/>
);
}
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
@@ -24,7 +33,10 @@ function TooltipContent({
children,
...props
}: TooltipPrimitive.Popup.Props &
Pick<TooltipPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
Pick<
TooltipPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Positioner
@@ -38,7 +50,7 @@ function TooltipContent({
data-slot="tooltip-content"
className={cn(
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-none bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
className
)}
{...props}
>

View File

@@ -1,6 +1,5 @@
import { clsx, type ClassValue } from "clsx";
import { clsx } from "clsx";
import type { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));