floq 0.0.1 → 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.
package/dist/ui/App.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect } from 'react';
2
+ import { useState, useEffect, useCallback } from 'react';
3
3
  import { Box, Text, useInput, useApp } from 'ink';
4
4
  import TextInput from 'ink-text-input';
5
5
  import { eq, and } from 'drizzle-orm';
@@ -11,7 +11,7 @@ import { SplashScreen } from './SplashScreen.js';
11
11
  import { getDb, schema } from '../db/index.js';
12
12
  import { t, fmt } from '../i18n/index.js';
13
13
  import { ThemeProvider, useTheme } from './theme/index.js';
14
- import { getThemeName } from '../config.js';
14
+ import { getThemeName, isTursoEnabled, getTursoConfig } from '../config.js';
15
15
  const TABS = ['inbox', 'next', 'waiting', 'someday', 'projects'];
16
16
  export function App() {
17
17
  const themeName = getThemeName();
@@ -37,7 +37,7 @@ function AppContent() {
37
37
  const [taskToLink, setTaskToLink] = useState(null);
38
38
  const [projectSelectIndex, setProjectSelectIndex] = useState(0);
39
39
  const i18n = t();
40
- const loadTasks = () => {
40
+ const loadTasks = useCallback(async () => {
41
41
  const db = getDb();
42
42
  const newTasks = {
43
43
  inbox: [],
@@ -49,46 +49,43 @@ function AppContent() {
49
49
  // Load all tasks (including project children) by status
50
50
  const statusList = ['inbox', 'next', 'waiting', 'someday'];
51
51
  for (const status of statusList) {
52
- newTasks[status] = db
52
+ newTasks[status] = await db
53
53
  .select()
54
54
  .from(schema.tasks)
55
- .where(and(eq(schema.tasks.status, status), eq(schema.tasks.isProject, false)))
56
- .all();
55
+ .where(and(eq(schema.tasks.status, status), eq(schema.tasks.isProject, false)));
57
56
  }
58
57
  // Load projects (isProject = true, not done)
59
- newTasks.projects = db
58
+ newTasks.projects = await db
60
59
  .select()
61
60
  .from(schema.tasks)
62
- .where(and(eq(schema.tasks.isProject, true), eq(schema.tasks.status, 'next')))
63
- .all();
61
+ .where(and(eq(schema.tasks.isProject, true), eq(schema.tasks.status, 'next')));
64
62
  setTasks(newTasks);
65
- };
63
+ }, []);
66
64
  // Get parent project for a task
67
65
  const getParentProject = (parentId) => {
68
66
  if (!parentId)
69
67
  return undefined;
70
68
  return tasks.projects.find(p => p.id === parentId);
71
69
  };
72
- const loadProjectTasks = (projectId) => {
70
+ const loadProjectTasks = useCallback(async (projectId) => {
73
71
  const db = getDb();
74
- const children = db
72
+ const children = await db
75
73
  .select()
76
74
  .from(schema.tasks)
77
- .where(eq(schema.tasks.parentId, projectId))
78
- .all();
75
+ .where(eq(schema.tasks.parentId, projectId));
79
76
  setProjectTasks(children);
80
- };
77
+ }, []);
81
78
  useEffect(() => {
82
79
  loadTasks();
83
- }, []);
80
+ }, [loadTasks]);
84
81
  const currentTab = TABS[currentListIndex];
85
82
  const currentTasks = mode === 'project-detail' ? projectTasks : tasks[currentTab];
86
- const addTask = (title, parentId) => {
83
+ const addTask = useCallback(async (title, parentId) => {
87
84
  if (!title.trim())
88
85
  return;
89
86
  const db = getDb();
90
87
  const now = new Date();
91
- db.insert(schema.tasks)
88
+ await db.insert(schema.tasks)
92
89
  .values({
93
90
  id: uuidv4(),
94
91
  title: title.trim(),
@@ -96,20 +93,19 @@ function AppContent() {
96
93
  parentId: parentId || null,
97
94
  createdAt: now,
98
95
  updatedAt: now,
99
- })
100
- .run();
96
+ });
101
97
  setMessage(fmt(i18n.tui.added, { title: title.trim() }));
102
- loadTasks();
103
- };
104
- const handleInputSubmit = (value) => {
98
+ await loadTasks();
99
+ }, [i18n.tui.added, loadTasks]);
100
+ const handleInputSubmit = async (value) => {
105
101
  if (value.trim()) {
106
102
  if (mode === 'add-to-project' && selectedProject) {
107
- addTask(value, selectedProject.id);
108
- loadProjectTasks(selectedProject.id);
103
+ await addTask(value, selectedProject.id);
104
+ await loadProjectTasks(selectedProject.id);
109
105
  setMode('project-detail');
110
106
  }
111
107
  else {
112
- addTask(value);
108
+ await addTask(value);
113
109
  setMode('normal');
114
110
  }
115
111
  }
@@ -118,15 +114,38 @@ function AppContent() {
118
114
  }
119
115
  setInputValue('');
120
116
  };
121
- const linkTaskToProject = (task, project) => {
117
+ const linkTaskToProject = useCallback(async (task, project) => {
122
118
  const db = getDb();
123
- db.update(schema.tasks)
119
+ await db.update(schema.tasks)
124
120
  .set({ parentId: project.id, updatedAt: new Date() })
125
- .where(eq(schema.tasks.id, task.id))
126
- .run();
121
+ .where(eq(schema.tasks.id, task.id));
127
122
  setMessage(fmt(i18n.tui.linkedToProject || 'Linked "{title}" to {project}', { title: task.title, project: project.title }));
128
- loadTasks();
129
- };
123
+ await loadTasks();
124
+ }, [i18n.tui.linkedToProject, loadTasks]);
125
+ const markTaskDone = useCallback(async (task) => {
126
+ const db = getDb();
127
+ await db.update(schema.tasks)
128
+ .set({ status: 'done', updatedAt: new Date() })
129
+ .where(eq(schema.tasks.id, task.id));
130
+ setMessage(fmt(i18n.tui.completed, { title: task.title }));
131
+ await loadTasks();
132
+ }, [i18n.tui.completed, loadTasks]);
133
+ const moveTaskToStatus = useCallback(async (task, status) => {
134
+ const db = getDb();
135
+ await db.update(schema.tasks)
136
+ .set({ status, waitingFor: null, updatedAt: new Date() })
137
+ .where(eq(schema.tasks.id, task.id));
138
+ setMessage(fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[status] }));
139
+ await loadTasks();
140
+ }, [i18n.tui.movedTo, i18n.status, loadTasks]);
141
+ const makeTaskProject = useCallback(async (task) => {
142
+ const db = getDb();
143
+ await db.update(schema.tasks)
144
+ .set({ isProject: true, status: 'next', updatedAt: new Date() })
145
+ .where(eq(schema.tasks.id, task.id));
146
+ setMessage(fmt(i18n.tui.madeProject || 'Made project: {title}', { title: task.title }));
147
+ await loadTasks();
148
+ }, [i18n.tui.madeProject, loadTasks]);
130
149
  const getTabLabel = (tab) => {
131
150
  switch (tab) {
132
151
  case 'inbox':
@@ -214,16 +233,11 @@ function AppContent() {
214
233
  // Mark child task as done
215
234
  if (input === 'd' && projectTasks.length > 0) {
216
235
  const task = projectTasks[selectedTaskIndex];
217
- const db = getDb();
218
- db.update(schema.tasks)
219
- .set({ status: 'done', updatedAt: new Date() })
220
- .where(eq(schema.tasks.id, task.id))
221
- .run();
222
- setMessage(fmt(i18n.tui.completed, { title: task.title }));
223
- if (selectedProject) {
224
- loadProjectTasks(selectedProject.id);
225
- }
226
- loadTasks();
236
+ markTaskDone(task).then(() => {
237
+ if (selectedProject) {
238
+ loadProjectTasks(selectedProject.id);
239
+ }
240
+ });
227
241
  return;
228
242
  }
229
243
  return;
@@ -247,31 +261,21 @@ function AppContent() {
247
261
  if ((input === '\x1bOR' || input === '\x1b[13~') && currentTasks.length > 0) {
248
262
  // F3 - Done
249
263
  const task = currentTasks[selectedTaskIndex];
250
- const db = getDb();
251
- db.update(schema.tasks)
252
- .set({ status: 'done', updatedAt: new Date() })
253
- .where(eq(schema.tasks.id, task.id))
254
- .run();
255
- setMessage(fmt(i18n.tui.completed, { title: task.title }));
256
- loadTasks();
257
- if (selectedTaskIndex >= currentTasks.length - 1) {
258
- setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
259
- }
264
+ markTaskDone(task).then(() => {
265
+ if (selectedTaskIndex >= currentTasks.length - 1) {
266
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
267
+ }
268
+ });
260
269
  return;
261
270
  }
262
271
  if ((input === '\x1bOS' || input === '\x1b[14~') && currentTasks.length > 0 && currentTab !== 'next' && currentTab !== 'projects') {
263
272
  // F4 - Move to Next
264
273
  const task = currentTasks[selectedTaskIndex];
265
- const db = getDb();
266
- db.update(schema.tasks)
267
- .set({ status: 'next', waitingFor: null, updatedAt: new Date() })
268
- .where(eq(schema.tasks.id, task.id))
269
- .run();
270
- setMessage(fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status.next }));
271
- loadTasks();
272
- if (selectedTaskIndex >= currentTasks.length - 1) {
273
- setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
274
- }
274
+ moveTaskToStatus(task, 'next').then(() => {
275
+ if (selectedTaskIndex >= currentTasks.length - 1) {
276
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
277
+ }
278
+ });
275
279
  return;
276
280
  }
277
281
  if (input === '\x1b[15~') {
@@ -358,16 +362,11 @@ function AppContent() {
358
362
  // Mark as project (p key)
359
363
  if (input === 'p' && currentTasks.length > 0 && currentTab !== 'projects') {
360
364
  const task = currentTasks[selectedTaskIndex];
361
- const db = getDb();
362
- db.update(schema.tasks)
363
- .set({ isProject: true, status: 'next', updatedAt: new Date() })
364
- .where(eq(schema.tasks.id, task.id))
365
- .run();
366
- setMessage(fmt(i18n.tui.madeProject || 'Made project: {title}', { title: task.title }));
367
- loadTasks();
368
- if (selectedTaskIndex >= currentTasks.length - 1) {
369
- setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
370
- }
365
+ makeTaskProject(task).then(() => {
366
+ if (selectedTaskIndex >= currentTasks.length - 1) {
367
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
368
+ }
369
+ });
371
370
  return;
372
371
  }
373
372
  // Link to project (P key - shift+p)
@@ -381,61 +380,41 @@ function AppContent() {
381
380
  // Mark as done
382
381
  if (input === 'd' && currentTasks.length > 0) {
383
382
  const task = currentTasks[selectedTaskIndex];
384
- const db = getDb();
385
- db.update(schema.tasks)
386
- .set({ status: 'done', updatedAt: new Date() })
387
- .where(eq(schema.tasks.id, task.id))
388
- .run();
389
- setMessage(fmt(i18n.tui.completed, { title: task.title }));
390
- loadTasks();
391
- if (selectedTaskIndex >= currentTasks.length - 1) {
392
- setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
393
- }
383
+ markTaskDone(task).then(() => {
384
+ if (selectedTaskIndex >= currentTasks.length - 1) {
385
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
386
+ }
387
+ });
394
388
  return;
395
389
  }
396
390
  // Move to next actions
397
391
  if (input === 'n' && currentTasks.length > 0 && currentTab !== 'next' && currentTab !== 'projects') {
398
392
  const task = currentTasks[selectedTaskIndex];
399
- const db = getDb();
400
- db.update(schema.tasks)
401
- .set({ status: 'next', waitingFor: null, updatedAt: new Date() })
402
- .where(eq(schema.tasks.id, task.id))
403
- .run();
404
- setMessage(fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status.next }));
405
- loadTasks();
406
- if (selectedTaskIndex >= currentTasks.length - 1) {
407
- setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
408
- }
393
+ moveTaskToStatus(task, 'next').then(() => {
394
+ if (selectedTaskIndex >= currentTasks.length - 1) {
395
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
396
+ }
397
+ });
409
398
  return;
410
399
  }
411
400
  // Move to someday
412
401
  if (input === 's' && currentTasks.length > 0 && currentTab !== 'someday' && currentTab !== 'projects') {
413
402
  const task = currentTasks[selectedTaskIndex];
414
- const db = getDb();
415
- db.update(schema.tasks)
416
- .set({ status: 'someday', waitingFor: null, updatedAt: new Date() })
417
- .where(eq(schema.tasks.id, task.id))
418
- .run();
419
- setMessage(fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status.someday }));
420
- loadTasks();
421
- if (selectedTaskIndex >= currentTasks.length - 1) {
422
- setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
423
- }
403
+ moveTaskToStatus(task, 'someday').then(() => {
404
+ if (selectedTaskIndex >= currentTasks.length - 1) {
405
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
406
+ }
407
+ });
424
408
  return;
425
409
  }
426
410
  // Move to inbox
427
411
  if (input === 'i' && currentTasks.length > 0 && currentTab !== 'inbox' && currentTab !== 'projects') {
428
412
  const task = currentTasks[selectedTaskIndex];
429
- const db = getDb();
430
- db.update(schema.tasks)
431
- .set({ status: 'inbox', waitingFor: null, updatedAt: new Date() })
432
- .where(eq(schema.tasks.id, task.id))
433
- .run();
434
- setMessage(fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status.inbox }));
435
- loadTasks();
436
- if (selectedTaskIndex >= currentTasks.length - 1) {
437
- setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
438
- }
413
+ moveTaskToStatus(task, 'inbox').then(() => {
414
+ if (selectedTaskIndex >= currentTasks.length - 1) {
415
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
416
+ }
417
+ });
439
418
  return;
