@assistkick/create 1.7.0 → 1.9.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/bin/create.js +0 -0
- package/package.json +9 -7
- package/templates/assistkick-product-system/.env.example +1 -0
- package/templates/assistkick-product-system/local.db +0 -0
- package/templates/assistkick-product-system/package.json +4 -2
- package/templates/assistkick-product-system/packages/backend/package.json +2 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +165 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +358 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +356 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +96 -1
- package/templates/assistkick-product-system/packages/backend/src/routes/graph.ts +1 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +61 -6
- package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +200 -84
- package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +6 -3
- package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +53 -17
- package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +218 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +119 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +158 -0
- package/templates/assistkick-product-system/packages/backend/src/server.ts +60 -9
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +489 -0
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +416 -0
- package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.test.ts +189 -0
- package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.ts +182 -0
- package/templates/assistkick-product-system/packages/backend/src/services/init.ts +43 -77
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +16 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +73 -2
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +4 -4
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +87 -11
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +210 -69
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +210 -215
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +162 -0
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +148 -0
- package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +11 -5
- package/templates/assistkick-product-system/packages/backend/src/services/tts_service.test.ts +64 -0
- package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +134 -0
- package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.test.ts +256 -0
- package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +258 -0
- package/templates/assistkick-product-system/packages/backend/src/services/workflow_group_service.ts +106 -0
- package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.test.ts +275 -0
- package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.ts +245 -0
- package/templates/assistkick-product-system/packages/frontend/package-lock.json +3455 -0
- package/templates/assistkick-product-system/packages/frontend/package.json +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/App.tsx +8 -0
- package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +458 -18
- package/templates/assistkick-product-system/packages/frontend/src/api/client_files.test.ts +172 -0
- package/templates/assistkick-product-system/packages/frontend/src/api/client_video.test.ts +238 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +307 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/CoherenceView.tsx +82 -66
- package/templates/assistkick-product-system/packages/frontend/src/components/CompositionPlaceholder.tsx +97 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/DesignSystemView.tsx +20 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +57 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +313 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeContextMenu.tsx +61 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +73 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +404 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +187 -56
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +71 -73
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphSettings.tsx +8 -8
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphView.tsx +1 -1
- package/templates/assistkick-product-system/packages/frontend/src/components/InviteUserDialog.tsx +15 -11
- package/templates/assistkick-product-system/packages/frontend/src/components/IterationCommentModal.tsx +80 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +263 -167
- package/templates/assistkick-product-system/packages/frontend/src/components/LoginPage.tsx +14 -14
- package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +54 -33
- package/templates/assistkick-product-system/packages/frontend/src/components/QaIssueSheet.tsx +32 -49
- package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +43 -48
- package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +121 -52
- package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +20 -14
- package/templates/assistkick-product-system/packages/frontend/src/components/UsersView.tsx +52 -52
- package/templates/assistkick-product-system/packages/frontend/src/components/VideoGallery.tsx +313 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/VideographyView.tsx +250 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/WorkflowsView.tsx +474 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/AccentBorderList.tsx +53 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/Button.tsx +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonGroup.tsx +29 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonShowcase.tsx +221 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/CardGlass.tsx +141 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/CompletionRing.tsx +30 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/ContentCard.tsx +34 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/IconButton.tsx +74 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCard.tsx +103 -87
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +9 -188
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/Kbd.tsx +11 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KindBadge.tsx +21 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/NavBarSidekick.tsx +81 -37
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/SidePanelShowcase.tsx +370 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/SideSheet.tsx +64 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/StatusDot.tsx +18 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCardPositionNode.tsx +36 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCycleCountNode.tsx +60 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/EndNode.tsx +42 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GenerateTTSNode.tsx +52 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +189 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +123 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RebuildBundleNode.tsx +20 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RenderVideoNode.tsx +72 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RunAgentNode.tsx +51 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/SetCardMetadataNode.tsx +53 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/StartNode.tsx +18 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/TransitionCardNode.tsx +59 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +341 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +643 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/autoLayout.ts +103 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/edgeColors.ts +35 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +246 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.test.ts +119 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +136 -0
- package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +13 -11
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useAutoSave.ts +75 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useToast.tsx +16 -3
- package/templates/assistkick-product-system/packages/frontend/src/pages/accept_invitation_page.tsx +30 -27
- package/templates/assistkick-product-system/packages/frontend/src/pages/forgot_password_page.tsx +18 -15
- package/templates/assistkick-product-system/packages/frontend/src/pages/register_page.tsx +21 -18
- package/templates/assistkick-product-system/packages/frontend/src/pages/reset_password_page.tsx +28 -25
- package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +1 -1
- package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +13 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +13 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +6 -3
- package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +4 -4
- package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +275 -3535
- package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.test.ts +167 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.ts +101 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.test.ts +42 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.ts +17 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.test.ts +145 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.ts +42 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.test.ts +4 -10
- package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.ts +19 -1
- package/templates/assistkick-product-system/packages/frontend/vite.config.ts +5 -0
- package/templates/assistkick-product-system/packages/shared/db/local.db +0 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0004_tidy_matthew_murdock.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0005_mysterious_falcon.sql +692 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0006_next_venom.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0007_deep_barracuda.sql +39 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0008_puzzling_hannibal_king.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0009_amused_beast.sql +8 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0010_spotty_moira_mactaggert.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0011_goofy_snowbird.sql +3 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0011_supreme_doctor_octopus.sql +3 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0013_reflective_prowler.sql +15 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0014_nifty_punisher.sql +15 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0004_snapshot.json +921 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0005_snapshot.json +1042 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0006_snapshot.json +1101 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0007_snapshot.json +1336 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0008_snapshot.json +1275 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0009_snapshot.json +1327 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0010_snapshot.json +1393 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0011_snapshot.json +1436 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0013_snapshot.json +1538 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0014_snapshot.json +1545 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +77 -0
- package/templates/assistkick-product-system/packages/shared/db/schema.ts +114 -0
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +32 -7
- package/templates/assistkick-product-system/packages/shared/lib/constants.ts +9 -0
- package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +12 -4
- package/templates/assistkick-product-system/packages/shared/lib/graph.ts +5 -0
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +1999 -0
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +1437 -0
- package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +211 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +43 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +13 -2
- package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +1 -1
- package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.test.ts +226 -0
- package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.ts +251 -0
- package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
- package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.test.ts +10 -0
- package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.ts +6 -0
- package/templates/assistkick-product-system/packages/video/Root.tsx +85 -0
- package/templates/assistkick-product-system/packages/video/components/email_scene.tsx +231 -0
- package/templates/assistkick-product-system/packages/video/components/outro_scene.tsx +153 -0
- package/templates/assistkick-product-system/packages/video/components/part_divider.tsx +90 -0
- package/templates/assistkick-product-system/packages/video/components/scene.tsx +226 -0
- package/templates/assistkick-product-system/packages/video/components/theme.ts +22 -0
- package/templates/assistkick-product-system/packages/video/components/title_scene.tsx +169 -0
- package/templates/assistkick-product-system/packages/video/components/video_split_layout.tsx +84 -0
- package/templates/assistkick-product-system/packages/video/compositions/.gitkeep +0 -0
- package/templates/assistkick-product-system/packages/video/index.ts +4 -0
- package/templates/assistkick-product-system/packages/video/package.json +28 -0
- package/templates/assistkick-product-system/packages/video/remotion.config.ts +11 -0
- package/templates/assistkick-product-system/packages/video/scripts/process_script.test.ts +326 -0
- package/templates/assistkick-product-system/packages/video/scripts/process_script.ts +630 -0
- package/templates/assistkick-product-system/packages/video/style.css +1 -0
- package/templates/assistkick-product-system/packages/video/tsconfig.json +18 -0
- package/templates/assistkick-product-system/tests/graph_legend.test.ts +2 -1
- package/templates/assistkick-product-system/tests/video_render_service.test.ts +181 -0
- package/templates/assistkick-product-system/tests/web_terminal.test.ts +219 -455
- package/templates/assistkick-product-system/tests/workflow_integration.test.ts +341 -0
- package/templates/skills/assistkick-developer/SKILL.md +3 -0
- package/templates/skills/assistkick-developer/references/react_development_guidelines.md +225 -0
- package/templates/skills/product-system/graph.json +1890 -0
- package/templates/skills/product-system/kanban.json +304 -0
- package/templates/skills/product-system/nodes/comp_001.md +56 -0
- package/templates/skills/product-system/nodes/comp_002.md +57 -0
- package/templates/skills/product-system/nodes/data_001.md +51 -0
- package/templates/skills/product-system/nodes/data_002.md +40 -0
- package/templates/skills/product-system/nodes/data_004.md +38 -0
- package/templates/skills/product-system/nodes/dec_001.md +34 -0
- package/templates/skills/product-system/nodes/dec_016.md +32 -0
- package/templates/skills/product-system/nodes/feat_008.md +30 -0
- package/templates/skills/video-composition-agent/SKILL.md +232 -0
- package/templates/skills/video-script-writer/SKILL.md +136 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Kanban Workflow Integration — dependency ordering,
|
|
3
|
+
* status mapping, and default workflow resolution logic.
|
|
4
|
+
* Uses node:test built-in runner.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it } from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Dependency-checking logic matching WorkflowOrchestrator.runLoop.
|
|
11
|
+
* Tests the pure data transformation.
|
|
12
|
+
*/
|
|
13
|
+
const checkDeps = (featureId: string, graphData: any, kanbanData: any): boolean => {
|
|
14
|
+
const deps = graphData.edges
|
|
15
|
+
.filter((e: any) => e.from === featureId && e.relation === 'depends_on')
|
|
16
|
+
.map((e: any) => e.to)
|
|
17
|
+
.filter((depId: string) => graphData.nodes.some((n: any) => n.id === depId && n.type === 'feature'));
|
|
18
|
+
return deps.some((depId: string) =>
|
|
19
|
+
!kanbanData[depId] || kanbanData[depId].column !== 'done'
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Simulates the Play All ordering: find next processable feature using
|
|
25
|
+
* the workflow orchestrator's dependency checking logic.
|
|
26
|
+
*/
|
|
27
|
+
const findNextProcessable = (
|
|
28
|
+
todoCards: { id: string; devBlocked: boolean }[],
|
|
29
|
+
graphData: any,
|
|
30
|
+
kanbanData: any,
|
|
31
|
+
) => {
|
|
32
|
+
for (const card of todoCards) {
|
|
33
|
+
if (card.devBlocked) continue;
|
|
34
|
+
if (!checkDeps(card.id, graphData, kanbanData)) return card;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Maps workflow status to display label, matching the frontend
|
|
41
|
+
* WORKFLOW_STATUS_LABELS constant.
|
|
42
|
+
*/
|
|
43
|
+
const WORKFLOW_STATUS_LABELS: Record<string, string> = {
|
|
44
|
+
running: 'Running',
|
|
45
|
+
completed: 'Completed',
|
|
46
|
+
failed: 'Failed',
|
|
47
|
+
idle: 'Idle',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Default workflow resolution logic — project-specific first, then global.
|
|
52
|
+
*/
|
|
53
|
+
const resolveDefault = (
|
|
54
|
+
workflows: { id: string; projectId: string | null; isDefault: number }[],
|
|
55
|
+
projectId?: string,
|
|
56
|
+
) => {
|
|
57
|
+
if (projectId) {
|
|
58
|
+
const projectDefault = workflows.find(
|
|
59
|
+
w => w.projectId === projectId && w.isDefault === 1,
|
|
60
|
+
);
|
|
61
|
+
if (projectDefault) return projectDefault;
|
|
62
|
+
}
|
|
63
|
+
// Fall back to global default
|
|
64
|
+
return workflows.find(w => w.projectId === null && w.isDefault === 1) || null;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
describe('WorkflowOrchestrator dependency ordering', () => {
|
|
68
|
+
it('unblocked when all deps are done', () => {
|
|
69
|
+
const graphData = {
|
|
70
|
+
nodes: [
|
|
71
|
+
{ id: 'feat_001', type: 'feature' },
|
|
72
|
+
{ id: 'feat_002', type: 'feature' },
|
|
73
|
+
],
|
|
74
|
+
edges: [{ from: 'feat_001', to: 'feat_002', relation: 'depends_on' }],
|
|
75
|
+
};
|
|
76
|
+
const kanbanData = {
|
|
77
|
+
feat_001: { column: 'todo' },
|
|
78
|
+
feat_002: { column: 'done' },
|
|
79
|
+
};
|
|
80
|
+
assert.equal(checkDeps('feat_001', graphData, kanbanData), false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('blocked when a dep is not done', () => {
|
|
84
|
+
const graphData = {
|
|
85
|
+
nodes: [
|
|
86
|
+
{ id: 'feat_001', type: 'feature' },
|
|
87
|
+
{ id: 'feat_002', type: 'feature' },
|
|
88
|
+
],
|
|
89
|
+
edges: [{ from: 'feat_001', to: 'feat_002', relation: 'depends_on' }],
|
|
90
|
+
};
|
|
91
|
+
const kanbanData = {
|
|
92
|
+
feat_001: { column: 'todo' },
|
|
93
|
+
feat_002: { column: 'in_progress' },
|
|
94
|
+
};
|
|
95
|
+
assert.equal(checkDeps('feat_001', graphData, kanbanData), true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('unblocked when no depends_on edges', () => {
|
|
99
|
+
const graphData = {
|
|
100
|
+
nodes: [{ id: 'feat_001', type: 'feature' }],
|
|
101
|
+
edges: [],
|
|
102
|
+
};
|
|
103
|
+
const kanbanData = { feat_001: { column: 'todo' } };
|
|
104
|
+
assert.equal(checkDeps('feat_001', graphData, kanbanData), false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('ignores non-feature dependency targets', () => {
|
|
108
|
+
const graphData = {
|
|
109
|
+
nodes: [
|
|
110
|
+
{ id: 'feat_001', type: 'feature' },
|
|
111
|
+
{ id: 'comp_001', type: 'component' },
|
|
112
|
+
],
|
|
113
|
+
edges: [{ from: 'feat_001', to: 'comp_001', relation: 'depends_on' }],
|
|
114
|
+
};
|
|
115
|
+
const kanbanData = { feat_001: { column: 'todo' } };
|
|
116
|
+
assert.equal(checkDeps('feat_001', graphData, kanbanData), false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('ignores non-depends_on relations', () => {
|
|
120
|
+
const graphData = {
|
|
121
|
+
nodes: [
|
|
122
|
+
{ id: 'feat_001', type: 'feature' },
|
|
123
|
+
{ id: 'feat_002', type: 'feature' },
|
|
124
|
+
],
|
|
125
|
+
edges: [{ from: 'feat_001', to: 'feat_002', relation: 'relates_to' }],
|
|
126
|
+
};
|
|
127
|
+
const kanbanData = {
|
|
128
|
+
feat_001: { column: 'todo' },
|
|
129
|
+
feat_002: { column: 'todo' },
|
|
130
|
+
};
|
|
131
|
+
assert.equal(checkDeps('feat_001', graphData, kanbanData), false);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('findNextProcessable', () => {
|
|
136
|
+
const graphData = {
|
|
137
|
+
nodes: [
|
|
138
|
+
{ id: 'feat_001', type: 'feature' },
|
|
139
|
+
{ id: 'feat_002', type: 'feature' },
|
|
140
|
+
{ id: 'feat_003', type: 'feature' },
|
|
141
|
+
],
|
|
142
|
+
edges: [
|
|
143
|
+
{ from: 'feat_002', to: 'feat_001', relation: 'depends_on' },
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
it('returns first unblocked feature', () => {
|
|
148
|
+
const kanbanData = {
|
|
149
|
+
feat_001: { column: 'todo' },
|
|
150
|
+
feat_002: { column: 'todo' },
|
|
151
|
+
};
|
|
152
|
+
const cards = [
|
|
153
|
+
{ id: 'feat_001', devBlocked: false },
|
|
154
|
+
{ id: 'feat_002', devBlocked: false },
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
const next = findNextProcessable(cards, graphData, kanbanData);
|
|
158
|
+
assert.equal(next!.id, 'feat_001');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('skips features with unmet dependencies', () => {
|
|
162
|
+
const kanbanData = {
|
|
163
|
+
feat_001: { column: 'todo' },
|
|
164
|
+
feat_002: { column: 'todo' },
|
|
165
|
+
feat_003: { column: 'todo' },
|
|
166
|
+
};
|
|
167
|
+
const cards = [
|
|
168
|
+
{ id: 'feat_002', devBlocked: false }, // depends on feat_001 in todo
|
|
169
|
+
{ id: 'feat_001', devBlocked: false }, // no deps
|
|
170
|
+
{ id: 'feat_003', devBlocked: false }, // no deps
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
const next = findNextProcessable(cards, graphData, kanbanData);
|
|
174
|
+
assert.equal(next!.id, 'feat_001');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('skips devBlocked features', () => {
|
|
178
|
+
const kanbanData = {
|
|
179
|
+
feat_001: { column: 'todo' },
|
|
180
|
+
feat_003: { column: 'todo' },
|
|
181
|
+
};
|
|
182
|
+
const cards = [
|
|
183
|
+
{ id: 'feat_001', devBlocked: true },
|
|
184
|
+
{ id: 'feat_003', devBlocked: false },
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
const next = findNextProcessable(cards, graphData, kanbanData);
|
|
188
|
+
assert.equal(next!.id, 'feat_003');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('returns null when all features are blocked', () => {
|
|
192
|
+
const kanbanData = {
|
|
193
|
+
feat_002: { column: 'todo' },
|
|
194
|
+
};
|
|
195
|
+
const cards = [
|
|
196
|
+
{ id: 'feat_002', devBlocked: false }, // depends on feat_001 not done
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
const next = findNextProcessable(cards, graphData, kanbanData);
|
|
200
|
+
assert.equal(next, null);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('Workflow status labels', () => {
|
|
205
|
+
it('maps running to Running', () => {
|
|
206
|
+
assert.equal(WORKFLOW_STATUS_LABELS['running'], 'Running');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('maps completed to Completed', () => {
|
|
210
|
+
assert.equal(WORKFLOW_STATUS_LABELS['completed'], 'Completed');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('maps failed to Failed', () => {
|
|
214
|
+
assert.equal(WORKFLOW_STATUS_LABELS['failed'], 'Failed');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('maps idle to Idle', () => {
|
|
218
|
+
assert.equal(WORKFLOW_STATUS_LABELS['idle'], 'Idle');
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('Workflow status resolution', () => {
|
|
223
|
+
it('running status with agent name indicates active agent node', () => {
|
|
224
|
+
const status = { status: 'running', currentAgentName: 'Developer', error: null };
|
|
225
|
+
assert.equal(status.status, 'running');
|
|
226
|
+
assert.equal(status.currentAgentName, 'Developer');
|
|
227
|
+
assert.equal(status.error, null);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('running status without agent name indicates non-agent node', () => {
|
|
231
|
+
const status = { status: 'running', currentAgentName: null, error: null };
|
|
232
|
+
assert.equal(status.currentAgentName, null);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('failed status includes error message', () => {
|
|
236
|
+
const status = { status: 'failed', currentAgentName: null, error: 'Agent crashed' };
|
|
237
|
+
assert.equal(status.status, 'failed');
|
|
238
|
+
assert.equal(status.error, 'Agent crashed');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('idle status when no execution exists', () => {
|
|
242
|
+
const status = { status: 'idle', featureId: 'feat_001' };
|
|
243
|
+
assert.equal(status.status, 'idle');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('completed status has no error', () => {
|
|
247
|
+
const status = { status: 'completed', error: null };
|
|
248
|
+
assert.equal(status.status, 'completed');
|
|
249
|
+
assert.equal(status.error, null);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('Default workflow resolution', () => {
|
|
254
|
+
const workflows = [
|
|
255
|
+
{ id: 'wf-global', projectId: null, isDefault: 1 },
|
|
256
|
+
{ id: 'wf-proj-1', projectId: 'proj_001', isDefault: 1 },
|
|
257
|
+
{ id: 'wf-proj-2', projectId: 'proj_001', isDefault: 0 },
|
|
258
|
+
{ id: 'wf-other', projectId: 'proj_002', isDefault: 1 },
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
it('resolves project-specific default when available', () => {
|
|
262
|
+
const result = resolveDefault(workflows, 'proj_001');
|
|
263
|
+
assert.equal(result!.id, 'wf-proj-1');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('falls back to global default when no project default', () => {
|
|
267
|
+
const result = resolveDefault(workflows, 'proj_999');
|
|
268
|
+
assert.equal(result!.id, 'wf-global');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('uses global default when no projectId provided', () => {
|
|
272
|
+
const result = resolveDefault(workflows);
|
|
273
|
+
assert.equal(result!.id, 'wf-global');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('returns null when no default exists', () => {
|
|
277
|
+
const noDefaults = [
|
|
278
|
+
{ id: 'wf-1', projectId: null, isDefault: 0 },
|
|
279
|
+
];
|
|
280
|
+
const result = resolveDefault(noDefaults);
|
|
281
|
+
assert.equal(result, null);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('prefers project-specific over global', () => {
|
|
285
|
+
const result = resolveDefault(workflows, 'proj_002');
|
|
286
|
+
assert.equal(result!.id, 'wf-other');
|
|
287
|
+
assert.equal(result!.projectId, 'proj_002');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('Resume button visibility logic', () => {
|
|
292
|
+
it('shows resume for failed workflow on in_progress card', () => {
|
|
293
|
+
const card = { column: 'in_progress' };
|
|
294
|
+
const workflowStatus = 'failed';
|
|
295
|
+
const isActive = workflowStatus === 'running';
|
|
296
|
+
const showResume = ['in_progress', 'in_review'].includes(card.column)
|
|
297
|
+
&& !isActive
|
|
298
|
+
&& workflowStatus === 'failed';
|
|
299
|
+
assert.equal(showResume, true);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('shows resume for failed workflow on in_review card', () => {
|
|
303
|
+
const card = { column: 'in_review' };
|
|
304
|
+
const workflowStatus = 'failed';
|
|
305
|
+
const isActive = workflowStatus === 'running';
|
|
306
|
+
const showResume = ['in_progress', 'in_review'].includes(card.column)
|
|
307
|
+
&& !isActive
|
|
308
|
+
&& workflowStatus === 'failed';
|
|
309
|
+
assert.equal(showResume, true);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('hides resume for running workflow', () => {
|
|
313
|
+
const card = { column: 'in_progress' };
|
|
314
|
+
const workflowStatus = 'running';
|
|
315
|
+
const isActive = workflowStatus === 'running';
|
|
316
|
+
const showResume = ['in_progress', 'in_review'].includes(card.column)
|
|
317
|
+
&& !isActive
|
|
318
|
+
&& workflowStatus === 'failed';
|
|
319
|
+
assert.equal(showResume, false);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('hides resume for todo card', () => {
|
|
323
|
+
const card = { column: 'todo' };
|
|
324
|
+
const workflowStatus = 'failed';
|
|
325
|
+
const isActive = workflowStatus === 'running';
|
|
326
|
+
const showResume = ['in_progress', 'in_review'].includes(card.column)
|
|
327
|
+
&& !isActive
|
|
328
|
+
&& workflowStatus === 'failed';
|
|
329
|
+
assert.equal(showResume, false);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('hides resume for completed workflow', () => {
|
|
333
|
+
const card = { column: 'in_progress' };
|
|
334
|
+
const workflowStatus = 'completed';
|
|
335
|
+
const isActive = workflowStatus === 'running';
|
|
336
|
+
const showResume = ['in_progress', 'in_review'].includes(card.column)
|
|
337
|
+
&& !isActive
|
|
338
|
+
&& workflowStatus === 'failed';
|
|
339
|
+
assert.equal(showResume, false);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
@@ -13,6 +13,9 @@ with the project spec and kanban exclusively through the tools below.
|
|
|
13
13
|
|
|
14
14
|
All tools live in `assistkick-product-system/packages/shared/tools/` and are run with `npx tsx`.
|
|
15
15
|
|
|
16
|
+
## References
|
|
17
|
+
- For React development best practices, see [react_development_guidelines.md](references/react_development_guidelines.md)
|
|
18
|
+
|
|
16
19
|
## Session Start Protocol
|
|
17
20
|
1. Call `get_status` — understand current project state
|
|
18
21
|
2. Call `get_kanban --column todo` — identify which features are available to implement
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# React Development Guidelines (2026)
|
|
2
|
+
|
|
3
|
+
## Core Principles
|
|
4
|
+
|
|
5
|
+
### Use React 19+ Features
|
|
6
|
+
- **React Compiler** is the default — do not manually wrap in `useMemo`, `useCallback`, or `React.memo` unless profiling shows a specific need. The compiler handles memoization automatically.
|
|
7
|
+
- **Server Components** are the default rendering model. Only add `'use client'` when the component needs interactivity (event handlers, state, effects, browser APIs).
|
|
8
|
+
- **Server Actions** (`'use server'`) for mutations — prefer over custom API routes for form submissions and data mutations.
|
|
9
|
+
- **`use()` hook** for reading promises and context inside render. Replaces many patterns that previously required `useEffect` + `useState`.
|
|
10
|
+
- **`useActionState`** for form state management — replaces the old `useFormState` pattern.
|
|
11
|
+
- **`useOptimistic`** for optimistic UI updates during async transitions.
|
|
12
|
+
- **`<form action={...}>`** — use the action prop on forms for progressive enhancement.
|
|
13
|
+
|
|
14
|
+
### Component Design
|
|
15
|
+
- **One component per file** — every component lives in its own file. No exceptions. If you need a helper component, extract it to a separate file.
|
|
16
|
+
- **Never define a component inside another component** — nested component definitions cause remounts on every render and destroy state. Extract inner components to their own files and import them.
|
|
17
|
+
- **Function components only** — never use class components.
|
|
18
|
+
- **Arrow function exports** — `export const MyComponent = () => { ... }`.
|
|
19
|
+
- **Props destructuring** in the parameter list — `({ title, onClose }) => ...`.
|
|
20
|
+
- **Co-locate related code** — keep component, types, and styles in the same directory.
|
|
21
|
+
- **Single responsibility** — one component does one thing. Extract when a component exceeds ~100 lines or has multiple concerns.
|
|
22
|
+
- **Composition over configuration** — prefer children and render props over deeply nested prop objects.
|
|
23
|
+
- **Always check the design system first** — before creating any new component, check if a matching component already exists in the project's design system. Reuse and extend existing design system components rather than building from scratch.
|
|
24
|
+
|
|
25
|
+
### State Management
|
|
26
|
+
- **Local state first** — `useState` for component-scoped state.
|
|
27
|
+
- **Zustand** for shared client state — lightweight, no boilerplate, works well with React 19.
|
|
28
|
+
- One store per domain concern (e.g., `useAuthStore`, `useUIStore`).
|
|
29
|
+
- Use selectors to subscribe to specific slices: `useStore(state => state.count)`.
|
|
30
|
+
- Keep stores flat — avoid deeply nested state objects.
|
|
31
|
+
- Use `immer` middleware only when state updates are genuinely complex.
|
|
32
|
+
- **Do not use** `useReducer` for complex state — Zustand stores are simpler and more testable.
|
|
33
|
+
- **URL state** for filters, pagination, search — use `useSearchParams` or the router.
|
|
34
|
+
- **Server state** — use the framework's data loading (Server Components, loaders) rather than client-side fetching libraries when possible.
|
|
35
|
+
|
|
36
|
+
### Data Fetching
|
|
37
|
+
- **Server Components** for initial data loading — fetch directly in the component, no `useEffect`.
|
|
38
|
+
- **Server Actions** for mutations — define with `'use server'`, call from client components.
|
|
39
|
+
- **`use()` hook** to unwrap promises passed from Server Components to Client Components.
|
|
40
|
+
- **Avoid `useEffect` for fetching** — it causes waterfalls, race conditions, and no caching. Use the framework's data loading primitives instead.
|
|
41
|
+
- **When client-side fetching is needed** (real-time, user-triggered), use `fetch` + `use()` or a minimal wrapper. Keep it simple.
|
|
42
|
+
|
|
43
|
+
### Hooks
|
|
44
|
+
- **Custom hooks** for reusable logic — prefix with `use`, keep them focused on one concern.
|
|
45
|
+
- **`useRef`** for DOM references and mutable values that don't trigger re-renders.
|
|
46
|
+
- **`useEffect`** only for synchronization with external systems (DOM manipulation, subscriptions, timers). Never for data fetching or derived state.
|
|
47
|
+
- **`useId`** for generating unique IDs for accessibility attributes.
|
|
48
|
+
- **No `useEffect` chains** — if one effect sets state that triggers another effect, restructure the logic.
|
|
49
|
+
|
|
50
|
+
### Styling — Tailwind CSS v4 Migration
|
|
51
|
+
- **All new components must use Tailwind CSS v4 classes** — do not create new `.css` files or add new CSS rules to existing stylesheets.
|
|
52
|
+
- **Migrate existing CSS when touching a component** — if you modify a component that uses CSS, convert its styles to Tailwind classes as part of the same change.
|
|
53
|
+
- **Use Tailwind utilities directly in JSX** — `className="flex items-center gap-2 p-4 rounded-lg bg-white"`.
|
|
54
|
+
- **Use `@apply` sparingly** — only when a utility combination is repeated across many elements in the same component and extraction to a shared component isn't practical.
|
|
55
|
+
- **Design tokens via Tailwind theme** — use theme values (`text-primary`, `bg-surface`) rather than hardcoded hex colors.
|
|
56
|
+
|
|
57
|
+
### TypeScript
|
|
58
|
+
- **Strict mode always** — `strict: true` in tsconfig.
|
|
59
|
+
- **Interface for component props** — `interface Props { ... }` defined above the component.
|
|
60
|
+
- **Avoid `any`** — use `unknown` and narrow, or define proper types.
|
|
61
|
+
- **Infer when possible** — don't annotate what TypeScript can infer (return types, simple variables).
|
|
62
|
+
- **Discriminated unions** for variant props — not boolean flags.
|
|
63
|
+
- **`satisfies`** operator for type-safe object literals while preserving inference.
|
|
64
|
+
- **Generic components** when the component works with multiple data shapes.
|
|
65
|
+
|
|
66
|
+
## Patterns
|
|
67
|
+
|
|
68
|
+
### File & Directory Structure
|
|
69
|
+
```
|
|
70
|
+
src/
|
|
71
|
+
components/ # Shared UI components
|
|
72
|
+
button/
|
|
73
|
+
button.tsx
|
|
74
|
+
button.test.tsx
|
|
75
|
+
features/ # Feature-specific modules
|
|
76
|
+
auth/
|
|
77
|
+
components/
|
|
78
|
+
hooks/
|
|
79
|
+
stores/
|
|
80
|
+
actions/
|
|
81
|
+
lib/ # Utilities and helpers
|
|
82
|
+
types/ # Shared type definitions
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Component File Structure
|
|
86
|
+
```tsx
|
|
87
|
+
// 1. Imports
|
|
88
|
+
import { useState } from 'react'
|
|
89
|
+
import { Button } from '@/components/button/button'
|
|
90
|
+
|
|
91
|
+
// 2. Types
|
|
92
|
+
interface Props {
|
|
93
|
+
title: string
|
|
94
|
+
onClose: () => void
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 3. Component
|
|
98
|
+
export const Dialog = ({ title, onClose }: Props) => {
|
|
99
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div role="dialog" aria-label={title}>
|
|
103
|
+
<h2>{title}</h2>
|
|
104
|
+
<Button onClick={onClose}>Close</Button>
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Zustand Store Pattern
|
|
111
|
+
```tsx
|
|
112
|
+
import { create } from 'zustand'
|
|
113
|
+
|
|
114
|
+
interface AuthState {
|
|
115
|
+
user: User | null
|
|
116
|
+
isLoading: boolean
|
|
117
|
+
login: (credentials: Credentials) => Promise<void>
|
|
118
|
+
logout: () => void
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const useAuthStore = create<AuthState>((set) => ({
|
|
122
|
+
user: null,
|
|
123
|
+
isLoading: false,
|
|
124
|
+
login: async (credentials) => {
|
|
125
|
+
set({ isLoading: true })
|
|
126
|
+
const user = await authApi.login(credentials)
|
|
127
|
+
set({ user, isLoading: false })
|
|
128
|
+
},
|
|
129
|
+
logout: () => set({ user: null }),
|
|
130
|
+
}))
|
|
131
|
+
|
|
132
|
+
// Usage — always use selectors
|
|
133
|
+
const user = useAuthStore((s) => s.user)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Server Component + Client Component Split
|
|
137
|
+
```tsx
|
|
138
|
+
// page.tsx (Server Component — no 'use client')
|
|
139
|
+
import { db } from '@/lib/db'
|
|
140
|
+
import { ItemList } from './item_list'
|
|
141
|
+
|
|
142
|
+
export const Page = async () => {
|
|
143
|
+
const items = await db.query.items.findMany()
|
|
144
|
+
return <ItemList items={items} />
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// item_list.tsx (Client Component — needs interactivity)
|
|
148
|
+
'use client'
|
|
149
|
+
import { useState } from 'react'
|
|
150
|
+
|
|
151
|
+
interface Props {
|
|
152
|
+
items: Item[]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const ItemList = ({ items }: Props) => {
|
|
156
|
+
const [filter, setFilter] = useState('')
|
|
157
|
+
const filtered = items.filter((i) => i.name.includes(filter))
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div>
|
|
161
|
+
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
|
|
162
|
+
{filtered.map((item) => <div key={item.id}>{item.name}</div>)}
|
|
163
|
+
</div>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Form with Server Action
|
|
169
|
+
```tsx
|
|
170
|
+
// actions.ts
|
|
171
|
+
'use server'
|
|
172
|
+
import { db } from '@/lib/db'
|
|
173
|
+
|
|
174
|
+
export const createItem = async (formData: FormData) => {
|
|
175
|
+
const name = formData.get('name') as string
|
|
176
|
+
await db.insert(items).values({ name })
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// form.tsx
|
|
180
|
+
'use client'
|
|
181
|
+
import { useActionState } from 'react'
|
|
182
|
+
import { createItem } from './actions'
|
|
183
|
+
|
|
184
|
+
export const CreateItemForm = () => {
|
|
185
|
+
const [state, action, isPending] = useActionState(createItem, null)
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<form action={action}>
|
|
189
|
+
<input name="name" required />
|
|
190
|
+
<button type="submit" disabled={isPending}>
|
|
191
|
+
{isPending ? 'Creating...' : 'Create'}
|
|
192
|
+
</button>
|
|
193
|
+
</form>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Anti-Patterns to Avoid
|
|
199
|
+
|
|
200
|
+
- **No `useEffect` for derived state** — compute during render instead.
|
|
201
|
+
- **No prop drilling beyond 2 levels** — use Zustand or composition.
|
|
202
|
+
- **No `index` as `key`** in dynamic lists — use stable unique IDs.
|
|
203
|
+
- **No barrel files** (`index.ts` re-exports) — they break tree-shaking and slow builds.
|
|
204
|
+
- **No default exports** — use named exports for better refactoring and imports.
|
|
205
|
+
- **No `React.FC`** — just type the props directly, it's simpler and more flexible.
|
|
206
|
+
- **No manual memoization** unless the React Compiler is explicitly disabled and profiling shows a need.
|
|
207
|
+
- **No `useEffect` + `setState` for data fetching** — use Server Components or the framework's data loading.
|
|
208
|
+
- **No new CSS files** — we are incrementally migrating from CSS to Tailwind CSS v4. All new components must use Tailwind classes exclusively. When touching existing components that use CSS, migrate their styles to Tailwind as part of the change.
|
|
209
|
+
- **No `any` type** — use `unknown` and narrow, or define the actual type.
|
|
210
|
+
|
|
211
|
+
## Accessibility Baseline
|
|
212
|
+
- Every interactive element is keyboard accessible.
|
|
213
|
+
- Images have `alt` text; decorative images use `alt=""`.
|
|
214
|
+
- Form inputs have associated `<label>` elements.
|
|
215
|
+
- Use semantic HTML (`<nav>`, `<main>`, `<article>`, `<button>`) — not `div` with `onClick`.
|
|
216
|
+
- Color is never the sole indicator of state.
|
|
217
|
+
- Use `useId()` for dynamic `id`/`aria-*` attributes.
|
|
218
|
+
|
|
219
|
+
## Performance
|
|
220
|
+
- **React Compiler handles memoization** — trust it, don't fight it.
|
|
221
|
+
- **Lazy load routes and heavy components** — `React.lazy()` + `Suspense`.
|
|
222
|
+
- **Virtualize long lists** — use `@tanstack/react-virtual` for lists > 100 items.
|
|
223
|
+
- **Avoid layout thrashing** — batch DOM reads/writes, use `useLayoutEffect` only when needed.
|
|
224
|
+
- **Image optimization** — use the framework's `<Image>` component or `loading="lazy"`.
|
|
225
|
+
- **Code split at the route level** — each route loads only what it needs.
|