feat: 移除backend-old目录中的废弃文件

移除以下文件:
- .dockerignore 和 .gitignore 配置文件
- A2A_Multiagent_change.md 设计文档
- COMMUNICATION.md 通讯信息文档
- Dockerfile 构建配置
- LICENSE 许可证文件

这些文件属于旧版本后端代码,不再需要维护。
This commit is contained in:
2026-05-14 17:19:25 +08:00
parent ebfa242862
commit b59968167e
312 changed files with 0 additions and 53485 deletions

View File

@ -1,3 +0,0 @@
{
"template": "nextjs-shadcn"
}

View File

@ -1,2 +0,0 @@
components/ui/*
hooks/use-toast.ts

View File

@ -1,9 +0,0 @@
For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
When using client-side hooks (useState and useEffect) in a component that's being treated as a Server Component by Next.js, always add the "use client" directive at the top of the file.
Do not write code that will trigger this error: "Warning: Extra attributes from the server: %s%s""class,style"
By default, this template supports JSX syntax with Tailwind CSS classes, the shadcn/ui library, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.
Use icons from lucide-react for logos.

View File

@ -1,2 +0,0 @@
NEXT_PUBLIC_API_URL=http://10.6.80.29:10000
NEXT_PUBLIC_WS_URL=wss://10.6.80.29:10000

View File

@ -1,790 +0,0 @@
# Frontend Multi-Agent / MCP Process UI Change
## 1. 目标
前端聊天页要从“单一消息流”升级成“可视化协作工作台”,让用户在一次聊天里同时看到:
1. 主对话区里的用户问题与最终总结回复。
2. 每个 sub-agent / A2A agent / MCP server 的独立处理框。
3. agent 之间的流式进展、状态变化、问答片段。
4. MCP 工具调用产物,例如文本结果、结构化 JSON、文件、链接、图片。
5. 一个固定的结果侧栏,用来汇总当前运行中的过程结果与最终产物。
6. 独立的 Agent 管理页和 MCP 管理页,体验上与现有 `skills` / `plugins` 页面一致。
这个需求本质上不是“把聊天页面做复杂一点”,而是要把聊天 UI 的数据模型从 `messages[]` 升级成 `messages[] + process runs[] + artifacts[] + actor registry[]`
## 2. 当前现状
### 2.1 前端现状
当前前端的核心限制如下:
- 聊天页集中在 `/home/ivan/xuan/steven_project/nanobot-fronted/app/page.tsx`
- 聊天状态只在 `/home/ivan/xuan/steven_project/nanobot-fronted/lib/store.ts` 里维护:
- `messages`
- `isLoading`
- `isThinking`
- `streamingContent`
- WebSocket 只在 `/home/ivan/xuan/steven_project/nanobot-fronted/lib/api.ts` 里处理非常薄的一层消息:
- `type=status`
- `type=message`
- 类型定义 `/home/ivan/xuan/steven_project/nanobot-fronted/types/index.ts` 没有“运行事件 / agent 卡片 / artifact / process timeline”概念。
- 顶部导航 `/home/ivan/xuan/steven_project/nanobot-fronted/components/Header.tsx` 目前没有 `Agents` / `MCP` 页入口。
- 现有 `skills` / `plugins` 页面适合复用作管理页风格参考:
- `/home/ivan/xuan/steven_project/nanobot-fronted/app/skills/page.tsx`
- `/home/ivan/xuan/steven_project/nanobot-fronted/app/plugins/page.tsx`
### 2.2 后端现状
后端已经具备部分多 agent 能力,但还不够支撑前端过程可视化:
- 已有统一 agent 列表接口:`GET /api/agents`
- 位置:`/home/ivan/xuan/steven_project/nanobot-backend/nanobot/web/server.py`
- 已有 A2A / group delegation 逻辑:
- `/home/ivan/xuan/steven_project/nanobot-backend/nanobot/agent/delegation.py`
- 已有 A2A streaming / resubscribe / cancel
- `/home/ivan/xuan/steven_project/nanobot-backend/nanobot/a2a/client.py`
- 但当前对前端暴露的实时消息仍然只有:
- `status=thinking`
- `assistant message`
- `DelegationManager` 现在对外发布的也只是普通 `_progress` 文本,例如:
- `[AgentName] ...`
- MCP 目前只有后端连接配置和工具注册,没有独立的 Web 管理接口,也没有结构化 MCP 运行事件。
结论:
前端可以先做布局和状态层改造,但如果想真正展示“每个 agent 的框、每个 MCP 的产物、agent 间问答”,后端必须补一层结构化 process event 协议。只靠现在的纯文本 progress 不够。
## 3. 推荐的界面形态
桌面端建议改成三栏工作台,而不是继续沿用现在的单栏聊天布局。
### 3.1 桌面布局
```text
┌──────────────┬───────────────────────────────────────┬──────────────────────────┐
│ 会话侧栏 │ 主聊天 + 过程泳道 │ 结果侧栏 │
│ Sessions │ │ Results / Artifacts │
│ │ 用户消息 │ 当前运行摘要 │
│ │ assistant 最终总结 │ agent 产物列表 │
│ │ ─────────────────────────────────── │ MCP 产物列表 │
│ │ Agent A 卡片 │ 文件/图片/JSON 预览 │
│ │ Agent B 卡片 │ 错误/告警 │
│ │ MCP github 卡片 │ 最终汇总结论 │
│ │ MCP browser 卡片 │ │
└──────────────┴───────────────────────────────────────┴──────────────────────────┘
```
### 3.2 移动端布局
移动端不要硬保留三栏:
1. 主聊天区保留为默认视图。
2. 过程泳道和结果侧栏改成底部 `Tabs``Drawer`
3. 正在运行时,顶部显示一个 `Process (3)` 悬浮入口。
### 3.3 视觉原则
不要把过程信息混成普通 assistant markdown。
应明确区分三类对象:
1. `Chat Message`:用户问题、最终总结。
2. `Process Card`:某个 agent 或 MCP 的运行容器。
3. `Artifact`:某个步骤产出的结构化结果。
建议:
- Agent 卡片用清晰的状态边框和标题区。
- MCP 卡片强调“工具/服务器”属性,避免与 agent 混淆。
- 结果侧栏始终可见,展示当前选中卡片的详细结果。
## 4. 目标交互
### 4.1 单 Agent
用户发出问题后:
1. 主聊天区出现用户消息。
2. assistant 进入 `thinking`
3. 若命中 `spawn` / A2A delegation过程泳道新增一个 Agent 卡片。
4. 卡片内部流式更新:
- 状态queued / running / waiting / done / error / cancelled
- 文本片段
- agent 生成的中间消息
- 关键参数或结果摘要
5. 如果 agent 调了 MCP再在该卡片内部挂子步骤或在泳道新增 MCP 卡片。
6. 右侧结果栏展示:
- 当前 agent 的最新摘要
- 产物列表
- 可预览文件
7. 所有 agent 结束后,主 assistant 再给一条最终总结回复。
### 4.2 多 Agent Group
如果是 group delegation
1. 过程泳道里要同时出现多个 Agent 卡片。
2. 每个卡片独立流式刷新,不要合并成一条文本。
3. 结果侧栏支持切换:
- `All`
- `Agent A`
- `Agent B`
- `MCP Outputs`
4. 最终 assistant 总结要包含:
- 共识
- 分歧
- 失败项
- 最终建议
### 4.3 Agent 间“一问一答”
如果未来后端能发出 agent-to-agent message event前端直接把这些消息渲染到卡片里的 transcript 区。
建议 UI 表现:
- 卡片头agent 名称、来源、状态、耗时
- 卡片体:
- `Transcript`
- `Steps`
- `Artifacts`
- 卡片尾:最终摘要 / 错误信息
## 5. 前端改造点
## 5.1 先不要继续把逻辑堆进 `app/page.tsx`
当前 `/home/ivan/xuan/steven_project/nanobot-fronted/app/page.tsx` 已经过大。这个需求如果继续直接堆,会很快失控。
建议拆分。
### 5.2 建议新增的组件与文件
建议新增目录:
- `components/chat-workbench/ChatWorkbench.tsx`
- `components/chat-workbench/ProcessLane.tsx`
- `components/chat-workbench/ProcessRunCard.tsx`
- `components/chat-workbench/ProcessTranscript.tsx`
- `components/chat-workbench/ArtifactSidebar.tsx`
- `components/chat-workbench/RunSummaryPanel.tsx`
- `components/chat-workbench/AgentBadge.tsx`
- `components/chat-workbench/McpBadge.tsx`
- `components/chat-workbench/StatusPill.tsx`
建议职责:
- `ChatWorkbench.tsx`
- 负责三栏布局组合。
- `ProcessLane.tsx`
- 渲染当前 session 的所有 process run。
- `ProcessRunCard.tsx`
- 渲染单个 agent / MCP 卡片。
- `ProcessTranscript.tsx`
- 渲染步骤流、问答片段、进度文本。
- `ArtifactSidebar.tsx`
- 渲染右侧产物栏。
- `RunSummaryPanel.tsx`
- 展示当前 run 的状态概览和最终摘要。
### 5.3 对现有文件的插入建议
#### `/home/ivan/xuan/steven_project/nanobot-fronted/app/page.tsx`
保留职责:
- session 列表
- 输入框
- 顶层页面组织
减少职责:
- 不再在这里直接渲染复杂过程 UI
- 不再在这里直接解析 process 事件
建议修改为:
1. 左侧会话侧栏基本保留。
2. 中间改成 `<ChatWorkbench />`
3. `MessageBubble` 可以保留,但只负责普通 `user/assistant` 消息。
4. 新增一个 `selectedRunId` / `selectedArtifactId` 的页面级状态,或者放进 Zustand store。
#### `/home/ivan/xuan/steven_project/nanobot-fronted/lib/store.ts`
这里需要从“聊天 store”升级成“聊天 + 过程 store”。
建议新增状态:
- `processRuns: ProcessRun[]`
- `processEvents: ProcessEvent[]`
- `artifacts: ProcessArtifact[]`
- `selectedRunId: string | null`
- `selectedArtifactId: string | null`
- `activeRunIds: string[]`
- `agentRegistry: UiAgentDescriptor[]`
- `mcpRegistry: UiMcpServerDescriptor[]`
建议新增 action
- `resetProcessState(sessionId)`
- `upsertProcessRun(run)`
- `appendProcessEvent(event)`
- `appendProcessArtifact(artifact)`
- `finishProcessRun(runId, status)`
- `cancelProcessRun(runId)`
- `setSelectedRunId(runId)`
- `setSelectedArtifactId(artifactId)`
- `setAgentRegistry(agents)`
- `setMcpRegistry(servers)`
#### `/home/ivan/xuan/steven_project/nanobot-fronted/types/index.ts`
这里需要新增完整类型层。
建议新增:
```ts
export type ProcessActorType = 'agent' | 'mcp' | 'system';
export type ProcessRunStatus = 'queued' | 'running' | 'waiting' | 'done' | 'error' | 'cancelled';
export type ProcessEventKind =
| 'run_started'
| 'run_progress'
| 'run_message'
| 'run_artifact'
| 'run_status'
| 'run_finished'
| 'run_cancelled';
export interface UiAgentDescriptor {
id: string;
name: string;
description: string;
source: 'workspace' | 'plugin' | 'skill' | 'builtin';
kind: string;
protocol: string | null;
tags: string[];
aliases: string[];
support_streaming: boolean;
}
export interface UiMcpServerDescriptor {
id: string;
name: string;
transport: 'stdio' | 'http';
url?: string;
command?: string;
enabled: boolean;
tool_count?: number;
tool_names?: string[];
status?: 'connected' | 'disconnected' | 'error';
last_error?: string | null;
}
export interface ProcessRun {
run_id: string;
parent_run_id?: string | null;
session_id: string;
actor_type: ProcessActorType;
actor_id: string;
actor_name: string;
title: string;
status: ProcessRunStatus;
started_at: string;
finished_at?: string | null;
summary?: string | null;
source?: string | null;
}
export interface ProcessEvent {
event_id: string;
run_id: string;
parent_run_id?: string | null;
kind: ProcessEventKind;
actor_type: ProcessActorType;
actor_id: string;
actor_name: string;
text?: string;
status?: ProcessRunStatus;
message_role?: 'system' | 'user' | 'assistant' | 'tool';
metadata?: Record<string, unknown>;
created_at: string;
}
export interface ProcessArtifact {
artifact_id: string;
run_id: string;
actor_type: ProcessActorType;
actor_id: string;
title: string;
artifact_type: 'text' | 'json' | 'file' | 'image' | 'link' | 'markdown';
content?: string;
data?: Record<string, unknown> | unknown[];
file_id?: string;
url?: string;
created_at: string;
}
```
#### `/home/ivan/xuan/steven_project/nanobot-fronted/lib/api.ts`
这里要扩展三类能力:
1. Agent 管理 API
2. MCP 管理 API
3. WebSocket process event 订阅
建议新增:
- `listAgents()`
- `addAgent()`
- `deleteAgent()`
- `refreshAgents()`
- `listMcpServers()`
- `addMcpServer()`
- `updateMcpServer()`
- `deleteMcpServer()`
- `testMcpServer()`
同时把 `WsMessageHandler` 从现在的宽松结构,升级成联合类型:
```ts
export type WsEvent =
| ChatAssistantEvent
| ChatThinkingEvent
| ProcessRunStartedEvent
| ProcessRunUpdatedEvent
| ProcessArtifactEvent
| ProcessRunFinishedEvent
| ProcessRunCancelledEvent;
```
#### `/home/ivan/xuan/steven_project/nanobot-fronted/components/Header.tsx`
导航中建议新增:
- `/agents`
- `/mcp`
放在 `skills` / `plugins` 旁边,不要塞进聊天页内部。
### 5.4 建议新增页面
- `/home/ivan/xuan/steven_project/nanobot-fronted/app/agents/page.tsx`
- `/home/ivan/xuan/steven_project/nanobot-fronted/app/mcp/page.tsx`
Agent 页面参考 `skills + plugins` 的中间态:
- 列表视图
- 支持新增、删除、刷新
- 展示来源workspace / plugin / skill / builtin
- 展示协议a2a / local
- 展示标签、别名、streaming/group 支持
MCP 页面建议分两块:
1. `Configured Servers`
2. `Discovered Tools`
每个 MCP server 展示:
- 连接方式stdio / http
- 地址或命令
- tool 数量
- 连接状态
- 最后错误
- 编辑/删除/测试按钮
## 6. 聊天页的推荐逻辑链路
这是前端应当遵守的主链路。
### 6.1 用户发消息
1. 用户在 `app/page.tsx` 输入消息。
2. 立即写入 `messages[]`
3. 设置 `isLoading=true`
4. 如果 WebSocket 已连接,消息通过 `wsManager.sendRaw()` 发出去。
5. 前端等待两类数据:
- 普通 assistant reply
- process events
### 6.2 触发 sub-agent / group / MCP
后端一旦进入 delegation / MCP tool 调用,应向前端发结构化 process event。
前端收到后:
1. `run_started` -> 创建卡片。
2. `run_progress` -> 更新卡片中的 transcript。
3. `run_artifact` -> 写入右侧侧栏。
4. `run_status` -> 更新状态 pill。
5. `run_finished` -> 收起 loading保留结果。
6. 最终 `assistant message` -> 输出总结性回复。
### 6.3 用户点击某个 Agent / MCP 卡片
1. 设置 `selectedRunId`
2. 右侧 `ArtifactSidebar` 切换到该 run 的 artifact 列表。
3. 中间卡片高亮。
4. 若有 transcript则显示完整流。
### 6.4 用户取消运行
如果后端暴露 cancel 接口或 WebSocket cancel command
1. 卡片上显示 `Cancel`
2. 用户点击后发送 cancel 请求。
3. run 状态变为 `cancelled`
4. 侧栏保留已有产物,但标记“未完成”。
## 7. 后端必须补的事件协议
这是这次前端能否做成的关键。
当前后端只发普通文本 `_progress`,不够。
必须新增结构化 WebSocket 事件。建议统一成 `type=process_*`
### 7.1 建议的事件集合
#### `process_run_started`
```json
{
"type": "process_run_started",
"session_id": "web:default",
"run_id": "deleg-123",
"parent_run_id": null,
"actor_type": "agent",
"actor_id": "repo-reviewer",
"actor_name": "Repo Reviewer",
"source": "workspace",
"title": "Review auth refactor",
"status": "running",
"created_at": "2026-03-06T10:00:00Z"
}
```
#### `process_run_progress`
```json
{
"type": "process_run_progress",
"run_id": "deleg-123",
"actor_type": "agent",
"actor_id": "repo-reviewer",
"text": "Scanning auth middleware and session lifecycle",
"created_at": "2026-03-06T10:00:03Z"
}
```
#### `process_run_message`
用于展示 agent 间问答或 agent 内部消息。
```json
{
"type": "process_run_message",
"run_id": "deleg-123",
"actor_type": "agent",
"actor_id": "repo-reviewer",
"message_role": "assistant",
"text": "I need the gateway config file before deciding.",
"created_at": "2026-03-06T10:00:04Z"
}
```
#### `process_run_artifact`
```json
{
"type": "process_run_artifact",
"run_id": "mcp-456",
"actor_type": "mcp",
"actor_id": "github",
"title": "Pull Request Diff Summary",
"artifact_type": "markdown",
"content": "...",
"created_at": "2026-03-06T10:00:08Z"
}
```
#### `process_run_status`
```json
{
"type": "process_run_status",
"run_id": "deleg-123",
"status": "waiting",
"text": "Waiting for remote agent task completion",
"created_at": "2026-03-06T10:00:10Z"
}
```
#### `process_run_finished`
```json
{
"type": "process_run_finished",
"run_id": "deleg-123",
"status": "done",
"summary": "Found 2 risks in auth token refresh flow.",
"created_at": "2026-03-06T10:00:20Z"
}
```
#### `process_run_cancelled`
```json
{
"type": "process_run_cancelled",
"run_id": "deleg-123",
"status": "cancelled",
"created_at": "2026-03-06T10:00:12Z"
}
```
### 7.2 后端推荐插入点
如果你后面让我直接改前后端,我会从这些点切:
#### `/home/ivan/xuan/steven_project/nanobot-backend/nanobot/agent/delegation.py`
这里最适合发 agent 级 process event
- `dispatch()` 开始时发 `process_run_started`
- `_build_progress_callback()` 中把 A2A stream 文本转成 `process_run_progress`
- `_run_group()` 中每个 descriptor 启动时发独立子 run
- `_announce_single_result()` 之前发 `process_run_finished`
- `_announce_group_result()` 之前发 group summary run finished
- `cancel()` / `_announce_cancelled()``process_run_cancelled`
#### `/home/ivan/xuan/steven_project/nanobot-backend/nanobot/a2a/client.py`
这里最适合补更细的远端 agent 消息:
- `_consume_stream_method()`
- `_resume_subscription()`
如果远端流里有 message chunk / state / artifact就在这里归一化后向上抛给 `DelegationManager`
#### `/home/ivan/xuan/steven_project/nanobot-backend/nanobot/agent/tools/mcp.py`
这里最适合发 MCP 级事件:
- MCP 工具调用开始 -> `process_run_started` (`actor_type=mcp`)
- 工具标准输出 / 中间结果 -> `process_run_progress`
- 工具返回文本 / JSON / 文件 -> `process_run_artifact`
- 工具调用完成 -> `process_run_finished`
- 超时 / 失败 -> `process_run_status` + `process_run_finished(status=error)`
#### `/home/ivan/xuan/steven_project/nanobot-backend/nanobot/web/server.py`
这里需要扩展 WebSocket 发送协议,而不是只发 `thinking/message`
## 8. Agent 管理页方案
### 8.1 页面目标
让用户能像管理 `skills` 一样管理委派目标 agent。
### 8.2 数据来源
直接用现有接口:
- `GET /api/agents`
- `POST /api/agents`
- `DELETE /api/agents/{id}`
- `POST /api/agents/refresh`
### 8.3 页面布局建议
参考 `plugins` 页的卡片布局,但要比 `plugins` 更偏“资源管理”。
建议字段:
- 名称
- id
- description
- source
- protocol
- tags
- aliases
- support_streaming
- endpoint / base_url / card_url
建议交互:
1. 顶部 `Refresh`
2. 顶部 `Add Agent`
3. 列表卡片
4. workspace agent 允许删除
5. plugin / skill / builtin agent 只读
### 8.4 Add Agent 弹窗字段
- `id`
- `name`
- `description`
- `protocol`,先只放 `a2a`
- `base_url`
- `endpoint`
- `card_url`
- `auth_env`
- `tags`
- `aliases`
- `enabled`
## 9. MCP 管理页方案
### 9.1 结论先说
这个页面前端不能单独完成,因为当前后端没有 MCP 管理 API。
所以文档给的是“前端页面方案 + 后端配套接口定义”。
### 9.2 后端建议增加的 API
建议新增:
- `GET /api/mcp/servers`
- `POST /api/mcp/servers`
- `PUT /api/mcp/servers/{id}`
- `DELETE /api/mcp/servers/{id}`
- `POST /api/mcp/servers/{id}/test`
- `GET /api/mcp/tools`
### 9.3 MCP server 返回结构建议
```json
{
"id": "github",
"name": "github",
"transport": "http",
"url": "http://localhost:3001/mcp",
"command": "",
"args": [],
"enabled": true,
"tool_timeout": 30,
"headers": {},
"status": "connected",
"tool_count": 12,
"tool_names": ["search_repos", "list_prs"],
"last_error": null
}
```
### 9.4 页面布局建议
上半区MCP servers
- 卡片或表格
- 编辑 / 删除 / 测试连接
下半区Discovered tools
- 按 server 分组
- 展示工具名、说明、schema 摘要
## 10. 建议的前端实现顺序
按这个顺序做最稳。
### Phase 1: 先做前端数据结构重构
1.`types/index.ts`
2.`lib/store.ts`
3.`lib/api.ts` 的 ws event 类型
4.`app/page.tsx` 拆出 `ChatWorkbench`
这一步即使后端结构化事件还没补,也可以先用 mock data 跑布局。
### Phase 2: 落三栏工作台 UI
1. 中间主聊天区保留现有 message bubble
2.`ProcessLane`
3.`ArtifactSidebar`
4. 支持选中某个 run
### Phase 3: 接后端 process events
1.`wsManager.onMessage()` 加 process 事件分发
2. store 按 event 更新 process state
3. 卡片流式刷新
### Phase 4: 新增 Agent 管理页
1. `app/agents/page.tsx`
2. `lib/api.ts` 增 Agent API
3. `Header.tsx` 增导航
### Phase 5: 新增 MCP 管理页
1. 后端先补接口
2. 前端 `app/mcp/page.tsx`
3. 管理 + 测试连接 + 工具查看
## 11. 为什么我建议最好由同一个 Codex 连前后端一起改
如果只是做视觉壳子,另一个 Codex 在前端仓库里单独改也可以。
但如果目标是你描述的完整体验:
- 每个 agent / MCP 弹出独立框
- 展示过程中的一问一答
- 展示 MCP 产物
- 最后再统一总结
那就不是纯前端问题,而是“后端事件模型 + 前端状态模型”联动问题。
结论:
- 只写前端:可以先做静态布局和 store 重构。
- 真正做成:最好同一个人连续改 backend + frontend避免事件协议和 UI 状态设计脱节。
## 12. 给另一个 Codex 的明确施工指令
如果你把这份文档交给另一个 Codex建议直接给它下面这段要求
1. 先阅读:
- `/home/ivan/xuan/steven_project/nanobot-fronted/app/page.tsx`
- `/home/ivan/xuan/steven_project/nanobot-fronted/lib/store.ts`
- `/home/ivan/xuan/steven_project/nanobot-fronted/lib/api.ts`
- `/home/ivan/xuan/steven_project/nanobot-fronted/types/index.ts`
- `/home/ivan/xuan/steven_project/nanobot-fronted/app/plugins/page.tsx`
- `/home/ivan/xuan/steven_project/nanobot-fronted/app/skills/page.tsx`
2. 先把聊天页拆成三栏工作台,不要继续把复杂逻辑堆在 `app/page.tsx`
3. 先做 `processRuns / processEvents / artifacts` 的前端数据模型。
4. 先接 `/api/agents` 做 Agent 管理页。
5. MCP 管理页先按文档搭 UI 壳子,但要显式标记“依赖后端 MCP API”。
6. 如果要做真实过程可视化,不要拿普通 markdown 消息硬解析,必须等结构化 WebSocket process events。
## 13. 最小可交付版本
如果要先做一个能看的版本,建议这样收敛:
1. 聊天页先做三栏布局。
2. 用当前 `_progress` 文本先临时映射成 Agent 卡片日志。
3. 先接 `/api/agents` 做管理页。
4. MCP 页先做只读占位页,提示“等待后端 MCP API”。
5. 第二轮再补真正结构化 process events。
这条路径的好处是:
- UI 先起来
- 后端协议第二轮再精修
- 不会一开始就卡死在全链路联调上
## 14. 推荐文档结论
最合适的落地方式是:
1. 前端先重构成“聊天消息”和“过程运行”两套状态。
2. 聊天页改成三栏工作台。
3. 先接现有 `/api/agents` 做 Agent 管理页。
4. MCP 管理页需要后端先补接口。
5. 真正的过程可视化必须补结构化 WebSocket process events核心后端插入点是
- `nanobot/agent/delegation.py`
- `nanobot/a2a/client.py`
- `nanobot/agent/tools/mcp.py`
- `nanobot/web/server.py`

View File

@ -1,5 +0,0 @@
import { redirect } from 'next/navigation';
export default function OfficeTaskRedirectPage({ params }: { params: { taskId: string } }) {
redirect(`/tasks/${params.taskId}`);
}

View File

@ -1,5 +0,0 @@
import { redirect } from 'next/navigation';
export default function OfficeRedirectPage() {
redirect('/tasks');
}

View File

@ -1,293 +0,0 @@
'use client';
import React, { useEffect, useState } from 'react';
import {
Blocks,
RefreshCw,
Loader2,
AlertCircle,
Bot,
Terminal,
Wrench,
ChevronDown,
ChevronRight,
Globe,
FolderOpen,
} from 'lucide-react';
import { listPlugins } from '@/lib/api';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import type { PluginInfo } from '@/types';
import { pickAppText } from '@/lib/i18n/core';
import { useAppI18n } from '@/lib/i18n/provider';
export default function PluginsPage() {
const { locale } = useAppI18n();
const [plugins, setPlugins] = useState<PluginInfo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const load = async () => {
setLoading(true);
setError(null);
try {
const data = await listPlugins();
setPlugins(Array.isArray(data) ? data : []);
} catch (err: any) {
setError(err.message || pickAppText(locale, '加载插件失败', 'Failed to load plugins'));
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
}, []);
if (loading) {
return (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="max-w-5xl mx-auto p-6 space-y-6">
{/* Page header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
<Blocks className="w-6 h-6" />
{pickAppText(locale, '插件', 'Plugins')}
</h1>
<p className="text-sm text-muted-foreground mt-1">
{pickAppText(locale, '已安装位置:全局插件目录或当前 workspace 的 ', 'Installed from the global plugin directory or this workspace\'s ')}
<code className="text-xs bg-muted px-1 py-0.5 rounded">plugins/</code>
</p>
</div>
<Button onClick={load} variant="outline" size="sm">
<RefreshCw className="w-4 h-4 mr-2" />
{pickAppText(locale, '刷新', 'Refresh')}
</Button>
</div>
{/* Error */}
{error && (
<Card className="border-destructive">
<CardContent className="pt-6">
<div className="flex items-center gap-2 text-destructive text-sm">
<AlertCircle className="w-4 h-4" />
{error}
</div>
</CardContent>
</Card>
)}
{/* Empty state */}
{!error && plugins.length === 0 && (
<Card>
<CardContent className="py-16 text-center text-muted-foreground">
<Blocks className="w-12 h-12 mx-auto mb-4 opacity-30" />
<p className="font-medium">{pickAppText(locale, '还没有安装任何插件', 'No plugins are installed yet')}</p>
<p className="text-sm mt-2 max-w-sm mx-auto">
{pickAppText(locale, '把插件目录放到全局插件目录或当前 workspace 的 ', 'Put a plugin directory in the global plugin directory or this workspace\'s ')}
<code className="text-xs bg-muted px-1 py-0.5 rounded">plugins/</code>
{pickAppText(locale, ',然后重启 Boardware Agent Sandbox。', ', then restart Boardware Agent Sandbox.')}
</p>
</CardContent>
</Card>
)}
{/* Plugin cards */}
<div className="space-y-4">
{plugins.map((plugin) => (
<PluginCard key={plugin.name} plugin={plugin} />
))}
</div>
</div>
);
}
function PluginCard({ plugin }: { plugin: PluginInfo }) {
const { locale } = useAppI18n();
const [agentsOpen, setAgentsOpen] = useState(true);
const [commandsOpen, setCommandsOpen] = useState(true);
const [skillsOpen, setSkillsOpen] = useState(false);
const totalItems = plugin.agents.length + plugin.commands.length + plugin.skills.length;
return (
<Card>
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<CardTitle className="text-base font-semibold">{plugin.name}</CardTitle>
<SourceBadge source={plugin.source} />
</div>
{plugin.description && (
<p className="text-sm text-muted-foreground mt-1 leading-relaxed">
{plugin.description}
</p>
)}
</div>
{/* Summary chips */}
<div className="flex items-center gap-1.5 shrink-0 flex-wrap justify-end">
{plugin.agents.length > 0 && (
<span className="flex items-center gap-1 text-xs bg-muted px-2 py-0.5 rounded-full">
<Bot className="w-3 h-3" />
{pickAppText(locale, `${plugin.agents.length} 个智能体`, `${plugin.agents.length} agents`)}
</span>
)}
{plugin.commands.length > 0 && (
<span className="flex items-center gap-1 text-xs bg-muted px-2 py-0.5 rounded-full">
<Terminal className="w-3 h-3" />
{pickAppText(locale, `${plugin.commands.length} 条命令`, `${plugin.commands.length} commands`)}
</span>
)}
{plugin.skills.length > 0 && (
<span className="flex items-center gap-1 text-xs bg-muted px-2 py-0.5 rounded-full">
<Wrench className="w-3 h-3" />
{pickAppText(locale, `${plugin.skills.length} 个技能`, `${plugin.skills.length} skills`)}
</span>
)}
</div>
</div>
</CardHeader>
{totalItems > 0 && (
<CardContent className="pt-0 space-y-3">
{/* Agents */}
{plugin.agents.length > 0 && (
<Section
icon={<Bot className="w-3.5 h-3.5" />}
label={pickAppText(locale, '智能体', 'Agents')}
count={plugin.agents.length}
open={agentsOpen}
onToggle={() => setAgentsOpen((v) => !v)}
>
<div className="divide-y divide-border rounded-md border">
{plugin.agents.map((agent) => (
<div key={agent.name} className="px-3 py-2 flex items-start gap-3">
<code className="text-xs font-mono text-primary shrink-0 mt-0.5">
{agent.name}
</code>
<div className="flex-1 min-w-0">
<p className="text-xs text-muted-foreground leading-relaxed line-clamp-2">{agent.description || '—'}</p>
</div>
{agent.model && (
<Badge variant="outline" className="text-xs shrink-0">
{agent.model}
</Badge>
)}
</div>
))}
</div>
</Section>
)}
{/* Commands */}
{plugin.commands.length > 0 && (
<Section
icon={<Terminal className="w-3.5 h-3.5" />}
label={pickAppText(locale, '命令', 'Commands')}
count={plugin.commands.length}
open={commandsOpen}
onToggle={() => setCommandsOpen((v) => !v)}
>
<div className="divide-y divide-border rounded-md border">
{plugin.commands.map((cmd) => (
<div key={cmd.name} className="px-3 py-2 flex items-start gap-3">
<div className="flex items-center gap-1.5 shrink-0 mt-0.5">
<code className="text-xs font-mono text-primary">/{cmd.name}</code>
{cmd.argument_hint && (
<span className="text-xs text-muted-foreground">{cmd.argument_hint}</span>
)}
</div>
<p className="text-xs text-muted-foreground leading-relaxed line-clamp-2">
{cmd.description || '—'}
</p>
</div>
))}
</div>
</Section>
)}
{/* Skills */}
{plugin.skills.length > 0 && (
<Section
icon={<Wrench className="w-3.5 h-3.5" />}
label={pickAppText(locale, '技能', 'Skills')}
count={plugin.skills.length}
open={skillsOpen}
onToggle={() => setSkillsOpen((v) => !v)}
>
<div className="flex flex-wrap gap-1.5">
{plugin.skills.map((skill) => (
<Badge key={skill} variant="secondary" className="text-xs font-mono">
{skill}
</Badge>
))}
</div>
</Section>
)}
</CardContent>
)}
</Card>
);
}
function SourceBadge({ source }: { source: 'global' | 'workspace' }) {
const { locale } = useAppI18n();
if (source === 'workspace') {
return (
<Badge variant="default" className="text-xs gap-1">
<FolderOpen className="w-3 h-3" />
{pickAppText(locale, '工作区', 'Workspace')}
</Badge>
);
}
return (
<Badge variant="secondary" className="text-xs gap-1">
<Globe className="w-3 h-3" />
{pickAppText(locale, '全局', 'Global')}
</Badge>
);
}
function Section({
icon,
label,
count,
open,
onToggle,
children,
}: {
icon: React.ReactNode;
label: string;
count: number;
open: boolean;
onToggle: () => void;
children: React.ReactNode;
}) {
return (
<div>
<button
onClick={onToggle}
className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors mb-2 w-full text-left"
>
{open ? (
<ChevronDown className="w-3.5 h-3.5" />
) : (
<ChevronRight className="w-3.5 h-3.5" />
)}
{icon}
{label}
<span className="ml-1 text-muted-foreground/60">({count})</span>
</button>
{open && children}
</div>
);
}

