@hustle-together/api-dev-tools 3.9.2 → 3.10.1

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 (30) hide show
  1. package/README.md +319 -22
  2. package/bin/cli.js +61 -13
  3. package/commands/hustle-api-create.md +13 -13
  4. package/commands/hustle-ui-create-page.md +933 -0
  5. package/hooks/api-workflow-check.py +160 -2
  6. package/hooks/check-api-routes.py +168 -0
  7. package/hooks/enforce-a11y-audit.py +202 -0
  8. package/hooks/enforce-brand-guide.py +115 -5
  9. package/hooks/enforce-page-components.py +186 -0
  10. package/hooks/enforce-page-data-schema.py +155 -0
  11. package/hooks/generate-manifest-entry.py +181 -1
  12. package/hooks/session-startup.py +95 -5
  13. package/hooks/update-ui-showcase.py +67 -3
  14. package/package.json +1 -2
  15. package/templates/api-dev-state.json +39 -1
  16. package/templates/settings.json +16 -0
  17. package/templates/shared/HeroHeader.tsx +1 -1
  18. package/demo/hustle-together/blog/gemini-vs-claude-widgets.html +0 -959
  19. package/demo/hustle-together/blog/interview-driven-api-development.html +0 -1146
  20. package/demo/hustle-together/blog/tdd-for-ai.html +0 -982
  21. package/demo/hustle-together/index.html +0 -1312
  22. package/demo/workflow-demo-v3.5-backup.html +0 -5008
  23. package/demo/workflow-demo.html +0 -6202
  24. /package/templates/api-showcase/{APICard.tsx → _components/APICard.tsx} +0 -0
  25. /package/templates/api-showcase/{APIModal.tsx → _components/APIModal.tsx} +0 -0
  26. /package/templates/api-showcase/{APIShowcase.tsx → _components/APIShowcase.tsx} +0 -0
  27. /package/templates/api-showcase/{APITester.tsx → _components/APITester.tsx} +0 -0
  28. /package/templates/ui-showcase/{PreviewCard.tsx → _components/PreviewCard.tsx} +0 -0
  29. /package/templates/ui-showcase/{PreviewModal.tsx → _components/PreviewModal.tsx} +0 -0
  30. /package/templates/ui-showcase/{UIShowcase.tsx → _components/UIShowcase.tsx} +0 -0
@@ -787,8 +787,169 @@ def generate_test_cases(schema: dict) -> list:
787
787
  return test_cases
788
788
 
789
789
 
