@assistkick/create 1.0.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 (178) hide show
  1. package/dist/bin/create.d.ts +2 -0
  2. package/dist/bin/create.js +25 -0
  3. package/dist/bin/create.js.map +1 -0
  4. package/dist/src/scaffolder.d.ts +22 -0
  5. package/dist/src/scaffolder.js +120 -0
  6. package/dist/src/scaffolder.js.map +1 -0
  7. package/package.json +24 -0
  8. package/templates/product-system/.env.example +8 -0
  9. package/templates/product-system/CLAUDE.md +45 -0
  10. package/templates/product-system/package.json +32 -0
  11. package/templates/product-system/packages/backend/package.json +37 -0
  12. package/templates/product-system/packages/backend/src/middleware/auth_middleware.test.ts +86 -0
  13. package/templates/product-system/packages/backend/src/middleware/auth_middleware.ts +35 -0
  14. package/templates/product-system/packages/backend/src/routes/auth.ts +463 -0
  15. package/templates/product-system/packages/backend/src/routes/coherence.ts +187 -0
  16. package/templates/product-system/packages/backend/src/routes/graph.ts +67 -0
  17. package/templates/product-system/packages/backend/src/routes/kanban.ts +201 -0
  18. package/templates/product-system/packages/backend/src/routes/pipeline.ts +41 -0
  19. package/templates/product-system/packages/backend/src/routes/projects.ts +122 -0
  20. package/templates/product-system/packages/backend/src/routes/users.ts +97 -0
  21. package/templates/product-system/packages/backend/src/server.ts +159 -0
  22. package/templates/product-system/packages/backend/src/services/auth_service.test.ts +115 -0
  23. package/templates/product-system/packages/backend/src/services/auth_service.ts +82 -0
  24. package/templates/product-system/packages/backend/src/services/coherence-review.ts +339 -0
  25. package/templates/product-system/packages/backend/src/services/email_service.ts +75 -0
  26. package/templates/product-system/packages/backend/src/services/init.ts +80 -0
  27. package/templates/product-system/packages/backend/src/services/invitation_service.test.ts +235 -0
  28. package/templates/product-system/packages/backend/src/services/invitation_service.ts +193 -0
  29. package/templates/product-system/packages/backend/src/services/password_reset_service.test.ts +151 -0
  30. package/templates/product-system/packages/backend/src/services/password_reset_service.ts +135 -0
  31. package/templates/product-system/packages/backend/src/services/project_service.test.ts +215 -0
  32. package/templates/product-system/packages/backend/src/services/project_service.ts +171 -0
  33. package/templates/product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -0
  34. package/templates/product-system/packages/backend/src/services/pty_session_manager.ts +279 -0
  35. package/templates/product-system/packages/backend/src/services/terminal_ws_handler.ts +133 -0
  36. package/templates/product-system/packages/backend/src/services/user_management_service.test.ts +158 -0
  37. package/templates/product-system/packages/backend/src/services/user_management_service.ts +128 -0
  38. package/templates/product-system/packages/backend/tsconfig.json +22 -0
  39. package/templates/product-system/packages/frontend/index.html +13 -0
  40. package/templates/product-system/packages/frontend/package-lock.json +2666 -0
  41. package/templates/product-system/packages/frontend/package.json +30 -0
  42. package/templates/product-system/packages/frontend/public/favicon.svg +16 -0
  43. package/templates/product-system/packages/frontend/src/App.tsx +29 -0
  44. package/templates/product-system/packages/frontend/src/api/client.ts +386 -0
  45. package/templates/product-system/packages/frontend/src/api/client_projects.test.ts +104 -0
  46. package/templates/product-system/packages/frontend/src/api/client_refresh.test.ts +145 -0
  47. package/templates/product-system/packages/frontend/src/components/CoherenceView.tsx +414 -0
  48. package/templates/product-system/packages/frontend/src/components/GraphLegend.tsx +124 -0
  49. package/templates/product-system/packages/frontend/src/components/GraphSettings.tsx +112 -0
  50. package/templates/product-system/packages/frontend/src/components/GraphView.tsx +370 -0
  51. package/templates/product-system/packages/frontend/src/components/InviteUserDialog.tsx +85 -0
  52. package/templates/product-system/packages/frontend/src/components/KanbanView.tsx +470 -0
  53. package/templates/product-system/packages/frontend/src/components/LoginPage.tsx +116 -0
  54. package/templates/product-system/packages/frontend/src/components/ProjectSelector.tsx +187 -0
  55. package/templates/product-system/packages/frontend/src/components/QaIssueSheet.tsx +192 -0
  56. package/templates/product-system/packages/frontend/src/components/SidePanel.tsx +231 -0
  57. package/templates/product-system/packages/frontend/src/components/TerminalView.tsx +200 -0
  58. package/templates/product-system/packages/frontend/src/components/Toolbar.tsx +84 -0
  59. package/templates/product-system/packages/frontend/src/components/UsersView.tsx +249 -0
  60. package/templates/product-system/packages/frontend/src/constants/graph.ts +191 -0
  61. package/templates/product-system/packages/frontend/src/hooks/useAuth.tsx +54 -0
  62. package/templates/product-system/packages/frontend/src/hooks/useGraph.ts +27 -0
  63. package/templates/product-system/packages/frontend/src/hooks/useKanban.ts +21 -0
  64. package/templates/product-system/packages/frontend/src/hooks/useProjects.ts +86 -0
  65. package/templates/product-system/packages/frontend/src/hooks/useTheme.ts +26 -0
  66. package/templates/product-system/packages/frontend/src/hooks/useToast.tsx +62 -0
  67. package/templates/product-system/packages/frontend/src/hooks/use_projects_logic.test.ts +61 -0
  68. package/templates/product-system/packages/frontend/src/main.tsx +12 -0
  69. package/templates/product-system/packages/frontend/src/pages/accept_invitation_page.tsx +167 -0
  70. package/templates/product-system/packages/frontend/src/pages/forgot_password_page.tsx +100 -0
  71. package/templates/product-system/packages/frontend/src/pages/register_page.tsx +137 -0
  72. package/templates/product-system/packages/frontend/src/pages/reset_password_page.tsx +146 -0
  73. package/templates/product-system/packages/frontend/src/routes/ProtectedRoute.tsx +12 -0
  74. package/templates/product-system/packages/frontend/src/routes/accept_invitation.tsx +14 -0
  75. package/templates/product-system/packages/frontend/src/routes/dashboard.tsx +221 -0
  76. package/templates/product-system/packages/frontend/src/routes/forgot_password.tsx +13 -0
  77. package/templates/product-system/packages/frontend/src/routes/login.tsx +14 -0
  78. package/templates/product-system/packages/frontend/src/routes/register.tsx +14 -0
  79. package/templates/product-system/packages/frontend/src/routes/reset_password.tsx +13 -0
  80. package/templates/product-system/packages/frontend/src/styles/index.css +3358 -0
  81. package/templates/product-system/packages/frontend/src/utils/auth_validation.test.ts +51 -0
  82. package/templates/product-system/packages/frontend/src/utils/auth_validation.ts +19 -0
  83. package/templates/product-system/packages/frontend/src/utils/login_validation.test.ts +61 -0
  84. package/templates/product-system/packages/frontend/src/utils/login_validation.ts +24 -0
  85. package/templates/product-system/packages/frontend/src/utils/logout.test.ts +63 -0
  86. package/templates/product-system/packages/frontend/src/utils/node_sizing.test.ts +62 -0
  87. package/templates/product-system/packages/frontend/src/utils/node_sizing.ts +24 -0
  88. package/templates/product-system/packages/frontend/src/utils/task_status.test.ts +53 -0
  89. package/templates/product-system/packages/frontend/src/utils/task_status.ts +14 -0
  90. package/templates/product-system/packages/frontend/tsconfig.json +21 -0
  91. package/templates/product-system/packages/frontend/vite.config.ts +20 -0
  92. package/templates/product-system/packages/shared/.env.example +3 -0
  93. package/templates/product-system/packages/shared/README.md +1 -0
  94. package/templates/product-system/packages/shared/db/migrate.ts +32 -0
  95. package/templates/product-system/packages/shared/db/migrations/0000_dashing_gorgon.sql +128 -0
  96. package/templates/product-system/packages/shared/db/migrations/meta/0000_snapshot.json +819 -0
  97. package/templates/product-system/packages/shared/db/migrations/meta/_journal.json +13 -0
  98. package/templates/product-system/packages/shared/db/schema.ts +137 -0
  99. package/templates/product-system/packages/shared/drizzle.config.js +14 -0
  100. package/templates/product-system/packages/shared/lib/claude-service.ts +215 -0
  101. package/templates/product-system/packages/shared/lib/coherence.ts +278 -0
  102. package/templates/product-system/packages/shared/lib/completeness.ts +30 -0
  103. package/templates/product-system/packages/shared/lib/constants.ts +327 -0
  104. package/templates/product-system/packages/shared/lib/db.ts +81 -0
  105. package/templates/product-system/packages/shared/lib/git_workflow.ts +110 -0
  106. package/templates/product-system/packages/shared/lib/graph.ts +186 -0
  107. package/templates/product-system/packages/shared/lib/kanban.ts +161 -0
  108. package/templates/product-system/packages/shared/lib/markdown.ts +205 -0
  109. package/templates/product-system/packages/shared/lib/pipeline-state-store.ts +124 -0
  110. package/templates/product-system/packages/shared/lib/pipeline.ts +489 -0
  111. package/templates/product-system/packages/shared/lib/prompt_builder.ts +170 -0
  112. package/templates/product-system/packages/shared/lib/relevance_search.ts +159 -0
  113. package/templates/product-system/packages/shared/lib/session.ts +152 -0
  114. package/templates/product-system/packages/shared/lib/validator.ts +117 -0
  115. package/templates/product-system/packages/shared/lib/work_summary_parser.ts +130 -0
  116. package/templates/product-system/packages/shared/package.json +30 -0
  117. package/templates/product-system/packages/shared/scripts/assign-project.ts +52 -0
  118. package/templates/product-system/packages/shared/tools/add_edge.ts +61 -0
  119. package/templates/product-system/packages/shared/tools/add_node.ts +101 -0
  120. package/templates/product-system/packages/shared/tools/end_session.ts +87 -0
  121. package/templates/product-system/packages/shared/tools/get_gaps.ts +87 -0
  122. package/templates/product-system/packages/shared/tools/get_kanban.ts +125 -0
  123. package/templates/product-system/packages/shared/tools/get_node.ts +78 -0
  124. package/templates/product-system/packages/shared/tools/get_status.ts +98 -0
  125. package/templates/product-system/packages/shared/tools/migrate_to_turso.ts +385 -0
  126. package/templates/product-system/packages/shared/tools/move_card.ts +143 -0
  127. package/templates/product-system/packages/shared/tools/rebuild_index.ts +77 -0
  128. package/templates/product-system/packages/shared/tools/remove_edge.ts +59 -0
  129. package/templates/product-system/packages/shared/tools/remove_node.ts +96 -0
  130. package/templates/product-system/packages/shared/tools/resolve_question.ts +75 -0
  131. package/templates/product-system/packages/shared/tools/search_nodes.ts +106 -0
  132. package/templates/product-system/packages/shared/tools/start_session.ts +144 -0
  133. package/templates/product-system/packages/shared/tools/update_node.ts +133 -0
  134. package/templates/product-system/packages/shared/tsconfig.json +24 -0
  135. package/templates/product-system/pnpm-workspace.yaml +2 -0
  136. package/templates/product-system/smoke_test.ts +219 -0
  137. package/templates/product-system/tests/coherence_review.test.ts +562 -0
  138. package/templates/product-system/tests/db_sqlite_fallback.test.ts +75 -0
  139. package/templates/product-system/tests/edge_type_color_coding.test.ts +147 -0
  140. package/templates/product-system/tests/emit-tool-use-events.test.ts +85 -0
  141. package/templates/product-system/tests/feature_kind.test.ts +139 -0
  142. package/templates/product-system/tests/gap_indicators.test.ts +199 -0
  143. package/templates/product-system/tests/graceful_init.test.ts +142 -0
  144. package/templates/product-system/tests/graph_legend.test.ts +314 -0
  145. package/templates/product-system/tests/graph_settings_sheet.test.ts +804 -0
  146. package/templates/product-system/tests/hide_defined_filter.test.ts +205 -0
  147. package/templates/product-system/tests/kanban.test.ts +529 -0
  148. package/templates/product-system/tests/neighborhood_focus.test.ts +132 -0
  149. package/templates/product-system/tests/node_search.test.ts +340 -0
  150. package/templates/product-system/tests/node_sizing.test.ts +170 -0
  151. package/templates/product-system/tests/node_type_toggle_filters.test.ts +285 -0
  152. package/templates/product-system/tests/node_type_visual_encoding.test.ts +103 -0
  153. package/templates/product-system/tests/pipeline-state-store.test.ts +268 -0
  154. package/templates/product-system/tests/pipeline-unit.test.ts +593 -0
  155. package/templates/product-system/tests/pipeline.test.ts +195 -0
  156. package/templates/product-system/tests/pipeline_stats_all_cards.test.ts +193 -0
  157. package/templates/product-system/tests/play_all.test.ts +296 -0
  158. package/templates/product-system/tests/qa_issue_sheet.test.ts +464 -0
  159. package/templates/product-system/tests/relevance_search.test.ts +186 -0
  160. package/templates/product-system/tests/search_reorder.test.ts +88 -0
  161. package/templates/product-system/tests/serve_ui.test.ts +281 -0
  162. package/templates/product-system/tests/serve_ui_drizzle.test.ts +114 -0
  163. package/templates/product-system/tests/session_context_recall.test.ts +135 -0
  164. package/templates/product-system/tests/side_panel.test.ts +345 -0
  165. package/templates/product-system/tests/spec_completeness_label.test.ts +69 -0
  166. package/templates/product-system/tests/url_routing_test.ts +122 -0
  167. package/templates/product-system/tests/user_login.test.ts +150 -0
  168. package/templates/product-system/tests/user_registration.test.ts +205 -0
  169. package/templates/product-system/tests/web_terminal.test.ts +572 -0
  170. package/templates/product-system/tests/work_summary.test.ts +211 -0
  171. package/templates/product-system/tests/zoom_pan.test.ts +43 -0
  172. package/templates/product-system/tsconfig.json +24 -0
  173. package/templates/skills/product-bootstrap/SKILL.md +312 -0
  174. package/templates/skills/product-code-reviewer/SKILL.md +147 -0
  175. package/templates/skills/product-debugger/SKILL.md +206 -0
  176. package/templates/skills/product-debugger/references/agent-browser.md +1156 -0
  177. package/templates/skills/product-developer/SKILL.md +182 -0
  178. package/templates/skills/product-interview/SKILL.md +220 -0
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Graph visualization constants — colors, labels, shapes.
3
+ */
4
+
5
+ export const NODE_COLORS: Record<string, string> = {
6
+ feature: '#4dabf7',
7
+ component: '#69db7c',
8
+ data_entity: '#ffd43b',
9
+ decision: '#ff8787',
10
+ tech_choice: '#da77f2',
11
+ non_functional_requirement: '#ff922b',
12
+ design_token: '#20c997',
13
+ design_pattern: '#748ffc',
14
+ user_role: '#f783ac',
15
+ flow: '#66d9e8',
16
+ assumption: '#adb5bd',
17
+ open_question: '#ffe066',
18
+ };
19
+
20
+ export const EDGE_LABELS: Record<string, string> = {
21
+ contains: 'contains',
22
+ depends_on: 'depends on',
23
+ governed_by: 'governed by',
24
+ constrained_by: 'constrained by',
25
+ implemented_with: 'implemented with',
26
+ reads_writes: 'reads/writes',
27
+ exposes: 'exposes',
28
+ consumes: 'consumes',
29
+ performed_by: 'performed by',
30
+ escalates_to: 'escalates to',
31
+ relates_to: 'relates to',
32
+ };
33
+
34
+ export const EDGE_COLORS: Record<string, string> = {
35
+ contains: '#868e96',
36
+ depends_on: '#ff6b6b',
37
+ governed_by: '#748ffc',
38
+ constrained_by: '#ff922b',
39
+ implemented_with: '#da77f2',
40
+ reads_writes: '#ffd43b',
41
+ exposes: '#69db7c',
42
+ consumes: '#4dabf7',
43
+ performed_by: '#f783ac',
44
+ escalates_to: '#ff8787',
45
+ relates_to: '#adb5bd',
46
+ };
47
+
48
+ export const TYPE_LABELS: Record<string, string> = {
49
+ feature: 'FEAT',
50
+ component: 'COMP',
51
+ data_entity: 'DATA',
52
+ decision: 'DEC',
53
+ tech_choice: 'TECH',
54
+ non_functional_requirement: 'NFR',
55
+ design_token: 'DTOK',
56
+ design_pattern: 'DPAT',
57
+ user_role: 'ROLE',
58
+ flow: 'FLOW',
59
+ assumption: 'ASMP',
60
+ open_question: 'OQ',
61
+ };
62
+
63
+ export const LEGEND_LABELS: Record<string, string> = {
64
+ feature: 'Feature',
65
+ component: 'Component',
66
+ data_entity: 'Data Entity',
67
+ decision: 'Decision',
68
+ tech_choice: 'Tech Choice',
69
+ non_functional_requirement: 'Non-Functional Req',
70
+ design_token: 'Design Token',
71
+ design_pattern: 'Design Pattern',
72
+ user_role: 'User Role',
73
+ flow: 'Flow',
74
+ assumption: 'Assumption',
75
+ open_question: 'Open Question',
76
+ };
77
+
78
+ export const NODE_SHAPES: Record<string, string> = {
79
+ feature: 'circle',
80
+ component: 'rounded-rect',
81
+ data_entity: 'diamond',
82
+ decision: 'hexagon',
83
+ tech_choice: 'triangle-up',
84
+ non_functional_requirement: 'pentagon',
85
+ design_token: 'star',
86
+ design_pattern: 'square',
87
+ user_role: 'oval',
88
+ flow: 'arrow',
89
+ assumption: 'triangle-down',
90
+ open_question: 'octagon',
91
+ };
92
+
93
+ export const nodeShapePath = (shape: string, r: number): string => {
94
+ switch (shape) {
95
+ case 'circle':
96
+ return `M${-r},0 A${r},${r} 0 1,1 ${r},0 A${r},${r} 0 1,1 ${-r},0 Z`;
97
+ case 'rounded-rect': {
98
+ const cr = r * 0.3;
99
+ const s = r * 0.85;
100
+ return `M${-s + cr},${-s} L${s - cr},${-s} Q${s},${-s} ${s},${-s + cr} L${s},${s - cr} Q${s},${s} ${s - cr},${s} L${-s + cr},${s} Q${-s},${s} ${-s},${s - cr} L${-s},${-s + cr} Q${-s},${-s} ${-s + cr},${-s} Z`;
101
+ }
102
+ case 'diamond': {
103
+ const s = r * 1.1;
104
+ return `M0,${-s} L${s},0 L0,${s} L${-s},0 Z`;
105
+ }
106
+ case 'hexagon': {
107
+ const pts: string[] = [];
108
+ for (let i = 0; i < 6; i++) {
109
+ const angle = (Math.PI / 3) * i - Math.PI / 2;
110
+ pts.push(`${r * Math.cos(angle)},${r * Math.sin(angle)}`);
111
+ }
112
+ return `M${pts.join(' L')} Z`;
113
+ }
114
+ case 'triangle-up': {
115
+ const s = r * 1.15;
116
+ return `M0,${-s} L${s},${s * 0.7} L${-s},${s * 0.7} Z`;
117
+ }
118
+ case 'triangle-down': {
119
+ const s = r * 1.15;
120
+ return `M0,${s} L${s},${-s * 0.7} L${-s},${-s * 0.7} Z`;
121
+ }
122
+ case 'pentagon': {
123
+ const pts: string[] = [];
124
+ for (let i = 0; i < 5; i++) {
125
+ const angle = (2 * Math.PI / 5) * i - Math.PI / 2;
126
+ pts.push(`${r * Math.cos(angle)},${r * Math.sin(angle)}`);
127
+ }
128
+ return `M${pts.join(' L')} Z`;
129
+ }
130
+ case 'star': {
131
+ const pts: string[] = [];
132
+ for (let i = 0; i < 10; i++) {
133
+ const angle = (Math.PI / 5) * i - Math.PI / 2;
134
+ const rad = i % 2 === 0 ? r : r * 0.5;
135
+ pts.push(`${rad * Math.cos(angle)},${rad * Math.sin(angle)}`);
136
+ }
137
+ return `M${pts.join(' L')} Z`;
138
+ }
139
+ case 'square': {
140
+ const s = r * 0.85;
141
+ return `M${-s},${-s} L${s},${-s} L${s},${s} L${-s},${s} Z`;
142
+ }
143
+ case 'oval': {
144
+ const rx = r;
145
+ const ry = r * 0.7;
146
+ return `M${-rx},0 A${rx},${ry} 0 1,1 ${rx},0 A${rx},${ry} 0 1,1 ${-rx},0 Z`;
147
+ }
148
+ case 'arrow': {
149
+ const s = r * 0.9;
150
+ return `M${-s},${-s * 0.5} L${s * 0.3},${-s * 0.5} L${s * 0.3},${-s} L${s},0 L${s * 0.3},${s} L${s * 0.3},${s * 0.5} L${-s},${s * 0.5} Z`;
151
+ }
152
+ case 'octagon': {
153
+ const pts: string[] = [];
154
+ for (let i = 0; i < 8; i++) {
155
+ const angle = (Math.PI / 4) * i - Math.PI / 8;
156
+ pts.push(`${r * Math.cos(angle)},${r * Math.sin(angle)}`);
157
+ }
158
+ return `M${pts.join(' L')} Z`;
159
+ }
160
+ default:
161
+ return `M${-r},0 A${r},${r} 0 1,1 ${r},0 A${r},${r} 0 1,1 ${-r},0 Z`;
162
+ }
163
+ };
164
+
165
+ export const ALL_NODE_TYPES = [
166
+ 'feature', 'component', 'data_entity', 'decision', 'tech_choice',
167
+ 'non_functional_requirement', 'design_token', 'design_pattern',
168
+ 'user_role', 'flow', 'assumption', 'open_question',
169
+ ];
170
+
171
+ export const COLUMNS = [
172
+ { id: 'todo', label: 'Todo' },
173
+ { id: 'in_progress', label: 'In Progress' },
174
+ { id: 'in_review', label: 'In Review' },
175
+ { id: 'qa', label: 'QA' },
176
+ { id: 'done', label: 'Done' },
177
+ ];
178
+
179
+ export const PIPELINE_STATUS_LABELS: Record<string, string> = {
180
+ creating_worktree: 'Setting up...',
181
+ debugging: 'Debugging',
182
+ developing: 'Developing',
183
+ reviewing: 'Reviewing',
184
+ fixing_review: 'Fixing review',
185
+ fixing_merge: 'Fixing merge',
186
+ merging: 'Merging',
187
+ completed: 'Completed',
188
+ failed: 'Failed',
189
+ blocked: 'Blocked',
190
+ interrupted: 'Interrupted',
191
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Auth hook — fetches current user info from /api/auth/me.
3
+ * Attempts token refresh on 401 before giving up.
4
+ * Returns user object with role, or null if not authenticated.
5
+ */
6
+
7
+ import { useState, useEffect } from 'react';
8
+
9
+ interface User {
10
+ id: string;
11
+ email: string;
12
+ role: string;
13
+ }
14
+
15
+ interface AuthState {
16
+ user: User | null;
17
+ loading: boolean;
18
+ }
19
+
20
+ export function useAuth(): AuthState {
21
+ const [state, setState] = useState<AuthState>({ user: null, loading: true });
22
+
23
+ useEffect(() => {
24
+ const fetchUser = async () => {
25
+ try {
26
+ let res = await fetch('/api/auth/me', { credentials: 'same-origin' });
27
+
28
+ if (res.status === 401) {
29
+ const refreshRes = await fetch('/api/auth/refresh', {
30
+ method: 'POST',
31
+ credentials: 'same-origin',
32
+ });
33
+ if (refreshRes.ok) {
34
+ res = await fetch('/api/auth/me', { credentials: 'same-origin' });
35
+ }
36
+ }
37
+
38
+ if (!res.ok) {
39
+ setState({ user: null, loading: false });
40
+ return;
41
+ }
42
+
43
+ const data = await res.json();
44
+ setState({ user: data?.user ?? null, loading: false });
45
+ } catch {
46
+ setState({ user: null, loading: false });
47
+ }
48
+ };
49
+
50
+ fetchUser();
51
+ }, []);
52
+
53
+ return state;
54
+ }
@@ -0,0 +1,27 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { apiClient } from '../api/client';
3
+
4
+ export function useGraph(projectId?: string | null) {
5
+ const [graphData, setGraphData] = useState<any>(null);
6
+ const [error, setError] = useState<string | null>(null);
7
+ const [loading, setLoading] = useState(true);
8
+
9
+ const fetchGraph = useCallback(async () => {
10
+ try {
11
+ setLoading(true);
12
+ const data = await apiClient.fetchGraph(projectId ?? undefined);
13
+ setGraphData(data);
14
+ setError(null);
15
+ } catch (err: any) {
16
+ setError(err.message);
17
+ } finally {
18
+ setLoading(false);
19
+ }
20
+ }, [projectId]);
21
+
22
+ useEffect(() => {
23
+ fetchGraph();
24
+ }, [fetchGraph]);
25
+
26
+ return { graphData, error, loading, refetch: fetchGraph };
27
+ }
@@ -0,0 +1,21 @@
1
+ import { useState, useCallback } from 'react';
2
+ import { apiClient } from '../api/client';
3
+
4
+ export function useKanban() {
5
+ const [kanbanData, setKanbanData] = useState<any>(null);
6
+ const [loading, setLoading] = useState(false);
7
+
8
+ const fetchKanban = useCallback(async () => {
9
+ try {
10
+ setLoading(true);
11
+ const data = await apiClient.fetchKanban();
12
+ setKanbanData(data);
13
+ } catch (err) {
14
+ console.error('Failed to load kanban:', err);
15
+ } finally {
16
+ setLoading(false);
17
+ }
18
+ }, []);
19
+
20
+ return { kanbanData, loading, fetchKanban };
21
+ }
@@ -0,0 +1,86 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { apiClient } from '../api/client';
3
+
4
+ const STORAGE_KEY = 'selectedProjectId';
5
+
6
+ export interface Project {
7
+ id: string;
8
+ name: string;
9
+ isDefault: number;
10
+ archivedAt: string | null;
11
+ createdAt: string;
12
+ updatedAt: string;
13
+ }
14
+
15
+ export function useProjects() {
16
+ const [projects, setProjects] = useState<Project[]>([]);
17
+ const [selectedProjectId, setSelectedProjectId] = useState<string | null>(
18
+ () => localStorage.getItem(STORAGE_KEY),
19
+ );
20
+ const [loading, setLoading] = useState(true);
21
+
22
+ const fetchProjects = useCallback(async () => {
23
+ try {
24
+ const data = await apiClient.fetchProjects();
25
+ setProjects(data.projects);
26
+ return data.projects as Project[];
27
+ } catch {
28
+ setProjects([]);
29
+ return [] as Project[];
30
+ } finally {
31
+ setLoading(false);
32
+ }
33
+ }, []);
34
+
35
+ useEffect(() => {
36
+ fetchProjects().then((fetched) => {
37
+ const stored = localStorage.getItem(STORAGE_KEY);
38
+ if (stored && fetched.some((p: Project) => p.id === stored)) {
39
+ setSelectedProjectId(stored);
40
+ } else if (fetched.length > 0) {
41
+ const defaultProject = fetched.find((p: Project) => p.isDefault === 1);
42
+ const fallback = defaultProject ? defaultProject.id : fetched[0].id;
43
+ setSelectedProjectId(fallback);
44
+ localStorage.setItem(STORAGE_KEY, fallback);
45
+ }
46
+ });
47
+ }, [fetchProjects]);
48
+
49
+ const selectProject = useCallback((id: string) => {
50
+ setSelectedProjectId(id);
51
+ localStorage.setItem(STORAGE_KEY, id);
52
+ }, []);
53
+
54
+ const createProject = useCallback(async (name: string) => {
55
+ const data = await apiClient.createProject(name);
56
+ await fetchProjects();
57
+ selectProject(data.project.id);
58
+ return data.project;
59
+ }, [fetchProjects, selectProject]);
60
+
61
+ const renameProject = useCallback(async (id: string, name: string) => {
62
+ await apiClient.renameProject(id, name);
63
+ await fetchProjects();
64
+ }, [fetchProjects]);
65
+
66
+ const archiveProject = useCallback(async (id: string) => {
67
+ await apiClient.archiveProject(id);
68
+ const updated = await fetchProjects();
69
+ if (selectedProjectId === id && updated.length > 0) {
70
+ const defaultProject = updated.find((p: Project) => p.isDefault === 1);
71
+ const fallback = defaultProject ? defaultProject.id : updated[0].id;
72
+ selectProject(fallback);
73
+ }
74
+ }, [fetchProjects, selectProject, selectedProjectId]);
75
+
76
+ return {
77
+ projects,
78
+ selectedProjectId,
79
+ loading,
80
+ selectProject,
81
+ createProject,
82
+ renameProject,
83
+ archiveProject,
84
+ refetchProjects: fetchProjects,
85
+ };
86
+ }
@@ -0,0 +1,26 @@
1
+ import { useState, useCallback, useEffect } from 'react';
2
+
3
+ const STORAGE_KEY = 'graph-ui-theme';
4
+
5
+ export function useTheme() {
6
+ const [theme, setThemeState] = useState<'dark' | 'light'>(() => {
7
+ try {
8
+ return (localStorage.getItem(STORAGE_KEY) as 'dark' | 'light') || 'dark';
9
+ } catch {
10
+ return 'dark';
11
+ }
12
+ });
13
+
14
+ useEffect(() => {
15
+ document.documentElement.setAttribute('data-theme', theme);
16
+ try {
17
+ localStorage.setItem(STORAGE_KEY, theme);
18
+ } catch { /* storage unavailable */ }
19
+ }, [theme]);
20
+
21
+ const toggle = useCallback(() => {
22
+ setThemeState(t => t === 'dark' ? 'light' : 'dark');
23
+ }, []);
24
+
25
+ return { theme, toggleTheme: toggle };
26
+ }
@@ -0,0 +1,62 @@
1
+ import React, { createContext, useContext, useState, useCallback, useRef } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+
4
+ type ToastType = 'error' | 'info' | 'success';
5
+
6
+ interface Toast {
7
+ id: number;
8
+ message: string;
9
+ type: ToastType;
10
+ }
11
+
12
+ interface ToastContextValue {
13
+ showToast: (message: string, type?: ToastType) => void;
14
+ }
15
+
16
+ const ToastContext = createContext<ToastContextValue | null>(null);
17
+
18
+ let nextId = 0;
19
+
20
+ export function ToastProvider({ children }: { children: React.ReactNode }) {
21
+ const [toasts, setToasts] = useState<Toast[]>([]);
22
+ const timersRef = useRef<Map<number, ReturnType<typeof setTimeout>>>(new Map());
23
+
24
+ const dismiss = useCallback((id: number) => {
25
+ const timer = timersRef.current.get(id);
26
+ if (timer) {
27
+ clearTimeout(timer);
28
+ timersRef.current.delete(id);
29
+ }
30
+ setToasts(prev => prev.filter(t => t.id !== id));
31
+ }, []);
32
+
33
+ const showToast = useCallback((message: string, type: ToastType = 'error') => {
34
+ const id = nextId++;
35
+ setToasts(prev => [...prev, { id, message, type }]);
36
+ const timer = setTimeout(() => dismiss(id), 6000);
37
+ timersRef.current.set(id, timer);
38
+ }, [dismiss]);
39
+
40
+ return (
41
+ <ToastContext.Provider value={{ showToast }}>
42
+ {children}
43
+ {createPortal(
44
+ <div className="toast-container">
45
+ {toasts.map(toast => (
46
+ <div key={toast.id} className={`kanban-toast kanban-toast-${toast.type}`}>
47
+ {toast.message}
48
+ <button className="kanban-toast-close" onClick={() => dismiss(toast.id)}>&times;</button>
49
+ </div>
50
+ ))}
51
+ </div>,
52
+ document.body
53
+ )}
54
+ </ToastContext.Provider>
55
+ );
56
+ }
57
+
58
+ export function useToast(): ToastContextValue {
59
+ const ctx = useContext(ToastContext);
60
+ if (!ctx) throw new Error('useToast must be used within a ToastProvider');
61
+ return ctx;
62
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ const STORAGE_KEY = 'selectedProjectId';
5
+
6
+ /**
7
+ * Tests for the project selection logic (data transformations only, not React hooks).
8
+ * Validates localStorage persistence and fallback behavior.
9
+ */
10
+
11
+ function resolveSelectedProject(
12
+ stored: string | null,
13
+ projects: { id: string; isDefault: number }[],
14
+ ): string | null {
15
+ if (stored && projects.some(p => p.id === stored)) {
16
+ return stored;
17
+ }
18
+ if (projects.length > 0) {
19
+ const defaultProject = projects.find(p => p.isDefault === 1);
20
+ return defaultProject ? defaultProject.id : projects[0].id;
21
+ }
22
+ return null;
23
+ }
24
+
25
+ describe('Project selection logic', () => {
26
+ it('restores stored project when it exists in the list', () => {
27
+ const result = resolveSelectedProject('proj_002', [
28
+ { id: 'proj_001', isDefault: 1 },
29
+ { id: 'proj_002', isDefault: 0 },
30
+ ]);
31
+ assert.equal(result, 'proj_002');
32
+ });
33
+
34
+ it('falls back to default project when stored id is missing', () => {
35
+ const result = resolveSelectedProject('proj_999', [
36
+ { id: 'proj_001', isDefault: 1 },
37
+ { id: 'proj_002', isDefault: 0 },
38
+ ]);
39
+ assert.equal(result, 'proj_001');
40
+ });
41
+
42
+ it('falls back to first project when no default and stored is missing', () => {
43
+ const result = resolveSelectedProject(null, [
44
+ { id: 'proj_001', isDefault: 0 },
45
+ { id: 'proj_002', isDefault: 0 },
46
+ ]);
47
+ assert.equal(result, 'proj_001');
48
+ });
49
+
50
+ it('returns null when no projects exist', () => {
51
+ const result = resolveSelectedProject('proj_001', []);
52
+ assert.equal(result, null);
53
+ });
54
+
55
+ it('falls back to default when stored project was archived', () => {
56
+ const result = resolveSelectedProject('proj_archived', [
57
+ { id: 'proj_001', isDefault: 1 },
58
+ ]);
59
+ assert.equal(result, 'proj_001');
60
+ });
61
+ });
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { BrowserRouter } from 'react-router-dom';
4
+ import { App } from './App';
5
+ import './styles/index.css';
6
+
7
+ const root = createRoot(document.getElementById('root')!);
8
+ root.render(
9
+ <BrowserRouter>
10
+ <App />
11
+ </BrowserRouter>
12
+ );