View File

@ -1,194 +0,0 @@
'use client';
import { AlertCircle, Bot, BrainCircuit, Loader2, ServerCog, Square } from 'lucide-react';
import type { ProcessEvent, ProcessRun } from '@/types';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ScrollArea } from '@/components/ui/scroll-area';
import { appActorTypeLabel, appEventKindLabel, appStatusLabel } from '@/lib/i18n/common';
import { pickAppText } from '@/lib/i18n/core';
import { useAppI18n } from '@/lib/i18n/provider';
import { cn } from '@/lib/utils';
function statusTone(status: string) {
if (status === 'done') return 'border-[#B7C2B5] bg-[#E3E8E2] text-[#657162]';
if (status === 'error') return 'border-[#B8AEA8] bg-[#E7E2DE] text-[#342E2B]';
if (status === 'cancelled') return 'border-[#D8D2CE] bg-[#ECE8E5] text-[#6A5E58]';
if (status === 'waiting') return 'border-[#B8AEA8] bg-[#E7E2DE] text-[#5F5550]';
return 'border-[#BCC4CE] bg-[#E4E7EB] text-[#697281]';
}
function actorIcon(run: ProcessRun) {
if (run.actor_type === 'mcp') return <ServerCog className="w-4 h-4" />;
if (run.actor_type === 'system') return <BrainCircuit className="w-4 h-4" />;
return <Bot className="w-4 h-4" />;
}
export function ProcessLane({
runs,
events,
selectedRunId,
onSelectRun,
onCancelRun,
}: {
runs: ProcessRun[];
events: ProcessEvent[];
selectedRunId: string | null;
onSelectRun: (runId: string) => void;
onCancelRun: (runId: string) => void;
}) {
const { locale } = useAppI18n();
const sortedRuns = [...runs].sort((a, b) => {
const at = new Date(a.started_at).getTime();
const bt = new Date(b.started_at).getTime();
return bt - at;
});
if (sortedRuns.length === 0) {
return null;
}
return (
<div className="h-full flex flex-col bg-card/60 border-l border-border">
<div className="px-4 py-3 border-b border-border flex items-center justify-between">
<div>
<h2 className="text-sm font-semibold tracking-wide uppercase text-muted-foreground">{pickAppText(locale, '执行过程', 'Execution')}</h2>
<p className="text-xs text-muted-foreground mt-1">{pickAppText(locale, '智能体、A2A、MCP 的实时过程', 'Live process stream for agents, A2A, and MCP')}</p>
</div>
<Badge variant="outline" className="text-xs">
{pickAppText(locale, `${sortedRuns.length} 个任务`, `${sortedRuns.length} tasks`)}
</Badge>
</div>
<ScrollArea className="flex-1 px-4 py-4">
<div className="space-y-3">
{sortedRuns.map((run) => {
const runEvents = events
.filter((event) => event.run_id === run.run_id)
.slice(-5)
.reverse();
const isSelected = run.run_id === selectedRunId;
const canCancel =
!run.parent_run_id &&
run.actor_type !== 'mcp' &&
(run.status === 'running' || run.status === 'waiting');
return (
<Card
key={run.run_id}
className={cn(
'cursor-pointer transition-colors border-border/80 hover:border-primary/40',
isSelected && 'border-primary bg-primary/5'
)}
onClick={() => onSelectRun(run.run_id)}
>
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 flex-wrap">
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center text-muted-foreground">
{actorIcon(run)}
</div>
<div className="min-w-0">
<CardTitle className="text-sm leading-none truncate">{run.actor_name}</CardTitle>
<p className="text-xs text-muted-foreground mt-1 truncate">{run.title}</p>
</div>
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<Badge variant="outline" className={cn('text-[10px] border', statusTone(run.status))}>
{appStatusLabel(run.status, locale)}
</Badge>
{canCancel && (
<Button
variant="outline"
size="sm"
className="h-7 px-2"
onClick={(event) => {
event.stopPropagation();
onCancelRun(run.run_id);
}}
>
<Square className="w-3.5 h-3.5 mr-1" />
{pickAppText(locale, '取消', 'Cancel')}
</Button>
)}
</div>
</div>
</CardHeader>
<CardContent className="pt-0 space-y-2">
<div className="flex items-center gap-2 text-[11px] text-muted-foreground flex-wrap">
<span>{appActorTypeLabel(run.actor_type, locale)}</span>
{run.source && <span>{run.source}</span>}
{run.parent_run_id && <span>{pickAppText(locale, '子任务', 'Subtask')}</span>}
</div>
{run.summary && (
<div className="rounded-md bg-muted/40 px-3 py-2 text-xs text-muted-foreground whitespace-pre-wrap line-clamp-3">
{run.summary}
</div>
)}
<SkillMetadata metadata={run.metadata} />
<div className="space-y-1.5">
{runEvents.length === 0 && run.status === 'running' && (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Loader2 className="w-3.5 h-3.5 animate-spin" />
{pickAppText(locale, '等待首个事件...', 'Waiting for the first event...')}
</div>
)}
{runEvents.map((event) => (
<div key={event.event_id} className="text-xs rounded-md border border-border/50 bg-background/60 px-3 py-2">
<div className="flex items-center gap-2 text-[10px] uppercase tracking-wide text-muted-foreground mb-1">
<span>{appEventKindLabel(event.kind, locale)}</span>
{event.status && <span>{appStatusLabel(event.status, locale)}</span>}
</div>
<div className="text-foreground/90 whitespace-pre-wrap break-words">
{event.text || pickAppText(locale, '结构化更新', 'Structured update')}
</div>
</div>
))}
{run.status === 'error' && (
<div className="flex items-center gap-2 text-xs text-[#5F5550]">
<AlertCircle className="w-3.5 h-3.5" />
{pickAppText(locale, '此任务执行失败。', 'This task failed.')}
</div>
)}
</div>
</CardContent>
</Card>
);
})}
</div>
</ScrollArea>
</div>
);
}
function SkillMetadata({ metadata }: { metadata?: Record<string, unknown> }) {
const rawSelected = metadata?.selected_skill_names;
const rawEphemeral = metadata?.ephemeral_skill_names;
const selected = Array.isArray(rawSelected) ? rawSelected.map(String).filter(Boolean) : [];
const ephemeral = Array.isArray(rawEphemeral) ? rawEphemeral.map(String).filter(Boolean) : [];
const guidanceId = typeof metadata?.ephemeral_guidance_id === 'string' ? metadata.ephemeral_guidance_id : '';
if (selected.length === 0 && ephemeral.length === 0 && !guidanceId) {
return null;
}
return (
<div className="flex flex-wrap gap-1.5 text-[11px]">
{selected.map((name) => (
<Badge key={`skill:${name}`} variant="secondary" className="text-[10px]">
skill:{name}
</Badge>
))}
{ephemeral.map((name) => (
<Badge key={`ephemeral:${name}`} variant="outline" className="text-[10px]">
ephemeral:{name}
</Badge>
))}
{guidanceId && (
<Badge variant="outline" className="text-[10px]">
guidance:{guidanceId.slice(0, 8)}
</Badge>
)}
</div>
);
}