790
+ def generate_orchestration_examples(endpoint: str, combine_config: dict, method: str) -> list:
791
+ """Generate orchestration examples for combined API endpoints.
792
+
793
+ Shows how multiple APIs are called in sequence/parallel and how data flows.
794
+ """
795
+ examples = []
796
+ source_elements = combine_config.get("source_elements", [])
797
+ flow_type = combine_config.get("flow_type", "sequential")
798
+ error_strategy = combine_config.get("error_strategy", "fail-fast")
799
+
800
+ if len(source_elements) < 2:
801
+ return examples
802
+
803
+ base_url = f"http://localhost:3001/api/v2/{endpoint}"
804
+
805
+ # Get source API names
806
+ source_names = []
807
+ for elem in source_elements:
808
+ if isinstance(elem, dict):
809
+ source_names.append(elem.get("name", "unknown"))
810
+ else:
811
+ source_names.append(str(elem))
812
+
813
+ # ================================================================
814
+ # Example 1: Sequential flow
815
+ # ================================================================
816
+ if flow_type == "sequential":
817
+ seq_body = {
818
+ "steps": [
819
+ {"api": source_names[0], "params": {"input": "example"}},
820
+ {"api": source_names[1], "params": {"useResultFrom": source_names[0]}}
821
+ ]
822
+ }
823
+ curl = f'curl -X {method} {base_url} \\\n'
824
+ curl += ' -H "Content-Type: application/json" \\\n'
825
+ curl += f" -d '{json.dumps(seq_body, indent=2)}'"
826
+
827
+ examples.append({
828
+ "name": "Sequential orchestration",
829
+ "description": f"Calls {source_names[0]} first, then {source_names[1]} with results",
830
+ "request": seq_body,
831
+ "curl": curl,
832
+ "flow": f"{source_names[0]} → {source_names[1]}"
833
+ })
834
+
835
+ # ================================================================
836
+ # Example 2: Parallel flow
837
+ # ================================================================
838
+ elif flow_type == "parallel":
839
+ par_body = {
840
+ "parallel": True,
841
+ "apis": [
842
+ {"name": source_names[0], "params": {"query": "example1"}},
843
+ {"name": source_names[1], "params": {"query": "example2"}}
844
+ ],
845
+ "merge_strategy": "combine"
846
+ }
847
+ curl = f'curl -X {method} {base_url} \\\n'
848
+ curl += ' -H "Content-Type: application/json" \\\n'
849
+ curl += f" -d '{json.dumps(par_body, indent=2)}'"
850
+
851
+ examples.append({
852
+ "name": "Parallel orchestration",
853
+ "description": f"Calls {source_names[0]} and {source_names[1]} simultaneously",
854
+ "request": par_body,
855
+ "curl": curl,
856
+ "flow": f"[{source_names[0]} || {source_names[1]}] → merge"
857
+ })
858
+
859
+ # ================================================================
860
+ # Example 3: Conditional flow
861
+ # ================================================================
862
+ elif flow_type == "conditional":
863
+ cond_body = {
864
+ "condition": {
865
+ "check": source_names[0],
866
+ "if_success": source_names[1],
867
+ "if_failure": "fallback"
868
+ },
869
+ "params": {"input": "example"}
870
+ }
871
+ curl = f'curl -X {method} {base_url} \\\n'
872
+ curl += ' -H "Content-Type: application/json" \\\n'
873
+ curl += f" -d '{json.dumps(cond_body, indent=2)}'"
874
+
875
+ examples.append({
876
+ "name": "Conditional orchestration",
877
+ "description": f"Calls {source_names[1]} only if {source_names[0]} succeeds",
878
+ "request": cond_body,
879
+ "curl": curl,
880
+ "flow": f"{source_names[0]} ? {source_names[1]} : fallback"
881
+ })
882
+
883
+ # ================================================================
884
+ # Example 4: With error handling
885
+ # ================================================================
886
+ error_body = {
887
+ "steps": [
888
+ {"api": source_names[0], "params": {"input": "data"}},
889
+ {"api": source_names[1], "params": {"input": "data"}}
890
+ ],
891
+ "error_strategy": error_strategy,
892
+ "retry": {"attempts": 3, "delay_ms": 1000}
893
+ }
894
+ curl = f'curl -X {method} {base_url} \\\n'
895
+ curl += ' -H "Content-Type: application/json" \\\n'
896
+ curl += f" -d '{json.dumps(error_body, indent=2)}'"
897
+
898
+ examples.append({
899
+ "name": f"With {error_strategy} error handling",
900
+ "description": f"Orchestration with {error_strategy} strategy and retry logic",
901
+ "request": error_body,
902
+ "curl": curl
903
+ })
904
+
905
+ # ================================================================
906
+ # Example 5: Full data flow
907
+ # ================================================================
908
+ full_body = {
909
+ "input": {
910
+ "source": "user-provided",
911
+ "data": {"query": "example query", "options": {"format": "json"}}
912
+ },
913
+ "flow": [
914
+ {
915
+ "step": 1,
916
+ "api": source_names[0],
917
+ "transform": "extract.content"
918
+ },
919
+ {
920
+ "step": 2,
921
+ "api": source_names[1],
922
+ "input_mapping": {"content": "$.step1.result"},
923
+ "transform": "format.output"
924
+ }
925
+ ],
926
+ "output": {"include": ["final_result", "metadata"]}
927
+ }
928
+ curl = f'curl -X {method} {base_url} \\\n'
929
+ curl += ' -H "Content-Type: application/json" \\\n'
930
+ curl += f" -d '{json.dumps(full_body, indent=2)}'"
931
+
932
+ examples.append({
933
+ "name": "Complete orchestration with transforms",
934
+ "description": "Full data flow with input mapping and output transformation",
935
+ "request": full_body,
936
+ "curl": curl,
937
+ "flow_diagram": f"""
938
+ Input → [{source_names[0]}] → transform → [{source_names[1]}] → Output
939
+ ↑ ↑
940
+ extract.content format.output
941
+ """
942
+ })
943
+
944
+ return examples
945
+
946
+
790
947
  def generate_manifest_entry(endpoint: str, endpoint_data: dict, state: dict) -> dict:
