flowbook 0.2.5 → 0.2.7
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/package.json +2 -2
- package/src/client/App.tsx +30 -21
- package/src/client/components/EmptyState.tsx +14 -7
- package/src/client/components/FlowView.tsx +11 -31
- package/src/client/components/MermaidRenderer.tsx +28 -20
- package/src/client/components/Sidebar.tsx +46 -58
- package/src/client/public/android-chrome-192x192.png +0 -0
- package/src/client/public/android-chrome-512x512.png +0 -0
- package/src/client/public/apple-touch-icon.png +0 -0
- package/src/client/public/favicon-16x16.png +0 -0
- package/src/client/public/favicon-32x32.png +0 -0
- package/src/client/public/favicon.ico +0 -0
- package/src/client/styles/globals.css +16 -4
package/package.json
CHANGED
package/src/client/App.tsx
CHANGED
|
@@ -2,12 +2,11 @@ import { useState, useMemo } from "react";
|
|
|
2
2
|
import data from "virtual:flowbook-data";
|
|
3
3
|
import { Sidebar } from "./components/Sidebar";
|
|
4
4
|
import { FlowView } from "./components/FlowView";
|
|
5
|
-
import { Header } from "./components/Header";
|
|
6
5
|
import { EmptyState } from "./components/EmptyState";
|
|
7
6
|
|
|
8
7
|
export function App() {
|
|
9
8
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
10
|
-
const [searchQuery
|
|
9
|
+
const [searchQuery] = useState("");
|
|
11
10
|
|
|
12
11
|
const filteredFlows = useMemo(() => {
|
|
13
12
|
if (!searchQuery) return data.flows;
|
|
@@ -27,25 +26,35 @@ export function App() {
|
|
|
27
26
|
);
|
|
28
27
|
|
|
29
28
|
return (
|
|
30
|
-
<div className="h-screen flex
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
29
|
+
<div className="h-screen flex items-center justify-center p-6" style={{ background: 'var(--fb-bg-outer)' }}>
|
|
30
|
+
<div className="w-full max-w-6xl rounded-xl overflow-hidden shadow-2xl" style={{ background: 'var(--fb-bg-window)', border: '1px solid var(--fb-border)' }}>
|
|
31
|
+
{/* macOS-style title bar */}
|
|
32
|
+
<div className="flex items-center px-4 py-3 border-b" style={{ background: 'var(--fb-bg-titlebar)', borderColor: 'var(--fb-border)' }}>
|
|
33
|
+
<div className="flex items-center gap-2">
|
|
34
|
+
<div className="w-3 h-3 rounded-full bg-[#FF5F57]" />
|
|
35
|
+
<div className="w-3 h-3 rounded-full bg-[#FFBD2E]" />
|
|
36
|
+
<div className="w-3 h-3 rounded-full bg-[#27C93F]" />
|
|
37
|
+
</div>
|
|
38
|
+
<span className="ml-4 text-sm font-mono" style={{ color: 'var(--fb-text-muted)' }}>
|
|
39
|
+
flowbook — localhost:6200
|
|
40
|
+
</span>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{/* Content area */}
|
|
44
|
+
<div className="flex" style={{ height: '520px' }}>
|
|
45
|
+
<Sidebar
|
|
46
|
+
flows={filteredFlows}
|
|
47
|
+
selectedId={selectedId}
|
|
48
|
+
onSelect={setSelectedId}
|
|
49
|
+
/>
|
|
50
|
+
<main className="flex-1 overflow-auto" style={{ background: 'var(--fb-bg-content)' }}>
|
|
51
|
+
{selectedFlow ? (
|
|
52
|
+
<FlowView flow={selectedFlow} />
|
|
53
|
+
) : (
|
|
54
|
+
<EmptyState flowCount={data.flows.length} />
|
|
55
|
+
)}
|
|
56
|
+
</main>
|
|
57
|
+
</div>
|
|
49
58
|
</div>
|
|
50
59
|
</div>
|
|
51
60
|
);
|
|
@@ -7,7 +7,8 @@ export function EmptyState({ flowCount }: EmptyStateProps) {
|
|
|
7
7
|
<div className="flex items-center justify-center h-full">
|
|
8
8
|
<div className="text-center max-w-lg px-4">
|
|
9
9
|
<svg
|
|
10
|
-
className="w-16 h-16
|
|
10
|
+
className="w-16 h-16 mx-auto mb-6"
|
|
11
|
+
style={{ color: '#1e293b' }}
|
|
11
12
|
viewBox="0 0 24 24"
|
|
12
13
|
fill="none"
|
|
13
14
|
stroke="currentColor"
|
|
@@ -21,20 +22,26 @@ export function EmptyState({ flowCount }: EmptyStateProps) {
|
|
|
21
22
|
<path d="M6.5 10v1.5a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1V10" />
|
|
22
23
|
<path d="M11.5 12.5V14" />
|
|
23
24
|
</svg>
|
|
24
|
-
<h2 className="text-xl font-semibold
|
|
25
|
+
<h2 className="text-xl font-semibold mb-2" style={{ color: 'var(--fb-text-secondary)' }}>
|
|
25
26
|
{flowCount > 0 ? "Select a flow" : "No flows found"}
|
|
26
27
|
</h2>
|
|
27
|
-
<p className="text-
|
|
28
|
+
<p className="text-sm leading-relaxed" style={{ color: 'var(--fb-text-muted)' }}>
|
|
28
29
|
{flowCount > 0
|
|
29
30
|
? "Choose a flow from the sidebar to view its diagram."
|
|
30
31
|
: "Create .flow.md or .flowchart.md files with mermaid diagrams to get started."}
|
|
31
32
|
</p>
|
|
32
33
|
{flowCount === 0 && (
|
|
33
|
-
<div
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
<div
|
|
35
|
+
className="mt-8 rounded-xl p-5 text-left border"
|
|
36
|
+
style={{
|
|
37
|
+
background: 'rgba(255,255,255,0.03)',
|
|
38
|
+
borderColor: 'var(--fb-border)',
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<p className="text-xs mb-3 font-medium" style={{ color: 'var(--fb-text-muted)' }}>
|
|
42
|
+
Example: <code style={{ color: 'var(--fb-accent-violet)' }}>auth/login.flow.md</code>
|
|
36
43
|
</p>
|
|
37
|
-
<pre className="text-xs
|
|
44
|
+
<pre className="text-xs font-mono leading-relaxed whitespace-pre overflow-x-auto" style={{ color: 'var(--fb-text-secondary)' }}>
|
|
38
45
|
{`---
|
|
39
46
|
title: Login Flow
|
|
40
47
|
category: Authentication
|
|
@@ -8,50 +8,30 @@ interface FlowViewProps {
|
|
|
8
8
|
export function FlowView({ flow }: FlowViewProps) {
|
|
9
9
|
return (
|
|
10
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
11
|
{/* Diagrams */}
|
|
38
12
|
<div className="space-y-6">
|
|
39
13
|
{flow.mermaidBlocks.map((block, i) => (
|
|
40
14
|
<div
|
|
41
15
|
key={i}
|
|
42
|
-
className="
|
|
16
|
+
className="flex items-center justify-center min-h-[400px]"
|
|
43
17
|
>
|
|
44
18
|
<MermaidRenderer code={block} />
|
|
45
19
|
</div>
|
|
46
20
|
))}
|
|
47
21
|
|
|
48
22
|
{flow.mermaidBlocks.length === 0 && (
|
|
49
|
-
<div
|
|
50
|
-
|
|
23
|
+
<div
|
|
24
|
+
className="rounded-xl p-12 text-center border"
|
|
25
|
+
style={{
|
|
26
|
+
background: 'rgba(255,255,255,0.03)',
|
|
27
|
+
borderColor: 'var(--fb-border)',
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
<p style={{ color: 'var(--fb-text-muted)' }}>
|
|
51
31
|
No mermaid diagrams found in this file.
|
|
52
32
|
</p>
|
|
53
|
-
<p className="text-
|
|
54
|
-
Add a <code
|
|
33
|
+
<p className="text-sm mt-1" style={{ color: 'var(--fb-text-muted)' }}>
|
|
34
|
+
Add a <code style={{ color: 'var(--fb-accent-violet)' }}>```mermaid</code> code
|
|
55
35
|
block to see it rendered here.
|
|
56
36
|
</p>
|
|
57
37
|
</div>
|
|
@@ -9,23 +9,27 @@ function ensureInit() {
|
|
|
9
9
|
startOnLoad: false,
|
|
10
10
|
theme: "dark",
|
|
11
11
|
securityLevel: "loose",
|
|
12
|
-
fontFamily: "ui-
|
|
12
|
+
fontFamily: "'Inter', ui-sans-serif, system-ui, sans-serif",
|
|
13
13
|
themeVariables: {
|
|
14
14
|
darkMode: true,
|
|
15
|
-
background: "#
|
|
16
|
-
mainBkg: "#
|
|
17
|
-
primaryColor: "#
|
|
18
|
-
primaryTextColor: "#
|
|
19
|
-
primaryBorderColor: "#
|
|
20
|
-
secondaryColor: "#
|
|
21
|
-
secondaryTextColor: "#
|
|
22
|
-
secondaryBorderColor: "#
|
|
23
|
-
tertiaryColor: "#
|
|
24
|
-
tertiaryTextColor: "#
|
|
25
|
-
tertiaryBorderColor: "#
|
|
26
|
-
lineColor: "#
|
|
27
|
-
textColor: "#
|
|
28
|
-
nodeTextColor: "#
|
|
15
|
+
background: "#131a2b",
|
|
16
|
+
mainBkg: "#1e2740",
|
|
17
|
+
primaryColor: "#6366f1",
|
|
18
|
+
primaryTextColor: "#e2e8f0",
|
|
19
|
+
primaryBorderColor: "#4f46e5",
|
|
20
|
+
secondaryColor: "#1e293b",
|
|
21
|
+
secondaryTextColor: "#cbd5e1",
|
|
22
|
+
secondaryBorderColor: "#334155",
|
|
23
|
+
tertiaryColor: "#1e2740",
|
|
24
|
+
tertiaryTextColor: "#94a3b8",
|
|
25
|
+
tertiaryBorderColor: "#334155",
|
|
26
|
+
lineColor: "#6366f1",
|
|
27
|
+
textColor: "#e2e8f0",
|
|
28
|
+
nodeTextColor: "#e2e8f0",
|
|
29
|
+
nodeBorder: "#4f46e5",
|
|
30
|
+
clusterBkg: "#0f172a",
|
|
31
|
+
clusterBorder: "#1e293b",
|
|
32
|
+
edgeLabelBackground: "#131a2b",
|
|
29
33
|
},
|
|
30
34
|
});
|
|
31
35
|
initialized = true;
|
|
@@ -71,13 +75,17 @@ export function MermaidRenderer({ code, className }: Props) {
|
|
|
71
75
|
if (error) {
|
|
72
76
|
return (
|
|
73
77
|
<div
|
|
74
|
-
className={`
|
|
78
|
+
className={`rounded-lg p-4 border ${className ?? ""}`}
|
|
79
|
+
style={{
|
|
80
|
+
background: 'rgba(239, 68, 68, 0.08)',
|
|
81
|
+
borderColor: 'rgba(239, 68, 68, 0.2)',
|
|
82
|
+
}}
|
|
75
83
|
>
|
|
76
|
-
<p className="text-
|
|
84
|
+
<p className="text-sm font-medium mb-2" style={{ color: '#f87171' }}>
|
|
77
85
|
Render error
|
|
78
86
|
</p>
|
|
79
|
-
<p className="text-
|
|
80
|
-
<pre className="text-xs
|
|
87
|
+
<p className="text-xs font-mono mb-3" style={{ color: 'rgba(248, 113, 113, 0.7)' }}>{error}</p>
|
|
88
|
+
<pre className="text-xs overflow-x-auto whitespace-pre-wrap" style={{ color: 'var(--fb-text-muted)' }}>
|
|
81
89
|
{code}
|
|
82
90
|
</pre>
|
|
83
91
|
</div>
|
|
@@ -89,7 +97,7 @@ export function MermaidRenderer({ code, className }: Props) {
|
|
|
89
97
|
<div
|
|
90
98
|
className={`flex items-center justify-center py-12 ${className ?? ""}`}
|
|
91
99
|
>
|
|
92
|
-
<div className="w-5 h-5 border-2
|
|
100
|
+
<div className="w-5 h-5 border-2 rounded-full animate-spin" style={{ borderColor: 'rgba(99, 102, 241, 0.3)', borderTopColor: '#6366f1' }} />
|
|
93
101
|
</div>
|
|
94
102
|
);
|
|
95
103
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo
|
|
1
|
+
import { useMemo } from "react";
|
|
2
2
|
import type { FlowEntry } from "../../types";
|
|
3
3
|
|
|
4
4
|
interface SidebarProps {
|
|
@@ -8,10 +8,6 @@ interface SidebarProps {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function Sidebar({ flows, selectedId, onSelect }: SidebarProps) {
|
|
11
|
-
const [collapsedCategories, setCollapsedCategories] = useState<Set<string>>(
|
|
12
|
-
new Set(),
|
|
13
|
-
);
|
|
14
|
-
|
|
15
11
|
const categories = useMemo(() => {
|
|
16
12
|
const map = new Map<string, FlowEntry[]>();
|
|
17
13
|
for (const flow of flows) {
|
|
@@ -22,67 +18,59 @@ export function Sidebar({ flows, selectedId, onSelect }: SidebarProps) {
|
|
|
22
18
|
return Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
23
19
|
}, [flows]);
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
21
|
+
const selectedFlow = flows.find((f) => f.id === selectedId);
|
|
22
|
+
const selectedCategory = selectedFlow?.category ?? null;
|
|
23
|
+
|
|
24
|
+
function handleCategoryClick(items: FlowEntry[]) {
|
|
25
|
+
if (items.length > 0) {
|
|
26
|
+
onSelect(items[0].id);
|
|
27
|
+
}
|
|
32
28
|
}
|
|
33
29
|
|
|
34
30
|
return (
|
|
35
|
-
<aside
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
<aside
|
|
32
|
+
className="w-64 overflow-y-auto shrink-0 border-r"
|
|
33
|
+
style={{
|
|
34
|
+
background: 'var(--fb-bg-sidebar)',
|
|
35
|
+
borderColor: 'var(--fb-border)',
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
<div className="p-5">
|
|
39
|
+
<h2
|
|
40
|
+
className="text-xs font-bold uppercase tracking-widest mb-5"
|
|
41
|
+
style={{ color: 'var(--fb-text-primary)' }}
|
|
42
|
+
>
|
|
43
|
+
Categories
|
|
44
|
+
</h2>
|
|
45
|
+
<nav className="space-y-1">
|
|
46
|
+
{categories.map(([category, items]) => {
|
|
47
|
+
const isActive = selectedCategory === category;
|
|
39
48
|
|
|
40
|
-
|
|
41
|
-
<div key={category}>
|
|
49
|
+
return (
|
|
42
50
|
<button
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
key={category}
|
|
52
|
+
onClick={() => handleCategoryClick(items)}
|
|
53
|
+
className={`w-full text-left px-4 py-2.5 rounded-lg text-sm transition-colors ${
|
|
54
|
+
isActive
|
|
55
|
+
? "font-medium"
|
|
56
|
+
: "hover:bg-white/5"
|
|
57
|
+
}`}
|
|
58
|
+
style={{
|
|
59
|
+
color: isActive ? 'var(--fb-accent-green)' : 'var(--fb-text-secondary)',
|
|
60
|
+
background: isActive ? 'rgba(74, 222, 128, 0.08)' : undefined,
|
|
61
|
+
}}
|
|
45
62
|
>
|
|
46
|
-
<svg
|
|
47
|
-
className={`w-3 h-3 transition-transform ${isCollapsed ? "" : "rotate-90"}`}
|
|
48
|
-
viewBox="0 0 24 24"
|
|
49
|
-
fill="currentColor"
|
|
50
|
-
>
|
|
51
|
-
<path d="M8 5l8 7-8 7z" />
|
|
52
|
-
</svg>
|
|
53
63
|
{category}
|
|
54
|
-
<span className="ml-auto text-zinc-600 font-normal normal-case">
|
|
55
|
-
{items.length}
|
|
56
|
-
</span>
|
|
57
64
|
</button>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
: "text-zinc-400 hover:bg-zinc-800/60 hover:text-zinc-200"
|
|
68
|
-
}`}
|
|
69
|
-
title={flow.title}
|
|
70
|
-
>
|
|
71
|
-
{flow.title}
|
|
72
|
-
</button>
|
|
73
|
-
</li>
|
|
74
|
-
))}
|
|
75
|
-
</ul>
|
|
76
|
-
)}
|
|
77
|
-
</div>
|
|
78
|
-
);
|
|
79
|
-
})}
|
|
80
|
-
{categories.length === 0 && (
|
|
81
|
-
<p className="text-sm text-zinc-600 px-2 py-4 text-center">
|
|
82
|
-
No flows match your search
|
|
83
|
-
</p>
|
|
84
|
-
)}
|
|
85
|
-
</nav>
|
|
65
|
+
);
|
|
66
|
+
})}
|
|
67
|
+
{categories.length === 0 && (
|
|
68
|
+
<p className="text-sm px-2 py-4 text-center" style={{ color: 'var(--fb-text-muted)' }}>
|
|
69
|
+
No flows found
|
|
70
|
+
</p>
|
|
71
|
+
)}
|
|
72
|
+
</nav>
|
|
73
|
+
</div>
|
|
86
74
|
</aside>
|
|
87
75
|
);
|
|
88
76
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -2,15 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
:root {
|
|
4
4
|
color-scheme: dark;
|
|
5
|
+
--fb-bg-outer: #0b0e17;
|
|
6
|
+
--fb-bg-window: #111827;
|
|
7
|
+
--fb-bg-titlebar: #161b28;
|
|
8
|
+
--fb-bg-content: #131a2b;
|
|
9
|
+
--fb-bg-sidebar: #111827;
|
|
10
|
+
--fb-border: #1e293b;
|
|
11
|
+
--fb-text-primary: #e2e8f0;
|
|
12
|
+
--fb-text-secondary: #94a3b8;
|
|
13
|
+
--fb-text-muted: #64748b;
|
|
14
|
+
--fb-accent-green: #4ade80;
|
|
15
|
+
--fb-accent-violet: #8b5cf6;
|
|
5
16
|
}
|
|
6
17
|
|
|
7
18
|
body {
|
|
8
19
|
margin: 0;
|
|
9
20
|
font-family:
|
|
10
|
-
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
11
|
-
Arial, sans-serif;
|
|
21
|
+
"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
22
|
+
"Helvetica Neue", Arial, sans-serif;
|
|
12
23
|
-webkit-font-smoothing: antialiased;
|
|
13
24
|
-moz-osx-font-smoothing: grayscale;
|
|
25
|
+
background: var(--fb-bg-outer);
|
|
14
26
|
}
|
|
15
27
|
|
|
16
28
|
/* Mermaid SVG sizing */
|
|
@@ -30,10 +42,10 @@ body {
|
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
::-webkit-scrollbar-thumb {
|
|
33
|
-
background: #
|
|
45
|
+
background: #1e293b;
|
|
34
46
|
border-radius: 3px;
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
::-webkit-scrollbar-thumb:hover {
|
|
38
|
-
background: #
|
|
50
|
+
background: #334155;
|
|
39
51
|
}
|