View File

@ -1,488 +0,0 @@
'use client';
import React from 'react';
import type { OfficeMemberView, OfficeTaskStatus, OfficeView, OfficeZoneId } from '@/lib/office';
import { cn } from '@/lib/utils';
type ZoneLayout = {
x: number;
y: number;
width: number;
height: number;
};
const WORLD_WIDTH = 400;
const WORLD_HEIGHT = 225;
const RENDER_SCALE = 2;
const SCENE_WIDTH = WORLD_WIDTH * RENDER_SCALE;
const SCENE_HEIGHT = WORLD_HEIGHT * RENDER_SCALE;
const TILE_SIZE = 16;
const MAP_KEY = 'office-winter-v1';
const TILESET_KEY = 'office-winter-tileset';
const MAP_PATH = '/office/maps/office-winter-v1.tmj';
const TILESET_PATH = '/office/tiles/office-winter-tileset.png';
const PIXEL_AGENTS_BASE = '/office/vendor/pixel-agents/assets';
const FURNITURE_ASSETS = {
deskFront: { key: 'pixel-agents-desk-front', path: `${PIXEL_AGENTS_BASE}/furniture/DESK/DESK_FRONT.png` },
chairFront: { key: 'pixel-agents-chair-front', path: `${PIXEL_AGENTS_BASE}/furniture/WOODEN_CHAIR/WOODEN_CHAIR_FRONT.png` },
sofaFront: { key: 'pixel-agents-sofa-front', path: `${PIXEL_AGENTS_BASE}/furniture/SOFA/SOFA_FRONT.png` },
tableFront: { key: 'pixel-agents-table-front', path: `${PIXEL_AGENTS_BASE}/furniture/TABLE_FRONT/TABLE_FRONT.png` },
coffeeTable: { key: 'pixel-agents-coffee-table', path: `${PIXEL_AGENTS_BASE}/furniture/COFFEE_TABLE/COFFEE_TABLE.png` },
doubleBookshelf: { key: 'pixel-agents-double-bookshelf', path: `${PIXEL_AGENTS_BASE}/furniture/DOUBLE_BOOKSHELF/DOUBLE_BOOKSHELF.png` },
pcOn: { key: 'pixel-agents-pc-on', path: `${PIXEL_AGENTS_BASE}/furniture/PC/PC_FRONT_ON_1.png` },
whiteboard: { key: 'pixel-agents-whiteboard', path: `${PIXEL_AGENTS_BASE}/furniture/WHITEBOARD/WHITEBOARD.png` },
} as const;
const CHARACTER_ASSETS = [
{ key: 'pixel-agent-char-0', path: `${PIXEL_AGENTS_BASE}/characters/char_0.png` },
{ key: 'pixel-agent-char-1', path: `${PIXEL_AGENTS_BASE}/characters/char_1.png` },
{ key: 'pixel-agent-char-2', path: `${PIXEL_AGENTS_BASE}/characters/char_2.png` },
{ key: 'pixel-agent-char-3', path: `${PIXEL_AGENTS_BASE}/characters/char_3.png` },
{ key: 'pixel-agent-char-4', path: `${PIXEL_AGENTS_BASE}/characters/char_4.png` },
{ key: 'pixel-agent-char-5', path: `${PIXEL_AGENTS_BASE}/characters/char_5.png` },
] as const;
const CHARACTER_FRAME = {
width: 16,
height: 24,
columnsPerRow: 7,
frontRow: 0,
idleColumns: [0, 1, 2],
};
const ZONE_LAYOUTS: Record<OfficeZoneId, ZoneLayout> = {
reception: { x: 144, y: 28, width: 68, height: 40 },
workspace: { x: 32, y: 28, width: 86, height: 100 },
collab: { x: 152, y: 118, width: 104, height: 62 },
research: { x: 272, y: 28, width: 66, height: 66 },
alert: { x: 284, y: 92, width: 52, height: 54 },
done: { x: 30, y: 154, width: 76, height: 40 },
};
const STATUS_TONES: Record<
OfficeTaskStatus,
{ body: number; outline: number; lamp: number; badge: number; badgeText: string; text: string }
> = {
queued: { body: 0x8aa0b8, outline: 0xe8f0f8, lamp: 0xcbd5e1, badge: 0x31425b, badgeText: 'Q', text: '#e8f0f8' },
running: { body: 0x90caf9, outline: 0xf5faff, lamp: 0xfff59d, badge: 0x4a5a72, badgeText: 'R', text: '#f5faff' },
waiting: { body: 0xd8c79a, outline: 0xfff7ed, lamp: 0xfde68a, badge: 0x7c6843, badgeText: 'W', text: '#fff7ed' },
blocked: { body: 0xd96c75, outline: 0xffe4e6, lamp: 0xffab91, badge: 0x7b3340, badgeText: '!', text: '#fff1f2' },
done: { body: 0x78c27a, outline: 0xe8f5e9, lamp: 0xc5e1a5, badge: 0x44664b, badgeText: 'D', text: '#f0fdf4' },
error: { body: 0xf36d7d, outline: 0xffd1dc, lamp: 0xffab91, badge: 0x7b2634, badgeText: 'X', text: '#fff1f2' },
cancelled: { body: 0x6b7280, outline: 0xe5e7eb, lamp: 0xd1d5db, badge: 0x374151, badgeText: 'S', text: '#f3f4f6' },
};
function groupMembersByZone(members: OfficeMemberView[]) {
const grouped = new Map<OfficeZoneId, OfficeMemberView[]>();
for (const member of members) {
const bucket = grouped.get(member.zoneId);
if (bucket) {
bucket.push(member);
} else {
grouped.set(member.zoneId, [member]);
}
}
return grouped;
}
function zoneGridPoints(layout: ZoneLayout, count: number) {
if (count <= 0) return [];
const innerLeft = layout.x + 12;
const innerTop = layout.y + 14;
const innerWidth = Math.max(layout.width - 24, 10);
const innerHeight = Math.max(layout.height - 20, 10);
const columns = count <= 2 ? count : count <= 4 ? 2 : 3;
const rows = Math.ceil(count / columns);
const points: Array<{ x: number; y: number }> = [];
for (let index = 0; index < count; index += 1) {
const column = index % columns;
const row = Math.floor(index / columns);
const x = innerLeft + ((column + 0.5) * innerWidth) / columns;
const y = innerTop + ((row + 0.5) * innerHeight) / rows;
points.push({ x: Math.round(x), y: Math.round(y) });
}
return points;
}
function buildMemberPositions(office: OfficeView) {
const grouped = groupMembersByZone(office.members);
const positions = new Map<string, { x: number; y: number }>();
for (const zone of office.zones) {
const layout = ZONE_LAYOUTS[zone.id];
const members = grouped.get(zone.id) ?? [];
const points = zoneGridPoints(layout, members.length);
members.forEach((member, index) => {
positions.set(member.currentRunId, points[index] ?? { x: layout.x + 20, y: layout.y + 20 });
});
}
return positions;
}
function truncateLabel(value: string, maxLength: number) {
if (value.length <= maxLength) return value;
return `${value.slice(0, Math.max(1, maxLength - 1))}`;
}
function pickCharacterAsset(member: OfficeMemberView, index: number) {
if (member.isPrimary) return CHARACTER_ASSETS[0];
return CHARACTER_ASSETS[(index % (CHARACTER_ASSETS.length - 1)) + 1];
}
function resolveCharacterPose() {
return {
row: CHARACTER_FRAME.frontRow,
columns: CHARACTER_FRAME.idleColumns,
interval: 220,
};
}
function addFurnitureSprite(scene: any, object: any) {
const x = object.x ?? 0;
const y = object.y ?? 0;
const width = object.width ?? TILE_SIZE;
const height = object.height ?? TILE_SIZE;
const centerX = x + width / 2;
const type = object.type ?? 'anchor';
const addImage = (assetKey: string, px: number, py: number, depth = 20) =>
scene.add.image(px, py, assetKey).setOrigin(0.5, 1).setDepth(depth);
if (type === 'desk-anchor') {
const desk = addImage(FURNITURE_ASSETS.deskFront.key, centerX, y + height + 4);
const pc = addImage(FURNITURE_ASSETS.pcOn.key, centerX, y + height + 2, 21);
return [desk, pc];
}
if (type === 'chair-anchor') return [addImage(FURNITURE_ASSETS.chairFront.key, centerX, y + height + 1)];
if (type === 'sofa-anchor') return [addImage(FURNITURE_ASSETS.sofaFront.key, centerX, y + height)];
if (type === 'coffee-anchor') return [addImage(FURNITURE_ASSETS.coffeeTable.key, centerX, y + height)];
if (type === 'meeting-anchor') return [addImage(FURNITURE_ASSETS.tableFront.key, centerX, y + height + 16)];
if (type === 'server-anchor') return [addImage(FURNITURE_ASSETS.doubleBookshelf.key, centerX, y + height)];
if (type === 'archive-anchor') return [addImage(FURNITURE_ASSETS.doubleBookshelf.key, centerX, y + height)];
if (type === 'whiteboard-anchor') return [addImage(FURNITURE_ASSETS.whiteboard.key, centerX, y + height)];
return [];
}
export function OfficePhaserCanvas({
office,
selectedRunId,
onRunSelect,
className,
showMetaBar = true,
}: {
office: OfficeView;
selectedRunId: string | null;
onRunSelect: (runId: string) => void;
className?: string;
showMetaBar?: boolean;
}) {
const containerRef = React.useRef<HTMLDivElement | null>(null);
const selectRef = React.useRef(onRunSelect);
React.useEffect(() => {
selectRef.current = onRunSelect;
}, [onRunSelect]);
React.useEffect(() => {
let destroyed = false;
let game: any = null;
async function mountScene() {
if (!containerRef.current) return;
const PhaserImport = await import('phaser');
const Phaser = (PhaserImport.default ?? PhaserImport) as any;
if (destroyed || !containerRef.current) return;
const memberPositions = buildMemberPositions(office);
class OfficeScene extends Phaser.Scene {
preload(this: any) {
if (!this.textures.exists(TILESET_KEY)) {
this.load.image(TILESET_KEY, TILESET_PATH);
}
if (!this.cache.tilemap.exists(MAP_KEY)) {
this.load.tilemapTiledJSON(MAP_KEY, MAP_PATH);
}
Object.values(FURNITURE_ASSETS).forEach((asset) => {
if (!this.textures.exists(asset.key)) {
this.load.image(asset.key, asset.path);
}
});
CHARACTER_ASSETS.forEach((asset) => {
if (!this.textures.exists(asset.key)) {
this.load.spritesheet(asset.key, asset.path, {
frameWidth: CHARACTER_FRAME.width,
frameHeight: CHARACTER_FRAME.height,
});
}
});
}
create(this: any) {
this.cameras.main.setBackgroundColor('#1a2433');
this.cameras.main.roundPixels = true;
this.cameras.main.setZoom(RENDER_SCALE);
this.cameras.main.setBounds(0, 0, WORLD_WIDTH, WORLD_HEIGHT);
const map = this.make.tilemap({ key: MAP_KEY });
const tileset = map.addTilesetImage('office-winter-tileset', TILESET_KEY, TILE_SIZE, TILE_SIZE, 0, 0);
if (!tileset) {
throw new Error('Failed to load office-winter-tileset into tilemap');
}
['bg-floor', 'bg-rug', 'walls', 'windows', 'markers'].forEach((layerName, index) => {
const layer = map.createLayer(layerName, tileset, 0, 0);
layer?.setDepth(index);
});
const frame = this.add.rectangle(0, 0, WORLD_WIDTH, WORLD_HEIGHT, 0x000000, 0).setOrigin(0, 0);
frame.setStrokeStyle(4, 0x101827, 1);
frame.setDepth(10);
const objectLayer = map.getObjectLayer('furniture-anchors');
objectLayer?.objects.forEach((object: any) => {
const placed = addFurnitureSprite(this, object);
if (placed.length > 0) return;
const x = object.x ?? 0;
const y = object.y ?? 0;
const width = object.width ?? TILE_SIZE;
const height = object.height ?? TILE_SIZE;
const fallback = this.add.rectangle(x, y, width, height, 0x384b69, 0.18).setOrigin(0, 0);
fallback.setStrokeStyle(2, 0x90caf9, 0.9);
fallback.setDepth(20);
});
const assignmentLines = this.add.graphics();
assignmentLines.setDepth(50);
office.assignments.forEach((assignment) => {
const from = memberPositions.get(assignment.ownerRunId);
if (!from) return;
assignment.assigneeRunIds.forEach((assigneeRunId) => {
const to = memberPositions.get(assigneeRunId);
if (!to) return;
assignmentLines.lineStyle(1, 0xffd166, 0.75);
assignmentLines.beginPath();
assignmentLines.moveTo(from.x, from.y);
assignmentLines.lineTo(to.x, to.y);
assignmentLines.strokePath();
assignmentLines.fillStyle(0xffd166, 1);
assignmentLines.fillRect(to.x - 1, to.y - 1, 2, 2);
});
});
office.members.forEach((member, memberIndex) => {
const point = memberPositions.get(member.currentRunId);
if (!point) return;
const tone = STATUS_TONES[member.status];
const isSelected = selectedRunId === member.currentRunId;
const isPrimary = member.isPrimary;
const container = this.add.container(point.x, point.y);
container.setDepth(60);
const clickTarget = this.add.rectangle(0, 0, isPrimary ? 34 : 30, isPrimary ? 36 : 32, 0x000000, 0.001);
clickTarget.setInteractive({ useHandCursor: true });
clickTarget.setOrigin(0.5);
const shadow = this.add.rectangle(0, 9, isPrimary ? 15 : 13, 4, 0x0f172a, 0.7);
shadow.setOrigin(0.5);
const characterAsset = pickCharacterAsset(member, memberIndex);
const pose = resolveCharacterPose();
let frameIndex = 0;
const character = this.add
.sprite(0, 4, characterAsset.key, 0)
.setDisplaySize(isPrimary ? 24 : 21, isPrimary ? 36 : 32)
.setOrigin(0.5, 1);
const applyCharacterFrame = () => {
const column = pose.columns[frameIndex % pose.columns.length] ?? pose.columns[0] ?? 0;
const frame = pose.row * CHARACTER_FRAME.columnsPerRow + column;
character.setFrame(frame);
frameIndex += 1;
};
applyCharacterFrame();
this.time.addEvent({
delay: pose.interval,
loop: true,
callback: applyCharacterFrame,
});
const highlight = this.add.rectangle(0, -9, isPrimary ? 14 : 12, 19, tone.body, 0.12);
highlight.setStrokeStyle(isSelected ? 2 : 1, isSelected ? 0xfef3c7 : tone.outline, isSelected ? 1 : 0.7);
highlight.setOrigin(0.5);
const lamp = this.add.rectangle(isPrimary ? 8 : 7, -9, 3, 3, tone.lamp, 1);
lamp.setStrokeStyle(1, 0x101827, 1);
lamp.setOrigin(0.5);
const badge = this.add.rectangle(0, -14, isPrimary ? 12 : 10, 5, isPrimary ? 0xffd166 : tone.badge, 1);
badge.setStrokeStyle(1, 0x101827, 1);
badge.setOrigin(0.5);
const badgeText = this.add
.text(0, -16.5, isPrimary ? 'M' : tone.badgeText, {
color: isPrimary ? '#1a2433' : tone.text,
fontFamily: '"Courier New", monospace',
fontSize: '5px',
fontStyle: 'bold',
})
.setOrigin(0.5, 0);
const name = this.add
.text(0, 14, truncateLabel(member.actorName.toUpperCase(), isPrimary ? 10 : 8), {
color: '#f5faff',
fontFamily: '"Courier New", monospace',
fontSize: isPrimary ? '5px' : '4px',
fontStyle: 'bold',
align: 'center',
})
.setOrigin(0.5, 0);
const taskLabel = this.add
.text(0, 20, truncateLabel((member.stageLabel ?? member.currentTitle).toUpperCase(), 12), {
color: '#cbd5e1',
fontFamily: '"Courier New", monospace',
fontSize: '4px',
align: 'center',
})
.setOrigin(0.5, 0);
container.add([clickTarget, shadow, highlight, badge, badgeText, character, lamp, name, taskLabel]);
clickTarget.on('pointerdown', () => {
selectRef.current(member.currentRunId);
});
clickTarget.on('pointerover', () => {
this.tweens.add({ targets: container, scaleX: 1.08, scaleY: 1.08, duration: 90 });
});
clickTarget.on('pointerout', () => {
this.tweens.add({ targets: container, scaleX: 1, scaleY: 1, duration: 90 });
});
if (member.status === 'running') {
this.tweens.add({
targets: container,
y: point.y - 1.5,
duration: 500,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut',
});
this.tweens.add({
targets: lamp,
alpha: 0.2,
duration: 180,
yoyo: true,
repeat: -1,
});
}
if (member.status === 'blocked' || member.status === 'error') {
const warn = this.add
.text(isPrimary ? 8 : 7, -3, '!', {
color: '#fff7ed',
fontFamily: '"Courier New", monospace',
fontSize: '8px',
fontStyle: 'bold',
})
.setOrigin(0.5);
container.add(warn);
this.tweens.add({
targets: warn,
alpha: 0.25,
duration: 180,
yoyo: true,
repeat: -1,
});
}
if (member.status === 'done') {
const doneMark = this.add.rectangle(isPrimary ? 7 : 6, 7, 3, 3, 0x78c27a, 1);
doneMark.setStrokeStyle(1, 0xf0fdf4, 1);
doneMark.setOrigin(0.5);
container.add(doneMark);
}
});
}
}
game = new Phaser.Game({
type: Phaser.CANVAS,
width: SCENE_WIDTH,
height: SCENE_HEIGHT,
parent: containerRef.current,
pixelArt: true,
antialias: false,
roundPixels: true,
backgroundColor: '#1a2433',
scene: OfficeScene,
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
});
}
mountScene().catch((error) => {
console.error('Failed to mount Office Phaser canvas', error);
});
return () => {
destroyed = true;
game?.destroy(true);
};
}, [office, selectedRunId]);
return (
<div className={cn('space-y-3', className)}>
{showMetaBar ? (
<div className="flex flex-wrap items-center gap-2 text-[#cbd5e1]">
<span className="rounded-none border-2 border-[#5a7092] bg-[#1a2433] px-3 py-1 text-[11px] font-semibold tracking-[0.2em] text-[#f5faff]">
WINTER OFFICE MAP
</span>
<span className="rounded-none border-2 border-[#30364d] bg-[#171b29] px-3 py-1 text-[11px]">
400 x 225 LOGIC / 800 x 450 RENDER
</span>
<span className="rounded-none border-2 border-[#30364d] bg-[#171b29] px-3 py-1 text-[11px]">
{office.members.length} AGENTS
</span>
<span className="rounded-none border-2 border-[#30364d] bg-[#171b29] px-3 py-1 text-[11px]">
{office.assignments.length} LINKS
</span>
</div>
) : null}
<div className="overflow-hidden rounded-none border-4 border-[#0e1119] bg-[#171522] p-3 shadow-[0_0_0_2px_#2a223b_inset]">
<div
className="mx-auto w-full max-w-[1200px] overflow-hidden border-4 border-[#5a7092] bg-[#1a2433]"
style={{ aspectRatio: `${WORLD_WIDTH} / ${WORLD_HEIGHT}` }}
>
<div
ref={containerRef}
className="h-full w-full [&_canvas]:!block [&_canvas]:!h-full [&_canvas]:!w-full [&_canvas]:image-rendering-[pixelated]"
/>
</div>
</div>
</div>
);
}

