Initial EdgeEver implementation
This commit is contained in:
commit
dd746e1d60
33 files changed
+3411
No files matched your search
+14
@@ -0,0 +1,14 @@
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
coverage/
|
||||
|
||||
.wrangler/
|
||||
.dev.vars
|
||||
|
||||
*.log
|
||||
@@ -0,0 +1,194 @@
|
||||
# EdgeEver
|
||||
|
||||
> **EdgeEver: A self-hosted, Cloudflare-native Evernote alternative.**
|
||||
>
|
||||
> **EdgeEver:基于 Cloudflare 全家桶自托管的开源『印象笔记』。**
|
||||
|
||||
EdgeEver 是一个完全开源、支持自部署、面向人类和 AI Agent 的现代笔记工作区。
|
||||
|
||||
它致敬经典印象笔记的大屏三栏交互:笔记本目录、笔记列表、主编辑区。但 EdgeEver 不想复刻臃肿的旧时代套件,而是用 Cloudflare 的边缘网络、轻量前端、开放 API、MCP 和 CLI,把笔记系统重新做成一个可自托管、可编程、可被 AI 读写的个人知识库底座。
|
||||
|
||||
## 产品定位
|
||||
|
||||
EdgeEver 是一个现代个人知识库,面向三类一等用户:
|
||||
|
||||
- **人类用户**:在专注的三栏界面中捕获、整理、编辑、搜索、合并笔记。
|
||||
- **AI Agent**:通过 REST API、MCP 或 CLI 读取、搜索、新建、追加、合并和整理笔记,而不是模拟点击 UI。
|
||||
- **自部署用户**:把完整系统部署到自己的 Cloudflare 账号下,用很低的运维成本拥有自己的笔记后端。
|
||||
|
||||
EdgeEver 的目标不是再做一个大而全的效率套件,而是做一个锋利、轻快、开放的笔记核心:本地优先的体验,全球同步的部署,面向 AI 的明确接口。
|
||||
|
||||
## 项目背景
|
||||
|
||||
经典印象笔记有一个至今仍然优秀的设计:大屏三栏工作流。
|
||||
|
||||
```text
|
||||
笔记本目录 -> 笔记列表 -> 编辑器
|
||||
```
|
||||
|
||||
这个布局依旧是浏览个人资料库、整理长期知识、快速切换上下文的高效方式。但现代笔记系统需要一个新的技术底座:
|
||||
|
||||
- 笔记内容应该可读、可导出、可迁移。
|
||||
- 前端应该是轻量 SPA/PWA,而不是不必要的重型服务端应用。
|
||||
- 图片和附件应该进入对象存储,而不是塞进不透明的数据块。
|
||||
- 搜索应该建立在清晰的数据模型之上。
|
||||
- AI Agent 应该拥有显式、受权限控制的接口,而不是伪装成人类用户。
|
||||
- 自托管应该对个人开发者足够现实。
|
||||
|
||||
EdgeEver 要做的就是这个版本:经典的信息架构,现代的边缘基础设施,从第一天起就为 Agent 读写做好准备。
|
||||
|
||||
## 核心体验
|
||||
|
||||
- **桌面三栏布局**:笔记本树、笔记卡片流、富文本编辑器。
|
||||
- **无限级嵌套笔记本**:通过 `parent_id` 支持多级目录树。
|
||||
- **Markdown 友好的富文本编辑**:TipTap/ProseMirror JSON 作为权威编辑器格式,Markdown 作为 Agent、CLI 和导出格式。
|
||||
- **PWA 响应式布局**:桌面端三栏,移动端自动折叠为单栏。
|
||||
- **多选合并笔记**:选中多条笔记后创建一条合并后的新笔记,用 Markdown 分割线连接内容,原笔记软删除,资源引用重新指向新笔记。
|
||||
- **Headless 能力**:REST、MCP 和 CLI 共享同一套核心服务。
|
||||
- **资源模型**:图片和附件存入 R2,D1 保存资源元数据和笔记关联关系。
|
||||
|
||||
## 架构规划
|
||||
|
||||
```text
|
||||
edgeever/
|
||||
├── apps/
|
||||
│ ├── web/ # Vite + React PWA
|
||||
│ └── api/ # Cloudflare Worker + Hono
|
||||
├── packages/
|
||||
│ └── shared/ # 共享类型、Zod schema、内容格式转换
|
||||
├── migrations/ # Cloudflare D1 SQL 迁移
|
||||
├── docs/
|
||||
├── tailwind.config.ts
|
||||
└── wrangler.toml # Worker、Assets、D1、R2 绑定配置
|
||||
```
|
||||
|
||||
目标运行形态是一个 Cloudflare Worker:
|
||||
|
||||
- `/api/v1/*` 提供 REST API。
|
||||
- `/mcp` 提供远程 MCP Streamable HTTP 入口。
|
||||
- Workers Assets 从 `apps/web/dist` 托管构建后的 SPA。
|
||||
- D1 存储笔记本、笔记元数据、正文记录、搜索索引、Token、修订记录和审计事件。
|
||||
- R2 存储图片和附件。
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 工程工具链
|
||||
|
||||
- **Bun**:包管理器、脚本运行入口和 monorepo workspace 工具。
|
||||
|
||||
### 前端
|
||||
|
||||
- **Vite**:轻量 SPA 构建工具。
|
||||
- **React**:应用 UI 层。
|
||||
- **Tailwind CSS**:原子化样式系统。
|
||||
- **shadcn/ui**:可组合、可访问的 UI 组件基础。
|
||||
- **TipTap**:基于 ProseMirror 的富文本编辑器核心。
|
||||
- **TanStack Query**:API 缓存、乐观更新、重试和服务端状态管理。
|
||||
- **Dexie**:基于 IndexedDB 的离线缓存和本地草稿队列。
|
||||
- **Zod**:前后端共享数据校验。
|
||||
|
||||
### 后端
|
||||
|
||||
- **Cloudflare Workers**:边缘运行时。
|
||||
- **Hono**:轻量、快速的 HTTP 路由框架。
|
||||
- **Cloudflare D1**:SQLite 语义的关系型数据库。
|
||||
- **Cloudflare R2**:图片和附件对象存储。
|
||||
- **D1 FTS5**:全文搜索能力。
|
||||
- **Wrangler**:本地开发、迁移和部署工具。
|
||||
|
||||
### Agent 与开发者接口
|
||||
|
||||
- **REST API**:供 Web、CLI 和第三方集成调用。
|
||||
- **MCP Streamable HTTP**:通过 `/mcp` 暴露给支持远程 MCP 的客户端。
|
||||
- **本地 stdio MCP Server**:通过 EdgeEver CLI 启动,供桌面 AI 工具接入。
|
||||
- **CLI**:用于脚本化、导入导出和终端工作流。
|
||||
|
||||
## 内容存储策略
|
||||
|
||||
EdgeEver 不应该只把 Markdown 当作唯一真实格式。
|
||||
|
||||
```text
|
||||
content_json TipTap/ProseMirror 文档,权威编辑器格式
|
||||
content_markdown Agent、CLI、导入导出、diff 使用的格式
|
||||
content_text 全文搜索、摘要、embedding 使用的纯文本
|
||||
```
|
||||
|
||||
浏览器编辑器写入 TipTap JSON,并派生 Markdown 和纯文本。MCP 与 CLI 可以接受 Markdown 输入,但服务端需要把 Markdown 转换并校验为权威的 TipTap JSON 后再入库。
|
||||
|
||||
## Cloudflare 初始化
|
||||
|
||||
创建 D1 数据库和 R2 存储桶:
|
||||
|
||||
```sh
|
||||
bunx wrangler d1 create edgeever
|
||||
bunx wrangler r2 bucket create edgeever-resources
|
||||
```
|
||||
|
||||
把 D1 创建命令返回的 `database_id` 填入 `wrangler.toml`。
|
||||
|
||||
应用本地迁移:
|
||||
|
||||
```sh
|
||||
bunx wrangler d1 migrations apply DB --local
|
||||
```
|
||||
|
||||
应用远程迁移:
|
||||
|
||||
```sh
|
||||
bunx wrangler d1 migrations apply DB --remote
|
||||
```
|
||||
|
||||
## 本地开发
|
||||
|
||||
安装依赖:
|
||||
|
||||
```sh
|
||||
bun install
|
||||
```
|
||||
|
||||
应用本地 D1 迁移:
|
||||
|
||||
```sh
|
||||
bun run db:migrate:local
|
||||
```
|
||||
|
||||
同时启动 Worker API 和 Vite 前端:
|
||||
|
||||
```sh
|
||||
bun run dev
|
||||
```
|
||||
|
||||
也可以分开启动:
|
||||
|
||||
```sh
|
||||
bun run dev:api
|
||||
bun run dev:web
|
||||
```
|
||||
|
||||
常用校验:
|
||||
|
||||
```sh
|
||||
bun run typecheck
|
||||
bun run build
|
||||
```
|
||||
|
||||
## 当前阶段
|
||||
|
||||
当前已经具备第一条可运行纵向切片:
|
||||
|
||||
- Bun monorepo 基础工程。
|
||||
- D1 初始 schema:支持嵌套笔记本、笔记正文拆表、FTS5 搜索、资源、修订、审计、软删除和合并字段。
|
||||
- Hono REST API:笔记本列表/创建/更新/删除,笔记列表/创建/读取/更新/删除,搜索,多选合并。
|
||||
- Vite React PWA:桌面三栏布局,移动端单栏切换,递归笔记本树,笔记卡片流,TipTap 编辑器,标签和本地草稿备份。
|
||||
- 基础 `wrangler.toml`:配置 Workers Assets、D1、R2 和 `/api/*`、`/mcp` Worker-first 路由。
|
||||
|
||||
后续阶段会继续补完整 MCP Server、CLI、资源上传、认证 Token、导入导出和更完整的 Markdown 双向转换。
|
||||
|
||||
## 项目原则
|
||||
|
||||
- **默认开放**:数据应该可读、可导出、可脚本化。
|
||||
- **为 Agent 准备,但不牺牲人类体验**:人类使用最好的 UI,Agent 使用明确的 API。
|
||||
- **边缘原生,而不是边缘噱头**:只在 Cloudflare 真正降低部署和运维复杂度的地方使用它。
|
||||
- **内容可迁移**:Markdown 很重要,但结构化编辑器 JSON 才能保护富文本保真度。
|
||||
- **软删除和可审计**:破坏性操作必须可追踪、可恢复。
|
||||
- **小模块,清边界**:UI、REST、MCP、CLI 共享核心行为,而不是重复实现。
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@edgeever/api",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module"
|
||||
}
|
||||
@@ -0,0 +1,604 @@
|
||||
import {
|
||||
createExcerpt,
|
||||
docToMarkdown,
|
||||
docToText,
|
||||
emptyDoc,
|
||||
markdownToDoc,
|
||||
MemoCreateSchema,
|
||||
MemoUpdateSchema,
|
||||
MergeMemosSchema,
|
||||
normalizeTags,
|
||||
NotebookCreateSchema,
|
||||
NotebookUpdateSchema,
|
||||
type MemoDetail,
|
||||
type MemoSummary,
|
||||
type Notebook,
|
||||
type TiptapDoc,
|
||||
} from "@edgeever/shared";
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
import type { Context } from "hono";
|
||||
import { Hono } from "hono";
|
||||
import { cors } from "hono/cors";
|
||||
|
||||
type Bindings = {
|
||||
DB: D1Database;
|
||||
RESOURCES: R2Bucket;
|
||||
};
|
||||
|
||||
type NotebookRow = {
|
||||
id: string;
|
||||
parent_id: string | null;
|
||||
name: string;
|
||||
slug: string | null;
|
||||
icon: string | null;
|
||||
color: string | null;
|
||||
sort_order: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
type MemoSummaryRow = {
|
||||
id: string;
|
||||
notebook_id: string;
|
||||
title: string | null;
|
||||
excerpt: string;
|
||||
tags_json: string;
|
||||
is_pinned: number;
|
||||
is_archived: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
revision: number;
|
||||
};
|
||||
|
||||
type MemoDetailRow = MemoSummaryRow & {
|
||||
content_json: string;
|
||||
content_markdown: string;
|
||||
content_text: string;
|
||||
source_memo_ids: string;
|
||||
merge_source_count: number;
|
||||
merged_into_memo_id: string | null;
|
||||
content_hash: string;
|
||||
};
|
||||
|
||||
const app = new Hono<{ Bindings: Bindings }>();
|
||||
|
||||
app.use(
|
||||
"/api/*",
|
||||
cors({
|
||||
origin: ["http://127.0.0.1:5173", "http://localhost:5173"],
|
||||
allowHeaders: ["Content-Type", "Authorization"],
|
||||
allowMethods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
|
||||
})
|
||||
);
|
||||
|
||||
app.get("/api/health", (c) =>
|
||||
c.json({
|
||||
ok: true,
|
||||
name: "edgeever",
|
||||
runtime: "cloudflare-workers",
|
||||
})
|
||||
);
|
||||
|
||||
app.get("/api/v1/notebooks", async (c) => {
|
||||
const rows = await c.env.DB.prepare(
|
||||
`SELECT id, parent_id, name, slug, icon, color, sort_order, created_at, updated_at
|
||||
FROM notebooks
|
||||
WHERE is_deleted = 0
|
||||
ORDER BY parent_id IS NOT NULL, sort_order ASC, name ASC`
|
||||
).all<NotebookRow>();
|
||||
|
||||
return c.json({ notebooks: rows.results.map(mapNotebook) });
|
||||
});
|
||||
|
||||
app.post("/api/v1/notebooks", zValidator("json", NotebookCreateSchema), async (c) => {
|
||||
const input = c.req.valid("json");
|
||||
const id = createId("nb");
|
||||
const now = isoNow();
|
||||
|
||||
await c.env.DB.prepare(
|
||||
`INSERT INTO notebooks (id, parent_id, name, slug, sort_order, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||
)
|
||||
.bind(id, input.parentId ?? null, input.name, slugify(input.name), Date.now(), now, now)
|
||||
.run();
|
||||
|
||||
const notebook = await getNotebook(c.env.DB, id);
|
||||
await audit(c.env.DB, "user", null, "notebook.create", "notebook", id, { name: input.name });
|
||||
|
||||
return c.json({ notebook }, 201);
|
||||
});
|
||||
|
||||
app.patch("/api/v1/notebooks/:id", zValidator("json", NotebookUpdateSchema), async (c) => {
|
||||
const id = c.req.param("id");
|
||||
const input = c.req.valid("json");
|
||||
const current = await getNotebook(c.env.DB, id);
|
||||
|
||||
if (!current) {
|
||||
return notFound(c, "Notebook not found");
|
||||
}
|
||||
|
||||
const nextName = input.name ?? current.name;
|
||||
const nextParentId = input.parentId === undefined ? current.parentId : input.parentId;
|
||||
const nextSortOrder = input.sortOrder ?? current.sortOrder;
|
||||
const now = isoNow();
|
||||
|
||||
await c.env.DB.prepare(
|
||||
`UPDATE notebooks
|
||||
SET name = ?, slug = ?, parent_id = ?, sort_order = ?, updated_at = ?
|
||||
WHERE id = ? AND is_deleted = 0`
|
||||
)
|
||||
.bind(nextName, slugify(nextName), nextParentId ?? null, nextSortOrder, now, id)
|
||||
.run();
|
||||
|
||||
await audit(c.env.DB, "user", null, "notebook.update", "notebook", id, input);
|
||||
return c.json({ notebook: await getNotebook(c.env.DB, id) });
|
||||
});
|
||||
|
||||
app.delete("/api/v1/notebooks/:id", async (c) => {
|
||||
const id = c.req.param("id");
|
||||
const now = isoNow();
|
||||
|
||||
await c.env.DB.prepare(
|
||||
`UPDATE notebooks
|
||||
SET is_deleted = 1, deleted_at = ?, updated_at = ?
|
||||
WHERE id = ? AND id <> 'nb_inbox'`
|
||||
)
|
||||
.bind(now, now, id)
|
||||
.run();
|
||||
|
||||
await audit(c.env.DB, "user", null, "notebook.delete", "notebook", id, {});
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
app.get("/api/v1/memos", async (c) => {
|
||||
const notebookId = c.req.query("notebookId");
|
||||
const q = c.req.query("q")?.trim();
|
||||
const limit = clampNumber(Number(c.req.query("limit") ?? 80), 1, 100);
|
||||
|
||||
if (q) {
|
||||
const ftsQuery = toFtsQuery(q);
|
||||
|
||||
if (ftsQuery) {
|
||||
const rows = await c.env.DB.prepare(
|
||||
`SELECT m.id, m.notebook_id, m.title, m.excerpt, m.tags_json, m.is_pinned,
|
||||
m.is_archived, m.created_at, m.updated_at, c.revision
|
||||
FROM memos_fts f
|
||||
INNER JOIN memos m ON m.id = f.memo_id
|
||||
INNER JOIN memo_contents c ON c.memo_id = m.id
|
||||
WHERE memos_fts MATCH ?
|
||||
AND m.is_deleted = 0
|
||||
AND (? IS NULL OR m.notebook_id = ?)
|
||||
ORDER BY m.is_pinned DESC, m.updated_at DESC
|
||||
LIMIT ?`
|
||||
)
|
||||
.bind(ftsQuery, notebookId ?? null, notebookId ?? null, limit)
|
||||
.all<MemoSummaryRow>();
|
||||
|
||||
return c.json({ memos: rows.results.map(mapMemoSummary) });
|
||||
}
|
||||
}
|
||||
|
||||
const rows = await c.env.DB.prepare(
|
||||
`SELECT m.id, m.notebook_id, m.title, m.excerpt, m.tags_json, m.is_pinned,
|
||||
m.is_archived, m.created_at, m.updated_at, c.revision
|
||||
FROM memos m
|
||||
INNER JOIN memo_contents c ON c.memo_id = m.id
|
||||
WHERE m.is_deleted = 0
|
||||
AND (? IS NULL OR m.notebook_id = ?)
|
||||
ORDER BY m.is_pinned DESC, m.updated_at DESC
|
||||
LIMIT ?`
|
||||
)
|
||||
.bind(notebookId ?? null, notebookId ?? null, limit)
|
||||
.all<MemoSummaryRow>();
|
||||
|
||||
return c.json({ memos: rows.results.map(mapMemoSummary) });
|
||||
});
|
||||
|
||||
app.post("/api/v1/memos", zValidator("json", MemoCreateSchema), async (c) => {
|
||||
const input = c.req.valid("json");
|
||||
const tags = normalizeTags(input.tags);
|
||||
const contentMarkdown = input.contentMarkdown ?? "";
|
||||
const contentJson = markdownToDoc(contentMarkdown);
|
||||
const contentText = docToText(contentJson);
|
||||
const title = input.title || deriveTitle(contentText);
|
||||
const excerpt = createExcerpt(contentText || title || "");
|
||||
const contentHash = await sha256(contentMarkdown + JSON.stringify(contentJson));
|
||||
const id = createId("memo");
|
||||
const now = isoNow();
|
||||
|
||||
await c.env.DB.batch([
|
||||
c.env.DB.prepare(
|
||||
`INSERT INTO memos (
|
||||
id, notebook_id, title, excerpt, tags_json, created_by, updated_by, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, 'user', 'user', ?, ?)`
|
||||
).bind(id, input.notebookId, title, excerpt, JSON.stringify(tags), now, now),
|
||||
c.env.DB.prepare(
|
||||
`INSERT INTO memo_contents (
|
||||
memo_id, content_json, content_markdown, content_text, content_hash, revision, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, 0, ?, ?)`
|
||||
).bind(id, JSON.stringify(contentJson), contentMarkdown, contentText, contentHash, now, now),
|
||||
c.env.DB.prepare(
|
||||
`INSERT INTO memos_fts (memo_id, title, content_text, tags)
|
||||
VALUES (?, ?, ?, ?)`
|
||||
).bind(id, title, contentText, tags.join(" ")),
|
||||
auditStatement(c.env.DB, "user", null, "memo.create", "memo", id, { notebookId: input.notebookId }),
|
||||
]);
|
||||
|
||||
return c.json({ memo: await getMemoDetail(c.env.DB, id) }, 201);
|
||||
});
|
||||
|
||||
app.get("/api/v1/memos/:id", async (c) => {
|
||||
const memo = await getMemoDetail(c.env.DB, c.req.param("id"));
|
||||
|
||||
if (!memo) {
|
||||
return notFound(c, "Memo not found");
|
||||
}
|
||||
|
||||
return c.json({ memo });
|
||||
});
|
||||
|
||||
app.patch("/api/v1/memos/:id", zValidator("json", MemoUpdateSchema), async (c) => {
|
||||
const id = c.req.param("id");
|
||||
const input = c.req.valid("json");
|
||||
const current = await getMemoDetailRow(c.env.DB, id);
|
||||
|
||||
if (!current) {
|
||||
return notFound(c, "Memo not found");
|
||||
}
|
||||
|
||||
if (input.expectedRevision !== undefined && input.expectedRevision !== current.revision) {
|
||||
return c.json(
|
||||
{
|
||||
error: {
|
||||
code: "revision_conflict",
|
||||
message: "Memo was updated elsewhere. Reload before saving.",
|
||||
details: {
|
||||
expectedRevision: input.expectedRevision,
|
||||
currentRevision: current.revision,
|
||||
},
|
||||
},
|
||||
},
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
const currentContentJson = JSON.parse(current.content_json) as TiptapDoc;
|
||||
const contentJson = input.contentJson
|
||||
? (input.contentJson as TiptapDoc)
|
||||
: input.contentMarkdown !== undefined
|
||||
? markdownToDoc(input.contentMarkdown)
|
||||
: currentContentJson;
|
||||
const contentMarkdown =
|
||||
input.contentMarkdown !== undefined ? input.contentMarkdown : docToMarkdown(contentJson);
|
||||
const contentText = docToText(contentJson);
|
||||
const title = input.title ?? current.title ?? deriveTitle(contentText);
|
||||
const tags = input.tags === undefined ? parseJsonArray(current.tags_json) : normalizeTags(input.tags);
|
||||
const excerpt = createExcerpt(contentText || title || "");
|
||||
const notebookId = input.notebookId ?? current.notebook_id;
|
||||
const nextRevision = current.revision + 1;
|
||||
const contentHash = await sha256(contentMarkdown + JSON.stringify(contentJson));
|
||||
const now = isoNow();
|
||||
|
||||
await c.env.DB.batch([
|
||||
c.env.DB.prepare(
|
||||
`INSERT INTO memo_revisions (
|
||||
id, memo_id, revision, title, content_json, content_markdown, content_hash, created_by, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, 'user', ?)`
|
||||
).bind(
|
||||
createId("rev"),
|
||||
id,
|
||||
current.revision,
|
||||
current.title,
|
||||
current.content_json,
|
||||
current.content_markdown,
|
||||
current.content_hash,
|
||||
now
|
||||
),
|
||||
c.env.DB.prepare(
|
||||
`UPDATE memos
|
||||
SET notebook_id = ?, title = ?, excerpt = ?, tags_json = ?, updated_by = 'user', updated_at = ?
|
||||
WHERE id = ? AND is_deleted = 0`
|
||||
).bind(notebookId, title, excerpt, JSON.stringify(tags), now, id),
|
||||
c.env.DB.prepare(
|
||||
`UPDATE memo_contents
|
||||
SET content_json = ?, content_markdown = ?, content_text = ?, content_hash = ?,
|
||||
revision = ?, updated_at = ?
|
||||
WHERE memo_id = ?`
|
||||
).bind(JSON.stringify(contentJson), contentMarkdown, contentText, contentHash, nextRevision, now, id),
|
||||
c.env.DB.prepare(`DELETE FROM memos_fts WHERE memo_id = ?`).bind(id),
|
||||
c.env.DB.prepare(
|
||||
`INSERT INTO memos_fts (memo_id, title, content_text, tags)
|
||||
VALUES (?, ?, ?, ?)`
|
||||
).bind(id, title, contentText, tags.join(" ")),
|
||||
auditStatement(c.env.DB, "user", null, "memo.update", "memo", id, { revision: nextRevision }),
|
||||
]);
|
||||
|
||||
return c.json({ memo: await getMemoDetail(c.env.DB, id) });
|
||||
});
|
||||
|
||||
app.delete("/api/v1/memos/:id", async (c) => {
|
||||
const id = c.req.param("id");
|
||||
const now = isoNow();
|
||||
|
||||
await c.env.DB.batch([
|
||||
c.env.DB.prepare(
|
||||
`UPDATE memos
|
||||
SET is_deleted = 1, deleted_at = ?, updated_at = ?
|
||||
WHERE id = ? AND is_deleted = 0`
|
||||
).bind(now, now, id),
|
||||
auditStatement(c.env.DB, "user", null, "memo.delete", "memo", id, {}),
|
||||
]);
|
||||
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
app.post("/api/v1/memos/merge", zValidator("json", MergeMemosSchema), async (c) => {
|
||||
const input = c.req.valid("json");
|
||||
const uniqueMemoIds = Array.from(new Set(input.memoIds));
|
||||
const placeholders = uniqueMemoIds.map(() => "?").join(", ");
|
||||
const rows = await c.env.DB.prepare(
|
||||
`SELECT m.id, m.notebook_id, m.title, m.excerpt, m.tags_json, m.is_pinned,
|
||||
m.is_archived, m.created_at, m.updated_at, c.revision,
|
||||
c.content_json, c.content_markdown, c.content_text, c.content_hash,
|
||||
m.source_memo_ids, m.merge_source_count, m.merged_into_memo_id
|
||||
FROM memos m
|
||||
INNER JOIN memo_contents c ON c.memo_id = m.id
|
||||
WHERE m.is_deleted = 0 AND m.id IN (${placeholders})`
|
||||
)
|
||||
.bind(...uniqueMemoIds)
|
||||
.all<MemoDetailRow>();
|
||||
|
||||
if (rows.results.length !== uniqueMemoIds.length) {
|
||||
return c.json(
|
||||
{
|
||||
error: {
|
||||
code: "missing_memos",
|
||||
message: "One or more memos cannot be merged.",
|
||||
},
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const ordered = uniqueMemoIds
|
||||
.map((memoId) => rows.results.find((row) => row.id === memoId))
|
||||
.filter((row): row is MemoDetailRow => Boolean(row));
|
||||
const notebookId = input.notebookId ?? ordered[0].notebook_id;
|
||||
const title = input.title || `合并笔记 ${new Date().toLocaleDateString("zh-CN")}`;
|
||||
const mergedMarkdown = ordered.map((memo) => memo.content_markdown).join("\n\n---\n\n");
|
||||
const contentJson = markdownToDoc(mergedMarkdown);
|
||||
const contentText = docToText(contentJson);
|
||||
const tags = Array.from(new Set(ordered.flatMap((memo) => parseJsonArray(memo.tags_json))));
|
||||
const excerpt = createExcerpt(contentText || title);
|
||||
const contentHash = await sha256(mergedMarkdown + JSON.stringify(contentJson));
|
||||
const newMemoId = createId("memo");
|
||||
const now = isoNow();
|
||||
|
||||
await c.env.DB.batch([
|
||||
c.env.DB.prepare(
|
||||
`INSERT INTO memos (
|
||||
id, notebook_id, title, excerpt, tags_json, source_memo_ids, merge_source_count,
|
||||
created_by, updated_by, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, 'user', 'user', ?, ?)`
|
||||
).bind(
|
||||
newMemoId,
|
||||
notebookId,
|
||||
title,
|
||||
excerpt,
|
||||
JSON.stringify(tags),
|
||||
JSON.stringify(uniqueMemoIds),
|
||||
uniqueMemoIds.length,
|
||||
now,
|
||||
now
|
||||
),
|
||||
c.env.DB.prepare(
|
||||
`INSERT INTO memo_contents (
|
||||
memo_id, content_json, content_markdown, content_text, content_hash, revision, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, 0, ?, ?)`
|
||||
).bind(newMemoId, JSON.stringify(contentJson), mergedMarkdown, contentText, contentHash, now, now),
|
||||
c.env.DB.prepare(
|
||||
`INSERT INTO memos_fts (memo_id, title, content_text, tags)
|
||||
VALUES (?, ?, ?, ?)`
|
||||
).bind(newMemoId, title, contentText, tags.join(" ")),
|
||||
c.env.DB.prepare(
|
||||
`UPDATE memos
|
||||
SET is_deleted = 1, deleted_at = ?, merged_into_memo_id = ?, merged_at = ?, updated_at = ?
|
||||
WHERE id IN (${placeholders})`
|
||||
).bind(now, newMemoId, now, now, ...uniqueMemoIds),
|
||||
c.env.DB.prepare(
|
||||
`UPDATE resources
|
||||
SET original_memo_id = COALESCE(original_memo_id, memo_id),
|
||||
memo_id = ?,
|
||||
updated_at = ?
|
||||
WHERE memo_id IN (${placeholders})`
|
||||
).bind(newMemoId, now, ...uniqueMemoIds),
|
||||
auditStatement(c.env.DB, "user", null, "memo.merge", "memo", newMemoId, {
|
||||
sourceMemoIds: uniqueMemoIds,
|
||||
}),
|
||||
]);
|
||||
|
||||
return c.json({ memo: await getMemoDetail(c.env.DB, newMemoId) }, 201);
|
||||
});
|
||||
|
||||
app.all("/mcp", (c) =>
|
||||
c.json({
|
||||
name: "EdgeEver MCP endpoint",
|
||||
status: "planned",
|
||||
message: "Remote MCP will be wired to the same memo and notebook services as the REST API.",
|
||||
restBasePath: "/api/v1",
|
||||
})
|
||||
);
|
||||
|
||||
app.notFound((c) =>
|
||||
c.json(
|
||||
{
|
||||
error: {
|
||||
code: "not_found",
|
||||
message: "Route not found",
|
||||
},
|
||||
},
|
||||
404
|
||||
)
|
||||
);
|
||||
|
||||
export default app;
|
||||
|
||||
const mapNotebook = (row: NotebookRow): Notebook => ({
|
||||
id: row.id,
|
||||
parentId: row.parent_id,
|
||||
name: row.name,
|
||||
slug: row.slug,
|
||||
icon: row.icon,
|
||||
color: row.color,
|
||||
sortOrder: row.sort_order,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
});
|
||||
|
||||
const mapMemoSummary = (row: MemoSummaryRow): MemoSummary => ({
|
||||
id: row.id,
|
||||
notebookId: row.notebook_id,
|
||||
title: row.title,
|
||||
excerpt: row.excerpt,
|
||||
tags: parseJsonArray(row.tags_json),
|
||||
isPinned: Boolean(row.is_pinned),
|
||||
isArchived: Boolean(row.is_archived),
|
||||
revision: row.revision,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
});
|
||||
|
||||
const mapMemoDetail = (row: MemoDetailRow): MemoDetail => ({
|
||||
...mapMemoSummary(row),
|
||||
contentJson: parseDoc(row.content_json),
|
||||
contentMarkdown: row.content_markdown,
|
||||
contentText: row.content_text,
|
||||
sourceMemoIds: parseJsonArray(row.source_memo_ids),
|
||||
mergeSourceCount: row.merge_source_count,
|
||||
mergedIntoMemoId: row.merged_into_memo_id,
|
||||
});
|
||||
|
||||
const getNotebook = async (db: D1Database, id: string): Promise<Notebook | null> => {
|
||||
const row = await db
|
||||
.prepare(
|
||||
`SELECT id, parent_id, name, slug, icon, color, sort_order, created_at, updated_at
|
||||
FROM notebooks
|
||||
WHERE id = ? AND is_deleted = 0`
|
||||
)
|
||||
.bind(id)
|
||||
.first<NotebookRow>();
|
||||
|
||||
return row ? mapNotebook(row) : null;
|
||||
};
|
||||
|
||||
const getMemoDetailRow = async (db: D1Database, id: string): Promise<MemoDetailRow | null> =>
|
||||
db
|
||||
.prepare(
|
||||
`SELECT m.id, m.notebook_id, m.title, m.excerpt, m.tags_json, m.is_pinned,
|
||||
m.is_archived, m.created_at, m.updated_at, c.revision,
|
||||
c.content_json, c.content_markdown, c.content_text, c.content_hash,
|
||||
m.source_memo_ids, m.merge_source_count, m.merged_into_memo_id
|
||||
FROM memos m
|
||||
INNER JOIN memo_contents c ON c.memo_id = m.id
|
||||
WHERE m.id = ? AND m.is_deleted = 0`
|
||||
)
|
||||
.bind(id)
|
||||
.first<MemoDetailRow>();
|
||||
|
||||
const getMemoDetail = async (db: D1Database, id: string): Promise<MemoDetail | null> => {
|
||||
const row = await getMemoDetailRow(db, id);
|
||||
return row ? mapMemoDetail(row) : null;
|
||||
};
|
||||
|
||||
const parseJsonArray = (json: string): string[] => {
|
||||
try {
|
||||
const value = JSON.parse(json);
|
||||
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const parseDoc = (json: string): TiptapDoc => {
|
||||
try {
|
||||
const value = JSON.parse(json);
|
||||
return value && typeof value === "object" ? (value as TiptapDoc) : emptyDoc();
|
||||
} catch {
|
||||
return emptyDoc();
|
||||
}
|
||||
};
|
||||
|
||||
const audit = async (
|
||||
db: D1Database,
|
||||
actorType: "user" | "agent" | "system",
|
||||
actorId: string | null,
|
||||
action: string,
|
||||
entityType: string,
|
||||
entityId: string,
|
||||
metadata: unknown
|
||||
) => auditStatement(db, actorType, actorId, action, entityType, entityId, metadata).run();
|
||||
|
||||
const auditStatement = (
|
||||
db: D1Database,
|
||||
actorType: "user" | "agent" | "system",
|
||||
actorId: string | null,
|
||||
action: string,
|
||||
entityType: string,
|
||||
entityId: string,
|
||||
metadata: unknown
|
||||
) =>
|
||||
db
|
||||
.prepare(
|
||||
`INSERT INTO audit_events (
|
||||
id, actor_type, actor_id, action, entity_type, entity_id, metadata_json, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
)
|
||||
.bind(createId("audit"), actorType, actorId, action, entityType, entityId, JSON.stringify(metadata ?? {}), isoNow());
|
||||
|
||||
const createId = (prefix: string) => `${prefix}_${crypto.randomUUID().replace(/-/g, "")}`;
|
||||
|
||||
const isoNow = () => new Date().toISOString();
|
||||
|
||||
const slugify = (value: string) =>
|
||||
value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/['"]/g, "")
|
||||
.replace(/[^\p{L}\p{N}]+/gu, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.slice(0, 80);
|
||||
|
||||
const deriveTitle = (text: string) => {
|
||||
const title = text.trim().split(/\s+/).slice(0, 10).join(" ");
|
||||
return title || "Untitled memo";
|
||||
};
|
||||
|
||||
const clampNumber = (value: number, min: number, max: number) => {
|
||||
if (Number.isNaN(value)) {
|
||||
return min;
|
||||
}
|
||||
|
||||
return Math.min(Math.max(value, min), max);
|
||||
};
|
||||
|
||||
const toFtsQuery = (value: string) => {
|
||||
const tokens = value.match(/[\p{L}\p{N}_-]+/gu) ?? [];
|
||||
return tokens.slice(0, 8).join(" ");
|
||||
};
|
||||
|
||||
const sha256 = async (value: string) => {
|
||||
const bytes = new TextEncoder().encode(value);
|
||||
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
||||
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
};
|
||||
|
||||
const notFound = (c: Context, message: string) =>
|
||||
c.json(
|
||||
{
|
||||
error: {
|
||||
code: "not_found",
|
||||
message,
|
||||
},
|
||||
},
|
||||
404
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content"
|
||||
/>
|
||||
<meta name="theme-color" content="#d1fae5" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta
|
||||
name="description"
|
||||
content="EdgeEver:基于 Cloudflare 全家桶自托管的开源印象笔记。"
|
||||
/>
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<title>EdgeEver</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@edgeever/web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "EdgeEver",
|
||||
"short_name": "EdgeEver",
|
||||
"description": "基于 Cloudflare 全家桶自托管的开源印象笔记。",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#f0fdf4",
|
||||
"theme_color": "#d1fae5",
|
||||
"lang": "zh-CN",
|
||||
"icons": []
|
||||
}
|
||||
@@ -0,0 +1,804 @@
|
||||
import type { MemoDetail, MemoSummary, Notebook, TiptapDoc } from "@edgeever/shared";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import { EditorContent, useEditor } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Archive,
|
||||
CheckSquare,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
FilePlus2,
|
||||
Folder,
|
||||
Inbox,
|
||||
LayoutList,
|
||||
Merge,
|
||||
MoreHorizontal,
|
||||
PanelLeft,
|
||||
Plus,
|
||||
Save,
|
||||
Search,
|
||||
Sparkles,
|
||||
Tags,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState, type MouseEvent } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { localDb } from "@/lib/local-db";
|
||||
import { buildNotebookTree, cn, formatDateTime, parseTagsText, type NotebookNode } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type Pane = "notebooks" | "memos" | "editor";
|
||||
|
||||
export const App = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const [activePane, setActivePane] = useState<Pane>("memos");
|
||||
const [selectedNotebookId, setSelectedNotebookId] = useState<string | null>(null);
|
||||
const [selectedMemoId, setSelectedMemoId] = useState<string | null>(null);
|
||||
const [selectedMemoIds, setSelectedMemoIds] = useState<Set<string>>(new Set());
|
||||
const [multiSelectKeyDown, setMultiSelectKeyDown] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const notebooksQuery = useQuery({
|
||||
queryKey: ["notebooks"],
|
||||
queryFn: () => api.listNotebooks(),
|
||||
});
|
||||
|
||||
const notebooks = notebooksQuery.data?.notebooks ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedNotebookId && notebooks.length > 0) {
|
||||
setSelectedNotebookId(notebooks[0].id);
|
||||
}
|
||||
}, [notebooks, selectedNotebookId]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.ctrlKey || event.metaKey || event.key === "Control" || event.key === "Meta") {
|
||||
setMultiSelectKeyDown(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp = (event: KeyboardEvent) => {
|
||||
setMultiSelectKeyDown(event.ctrlKey || event.metaKey);
|
||||
};
|
||||
|
||||
const handleBlur = () => setMultiSelectKeyDown(false);
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
window.addEventListener("blur", handleBlur);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
window.removeEventListener("keyup", handleKeyUp);
|
||||
window.removeEventListener("blur", handleBlur);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const memosQuery = useQuery({
|
||||
queryKey: ["memos", selectedNotebookId, search],
|
||||
queryFn: () => api.listMemos({ notebookId: selectedNotebookId, q: search }),
|
||||
enabled: Boolean(selectedNotebookId),
|
||||
});
|
||||
|
||||
const memos = memosQuery.data?.memos ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
if (memos.length === 0) {
|
||||
setSelectedMemoId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedMemoId || !memos.some((memo) => memo.id === selectedMemoId)) {
|
||||
setSelectedMemoId(memos[0].id);
|
||||
}
|
||||
}, [memos, selectedMemoId]);
|
||||
|
||||
const memoQuery = useQuery({
|
||||
queryKey: ["memo", selectedMemoId],
|
||||
queryFn: () => api.getMemo(selectedMemoId as string),
|
||||
enabled: Boolean(selectedMemoId),
|
||||
});
|
||||
|
||||
const createNotebookMutation = useMutation({
|
||||
mutationFn: api.createNotebook,
|
||||
onSuccess: async (data) => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["notebooks"] });
|
||||
setSelectedNotebookId(data.notebook.id);
|
||||
setActivePane("memos");
|
||||
},
|
||||
});
|
||||
|
||||
const createMemoMutation = useMutation({
|
||||
mutationFn: api.createMemo,
|
||||
onSuccess: async (data) => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["memos"] });
|
||||
queryClient.setQueryData(["memo", data.memo.id], { memo: data.memo });
|
||||
setSelectedMemoId(data.memo.id);
|
||||
setActivePane("editor");
|
||||
},
|
||||
});
|
||||
|
||||
const mergeMutation = useMutation({
|
||||
mutationFn: api.mergeMemos,
|
||||
onSuccess: async (data) => {
|
||||
setSelectedMemoIds(new Set());
|
||||
await queryClient.invalidateQueries({ queryKey: ["memos"] });
|
||||
queryClient.setQueryData(["memo", data.memo.id], { memo: data.memo });
|
||||
setSelectedMemoId(data.memo.id);
|
||||
setActivePane("editor");
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMemoMutation = useMutation({
|
||||
mutationFn: api.deleteMemo,
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["memos"] });
|
||||
},
|
||||
});
|
||||
|
||||
const selectedNotebook = notebooks.find((notebook) => notebook.id === selectedNotebookId) ?? null;
|
||||
const selectedMemo = memoQuery.data?.memo ?? null;
|
||||
|
||||
const handleCreateNotebook = (parentId?: string | null) => {
|
||||
const name = window.prompt("新笔记本名称");
|
||||
|
||||
if (!name?.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
createNotebookMutation.mutate({ name: name.trim(), parentId: parentId ?? null });
|
||||
};
|
||||
|
||||
const handleCreateMemo = () => {
|
||||
if (!selectedNotebookId) {
|
||||
return;
|
||||
}
|
||||
|
||||
createMemoMutation.mutate({
|
||||
notebookId: selectedNotebookId,
|
||||
title: "Untitled memo",
|
||||
contentMarkdown: "",
|
||||
tags: [],
|
||||
});
|
||||
};
|
||||
|
||||
const handleMerge = () => {
|
||||
if (!selectedNotebookId || selectedMemoIds.size < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
mergeMutation.mutate({
|
||||
memoIds: Array.from(selectedMemoIds),
|
||||
notebookId: selectedNotebookId,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-[100dvh] overflow-hidden bg-emerald-50 text-slate-950">
|
||||
<div className="min-w-0 flex-1">
|
||||
<main className="grid h-[100dvh] min-h-0 lg:grid-cols-[260px_360px_minmax(0,1fr)]">
|
||||
<aside
|
||||
className={cn(
|
||||
"min-h-0 border-r border-emerald-100 bg-white/90 lg:block",
|
||||
activePane === "notebooks" ? "block" : "hidden"
|
||||
)}
|
||||
>
|
||||
<NotebookPane
|
||||
notebooks={notebooks}
|
||||
selectedNotebookId={selectedNotebookId}
|
||||
isLoading={notebooksQuery.isLoading}
|
||||
onSelect={(notebookId) => {
|
||||
setSelectedNotebookId(notebookId);
|
||||
setSelectedMemoIds(new Set());
|
||||
setActivePane("memos");
|
||||
}}
|
||||
onCreateNotebook={handleCreateNotebook}
|
||||
onBackToList={() => setActivePane("memos")}
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<section
|
||||
className={cn(
|
||||
"min-h-0 border-r border-emerald-100 bg-emerald-50/80 lg:block",
|
||||
activePane === "memos" ? "block" : "hidden"
|
||||
)}
|
||||
>
|
||||
<MemoListPane
|
||||
notebook={selectedNotebook}
|
||||
memos={memos}
|
||||
selectedMemoId={selectedMemoId}
|
||||
selectedMemoIds={selectedMemoIds}
|
||||
search={search}
|
||||
isLoading={memosQuery.isLoading}
|
||||
isCreating={createMemoMutation.isPending}
|
||||
isMerging={mergeMutation.isPending}
|
||||
multiSelectKeyDown={multiSelectKeyDown}
|
||||
onBackToNotebooks={() => setActivePane("notebooks")}
|
||||
onSearch={setSearch}
|
||||
onCreateMemo={handleCreateMemo}
|
||||
onOpenMemo={(memoId) => {
|
||||
setSelectedMemoId(memoId);
|
||||
setActivePane("editor");
|
||||
}}
|
||||
onToggleMemo={(memoId) => {
|
||||
setSelectedMemoIds((current) => toggleMemoSelection(current, memoId));
|
||||
}}
|
||||
onMerge={handleMerge}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className={cn("min-h-0 min-w-0 bg-white lg:block", activePane === "editor" ? "block" : "hidden")}>
|
||||
<EditorPane
|
||||
memo={selectedMemo}
|
||||
notebooks={notebooks}
|
||||
isLoading={memoQuery.isLoading}
|
||||
onBackToList={() => setActivePane("memos")}
|
||||
onSaved={async (memo) => {
|
||||
queryClient.setQueryData(["memo", memo.id], { memo });
|
||||
await queryClient.invalidateQueries({ queryKey: ["memos"] });
|
||||
}}
|
||||
onDeleted={async (memoId) => {
|
||||
await deleteMemoMutation.mutateAsync(memoId);
|
||||
setSelectedMemoId(null);
|
||||
setActivePane("memos");
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const toggleMemoSelection = (current: Set<string>, memoId: string) => {
|
||||
const next = new Set(current);
|
||||
|
||||
if (next.has(memoId)) {
|
||||
next.delete(memoId);
|
||||
} else {
|
||||
next.add(memoId);
|
||||
}
|
||||
|
||||
return next;
|
||||
};
|
||||
|
||||
const NotebookPane = ({
|
||||
notebooks,
|
||||
selectedNotebookId,
|
||||
isLoading,
|
||||
onSelect,
|
||||
onCreateNotebook,
|
||||
onBackToList,
|
||||
}: {
|
||||
notebooks: Notebook[];
|
||||
selectedNotebookId: string | null;
|
||||
isLoading: boolean;
|
||||
onSelect: (notebookId: string) => void;
|
||||
onCreateNotebook: (parentId?: string | null) => void;
|
||||
onBackToList: () => void;
|
||||
}) => {
|
||||
const tree = useMemo(() => buildNotebookTree(notebooks), [notebooks]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
<header className="flex h-[calc(4rem+env(safe-area-inset-top))] shrink-0 items-end justify-between border-b border-emerald-100 px-4 pb-3 pt-[env(safe-area-inset-top)] lg:h-16 lg:items-center lg:pb-0 lg:pt-0">
|
||||
<div>
|
||||
<div className="text-base font-semibold tracking-normal lg:hidden">笔记本</div>
|
||||
<div className="hidden text-base font-semibold tracking-normal lg:block">EdgeEver</div>
|
||||
<div className="text-xs text-slate-500">Cloudflare-native notes</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button className="lg:hidden" size="icon" variant="ghost" title="返回笔记列表" onClick={onBackToList}>
|
||||
<LayoutList className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" title="新建笔记本" onClick={() => onCreateNotebook(null)}>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-3 py-4">
|
||||
<div className="mb-3 flex items-center gap-2 px-2 text-xs font-semibold uppercase text-slate-500">
|
||||
<Folder className="h-4 w-4" />
|
||||
Notebooks
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="px-2 py-3 text-sm text-slate-500">加载中</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{tree.map((node) => (
|
||||
<NotebookTreeItem
|
||||
key={node.id}
|
||||
node={node}
|
||||
depth={0}
|
||||
selectedNotebookId={selectedNotebookId}
|
||||
onSelect={onSelect}
|
||||
onCreateNotebook={onCreateNotebook}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<footer className="border-t border-emerald-100 px-4 pb-[max(0.75rem,env(safe-area-inset-bottom))] pt-3">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<Button size="icon" variant="ghost" title="标签">
|
||||
<Tags className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" title="资产">
|
||||
<Archive className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" title="设置">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const NotebookTreeItem = ({
|
||||
node,
|
||||
depth,
|
||||
selectedNotebookId,
|
||||
onSelect,
|
||||
onCreateNotebook,
|
||||
}: {
|
||||
node: NotebookNode;
|
||||
depth: number;
|
||||
selectedNotebookId: string | null;
|
||||
onSelect: (notebookId: string) => void;
|
||||
onCreateNotebook: (parentId?: string | null) => void;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(true);
|
||||
const hasChildren = node.children.length > 0;
|
||||
const selected = node.id === selectedNotebookId;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={cn(
|
||||
"group flex h-9 items-center gap-1 rounded-md px-2 text-sm transition",
|
||||
selected ? "border border-emerald-200 bg-emerald-100 text-emerald-950" : "text-slate-700 hover:bg-emerald-50"
|
||||
)}
|
||||
style={{ paddingLeft: `${8 + depth * 14}px` }}
|
||||
>
|
||||
<button
|
||||
className="flex h-6 w-5 items-center justify-center rounded"
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
title={hasChildren ? "展开/折叠" : undefined}
|
||||
>
|
||||
{hasChildren ? (
|
||||
open ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />
|
||||
) : (
|
||||
<span className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
<button className="flex min-w-0 flex-1 items-center gap-2 text-left" onClick={() => onSelect(node.id)}>
|
||||
{node.slug === "inbox" ? <Inbox className="h-4 w-4 shrink-0" /> : <Folder className="h-4 w-4 shrink-0" />}
|
||||
<span className="truncate">{node.name}</span>
|
||||
</button>
|
||||
<button
|
||||
className={cn(
|
||||
"hidden h-6 w-6 items-center justify-center rounded-md group-hover:flex",
|
||||
selected ? "hover:bg-emerald-200" : "hover:bg-emerald-100"
|
||||
)}
|
||||
title="新建子笔记本"
|
||||
onClick={() => onCreateNotebook(node.id)}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{hasChildren && open ? (
|
||||
<div className="mt-1 space-y-1">
|
||||
{node.children.map((child) => (
|
||||
<NotebookTreeItem
|
||||
key={child.id}
|
||||
node={child}
|
||||
depth={depth + 1}
|
||||
selectedNotebookId={selectedNotebookId}
|
||||
onSelect={onSelect}
|
||||
onCreateNotebook={onCreateNotebook}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MemoListPane = ({
|
||||
notebook,
|
||||
memos,
|
||||
selectedMemoId,
|
||||
selectedMemoIds,
|
||||
search,
|
||||
isLoading,
|
||||
isCreating,
|
||||
isMerging,
|
||||
multiSelectKeyDown,
|
||||
onBackToNotebooks,
|
||||
onSearch,
|
||||
onCreateMemo,
|
||||
onOpenMemo,
|
||||
onToggleMemo,
|
||||
onMerge,
|
||||
}: {
|
||||
notebook: Notebook | null;
|
||||
memos: MemoSummary[];
|
||||
selectedMemoId: string | null;
|
||||
selectedMemoIds: Set<string>;
|
||||
search: string;
|
||||
isLoading: boolean;
|
||||
isCreating: boolean;
|
||||
isMerging: boolean;
|
||||
multiSelectKeyDown: boolean;
|
||||
onBackToNotebooks: () => void;
|
||||
onSearch: (value: string) => void;
|
||||
onCreateMemo: () => void;
|
||||
onOpenMemo: (memoId: string) => void;
|
||||
onToggleMemo: (memoId: string) => void;
|
||||
onMerge: () => void;
|
||||
}) => (
|
||||
<div className="relative flex h-full min-h-0 flex-col">
|
||||
<header className="border-b border-emerald-100 bg-white px-4 pb-3 pt-[max(0.75rem,env(safe-area-inset-top))] lg:py-3">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Button className="lg:hidden" size="icon" variant="ghost" title="打开笔记本" onClick={onBackToNotebooks}>
|
||||
<PanelLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-lg font-semibold text-slate-950 lg:text-sm">{notebook?.name ?? "全部笔记"}</div>
|
||||
<div className="text-xs text-slate-500">{memos.length} memos</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="hidden lg:inline-flex" size="icon" variant="solid" title="新建笔记" onClick={onCreateMemo} disabled={!notebook || isCreating}>
|
||||
<FilePlus2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<label className="flex h-9 items-center gap-2 rounded-md border border-emerald-100 bg-emerald-50/70 px-3 text-sm text-slate-500">
|
||||
<Search className="h-4 w-4" />
|
||||
<input
|
||||
value={search}
|
||||
onChange={(event) => onSearch(event.target.value)}
|
||||
className="min-w-0 flex-1 bg-transparent text-slate-900 outline-none placeholder:text-slate-400"
|
||||
placeholder="Search memos"
|
||||
/>
|
||||
</label>
|
||||
</header>
|
||||
|
||||
<div className="relative min-h-0 flex-1 overflow-y-auto p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]">
|
||||
{selectedMemoIds.size > 0 ? (
|
||||
<div className="sticky top-0 z-10 mb-3 flex items-center justify-between rounded-md border border-emerald-100 bg-white px-3 py-2 shadow-panel">
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
<CheckSquare className="h-4 w-4 text-emerald-700" />
|
||||
{selectedMemoIds.size} selected
|
||||
</div>
|
||||
<Button size="sm" variant="solid" onClick={onMerge} disabled={selectedMemoIds.size < 2 || isMerging}>
|
||||
<Merge className="h-4 w-4" />
|
||||
合并
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="px-2 py-4 text-sm text-slate-500">加载中</div>
|
||||
) : memos.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed border-slate-300 bg-white px-4 py-8 text-center text-sm text-slate-500">
|
||||
No memos
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{memos.map((memo) => (
|
||||
<MemoCard
|
||||
key={memo.id}
|
||||
memo={memo}
|
||||
selected={memo.id === selectedMemoId}
|
||||
checked={selectedMemoIds.has(memo.id)}
|
||||
multiSelectKeyDown={multiSelectKeyDown}
|
||||
onOpen={() => onOpenMemo(memo.id)}
|
||||
onToggle={() => onToggleMemo(memo.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{selectedMemoIds.size === 0 ? (
|
||||
<button
|
||||
className="absolute bottom-[max(1rem,env(safe-area-inset-bottom))] right-4 flex h-14 items-center gap-2 rounded-full border border-emerald-200 bg-emerald-100 px-5 text-sm font-semibold text-emerald-950 shadow-panel transition hover:bg-emerald-200 disabled:opacity-50 lg:hidden"
|
||||
title="新建笔记"
|
||||
onClick={onCreateMemo}
|
||||
disabled={!notebook || isCreating}
|
||||
>
|
||||
<FilePlus2 className="h-5 w-5" />
|
||||
新建
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
const MemoCard = ({
|
||||
memo,
|
||||
selected,
|
||||
checked,
|
||||
multiSelectKeyDown,
|
||||
onOpen,
|
||||
onToggle,
|
||||
}: {
|
||||
memo: MemoSummary;
|
||||
selected: boolean;
|
||||
checked: boolean;
|
||||
multiSelectKeyDown: boolean;
|
||||
onOpen: () => void;
|
||||
onToggle: () => void;
|
||||
}) => {
|
||||
const handledModifierPointerRef = useRef(false);
|
||||
|
||||
const shouldToggleSelection = (event: MouseEvent<HTMLElement>) =>
|
||||
event.ctrlKey || event.metaKey || multiSelectKeyDown;
|
||||
|
||||
const markModifierPointerHandled = () => {
|
||||
handledModifierPointerRef.current = true;
|
||||
window.setTimeout(() => {
|
||||
handledModifierPointerRef.current = false;
|
||||
}, 450);
|
||||
};
|
||||
|
||||
const handleModifierToggle = (event: MouseEvent<HTMLElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
markModifierPointerHandled();
|
||||
onToggle();
|
||||
};
|
||||
|
||||
const handleMouseDown = (event: MouseEvent<HTMLButtonElement>) => {
|
||||
if (shouldToggleSelection(event)) {
|
||||
handleModifierToggle(event);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
|
||||
if (handledModifierPointerRef.current) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handledModifierPointerRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldToggleSelection(event)) {
|
||||
handleModifierToggle(event);
|
||||
return;
|
||||
}
|
||||
|
||||
onOpen();
|
||||
};
|
||||
|
||||
const handleContextMenu = (event: MouseEvent<HTMLButtonElement>) => {
|
||||
if (!shouldToggleSelection(event) && !handledModifierPointerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (!handledModifierPointerRef.current) {
|
||||
markModifierPointerHandled();
|
||||
onToggle();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<article
|
||||
className={cn(
|
||||
"rounded-md border bg-white p-3 transition",
|
||||
selected ? "border-emerald-300 shadow-panel" : "border-emerald-100 hover:border-emerald-200",
|
||||
checked && "border-emerald-300 bg-emerald-50/70"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={onToggle}
|
||||
className="mt-1 h-5 w-5 shrink-0 rounded border-emerald-300 text-emerald-600"
|
||||
aria-label={`选择 ${memo.title ?? memo.excerpt}`}
|
||||
/>
|
||||
<button
|
||||
className={cn("min-w-0 flex-1 text-left", multiSelectKeyDown && "cursor-copy")}
|
||||
onMouseDown={handleMouseDown}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
title="Ctrl/Cmd 点击切换选择"
|
||||
>
|
||||
<div className="mb-1 truncate text-sm font-semibold text-slate-950">{memo.title || "Untitled memo"}</div>
|
||||
<div className="line-clamp-2 min-h-[40px] text-sm leading-5 text-slate-600">{memo.excerpt || "Empty memo"}</div>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||
<time className="text-xs text-slate-400">{formatDateTime(memo.updatedAt)}</time>
|
||||
{memo.tags.slice(0, 3).map((tag) => (
|
||||
<span key={tag} className="rounded bg-emerald-50 px-1.5 py-0.5 text-xs font-medium text-emerald-700">
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
||||
const EditorPane = ({
|
||||
memo,
|
||||
notebooks,
|
||||
isLoading,
|
||||
onBackToList,
|
||||
onSaved,
|
||||
onDeleted,
|
||||
}: {
|
||||
memo: MemoDetail | null;
|
||||
notebooks: Notebook[];
|
||||
isLoading: boolean;
|
||||
onBackToList: () => void;
|
||||
onSaved: (memo: MemoDetail) => Promise<void>;
|
||||
onDeleted: (memoId: string) => Promise<void>;
|
||||
}) => {
|
||||
const [title, setTitle] = useState("");
|
||||
const [tagsText, setTagsText] = useState("");
|
||||
const [saveState, setSaveState] = useState<"idle" | "saving" | "saved">("idle");
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Placeholder.configure({
|
||||
placeholder: "Start writing...",
|
||||
}),
|
||||
],
|
||||
content: memo?.contentJson ?? { type: "doc", content: [{ type: "paragraph" }] },
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: "prose prose-slate max-w-none",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!memo) {
|
||||
setTitle("");
|
||||
setTagsText("");
|
||||
editor?.commands.clearContent();
|
||||
return;
|
||||
}
|
||||
|
||||
setTitle(memo.title ?? "");
|
||||
setTagsText(memo.tags.join(", "));
|
||||
|
||||
if (editor) {
|
||||
editor.commands.setContent(memo.contentJson);
|
||||
}
|
||||
}, [editor, memo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor || !memo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const persistDraft = () => {
|
||||
void localDb.drafts.put({
|
||||
memoId: memo.id,
|
||||
title,
|
||||
tagsText,
|
||||
contentJson: editor.getJSON() as TiptapDoc,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
};
|
||||
|
||||
editor.on("update", persistDraft);
|
||||
return () => {
|
||||
editor.off("update", persistDraft);
|
||||
};
|
||||
}, [editor, memo, tagsText, title]);
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!memo || !editor) {
|
||||
throw new Error("No memo selected");
|
||||
}
|
||||
|
||||
return api.updateMemo(memo.id, {
|
||||
expectedRevision: memo.revision,
|
||||
title,
|
||||
contentJson: editor.getJSON() as TiptapDoc,
|
||||
tags: parseTagsText(tagsText),
|
||||
});
|
||||
},
|
||||
onMutate: () => setSaveState("saving"),
|
||||
onSuccess: async (data) => {
|
||||
await localDb.drafts.delete(data.memo.id);
|
||||
await onSaved(data.memo);
|
||||
setSaveState("saved");
|
||||
window.setTimeout(() => setSaveState("idle"), 1400);
|
||||
},
|
||||
onError: () => setSaveState("idle"),
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="flex h-full items-center justify-center text-sm text-slate-500">加载中</div>;
|
||||
}
|
||||
|
||||
if (!memo) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-white px-8 text-center">
|
||||
<div>
|
||||
<Sparkles className="mx-auto mb-3 h-8 w-8 text-slate-300" />
|
||||
<div className="text-sm font-medium text-slate-700">Select or create a memo</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-w-0 flex-col">
|
||||
<header className="shrink-0 border-b border-emerald-100 bg-white">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 px-3 py-3 sm:px-5">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Button className="lg:hidden" size="icon" variant="ghost" title="返回列表" onClick={onBackToList}>
|
||||
<LayoutList className="h-4 w-4" />
|
||||
</Button>
|
||||
<select
|
||||
value={memo.notebookId}
|
||||
className="h-8 min-w-0 max-w-[170px] rounded-md border border-emerald-100 bg-emerald-50/70 px-2 text-xs text-emerald-900 outline-none sm:max-w-none"
|
||||
onChange={(event) => {
|
||||
void api
|
||||
.updateMemo(memo.id, {
|
||||
expectedRevision: memo.revision,
|
||||
notebookId: event.target.value,
|
||||
})
|
||||
.then((data) => onSaved(data.memo));
|
||||
}}
|
||||
>
|
||||
{notebooks.map((notebook) => (
|
||||
<option key={notebook.id} value={notebook.id}>
|
||||
{notebook.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" variant="ghost" title="删除笔记" onClick={() => void onDeleted(memo.id)}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="solid" onClick={() => saveMutation.mutate()} disabled={!editor || saveMutation.isPending}>
|
||||
<Save className="h-4 w-4" />
|
||||
{saveState === "saving" ? "保存中" : saveState === "saved" ? "已保存" : "保存"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 px-4 pb-4 sm:px-7">
|
||||
<input
|
||||
value={title}
|
||||
onChange={(event) => setTitle(event.target.value)}
|
||||
className="block w-full border-0 bg-transparent text-2xl font-semibold leading-tight text-slate-950 outline-none placeholder:text-slate-300 sm:text-3xl"
|
||||
placeholder="Untitled memo"
|
||||
/>
|
||||
<label className="flex h-8 items-center gap-2 text-sm text-slate-500">
|
||||
<Tags className="h-4 w-4" />
|
||||
<input
|
||||
value={tagsText}
|
||||
onChange={(event) => setTagsText(event.target.value)}
|
||||
className="min-w-0 flex-1 bg-transparent outline-none placeholder:text-slate-400"
|
||||
placeholder="tags"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="edgeever-editor min-h-0 flex-1 overflow-y-auto">
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ButtonHTMLAttributes } from "react";
|
||||
|
||||
type ButtonVariant = "solid" | "soft" | "ghost" | "outline" | "danger";
|
||||
type ButtonSize = "sm" | "md" | "icon";
|
||||
|
||||
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
|
||||
const variantClassName: Record<ButtonVariant, string> = {
|
||||
solid: "bg-emerald-100 text-emerald-950 hover:bg-emerald-200 border-emerald-200",
|
||||
soft: "bg-emerald-50 text-emerald-950 hover:bg-emerald-100 border-emerald-100",
|
||||
ghost: "bg-transparent text-emerald-800 hover:bg-emerald-50 hover:text-emerald-950 border-transparent",
|
||||
outline: "bg-white text-emerald-900 hover:bg-emerald-50 border-emerald-200",
|
||||
danger: "bg-rose-50 text-rose-700 hover:bg-rose-100 border-rose-100",
|
||||
};
|
||||
|
||||
const sizeClassName: Record<ButtonSize, string> = {
|
||||
sm: "h-8 gap-1.5 px-2.5 text-xs",
|
||||
md: "h-9 gap-2 px-3 text-sm",
|
||||
icon: "h-8 w-8 justify-center p-0",
|
||||
};
|
||||
|
||||
export const Button = ({ className, variant = "soft", size = "md", ...props }: ButtonProps) => (
|
||||
<button
|
||||
className={cn(
|
||||
"inline-flex shrink-0 items-center rounded-md border font-medium transition disabled:pointer-events-none disabled:opacity-50",
|
||||
variantClassName[variant],
|
||||
sizeClassName[size],
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { MemoDetail, MemoSummary, Notebook, TiptapDoc } from "@edgeever/shared";
|
||||
|
||||
type ListNotebooksResponse = {
|
||||
notebooks: Notebook[];
|
||||
};
|
||||
|
||||
type ListMemosResponse = {
|
||||
memos: MemoSummary[];
|
||||
};
|
||||
|
||||
type MemoResponse = {
|
||||
memo: MemoDetail;
|
||||
};
|
||||
|
||||
type NotebookResponse = {
|
||||
notebook: Notebook;
|
||||
};
|
||||
|
||||
const request = async <T>(path: string, init?: RequestInit): Promise<T> => {
|
||||
const response = await fetch(path, {
|
||||
...init,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...init?.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => null);
|
||||
const message =
|
||||
body && typeof body === "object" && "error" in body
|
||||
? (body as { error?: { message?: string } }).error?.message
|
||||
: response.statusText;
|
||||
|
||||
throw new Error(message || "Request failed");
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
};
|
||||
|
||||
export const api = {
|
||||
listNotebooks: () => request<ListNotebooksResponse>("/api/v1/notebooks"),
|
||||
|
||||
createNotebook: (payload: { name: string; parentId?: string | null }) =>
|
||||
request<NotebookResponse>("/api/v1/notebooks", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
|
||||
listMemos: (params: { notebookId?: string | null; q?: string }) => {
|
||||
const search = new URLSearchParams();
|
||||
|
||||
if (params.notebookId) {
|
||||
search.set("notebookId", params.notebookId);
|
||||
}
|
||||
|
||||
if (params.q?.trim()) {
|
||||
search.set("q", params.q.trim());
|
||||
}
|
||||
|
||||
return request<ListMemosResponse>(`/api/v1/memos?${search.toString()}`);
|
||||
},
|
||||
|
||||
createMemo: (payload: { notebookId: string; title?: string; contentMarkdown?: string; tags?: string[] }) =>
|
||||
request<MemoResponse>("/api/v1/memos", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
|
||||
getMemo: (memoId: string) => request<MemoResponse>(`/api/v1/memos/${memoId}`),
|
||||
|
||||
updateMemo: (
|
||||
memoId: string,
|
||||
payload: {
|
||||
expectedRevision?: number;
|
||||
notebookId?: string;
|
||||
title?: string;
|
||||
contentJson?: TiptapDoc;
|
||||
contentMarkdown?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
) =>
|
||||
request<MemoResponse>(`/api/v1/memos/${memoId}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
|
||||
deleteMemo: (memoId: string) =>
|
||||
request<{ ok: true }>(`/api/v1/memos/${memoId}`, {
|
||||
method: "DELETE",
|
||||
}),
|
||||
|
||||
mergeMemos: (payload: { memoIds: string[]; notebookId?: string; title?: string }) =>
|
||||
request<MemoResponse>("/api/v1/memos/merge", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { TiptapDoc } from "@edgeever/shared";
|
||||
import Dexie, { type Table } from "dexie";
|
||||
|
||||
export type LocalDraft = {
|
||||
memoId: string;
|
||||
title: string;
|
||||
contentJson: TiptapDoc;
|
||||
tagsText: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
class EdgeEverLocalDb extends Dexie {
|
||||
drafts!: Table<LocalDraft, string>;
|
||||
|
||||
constructor() {
|
||||
super("edgeever-local");
|
||||
this.version(1).stores({
|
||||
drafts: "memoId, updatedAt",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const localDb = new EdgeEverLocalDb();
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { Notebook } from "@edgeever/shared";
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export type NotebookNode = Notebook & {
|
||||
children: NotebookNode[];
|
||||
};
|
||||
|
||||
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
|
||||
|
||||
export const buildNotebookTree = (notebooks: Notebook[]): NotebookNode[] => {
|
||||
const nodes = new Map<string, NotebookNode>();
|
||||
|
||||
for (const notebook of notebooks) {
|
||||
nodes.set(notebook.id, { ...notebook, children: [] });
|
||||
}
|
||||
|
||||
const roots: NotebookNode[] = [];
|
||||
|
||||
for (const node of nodes.values()) {
|
||||
if (node.parentId && nodes.has(node.parentId)) {
|
||||
nodes.get(node.parentId)?.children.push(node);
|
||||
} else {
|
||||
roots.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
const sortNodes = (items: NotebookNode[]) => {
|
||||
items.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name));
|
||||
items.forEach((item) => sortNodes(item.children));
|
||||
};
|
||||
|
||||
sortNodes(roots);
|
||||
return roots;
|
||||
};
|
||||
|
||||
export const formatDateTime = (value: string) =>
|
||||
new Intl.DateTimeFormat("zh-CN", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).format(new Date(value));
|
||||
|
||||
export const parseTagsText = (value: string) =>
|
||||
value
|
||||
.split(/[,,\s]+/)
|
||||
.map((tag) => tag.trim().replace(/^#/, ""))
|
||||
.filter(Boolean);
|
||||
@@ -0,0 +1,29 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { App } from "./app/App";
|
||||
import "./styles/globals.css";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1,
|
||||
staleTime: 15_000,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const root = document.getElementById("root");
|
||||
|
||||
if (!root) {
|
||||
throw new Error("Root element not found");
|
||||
}
|
||||
|
||||
createRoot(root).render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,88 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(240, 253, 244, 0.98), rgba(236, 253, 245, 0.98)),
|
||||
#f0fdf4;
|
||||
color: #0f172a;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
min-height: 100%;
|
||||
outline: none;
|
||||
padding: 24px 28px 56px;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.ProseMirror {
|
||||
padding: 18px 16px max(4rem, env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 760;
|
||||
line-height: 1.15;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.ProseMirror h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 720;
|
||||
line-height: 1.2;
|
||||
margin: 1.4rem 0 0.7rem;
|
||||
}
|
||||
|
||||
.ProseMirror h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin: 1.2rem 0 0.6rem;
|
||||
}
|
||||
|
||||
.ProseMirror p {
|
||||
line-height: 1.8;
|
||||
margin: 0 0 0.95rem;
|
||||
}
|
||||
|
||||
.ProseMirror ul,
|
||||
.ProseMirror ol {
|
||||
padding-left: 1.5rem;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.ProseMirror li {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.ProseMirror p.is-editor-empty:first-child::before {
|
||||
color: #94a3b8;
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
export default {
|
||||
content: {
|
||||
relative: true,
|
||||
files: ["./index.html", "./src/**/*.{ts,tsx}"],
|
||||
},
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: [
|
||||
"Inter",
|
||||
"ui-sans-serif",
|
||||
"system-ui",
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
"Segoe UI",
|
||||
"sans-serif",
|
||||
],
|
||||
},
|
||||
boxShadow: {
|
||||
panel: "0 1px 2px rgb(15 23 42 / 0.08), 0 12px 30px rgb(15 23 42 / 0.06)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
} satisfies Config;
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "vite.config.ts", "tailwind.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
root: "apps/web",
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://127.0.0.1:8787",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/mcp": {
|
||||
target: "http://127.0.0.1:8787",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: "dist",
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,651 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "edgeever",
|
||||
"dependencies": {
|
||||
"@edgeever/shared": "workspace:*",
|
||||
"@hono/zod-validator": "latest",
|
||||
"@tanstack/react-query": "latest",
|
||||
"@tiptap/extension-placeholder": "latest",
|
||||
"@tiptap/react": "latest",
|
||||
"@tiptap/starter-kit": "latest",
|
||||
"class-variance-authority": "latest",
|
||||
"clsx": "latest",
|
||||
"dexie": "latest",
|
||||
"hono": "latest",
|
||||
"lucide-react": "latest",
|
||||
"react": "latest",
|
||||
"react-dom": "latest",
|
||||
"tailwind-merge": "latest",
|
||||
"zod": "latest",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "latest",
|
||||
"@types/node": "latest",
|
||||
"@types/react": "latest",
|
||||
"@types/react-dom": "latest",
|
||||
"@vitejs/plugin-react": "latest",
|
||||
"autoprefixer": "latest",
|
||||
"concurrently": "latest",
|
||||
"postcss": "latest",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "latest",
|
||||
"vite": "latest",
|
||||
"wrangler": "latest",
|
||||
},
|
||||
},
|
||||
"apps/api": {
|
||||
"name": "@edgeever/api",
|
||||
"version": "0.1.0",
|
||||
},
|
||||
"apps/web": {
|
||||
"name": "@edgeever/web",
|
||||
"version": "0.1.0",
|
||||
},
|
||||
"packages/shared": {
|
||||
"name": "@edgeever/shared",
|
||||
"version": "0.1.0",
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
|
||||
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="],
|
||||
|
||||
"@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="],
|
||||
|
||||
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260625.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-naCfBv0WnnTQIQPTniqMoUlklOIFjrAcSn1X+IAOhY8aFLF/xGYtFjs1eEE8sFib3ZuChGGpU23FFORVczqr0A=="],
|
||||
|
||||
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260625.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jmH6zjp6Wrux46+qtFwDwrj+vd7s5bdwEqeGvdnwE0a4IEeAhKs0L42HQOyID+g5lkrHq9m55+AbhtmRAm63Pw=="],
|
||||
|
||||
"@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260625.1", "", { "os": "linux", "cpu": "x64" }, "sha512-MiQkpA/dX8d83Zp64pzHUKfd6ca4cvwxnNobSP6CnXvfESvnNI9pfa+nfwnParla36sPmnYntNkjR7NjRuDeKQ=="],
|
||||
|
||||
"@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260625.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LxxW7Qv60Xvv37+w6gUSDpYZziyqMy+cZWd9IvSA5ehVgKAxmzEaYPMiSZlxk32nbIWL9u/tfjXYCOKJ4Lo+XQ=="],
|
||||
|
||||
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260625.1", "", { "os": "win32", "cpu": "x64" }, "sha512-LH6iIX1HHaTwVKV5VokDxxUErXJzQoNZFRwVm7Vx/3fB/ApcTcRCUaMqcxI4as94jEUqg+pmX5czOndiveohow=="],
|
||||
|
||||
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260626.1", "", {}, "sha512-fBnpQyFRS3Ce1l2IUd3k+aUxgy/7VMlVXF4F672/eSrpXFeezCy3Ha6Z2uTyGgqu9sGvQPOj8nqKBv2yeI+ciw=="],
|
||||
|
||||
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
|
||||
|
||||
"@edgeever/api": ["@edgeever/api@workspace:apps/api"],
|
||||
|
||||
"@edgeever/shared": ["@edgeever/shared@workspace:packages/shared"],
|
||||
|
||||
"@edgeever/web": ["@edgeever/web@workspace:apps/web"],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
|
||||
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
|
||||
|
||||
"@hono/zod-validator": ["@hono/zod-validator@0.8.0", "", { "peerDependencies": { "hono": ">=4.10.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-5uS4S1/LKtZQYvD4BtpPUFkOv8d1wNxHHrChm26buMiEYc1FrHWvDUaKVBwkiVtvSExHSpLGDvcnpI2Copyj9w=="],
|
||||
|
||||
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
|
||||
|
||||
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
|
||||
|
||||
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
|
||||
|
||||
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
|
||||
|
||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
|
||||
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
|
||||
|
||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
|
||||
|
||||
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
|
||||
|
||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.137.0", "", {}, "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA=="],
|
||||
|
||||
"@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="],
|
||||
|
||||
"@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="],
|
||||
|
||||
"@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.3", "", { "os": "android", "cpu": "arm64" }, "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.3", "", { "os": "linux", "cpu": "arm" }, "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w=="],
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw=="],
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g=="],
|
||||
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.3", "", { "os": "none", "cpu": "arm64" }, "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.3", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.3", "", { "os": "win32", "cpu": "x64" }, "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||
|
||||
"@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="],
|
||||
|
||||
"@speed-highlight/core": ["@speed-highlight/core@1.2.17", "", {}, "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.101.1", "", {}, "sha512-Y6Y92dkXtNqx67m2pMSxUsA3zOCwv862JexZRP8/EPwvKXMPu9m8rv43spiXWzOUIggQ3SQApttALStzhA8B4g=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.101.1", "", { "dependencies": { "@tanstack/query-core": "5.101.1" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-ZnONUuQKJe1bJMStXUL1s5uKN9FcfC28j5cK+iDZcdSHtUv1wtin1cGc/Oewhf2Oc4eKY7lggtpvT/AbMmhHew=="],
|
||||
|
||||
"@tiptap/core": ["@tiptap/core@3.27.1", "", { "peerDependencies": { "@tiptap/pm": "3.27.1" } }, "sha512-rV6Qn4wmC6BxfF+4mu6bqGWj9vA4oXXhsrpXaJL2uhjxeHAGofjwcHof2X84VYzeyXgdlsGmqKie4TAppVXZUQ=="],
|
||||
|
||||
"@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-VMF7xJx6qEGiX6DTKNiL31NLqypOcd/4sNjFSe8rb41PwejBJh/nOqVIbBvWkiT6NMGFLxMhj7zJ8/zPo1hXeg=="],
|
||||
|
||||
"@tiptap/extension-bold": ["@tiptap/extension-bold@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-TlC5bsS+pqETTrlz4CZz9RO/cKBYtELGIxwtKeivUn3eNfnOxQbbu4WDsiwIfzRFyd0OMnKl6BPM2KnYEehoEQ=="],
|
||||
|
||||
"@tiptap/extension-bubble-menu": ["@tiptap/extension-bubble-menu@3.27.1", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-j/j8Qp9Z5nViade2m7zjrO/CYH/Ca80Qj7aqo0eUaei6FZQ5izlF9o4XQU5EFMAutV6mwynsPUp8FVo5sCuYfw=="],
|
||||
|
||||
"@tiptap/extension-bullet-list": ["@tiptap/extension-bullet-list@3.27.1", "", { "peerDependencies": { "@tiptap/extension-list": "3.27.1" } }, "sha512-faCUHnRP47o9Zh9VZZX6EX/569udw9Vopm2PgEKPWuKLE2qaS5WBuUVU0iItdJmKUqaWiOZkpoW4jvnDmj0dfg=="],
|
||||
|
||||
"@tiptap/extension-code": ["@tiptap/extension-code@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-epOUpFfEmBzjvnqvjv2qHX7NAuLo5dlOGV690lWu+sAYMjibuJBeVvAiKPyFCfRCCTUxdbDB3jbaOA1yEcEJ7w=="],
|
||||
|
||||
"@tiptap/extension-code-block": ["@tiptap/extension-code-block@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-pHlzmZx2OlHfyQ0yRlT5UL4mGokz947DthZuYefN1OleVqOkHpWBG+2JQwqoNq6bmzMne92zbH32rhcJUEYSjA=="],
|
||||
|
||||
"@tiptap/extension-document": ["@tiptap/extension-document@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-8FbBTkfnRP4iVaoj+2h3iWa+H0eGDD3yTyVCwrmue/sQTkqUNUoSuAZa3GDG4Sd41xdPwTJxl9nUWGgM1qDCnw=="],
|
||||
|
||||
"@tiptap/extension-dropcursor": ["@tiptap/extension-dropcursor@3.27.1", "", { "peerDependencies": { "@tiptap/extensions": "3.27.1" } }, "sha512-blFf9x9RG0Qr7P3FoAH/033ffa+mMLZn34trVs8Vi0Ppk6FmJAg5HpYFOtmYoeREdNDJ5rHJKV7SoACbOHgskQ=="],
|
||||
|
||||
"@tiptap/extension-floating-menu": ["@tiptap/extension-floating-menu@3.27.1", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-BmJF1VqB7dSJkgAalrpVFj88WLhxKjcWPuWHOqf2ITrUU2832BhKLXKmxjWUy1gqV8PfNNVWtGfIERy7I0y0+Q=="],
|
||||
|
||||
"@tiptap/extension-gapcursor": ["@tiptap/extension-gapcursor@3.27.1", "", { "peerDependencies": { "@tiptap/extensions": "3.27.1" } }, "sha512-QoezN0wdvXIwLQ4ee2ccWDaX3RG0lzgQpIMpMz55oPDhpUVax1+19ApsS53LkcktpS4EbnPL4xO4DaJk0Vp7PQ=="],
|
||||
|
||||
"@tiptap/extension-hard-break": ["@tiptap/extension-hard-break@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-iv/m9hzl6jfSj9Q8UEjAxONvCoUDaP7M9SRCPx3PaLNxA230TTD6RE0Ye4zFJ8ze7ZVoJJMAqg9Qpq1iYg2JOQ=="],
|
||||
|
||||
"@tiptap/extension-heading": ["@tiptap/extension-heading@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-SrC4l1kEIyv9ZXFaI/8LQqU2MyMmjczw7XXsWUQOTN4YXv0JyVgMNR3cI/wz0d2xsTfBdZ1N85Tdng+Ga1t0Sg=="],
|
||||
|
||||
"@tiptap/extension-horizontal-rule": ["@tiptap/extension-horizontal-rule@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-QlKE7qn5qMnIGVGhXQlvYedvLtNJ9z0dmit5w8vPb8tKzW4Spk6M7N2kruprrDA8GBwHfeR5wmF+njfUm34qxg=="],
|
||||
|
||||
"@tiptap/extension-italic": ["@tiptap/extension-italic@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-jGGeyn9uRUnNjSTHpbqhiGsp6KaYTSbV09jDXPJI9cDwfV9hpugLvpaCZd0BMBbhU1B1W6kOfX0BE15qX/HQfA=="],
|
||||
|
||||
"@tiptap/extension-link": ["@tiptap/extension-link@3.27.1", "", { "dependencies": { "linkifyjs": "^4.3.3" }, "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-/2jBfsxBZUDGJmpZifqRQPz7f1E5qpS1BckTZ39TADzUJX+feKy7RJ3DtQ02+8y6SSMzvP9loGVjrk6zEMTk4g=="],
|
||||
|
||||
"@tiptap/extension-list": ["@tiptap/extension-list@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-c2Upru7lj0/ZV/Ibww6cNz6sUS8m6Dp/9uygFhYcZOd3X8M0xBIEk42c6m6SQehkPziVA8QOgNJz7sMqsbz1OQ=="],
|
||||
|
||||
"@tiptap/extension-list-item": ["@tiptap/extension-list-item@3.27.1", "", { "peerDependencies": { "@tiptap/extension-list": "3.27.1" } }, "sha512-zwRl01ETfCkWUvtvK5fw9bXtAajMPkvlkE3Cq6JvH3LF7XXJwDtNj5Tj7exacMpCaSZmlNc43vFb2rAYnrnwMA=="],
|
||||
|
||||
"@tiptap/extension-list-keymap": ["@tiptap/extension-list-keymap@3.27.1", "", { "peerDependencies": { "@tiptap/extension-list": "3.27.1" } }, "sha512-OIMZNlzPSO8WRd4ic73Fxckzl4N1tesjjLL2XApaNA/uMpO0LoF6WSRPAWv+Z24Wp92ARRJAnRP7iZoI5+Jxig=="],
|
||||
|
||||
"@tiptap/extension-ordered-list": ["@tiptap/extension-ordered-list@3.27.1", "", { "peerDependencies": { "@tiptap/extension-list": "3.27.1" } }, "sha512-GYrKqD//9nHJ2r80uXqbDMzRnFpGzbaEQRTSGaO/SH7DvXWFMow8evkOdjQ7PCQO07jNjJo75+A85Jwu3Ov3AA=="],
|
||||
|
||||
"@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-7K7eo1gruOgAsnbK+GCV23AUVUI0cL1bTig8HaPneoFMVbig7vddk8jNLKBWO8TXVbG7TuHdnDN4F98vdtwh5Q=="],
|
||||
|
||||
"@tiptap/extension-placeholder": ["@tiptap/extension-placeholder@3.27.1", "", { "peerDependencies": { "@tiptap/extensions": "3.27.1" } }, "sha512-lhcNDcczQ75yJOSywCHb58Hmtg1aPL/2TdYmeLPxVrP048D7rRs133sfONcgyyw0AvhnfmPOkHLTv3QtKSowhw=="],
|
||||
|
||||
"@tiptap/extension-strike": ["@tiptap/extension-strike@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-Y3DW1jlSlCNCyMGHP3+3qBNNPS83wuFz4RTYGjZtvRRTCRh7apZme9XRWMq1rN5mJ2Cr7fKocA2/5Bs13KgN6Q=="],
|
||||
|
||||
"@tiptap/extension-text": ["@tiptap/extension-text@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-6ZwaZwSrDh+KFFv6V1J79oO37yPs7y1bFxvk1/9Ih2rn3Xr5AWz+eMS+n8RpH3djBVVAQpdIAeYQgcn+VCSsTg=="],
|
||||
|
||||
"@tiptap/extension-underline": ["@tiptap/extension-underline@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-N889J4nXN/TPfVt8uF9N1A0SY82E90zwc1y26lqOcw6KWNLmQrlhMh/9OD4ikLDbekmFpOBq/UicpHf/6S8hbQ=="],
|
||||
|
||||
"@tiptap/extensions": ["@tiptap/extensions@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-1Tdx9faw8k0/83V6X+xCDVhV8yElGt95JxeW3YMkKQJI56QdlPz0xOdJPlMiSGJKinPyVier+x9LJD/YZUZIaw=="],
|
||||
|
||||
"@tiptap/pm": ["@tiptap/pm@3.27.1", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.3", "prosemirror-model": "^1.25.7", "prosemirror-schema-list": "^1.5.0", "prosemirror-state": "^1.4.4", "prosemirror-tables": "^1.8.0", "prosemirror-transform": "^1.12.0", "prosemirror-view": "^1.41.8" } }, "sha512-Ffjx+vimmBU7zH/KrpXzJid3+pziCe/VL2aexSTP63cyQwKQ65LkFkCKaIsSpFdQQuakVZBGWjCA5RoBV852pw=="],
|
||||
|
||||
"@tiptap/react": ["@tiptap/react@3.27.1", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "fast-equals": "^5.3.3", "use-sync-external-store": "^1.4.0" }, "optionalDependencies": { "@tiptap/extension-bubble-menu": "^3.27.1", "@tiptap/extension-floating-menu": "^3.27.1" }, "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-/Wn2fc9zMtX08MXYScDFsm4wJ8lzfhfPEdbtls7WCDlbtrop48PWlkHDBBJrywARfAQTB2mFs9KiFy9yrQm5Lg=="],
|
||||
|
||||
"@tiptap/starter-kit": ["@tiptap/starter-kit@3.27.1", "", { "dependencies": { "@tiptap/core": "^3.27.1", "@tiptap/extension-blockquote": "^3.27.1", "@tiptap/extension-bold": "^3.27.1", "@tiptap/extension-bullet-list": "^3.27.1", "@tiptap/extension-code": "^3.27.1", "@tiptap/extension-code-block": "^3.27.1", "@tiptap/extension-document": "^3.27.1", "@tiptap/extension-dropcursor": "^3.27.1", "@tiptap/extension-gapcursor": "^3.27.1", "@tiptap/extension-hard-break": "^3.27.1", "@tiptap/extension-heading": "^3.27.1", "@tiptap/extension-horizontal-rule": "^3.27.1", "@tiptap/extension-italic": "^3.27.1", "@tiptap/extension-link": "^3.27.1", "@tiptap/extension-list": "^3.27.1", "@tiptap/extension-list-item": "^3.27.1", "@tiptap/extension-list-keymap": "^3.27.1", "@tiptap/extension-ordered-list": "^3.27.1", "@tiptap/extension-paragraph": "^3.27.1", "@tiptap/extension-strike": "^3.27.1", "@tiptap/extension-text": "^3.27.1", "@tiptap/extension-underline": "^3.27.1", "@tiptap/extensions": "^3.27.1", "@tiptap/pm": "^3.27.1" } }, "sha512-vfxRsqW8rCc0k4pzo0ilU3wobVi2wqVj88VZI2SlgZlNnUAkrDGDIAph7CTa9k9fshV+O1ivpEgPC5yC046jow=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||
|
||||
"@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.3", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
|
||||
|
||||
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
||||
|
||||
"autoprefixer": ["autoprefixer@10.5.2", "", { "dependencies": { "browserslist": "^4.28.4", "caniuse-lite": "^1.0.30001799", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.40", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw=="],
|
||||
|
||||
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||
|
||||
"blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.4", "", { "dependencies": { "baseline-browser-mapping": "^2.10.38", "caniuse-lite": "^1.0.30001799", "electron-to-chromium": "^1.5.376", "node-releases": "^2.0.48", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw=="],
|
||||
|
||||
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="],
|
||||
|
||||
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||
|
||||
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||
|
||||
"cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||
|
||||
"concurrently": ["concurrently@10.0.3", "", { "dependencies": { "chalk": "5.6.2", "rxjs": "7.8.2", "shell-quote": "1.8.4", "supports-color": "10.2.2", "tree-kill": "1.2.2", "yargs": "18.0.0" }, "bin": { "conc": "dist/bin/index.js", "concurrently": "dist/bin/index.js" } }, "sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA=="],
|
||||
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"dexie": ["dexie@4.4.4", "", {}, "sha512-jIwsYI8Os2hgnqc6O49YwFDKGc5v5QjGx0wPVp543ip1F53VFAKMLthV2pQosQcVTv3eAskTWYspOx195PM0FQ=="],
|
||||
|
||||
"didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="],
|
||||
|
||||
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.379", "", {}, "sha512-v/qV5aV5EUA2pGilzUCq5/eyOloZAqDZBu9UMBIzgPpLlprjSR6zswsWBTv0KpqxLGUAZEwhO95ZCt7srymNVA=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
|
||||
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
|
||||
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||
|
||||
"hono": ["hono@4.12.27", "", {}, "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q=="],
|
||||
|
||||
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
|
||||
|
||||
"is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
|
||||
"jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||
|
||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||
|
||||
"linkifyjs": ["linkifyjs@4.3.3", "", {}, "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg=="],
|
||||
|
||||
"lucide-react": ["lucide-react@1.21.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"miniflare": ["miniflare@4.20260625.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", "workerd": "1.20260625.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-3kKXwRUObJsnBYPBgR0NiNZYKF/yv8GFyha1cx2EeAEraxNODgRVcyeRo+F1ok1tg5Mg7iUpOWSkknQTHuFhwA=="],
|
||||
|
||||
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.50", "", {}, "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
|
||||
|
||||
"orderedmap": ["orderedmap@2.1.1", "", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="],
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
|
||||
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
|
||||
|
||||
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
|
||||
|
||||
"postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
|
||||
|
||||
"postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="],
|
||||
|
||||
"postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="],
|
||||
|
||||
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
|
||||
|
||||
"postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="],
|
||||
|
||||
"postcss-selector-parser": ["postcss-selector-parser@6.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ=="],
|
||||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"prosemirror-changeset": ["prosemirror-changeset@2.4.1", "", { "dependencies": { "prosemirror-transform": "^1.0.0" } }, "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw=="],
|
||||
|
||||
"prosemirror-commands": ["prosemirror-commands@1.7.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.10.2" } }, "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w=="],
|
||||
|
||||
"prosemirror-dropcursor": ["prosemirror-dropcursor@1.8.2", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0", "prosemirror-view": "^1.1.0" } }, "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw=="],
|
||||
|
||||
"prosemirror-gapcursor": ["prosemirror-gapcursor@1.4.1", "", { "dependencies": { "prosemirror-keymap": "^1.0.0", "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-view": "^1.0.0" } }, "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw=="],
|
||||
|
||||
"prosemirror-history": ["prosemirror-history@1.5.0", "", { "dependencies": { "prosemirror-state": "^1.2.2", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.31.0", "rope-sequence": "^1.3.0" } }, "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg=="],
|
||||
|
||||
"prosemirror-inputrules": ["prosemirror-inputrules@1.5.1", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw=="],
|
||||
|
||||
"prosemirror-keymap": ["prosemirror-keymap@1.2.3", "", { "dependencies": { "prosemirror-state": "^1.0.0", "w3c-keyname": "^2.2.0" } }, "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw=="],
|
||||
|
||||
"prosemirror-model": ["prosemirror-model@1.25.9", "", { "dependencies": { "orderedmap": "^2.0.0" } }, "sha512-pRTklkDDMMRopyoAcrr9wV/8g/RYgrLHBuJAb5hlEuYZRdm5yqmPjWId83fpBwPpSFqEdja0H7Dfd7z1X/npcA=="],
|
||||
|
||||
"prosemirror-schema-list": ["prosemirror-schema-list@1.5.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.7.3" } }, "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q=="],
|
||||
|
||||
"prosemirror-state": ["prosemirror-state@1.4.4", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.27.0" } }, "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw=="],
|
||||
|
||||
"prosemirror-tables": ["prosemirror-tables@1.8.5", "", { "dependencies": { "prosemirror-keymap": "^1.2.3", "prosemirror-model": "^1.25.4", "prosemirror-state": "^1.4.4", "prosemirror-transform": "^1.10.5", "prosemirror-view": "^1.41.4" } }, "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw=="],
|
||||
|
||||
"prosemirror-transform": ["prosemirror-transform@1.12.0", "", { "dependencies": { "prosemirror-model": "^1.21.0" } }, "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w=="],
|
||||
|
||||
"prosemirror-view": ["prosemirror-view@1.41.9", "", { "dependencies": { "prosemirror-model": "^1.25.8", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-clTunTX+eaLbr87L1V1QPheRlEQJyTlL3gXe9x3jQIk3rL0RVWxviDGz8tFaydwIVm+hKhYCyr+R/zBtWr9s6A=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="],
|
||||
|
||||
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
|
||||
|
||||
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rolldown": ["rolldown@1.1.3", "", { "dependencies": { "@oxc-project/types": "=0.137.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.3", "@rolldown/binding-darwin-arm64": "1.1.3", "@rolldown/binding-darwin-x64": "1.1.3", "@rolldown/binding-freebsd-x64": "1.1.3", "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", "@rolldown/binding-linux-arm64-gnu": "1.1.3", "@rolldown/binding-linux-arm64-musl": "1.1.3", "@rolldown/binding-linux-ppc64-gnu": "1.1.3", "@rolldown/binding-linux-s390x-gnu": "1.1.3", "@rolldown/binding-linux-x64-gnu": "1.1.3", "@rolldown/binding-linux-x64-musl": "1.1.3", "@rolldown/binding-openharmony-arm64": "1.1.3", "@rolldown/binding-wasm32-wasi": "1.1.3", "@rolldown/binding-win32-arm64-msvc": "1.1.3", "@rolldown/binding-win32-x64-msvc": "1.1.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g=="],
|
||||
|
||||
"rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
||||
|
||||
"shell-quote": ["shell-quote@1.8.4", "", {}, "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
|
||||
"sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
|
||||
|
||||
"supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="],
|
||||
|
||||
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
||||
|
||||
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
|
||||
|
||||
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||
|
||||
"undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="],
|
||||
|
||||
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||
|
||||
"unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"vite": ["vite@8.1.0", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "~1.1.2", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q=="],
|
||||
|
||||
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
|
||||
|
||||
"workerd": ["workerd@1.20260625.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260625.1", "@cloudflare/workerd-darwin-arm64": "1.20260625.1", "@cloudflare/workerd-linux-64": "1.20260625.1", "@cloudflare/workerd-linux-arm64": "1.20260625.1", "@cloudflare/workerd-windows-64": "1.20260625.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-GApQvFX52SDM6L4u0+RRnUDB1wJOnEwoXjinkmOPtIyofWBxrlZckdegJSYc1leg++lLZ3+DQ4zMVmBqYVtzfA=="],
|
||||
|
||||
"wrangler": ["wrangler@4.105.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "4.20260625.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260625.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260625.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "cf-wrangler": "bin/cf-wrangler.js", "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-7dXFH6OLj1Fv0y6ZeRPUxFTkp+duWD7/xxVi/1c0vfOeEYwIFKWB7cdqnY05DvY1Ta3BnqAwRkXfLs8PDj538g=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||
|
||||
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
|
||||
|
||||
"youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
|
||||
|
||||
"youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],
|
||||
|
||||
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||
|
||||
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
# EdgeEver Project Structure
|
||||
|
||||
```text
|
||||
edgeever/
|
||||
├── apps/
|
||||
│ ├── web/
|
||||
│ │ ├── public/
|
||||
│ │ │ ├── manifest.webmanifest
|
||||
│ │ │ └── icons/
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── app/
|
||||
│ │ │ │ ├── App.tsx
|
||||
│ │ │ │ └── routes.tsx
|
||||
│ │ │ ├── components/
|
||||
│ │ │ │ ├── editor/
|
||||
│ │ │ │ ├── layout/
|
||||
│ │ │ │ ├── memo-list/
|
||||
│ │ │ │ ├── notebook-tree/
|
||||
│ │ │ │ └── ui/
|
||||
│ │ │ ├── hooks/
|
||||
│ │ │ ├── lib/
|
||||
│ │ │ ├── styles/
|
||||
│ │ │ └── main.tsx
|
||||
│ │ ├── index.html
|
||||
│ │ ├── package.json
|
||||
│ │ ├── tsconfig.json
|
||||
│ │ └── vite.config.ts
|
||||
│ └── api/
|
||||
│ ├── src/
|
||||
│ │ ├── index.ts
|
||||
│ │ ├── env.ts
|
||||
│ │ ├── db/
|
||||
│ │ ├── routes/
|
||||
│ │ │ ├── memos.ts
|
||||
│ │ │ ├── notebooks.ts
|
||||
│ │ │ └── resources.ts
|
||||
│ │ └── services/
|
||||
│ │ ├── merge-memos.ts
|
||||
│ │ └── resource-store.ts
|
||||
│ ├── package.json
|
||||
│ └── tsconfig.json
|
||||
├── packages/
|
||||
│ └── shared/
|
||||
│ ├── src/
|
||||
│ │ ├── content.ts
|
||||
│ │ ├── index.ts
|
||||
│ │ ├── schemas.ts
|
||||
│ │ └── types.ts
|
||||
│ ├── package.json
|
||||
│ └── tsconfig.json
|
||||
├── migrations/
|
||||
│ └── 0001_initial.sql
|
||||
├── docs/
|
||||
│ └── project-structure.md
|
||||
├── tailwind.config.ts
|
||||
├── wrangler.toml
|
||||
├── bun.lock
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Deployment Shape
|
||||
|
||||
EdgeEver should deploy as one Cloudflare Worker:
|
||||
|
||||
- `/api/*` is routed to the Hono app first.
|
||||
- Static files from `apps/web/dist` are served by Workers Assets.
|
||||
- Unknown static routes fall back to `index.html` for SPA and PWA navigation.
|
||||
- `env.DB` is the D1 binding.
|
||||
- `env.RESOURCES` is the R2 bucket binding for images and attachments.
|
||||
|
||||
## Frontend Boundaries
|
||||
|
||||
- `components/layout` owns the responsive three-pane shell.
|
||||
- `components/notebook-tree` renders recursive notebooks from `parent_id`.
|
||||
- `components/memo-list` owns checkbox selection and merge action surfaces.
|
||||
- `components/editor` owns TipTap integration and Markdown serialization.
|
||||
- `lib/api-client.ts` should be the only browser module that talks to `/api/*`.
|
||||
|
||||
## API Boundaries
|
||||
|
||||
- `routes/*` should stay thin: validate input, call services, return JSON.
|
||||
- `services/merge-memos.ts` owns the D1 transaction that creates a merged memo, soft deletes source memos, and re-points resources.
|
||||
- `services/resource-store.ts` owns R2 object keys and upload/download URL policy.
|
||||
- `db/*` owns SQL snippets and row mapping.
|
||||
|
||||
## Data Model Notes
|
||||
|
||||
- Notebook nesting is unbounded through `notebooks.parent_id`.
|
||||
- Every memo belongs to exactly one notebook through `memos.notebook_id`.
|
||||
- Merge output is represented by a new memo with `source_memo_ids` and `merge_source_count`.
|
||||
- Merge inputs are retained as soft-deleted rows with `merged_into_memo_id`.
|
||||
- R2 objects are not moved during merge; `resources.memo_id` is updated to the new memo.
|
||||
@@ -0,0 +1,239 @@
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE notebooks (
|
||||
id TEXT PRIMARY KEY,
|
||||
parent_id TEXT,
|
||||
name TEXT NOT NULL CHECK (length(trim(name)) > 0),
|
||||
slug TEXT,
|
||||
icon TEXT,
|
||||
color TEXT,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_deleted INTEGER NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1)),
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
deleted_at TEXT,
|
||||
CHECK (parent_id IS NULL OR parent_id <> id),
|
||||
FOREIGN KEY (parent_id) REFERENCES notebooks(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_notebooks_parent
|
||||
ON notebooks(parent_id, is_deleted, sort_order, name);
|
||||
|
||||
CREATE INDEX idx_notebooks_deleted
|
||||
ON notebooks(is_deleted, updated_at);
|
||||
|
||||
CREATE TRIGGER trg_notebooks_prevent_cycles
|
||||
BEFORE UPDATE OF parent_id ON notebooks
|
||||
FOR EACH ROW
|
||||
WHEN NEW.parent_id IS NOT NULL
|
||||
BEGIN
|
||||
WITH RECURSIVE ancestors(id, parent_id) AS (
|
||||
SELECT id, parent_id
|
||||
FROM notebooks
|
||||
WHERE id = NEW.parent_id
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT n.id, n.parent_id
|
||||
FROM notebooks n
|
||||
INNER JOIN ancestors a ON n.id = a.parent_id
|
||||
WHERE a.parent_id IS NOT NULL
|
||||
)
|
||||
SELECT RAISE(ABORT, 'notebook cycle detected')
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM ancestors
|
||||
WHERE id = NEW.id
|
||||
);
|
||||
END;
|
||||
|
||||
CREATE TABLE memos (
|
||||
id TEXT PRIMARY KEY,
|
||||
notebook_id TEXT NOT NULL,
|
||||
title TEXT,
|
||||
excerpt TEXT NOT NULL DEFAULT '',
|
||||
tags_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(tags_json)),
|
||||
is_pinned INTEGER NOT NULL DEFAULT 0 CHECK (is_pinned IN (0, 1)),
|
||||
is_archived INTEGER NOT NULL DEFAULT 0 CHECK (is_archived IN (0, 1)),
|
||||
is_deleted INTEGER NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1)),
|
||||
source_memo_ids TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(source_memo_ids)),
|
||||
merge_source_count INTEGER NOT NULL DEFAULT 0 CHECK (merge_source_count >= 0),
|
||||
merged_into_memo_id TEXT,
|
||||
merged_at TEXT,
|
||||
created_by TEXT NOT NULL DEFAULT 'user',
|
||||
updated_by TEXT NOT NULL DEFAULT 'user',
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
deleted_at TEXT,
|
||||
FOREIGN KEY (notebook_id) REFERENCES notebooks(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY (merged_into_memo_id) REFERENCES memos(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_memos_notebook_feed
|
||||
ON memos(notebook_id, is_deleted, updated_at DESC);
|
||||
|
||||
CREATE INDEX idx_memos_archive_feed
|
||||
ON memos(is_archived, is_deleted, updated_at DESC);
|
||||
|
||||
CREATE INDEX idx_memos_merge_target
|
||||
ON memos(merged_into_memo_id);
|
||||
|
||||
CREATE TABLE memo_contents (
|
||||
memo_id TEXT PRIMARY KEY,
|
||||
schema_version INTEGER NOT NULL DEFAULT 1,
|
||||
content_json TEXT NOT NULL CHECK (json_valid(content_json)),
|
||||
content_markdown TEXT NOT NULL DEFAULT '',
|
||||
content_text TEXT NOT NULL DEFAULT '',
|
||||
content_hash TEXT NOT NULL,
|
||||
revision INTEGER NOT NULL DEFAULT 0 CHECK (revision >= 0),
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
FOREIGN KEY (memo_id) REFERENCES memos(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_memo_contents_revision
|
||||
ON memo_contents(memo_id, revision);
|
||||
|
||||
CREATE VIRTUAL TABLE memos_fts USING fts5(
|
||||
memo_id UNINDEXED,
|
||||
title,
|
||||
content_text,
|
||||
tags
|
||||
);
|
||||
|
||||
CREATE TABLE resources (
|
||||
id TEXT PRIMARY KEY,
|
||||
memo_id TEXT NOT NULL,
|
||||
original_memo_id TEXT,
|
||||
bucket_name TEXT NOT NULL DEFAULT 'edgeever-resources',
|
||||
object_key TEXT NOT NULL,
|
||||
kind TEXT NOT NULL CHECK (kind IN ('image', 'attachment')),
|
||||
mime_type TEXT,
|
||||
filename TEXT,
|
||||
byte_size INTEGER NOT NULL DEFAULT 0 CHECK (byte_size >= 0),
|
||||
sha256 TEXT,
|
||||
width INTEGER CHECK (width IS NULL OR width > 0),
|
||||
height INTEGER CHECK (height IS NULL OR height > 0),
|
||||
metadata_json TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(metadata_json)),
|
||||
is_deleted INTEGER NOT NULL DEFAULT 0 CHECK (is_deleted IN (0, 1)),
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
deleted_at TEXT,
|
||||
FOREIGN KEY (memo_id) REFERENCES memos(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY (original_memo_id) REFERENCES memos(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE SET NULL,
|
||||
UNIQUE (bucket_name, object_key)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_resources_memo
|
||||
ON resources(memo_id, is_deleted, kind);
|
||||
|
||||
CREATE INDEX idx_resources_original_memo
|
||||
ON resources(original_memo_id);
|
||||
|
||||
CREATE INDEX idx_resources_object
|
||||
ON resources(bucket_name, object_key);
|
||||
|
||||
CREATE TABLE memo_revisions (
|
||||
id TEXT PRIMARY KEY,
|
||||
memo_id TEXT NOT NULL,
|
||||
revision INTEGER NOT NULL,
|
||||
title TEXT,
|
||||
content_json TEXT NOT NULL CHECK (json_valid(content_json)),
|
||||
content_markdown TEXT NOT NULL DEFAULT '',
|
||||
content_hash TEXT NOT NULL,
|
||||
created_by TEXT NOT NULL DEFAULT 'system',
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
FOREIGN KEY (memo_id) REFERENCES memos(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_memo_revisions_memo
|
||||
ON memo_revisions(memo_id, revision DESC);
|
||||
|
||||
CREATE TABLE api_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
scopes_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(scopes_json)),
|
||||
last_used_at TEXT,
|
||||
expires_at TEXT,
|
||||
is_revoked INTEGER NOT NULL DEFAULT 0 CHECK (is_revoked IN (0, 1)),
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
);
|
||||
|
||||
CREATE TABLE audit_events (
|
||||
id TEXT PRIMARY KEY,
|
||||
actor_type TEXT NOT NULL CHECK (actor_type IN ('user', 'agent', 'system')),
|
||||
actor_id TEXT,
|
||||
action TEXT NOT NULL,
|
||||
entity_type TEXT NOT NULL,
|
||||
entity_id TEXT NOT NULL,
|
||||
metadata_json TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(metadata_json)),
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_audit_events_entity
|
||||
ON audit_events(entity_type, entity_id, created_at DESC);
|
||||
|
||||
INSERT INTO notebooks (id, parent_id, name, slug, icon, color, sort_order)
|
||||
VALUES
|
||||
('nb_inbox', NULL, 'Inbox', 'inbox', 'inbox', '#0f766e', 10),
|
||||
('nb_projects', NULL, 'Projects', 'projects', 'folder-kanban', '#2563eb', 20),
|
||||
('nb_edgeever', 'nb_projects', 'EdgeEver', 'edgeever', 'notebook-tabs', '#7c3aed', 10);
|
||||
|
||||
INSERT INTO memos (
|
||||
id,
|
||||
notebook_id,
|
||||
title,
|
||||
excerpt,
|
||||
tags_json,
|
||||
created_by,
|
||||
updated_by
|
||||
)
|
||||
VALUES (
|
||||
'memo_welcome',
|
||||
'nb_edgeever',
|
||||
'欢迎来到 EdgeEver',
|
||||
'这是第一条 EdgeEver 笔记,三栏、边缘、Agent-ready。',
|
||||
'["edgeever","welcome"]',
|
||||
'system',
|
||||
'system'
|
||||
);
|
||||
|
||||
INSERT INTO memo_contents (
|
||||
memo_id,
|
||||
content_json,
|
||||
content_markdown,
|
||||
content_text,
|
||||
content_hash,
|
||||
revision
|
||||
)
|
||||
VALUES (
|
||||
'memo_welcome',
|
||||
'{"type":"doc","content":[{"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"欢迎来到 EdgeEver"}]},{"type":"paragraph","content":[{"type":"text","text":"这是第一条 EdgeEver 笔记,三栏、边缘、Agent-ready。"}]},{"type":"paragraph","content":[{"type":"text","text":"接下来可以创建笔记本、写笔记、搜索内容,并把多条笔记合并成一条新的长期笔记。"}]}]}',
|
||||
'## 欢迎来到 EdgeEver\n\n这是第一条 EdgeEver 笔记,三栏、边缘、Agent-ready。\n\n接下来可以创建笔记本、写笔记、搜索内容,并把多条笔记合并成一条新的长期笔记。',
|
||||
'欢迎来到 EdgeEver 这是第一条 EdgeEver 笔记,三栏、边缘、Agent-ready。 接下来可以创建笔记本、写笔记、搜索内容,并把多条笔记合并成一条新的长期笔记。',
|
||||
'seed',
|
||||
0
|
||||
);
|
||||
|
||||
INSERT INTO memos_fts (memo_id, title, content_text, tags)
|
||||
VALUES (
|
||||
'memo_welcome',
|
||||
'欢迎来到 EdgeEver',
|
||||
'欢迎来到 EdgeEver 这是第一条 EdgeEver 笔记,三栏、边缘、Agent-ready。 接下来可以创建笔记本、写笔记、搜索内容,并把多条笔记合并成一条新的长期笔记。',
|
||||
'edgeever welcome'
|
||||
);
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "edgeever",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "concurrently -k -n api,web -c cyan,magenta \"bun run dev:api\" \"bun run dev:web\"",
|
||||
"dev:api": "wrangler dev --local --port 8787",
|
||||
"dev:web": "vite --config apps/web/vite.config.ts --host 127.0.0.1 --port 5173",
|
||||
"build": "bun run build:web",
|
||||
"build:web": "vite build --config apps/web/vite.config.ts",
|
||||
"preview:web": "vite preview --config apps/web/vite.config.ts --host 127.0.0.1 --port 4173",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"db:migrate:local": "wrangler d1 migrations apply DB --local",
|
||||
"db:migrate:remote": "wrangler d1 migrations apply DB --remote",
|
||||
"deploy": "bun run build && bun run db:migrate:remote && wrangler deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edgeever/shared": "workspace:*",
|
||||
"@hono/zod-validator": "latest",
|
||||
"@tanstack/react-query": "latest",
|
||||
"@tiptap/extension-placeholder": "latest",
|
||||
"@tiptap/react": "latest",
|
||||
"@tiptap/starter-kit": "latest",
|
||||
"class-variance-authority": "latest",
|
||||
"clsx": "latest",
|
||||
"dexie": "latest",
|
||||
"hono": "latest",
|
||||
"lucide-react": "latest",
|
||||
"react": "latest",
|
||||
"react-dom": "latest",
|
||||
"tailwind-merge": "latest",
|
||||
"zod": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "latest",
|
||||
"@types/node": "latest",
|
||||
"@types/react": "latest",
|
||||
"@types/react-dom": "latest",
|
||||
"@vitejs/plugin-react": "latest",
|
||||
"autoprefixer": "latest",
|
||||
"concurrently": "latest",
|
||||
"postcss": "latest",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "latest",
|
||||
"vite": "latest",
|
||||
"wrangler": "latest"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@edgeever/shared",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"types": "./src/index.ts"
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
export type TiptapTextNode = {
|
||||
type: "text";
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type TiptapNode = {
|
||||
type: string;
|
||||
attrs?: Record<string, unknown>;
|
||||
content?: Array<TiptapNode | TiptapTextNode>;
|
||||
};
|
||||
|
||||
export type TiptapDoc = {
|
||||
type: "doc";
|
||||
content: TiptapNode[];
|
||||
};
|
||||
|
||||
export const emptyDoc = (): TiptapDoc => ({
|
||||
type: "doc",
|
||||
content: [{ type: "paragraph" }],
|
||||
});
|
||||
|
||||
export const markdownToDoc = (markdown: string): TiptapDoc => {
|
||||
const blocks = markdown
|
||||
.replace(/\r\n/g, "\n")
|
||||
.split(/\n{2,}/)
|
||||
.map((block) => block.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (blocks.length === 0) {
|
||||
return emptyDoc();
|
||||
}
|
||||
|
||||
return {
|
||||
type: "doc",
|
||||
content: blocks.map((block) => {
|
||||
const heading = /^(#{1,3})\s+(.+)$/.exec(block);
|
||||
|
||||
if (heading) {
|
||||
return {
|
||||
type: "heading",
|
||||
attrs: { level: heading[1].length },
|
||||
content: [{ type: "text", text: heading[2] }],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: "paragraph",
|
||||
content: [{ type: "text", text: block }],
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export const docToText = (doc: unknown): string => {
|
||||
const pieces: string[] = [];
|
||||
|
||||
const walk = (node: unknown) => {
|
||||
if (!node || typeof node !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
const current = node as { text?: unknown; content?: unknown };
|
||||
|
||||
if (typeof current.text === "string") {
|
||||
pieces.push(current.text);
|
||||
}
|
||||
|
||||
if (Array.isArray(current.content)) {
|
||||
for (const child of current.content) {
|
||||
walk(child);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
walk(doc);
|
||||
|
||||
return pieces.join(" ").replace(/\s+/g, " ").trim();
|
||||
};
|
||||
|
||||
export const docToMarkdown = (doc: unknown): string => {
|
||||
if (!doc || typeof doc !== "object") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const root = doc as { content?: unknown };
|
||||
|
||||
if (!Array.isArray(root.content)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return root.content
|
||||
.map((node) => {
|
||||
if (!node || typeof node !== "object") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const current = node as {
|
||||
type?: unknown;
|
||||
attrs?: { level?: unknown };
|
||||
content?: unknown;
|
||||
};
|
||||
const text = docToText({ content: current.content });
|
||||
|
||||
if (!text) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (current.type === "heading") {
|
||||
const level = typeof current.attrs?.level === "number" ? current.attrs.level : 1;
|
||||
return `${"#".repeat(Math.min(Math.max(level, 1), 6))} ${text}`;
|
||||
}
|
||||
|
||||
return text;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
};
|
||||
|
||||
export const createExcerpt = (text: string, maxLength = 30): string => {
|
||||
const normalized = text.replace(/\s+/g, " ").trim();
|
||||
return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized;
|
||||
};
|
||||
|
||||
export const normalizeTags = (tags: unknown): string[] => {
|
||||
if (!Array.isArray(tags)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.from(
|
||||
new Set(
|
||||
tags
|
||||
.map((tag) => (typeof tag === "string" ? tag.trim() : ""))
|
||||
.filter(Boolean)
|
||||
.map((tag) => tag.replace(/^#/, ""))
|
||||
)
|
||||
).slice(0, 24);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./content";
|
||||
export * from "./schemas";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,40 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const NotebookCreateSchema = z.object({
|
||||
name: z.string().trim().min(1).max(80),
|
||||
parentId: z.string().trim().min(1).nullable().optional(),
|
||||
});
|
||||
|
||||
export const NotebookUpdateSchema = z.object({
|
||||
name: z.string().trim().min(1).max(80).optional(),
|
||||
parentId: z.string().trim().min(1).nullable().optional(),
|
||||
sortOrder: z.number().int().optional(),
|
||||
});
|
||||
|
||||
export const MemoCreateSchema = z.object({
|
||||
notebookId: z.string().trim().min(1),
|
||||
title: z.string().trim().max(160).optional(),
|
||||
contentMarkdown: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export const MemoUpdateSchema = z.object({
|
||||
expectedRevision: z.number().int().min(0).optional(),
|
||||
notebookId: z.string().trim().min(1).optional(),
|
||||
title: z.string().trim().max(160).optional(),
|
||||
contentJson: z.unknown().optional(),
|
||||
contentMarkdown: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export const MergeMemosSchema = z.object({
|
||||
memoIds: z.array(z.string().trim().min(1)).min(2).max(50),
|
||||
notebookId: z.string().trim().min(1).optional(),
|
||||
title: z.string().trim().max(160).optional(),
|
||||
});
|
||||
|
||||
export type NotebookCreateInput = z.infer<typeof NotebookCreateSchema>;
|
||||
export type NotebookUpdateInput = z.infer<typeof NotebookUpdateSchema>;
|
||||
export type MemoCreateInput = z.infer<typeof MemoCreateSchema>;
|
||||
export type MemoUpdateInput = z.infer<typeof MemoUpdateSchema>;
|
||||
export type MergeMemosInput = z.infer<typeof MergeMemosSchema>;
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { TiptapDoc } from "./content";
|
||||
|
||||
export type Notebook = {
|
||||
id: string;
|
||||
parentId: string | null;
|
||||
name: string;
|
||||
slug: string | null;
|
||||
icon: string | null;
|
||||
color: string | null;
|
||||
sortOrder: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type MemoSummary = {
|
||||
id: string;
|
||||
notebookId: string;
|
||||
title: string | null;
|
||||
excerpt: string;
|
||||
tags: string[];
|
||||
isPinned: boolean;
|
||||
isArchived: boolean;
|
||||
revision: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type MemoDetail = MemoSummary & {
|
||||
contentJson: TiptapDoc;
|
||||
contentMarkdown: string;
|
||||
contentText: string;
|
||||
sourceMemoIds: string[];
|
||||
mergeSourceCount: number;
|
||||
mergedIntoMemoId: string | null;
|
||||
};
|
||||
|
||||
export type ApiError = {
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: unknown;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
export default {
|
||||
content: ["./apps/web/index.html", "./apps/web/src/**/*.{ts,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: [
|
||||
"Inter",
|
||||
"ui-sans-serif",
|
||||
"system-ui",
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
"Segoe UI",
|
||||
"sans-serif",
|
||||
],
|
||||
},
|
||||
boxShadow: {
|
||||
panel: "0 1px 2px rgb(15 23 42 / 0.08), 0 12px 30px rgb(15 23 42 / 0.06)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
} satisfies Config;
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"ignoreDeprecations": "6.0",
|
||||
"types": ["@cloudflare/workers-types", "node"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["apps/web/src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"apps/**/*.ts",
|
||||
"apps/**/*.tsx",
|
||||
"packages/**/*.ts",
|
||||
"packages/**/*.tsx"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
name = "edgeever"
|
||||
main = "apps/api/src/index.ts"
|
||||
compatibility_date = "2026-06-26"
|
||||
workers_dev = true
|
||||
|
||||
[assets]
|
||||
directory = "apps/web/dist"
|
||||
binding = "ASSETS"
|
||||
not_found_handling = "single-page-application"
|
||||
run_worker_first = ["/api/*", "/mcp"]
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "edgeever"
|
||||
database_id = "00000000-0000-0000-0000-000000000000"
|
||||
migrations_dir = "migrations"
|
||||
|
||||
[[r2_buckets]]
|
||||
binding = "RESOURCES"
|
||||
bucket_name = "edgeever-resources"
|
||||
preview_bucket_name = "edgeever-resources-preview"
|
||||
|
||||
[observability]
|
||||
enabled = true
|
||||
Reference in new issue
Block a user