claude-plan-viewer 1.3.0 → 1.4.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.
@@ -16,6 +16,7 @@ export function App() {
16
16
  const [selectedPlan, setSelectedPlan] = useState<Plan | null>(null);
17
17
  const [showOverlay, setShowOverlay] = useState(false);
18
18
  const [showHelp, setShowHelp] = useState(false);
19
+ const [copied, setCopied] = useState(false);
19
20
 
20
21
  // Filter state
21
22
  const {
@@ -35,11 +36,11 @@ export function App() {
35
36
  // Convert Set to array for API
36
37
  const projectsArray = useMemo(
37
38
  () => Array.from(selectedProjects),
38
- [selectedProjects]
39
+ [selectedProjects],
39
40
  );
40
41
 
41
42
  // Fetch plans with server-side filtering
42
- const { plans, loading, refresh, ensureContent } = usePlans({
43
+ const { plans, loading, refreshing, refresh, ensureContent } = usePlans({
43
44
  q: debouncedSearch,
44
45
  sort: sortKey,
45
46
  dir: sortDir,
@@ -52,6 +53,15 @@ export function App() {
52
53
  // Load content when plan is selected
53
54
  const handleSelectPlan = useCallback(
54
55
  async (plan: Plan | null) => {
56
+ // Update URL query parameter
57
+ const url = new URL(window.location.href);
58
+ if (plan) {
59
+ url.searchParams.set("plan", plan.filename);
60
+ } else {
61
+ url.searchParams.delete("plan");
62
+ }
63
+ window.history.replaceState({}, "", url.toString());
64
+
55
65
  if (plan) {
56
66
  const withContent = await ensureContent(plan);
57
67
  setSelectedPlan(withContent);
@@ -59,7 +69,7 @@ export function App() {
59
69
  setSelectedPlan(null);
60
70
  }
61
71
  },
62
- [ensureContent]
72
+ [ensureContent],
63
73
  );
64
74
 
65
75
  // Open in editor
@@ -74,6 +84,20 @@ export function App() {
74
84
  navigator.clipboard.writeText(`claude --resume ${sessionId}`);
75
85
  }, []);
76
86
 
87
+ // Copy filepath
88
+ const handleCopyFilepath = useCallback((filepath: string) => {
89
+ navigator.clipboard.writeText(filepath);
90
+ }, []);
91
+
92
+ // Copy plan content
93
+ const handleCopyPlan = useCallback(() => {
94
+ if (selectedPlan?.content) {
95
+ navigator.clipboard.writeText(selectedPlan.content);
96
+ setCopied(true);
97
+ setTimeout(() => setCopied(false), 2000);
98
+ }
99
+ }, [selectedPlan]);
100
+
77
101
  // Clear search
78
102
  const handleClearSearch = useCallback(() => {
79
103
  setSearchQuery("");
@@ -92,19 +116,34 @@ export function App() {
92
116
  onClearSearch: handleClearSearch,
93
117
  });
94
118
 
95
- // Auto-select first plan when plans change
119
+ // Select plan from URL or auto-select first plan
96
120
  useEffect(() => {
97
- if (!selectedPlan && plans.length > 0) {
98
- const firstPlan = plans[0];
99
- if (firstPlan) {
100
- handleSelectPlan(firstPlan);
121
+ if (plans.length === 0) return;
122
+
123
+ // Check URL for plan parameter
124
+ const url = new URL(window.location.href);
125
+ const planFromUrl = url.searchParams.get("plan");
126
+
127
+ if (planFromUrl) {
128
+ const matchingPlan = plans.find((p) => p.filename === planFromUrl);
129
+ if (matchingPlan && selectedPlan?.filename !== planFromUrl) {
130
+ handleSelectPlan(matchingPlan);
131
+ return;
101
132
  }
102
133
  }
134
+
135
+ // Fall back to selecting first plan if nothing selected
136
+ if (!selectedPlan) {
137
+ handleSelectPlan(plans[0]);
138
+ }
103
139
  }, [plans, selectedPlan, handleSelectPlan]);
104
140
 
105
141
  // Clear selection if selected plan is no longer in results
106
142
  useEffect(() => {
107
- if (selectedPlan && !plans.find((p) => p.filename === selectedPlan.filename)) {
143
+ if (
144
+ selectedPlan &&
145
+ !plans.find((p) => p.filename === selectedPlan.filename)
146
+ ) {
108
147
  setSelectedPlan(null);
109
148
  }
110
149
  }, [plans, selectedPlan]);
@@ -130,6 +169,7 @@ export function App() {
130
169
  onToggleProject={toggleProject}
131
170
  onClearProjects={clearProjects}
132
171
  onRefresh={refresh}
172
+ refreshing={refreshing}
133
173
  />
134
174
 
135
175
  {plans.length === 0 ? (
@@ -156,6 +196,9 @@ export function App() {
156
196
  onOpenEditor={handleOpenEditor}
157
197
  onToggleOverlay={() => setShowOverlay(true)}
158
198
  onCopySession={handleCopySession}
199
+ onCopyFilepath={handleCopyFilepath}
200
+ onCopyPlan={handleCopyPlan}
201
+ copied={copied}
159
202
  />
160
203
 
161
204
  {showOverlay && selectedPlan && (
@@ -164,6 +207,8 @@ export function App() {
164
207
  onClose={() => setShowOverlay(false)}
165
208
  onOpenEditor={handleOpenEditor}
166
209
  onCopySession={handleCopySession}
210
+ onCopyPlan={handleCopyPlan}
211
+ copied={copied}
167
212
  />
168
213
  )}
169
214
 
@@ -1,4 +1,5 @@
1
- import { useEffect, useCallback } from "react";
1
+ import { useEffect, useCallback, useRef } from "react";
2
+ import { useFocusTrap } from "../hooks/useFocusTrap";
2
3
  import type { Plan } from "../types.ts";
3
4
  import { formatFullDate, formatSize } from "../utils/formatters.ts";
4
5
  import { Markdown } from "./Markdown.tsx";
@@ -8,6 +9,8 @@ interface DetailOverlayProps {
8
9
  onClose: () => void;
9
10
  onOpenEditor: () => void;
10
11
  onCopySession: (sessionId: string) => void;
12
+ onCopyPlan: () => void;
13
+ copied: boolean;
11
14
  }
12
15
 
13
16
  export function DetailOverlay({
@@ -15,7 +18,12 @@ export function DetailOverlay({
15
18
  onClose,
16
19
  onOpenEditor,
17
20
  onCopySession,
21
+ onCopyPlan,
22
+ copied,
18
23
  }: DetailOverlayProps) {
24
+ const panelRef = useRef<HTMLDivElement>(null);
25
+ useFocusTrap(panelRef, true);
26
+
19
27
  const handleKeyDown = useCallback(
20
28
  (e: KeyboardEvent) => {
21
29
  if (e.key === "Escape" || e.key === "f") {
@@ -23,7 +31,7 @@ export function DetailOverlay({
23
31
  onClose();
24
32
  }
25
33
  },
26
- [onClose]
34
+ [onClose],
27
35
  );
28
36
 
29
37
  useEffect(() => {
@@ -43,11 +51,20 @@ export function DetailOverlay({
43
51
  aria-hidden="false"
44
52
  onClick={onClose}
45
53
  >
46
- <div className="detail-overlay-panel" onClick={(e) => e.stopPropagation()}>
54
+ <div
55
+ ref={panelRef}
56
+ className="detail-overlay-panel"
57
+ role="dialog"
58
+ aria-modal="true"
59
+ aria-labelledby="detail-overlay-title"
60
+ onClick={(e) => e.stopPropagation()}
61
+ >
47
62
  <div className="detail-overlay-bar">
48
63
  <div className="detail-meta detail-overlay-meta">
49
- {plan.project && <span className="project-tag">{plan.project}</span>}
50
- <span>{plan.filename}</span>
64
+ {plan.project && (
65
+ <span className="project-tag">{plan.project}</span>
66
+ )}
67
+ <span id="detail-overlay-title">{plan.filename}</span>
51
68
  <span>{formatFullDate(plan.modified)}</span>
52
69
  <span>{formatSize(plan.size)}</span>
53
70
  <span>{plan.lineCount} lines</span>
@@ -61,14 +78,42 @@ export function DetailOverlay({
61
78
  </button>
62
79
  )}
63
80
  </div>
81
+ <button
82
+ className={copied ? "action-btn copied" : "action-btn"}
83
+ onClick={onCopyPlan}
84
+ title="Copy plan to clipboard"
85
+ >
86
+ <svg
87
+ xmlns="http://www.w3.org/2000/svg"
88
+ viewBox="0 0 24 24"
89
+ fill="none"
90
+ stroke="currentColor"
91
+ strokeWidth="2"
92
+ strokeLinecap="round"
93
+ strokeLinejoin="round"
94
+ className="icon"
95
+ >
96
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
97
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
98
+ </svg>
99
+ </button>
64
100
  <button
65
101
  className="action-btn"
66
102
  onClick={onOpenEditor}
67
103
  title="Open in editor (Enter)"
68
104
  >
69
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="icon">
70
- <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
71
- <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
105
+ <svg
106
+ xmlns="http://www.w3.org/2000/svg"
107
+ viewBox="0 0 24 24"
108
+ fill="none"
109
+ stroke="currentColor"
110
+ strokeWidth="2"
111
+ strokeLinecap="round"
112
+ strokeLinejoin="round"
113
+ className="icon"
114
+ >
115
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
116
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
72
117
  </svg>
73
118
  </button>
74
119
  <button
@@ -76,7 +121,19 @@ export function DetailOverlay({
76
121
  onClick={onClose}
77
122
  title="Close fullscreen (Esc or F)"
78
123
  >
79
- ×
124
+ <svg
125
+ width="14"
126
+ height="14"
127
+ viewBox="0 0 24 24"
128
+ fill="none"
129
+ stroke="currentColor"
130
+ strokeWidth="2.5"
131
+ strokeLinecap="round"
132
+ strokeLinejoin="round"
133
+ >
134
+ <line x1="18" y1="6" x2="6" y2="18" />
135
+ <line x1="6" y1="6" x2="18" y2="18" />
136
+ </svg>
80
137
  </button>
81
138
  </div>
82
139
  <div className="detail-overlay-content">
@@ -1,5 +1,10 @@
1
1
  import type { Plan } from "../types.ts";
2
- import { formatFullDate, formatDateISO, formatSize } from "../utils/formatters.ts";
2
+ import {
3
+ formatDate,
4
+ formatCompactDateTime,
5
+ formatCreatedShort,
6
+ isSameDay,
7
+ } from "../utils/formatters.ts";
3
8
  import { Markdown } from "./Markdown.tsx";
4
9
 
5
10
  interface DetailPanelProps {
@@ -7,6 +12,9 @@ interface DetailPanelProps {
7
12
  onOpenEditor: () => void;
8
13
  onToggleOverlay: () => void;
9
14
  onCopySession: (sessionId: string) => void;
15
+ onCopyFilepath: (filepath: string) => void;
16
+ onCopyPlan: () => void;
17
+ copied: boolean;
10
18
  }
11
19
 
12
20
  export function DetailPanel({
@@ -14,13 +22,18 @@ export function DetailPanel({
14
22
  onOpenEditor,
15
23
  onToggleOverlay,
16
24
  onCopySession,
25
+ onCopyFilepath,
26
+ onCopyPlan,
27
+ copied,
17
28
  }: DetailPanelProps) {
18
29
  if (!plan) {
19
30
  return (
20
31
  <div id="detail-panel" className="detail-panel">
21
32
  <div className="detail-empty">
22
33
  <p>Select a plan to view details</p>
23
- <p className="hint">Use ↑↓ arrows to navigate, Enter to open in editor</p>
34
+ <p className="hint">
35
+ Use ↑↓ arrows to navigate, Enter to open in editor
36
+ </p>
24
37
  </div>
25
38
  </div>
26
39
  );
@@ -32,14 +45,42 @@ export function DetailPanel({
32
45
  <div className="detail-header-top">
33
46
  <h2 className="detail-title">{plan.title}</h2>
34
47
  <div className="detail-actions">
48
+ <button
49
+ className={copied ? "action-btn copied" : "action-btn"}
50
+ onClick={onCopyPlan}
51
+ title="Copy plan to clipboard"
52
+ >
53
+ <svg
54
+ xmlns="http://www.w3.org/2000/svg"
55
+ viewBox="0 0 24 24"
56
+ fill="none"
57
+ stroke="currentColor"
58
+ strokeWidth="2"
59
+ strokeLinecap="round"
60
+ strokeLinejoin="round"
61
+ className="icon"
62
+ >
63
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
64
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
65
+ </svg>
66
+ </button>
35
67
  <button
36
68
  className="action-btn"
37
69
  onClick={onOpenEditor}
38
70
  title="Open in editor (Enter)"
39
71
  >
40
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="icon">
41
- <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
42
- <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
72
+ <svg
73
+ xmlns="http://www.w3.org/2000/svg"
74
+ viewBox="0 0 24 24"
75
+ fill="none"
76
+ stroke="currentColor"
77
+ strokeWidth="2"
78
+ strokeLinecap="round"
79
+ strokeLinejoin="round"
80
+ className="icon"
81
+ >
82
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
83
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
43
84
  </svg>
44
85
  </button>
45
86
  <button
@@ -53,13 +94,24 @@ export function DetailPanel({
53
94
  </div>
54
95
 
55
96
  <div className="detail-meta">
56
- <span title={plan.modified}>
57
- Modified: {formatFullDate(plan.modified)}
58
- </span>
59
- <span>Created: {formatDateISO(plan.created)}</span>
60
-
97
+ <span title={plan.modified}>{formatDate(plan.modified)}</span>
98
+ <span>{formatCompactDateTime(plan.modified)}</span>
99
+ {!isSameDay(plan.modified, plan.created) && (
100
+ <span className="created-date">
101
+ (created {formatCreatedShort(plan.created)})
102
+ </span>
103
+ )}
61
104
  <span>{plan.wordCount} words</span>
62
- {plan.project && <span className="project-badge">{plan.project}</span>}
105
+ <button
106
+ className="filename-tag"
107
+ onClick={() => onCopyFilepath(plan.filepath)}
108
+ title={`Click to copy: ${plan.filepath}`}
109
+ >
110
+ {plan.filename}
111
+ </button>
112
+ {plan.project && (
113
+ <span className="project-badge">{plan.project}</span>
114
+ )}
63
115
  {plan.sessionId && (
64
116
  <button
65
117
  className="session-tag"
@@ -9,6 +9,7 @@ interface HeaderProps {
9
9
  onToggleProject: (project: string) => void;
10
10
  onClearProjects: () => void;
11
11
  onRefresh: () => void;
12
+ refreshing: boolean;
12
13
  }
13
14
 
14
15
  export function Header({
@@ -19,13 +20,19 @@ export function Header({
19
20
  onToggleProject,
20
21
  onClearProjects,
21
22
  onRefresh,
23
+ refreshing,
22
24
  }: HeaderProps) {
23
25
  return (
24
26
  <div className="header">
25
27
  <div className="header-row">
26
28
  <h1>
27
- <svg className="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
28
- <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
29
+ <svg
30
+ className="icon"
31
+ xmlns="http://www.w3.org/2000/svg"
32
+ viewBox="0 0 24 24"
33
+ fill="currentColor"
34
+ >
35
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
29
36
  </svg>
30
37
  Claude Plan Viewer
31
38
  </h1>
@@ -40,13 +47,25 @@ export function Header({
40
47
  />
41
48
  )}
42
49
  <button
43
- className="action-btn"
50
+ className={refreshing ? "action-btn loading" : "action-btn"}
44
51
  id="refresh-btn"
45
52
  onClick={onRefresh}
53
+ disabled={refreshing}
46
54
  title="Refresh plans"
47
55
  >
48
- <svg className="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
49
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
56
+ <svg
57
+ className="icon"
58
+ xmlns="http://www.w3.org/2000/svg"
59
+ fill="none"
60
+ viewBox="0 0 24 24"
61
+ stroke="currentColor"
62
+ >
63
+ <path
64
+ strokeLinecap="round"
65
+ strokeLinejoin="round"
66
+ strokeWidth="2"
67
+ d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
68
+ />
50
69
  </svg>
51
70
  </button>
52
71
  </div>
@@ -1,4 +1,5 @@
1
- import { useEffect, useCallback } from "react";
1
+ import { useEffect, useCallback, useRef } from "react";
2
+ import { useFocusTrap } from "../hooks/useFocusTrap";
2
3
 
3
4
  interface HelpModalProps {
4
5
  onClose: () => void;
@@ -14,6 +15,9 @@ const SHORTCUTS = [
14
15
  ];
15
16
 
16
17
  export function HelpModal({ onClose }: HelpModalProps) {
18
+ const modalRef = useRef<HTMLDivElement>(null);
19
+ useFocusTrap(modalRef, true);
20
+
17
21
  const handleKeyDown = useCallback(
18
22
  (e: KeyboardEvent) => {
19
23
  if (e.key === "Escape" || e.key === "?") {
@@ -21,7 +25,7 @@ export function HelpModal({ onClose }: HelpModalProps) {
21
25
  onClose();
22
26
  }
23
27
  },
24
- [onClose]
28
+ [onClose],
25
29
  );
26
30
 
27
31
  useEffect(() => {
@@ -30,12 +34,31 @@ export function HelpModal({ onClose }: HelpModalProps) {
30
34
  }, [handleKeyDown]);
31
35
 
32
36
  return (
33
- <div className="modal-overlay" onClick={onClose}>
34
- <div className="modal-content help-modal" onClick={(e) => e.stopPropagation()}>
37
+ <div className="modal-backdrop" onClick={onClose}>
38
+ <div
39
+ ref={modalRef}
40
+ className="modal help-modal"
41
+ role="dialog"
42
+ aria-modal="true"
43
+ aria-labelledby="help-modal-title"
44
+ onClick={(e) => e.stopPropagation()}
45
+ >
35
46
  <div className="modal-header">
36
- <h3>Keyboard Shortcuts</h3>
37
- <button className="btn btn-secondary" onClick={onClose}>
38
-
47
+ <h3 id="help-modal-title">Keyboard Shortcuts</h3>
48
+ <button className="modal-close" onClick={onClose} title="Close (Esc)">
49
+ <svg
50
+ width="14"
51
+ height="14"
52
+ viewBox="0 0 24 24"
53
+ fill="none"
54
+ stroke="currentColor"
55
+ strokeWidth="2.5"
56
+ strokeLinecap="round"
57
+ strokeLinejoin="round"
58
+ >
59
+ <line x1="18" y1="6" x2="6" y2="18" />
60
+ <line x1="6" y1="6" x2="18" y2="18" />
61
+ </svg>
39
62
  </button>
40
63
  </div>
41
64
  <div className="modal-body">
@@ -1,16 +1,46 @@
1
- import { renderMarkdown } from "../utils/markdown.ts";
1
+ import ReactMarkdown from "react-markdown";
2
+ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
3
+ import { atomDark } from "react-syntax-highlighter/dist/esm/styles/prism";
4
+ import remarkGfm from "remark-gfm";
2
5
 
3
6
  interface MarkdownProps {
4
7
  content: string;
5
8
  }
6
9
 
7
10
  export function Markdown({ content }: MarkdownProps) {
8
- const html = renderMarkdown(content);
9
-
10
11
  return (
11
- <div
12
- className="markdown"
13
- dangerouslySetInnerHTML={{ __html: html }}
14
- />
12
+ <div className="markdown">
13
+ <ReactMarkdown
14
+ remarkPlugins={[remarkGfm]}
15
+ components={{
16
+ code({ inline, className, children, ...props }) {
17
+ const match = /language-(\w+)/.exec(className || "");
18
+ return !inline && match ? (
19
+ <SyntaxHighlighter
20
+ style={atomDark}
21
+ language={match[1]}
22
+ PreTag="div"
23
+ {...props}
24
+ >
25
+ {String(children).replace(/\n$/, "")}
26
+ </SyntaxHighlighter>
27
+ ) : (
28
+ <code className={className} {...props}>
29
+ {children}
30
+ </code>
31
+ );
32
+ },
33
+ table({ children, ...props }) {
34
+ return (
35
+ <div className="markdown-table-wrapper">
36
+ <table {...props}>{children}</table>
37
+ </div>
38
+ );
39
+ },
40
+ }}
41
+ >
42
+ {content}
43
+ </ReactMarkdown>
44
+ </div>
15
45
  );
16
46
  }
@@ -9,7 +9,12 @@ interface PlanRowProps {
9
9
  onSelect: (plan: Plan) => void;
10
10
  }
11
11
 
12
- export function PlanRow({ plan, selected, searchQuery, onSelect }: PlanRowProps) {
12
+ export function PlanRow({
13
+ plan,
14
+ selected,
15
+ searchQuery,
16
+ onSelect,
17
+ }: PlanRowProps) {
13
18
  return (
14
19
  <tr
15
20
  className={selected ? "selected" : ""}
@@ -22,14 +27,18 @@ export function PlanRow({ plan, selected, searchQuery, onSelect }: PlanRowProps)
22
27
  data-filename={plan.filename}
23
28
  title={plan.title}
24
29
  dangerouslySetInnerHTML={{
25
- __html: searchQuery ? highlightText(plan.title, searchQuery) : plan.title,
30
+ __html: searchQuery
31
+ ? highlightText(plan.title, searchQuery)
32
+ : plan.title,
26
33
  }}
27
34
  />
28
35
  </td>
29
36
  <td className="filename-cell">
30
37
  <span
31
38
  dangerouslySetInnerHTML={{
32
- __html: searchQuery ? highlightText(plan.filename, searchQuery) : plan.filename,
39
+ __html: searchQuery
40
+ ? highlightText(plan.filename, searchQuery)
41
+ : plan.filename,
33
42
  }}
34
43
  />
35
44
  </td>
@@ -37,7 +46,9 @@ export function PlanRow({ plan, selected, searchQuery, onSelect }: PlanRowProps)
37
46
  {plan.project ? (
38
47
  <span
39
48
  dangerouslySetInnerHTML={{
40
- __html: searchQuery ? highlightText(plan.project, searchQuery) : plan.project,
49
+ __html: searchQuery
50
+ ? highlightText(plan.project, searchQuery)
51
+ : plan.project,
41
52
  }}
42
53
  />
43
54
  ) : (
@@ -20,11 +20,9 @@ export function ProjectFilter({
20
20
  .map((p) => ({ value: p, label: p }));
21
21
 
22
22
  const handleChange = (
23
- newValue: MultiValue<{ value: string; label: string }> | null
23
+ newValue: MultiValue<{ value: string; label: string }> | null,
24
24
  ) => {
25
- const selectedSet = new Set(
26
- (newValue ?? []).map((option) => option.value)
27
- );
25
+ const selectedSet = new Set((newValue ?? []).map((option) => option.value));
28
26
 
29
27
  projects.forEach((project) => {
30
28
  const isSelected = selectedSet.has(project);
@@ -36,10 +34,7 @@ export function ProjectFilter({
36
34
  };
37
35
 
38
36
  const customStyles = {
39
- control: (
40
- base: object,
41
- state: { isFocused: boolean }
42
- ) => ({
37
+ control: (base: object, state: { isFocused: boolean }) => ({
43
38
  ...base,
44
39
  background: "var(--bg-tertiary)",
45
40
  border: "1px solid var(--border)",
@@ -100,14 +95,14 @@ export function ProjectFilter({
100
95
  }),
101
96
  option: (
102
97
  base: object,
103
- state: { isSelected: boolean; isFocused: boolean }
98
+ state: { isSelected: boolean; isFocused: boolean },
104
99
  ) => ({
105
100
  ...base,
106
101
  background: state.isSelected
107
102
  ? "var(--accent-dim)"
108
103
  : state.isFocused
109
- ? "var(--bg-hover)"
110
- : "transparent",
104
+ ? "var(--bg-hover)"
105
+ : "transparent",
111
106
  color: "var(--text-primary)",
112
107
  fontSize: "12px",
113
108
  cursor: "pointer",
@@ -23,7 +23,13 @@ export function SearchInput({ value, onChange }: SearchInputProps) {
23
23
 
24
24
  return (
25
25
  <div className="search-wrapper">
26
- <svg className="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
26
+ <svg
27
+ className="search-icon"
28
+ viewBox="0 0 24 24"
29
+ fill="none"
30
+ stroke="currentColor"
31
+ strokeWidth="2"
32
+ >
27
33
  <circle cx="11" cy="11" r="8" />
28
34
  <path d="m21 21-4.35-4.35" />
29
35
  </svg>
@@ -4,7 +4,6 @@ export { PlansTable } from "./PlansTable.tsx";
4
4
  export { DetailPanel } from "./DetailPanel.tsx";
5
5
  export { DetailOverlay } from "./DetailOverlay.tsx";
6
6
  export { SearchInput } from "./SearchInput.tsx";
7
- export { SortDropdown } from "./SortDropdown.tsx";
8
7
  export { ProjectFilter } from "./ProjectFilter.tsx";
9
8
  export { HelpModal } from "./HelpModal.tsx";
10
9
  export { Header } from "./Header.tsx";