View File

@ -1,58 +0,0 @@
'use client';
import * as React from 'react';
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import { ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn('border-b', className)}
{...props}
/>
));
AccordionItem.displayName = 'AccordionItem';
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn('pb-4 pt-0', className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@ -1,141 +0,0 @@
'use client';
import * as React from 'react';
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
import { cn } from '@/lib/utils';
import { buttonVariants } from '@/components/ui/button';
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
className
)}
{...props}
/>
);
AlertDialogHeader.displayName = 'AlertDialogHeader';
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className
)}
{...props}
/>
);
AlertDialogFooter.displayName = 'AlertDialogFooter';
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold', className)}
{...props}
/>
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: 'outline' }),
'mt-2 sm:mt-0',
className
)}
{...props}
/>
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};

View File

@ -1,59 +0,0 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const alertVariants = cva(
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
{
variants: {
variant: {
default: 'bg-background text-foreground',
destructive:
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
},
},
defaultVariants: {
variant: 'default',
},
}
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
));
Alert.displayName = 'Alert';
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
{...props}
/>
));
AlertTitle.displayName = 'AlertTitle';
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('text-sm [&_p]:leading-relaxed', className)}
{...props}
/>
));
AlertDescription.displayName = 'AlertDescription';
export { Alert, AlertTitle, AlertDescription };

