flowbook 0.2.15 → 0.2.17

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowbook",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "flowbook": "dist/cli.js"
@@ -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
- <h1 className="text-2xl font-bold mb-1" style={{ color: 'var(--fb-text-primary)' }}>
13
- {flow.title}
14
- </h1>
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 && (
@@ -121,6 +121,12 @@ export function MermaidRenderer({ code, className }: Props) {
121
121
  };
122
122
  }, [code, safeId]);
123
123
 
124
+ // Reset zoom & pan when switching to a different diagram
125
+ useEffect(() => {
126
+ setScale(1);
127
+ setPos({ x: 0, y: 0 });
128
+ }, [code]);
129
+
124
130
  // Wheel zoom (non-passive to allow preventDefault)
125
131
  useEffect(() => {
126
132
  const el = containerRef.current;
@@ -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
- <nav className="p-5 space-y-4">
33
- {categories.map(([category, items]) => (
34
- <div key={category}>
35
- <h3
36
- className="text-xs font-bold uppercase tracking-widest mb-2 px-2"
37
- style={{ color: 'var(--fb-text-muted)' }}
38
- >
39
- {category}
40
- </h3>
41
- <div className="space-y-0.5">
42
- {items.map((flow) => {
43
- const isActive = selectedId === flow.id;
44
- return (
45
- <button
46
- key={flow.id}
47
- onClick={() => onSelect(flow.id)}
48
- className={`w-full text-left px-4 py-2 rounded-lg text-sm transition-colors ${
49
- isActive ? 'font-medium' : 'hover:bg-white/5'
50
- }`}
51
- style={{
52
- color: isActive ? 'var(--fb-accent-green)' : 'var(--fb-text-secondary)',
53
- background: isActive ? 'rgba(74, 222, 128, 0.08)' : undefined,
54
- }}
55
- >
56
- {flow.title}
57
- </button>
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
- </div>
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