flowbook 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.de.md +170 -0
- package/README.es.md +170 -0
- package/README.fr.md +170 -0
- package/README.ja.md +170 -0
- package/README.ko.md +170 -0
- package/README.md +170 -0
- package/README.pt-BR.md +170 -0
- package/README.ru.md +171 -0
- package/README.zh-CN.md +170 -0
- package/dist/cli.js +244 -0
- package/package.json +35 -0
- package/src/client/App.tsx +52 -0
- package/src/client/components/EmptyState.tsx +57 -0
- package/src/client/components/FlowView.tsx +62 -0
- package/src/client/components/Header.tsx +48 -0
- package/src/client/components/MermaidRenderer.tsx +104 -0
- package/src/client/components/Sidebar.tsx +88 -0
- package/src/client/index.html +13 -0
- package/src/client/main.tsx +10 -0
- package/src/client/styles/globals.css +39 -0
- package/src/client/vite-env.d.ts +21 -0
- package/src/node/cli.ts +59 -0
- package/src/node/discovery.ts +16 -0
- package/src/node/init.ts +71 -0
- package/src/node/parser.ts +41 -0
- package/src/node/plugin.ts +53 -0
- package/src/node/server.ts +56 -0
- package/src/types.ts +20 -0
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Flowbook
|
|
2
|
+
|
|
3
|
+
> [English](./README.md) | [한국어](./README.ko.md) | **简体中文** | [日本語](./README.ja.md) | [Español](./README.es.md) | [Português (BR)](./README.pt-BR.md) | [Français](./README.fr.md) | [Русский](./README.ru.md) | [Deutsch](./README.de.md)
|
|
4
|
+
|
|
5
|
+
流程图的 Storybook。自动发现代码库中的 Mermaid 图表文件,按类别组织,并在可浏览的查看器中渲染。
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
## 快速开始
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# 安装
|
|
16
|
+
npm install -D flowbook
|
|
17
|
+
|
|
18
|
+
# 初始化 — 添加脚本 + 示例文件
|
|
19
|
+
npx flowbook init
|
|
20
|
+
|
|
21
|
+
# 启动开发服务器
|
|
22
|
+
npm run flowbook
|
|
23
|
+
# → http://localhost:6200
|
|
24
|
+
|
|
25
|
+
# 构建静态站点
|
|
26
|
+
npm run build-flowbook
|
|
27
|
+
# → flowbook-static/
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## CLI
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
flowbook init 在项目中设置 Flowbook
|
|
34
|
+
flowbook dev [--port 6200] 启动开发服务器
|
|
35
|
+
flowbook build [--out-dir d] 构建静态站点
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### `flowbook init`
|
|
39
|
+
|
|
40
|
+
- 在 `package.json` 中添加 `"flowbook"` 和 `"build-flowbook"` 脚本
|
|
41
|
+
- 创建 `flows/example.flow.md` 作为入门模板
|
|
42
|
+
|
|
43
|
+
### `flowbook dev`
|
|
44
|
+
|
|
45
|
+
在 `http://localhost:6200` 启动支持 HMR 的 Vite 开发服务器。任何 `.flow.md` 或 `.flowchart.md` 文件的更改都会即时生效。
|
|
46
|
+
|
|
47
|
+
### `flowbook build`
|
|
48
|
+
|
|
49
|
+
将静态站点构建到 `flowbook-static/` 目录(可通过 `--out-dir` 配置)。可部署到任何地方。
|
|
50
|
+
|
|
51
|
+
## 编写流程文件
|
|
52
|
+
|
|
53
|
+
在项目中任意位置创建 `.flow.md`(或 `.flowchart.md`)文件:
|
|
54
|
+
|
|
55
|
+
````markdown
|
|
56
|
+
---
|
|
57
|
+
title: 登录流程
|
|
58
|
+
category: 认证
|
|
59
|
+
tags: [auth, login, oauth]
|
|
60
|
+
order: 1
|
|
61
|
+
description: 使用 OAuth2 的用户认证流程
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
```mermaid
|
|
65
|
+
flowchart TD
|
|
66
|
+
A[用户] --> B{已认证?}
|
|
67
|
+
B -->|是| C[仪表盘]
|
|
68
|
+
B -->|否| D[登录页面]
|
|
69
|
+
```
|
|
70
|
+
````
|
|
71
|
+
|
|
72
|
+
Flowbook 会自动发现文件并将其添加到查看器中。
|
|
73
|
+
|
|
74
|
+
## Frontmatter 模式
|
|
75
|
+
|
|
76
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
77
|
+
|---------------|------------|------|-------------------------------------|
|
|
78
|
+
| `title` | `string` | 否 | 显示标题(默认为文件名) |
|
|
79
|
+
| `category` | `string` | 否 | 侧边栏分类(默认为 "Uncategorized")|
|
|
80
|
+
| `tags` | `string[]` | 否 | 可筛选的标签 |
|
|
81
|
+
| `order` | `number` | 否 | 分类内排序(默认:999) |
|
|
82
|
+
| `description` | `string` | 否 | 详情视图中显示的描述 |
|
|
83
|
+
|
|
84
|
+
## 文件发现
|
|
85
|
+
|
|
86
|
+
Flowbook 默认扫描以下模式:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
**/*.flow.md
|
|
90
|
+
**/*.flowchart.md
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
忽略 `node_modules/`、`.git/` 和 `dist/`。
|
|
94
|
+
|
|
95
|
+
## 工作原理
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
.flow.md 文件 ──→ Vite 插件 ──→ 虚拟模块 ──→ React 查看器
|
|
99
|
+
│ │
|
|
100
|
+
├─ fast-glob 扫描 ├─ export default { flows: [...] }
|
|
101
|
+
├─ gray-matter │
|
|
102
|
+
│ 解析 └─ 文件更改时 HMR
|
|
103
|
+
└─ mermaid 块
|
|
104
|
+
提取
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
1. **发现** — `fast-glob` 扫描项目中的 `*.flow.md` / `*.flowchart.md`
|
|
108
|
+
2. **解析** — `gray-matter` 提取 YAML frontmatter;正则表达式提取 `` ```mermaid `` 块
|
|
109
|
+
3. **虚拟模块** — Vite 插件将解析后的数据作为 `virtual:flowbook-data` 提供
|
|
110
|
+
4. **渲染** — React 应用通过 `mermaid.render()` 渲染 Mermaid 图表
|
|
111
|
+
5. **HMR** — 文件更改时使虚拟模块失效,触发重新加载
|
|
112
|
+
|
|
113
|
+
## 项目结构
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
src/
|
|
117
|
+
├── types.ts # 共享类型 (FlowEntry, FlowbookData)
|
|
118
|
+
├── node/
|
|
119
|
+
│ ├── cli.ts # CLI 入口 (init, dev, build)
|
|
120
|
+
│ ├── server.ts # 编程式 Vite 服务器与构建
|
|
121
|
+
│ ├── init.ts # 项目初始化逻辑
|
|
122
|
+
│ ├── discovery.ts # 文件扫描器 (fast-glob)
|
|
123
|
+
│ ├── parser.ts # Frontmatter + mermaid 提取
|
|
124
|
+
│ └── plugin.ts # Vite 虚拟模块插件
|
|
125
|
+
└── client/
|
|
126
|
+
├── index.html # 入口 HTML
|
|
127
|
+
├── main.tsx # React 入口
|
|
128
|
+
├── App.tsx # 搜索 + 侧边栏 + 查看器布局
|
|
129
|
+
├── vite-env.d.ts # 虚拟模块类型声明
|
|
130
|
+
├── styles/globals.css # Tailwind v4 + 自定义样式
|
|
131
|
+
└── components/
|
|
132
|
+
├── Header.tsx # Logo、搜索栏、流程计数
|
|
133
|
+
├── Sidebar.tsx # 可折叠分类树
|
|
134
|
+
├── MermaidRenderer.tsx # Mermaid 图表渲染
|
|
135
|
+
├── FlowView.tsx # 单个流程详情视图
|
|
136
|
+
└── EmptyState.tsx # 空状态引导
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## 开发(贡献)
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
git clone https://github.com/Epsilondelta-ai/flowbook.git
|
|
143
|
+
cd flowbook
|
|
144
|
+
npm install
|
|
145
|
+
|
|
146
|
+
# 本地开发(使用根目录 vite.config.ts)
|
|
147
|
+
npm run dev
|
|
148
|
+
|
|
149
|
+
# 构建 CLI
|
|
150
|
+
npm run build
|
|
151
|
+
|
|
152
|
+
# 本地测试 CLI
|
|
153
|
+
node dist/cli.js dev
|
|
154
|
+
node dist/cli.js build
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## 技术栈
|
|
158
|
+
|
|
159
|
+
- **Vite** — 支持 HMR 的开发服务器
|
|
160
|
+
- **React 19** — 用户界面
|
|
161
|
+
- **Mermaid 11** — 图表渲染
|
|
162
|
+
- **Tailwind CSS v4** — 样式
|
|
163
|
+
- **gray-matter** — YAML frontmatter 解析
|
|
164
|
+
- **fast-glob** — 文件发现
|
|
165
|
+
- **tsup** — CLI 打包工具
|
|
166
|
+
- **TypeScript** — 类型安全
|
|
167
|
+
|
|
168
|
+
## 许可证
|
|
169
|
+
|
|
170
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/node/init.ts
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
5
|
+
import { resolve } from "path";
|
|
6
|
+
var EXAMPLE_FLOW = `---
|
|
7
|
+
title: Example Flow
|
|
8
|
+
category: Getting Started
|
|
9
|
+
tags: [example]
|
|
10
|
+
order: 1
|
|
11
|
+
description: An example flowchart to get you started
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
\`\`\`mermaid
|
|
15
|
+
flowchart TD
|
|
16
|
+
A[Start] --> B{Decision}
|
|
17
|
+
B -->|Yes| C[Action A]
|
|
18
|
+
B -->|No| D[Action B]
|
|
19
|
+
C --> E[End]
|
|
20
|
+
D --> E
|
|
21
|
+
\`\`\`
|
|
22
|
+
`;
|
|
23
|
+
async function initFlowbook() {
|
|
24
|
+
const cwd = process.cwd();
|
|
25
|
+
const pkgPath = resolve(cwd, "package.json");
|
|
26
|
+
if (!existsSync(pkgPath)) {
|
|
27
|
+
console.error(" No package.json found. Run 'npm init' first.");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const raw = readFileSync(pkgPath, "utf-8");
|
|
31
|
+
const pkg = JSON.parse(raw);
|
|
32
|
+
pkg.scripts = pkg.scripts ?? {};
|
|
33
|
+
let scriptsAdded = false;
|
|
34
|
+
if (!pkg.scripts.flowbook) {
|
|
35
|
+
pkg.scripts.flowbook = "flowbook dev";
|
|
36
|
+
scriptsAdded = true;
|
|
37
|
+
}
|
|
38
|
+
if (!pkg.scripts["build-flowbook"]) {
|
|
39
|
+
pkg.scripts["build-flowbook"] = "flowbook build";
|
|
40
|
+
scriptsAdded = true;
|
|
41
|
+
}
|
|
42
|
+
if (scriptsAdded) {
|
|
43
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
44
|
+
console.log(' \u2713 Added "flowbook" and "build-flowbook" scripts to package.json');
|
|
45
|
+
} else {
|
|
46
|
+
console.log(" \u2713 Scripts already exist in package.json");
|
|
47
|
+
}
|
|
48
|
+
const flowsDir = resolve(cwd, "flows");
|
|
49
|
+
const examplePath = resolve(flowsDir, "example.flow.md");
|
|
50
|
+
if (!existsSync(examplePath)) {
|
|
51
|
+
mkdirSync(flowsDir, { recursive: true });
|
|
52
|
+
writeFileSync(examplePath, EXAMPLE_FLOW);
|
|
53
|
+
console.log(" \u2713 Created flows/example.flow.md");
|
|
54
|
+
} else {
|
|
55
|
+
console.log(" \u2713 Example flow already exists");
|
|
56
|
+
}
|
|
57
|
+
console.log("");
|
|
58
|
+
console.log(" Next steps:");
|
|
59
|
+
console.log(" npm run flowbook Start the dev server");
|
|
60
|
+
console.log(" npm run build-flowbook Build static site");
|
|
61
|
+
console.log("");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/node/server.ts
|
|
65
|
+
import { createServer, build } from "vite";
|
|
66
|
+
import react from "@vitejs/plugin-react";
|
|
67
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
68
|
+
|
|
69
|
+
// src/node/discovery.ts
|
|
70
|
+
import fg from "fast-glob";
|
|
71
|
+
async function discoverFlowFiles(options) {
|
|
72
|
+
const patterns = options.include ?? ["**/*.flow.md", "**/*.flowchart.md"];
|
|
73
|
+
const ignore = options.ignore ?? [
|
|
74
|
+
"node_modules/**",
|
|
75
|
+
".git/**",
|
|
76
|
+
"dist/**"
|
|
77
|
+
];
|
|
78
|
+
const cwd = options.cwd ?? process.cwd();
|
|
79
|
+
return fg(patterns, { cwd, ignore, absolute: true });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/node/parser.ts
|
|
83
|
+
import matter from "gray-matter";
|
|
84
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
85
|
+
import { relative } from "path";
|
|
86
|
+
var MERMAID_BLOCK_RE = /```mermaid\n([\s\S]*?)```/g;
|
|
87
|
+
function parseFlowFile(filePath, cwd) {
|
|
88
|
+
const raw = readFileSync2(filePath, "utf-8");
|
|
89
|
+
const { data, content } = matter(raw);
|
|
90
|
+
const mermaidBlocks = [];
|
|
91
|
+
let match;
|
|
92
|
+
while ((match = MERMAID_BLOCK_RE.exec(content)) !== null) {
|
|
93
|
+
mermaidBlocks.push(match[1].trim());
|
|
94
|
+
}
|
|
95
|
+
MERMAID_BLOCK_RE.lastIndex = 0;
|
|
96
|
+
const relPath = relative(cwd, filePath);
|
|
97
|
+
const id = relPath.replace(/[/\\]/g, "-").replace(/\.flow(chart)?\.md$/, "");
|
|
98
|
+
const fileName = relPath.replace(/\.flow(chart)?\.md$/, "").split("/").pop();
|
|
99
|
+
return {
|
|
100
|
+
id,
|
|
101
|
+
title: typeof data.title === "string" ? data.title : fileName ?? "Untitled",
|
|
102
|
+
category: typeof data.category === "string" ? data.category : "Uncategorized",
|
|
103
|
+
tags: Array.isArray(data.tags) ? data.tags : [],
|
|
104
|
+
description: typeof data.description === "string" ? data.description : "",
|
|
105
|
+
order: typeof data.order === "number" ? data.order : 999,
|
|
106
|
+
filePath: relPath,
|
|
107
|
+
mermaidBlocks
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/node/plugin.ts
|
|
112
|
+
var VIRTUAL_MODULE_ID = "virtual:flowbook-data";
|
|
113
|
+
var RESOLVED_ID = "\0" + VIRTUAL_MODULE_ID;
|
|
114
|
+
function flowbookPlugin(options = {}) {
|
|
115
|
+
const cwd = options.cwd ?? process.cwd();
|
|
116
|
+
async function loadFlows() {
|
|
117
|
+
const files = await discoverFlowFiles({ ...options, cwd });
|
|
118
|
+
const flows = files.map((f) => parseFlowFile(f, cwd));
|
|
119
|
+
flows.sort((a, b) => {
|
|
120
|
+
if (a.category !== b.category)
|
|
121
|
+
return a.category.localeCompare(b.category);
|
|
122
|
+
return a.order - b.order;
|
|
123
|
+
});
|
|
124
|
+
return { flows };
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
name: "flowbook",
|
|
128
|
+
resolveId(id) {
|
|
129
|
+
if (id === VIRTUAL_MODULE_ID) return RESOLVED_ID;
|
|
130
|
+
},
|
|
131
|
+
async load(id) {
|
|
132
|
+
if (id === RESOLVED_ID) {
|
|
133
|
+
const data = await loadFlows();
|
|
134
|
+
return `export default ${JSON.stringify(data)}`;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
handleHotUpdate({ file, server }) {
|
|
138
|
+
if (file.endsWith(".flow.md") || file.endsWith(".flowchart.md")) {
|
|
139
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_ID);
|
|
140
|
+
if (mod) {
|
|
141
|
+
server.moduleGraph.invalidateModule(mod);
|
|
142
|
+
server.ws.send({ type: "full-reload" });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
configureServer(server) {
|
|
147
|
+
if (cwd !== process.cwd()) {
|
|
148
|
+
server.watcher.add(cwd);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/node/server.ts
|
|
155
|
+
import { resolve as resolve2, dirname } from "path";
|
|
156
|
+
import { fileURLToPath } from "url";
|
|
157
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
158
|
+
function getClientDir() {
|
|
159
|
+
return resolve2(__dirname, "..", "src", "client");
|
|
160
|
+
}
|
|
161
|
+
function createConfig(options) {
|
|
162
|
+
const cwd = options.cwd ?? process.cwd();
|
|
163
|
+
const clientDir = getClientDir();
|
|
164
|
+
return {
|
|
165
|
+
configFile: false,
|
|
166
|
+
root: clientDir,
|
|
167
|
+
plugins: [
|
|
168
|
+
react(),
|
|
169
|
+
tailwindcss(),
|
|
170
|
+
flowbookPlugin({
|
|
171
|
+
include: ["**/*.flow.md", "**/*.flowchart.md"],
|
|
172
|
+
ignore: ["node_modules/**", ".git/**", "dist/**"],
|
|
173
|
+
cwd
|
|
174
|
+
})
|
|
175
|
+
],
|
|
176
|
+
server: {
|
|
177
|
+
port: options.port ?? 6200
|
|
178
|
+
},
|
|
179
|
+
build: {
|
|
180
|
+
outDir: resolve2(cwd, options.outDir ?? "flowbook-static"),
|
|
181
|
+
emptyOutDir: true
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
async function startDevServer(options) {
|
|
186
|
+
const config = createConfig({ port: options.port });
|
|
187
|
+
const server = await createServer(config);
|
|
188
|
+
await server.listen();
|
|
189
|
+
server.printUrls();
|
|
190
|
+
}
|
|
191
|
+
async function buildStatic(options) {
|
|
192
|
+
const config = createConfig({ outDir: options.outDir });
|
|
193
|
+
await build(config);
|
|
194
|
+
console.log(`
|
|
195
|
+
Static site built to ${options.outDir ?? "flowbook-static"}/
|
|
196
|
+
`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/node/cli.ts
|
|
200
|
+
async function main() {
|
|
201
|
+
const args = process.argv.slice(2);
|
|
202
|
+
const command = args[0];
|
|
203
|
+
switch (command) {
|
|
204
|
+
case "init":
|
|
205
|
+
await initFlowbook();
|
|
206
|
+
break;
|
|
207
|
+
case "dev": {
|
|
208
|
+
const port = Number(getFlag(args, "--port", "6200"));
|
|
209
|
+
await startDevServer({ port });
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
case "build": {
|
|
213
|
+
const outDir = String(getFlag(args, "--out-dir", "flowbook-static"));
|
|
214
|
+
await buildStatic({ outDir });
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
default:
|
|
218
|
+
printUsage();
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function getFlag(args, flag, fallback) {
|
|
223
|
+
const idx = args.indexOf(flag);
|
|
224
|
+
if (idx === -1 || idx + 1 >= args.length) return fallback;
|
|
225
|
+
return args[idx + 1];
|
|
226
|
+
}
|
|
227
|
+
function printUsage() {
|
|
228
|
+
console.log(`
|
|
229
|
+
flowbook \u2014 Storybook for flowcharts
|
|
230
|
+
|
|
231
|
+
Usage:
|
|
232
|
+
flowbook init Set up Flowbook in your project
|
|
233
|
+
flowbook dev [--port 6200] Start the dev server
|
|
234
|
+
flowbook build [--out-dir d] Build a static site
|
|
235
|
+
|
|
236
|
+
Options:
|
|
237
|
+
--port <number> Dev server port (default: 6200)
|
|
238
|
+
--out-dir <path> Build output directory (default: flowbook-static)
|
|
239
|
+
`);
|
|
240
|
+
}
|
|
241
|
+
main().catch((err) => {
|
|
242
|
+
console.error(err);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "flowbook",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"flowbook": "./dist/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"dev": "vite",
|
|
14
|
+
"build": "tsup",
|
|
15
|
+
"preview": "vite preview"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
19
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
20
|
+
"fast-glob": "^3.3.0",
|
|
21
|
+
"gray-matter": "^4.0.3",
|
|
22
|
+
"mermaid": "^11.4.0",
|
|
23
|
+
"react": "^19.0.0",
|
|
24
|
+
"react-dom": "^19.0.0",
|
|
25
|
+
"tailwindcss": "^4.0.0",
|
|
26
|
+
"vite": "^6.1.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^25.3.3",
|
|
30
|
+
"@types/react": "^19.0.0",
|
|
31
|
+
"@types/react-dom": "^19.0.0",
|
|
32
|
+
"tsup": "^8.0.0",
|
|
33
|
+
"typescript": "^5.7.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useState, useMemo } from "react";
|
|
2
|
+
import data from "virtual:flowbook-data";
|
|
3
|
+
import { Sidebar } from "./components/Sidebar";
|
|
4
|
+
import { FlowView } from "./components/FlowView";
|
|
5
|
+
import { Header } from "./components/Header";
|
|
6
|
+
import { EmptyState } from "./components/EmptyState";
|
|
7
|
+
|
|
8
|
+
export function App() {
|
|
9
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
10
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
11
|
+
|
|
12
|
+
const filteredFlows = useMemo(() => {
|
|
13
|
+
if (!searchQuery) return data.flows;
|
|
14
|
+
const q = searchQuery.toLowerCase();
|
|
15
|
+
return data.flows.filter(
|
|
16
|
+
(f) =>
|
|
17
|
+
f.title.toLowerCase().includes(q) ||
|
|
18
|
+
f.category.toLowerCase().includes(q) ||
|
|
19
|
+
f.tags.some((t) => t.toLowerCase().includes(q)) ||
|
|
20
|
+
f.description.toLowerCase().includes(q),
|
|
21
|
+
);
|
|
22
|
+
}, [searchQuery]);
|
|
23
|
+
|
|
24
|
+
const selectedFlow = useMemo(
|
|
25
|
+
() => data.flows.find((f) => f.id === selectedId) ?? null,
|
|
26
|
+
[selectedId],
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="h-screen flex flex-col bg-zinc-950 text-zinc-100">
|
|
31
|
+
<Header
|
|
32
|
+
searchQuery={searchQuery}
|
|
33
|
+
onSearchChange={setSearchQuery}
|
|
34
|
+
flowCount={data.flows.length}
|
|
35
|
+
/>
|
|
36
|
+
<div className="flex flex-1 overflow-hidden">
|
|
37
|
+
<Sidebar
|
|
38
|
+
flows={filteredFlows}
|
|
39
|
+
selectedId={selectedId}
|
|
40
|
+
onSelect={setSelectedId}
|
|
41
|
+
/>
|
|
42
|
+
<main className="flex-1 overflow-auto">
|
|
43
|
+
{selectedFlow ? (
|
|
44
|
+
<FlowView flow={selectedFlow} />
|
|
45
|
+
) : (
|
|
46
|
+
<EmptyState flowCount={data.flows.length} />
|
|
47
|
+
)}
|
|
48
|
+
</main>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
interface EmptyStateProps {
|
|
2
|
+
flowCount: number;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function EmptyState({ flowCount }: EmptyStateProps) {
|
|
6
|
+
return (
|
|
7
|
+
<div className="flex items-center justify-center h-full">
|
|
8
|
+
<div className="text-center max-w-lg px-4">
|
|
9
|
+
<svg
|
|
10
|
+
className="w-16 h-16 text-zinc-800 mx-auto mb-6"
|
|
11
|
+
viewBox="0 0 24 24"
|
|
12
|
+
fill="none"
|
|
13
|
+
stroke="currentColor"
|
|
14
|
+
strokeWidth="1"
|
|
15
|
+
strokeLinecap="round"
|
|
16
|
+
strokeLinejoin="round"
|
|
17
|
+
>
|
|
18
|
+
<rect x="3" y="3" width="7" height="7" rx="1" />
|
|
19
|
+
<rect x="14" y="3" width="7" height="7" rx="1" />
|
|
20
|
+
<rect x="8" y="14" width="7" height="7" rx="1" />
|
|
21
|
+
<path d="M6.5 10v1.5a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1V10" />
|
|
22
|
+
<path d="M11.5 12.5V14" />
|
|
23
|
+
</svg>
|
|
24
|
+
<h2 className="text-xl font-semibold text-zinc-400 mb-2">
|
|
25
|
+
{flowCount > 0 ? "Select a flow" : "No flows found"}
|
|
26
|
+
</h2>
|
|
27
|
+
<p className="text-zinc-600 text-sm leading-relaxed">
|
|
28
|
+
{flowCount > 0
|
|
29
|
+
? "Choose a flow from the sidebar to view its diagram."
|
|
30
|
+
: "Create .flow.md or .flowchart.md files with mermaid diagrams to get started."}
|
|
31
|
+
</p>
|
|
32
|
+
{flowCount === 0 && (
|
|
33
|
+
<div className="mt-8 bg-zinc-900 border border-zinc-800 rounded-xl p-5 text-left">
|
|
34
|
+
<p className="text-xs text-zinc-500 mb-3 font-medium">
|
|
35
|
+
Example: <code>auth/login.flow.md</code>
|
|
36
|
+
</p>
|
|
37
|
+
<pre className="text-xs text-zinc-400 font-mono leading-relaxed whitespace-pre overflow-x-auto">
|
|
38
|
+
{`---
|
|
39
|
+
title: Login Flow
|
|
40
|
+
category: Authentication
|
|
41
|
+
tags: [auth, login]
|
|
42
|
+
description: User login process
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
\`\`\`mermaid
|
|
46
|
+
flowchart TD
|
|
47
|
+
A[User] --> B{Authenticated?}
|
|
48
|
+
B -->|Yes| C[Dashboard]
|
|
49
|
+
B -->|No| D[Login Page]
|
|
50
|
+
\`\`\``}
|
|
51
|
+
</pre>
|
|
52
|
+
</div>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { FlowEntry } from "../../types";
|
|
2
|
+
import { MermaidRenderer } from "./MermaidRenderer";
|
|
3
|
+
|
|
4
|
+
interface FlowViewProps {
|
|
5
|
+
flow: FlowEntry;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function FlowView({ flow }: FlowViewProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="p-8 max-w-5xl mx-auto">
|
|
11
|
+
{/* Header */}
|
|
12
|
+
<div className="mb-8">
|
|
13
|
+
<div className="flex items-center gap-2 mb-3 flex-wrap">
|
|
14
|
+
<span className="text-xs font-medium text-violet-300 bg-violet-500/10 border border-violet-500/20 px-2.5 py-0.5 rounded-full">
|
|
15
|
+
{flow.category}
|
|
16
|
+
</span>
|
|
17
|
+
{flow.tags.map((tag) => (
|
|
18
|
+
<span
|
|
19
|
+
key={tag}
|
|
20
|
+
className="text-xs text-zinc-400 bg-zinc-800/80 px-2 py-0.5 rounded-full"
|
|
21
|
+
>
|
|
22
|
+
{tag}
|
|
23
|
+
</span>
|
|
24
|
+
))}
|
|
25
|
+
</div>
|
|
26
|
+
<h2 className="text-2xl font-bold text-zinc-50 tracking-tight">
|
|
27
|
+
{flow.title}
|
|
28
|
+
</h2>
|
|
29
|
+
{flow.description && (
|
|
30
|
+
<p className="mt-2 text-zinc-400 leading-relaxed">
|
|
31
|
+
{flow.description}
|
|
32
|
+
</p>
|
|
33
|
+
)}
|
|
34
|
+
<p className="mt-2 text-xs text-zinc-600 font-mono">{flow.filePath}</p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
{/* Diagrams */}
|
|
38
|
+
<div className="space-y-6">
|
|
39
|
+
{flow.mermaidBlocks.map((block, i) => (
|
|
40
|
+
<div
|
|
41
|
+
key={i}
|
|
42
|
+
className="bg-zinc-900/50 border border-zinc-800 rounded-xl p-6 backdrop-blur-sm"
|
|
43
|
+
>
|
|
44
|
+
<MermaidRenderer code={block} />
|
|
45
|
+
</div>
|
|
46
|
+
))}
|
|
47
|
+
|
|
48
|
+
{flow.mermaidBlocks.length === 0 && (
|
|
49
|
+
<div className="bg-zinc-900/50 border border-zinc-800 rounded-xl p-12 text-center">
|
|
50
|
+
<p className="text-zinc-500">
|
|
51
|
+
No mermaid diagrams found in this file.
|
|
52
|
+
</p>
|
|
53
|
+
<p className="text-zinc-600 text-sm mt-1">
|
|
54
|
+
Add a <code className="text-violet-400">```mermaid</code> code
|
|
55
|
+
block to see it rendered here.
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
interface HeaderProps {
|
|
2
|
+
searchQuery: string;
|
|
3
|
+
onSearchChange: (query: string) => void;
|
|
4
|
+
flowCount: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function Header({
|
|
8
|
+
searchQuery,
|
|
9
|
+
onSearchChange,
|
|
10
|
+
flowCount,
|
|
11
|
+
}: HeaderProps) {
|
|
12
|
+
return (
|
|
13
|
+
<header className="h-14 border-b border-zinc-800 flex items-center px-4 gap-4 shrink-0 bg-zinc-950">
|
|
14
|
+
<div className="flex items-center gap-2.5">
|
|
15
|
+
<svg
|
|
16
|
+
className="w-6 h-6 text-violet-400"
|
|
17
|
+
viewBox="0 0 24 24"
|
|
18
|
+
fill="none"
|
|
19
|
+
stroke="currentColor"
|
|
20
|
+
strokeWidth="2"
|
|
21
|
+
strokeLinecap="round"
|
|
22
|
+
strokeLinejoin="round"
|
|
23
|
+
>
|
|
24
|
+
<rect x="3" y="3" width="7" height="7" rx="1" />
|
|
25
|
+
<rect x="14" y="3" width="7" height="7" rx="1" />
|
|
26
|
+
<rect x="8" y="14" width="7" height="7" rx="1" />
|
|
27
|
+
<path d="M6.5 10v1.5a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1V10" />
|
|
28
|
+
<path d="M11.5 12.5V14" />
|
|
29
|
+
</svg>
|
|
30
|
+
<h1 className="text-lg font-semibold tracking-tight text-zinc-100">
|
|
31
|
+
Flowbook
|
|
32
|
+
</h1>
|
|
33
|
+
<span className="text-xs text-zinc-500 bg-zinc-800/80 px-2 py-0.5 rounded-full font-medium">
|
|
34
|
+
{flowCount}
|
|
35
|
+
</span>
|
|
36
|
+
</div>
|
|
37
|
+
<div className="flex-1 max-w-md">
|
|
38
|
+
<input
|
|
39
|
+
type="text"
|
|
40
|
+
placeholder="Search flows…"
|
|
41
|
+
value={searchQuery}
|
|
42
|
+
onChange={(e) => onSearchChange(e.target.value)}
|
|
43
|
+
className="w-full bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-1.5 text-sm placeholder-zinc-500 focus:outline-none focus:border-violet-500/60 focus:ring-1 focus:ring-violet-500/20 transition-all"
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
</header>
|
|
47
|
+
);
|
|
48
|
+
}
|