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.
@@ -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
+ ![Vite](https://img.shields.io/badge/vite-6.x-646CFF?logo=vite&logoColor=white)
8
+ ![React](https://img.shields.io/badge/react-19.x-61DAFB?logo=react&logoColor=white)
9
+ ![Mermaid](https://img.shields.io/badge/mermaid-11.x-FF3670?logo=mermaid&logoColor=white)
10
+ ![TypeScript](https://img.shields.io/badge/typescript-5.x-3178C6?logo=typescript&logoColor=white)
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
+ }