View File

@ -1,7 +0,0 @@
'use client';
import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';
const AspectRatio = AspectRatioPrimitive.Root;
export { AspectRatio };

View File

@ -1,115 +0,0 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { ChevronRight, MoreHorizontal } from 'lucide-react';
import { cn } from '@/lib/utils';
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<'nav'> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = 'Breadcrumb';
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<'ol'>
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
'flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5',
className
)}
{...props}
/>
));
BreadcrumbList.displayName = 'BreadcrumbList';
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<'li'>
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn('inline-flex items-center gap-1.5', className)}
{...props}
/>
));
BreadcrumbItem.displayName = 'BreadcrumbItem';
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<'a'> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : 'a';
return (
<Comp
ref={ref}
className={cn('transition-colors hover:text-foreground', className)}
{...props}
/>
);
});
BreadcrumbLink.displayName = 'BreadcrumbLink';
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<'span'>
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn('font-normal text-foreground', className)}
{...props}
/>
));
BreadcrumbPage.displayName = 'BreadcrumbPage';
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<'li'>) => (
<li
role="presentation"
aria-hidden="true"
className={cn('[&>svg]:size-3.5', className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
BreadcrumbSeparator.displayName = 'BreadcrumbSeparator';
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<'span'>) => (
<span
role="presentation"
aria-hidden="true"
className={cn('flex h-9 w-9 items-center justify-center', className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis';
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};

View File

@ -1,66 +0,0 @@
'use client';
import * as React from 'react';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { DayPicker } from 'react-day-picker';
import { cn } from '@/lib/utils';
import { buttonVariants } from '@/components/ui/button';
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn('p-3', className)}
classNames={{
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
month: 'space-y-4',
caption: 'flex justify-center pt-1 relative items-center',
caption_label: 'text-sm font-medium',
nav: 'space-x-1 flex items-center',
nav_button: cn(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100'
),
nav_button_previous: 'absolute left-1',
nav_button_next: 'absolute right-1',
table: 'w-full border-collapse space-y-1',
head_row: 'flex',
head_cell:
'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]',
row: 'flex w-full mt-2',
cell: 'h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
day: cn(
buttonVariants({ variant: 'ghost' }),
'h-9 w-9 p-0 font-normal aria-selected:opacity-100'
),
day_range_end: 'day-range-end',
day_selected:
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
day_today: 'bg-accent text-accent-foreground',
day_outside:
'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
day_disabled: 'text-muted-foreground opacity-50',
day_range_middle:
'aria-selected:bg-accent aria-selected:text-accent-foreground',
day_hidden: 'invisible',
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
);
}
Calendar.displayName = 'Calendar';
export { Calendar };

View File

@ -1,262 +0,0 @@
'use client';
import * as React from 'react';
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from 'embla-carousel-react';
import { ArrowLeft, ArrowRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: 'horizontal' | 'vertical';
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error('useCarousel must be used within a <Carousel />');
}
return context;
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{
orientation = 'horizontal',
opts,
setApi,
plugins,
className,
children,
...props
},
ref
) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === 'horizontal' ? 'x' : 'y',
},
plugins
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return;
}
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'ArrowLeft') {
event.preventDefault();
scrollPrev();
} else if (event.key === 'ArrowRight') {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext]
);
React.useEffect(() => {
if (!api || !setApi) {
return;
}
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) {
return;
}
onSelect(api);
api.on('reInit', onSelect);
api.on('select', onSelect);
return () => {
api?.off('select', onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn('relative', className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
}
);
Carousel.displayName = 'Carousel';
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel();
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
'flex',
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
className
)}
{...props}
/>
</div>
);
});
CarouselContent.displayName = 'CarouselContent';
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel();
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
'min-w-0 shrink-0 grow-0 basis-full',
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
className
)}
{...props}
/>
);
});
CarouselItem.displayName = 'CarouselItem';
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
'absolute h-8 w-8 rounded-full',
orientation === 'horizontal'
? '-left-12 top-1/2 -translate-y-1/2'
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
);
});
CarouselPrevious.displayName = 'CarouselPrevious';
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
'absolute h-8 w-8 rounded-full',
orientation === 'horizontal'
? '-right-12 top-1/2 -translate-y-1/2'
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
);
});
CarouselNext.displayName = 'CarouselNext';
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
};

