flowbook 0.2.14 → 0.2.16
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
CHANGED
package/src/client/App.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { EmptyState } from "./components/EmptyState";
|
|
|
6
6
|
|
|
7
7
|
export function App() {
|
|
8
8
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
9
|
-
const [searchQuery] = useState("");
|
|
9
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
10
10
|
|
|
11
11
|
const filteredFlows = useMemo(() => {
|
|
12
12
|
if (!searchQuery) return data.flows;
|
|
@@ -38,14 +38,18 @@ export function App() {
|
|
|
38
38
|
<span className="ml-4 text-sm font-mono" style={{ color: 'var(--fb-text-muted)' }}>
|
|
39
39
|
flowbook — localhost:6200
|
|
40
40
|
</span>
|
|
41
|
+
<span className="ml-auto text-sm font-mono" style={{ color: 'var(--fb-text-muted)' }}>
|
|
42
|
+
{data.flows.length} flows
|
|
43
|
+
</span>
|
|
41
44
|
</div>
|
|
42
|
-
|
|
43
45
|
{/* Content area */}
|
|
44
46
|
<div className="flex" style={{ height: '520px' }}>
|
|
45
47
|
<Sidebar
|
|
46
48
|
flows={filteredFlows}
|
|
47
49
|
selectedId={selectedId}
|
|
48
50
|
onSelect={setSelectedId}
|
|
51
|
+
searchQuery={searchQuery}
|
|
52
|
+
onSearchChange={setSearchQuery}
|
|
49
53
|
/>
|
|
50
54
|
<main className="flex-1 overflow-auto" style={{ background: 'var(--fb-bg-content)' }}>
|
|
51
55
|
{selectedFlow ? (
|
|
@@ -8,10 +8,22 @@ interface FlowViewProps {
|
|
|
8
8
|
export function FlowView({ flow }: FlowViewProps) {
|
|
9
9
|
return (
|
|
10
10
|
<div className="p-8 max-w-5xl mx-auto">
|
|
11
|
-
{/* Title */}
|
|
12
|
-
<
|
|
13
|
-
{
|
|
14
|
-
|
|
11
|
+
{/* Title + Category badge */}
|
|
12
|
+
<div className="flex items-center gap-3 mb-1">
|
|
13
|
+
<h1 className="text-2xl font-bold" style={{ color: 'var(--fb-text-primary)' }}>
|
|
14
|
+
{flow.title}
|
|
15
|
+
</h1>
|
|
16
|
+
<span
|
|
17
|
+
className="px-2.5 py-0.5 rounded-md text-xs font-medium"
|
|
18
|
+
style={{
|
|
19
|
+
background: 'rgba(99, 102, 241, 0.12)',
|
|
20
|
+
color: '#818cf8',
|
|
21
|
+
border: '1px solid rgba(99, 102, 241, 0.2)',
|
|
22
|
+
}}
|
|
23
|
+
>
|
|
24
|
+
{flow.category}
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
15
27
|
|
|
16
28
|
{/* Description */}
|
|
17
29
|
{flow.description && (
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
2
|
import type { FlowEntry } from "../../types";
|
|
3
3
|
|
|
4
4
|
interface SidebarProps {
|
|
5
5
|
flows: FlowEntry[];
|
|
6
6
|
selectedId: string | null;
|
|
7
7
|
onSelect: (id: string) => void;
|
|
8
|
+
searchQuery: string;
|
|
9
|
+
onSearchChange: (query: string) => void;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
export function Sidebar({ flows, selectedId, onSelect }: SidebarProps) {
|
|
12
|
+
export function Sidebar({ flows, selectedId, onSelect, searchQuery, onSearchChange }: SidebarProps) {
|
|
11
13
|
const categories = useMemo(() => {
|
|
12
14
|
const map = new Map<string, FlowEntry[]>();
|
|
13
15
|
for (const flow of flows) {
|
|
@@ -21,45 +23,107 @@ export function Sidebar({ flows, selectedId, onSelect }: SidebarProps) {
|
|
|
21
23
|
return Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
22
24
|
}, [flows]);
|
|
23
25
|
|
|
26
|
+
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
|
|
27
|
+
|
|
28
|
+
function toggleCategory(category: string) {
|
|
29
|
+
setCollapsed((prev) => {
|
|
30
|
+
const next = new Set(prev);
|
|
31
|
+
if (next.has(category)) next.delete(category);
|
|
32
|
+
else next.add(category);
|
|
33
|
+
return next;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
return (
|
|
25
38
|
<aside
|
|
26
|
-
className="w-64 overflow-y-auto shrink-0 border-r"
|
|
39
|
+
className="w-64 overflow-y-auto shrink-0 border-r flex flex-col"
|
|
27
40
|
style={{
|
|
28
41
|
background: 'var(--fb-bg-sidebar)',
|
|
29
42
|
borderColor: 'var(--fb-border)',
|
|
30
43
|
}}
|
|
31
44
|
>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
{/* Search */}
|
|
46
|
+
<div className="p-3 border-b" style={{ borderColor: 'var(--fb-border)' }}>
|
|
47
|
+
<div className="relative">
|
|
48
|
+
<svg
|
|
49
|
+
className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4"
|
|
50
|
+
style={{ color: 'var(--fb-text-muted)' }}
|
|
51
|
+
fill="none"
|
|
52
|
+
viewBox="0 0 24 24"
|
|
53
|
+
stroke="currentColor"
|
|
54
|
+
strokeWidth={2}
|
|
55
|
+
>
|
|
56
|
+
<circle cx="11" cy="11" r="8" />
|
|
57
|
+
<path d="m21 21-4.35-4.35" />
|
|
58
|
+
</svg>
|
|
59
|
+
<input
|
|
60
|
+
type="text"
|
|
61
|
+
placeholder="Search flows..."
|
|
62
|
+
value={searchQuery}
|
|
63
|
+
onChange={(e) => onSearchChange(e.target.value)}
|
|
64
|
+
className="w-full pl-9 pr-3 py-2 rounded-lg text-sm outline-none"
|
|
65
|
+
style={{
|
|
66
|
+
background: 'rgba(255,255,255,0.05)',
|
|
67
|
+
color: 'var(--fb-text-primary)',
|
|
68
|
+
border: '1px solid var(--fb-border)',
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{/* Categories */}
|
|
75
|
+
<nav className="p-3 space-y-1 flex-1 overflow-y-auto">
|
|
76
|
+
{categories.map(([category, items]) => {
|
|
77
|
+
const isCollapsed = collapsed.has(category);
|
|
78
|
+
return (
|
|
79
|
+
<div key={category}>
|
|
80
|
+
<button
|
|
81
|
+
onClick={() => toggleCategory(category)}
|
|
82
|
+
className="w-full flex items-center gap-1.5 px-2 py-2 rounded-lg text-xs font-bold uppercase tracking-widest hover:bg-white/5 transition-colors"
|
|
83
|
+
style={{ color: 'var(--fb-text-muted)' }}
|
|
84
|
+
>
|
|
85
|
+
<svg
|
|
86
|
+
className="w-3 h-3 transition-transform"
|
|
87
|
+
style={{ transform: isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)' }}
|
|
88
|
+
fill="currentColor"
|
|
89
|
+
viewBox="0 0 20 20"
|
|
90
|
+
>
|
|
91
|
+
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
|
|
92
|
+
</svg>
|
|
93
|
+
<span className="flex-1 text-left">{category}</span>
|
|
94
|
+
<span
|
|
95
|
+
className="text-xs font-medium tabular-nums"
|
|
96
|
+
style={{ color: 'var(--fb-text-muted)', opacity: 0.7 }}
|
|
97
|
+
>
|
|
98
|
+
{items.length}
|
|
99
|
+
</span>
|
|
100
|
+
</button>
|
|
101
|
+
{!isCollapsed && (
|
|
102
|
+
<div className="space-y-0.5 mt-0.5">
|
|
103
|
+
{items.map((flow) => {
|
|
104
|
+
const isActive = selectedId === flow.id;
|
|
105
|
+
return (
|
|
106
|
+
<button
|
|
107
|
+
key={flow.id}
|
|
108
|
+
onClick={() => onSelect(flow.id)}
|
|
109
|
+
className={`w-full text-left px-4 py-2 rounded-lg text-sm transition-colors ${
|
|
110
|
+
isActive ? 'font-medium' : 'hover:bg-white/5'
|
|
111
|
+
}`}
|
|
112
|
+
style={{
|
|
113
|
+
color: isActive ? 'var(--fb-accent-green)' : 'var(--fb-text-secondary)',
|
|
114
|
+
background: isActive ? 'rgba(74, 222, 128, 0.08)' : undefined,
|
|
115
|
+
paddingLeft: '1.75rem',
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
{flow.title}
|
|
119
|
+
</button>
|
|
120
|
+
);
|
|
121
|
+
})}
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
60
124
|
</div>
|
|
61
|
-
|
|
62
|
-
)
|
|
125
|
+
);
|
|
126
|
+
})}
|
|
63
127
|
{categories.length === 0 && (
|
|
64
128
|
<p className="text-sm px-2 py-4 text-center" style={{ color: 'var(--fb-text-muted)' }}>
|
|
65
129
|
No flows found
|