440
419
  }
441
420
  // Refresh
@@ -458,7 +437,21 @@ function AppContent() {
458
437
  const [open, close] = theme.style.tabBrackets;
459
438
  return `${open}${label}${close}`;
460
439
  };
461
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: formatTitle(i18n.tui.title) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), _jsx(Box, { marginBottom: 1, children: TABS.map((tab, index) => {
440
+ // Turso 接続情報を取得
441
+ const tursoEnabled = isTursoEnabled();
442
+ const tursoHost = tursoEnabled ? (() => {
443
+ const config = getTursoConfig();
444
+ if (config) {
445
+ try {
446
+ return new URL(config.url).host;
447
+ }
448
+ catch {
449
+ return config.url;
450
+ }
451
+ }
452
+ return '';
453
+ })() : '';
454
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: formatTitle(i18n.tui.title) }), tursoEnabled && (_jsxs(Text, { color: theme.colors.accent, children: [theme.name === 'modern' ? ' ☁️ ' : ' [SYNC] ', tursoHost] })), !tursoEnabled && (_jsx(Text, { color: theme.colors.textMuted, children: theme.name === 'modern' ? ' 💾 local' : ' [LOCAL]' }))] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), _jsx(Box, { marginBottom: 1, children: TABS.map((tab, index) => {
462
455
  const isActive = index === currentListIndex && mode !== 'project-detail';
463
456
  const count = tasks[tab].length;
464
457
  const label = `${index + 1}:${getTabLabel(tab)}(${count})`;
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { type ThemeName } from '../config.js';
2
+ import type { ThemeName } from './theme/types.js';
3
3
  interface ThemeSelectorProps {
4
4
  onSelect: (theme: ThemeName) => void;
5
5
  }
@@ -1,17 +1,30 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- import { Select } from '@inkjs/ui';
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
4
  import { themes, VALID_THEMES } from './theme/themes.js';
5
5
  import { getThemeName } from '../config.js';
6
6
  export function ThemeSelector({ onSelect }) {
7
7
  const currentTheme = getThemeName();
8
- const options = VALID_THEMES.map((themeName) => {
9
- const theme = themes[themeName];
10
- const isCurrent = themeName === currentTheme;
11
- return {
12
- label: `${theme.displayName}${isCurrent ? ' (current)' : ''}`,
13
- value: themeName,
14
- };
8
+ const initialIndex = VALID_THEMES.indexOf(currentTheme);
9
+ const [selectedIndex, setSelectedIndex] = useState(initialIndex >= 0 ? initialIndex : 0);
10
+ useInput((input, key) => {
11
+ // j or down arrow: move down
12
+ if (input === 'j' || key.downArrow) {
13
+ setSelectedIndex((prev) => (prev < VALID_THEMES.length - 1 ? prev + 1 : 0));
14
+ }
15
+ // k or up arrow: move up
16
+ if (input === 'k' || key.upArrow) {
17
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : VALID_THEMES.length - 1));
18
+ }
19
+ // Enter: select
20
+ if (key.return) {
21
+ onSelect(VALID_THEMES[selectedIndex]);
22
+ }
15
23
  });
16
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Select a theme:" }), _jsx(Box, { marginTop: 1, children: _jsx(Select, { options: options, onChange: (value) => onSelect(value) }) })] }));
24
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Select a theme:" }), _jsx(Text, { dimColor: true, children: "j/k: select, Enter: confirm" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: VALID_THEMES.map((themeName, index) => {
25
+ const theme = themes[themeName];
26
+ const isSelected = index === selectedIndex;
27
+ const isCurrent = themeName === currentTheme;
28
+ return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? 'cyan' : undefined, children: [isSelected ? '› ' : ' ', theme.displayName, isCurrent ? ' (current)' : ''] }) }, themeName));
29
+ }) })] }));
17
30
  }