View File

@ -1,365 +0,0 @@
'use client';
import * as React from 'react';
import * as RechartsPrimitive from 'recharts';
import { cn } from '@/lib/utils';
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: '', dark: '.dark' } as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
);
};
type ChartContextProps = {
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error('useChart must be used within a <ChartContainer />');
}
return context;
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & {
config: ChartConfig;
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>['children'];
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
});
ChartContainer.displayName = 'Chart';
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color
);
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join('\n')}
}
`
)
.join('\n'),
}}
/>
);
};
const ChartTooltip = RechartsPrimitive.Tooltip;
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<'div'> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: 'line' | 'dot' | 'dashed';
nameKey?: string;
labelKey?: string;
}
>(
(
{
active,
payload,
className,
indicator = 'dot',
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload;
const key = `${labelKey || item.dataKey || item.name || 'value'}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === 'string'
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) {
return (
<div className={cn('font-medium', labelClassName)}>
{labelFormatter(value, payload)}
</div>
);
}
if (!value) {
return null;
}
return <div className={cn('font-medium', labelClassName)}>{value}</div>;
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
]);
if (!active || !payload?.length) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== 'dot';
return (
<div
ref={ref}
className={cn(
'grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl',
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || 'value'}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
'flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground',
indicator === 'dot' && 'items-center'
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
'shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]',
{
'h-2.5 w-2.5': indicator === 'dot',
'w-1': indicator === 'line',
'w-0 border-[1.5px] border-dashed bg-transparent':
indicator === 'dashed',
'my-0.5': nestLabel && indicator === 'dashed',
}
)}
style={
{
'--color-bg': indicatorColor,
'--color-border': indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
'flex flex-1 justify-between leading-none',
nestLabel ? 'items-end' : 'items-center'
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
}
);
ChartTooltipContent.displayName = 'ChartTooltip';
const ChartLegend = RechartsPrimitive.Legend;
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> &
Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {
hideIcon?: boolean;
nameKey?: string;
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey },
ref
) => {
const { config } = useChart();
if (!payload?.length) {
return null;
}
return (
<div
ref={ref}
className={cn(
'flex items-center justify-center gap-4',
verticalAlign === 'top' ? 'pb-3' : 'pt-3',
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || 'value'}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn(
'flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground'
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
}
);
ChartLegendContent.displayName = 'ChartLegend';
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== 'object' || payload === null) {
return undefined;
}
const payloadPayload =
'payload' in payload &&
typeof payload.payload === 'object' &&
payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (
key in payload &&
typeof payload[key as keyof typeof payload] === 'string'
) {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string;
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config];
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
};

View File

@ -1,30 +0,0 @@
'use client';
import * as React from 'react';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { Check } from 'lucide-react';
import { cn } from '@/lib/utils';
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn('flex items-center justify-center text-current')}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };

View File

@ -1,155 +0,0 @@
'use client';
import * as React from 'react';
import { type DialogProps } from '@radix-ui/react-dialog';
import { Command as CommandPrimitive } from 'cmdk';
import { Search } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Dialog, DialogContent } from '@/components/ui/dialog';
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
className
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
'flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
className
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn('-mx-1 h-px bg-border', className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
className
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
'ml-auto text-xs tracking-widest text-muted-foreground',
className
)}
{...props}
/>
);
};
CommandShortcut.displayName = 'CommandShortcut';
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};

View File

@ -1,200 +0,0 @@
'use client';
import * as React from 'react';
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
import { Check, ChevronRight, Circle } from 'lucide-react';
import { cn } from '@/lib/utils';
const ContextMenu = ContextMenuPrimitive.Root;
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
const ContextMenuGroup = ContextMenuPrimitive.Group;
const ContextMenuPortal = ContextMenuPrimitive.Portal;
const ContextMenuSub = ContextMenuPrimitive.Sub;
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
inset && 'pl-8',
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
));
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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',
className
)}
{...props}
/>
));
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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',
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
));
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
className
)}
{...props}
/>
));
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
));
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName;
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
));
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold text-foreground',
inset && 'pl-8',
className
)}
{...props}
/>
));
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-border', className)}
{...props}
/>
));
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
'ml-auto text-xs tracking-widest text-muted-foreground',
className
)}
{...props}
/>
);
};
ContextMenuShortcut.displayName = 'ContextMenuShortcut';
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
};

View File

@ -1,118 +0,0 @@
'use client';
import * as React from 'react';
import { Drawer as DrawerPrimitive } from 'vaul';
import { cn } from '@/lib/utils';
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
);
Drawer.displayName = 'Drawer';
const DrawerTrigger = DrawerPrimitive.Trigger;
const DrawerPortal = DrawerPrimitive.Portal;
const DrawerClose = DrawerPrimitive.Close;
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn('fixed inset-0 z-50 bg-black/80', className)}
{...props}
/>
));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background',
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
));
DrawerContent.displayName = 'DrawerContent';
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('grid gap-1.5 p-4 text-center sm:text-left', className)}
{...props}
/>
);
DrawerHeader.displayName = 'DrawerHeader';
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
{...props}
/>
);
DrawerFooter.displayName = 'DrawerFooter';
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
'text-lg font-semibold leading-none tracking-tight',
className
)}
{...props}
/>
));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};

View File

@ -1,200 +0,0 @@
'use client';
import * as React from 'react';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { Check, ChevronRight, Circle } from 'lucide-react';
import { cn } from '@/lib/utils';
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
inset && 'pl-8',
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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',
className
)}
{...props}
/>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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',
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
className
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
className
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};

View File

