flue flow fixes
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 developer’s 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 repository’s 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;
|
||||
|
||||
@@ -157,12 +157,12 @@ Observe real Work with human decisions, merge outcomes, incidents, and post-rele
|
||||
|
||||
Score each candidate run 0–3 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:
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,4 +3,10 @@ import ultracite from "ultracite/oxfmt";
|
||||
|
||||
export default defineConfig({
|
||||
...ultracite,
|
||||
ignorePatterns: [
|
||||
...ultracite.ignorePatterns,
|
||||
"**/.agents/skills/**",
|
||||
"repos/**",
|
||||
"scripts/**",
|
||||
],
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
1
packages/agents/src/db.ts
Normal file
1
packages/agents/src/db.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./adapters/flue-persistence";
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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't have an account? <a href={signUpHref}>Sign up</a>
|
||||
Don't have an account?{" "}
|
||||
<a href={signUpHref}>Sign up</a>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { query } from "./_generated/server";
|
||||
|
||||
export const get = query({
|
||||
handler: () =>
|
||||
"OK"
|
||||
,
|
||||
handler: () => "OK",
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
6
packages/env/src/native.ts
vendored
6
packages/env/src/native.ts
vendored
@@ -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,
|
||||
});
|
||||
|
||||
8
packages/env/src/server.ts
vendored
8
packages/env/src/server.ts
vendored
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user