791
948
  """Generate a complete manifest entry for the endpoint."""
949
+ # Check if this is a combined workflow
950
+ combine_config = state.get("combine_config", {})
951
+ is_combined = bool(combine_config.get("source_elements"))
952
+
792
953
  # Find schema file
793
954
  schema_file = endpoint_data.get("phases", {}).get("schema_creation", {}).get("schema_file")
794
955
  if not schema_file:
@@ -819,6 +980,11 @@ def generate_manifest_entry(endpoint: str, endpoint_data: dict, state: dict) ->
819
980
  # Generate examples
820
981
  examples = generate_curl_examples(endpoint, method, request_schema)
821
982
 
983
+ # Add orchestration examples for combined endpoints
984
+ if is_combined:
985
+ orchestration_examples = generate_orchestration_examples(endpoint, combine_config, method)
986
+ examples.extend(orchestration_examples)
987
+
822
988
  # Generate test cases
823
989
  test_cases = generate_test_cases(request_schema)
824
990
 
@@ -853,12 +1019,26 @@ def generate_manifest_entry(endpoint: str, endpoint_data: dict, state: dict) ->
853
1019
  "testCases": test_cases,
854
1020
  "metadata": {
855
1021
  "generatedAt": datetime.now().isoformat(),
856
- "generatedBy": "api-dev-tools v3.6.7",
1022
+ "generatedBy": "api-dev-tools v3.10.0",
857
1023
  "schemaFile": schema_file,
858
1024
  "researchFresh": True
859
1025
  }
860
1026
  }
861
1027
 
1028
+ # Add combined workflow metadata
1029
+ if is_combined:
1030
+ source_names = []
1031
+ for elem in combine_config.get("source_elements", []):
1032
+ if isinstance(elem, dict):
1033
+ source_names.append(elem.get("name", "unknown"))
1034
+ else:
1035
+ source_names.append(str(elem))
1036
+
1037
+ entry["metadata"]["isCombined"] = True
1038
+ entry["metadata"]["sourceApis"] = source_names
1039
+ entry["metadata"]["flowType"] = combine_config.get("flow_type", "sequential")
1040
+ entry["metadata"]["errorStrategy"] = combine_config.get("error_strategy", "fail-fast")
1041
+
862
1042
  return entry
863
1043
 
864
1044
 
@@ -30,6 +30,22 @@ STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
30
30
  RESEARCH_INDEX = Path(__file__).parent.parent / "research" / "index.json"
31
31
 
32
32
 
33
+ def get_workflow_type(state):
34
+ """Detect the workflow type from state."""
35
+ workflow = state.get("workflow", "")
36
+ if workflow:
37
+ return workflow
38
+
39
+ # Infer from state structure
40
+ if state.get("combine_config"):
41
+ return "combine-api"
42
+ if state.get("ui_config"):
43
+ mode = state.get("ui_config", {}).get("mode", "")
44
+ return f"ui-create-{mode}" if mode else "ui-create-component"
45
+
46
+ return "api-create"
47
+
48
+
33
49
  def get_active_endpoint(state):
34
50
  """Get active endpoint - supports both old and new state formats."""
35
51
  # New format (v3.6.7+): endpoints object with active_endpoint pointer
@@ -39,12 +55,24 @@ def get_active_endpoint(state):
39
55
  return active, state["endpoints"][active]
40
56
  return None, None
41
57
 
58
+ # Support for elements (UI workflow)
59
+ if "elements" in state and "active_element" in state:
60
+ active = state.get("active_element")
61
+ if active and active in state["elements"]:
62
+ return active, state["elements"][active]
63
+ return None, None
64
+
42
65
  # Old format: single endpoint field
43
66
  endpoint = state.get("endpoint")
44
67
  if endpoint:
45
68
  # Return endpoint name and the entire state as endpoint data
46
69
  return endpoint, state
47
70
 
71
+ # Try active_element without elements dict
72
+ active = state.get("active_element")
73
+ if active:
74
+ return active, state
75
+
48
76
  return None, None