@ -1,179 +0,0 @@
'use client';
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from 'react-hook-form';
import { cn } from '@/lib/utils';
import { Label } from '@/components/ui/label';
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error('useFormField should be used within <FormField>');
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn('space-y-2', className)} {...props} />
</FormItemContext.Provider>
);
});
FormItem.displayName = 'FormItem';
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return (
<Label
ref={ref}
className={cn(error && 'text-destructive', className)}
htmlFor={formItemId}
{...props}
/>
);
});
FormLabel.displayName = 'FormLabel';
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
});
FormControl.displayName = 'FormControl';
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
);
});
FormDescription.displayName = 'FormDescription';
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p
ref={ref}
id={formMessageId}
className={cn('text-sm font-medium text-destructive', className)}
{...props}
>
{body}
</p>
);
});
FormMessage.displayName = 'FormMessage';
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};

View File

@ -1,29 +0,0 @@
'use client';
import * as React from 'react';
import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
import { cn } from '@/lib/utils';
const HoverCard = HoverCardPrimitive.Root;
const HoverCardTrigger = HoverCardPrimitive.Trigger;
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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',
className
)}
{...props}
/>
));
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
export { HoverCard, HoverCardTrigger, HoverCardContent };

View File

@ -1,71 +0,0 @@
'use client';
import * as React from 'react';
import { OTPInput, OTPInputContext } from 'input-otp';
import { Dot } from 'lucide-react';
import { cn } from '@/lib/utils';
const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>,
React.ComponentPropsWithoutRef<typeof OTPInput>
>(({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn(
'flex items-center gap-2 has-[:disabled]:opacity-50',
containerClassName
)}
className={cn('disabled:cursor-not-allowed', className)}
{...props}
/>
));
InputOTP.displayName = 'InputOTP';
const InputOTPGroup = React.forwardRef<
React.ElementRef<'div'>,
React.ComponentPropsWithoutRef<'div'>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex items-center', className)} {...props} />
));
InputOTPGroup.displayName = 'InputOTPGroup';
const InputOTPSlot = React.forwardRef<
React.ElementRef<'div'>,
React.ComponentPropsWithoutRef<'div'> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
return (
<div
ref={ref}
className={cn(
'relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md',
isActive && 'z-10 ring-2 ring-ring ring-offset-background',
className
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
</div>
)}
</div>
);
});
InputOTPSlot.displayName = 'InputOTPSlot';
const InputOTPSeparator = React.forwardRef<
React.ElementRef<'div'>,
React.ComponentPropsWithoutRef<'div'>
>(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<Dot />
</div>
));
InputOTPSeparator.displayName = 'InputOTPSeparator';
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

View File

@ -1,236 +0,0 @@
'use client';
import * as React from 'react';
import * as MenubarPrimitive from '@radix-ui/react-menubar';
import { Check, ChevronRight, Circle } from 'lucide-react';
import { cn } from '@/lib/utils';
const MenubarMenu = MenubarPrimitive.Menu;
const MenubarGroup = MenubarPrimitive.Group;
const MenubarPortal = MenubarPrimitive.Portal;
const MenubarSub = MenubarPrimitive.Sub;
const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn(
'flex h-10 items-center space-x-1 rounded-md border bg-background p-1',
className
)}
{...props}
/>
));
Menubar.displayName = MenubarPrimitive.Root.displayName;
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
className
)}
{...props}
/>
));
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
inset && 'pl-8',
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
));
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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',
className
)}
{...props}
/>
));
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(
(
{ className, align = 'start', alignOffset = -4, sideOffset = 8, ...props },
ref
) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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',
className
)}
{...props}
/>
</MenubarPrimitive.Portal>
)
);
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
className
)}
{...props}
/>
));
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
));
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
));
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
className
)}
{...props}
/>
));
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
));
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
const MenubarShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
'ml-auto text-xs tracking-widest text-muted-foreground',
className
)}
{...props}
/>
);
};
MenubarShortcut.displayname = 'MenubarShortcut';
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
};

View File

@ -1,128 +0,0 @@
import * as React from 'react';
import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';
import { cva } from 'class-variance-authority';
import { ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
'relative z-10 flex max-w-max flex-1 items-center justify-center',
className
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
'group flex flex-1 list-none items-center justify-center space-x-1',
className
)}
{...props}
/>
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
'group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50'
);
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), 'group', className)}
{...props}
>
{children}{' '}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
'left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ',
className
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn('absolute left-0 top-full flex justify-center')}>
<NavigationMenuPrimitive.Viewport
className={cn(
'origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]',
className
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
'top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in',
className
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName;
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
};

View File

@ -1,117 +0,0 @@
import * as React from 'react';
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
import { cn } from '@/lib/utils';
import { ButtonProps, buttonVariants } from '@/components/ui/button';
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
<nav
role="navigation"
aria-label="pagination"
className={cn('mx-auto flex w-full justify-center', className)}
{...props}
/>
);
Pagination.displayName = 'Pagination';
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<'ul'>
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn('flex flex-row items-center gap-1', className)}
{...props}
/>
));
PaginationContent.displayName = 'PaginationContent';
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<'li'>
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn('', className)} {...props} />
));
PaginationItem.displayName = 'PaginationItem';
type PaginationLinkProps = {
isActive?: boolean;
} & Pick<ButtonProps, 'size'> &
React.ComponentProps<'a'>;
const PaginationLink = ({
className,
isActive,
size = 'icon',
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? 'page' : undefined}
className={cn(
buttonVariants({
variant: isActive ? 'outline' : 'ghost',
size,
}),
className
)}
{...props}
/>
);
PaginationLink.displayName = 'PaginationLink';
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn('gap-1 pl-2.5', className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
);
PaginationPrevious.displayName = 'PaginationPrevious';
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn('gap-1 pr-2.5', className)}
{...props}
>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
);
PaginationNext.displayName = 'PaginationNext';
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<'span'>) => (
<span
aria-hidden
className={cn('flex h-9 w-9 items-center justify-center', className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
);
PaginationEllipsis.displayName = 'PaginationEllipsis';
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
};

View File

@ -1,28 +0,0 @@
'use client';
import * as React from 'react';
import * as ProgressPrimitive from '@radix-ui/react-progress';
import { cn } from '@/lib/utils';
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
'relative h-4 w-full overflow-hidden rounded-full bg-secondary',
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress };

View File

@ -1,44 +0,0 @@
'use client';
import * as React from 'react';
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
import { Circle } from 'lucide-react';
import { cn } from '@/lib/utils';
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn('grid gap-2', className)}
{...props}
ref={ref}
/>
);
});
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
'aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
});
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
export { RadioGroup, RadioGroupItem };

View File

@ -1,45 +0,0 @@
'use client';
import { GripVertical } from 'lucide-react';
import * as ResizablePrimitive from 'react-resizable-panels';
import { cn } from '@/lib/utils';
const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn(
'flex h-full w-full data-[panel-group-direction=vertical]:flex-col',
className
)}
{...props}
/>
);
const ResizablePanel = ResizablePrimitive.Panel;
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean;
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
'relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90',
className
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
);
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };

View File

@ -1,140 +0,0 @@
'use client';
import * as React from 'react';
import * as SheetPrimitive from '@radix-ui/react-dialog';
import { cva, type VariantProps } from 'class-variance-authority';
import { X } from 'lucide-react';
import { cn } from '@/lib/utils';
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
{
variants: {
side: {
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
bottom:
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
right:
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
},
},
defaultVariants: {
side: 'right',
},
}
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = 'right', className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
className
)}
{...props}
/>
);
SheetHeader.displayName = 'SheetHeader';
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className
)}
{...props}
/>
);
SheetFooter.displayName = 'SheetFooter';
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold text-foreground', className)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};

View File

@ -1,28 +0,0 @@
'use client';
import * as React from 'react';
import * as SliderPrimitive from '@radix-ui/react-slider';
import { cn } from '@/lib/utils';
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
'relative flex w-full touch-none select-none items-center',
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };

View File

@ -1,31 +0,0 @@
'use client';
import { useTheme } from 'next-themes';
import { Toaster as Sonner } from 'sonner';
type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = 'system' } = useTheme();
return (
<Sonner
theme={theme as ToasterProps['theme']}
className="toaster group"
toastOptions={{
classNames: {
toast:
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
actionButton:
'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton:
'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
},
}}
{...props}
/>
);
};
export { Toaster };

View File

@ -1,129 +0,0 @@
"use client"
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

View File

@ -1,35 +0,0 @@
"use client"
import { useToast } from "@/hooks/use-toast"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

View File

@ -1,61 +0,0 @@
'use client';
import * as React from 'react';
import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
import { type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
import { toggleVariants } from '@/components/ui/toggle';
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: 'default',
variant: 'default',
});
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn('flex items-center justify-center gap-1', className)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
));
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
});
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
export { ToggleGroup, ToggleGroupItem };

View File

