stratanodex 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.
Files changed (59) hide show
  1. package/README.md +251 -0
  2. package/dist/api/ApiError.js +10 -0
  3. package/dist/api/client.js +96 -0
  4. package/dist/commands/add.js +45 -0
  5. package/dist/commands/config.js +41 -0
  6. package/dist/commands/done.js +39 -0
  7. package/dist/commands/executor.js +457 -0
  8. package/dist/commands/list.js +86 -0
  9. package/dist/commands/login.js +1 -0
  10. package/dist/commands/loginFlow.js +86 -0
  11. package/dist/commands/logout.js +11 -0
  12. package/dist/commands/registry.js +351 -0
  13. package/dist/commands/resolver.js +184 -0
  14. package/dist/config.js +16 -0
  15. package/dist/index.js +77 -0
  16. package/dist/tui/App.js +141 -0
  17. package/dist/tui/components/AutocompleteOverlay.js +22 -0
  18. package/dist/tui/components/BottomBar.js +8 -0
  19. package/dist/tui/components/Breadcrumb.js +6 -0
  20. package/dist/tui/components/CommandInput.js +111 -0
  21. package/dist/tui/components/CommandPalette.js +1 -0
  22. package/dist/tui/components/ErrorBoundary.js +26 -0
  23. package/dist/tui/components/FocusMode.js +1 -0
  24. package/dist/tui/components/FolderItem.js +5 -0
  25. package/dist/tui/components/Header.js +5 -0
  26. package/dist/tui/components/Keybindings.js +5 -0
  27. package/dist/tui/components/ListItem.js +5 -0
  28. package/dist/tui/components/NodeRow.js +14 -0
  29. package/dist/tui/components/PriorityBadge.js +9 -0
  30. package/dist/tui/components/SearchOverlay.js +1 -0
  31. package/dist/tui/components/Spinner.js +23 -0
  32. package/dist/tui/components/StatusBadge.js +9 -0
  33. package/dist/tui/components/SuggestionItem.js +8 -0
  34. package/dist/tui/components/TopBar.js +13 -0
  35. package/dist/tui/components/TreeConnector.js +17 -0
  36. package/dist/tui/hooks/useAuth.js +37 -0
  37. package/dist/tui/hooks/useCommandInput.js +35 -0
  38. package/dist/tui/hooks/useFolders.js +16 -0
  39. package/dist/tui/hooks/useKeymap.js +30 -0
  40. package/dist/tui/hooks/useLists.js +16 -0
  41. package/dist/tui/hooks/useNavigation.js +15 -0
  42. package/dist/tui/hooks/useTree.js +93 -0
  43. package/dist/tui/screens/DailyScreen.js +77 -0
  44. package/dist/tui/screens/DashboardScreen.js +262 -0
  45. package/dist/tui/screens/HomeScreen.js +75 -0
  46. package/dist/tui/screens/ListsScreen.js +73 -0
  47. package/dist/tui/screens/LoginScreen.js +115 -0
  48. package/dist/tui/screens/NodeScreen.js +48 -0
  49. package/dist/tui/screens/TreeScreen.js +182 -0
  50. package/dist/tui/screens/WelcomeScreen.js +83 -0
  51. package/dist/tui/types.js +1 -0
  52. package/dist/types/index.js +1 -0
  53. package/dist/utils/auth.js +11 -0
  54. package/dist/utils/logger.js +32 -0
  55. package/dist/utils/numbering.js +38 -0
  56. package/dist/utils/recents.js +24 -0
  57. package/dist/utils/scoring.js +29 -0
  58. package/dist/utils/tree.js +125 -0
  59. package/package.json +74 -0