49
77
 
50
78
 
@@ -99,11 +127,64 @@ def main():
99
127
  print(json.dumps({"continue": True}))
100
128
  sys.exit(0)
101
129
 
130
+ # Detect workflow type
131
+ workflow_type = get_workflow_type(state)
132
+
102
133
  # Build context summary
103
134
  context_parts = []
104
- context_parts.append("## API Development Session Context")
135
+
136
+ # Header based on workflow type
137
+ if workflow_type == "combine-api":
138
+ context_parts.append("## Combined API Development Session Context")
139
+ elif workflow_type.startswith("ui-create"):
140
+ mode = "Page" if "page" in workflow_type else "Component"
141
+ context_parts.append(f"## UI {mode} Development Session Context")
142
+ else:
143
+ context_parts.append("## API Development Session Context")
144
+
105
145
  context_parts.append("")
106
- context_parts.append(f"**Active Endpoint:** {endpoint}")
146
+ context_parts.append(f"**Workflow:** {workflow_type}")
147
+ context_parts.append(f"**Active Element:** {endpoint}")
148
+
149
+ # Add combine-specific context
150
+ if workflow_type == "combine-api":
151
+ combine_config = state.get("combine_config", {})
152
+ source_elements = combine_config.get("source_elements", [])
153
+ flow_type = combine_config.get("flow_type", "sequential")
154
+ error_strategy = combine_config.get("error_strategy", "fail-fast")
155
+
156
+ if source_elements:
157
+ source_names = []
158
+ for elem in source_elements:
159
+ if isinstance(elem, dict):
160
+ source_names.append(elem.get("name", "unknown"))
161
+ else:
162
+ source_names.append(str(elem))
163
+
164
+ context_parts.append("")
165
+ context_parts.append("**Combining APIs:**")
166
+ for name in source_names:
167
+ context_parts.append(f" - {name}")
168
+ context_parts.append(f" Flow: {flow_type}")
169
+ context_parts.append(f" Error Strategy: {error_strategy}")
170
+
171
+ # Add UI-specific context
172
+ elif workflow_type.startswith("ui-create"):
173
+ ui_config = state.get("ui_config", {})
174
+ if not ui_config and endpoint_data:
175
+ ui_config = endpoint_data.get("ui_config", {})
176
+
177
+ if ui_config:
178
+ context_parts.append("")
179
+ context_parts.append("**UI Configuration:**")
180
+ if ui_config.get("use_brand_guide"):
181
+ context_parts.append(" - Brand guide: Applied")
182
+ if ui_config.get("component_type"):
183
+ context_parts.append(f" - Type: {ui_config['component_type']}")
184
+ if ui_config.get("accessibility_level"):
185
+ context_parts.append(f" - A11y: {ui_config['accessibility_level']}")
186
+ if ui_config.get("data_sources"):
187
+ context_parts.append(f" - Data sources: {len(ui_config['data_sources'])}")
107
188
 
108
189
  # Get phase status (from endpoint_data for multi-API, or state for legacy)
109
190
  phases = endpoint_data.get("phases", {})
@@ -183,10 +264,19 @@ def main():
183
264
  context_parts.append(" - Research: .claude/research/")
184
265
  context_parts.append(" - Manifest: src/app/api-test/api-tests-manifest.json (if exists)")
185
266
 
186
- # Workflow reminder
267
+ # Workflow reminder based on type
187
268
  context_parts.append("")
188
- context_parts.append("**Workflow Reminder:** This project uses interview-driven API development.")
189
- context_parts.append("Phases loop back if verification fails. Research before answering API questions.")
269
+ if workflow_type == "combine-api":
270
+ context_parts.append("**Workflow Reminder:** This is a combined API workflow.")
271
+ context_parts.append("Ensure all source APIs exist in registry before orchestration.")
272
+ context_parts.append("Test both individual APIs and the combined flow.")
273
+ elif workflow_type.startswith("ui-create"):
274
+ context_parts.append("**Workflow Reminder:** This is a UI development workflow.")
275
+ context_parts.append("Check registry for reusable components before creating new ones.")
276
+ context_parts.append("Ensure brand guide compliance and accessibility requirements.")
277
+ else:
278
+ context_parts.append("**Workflow Reminder:** This project uses interview-driven API development.")
279
+ context_parts.append("Phases loop back if verification fails. Research before answering API questions.")
190
280
 