@ -1,45 +0,0 @@
'use client';
import * as React from 'react';
import * as TogglePrimitive from '@radix-ui/react-toggle';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const toggleVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',
{
variants: {
variant: {
default: 'bg-transparent',
outline:
'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground',
},
size: {
default: 'h-10 px-3',
sm: 'h-9 px-2.5',
lg: 'h-11 px-5',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
));
Toggle.displayName = TogglePrimitive.Root.displayName;
export { Toggle, toggleVariants };

View File

@ -1,30 +0,0 @@
'use client';
import * as React from 'react';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { cn } from '@/lib/utils';
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 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',
className
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@ -1,194 +0,0 @@
"use client"
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +0,0 @@
# Office Pixel Assets
This folder holds pixel-art resources for the office runtime scene.
Structure:
- `tiles/`: reusable 16x16 room tiles
- `sprites/furniture/`: furniture sprites
- `sprites/agents/`: agent sprite sheets
- `sprites/status/`: small state icons and markers
- `atlas/`: packed atlases and metadata
- `maps/`: Tiled maps and layout sketches
Working rules:
- Logical scene resolution: `400x225`
- Base tile size: `16x16`
- Integer scaling only
- No anti-aliasing
- Prefer a small, coherent set of assets over many low-quality variants

View File

@ -1,9 +0,0 @@
# Atlas
Packed texture atlases and metadata for Phaser.
Possible outputs:
- `office-furniture-atlas.png`
- `office-furniture-atlas.json`
- `office-agents-atlas.png`
- `office-agents-atlas.json`

View File

@ -1,18 +0,0 @@
# Maps
This directory stores map sources and layout drafts.
Expected files later:
- `office-winter-v1.tmj`
- `office-winter-v1.json`
- `office-winter-v1-sketch.md`
Current placeholder map:
- `office-winter-v1.tmj`
- Uses a placeholder reference to `../tiles/office-winter-tileset.png`
- Furniture and decor are currently expressed as `object layers` so they can be replaced by real sprites later
Grid:
- logical scene: `400x225`
- tile size: `16x16`
- working map size: `25x14`

View File

@ -1,14 +0,0 @@
WWWWWWWWWWWWWWWWWWWWWWWWW
W.......WWWWW...........W
W..DCD.............VV...W
W..DCD.............VV...W
W..................VV...W
W..DCD.............AA...W
W..DCD.............AA...W
W.......................W
W.............MMMMM.....W
W..DCD........MMMMM.....W
W..DCD........RRRRR.....W
W.............RRRRR.....W
W.......................W
WWWWWWWWWWWWWWWWWWWWWWWWW

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,54 +0,0 @@
# Office Winter V1 Sketch
Grid:
- Width: `25`
- Height: `14`
- Tile size: `16x16`
Legend:
- `W`: wall/window band
- `F`: floor
- `R`: rug / lounge
- `D`: workstation desk
- `C`: chair
- `M`: meeting table
- `S`: sofa
- `T`: coffee table
- `V`: server rack / monitor
- `A`: archive crate
- `P`: plant / lamp accent
- `.`: walkable empty floor
Layout:
```text
01 WWWWWWWWWWWWWWWWWWWWWWWWW
02 W.......WWWWW...........W
03 W..DCD.............VV...W
04 W..DCD.............VV...W
05 W..................VV...W
06 W..DCD.............AA...W
07 W..DCD.............AA...W
08 W.......................W
09 W.............MMMMM.....W
10 W..DCD........MMMMM.....W
11 W..DCD........RRRRR.....W
12 W.............RRRRR.....W
13 W.......................W
14 WWWWWWWWWWWWWWWWWWWWWWWWW
```
Zone reading:
- Left block: primary workstation area with four seats
- Center-top: lounge corner with sofa and coffee table
- Center-mid: collaboration table
- Center-late: open rug for agent gathering and delegation moments
- Right block: server / monitoring wall
- Bottom-left: archive zone
- Corners: plants or lamps for warmth and silhouette
Recommended next conversion into Tiled:
1. Build wall and floor layers first
2. Drop furniture as object or top layers
3. Leave open walk lanes around the lounge and meeting table
4. Reserve the center rug as the most readable area for live agent activity

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +0,0 @@
# Agent Sprites
Directory for office character sprite sheets.
Naming:
- `agent-main.png`
- `agent-worker.png`
- `agent-visitor.png`
Base frame:
- `16x24`
Minimum animation set:
- `idle`
- `walk`
- `type`
- `blocked`
- `done`
Minimum facing set:
- front
- side
- back

View File

@ -1,25 +0,0 @@
# Furniture Sprites
Directory for standalone furniture sprites.
Naming:
- `desk-workstation.png`
- `chair-office.png`
- `table-meeting.png`
- `sofa-2seat.png`
- `table-coffee.png`
- `rack-server.png`
- `crate-archive.png`
- `lamp-floor.png`
- `plant-office.png`
Suggested sizes:
- Desk: `32x16`
- Chair: `16x16`
- Meeting table: `32x24`
- Sofa: `32x16`
- Coffee table: `16x16`
- Server rack: `16x32`
- Archive crate: `16x16`
- Floor lamp: `16x32`
- Plant: `16x24`

View File

@ -1,13 +0,0 @@
# Status Sprites
Small markers used to express runtime state.
Naming:
- `icon-alert.png`
- `icon-task.png`
- `icon-done.png`
- `icon-wait.png`
- `light-warning.png`
Suggested size:
- `8x8` or `12x12`

View File

@ -1,21 +0,0 @@
# Tiles
Purpose:
- Build the office shell with reusable `16x16` tiles.
First batch:
- `floor-dark`
- `floor-light`
- `wall-main`
- `wall-shadow`
- `window-night`
- `rug-center`
- `rug-edge`
- `trim-border`
Target output:
- `office-winter-tileset.png`
Current generated placeholders:
- `office-winter-tileset.png`
- `office-winter-tileset-preview.png`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 825 B

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 Pablo De Lucca
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,17 +0,0 @@
# Pixel Agents Vendor Assets
Vendored from:
- `https://github.com/pablodelucca/pixel-agents`
Included here for internal, non-commercial use in the office runtime prototype.
Copied content:
- `assets/furniture/`
- `assets/floors/`
- `assets/walls/`
- `assets/characters/`
- upstream `LICENSE`
Current usage:
- Furniture sprites are already mapped into the Phaser office canvas.
- Character sprites are copied locally but not wired into runtime rendering yet.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -1,92 +0,0 @@
{
"version": 1,
"cols": 21,
"rows": 22,
"layoutRevision": 1,
"tiles": [
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255,
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255,
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255,
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255,
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255,
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255,
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255,
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255,
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255,
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 9, 9, 9, 9, 9, 9, 9, 9, 0, 255,
0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 9, 9, 9, 9, 9, 9, 9, 9, 0, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
],
"tileColors": [
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
{"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, null,
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null,
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null,
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null,
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null,
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null,
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null,
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null,
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null,
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":214,"s":30,"b":-100,"c":-55}, null,
{"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":214,"s":30,"b":-100,"c":-55}, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null
],
"furniture": [
{"uid": "f-1773353910654-5cdg", "type": "TABLE_FRONT", "col": 4, "row": 16},
{"uid": "f-1773354646615-jhxl", "type": "COFFEE_TABLE", "col": 14, "row": 14},
{"uid": "f-1773354664329-hxsh", "type": "SOFA_SIDE", "col": 13, "row": 14},
{"uid": "f-1773354665989-zgrw", "type": "SOFA_BACK", "col": 14, "row": 16},
{"uid": "f-1773354668333-lo7w", "type": "SOFA_FRONT", "col": 14, "row": 13},
{"uid": "f-1773354670818-r1q2", "type": "SOFA_SIDE:left", "col": 16, "row": 14},
{"uid": "f-1773354686967-yiua", "type": "HANGING_PLANT", "col": 9, "row": 9},
{"uid": "f-1773354687677-hn2k", "type": "HANGING_PLANT", "col": 1, "row": 9},
{"uid": "f-1773354693077-f7aj", "type": "DOUBLE_BOOKSHELF", "col": 7, "row": 9},
{"uid": "f-1773354700513-f1zs", "type": "DOUBLE_BOOKSHELF", "col": 2, "row": 9},
{"uid": "f-1773354799984-j5ri", "type": "SMALL_PAINTING", "col": 12, "row": 9},
{"uid": "f-1773354827151-yox2", "type": "CLOCK", "col": 5, "row": 9},
{"uid": "f-1773354842615-f5md", "type": "PLANT", "col": 18, "row": 10},
{"uid": "f-1773354861273-67uo", "type": "COFFEE", "col": 14, "row": 15},
{"uid": "f-1773354877474-kt9s", "type": "WOODEN_CHAIR_SIDE", "col": 3, "row": 18},
{"uid": "f-1773354879805-px9b", "type": "WOODEN_CHAIR_SIDE", "col": 3, "row": 16},
{"uid": "f-1773354880309-yphd", "type": "WOODEN_CHAIR_SIDE:left", "col": 7, "row": 16},
{"uid": "f-1773354881902-9m50", "type": "WOODEN_CHAIR_SIDE:left", "col": 7, "row": 18},
{"uid": "f-1773354931010-8vvr", "type": "DESK_FRONT", "col": 2, "row": 12},
{"uid": "f-1773354932396-5uus", "type": "DESK_FRONT", "col": 6, "row": 12},
{"uid": "f-1773356768339-eo6u", "type": "CUSHIONED_BENCH", "col": 3, "row": 14},
{"uid": "f-1773356769007-a8jm", "type": "CUSHIONED_BENCH", "col": 7, "row": 14},
{"uid": "f-1773356781294-b69z", "type": "PC_FRONT_OFF", "col": 7, "row": 12},
{"uid": "f-1773356782055-vp70", "type": "PC_FRONT_OFF", "col": 3, "row": 12},
{"uid": "f-1773356784581-5jw9", "type": "PC_SIDE", "col": 4, "row": 16},
{"uid": "f-1773356785458-pyjn", "type": "PC_SIDE", "col": 4, "row": 18},
{"uid": "f-1773356787060-higb", "type": "PC_SIDE:left", "col": 6, "row": 16},
{"uid": "f-1773356787744-ykrz", "type": "PC_SIDE:left", "col": 6, "row": 18},
{"uid": "f-1773356878781-rncl", "type": "PLANT_2", "col": 11, "row": 10},
{"uid": "f-1773356974812-apra", "type": "LARGE_PAINTING", "col": 14, "row": 9},
{"uid": "f-1773357087399-3kfy", "type": "BIN", "col": 2, "row": 20},
{"uid": "f-1773357989802-thws", "type": "SMALL_TABLE_FRONT", "col": 17, "row": 19},
{"uid": "f-1773358001163-aqv4", "type": "SMALL_TABLE_SIDE", "col": 1, "row": 18},
{"uid": "f-1773358458100-4wm2", "type": "COFFEE", "col": 1, "row": 19},
{"uid": "f-1773358479734-biia", "type": "PLANT_2", "col": 1, "row": 17},
{"uid": "f-1773358485454-id8j", "type": "SMALL_PAINTING_2", "col": 17, "row": 9}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 B

View File

@ -1,13 +0,0 @@
{
"id": "BIN",
"name": "Bin",
"category": "misc",
"type": "asset",
"canPlaceOnWalls": false,
"canPlaceOnSurfaces": false,
"backgroundTiles": 0,
"width": 16,
"height": 16,
"footprintW": 1,
"footprintH": 1
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 B

View File

@ -1,13 +0,0 @@
{
"id": "BOOKSHELF",
"name": "Bookshelf",
"category": "wall",
"type": "asset",
"canPlaceOnWalls": true,
"canPlaceOnSurfaces": false,
"backgroundTiles": 0,
"width": 32,
"height": 16,
"footprintW": 2,
"footprintH": 1
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 558 B

View File

@ -1,13 +0,0 @@
{
"id": "CACTUS",
"name": "Cactus",
"category": "decor",
"type": "asset",
"canPlaceOnWalls": false,
"canPlaceOnSurfaces": false,
"backgroundTiles": 1,
"width": 16,
"height": 32,
"footprintW": 1,
"footprintH": 2
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 B

View File

@ -1,13 +0,0 @@
{
"id": "CLOCK",
"name": "Clock",
"category": "wall",
"type": "asset",
"canPlaceOnWalls": true,
"canPlaceOnSurfaces": false,
"backgroundTiles": 0,
"width": 16,
"height": 32,
"footprintW": 1,
"footprintH": 2
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 B

View File

@ -1,13 +0,0 @@
{
"id": "COFFEE",
"name": "Coffee",
"category": "decor",
"type": "asset",
"canPlaceOnWalls": false,
"canPlaceOnSurfaces": true,
"backgroundTiles": 0,
"width": 16,
"height": 16,
"footprintW": 1,
"footprintH": 1
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 B

View File

@ -1,13 +0,0 @@
{
"id": "COFFEE_TABLE",
"name": "Coffee Table",
"category": "desks",
"type": "asset",
"canPlaceOnWalls": false,
"canPlaceOnSurfaces": false,
"backgroundTiles": 0,
"width": 32,
"height": 32,
"footprintW": 2,
"footprintH": 2
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 B

View File

@ -1,13 +0,0 @@
{
"id": "CUSHIONED_BENCH",
"name": "Cushioned Bench",
"category": "chairs",
"type": "asset",
"canPlaceOnWalls": false,
"canPlaceOnSurfaces": false,
"backgroundTiles": 0,
"width": 16,
"height": 16,
"footprintW": 1,
"footprintH": 1
}

View File

@ -1,44 +0,0 @@
{
"id": "CUSHIONED_CHAIR",
"name": "Cushioned Chair",
"category": "chairs",
"type": "group",
"groupType": "rotation",
"rotationScheme": "3-way-mirror",
"canPlaceOnWalls": false,
"canPlaceOnSurfaces": false,
"backgroundTiles": 0,
"members": [
{
"type": "asset",
"id": "CUSHIONED_CHAIR_FRONT",
"file": "CUSHIONED_CHAIR_FRONT.png",
"width": 16,
"height": 16,
"footprintW": 1,
"footprintH": 1,
"orientation": "front"
},
{
"type": "asset",
"id": "CUSHIONED_CHAIR_BACK",
"file": "CUSHIONED_CHAIR_BACK.png",
"width": 16,
"height": 16,
"footprintW": 1,
"footprintH": 1,
"orientation": "back"
},
{
"type": "asset",
"id": "CUSHIONED_CHAIR_SIDE",
"file": "CUSHIONED_CHAIR_SIDE.png",
"width": 16,
"height": 16,
"footprintW": 1,
"footprintH": 1,
"orientation": "side",
"mirrorSide": true
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 B

View File

@ -1,33 +0,0 @@
{
"id": "DESK",
"name": "Desk",
"category": "desks",
"type": "group",
"groupType": "rotation",
"rotationScheme": "2-way",
"canPlaceOnWalls": false,
"canPlaceOnSurfaces": false,
"backgroundTiles": 1,
"members": [
{
"type": "asset",
"id": "DESK_FRONT",
"file": "DESK_FRONT.png",
"width": 48,
"height": 32,
"footprintW": 3,
"footprintH": 2,
"orientation": "front"
},
{
"type": "asset",
"id": "DESK_SIDE",
"file": "DESK_SIDE.png",
"width": 16,
"height": 64,
"footprintW": 1,
"footprintH": 4,
"orientation": "side"
}
]
}

Some files were not shown because too many files have changed in this diff Show More