@@ -1,10 +1,11 @@
1
1
  import React from 'react';
2
- interface FunctionKey {
2
+ interface ActionKey {
3
3
  key: string;
4
4
  label: string;
5
5
  }
6
- interface FunctionKeyBarProps {
7
- keys?: FunctionKey[];
6
+ interface ActionKeyBarProps {
7
+ keys?: ActionKey[];
8
8
  }
9
- export declare function FunctionKeyBar({ keys }: FunctionKeyBarProps): React.ReactElement;
9
+ export declare function ActionKeyBar({ keys }: ActionKeyBarProps): React.ReactElement;
10
+ export declare const FunctionKeyBar: typeof ActionKeyBar;
10
11
  export {};
@@ -1,19 +1,23 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { useTheme } from '../theme/index.js';
4
- const DEFAULT_KEYS = [
5
- { key: 'F1', label: 'Help' },
6
- { key: 'F2', label: 'Add' },
7
- { key: 'F3', label: 'Done' },
8
- { key: 'F4', label: 'Next' },
9
- { key: 'F5', label: 'Rfrsh' },
10
- { key: 'F6', label: '' },
11
- { key: 'F7', label: '' },
12
- { key: 'F8', label: '' },
13
- { key: 'F9', label: '' },
14
- { key: 'F10', label: 'Quit' },
15
- ];
16
- export function FunctionKeyBar({ keys = DEFAULT_KEYS }) {
4
+ import { t } from '../../i18n/index.js';
5
+ export function ActionKeyBar({ keys }) {
17
6
  const theme = useTheme();
18
- return (_jsx(Box, { children: keys.map((fn, index) => (_jsxs(Box, { marginRight: 0, children: [_jsx(Text, { backgroundColor: theme.colors.fnKeyLabel, color: "white", bold: true, children: fn.key }), _jsx(Text, { backgroundColor: theme.colors.fnKeyText, color: "black", children: fn.label.padEnd(5, ' ') })] }, index))) }));
7
+ const i18n = t();
8
+ const kb = i18n.tui.keyBar;
9
+ const defaultKeys = [
10
+ { key: 'a', label: kb.add },
11
+ { key: 'd', label: kb.done },
12
+ { key: 'n', label: kb.next },
13
+ { key: 's', label: kb.someday },
14
+ { key: 'i', label: kb.inbox },
15
+ { key: 'p', label: kb.project },
16
+ { key: '?', label: kb.help },
17
+ { key: 'q', label: kb.quit },
18
+ ];
19
+ const displayKeys = keys || defaultKeys;
20
+ return (_jsx(Box, { children: displayKeys.map((action, index) => (_jsxs(Box, { marginRight: 1, children: [_jsxs(Text, { color: theme.colors.fnKeyLabel, bold: true, children: ["[", action.key, "]"] }), _jsx(Text, { color: theme.colors.fnKeyText, children: action.label })] }, index))) }));
19
21
  }
22
+ // Alias for backward compatibility
23
+ export const FunctionKeyBar = ActionKeyBar;
@@ -3,6 +3,18 @@ export declare const modernTheme: Theme;
3
3
  export declare const nortonCommanderTheme: Theme;
4
4
  export declare const dosPromptTheme: Theme;
5
5
  export declare const turboPascalTheme: Theme;
6
+ export declare const classicMacTheme: Theme;
7
+ export declare const appleIITheme: Theme;
8
+ export declare const commodore64Theme: Theme;
9
+ export declare const amigaWorkbenchTheme: Theme;
10
+ export declare const matrixTheme: Theme;
11
+ export declare const amberCrtTheme: Theme;
12
+ export declare const phosphorTheme: Theme;
13
+ export declare const solarizedDarkTheme: Theme;
14
+ export declare const solarizedLightTheme: Theme;
15
+ export declare const synthwaveTheme: Theme;
16
+ export declare const paperTheme: Theme;
17
+ export declare const coffeeTheme: Theme;
6
18
  export declare const themes: Record<ThemeName, Theme>;
7
19
  export declare const VALID_THEMES: ThemeName[];
8
20
  export declare function getTheme(name: ThemeName): Theme;