@@ -0,0 +1,93 @@
1
+ // Tree hook
2
+ import { useState, useEffect, useCallback, useMemo } from 'react';
3
+ import { getNodes } from '../../api/client.js';
4
+ import { assignNumbers } from '../../utils/numbering.js';
5
+ /**
6
+ * Reconstruct a nested tree from the flat node array the backend returns.
7
+ * Each node has a `parentId`; we group children under their parent.
8
+ */
9
+ function buildTree(flat) {
10
+ const map = new Map();
11
+ const roots = [];
12
+ for (const node of flat) {
13
+ map.set(node.id, { ...node, children: [] });
14
+ }
15
+ for (const node of flat) {
16
+ const mapped = map.get(node.id);
17
+ if (node.parentId && map.has(node.parentId)) {
18
+ map.get(node.parentId).children.push(mapped);
19
+ }
20
+ else {
21
+ roots.push(mapped);
22
+ }
23
+ }
24
+ return roots;
25
+ }
26
+ /** Collect ALL node IDs (at every depth) so we can auto-expand the entire tree. */
27
+ function collectAllIds(nodes) {
28
+ const ids = new Set();
29
+ function walk(list) {
30
+ for (const n of list) {
31
+ ids.add(n.id);
32
+ if (n.children?.length)
33
+ walk(n.children);
34
+ }
35
+ }
36
+ walk(nodes);
37
+ return ids;
38
+ }
39
+ function computeVisible(nodes, expandedIds) {
40
+ const result = [];
41
+ function walk(children) {
42
+ const sorted = [...children].sort((a, b) => a.position - b.position);
43
+ for (const node of sorted) {
44
+ result.push(node);
45
+ if (expandedIds.has(node.id) && (node.children ?? []).length > 0) {
46
+ walk(node.children ?? []);
47
+ }
48
+ }
49
+ }
50
+ walk(nodes);
51
+ return result;
52
+ }
53
+ export function useTree(listId) {
54
+ const [state, setState] = useState({ nodes: [], loading: true, error: null });
55
+ const [expandedIds, setExpandedIds] = useState(new Set());
56
+ const fetch = useCallback(() => {
57
+ setState((s) => ({ ...s, loading: true, error: null }));
58
+ getNodes(listId)
59
+ .then((flatNodes) => {
60
+ // Backend returns a flat list — rebuild nested tree
61
+ const tree = buildTree(flatNodes);
62
+ // Auto-expand ALL nodes so the full tree is visible by default
63
+ setExpandedIds(collectAllIds(tree));
64
+ setState({ nodes: tree, loading: false, error: null });
65
+ })
66
+ .catch((err) => setState({ nodes: [], loading: false, error: err.message }));
67
+ }, [listId]);
68
+ useEffect(() => {
69
+ fetch();
70
+ }, [fetch]);
71
+ const toggleExpand = useCallback((nodeId) => {
72
+ setExpandedIds((prev) => {
73
+ const next = new Set(prev);
74
+ if (next.has(nodeId))
75
+ next.delete(nodeId);
76
+ else
77
+ next.add(nodeId);
78
+ return next;
79
+ });
80
+ }, []);
81
+ const numberMap = useMemo(() => assignNumbers(state.nodes), [state.nodes]);
82
+ const flatNodes = useMemo(() => computeVisible(state.nodes, expandedIds), [state.nodes, expandedIds]);
83
+ return {
84
+ nodes: state.nodes,
85
+ flatNodes,
86
+ expandedIds,
87
+ toggleExpand,
88
+ numberMap,
89
+ loading: state.loading,
90
+ error: state.error,
91
+ refetch: fetch,
92
+ };
93
+ }
@@ -0,0 +1,77 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Daily tasks screen
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { Box, Text } from 'ink';
5
+ import { getDailyToday, getDailyOverdue, updateNode } from '../../api/client.js';
6
+ import { NodeRow } from '../components/NodeRow.js';
7
+ import { Keybindings } from '../components/Keybindings.js';
8
+ import { Spinner } from '../components/Spinner.js';
9
+ const BINDINGS = '[↑↓] navigate [b] back [/done <n>] mark done';
10
+ export function DailyScreen({ push, pop, registerActions }) {
11
+ const [todayNodes, setTodayNodes] = useState([]);
12
+ const [overdueNodes, setOverdueNodes] = useState([]);
13
+ const [loading, setLoading] = useState(true);
14
+ const [error, setError] = useState(null);
15
+ const [cursor, setCursor] = useState(0);
16
+ const [status, setStatus] = useState(null);
17
+ const allNodes = [...todayNodes, ...overdueNodes];
18
+ const fetchData = useCallback(() => {
19
+ setLoading(true);
20
+ Promise.all([getDailyToday(), getDailyOverdue()])
21
+ .then(([today, overdue]) => {
22
+ setTodayNodes(today);
23
+ setOverdueNodes(overdue);
24
+ setLoading(false);
25
+ })
26
+ .catch((e) => {
27
+ setError(e.message);
28
+ setLoading(false);
29
+ });
30
+ }, []);
31
+ useEffect(() => {
32
+ fetchData();
33
+ }, [fetchData]);
34
+ const handleCommand = useCallback(async (cmd) => {
35
+ const parts = cmd.trim().split(/\s+/);
36
+ const verb = parts[0];
37
+ const arg = parts.slice(1).join(' ');
38
+ if (verb === '/done' && arg) {
39
+ const idx = parseInt(arg, 10) - 1;
40
+ const node = allNodes[idx];
41
+ if (!node) {
42
+ setStatus(`✗ Node "${arg}" not found.`);
43
+ return;
44
+ }
45
+ updateNode(node.id, { status: 'DONE' })
46
+ .then(() => {
47
+ setStatus(`✓ Done: ${node.title}`);
48
+ fetchData();
49
+ })
50
+ .catch((e) => setStatus(`✗ ${e.message}`));
51
+ }
52
+ else if (verb === '/back') {
53
+ pop();
54
+ }
55
+ }, [allNodes, fetchData, pop]);
56
+ useEffect(() => {
57
+ registerActions({
58
+ onUp: () => setCursor((c) => Math.max(0, c - 1)),
59
+ onDown: () => setCursor((c) => Math.min(allNodes.length - 1, c + 1)),
60
+ onEnter: () => {
61
+ const node = allNodes[cursor];
62
+ if (node) {
63
+ push('node-details', { nodeId: node.id });
64
+ }
65
+ },
66
+ onBack: () => pop(),
67
+ onCommand: handleCommand,
68
+ onRefetch: fetchData,
69
+ });
70
+ }, [allNodes.length, pop, registerActions, handleCommand, fetchData]);
71
+ if (loading)
72
+ return (_jsx(Box, { paddingX: 2, children: _jsx(Spinner, { label: "Loading daily tasks..." }) }));
73
+ if (error)
74
+ return (_jsx(Box, { paddingX: 2, children: _jsxs(Text, { color: "red", children: ["\u2717 ", error] }) }));
75
+ let idx = 0;
76
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "\uD83D\uDCC5 Today" }) }), todayNodes.length === 0 ? (_jsx(Text, { dimColor: true, children: " No tasks for today." })) : (todayNodes.map((node) => (_jsx(NodeRow, { node: node, number: String(++idx), depth: 0, isSelected: idx - 1 === cursor, isLast: true, isLastRoot: true, parentLines: [] }, node.id)))), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { bold: true, color: "red", children: "\u26A0 Overdue" }) }), overdueNodes.length === 0 ? (_jsx(Text, { dimColor: true, children: " No overdue tasks." })) : (overdueNodes.map((node) => (_jsx(NodeRow, { node: node, number: String(++idx), depth: 0, isSelected: idx - 1 === cursor, isLast: true, isLastRoot: true, parentLines: [] }, node.id)))), status && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: status }) })), _jsx(Box, { marginTop: 1, children: _jsx(Keybindings, { bindings: BINDINGS }) })] }));
77
+ }
@@ -0,0 +1,262 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // DashboardScreen.tsx — CLI Dashboard matching the web app design.
3
+ // Shows: Welcome header, streak, today's score, multi-range ASCII chart, and recents.
4
+ import { useEffect, useState, useCallback } from 'react';
5
+ import { Box, Text } from 'ink';
6
+ import { getDailyScore, getStreak, getScores, getFolders } from '../../api/client.js';
7
+ import { Spinner } from '../components/Spinner.js';
8
+ import { Keybindings } from '../components/Keybindings.js';
9
+ import { getRecents } from '../../utils/recents.js';
10
+ // ── Chart constants ──────────────────────────────────────────────────────────
11
+ const FILL = '█';
12
+ const SHADE = '▓';
13
+ const LIGHT = '░';
14
+ const EMPTY_CHAR = ' ';
15
+ const CHART_HEIGHT = 8;
16
+ const RANGES = ['1W', '1M', '3M', '1Y'];
17
+ const RANGE_LABEL = {
18
+ '1W': 'Weekly',
19
+ '1M': 'Monthly',
20
+ '3M': 'Quarterly',
21
+ '1Y': 'Yearly',
22
+ };
23
+ const RANGE_DAYS = {
24
+ '1W': 7,
25
+ '1M': 30,
26
+ '3M': 90,
27
+ '1Y': 365,
28
+ };
29
+ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
30
+ // ── Helpers ──────────────────────────────────────────────────────────────────
31
+ function todayISO() {
32
+ return new Date().toISOString().slice(0, 10);
33
+ }
34
+ function getStreakMessage(streak) {
35
+ if (streak === 0)
36
+ return "Let's get started — build your first streak!";
37
+ if (streak === 1)
38
+ return 'Good start! Keep it going tomorrow.';
39
+ if (streak <= 3)
40
+ return "You're building momentum!";
41
+ if (streak <= 6)
42
+ return "You're on a roll! Don't break it. 🔥";
43
+ if (streak <= 13)
44
+ return 'You have been on fire lately. 🔥';
45
+ if (streak <= 29)
46
+ return "Incredible consistency! You're unstoppable. 🔥🔥";
47
+ return "Legendary streak. You're a machine. 🔥🔥🔥";
48
+ }
49
+ function formatDateLabel(iso, range) {
50
+ const [y, m, d] = iso.split('-').map(Number);
51
+ if (range === '1Y')
52
+ return `${MONTHS[m - 1]} '${String(y).slice(2)}`;
53
+ return `${MONTHS[m - 1]} ${d}`;
54
+ }
55
+ function buildChartData(scores, days, range) {
56
+ const scoreMap = new Map(scores.map((s) => [s.date.slice(0, 10), s.points]));
57
+ const result = [];
58
+ for (let i = days - 1; i >= 0; i--) {
59
+ const d = new Date();
60
+ d.setDate(d.getDate() - i);
61
+ const iso = d.toISOString().slice(0, 10);
62
+ result.push({ label: formatDateLabel(iso, range), points: scoreMap.get(iso) ?? null });
63
+ }
64
+ return result;
65
+ }
66
+ /** Downsample data to fit the terminal width. */
67
+ function downsample(data, maxColumns) {
68
+ if (data.length <= maxColumns)
69
+ return data;
70
+ const step = data.length / maxColumns;
71
+ const result = [];
72
+ for (let i = 0; i < maxColumns; i++) {
73
+ const start = Math.floor(i * step);
74
+ const end = Math.floor((i + 1) * step);
75
+ const slice = data.slice(start, end);
76
+ const nonNull = slice.filter((d) => d.points !== null);
77
+ const avg = nonNull.length > 0 ? nonNull.reduce((a, b) => a + b.points, 0) / nonNull.length : null;
78
+ result.push({
79
+ label: slice[Math.floor(slice.length / 2)]?.label ?? '',
80
+ points: avg !== null ? Math.round(avg * 10) / 10 : null,
81
+ });
82
+ }
83
+ return result;
84
+ }
85
+ // ── Component ────────────────────────────────────────────────────────────────
86
+ const BINDINGS = '[←→] range [↑↓] recents [Enter] open [/folders] your folders';
87
+ export function DashboardScreen({ push, pop, registerActions, width, authUser }) {
88
+ const [streak, setStreak] = useState(0);
89
+ const [todayScore, setTodayScore] = useState(null);
90
+ const [allScores, setAllScores] = useState([]);
91
+ const [range, setRange] = useState('1W');
92
+ const [loading, setLoading] = useState(true);
93
+ const [error, setError] = useState(null);
94
+ const [recents, setRecents] = useState([]);
95
+ const [recentCursor, setRecentCursor] = useState(0);
96
+ const termWidth = width ?? 80;
97
+ // ── Load data ────────────────────────────────────────────────────────────
98
+ useEffect(() => {
99
+ let cancelled = false;
100
+ const load = async () => {
101
+ try {
102
+ // Don't call getMe() — user is passed from App via useAuth (already resolved)
103
+ const [streakData, scoresData] = await Promise.all([getStreak(), getScores(365)]);
104
+ let todayData = null;
105
+ try {
106
+ todayData = await getDailyScore(todayISO());
107
+ }
108
+ catch {
109
+ // 404 = no score yet today
110
+ }
111
+ // Build recents from persisted store + live folder/list data
112
+ const recentEntries = getRecents();
113
+ const recentItems = [];
114
+ if (recentEntries.length > 0) {
115
+ // Fetch all folders to resolve names and list folder associations
116
+ try {
117
+ const folders = await getFolders();
118
+ const folderMap = new Map(folders.map((f) => [f.id, f]));
119
+ for (const entry of recentEntries.slice(0, 10)) {
120
+ if (entry.type === 'folder') {
121
+ const folder = folderMap.get(entry.id);
122
+ recentItems.push({
123
+ id: entry.id,
124
+ name: folder?.name ?? entry.name,
125
+ type: 'folder',
126
+ });
127
+ }
128
+ else {
129
+ // List — try to find its folder
130
+ recentItems.push({
131
+ id: entry.id,
132
+ name: entry.name,
133
+ type: 'list',
134
+ folderId: entry.folderId,
135
+ folderName: entry.folderName,
136
+ });
137
+ }
138
+ }
139
+ }
140
+ catch {
141
+ // If folders API fails, still show entries with stored names
142
+ for (const entry of recentEntries.slice(0, 10)) {
143
+ recentItems.push({
144
+ id: entry.id,
145
+ name: entry.name,
146
+ type: entry.type,
147
+ folderId: entry.folderId,
148
+ folderName: entry.folderName,
149
+ });
150
+ }
151
+ }
152
+ }
153
+ if (!cancelled) {
154
+ setStreak(streakData.streak);
155
+ setTodayScore(todayData);
156
+ setAllScores(scoresData);
157
+ setRecents(recentItems);
158
+ setLoading(false);
159
+ }
160
+ }
161
+ catch (e) {
162
+ if (!cancelled) {
163
+ setError(e.message);
164
+ setLoading(false);
165
+ }
166
+ }
167
+ };
168
+ load();
169
+ return () => {
170
+ cancelled = true;
171
+ };
172
+ }, []);
173
+ // ── Key bindings ─────────────────────────────────────────────────────────
174
+ const openRecent = useCallback(() => {
175
+ const item = recents[recentCursor];
176
+ if (!item)
177
+ return;
178
+ if (item.type === 'folder') {
179
+ push('lists', { folderId: item.id, folderName: item.name });
180
+ }
181
+ else {
182
+ push('tree', {
183
+ listId: item.id,
184
+ listName: item.name,
185
+ ...(item.folderId ? { folderId: item.folderId } : {}),
186
+ ...(item.folderName ? { folderName: item.folderName } : {}),
187
+ });
188
+ }
189
+ }, [recents, recentCursor, push]);
190
+ useEffect(() => {
191
+ registerActions({
192
+ onLeft: () => setRange((r) => RANGES[Math.max(0, RANGES.indexOf(r) - 1)]),
193
+ onRight: () => setRange((r) => RANGES[Math.min(RANGES.length - 1, RANGES.indexOf(r) + 1)]),
194
+ onUp: () => setRecentCursor((c) => (c > 0 ? c - 1 : recents.length - 1)),
195
+ onDown: () => setRecentCursor((c) => (c < recents.length - 1 ? c + 1 : 0)),
196
+ onEnter: openRecent,
197
+ onBack: () => pop(),
198
+ onCommand: (cmd) => {
199
+ if (cmd === '/back')
200
+ pop();
201
+ },
202
+ });
203
+ }, [registerActions, pop, recents, openRecent]);
204
+ // ── Render ───────────────────────────────────────────────────────────────
205
+ if (loading) {
206
+ return (_jsx(Box, { paddingX: 2, children: _jsx(Spinner, { label: "Loading dashboard..." }) }));
207
+ }
208
+ if (error) {
209
+ return (_jsx(Box, { paddingX: 2, children: _jsxs(Text, { color: "red", children: ["\u2717 ", error] }) }));
210
+ }
211
+ // Chart data
212
+ const days = RANGE_DAYS[range];
213
+ const rawData = buildChartData(allScores, days, range);
214
+ // Reserve space: 2 padding + some chars for labels. Each column = 4 chars wide
215
+ const maxCols = Math.min(rawData.length, Math.floor((termWidth - 8) / 4));
216
+ const chartData = downsample(rawData, maxCols);
217
+ const allPoints = chartData.map((d) => d.points).filter((p) => p !== null);
218
+ const maxPts = allPoints.length > 0 ? Math.max(...allPoints) : 1;
219
+ const minPts = allPoints.length > 0 ? Math.min(...allPoints, 0) : 0;
220
+ const pointRange = Math.max(maxPts - minPts, 1);
221
+ // ── Sub-renders ──────────────────────────────────────────────────────────
222
+ const renderChart = () => {
223
+ // Build rows from top to bottom
224
+ const rows = [];
225
+ for (let row = CHART_HEIGHT - 1; row >= 0; row--) {
226
+ const threshold = minPts + (row / CHART_HEIGHT) * pointRange;
227
+ const cells = [];
228
+ for (let col = 0; col < chartData.length; col++) {
229
+ const p = chartData[col].points;
230
+ if (p === null || p < threshold) {
231
+ cells.push(_jsx(Text, { color: "#1a1a2e", children: ' ' }, col));
232
+ }
233
+ else {
234
+ // Determine intensity
235
+ const intensity = (p - minPts) / pointRange;
236
+ const char = intensity > 0.7 ? FILL : intensity > 0.35 ? SHADE : LIGHT;
237
+ cells.push(_jsx(Text, { color: "#00bfff", children: ` ${char} ` }, col));
238
+ }
239
+ }
240
+ rows.push(_jsx(Box, { flexDirection: "row", children: cells }, row));
241
+ }
242
+ return rows;
243
+ };
244
+ const renderLabels = () => {
245
+ // Show a subset of date labels
246
+ const labelInterval = Math.max(1, Math.floor(chartData.length / 6));
247
+ const labels = chartData.map((d, i) => {
248
+ if (i % labelInterval === 0 || i === chartData.length - 1) {
249
+ return d.label.padStart(3).padEnd(4).slice(0, 4);
250
+ }
251
+ return ' ';
252
+ });
253
+ return (_jsx(Box, { flexDirection: "row", children: labels.map((l, i) => (_jsx(Text, { color: "#8b949e", dimColor: true, children: l }, i))) }));
254
+ };
255
+ const userName = authUser?.name ?? authUser?.email ?? 'User';
256
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { bold: true, color: "#EDEFF3", children: ["Welcome Back, ", userName] }), _jsx(Text, { color: "#8b949e", children: getStreakMessage(streak) })] }), _jsxs(Box, { gap: 4, marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: "#00c896", children: "\uD83D\uDD25 Streak: " }), _jsxs(Text, { bold: true, children: [streak, " days"] })] }), _jsxs(Box, { children: [_jsx(Text, { color: "#00c896", children: "Today: " }), todayScore ? (_jsxs(Text, { bold: true, children: [todayScore.doneNodes, "/", todayScore.totalNodes, " tasks \u00B7 ", todayScore.points, " pts"] })) : (_jsx(Text, { dimColor: true, children: "No score yet today" }))] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { justifyContent: "space-between", marginBottom: 1, children: [_jsx(Text, { color: "#8b949e", children: "Account Performance" }), _jsx(Box, { gap: 1, children: RANGES.map((r) => (_jsx(Text, { color: r === range ? '#00bfff' : '#8b949e', bold: r === range, dimColor: r !== range, children: r === range ? `[${RANGE_LABEL[r]}]` : ` ${RANGE_LABEL[r]} ` }, r))) })] }), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#32363C", paddingX: 1, children: allPoints.length === 0 ? (_jsx(Box, { height: CHART_HEIGHT, alignItems: "center", justifyContent: "center", children: _jsx(Text, { color: "#8b949e", dimColor: true, children: "No data yet \u2014 complete some tasks to see your graph!" }) })) : (_jsxs(_Fragment, { children: [renderChart(), renderLabels()] })) })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "#EDEFF3", children: "Recents" }), recents.length === 0 ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "#8b949e", dimColor: true, children: "Open a folder or list to see it here." }) })) : (_jsx(Box, { flexDirection: "column", marginTop: 1, children: recents.map((item, i) => {
257
+ const isSelected = i === recentCursor;
258
+ const icon = item.type === 'folder' ? '📁' : '📋';
259
+ const suffix = item.type === 'list' && item.folderName ? ` (${item.folderName})` : '';
260
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? '#00bfff' : '#8b949e', bold: isSelected, children: [isSelected ? '❯ ' : ' ', icon, " ", item.name] }), suffix && (_jsx(Text, { color: "#8b949e", dimColor: true, children: suffix }))] }, `${item.type}-${item.id}`));
261
+ }) }))] }), _jsx(Box, { marginTop: 1, children: _jsx(Keybindings, { bindings: BINDINGS }) })] }));
262
+ }
@@ -0,0 +1,75 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Home screen - folders list
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { Box, Text } from 'ink';
5
+ import { useApp } from 'ink';
6
+ import { useFolders } from '../hooks/useFolders.js';
7
+ import { FolderItem } from '../components/FolderItem.js';
8
+ import { Keybindings } from '../components/Keybindings.js';
9
+ import { Spinner } from '../components/Spinner.js';
10
+ import { createFolder, deleteFolder } from '../../api/client.js';
11
+ import { recordRecent } from '../../utils/recents.js';
12
+ const BINDINGS = '[↑↓] navigate [Enter] open [n] new [e] edit [d] delete [q] quit';
13
+ export function HomeScreen({ push, registerActions }) {
14
+ const { folders, loading, error, refetch } = useFolders();
15
+ const [cursor, setCursor] = useState(0);
16
+ const [status, setStatus] = useState(null);
17
+ const { exit } = useApp();
18
+ const handleCommand = useCallback(async (cmd) => {
19
+ const parts = cmd.trim().split(/\s+/);
20
+ const verb = parts[0];
21
+ const arg = parts.slice(1).join(' ');
22
+ if (verb === '/retry') {
23
+ refetch();
24
+ return;
25
+ }
26
+ if (verb === '/add' && arg) {
27
+ setStatus('Creating folder...');
28
+ createFolder(arg)
29
+ .then(() => {
30
+ setStatus(`✓ Created "${arg}"`);
31
+ refetch();
32
+ })
33
+ .catch((e) => setStatus(`✗ ${e.message}`));
34
+ }
35
+ else if (verb === '/delete') {
36
+ const folder = folders[cursor];
37
+ if (!folder)
38
+ return;
39
+ setStatus('Deleting...');
40
+ deleteFolder(folder.id)
41
+ .then(() => {
42
+ setStatus(`✓ Deleted "${folder.name}"`);
43
+ setCursor(0);
44
+ refetch();
45
+ })
46
+ .catch((e) => setStatus(`✗ ${e.message}`));
47
+ }
48
+ else if (verb === '/quit' || verb === 'q') {
49
+ exit();
50
+ }
51
+ }, [folders, cursor, refetch, exit]);
52
+ useEffect(() => {
53
+ const len = folders.length;
54
+ registerActions({
55
+ onUp: () => setCursor((c) => (c > 0 ? c - 1 : len - 1)),
56
+ onDown: () => setCursor((c) => (c < len - 1 ? c + 1 : 0)),
57
+ onEnter: () => {
58
+ const folder = folders[cursor];
59
+ if (folder) {
60
+ recordRecent({ id: folder.id, name: folder.name, type: 'folder' });
61
+ push('lists', { folderId: folder.id, folderName: folder.name });
62
+ }
63
+ },
64
+ onBack: () => { },
65
+ onQuit: () => exit(),
66
+ onCommand: handleCommand,
67
+ onRefetch: refetch,
68
+ });
69
+ }, [folders, cursor, push, exit, registerActions, handleCommand, refetch]);
70
+ if (loading)
71
+ return (_jsx(Box, { paddingX: 2, children: _jsx(Spinner, { label: "Loading folders..." }) }));
72
+ if (error)
73
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Text, { dimColor: true, children: "Server offline or unreachable" }), _jsx(Text, { dimColor: true, children: error }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Type /retry to try again \u00B7 /add <name> to work offline" })] }));
74
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "#00bfff", children: "\uD83D\uDCC1 Your Folders" }) }), folders.length === 0 ? (_jsxs(Box, { children: [_jsxs(Text, { color: "#8b949e", dimColor: true, children: ["No folders yet. Type", ' '] }), _jsxs(Text, { color: "#00bfff", children: ["/new folder ", '<name>'] }), _jsxs(Text, { color: "#8b949e", dimColor: true, children: [' ', "to create one."] })] })) : (folders.map((folder, i) => (_jsx(FolderItem, { folder: folder, isSelected: i === cursor }, folder.id)))), status && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: status }) })), _jsx(Box, { marginTop: 1, children: _jsx(Keybindings, { bindings: BINDINGS }) })] }));
75
+ }
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Lists screen
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { Box, Text } from 'ink';
5
+ import { useLists } from '../hooks/useLists.js';
6
+ import { ListItem } from '../components/ListItem.js';
7
+ import { Breadcrumb } from '../components/Breadcrumb.js';
8
+ import { Keybindings } from '../components/Keybindings.js';
9
+ import { Spinner } from '../components/Spinner.js';
10
+ import { createList, deleteList } from '../../api/client.js';
11
+ import { recordRecent } from '../../utils/recents.js';
12
+ const BINDINGS = '[↑↓] navigate [Enter] open [b] back [n] new [e] edit [d] delete';
13
+ export function ListsScreen({ push, pop, registerActions, folderId, folderName }) {
14
+ const { lists, loading, error, refetch } = useLists(folderId);
15
+ const [cursor, setCursor] = useState(0);
16
+ const [status, setStatus] = useState(null);
17
+ const handleCommand = useCallback(async (cmd) => {
18
+ const parts = cmd.trim().split(/\s+/);
19
+ const verb = parts[0];
20
+ const arg = parts.slice(1).join(' ');
21
+ if (verb === '/add' && arg) {
22
+ setStatus('Creating list...');
23
+ createList(arg, folderId)
24
+ .then(() => {
25
+ setStatus(`✓ Created "${arg}"`);
26
+ refetch();
27
+ })
28
+ .catch((e) => setStatus(`✗ ${e.message}`));
29
+ }
30
+ else if (verb === '/delete') {
31
+ const list = lists[cursor];
32
+ if (!list)
33
+ return;
34
+ setStatus('Deleting...');
35
+ deleteList(list.id)
36
+ .then(() => {
37
+ setStatus(`✓ Deleted "${list.name}"`);
38
+ setCursor(0);
39
+ refetch();
40
+ })
41
+ .catch((e) => setStatus(`✗ ${e.message}`));
42
+ }
43
+ else if (verb === '/back') {
44
+ pop();
45
+ }
46
+ }, [lists, cursor, folderId, refetch, pop]);
47
+ useEffect(() => {
48
+ const len = lists.length;
49
+ registerActions({
50
+ onUp: () => setCursor((c) => (c > 0 ? c - 1 : Math.max(0, len - 1))),
51
+ onDown: () => setCursor((c) => (c < len - 1 ? c + 1 : 0)),
52
+ onEnter: () => {
53
+ const list = lists[cursor];
54
+ if (list) {
55
+ recordRecent({ id: list.id, name: list.name, type: 'list', folderId, folderName });
56
+ push('tree', { listId: list.id, listName: list.name, folderName: folderName ?? '' });
57
+ }
58
+ },
59
+ onBack: () => pop(),
60
+ onCommand: handleCommand,
61
+ onRefetch: refetch,
62
+ });
63
+ }, [lists, cursor, push, pop, folderName, registerActions, handleCommand, refetch]);
64
+ const breadcrumbParts = folderName ? [folderName, 'Lists'] : ['Lists'];
65
+ if (loading)
66
+ return (_jsx(Box, { paddingX: 2, children: _jsx(Spinner, { label: "Loading lists..." }) }));
67
+ if (error)
68
+ return (_jsx(Box, { paddingX: 2, children: _jsxs(Text, { color: "red", children: ["\u2717 ", error] }) }));
69
+ if (lists.length === 0) {
70
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, children: [_jsx(Breadcrumb, { parts: breadcrumbParts }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "No lists yet. Type /add <name> to create one." }) })] }));
71
+ }
72
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, children: [_jsx(Breadcrumb, { parts: breadcrumbParts }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { bold: true, children: "\uD83D\uDCCB Lists" }) }), lists.map((list, i) => (_jsx(ListItem, { list: list, isSelected: i === cursor }, list.id))), status && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: status }) })), _jsx(Box, { marginTop: 1, children: _jsx(Keybindings, { bindings: BINDINGS }) })] }));
73
+ }