191
281
  # Build the output
192
282
  additional_context = "\n".join(context_parts)
@@ -2,12 +2,15 @@
2
2
  """
3
3
  Hook: PostToolUse for Write/Edit
4
4
  Purpose: Auto-create UI Showcase page when first component/page is created
5
+ and auto-populate showcase data from registry.
5
6
 
6
7
  This hook monitors for new component or page registrations. When the first
7
8
  UI element is added to registry.json, it creates the UI Showcase page
8
9
  at src/app/ui-showcase/ if it doesn't exist.
9
10
 
10
- Version: 3.9.0
11
+ Also generates src/app/ui-showcase/data.json from registry for auto-population.
12
+
13
+ Version: 3.10.0
11
14
 
12
15
  Returns:
13
16
  - {"continue": true} - Always continues
@@ -17,12 +20,66 @@ import json
17
20
  import sys
18
21
  from pathlib import Path
19
22
  import shutil
23
+ from datetime import datetime
20
24
 
21
25
  # State and registry files in .claude/ directory
22
26
  STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
23
27
  REGISTRY_FILE = Path(__file__).parent.parent / "registry.json"
24
28
 
25
29
 
30
+ def generate_showcase_data(registry, cwd):
31
+ """Generate showcase data file from registry for auto-population.
32
+
33
+ Creates src/app/ui-showcase/data.json with component/page listings.
34
+ """
35
+ components = registry.get("components", {})
36
+ pages = registry.get("pages", {})
37
+
38
+ showcase_data = {
39
+ "version": "3.10.0",
40
+ "generated_at": datetime.now().isoformat(),
41
+ "components": [],
42
+ "pages": []
43
+ }
44
+
45
+ # Process components
46
+ for name, comp in components.items():
47
+ showcase_data["components"].append({
48
+ "id": name,
49
+ "name": comp.get("name", name),
50
+ "description": comp.get("description", ""),
51
+ "type": comp.get("type", "atom"),
52
+ "path": comp.get("path", f"src/components/{name}/{name}.tsx"),
53
+ "storybook_url": comp.get("storybook_url", f"/?path=/story/{name.lower()}--default"),
54
+ "variants": comp.get("variants", []),
55
+ "props": list(comp.get("props", {}).keys()) if isinstance(comp.get("props"), dict) else [],
56
+ "created_at": comp.get("created_at", ""),
57
+ "status": comp.get("status", "ready")
58
+ })
59
+
60
+ # Process pages
61
+ for name, page in pages.items():
62
+ showcase_data["pages"].append({
63
+ "id": name,
64
+ "name": page.get("name", name),
65
+ "description": page.get("description", ""),
66
+ "route": page.get("route", f"/{name}"),
67
+ "page_type": page.get("page_type", "landing"),
68
+ "path": page.get("path", f"src/app/{name}/page.tsx"),
69
+ "requires_auth": page.get("requires_auth", False),
70
+ "data_sources": page.get("data_sources", []),
71
+ "created_at": page.get("created_at", ""),
72
+ "status": page.get("status", "ready")
73
+ })
74
+
75
+ # Write data file
76
+ data_file = cwd / "src" / "app" / "ui-showcase" / "data.json"
77
+ data_file.parent.mkdir(parents=True, exist_ok=True)
78
+ data_file.write_text(json.dumps(showcase_data, indent=2))
79
+
80
+ return str(data_file.relative_to(cwd))
81
+
82
+
26
83
  def copy_showcase_templates(cwd):
27
84
  """Copy UI showcase templates to src/app/ui-showcase/."""
28
85
  # Source templates (installed by CLI)
@@ -131,13 +188,20 @@ def main():
131
188
  if components or pages:
132
189
  created_files = copy_showcase_templates(cwd)
133
190
 
191
+ # Always update data.json from registry
192
+ data_file = generate_showcase_data(registry, cwd)
193
+
134
194
  if created_files:
135
195
  print(json.dumps({
136
196
  "continue": True,
137
- "notify": f"Created UI Showcase at /ui-showcase ({len(created_files)} files)"
197
+ "notify": f"Created UI Showcase at /ui-showcase ({len(created_files)} files) + data.json"
138
198
  }))
139
199
  else:
140
- print(json.dumps({"continue": True}))
200
+ # Just updated data.json
201
+ print(json.dumps({
202
+ "continue": True,
203
+ "notify": f"Updated UI Showcase data ({len(components)} components, {len(pages)} pages)"
204
+ }))
141
205
  else:
142
206
  print(json.dumps({"continue": True}))
143
207
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hustle-together/api-dev-tools",
3
- "version": "3.9.2",
3
+ "version": "3.10.1",
4
4
  "description": "Interview-driven, research-first API and UI development workflow with continuous verification loops for Claude Code",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {
@@ -12,7 +12,6 @@
12
12
  "hooks/",
13
13
  "scripts/",
14
14
  "templates/",
15
- "demo/",
16
15
  "README.md",
17
16
  "LICENSE"
18
17
  ],
@@ -1,8 +1,46 @@
1
1
  {
2
- "version": "3.7.0",
2
+ "version": "3.10.0",
3
3
  "created_at": null,
4
+ "workflow": null,
5
+ "_workflow_options": ["api-create", "combine-api", "ui-create-component", "ui-create-page"],
4
6
  "active_endpoint": null,
7
+ "active_element": null,
5
8
  "endpoints": {},
9
+ "elements": {},
10
+ "combine_config": {
11
+ "_comment": "Configuration for combine-api workflow",
12
+ "mode": null,
13
+ "_mode_options": ["api", "ui"],
14
+ "source_elements": [],
15
+ "_source_elements_example": [
16
+ { "type": "api", "name": "openai", "registry_ref": "apis.openai" },
17
+ { "type": "api", "name": "wordpress", "registry_ref": "apis.wordpress" }
18
+ ],
19
+ "flow_type": null,
20
+ "_flow_type_options": ["sequential", "parallel", "conditional"],
21
+ "error_strategy": null,
22
+ "_error_strategy_options": ["fail-fast", "partial", "retry"],
23
+ "orchestration": {
24
+ "input_mapping": {},
25
+ "output_mapping": {},
26
+ "transforms": []
27
+ }
28
+ },
29
+ "ui_config": {
30
+ "_comment": "Configuration for ui-create-* workflows",
31
+ "mode": null,
32
+ "_mode_options": ["component", "page"],
33
+ "use_brand_guide": false,
34
+ "component_type": null,
35
+ "_component_type_options": ["atom", "molecule", "organism", "template"],
36
+ "page_type": null,
37
+ "_page_type_options": ["landing", "dashboard", "form", "list", "detail", "auth"],
38
+ "accessibility_level": null,
39
+ "_accessibility_level_options": ["AA", "AAA"],
40
+ "data_sources": [],
41
+ "variants": [],
42
+ "sizes": []
43
+ },
6
44
  "turn_count": 0,
7
45
  "last_turn_timestamp": null,
8
46
  "research_queries": [],
@@ -119,6 +119,18 @@
119
119
  {
120
120
  "type": "command",
121
121
  "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check-playwright-setup.py"
122
+ },
123
+ {
124
+ "type": "command",
125
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check-api-routes.py"
126
+ },
127
+ {
128
+ "type": "command",
129
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-page-components.py"
130
+ },
131
+ {
132
+ "type": "command",
133
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-page-data-schema.py"
122
134
  }
123
135
  ]
124
136
  },
@@ -181,6 +193,10 @@
181
193
  {
182
194
  "type": "command",
183
195
  "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/update-ui-showcase.py"
196
+ },
197
+ {
198
+ "type": "command",
199
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-a11y-audit.py"
184
200
  }
185
201
  ]
186
202
  }
@@ -237,7 +237,7 @@ export function HeroHeader({ title, description, badge }: HeroHeaderProps) {
237
237
  {/* Animated Grid Canvas */}
238
238
  <canvas
239
239
  ref={canvasRef}
240
- className="pointer-events-none absolute inset-0 z-0 opacity-60 blur-[1.5px]"
240
+ className="pointer-events-none absolute inset-0 z-0 opacity-80 dark:opacity-60 dark:blur-[0.5px]"
241
241
  />
242
242
 
243
243
  {/* Content - Uses same container as main content for alignment */}