southpaw 0.2.7

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 (169) hide show
  1. package/README.md +51 -0
  2. package/api/client.ts +83 -0
  3. package/api/helpers/fs.ts +98 -0
  4. package/api/helpers/stream.ts +6 -0
  5. package/api/index.ts +50 -0
  6. package/api/server.ts +566 -0
  7. package/constants/fonts.ts +1583 -0
  8. package/constants/icons.ts +2124 -0
  9. package/constants/langs.ts +2592 -0
  10. package/constants/ssr-loading.tsx +17 -0
  11. package/contexts/dev.tsx +124 -0
  12. package/hooks/useDebounceCb.ts +21 -0
  13. package/modules/app.tsx +102 -0
  14. package/modules/behaviors.ts +161 -0
  15. package/modules/component.tsx +483 -0
  16. package/modules/config/index.ts +8 -0
  17. package/modules/database.ts +25 -0
  18. package/modules/document.tsx +23 -0
  19. package/modules/error.tsx +16 -0
  20. package/modules/page/autozoom.tsx +92 -0
  21. package/modules/page/browser.tsx +166 -0
  22. package/modules/page/index.tsx +327 -0
  23. package/modules/page/insert-modal.tsx +225 -0
  24. package/modules/page/inspect/index.tsx +237 -0
  25. package/modules/page/inspect/wrapper.tsx +88 -0
  26. package/modules/page/knobs/action.tsx +58 -0
  27. package/modules/page/knobs/color.tsx +78 -0
  28. package/modules/page/knobs/helpers.ts +71 -0
  29. package/modules/page/knobs/icon.tsx +85 -0
  30. package/modules/page/knobs/linker.tsx +188 -0
  31. package/modules/page/knobs/list-json.tsx +149 -0
  32. package/modules/page/knobs/list-text.tsx +129 -0
  33. package/modules/page/knobs/number.tsx +84 -0
  34. package/modules/page/knobs/segmented.tsx +81 -0
  35. package/modules/page/knobs/select.tsx +89 -0
  36. package/modules/page/knobs/size.tsx +106 -0
  37. package/modules/page/knobs/switch.tsx +75 -0
  38. package/modules/page/knobs/text.tsx +93 -0
  39. package/modules/page/left-tabs.tsx +36 -0
  40. package/modules/page/right-tabs.tsx +31 -0
  41. package/modules/page/seo.tsx +69 -0
  42. package/modules/page/tree.tsx +860 -0
  43. package/modules/state.ts +787 -0
  44. package/modules/store.ts +37 -0
  45. package/modules/theme.ts +105 -0
  46. package/modules/translations.ts +15 -0
  47. package/package.json +68 -0
  48. package/scripts/bin.js +607 -0
  49. package/scripts/test.js +145 -0
  50. package/themes/base.tsx +8962 -0
  51. package/themes/index.ts +4 -0
  52. package/themes/m2.ts +39 -0
  53. package/themes/newspaper.ts +39 -0
  54. package/types/api.ts +57 -0
  55. package/types/fiber_node.ts +69 -0
  56. package/types/react_element.ts +14 -0
  57. package/ui/accordion.tsx +70 -0
  58. package/ui/actionicon.tsx +41 -0
  59. package/ui/affix.tsx +14 -0
  60. package/ui/alert.tsx +37 -0
  61. package/ui/anchor.tsx +52 -0
  62. package/ui/arrow.tsx +116 -0
  63. package/ui/aspectratio.tsx +21 -0
  64. package/ui/autocomplete.tsx +34 -0
  65. package/ui/avatar.tsx +36 -0
  66. package/ui/backgroundimage.tsx +16 -0
  67. package/ui/badge.tsx +28 -0
  68. package/ui/blockquote.tsx +15 -0
  69. package/ui/blocks/faq.tsx +145 -0
  70. package/ui/blocks/features.tsx +167 -0
  71. package/ui/blocks/footer.tsx +182 -0
  72. package/ui/blocks/herobanner.tsx +391 -0
  73. package/ui/blocks/index.ts +44 -0
  74. package/ui/blocks/leftnav.tsx +141 -0
  75. package/ui/blocks/pricing.tsx +210 -0
  76. package/ui/blocks/testimonials.tsx +149 -0
  77. package/ui/blocks/uidocs.tsx +1429 -0
  78. package/ui/box.tsx +27 -0
  79. package/ui/breadcrumbs.tsx +33 -0
  80. package/ui/burger.tsx +23 -0
  81. package/ui/button.tsx +85 -0
  82. package/ui/card.tsx +28 -0
  83. package/ui/center.tsx +26 -0
  84. package/ui/checkbox.tsx +35 -0
  85. package/ui/chip.tsx +32 -0
  86. package/ui/closebutton.tsx +15 -0
  87. package/ui/code.tsx +104 -0
  88. package/ui/collapse.tsx +16 -0
  89. package/ui/colorinput.tsx +32 -0
  90. package/ui/colorpicker.tsx +26 -0
  91. package/ui/colorswatch.tsx +17 -0
  92. package/ui/combobox.tsx +80 -0
  93. package/ui/container.tsx +27 -0
  94. package/ui/dialog.tsx +15 -0
  95. package/ui/divider.tsx +58 -0
  96. package/ui/drawer.tsx +28 -0
  97. package/ui/fieldset.tsx +15 -0
  98. package/ui/filebutton.tsx +23 -0
  99. package/ui/fileinput.tsx +36 -0
  100. package/ui/flex.tsx +139 -0
  101. package/ui/focustrap.tsx +15 -0
  102. package/ui/form.tsx +26 -0
  103. package/ui/grid.tsx +69 -0
  104. package/ui/highlight.tsx +15 -0
  105. package/ui/hoverable.tsx +66 -0
  106. package/ui/hovercard.tsx +45 -0
  107. package/ui/icon.tsx +76 -0
  108. package/ui/image.tsx +32 -0
  109. package/ui/index.ts +1355 -0
  110. package/ui/indicator.tsx +42 -0
  111. package/ui/infobox.tsx +200 -0
  112. package/ui/input.tsx +18 -0
  113. package/ui/inviewport.tsx +23 -0
  114. package/ui/jsoninput.tsx +35 -0
  115. package/ui/kbd.tsx +20 -0
  116. package/ui/list.tsx +33 -0
  117. package/ui/loader.tsx +26 -0
  118. package/ui/loadingoverlay.tsx +22 -0
  119. package/ui/mark.tsx +15 -0
  120. package/ui/menu.tsx +89 -0
  121. package/ui/modal.tsx +27 -0
  122. package/ui/multiselect.tsx +42 -0
  123. package/ui/nativeselect.tsx +17 -0
  124. package/ui/navlink.tsx +40 -0
  125. package/ui/notification.tsx +25 -0
  126. package/ui/numberformatter.tsx +15 -0
  127. package/ui/numberinput.tsx +37 -0
  128. package/ui/overlay.tsx +15 -0
  129. package/ui/pagination.tsx +21 -0
  130. package/ui/paper.tsx +33 -0
  131. package/ui/passwordinput.tsx +36 -0
  132. package/ui/pill.tsx +17 -0
  133. package/ui/pillsinput.tsx +37 -0
  134. package/ui/pininput.tsx +30 -0
  135. package/ui/popover.tsx +43 -0
  136. package/ui/portal.tsx +15 -0
  137. package/ui/progress.tsx +49 -0
  138. package/ui/props.ts +91 -0
  139. package/ui/propsDef.ts +38 -0
  140. package/ui/radio.tsx +30 -0
  141. package/ui/rating.tsx +25 -0
  142. package/ui/ringprogress.tsx +17 -0
  143. package/ui/scrollarea.tsx +27 -0
  144. package/ui/segmentedcontrol.tsx +38 -0
  145. package/ui/select.tsx +43 -0
  146. package/ui/simplegrid.tsx +23 -0
  147. package/ui/skeleton.tsx +15 -0
  148. package/ui/slider.tsx +31 -0
  149. package/ui/space.tsx +19 -0
  150. package/ui/spoiler.tsx +23 -0
  151. package/ui/stepper.tsx +63 -0
  152. package/ui/switch.tsx +29 -0
  153. package/ui/table.tsx +93 -0
  154. package/ui/tabs.tsx +80 -0
  155. package/ui/tagsinput.tsx +19 -0
  156. package/ui/terminal.tsx +44 -0
  157. package/ui/text.tsx +168 -0
  158. package/ui/textarea.tsx +36 -0
  159. package/ui/textinput.tsx +34 -0
  160. package/ui/timeline.tsx +30 -0
  161. package/ui/title.tsx +42 -0
  162. package/ui/tooltip.tsx +32 -0
  163. package/ui/transition.tsx +21 -0
  164. package/ui/tree.tsx +42 -0
  165. package/ui/typographystylesprovider.tsx +20 -0
  166. package/ui/unstyledbutton.tsx +18 -0
  167. package/ui/video.tsx +16 -0
  168. package/ui/visuallyhidden.tsx +15 -0
  169. package/utils/index.ts +76 -0
package/scripts/bin.js ADDED
@@ -0,0 +1,607 @@
1
+ #!/usr/bin/env node
2
+ import { input, select, confirm, checkbox } from "@inquirer/prompts";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import shell from "shelljs";
6
+ import { testRoutes } from "./test.js";
7
+
8
+ const model = "phi4";
9
+ const createProject = async () => {
10
+ const name = await input({
11
+ message: "Enter your project name",
12
+ required: true,
13
+ validate: (input) => {
14
+ if (input.length < 3) {
15
+ return "Project name must be at least 3 characters long";
16
+ }
17
+ if (!/^[a-z0-9_-]+$/i.test(input)) {
18
+ return "Project name must only contain letters, numbers, underscores, or hyphens";
19
+ }
20
+ if (input !== input.toLowerCase()) {
21
+ return "Project name must be lowercase";
22
+ }
23
+ return true;
24
+ },
25
+ });
26
+
27
+ const template = await select({
28
+ message: "Select a template",
29
+ choices: [
30
+ {
31
+ name: "Blank",
32
+ value: "blank",
33
+ description: "This is a blank template",
34
+ },
35
+ {
36
+ name: "Portfolio",
37
+ value: "portfolio",
38
+ description: "This is a portfolio template",
39
+ },
40
+ {
41
+ name: "Todolist",
42
+ value: "todolist",
43
+ description: "This is a todolist template",
44
+ },
45
+ ],
46
+ });
47
+
48
+ const ok = await confirm({ message: "Confirm?" });
49
+ if (ok) {
50
+ console.log(template);
51
+ shell.exec(
52
+ `git clone https://github.com/solidsnail/southpaw-templates-${template} ${name}`
53
+ );
54
+ console.log("✔ Project created successfully!");
55
+ console.log(`1. cd to ${name}`);
56
+ console.log(`2. npm i`);
57
+ console.log(`3. npm run dev`);
58
+ } else {
59
+ console.log("✘ Project creation cancelled!");
60
+ process.exit(1);
61
+ }
62
+ };
63
+
64
+ const generateBlock = async () => {
65
+ const name = await input({
66
+ message: "Enter your block name",
67
+ required: true,
68
+ validate: (input) => {
69
+ if (input.length < 3) {
70
+ return "Block name must be at least 3 characters long";
71
+ }
72
+ if (!/^[a-z0-9_-]+$/i.test(input)) {
73
+ return "Block name must only contain letters, numbers, underscores, or hyphens";
74
+ }
75
+ // Check if block already exists
76
+ if (fs.existsSync(`.southpaw/ui/blocks/${input}.tsx`)) {
77
+ return `Block '${input}' already exists. Choose a different name.`;
78
+ }
79
+ return true;
80
+ },
81
+ });
82
+
83
+ const description = await input({
84
+ message: "Enter your block description",
85
+ required: true,
86
+ validate: (input) => {
87
+ if (input.length < 10) {
88
+ return "Description must be at least 10 characters long for better generation quality";
89
+ }
90
+ return true;
91
+ },
92
+ });
93
+
94
+ const blockType = await select({
95
+ message: "What type of block are you creating?",
96
+ choices: [
97
+ {
98
+ name: "UI Component",
99
+ value: "ui",
100
+ description: "Interactive UI element (buttons, forms, cards)",
101
+ },
102
+ {
103
+ name: "Layout",
104
+ value: "layout",
105
+ description: "Layout component (navigation, hero, footer)",
106
+ },
107
+ {
108
+ name: "Data Display",
109
+ value: "data",
110
+ description: "Data visualization or list component",
111
+ },
112
+ {
113
+ name: "Form",
114
+ value: "form",
115
+ description: "Form or input-related component",
116
+ },
117
+ {
118
+ name: "Content",
119
+ value: "content",
120
+ description: "Content display component",
121
+ },
122
+ ],
123
+ });
124
+
125
+ const features = await checkbox({
126
+ message: "What features should this block have?",
127
+ choices: [
128
+ { name: "Interactive state", value: "state" },
129
+ { name: "Props configuration", value: "props" },
130
+ { name: "Click handlers", value: "handlers" },
131
+ { name: "Form inputs", value: "forms" },
132
+ { name: "Styling options", value: "styling" },
133
+ { name: "Animation/transitions", value: "animations" },
134
+ { name: "Responsive design", value: "responsive" },
135
+ ],
136
+ });
137
+
138
+ const complexity = await select({
139
+ message: "Component complexity level?",
140
+ choices: [
141
+ {
142
+ name: "Simple",
143
+ value: "simple",
144
+ description: "Basic component with minimal features",
145
+ },
146
+ {
147
+ name: "Medium",
148
+ value: "medium",
149
+ description: "Moderate complexity with some interactivity",
150
+ },
151
+ {
152
+ name: "Complex",
153
+ value: "complex",
154
+ description: "Advanced component with rich features",
155
+ },
156
+ ],
157
+ });
158
+
159
+ console.log("\n📋 Generation Summary:");
160
+ console.log(` Name: ${name}`);
161
+ console.log(` Type: ${blockType}`);
162
+ console.log(` Features: ${features.join(", ") || "None"}`);
163
+ console.log(` Complexity: ${complexity}`);
164
+ console.log(` Description: ${description}\n`);
165
+
166
+ const ok = await confirm({ message: "Generate this block?" });
167
+
168
+ if (ok) {
169
+ console.log("🤖 Generating block with AI...\n");
170
+
171
+ try {
172
+ // Read example files more safely
173
+ const exampleFiles = [
174
+ "herobanner1.tsx",
175
+ "leftnav.tsx",
176
+ "uidocs.tsx",
177
+ "counter.tsx",
178
+ "todolist.tsx",
179
+ ].filter((file) => fs.existsSync(`.southpaw/ui/blocks/${file}`));
180
+
181
+ if (exampleFiles.length === 0) {
182
+ console.error("❌ No example blocks found in .southpaw/ui/blocks/");
183
+ process.exit(1);
184
+ }
185
+
186
+ // Build examples string
187
+ let examplesContent = "";
188
+ for (const file of exampleFiles) {
189
+ try {
190
+ const content = fs.readFileSync(
191
+ `.southpaw/ui/blocks/${file}`,
192
+ "utf-8"
193
+ );
194
+ examplesContent += `//.southpaw/ui/blocks/${file}\n${content}\n\n`;
195
+ } catch {
196
+ console.warn(`⚠️ Could not read ${file}, skipping...`);
197
+ }
198
+ }
199
+
200
+ // Read supporting files
201
+ let uiIndexContent = "";
202
+ let componentModuleContent = "";
203
+
204
+ try {
205
+ uiIndexContent = fs.readFileSync(".southpaw/ui/index.ts", "utf-8");
206
+ } catch {
207
+ console.warn("⚠️ Could not read .southpaw/ui/index.ts");
208
+ }
209
+
210
+ try {
211
+ componentModuleContent = fs.readFileSync(
212
+ ".southpaw/modules/component.tsx",
213
+ "utf-8"
214
+ );
215
+ } catch {
216
+ console.warn("⚠️ Could not read .southpaw/modules/component.tsx");
217
+ }
218
+
219
+ const SYSTEM = `You are an expert Southpaw framework developer (a superset of Next.js). Your task is to generate high-quality, functional blocks based on user requirements.
220
+
221
+ FRAMEWORK KNOWLEDGE:
222
+ ${examplesContent}
223
+
224
+ //.southpaw/ui/index.ts
225
+ ${uiIndexContent}
226
+
227
+ //.southpaw/modules/component.tsx
228
+ ${componentModuleContent}
229
+
230
+ GENERATION RULES:
231
+ 1. ONLY return the TypeScript/TSX code for the component
232
+ 2. DO NOT wrap the code in backticks, markdown fences, or any code blocks
233
+ 3. DO NOT include any explanatory text before or after the code
234
+ 4. Start directly with imports and end with the export default statement
235
+ 5. Follow the exact pattern from the examples
236
+ 6. Use create_component() function with proper structure
237
+ 7. Include proper TypeScript types
238
+ 8. Use UI components from the UI object
239
+ 9. Implement proper props with propsDef for editor integration
240
+ 10. Add appropriate state management if needed
241
+ 11. Include proper methods for interactivity
242
+ 12. Follow React/Next.js best practices
243
+ 13. Ensure the component is self-contained and functional
244
+
245
+ CRITICAL: Your response should start with "import" and end with "export default Component;" with NO additional text or formatting.
246
+
247
+ COMPONENT REQUIREMENTS:
248
+ - Block Type: ${blockType}
249
+ - Features: ${features.join(", ") || "basic functionality"}
250
+ - Complexity: ${complexity}
251
+ - Must be production-ready and follow existing patterns
252
+ - Include proper prop definitions for the visual editor
253
+ - Use semantic and accessible HTML structure
254
+ - Follow the naming convention from examples
255
+
256
+ QUALITY CHECKLIST:
257
+ ✓ Proper TypeScript types
258
+ ✓ Complete propsDef object for editor
259
+ ✓ Functional render method
260
+ ✓ Appropriate default props
261
+ ✓ Clean, readable code structure
262
+ ✓ Follows Southpaw patterns exactly`;
263
+
264
+ const userPrompt = `Generate: //.southpaw/ui/blocks/${name}.tsx
265
+
266
+ Requirements:
267
+ - Component name: ${name}
268
+ - Description: ${description}
269
+ - Type: ${blockType}
270
+ - Features needed: ${features.join(", ") || "basic"}
271
+ - Complexity level: ${complexity}
272
+
273
+ Create a complete, functional Southpaw block component following the established patterns.`;
274
+
275
+ const messages = [
276
+ { role: "system", content: SYSTEM },
277
+ { role: "user", content: userPrompt },
278
+ ];
279
+
280
+ let generatedCode = "";
281
+
282
+ const { default: ollama } = await import("ollama");
283
+ const response = await ollama.chat({
284
+ model,
285
+ messages,
286
+ stream: true,
287
+ think: false,
288
+ options: {
289
+ temperature: 0.05,
290
+ num_ctx: 32000,
291
+ top_p: 0.9,
292
+ repeat_penalty: 1.1,
293
+ },
294
+ });
295
+
296
+ process.stdout.write("📝 ");
297
+ for await (const part of response) {
298
+ const content = part.message.content;
299
+ process.stdout.write(content);
300
+ generatedCode += content;
301
+ }
302
+
303
+ console.log("\n");
304
+
305
+ // Clean the generated code
306
+ generatedCode = cleanGeneratedCode(generatedCode);
307
+
308
+ // Validate the generated code
309
+ if (!validateGeneratedCode(generatedCode)) {
310
+ console.error("❌ Generated code validation failed. Please try again.");
311
+ process.exit(1);
312
+ }
313
+
314
+ // Save the generated block
315
+ const blockPath = `.southpaw/ui/blocks/${name}.tsx`;
316
+
317
+ try {
318
+ // Ensure directory exists
319
+ const dir = path.dirname(blockPath);
320
+ if (!fs.existsSync(dir)) {
321
+ fs.mkdirSync(dir, { recursive: true });
322
+ }
323
+
324
+ // Write the file
325
+ fs.writeFileSync(blockPath, generatedCode.trim(), "utf-8");
326
+ console.log(`✅ Block saved to: ${blockPath}`);
327
+
328
+ // Update the blocks index file
329
+ await updateBlocksIndex(name);
330
+
331
+ console.log("🎉 Block generation completed successfully!");
332
+ console.log("\nNext steps:");
333
+ console.log(`1. Review the generated code in ${blockPath}`);
334
+ console.log(`2. Test the component in your application`);
335
+ console.log(`3. Customize props and styling as needed`);
336
+ } catch (writeError) {
337
+ console.error("❌ Error saving block:", writeError.message);
338
+ process.exit(1);
339
+ }
340
+ } catch (error) {
341
+ console.error("❌ Error generating block:", error.message);
342
+
343
+ if (error.message.includes("model")) {
344
+ console.log("\n💡 Troubleshooting:");
345
+ console.log("• Make sure Ollama is running: `ollama serve`");
346
+ console.log(`• Check if ${model} model is installed: \`ollama list\``);
347
+ console.log(`• Install ${model} if needed: \`ollama pull ${model}\``);
348
+ }
349
+
350
+ process.exit(1);
351
+ }
352
+ } else {
353
+ console.log("❌ Block generation cancelled!");
354
+ process.exit(1);
355
+ }
356
+ };
357
+
358
+ const cleanGeneratedCode = (code) => {
359
+ // Remove markdown code fences
360
+ let cleaned = code.replace(/^```(?:tsx?|typescript|javascript)?\s*/gim, "");
361
+ cleaned = cleaned.replace(/```\s*$/gim, "");
362
+
363
+ // Remove any leading/trailing whitespace
364
+ cleaned = cleaned.trim();
365
+
366
+ // Remove any extra explanatory text that might be at the beginning
367
+ const lines = cleaned.split("\n");
368
+ let startIndex = 0;
369
+
370
+ // Find the first line that looks like an import or the component start
371
+ for (let i = 0; i < lines.length; i++) {
372
+ const line = lines[i].trim();
373
+ if (
374
+ line.startsWith("import ") ||
375
+ line.startsWith("type ") ||
376
+ line.startsWith("interface ") ||
377
+ line.includes("create_component")
378
+ ) {
379
+ startIndex = i;
380
+ break;
381
+ }
382
+ }
383
+
384
+ // Remove any trailing explanatory text after the last export
385
+ let endIndex = lines.length - 1;
386
+ for (let i = lines.length - 1; i >= 0; i--) {
387
+ const line = lines[i].trim();
388
+ if (line.startsWith("export default") || line === "});") {
389
+ endIndex = i;
390
+ break;
391
+ }
392
+ }
393
+
394
+ cleaned = lines.slice(startIndex, endIndex + 1).join("\n");
395
+
396
+ // Ensure proper formatting
397
+ cleaned = cleaned.trim();
398
+
399
+ return cleaned;
400
+ };
401
+
402
+ const validateGeneratedCode = (code) => {
403
+ // Basic validation checks
404
+ const checks = [
405
+ {
406
+ test: () => code.includes("create_component"),
407
+ message: "Missing create_component function call",
408
+ },
409
+ {
410
+ test: () => code.includes("export default"),
411
+ message: "Missing export default statement",
412
+ },
413
+ {
414
+ test: () => code.includes("render()"),
415
+ message: "Missing render method",
416
+ },
417
+ {
418
+ test: () => code.includes("import") && code.includes("from"),
419
+ message: "Missing proper imports",
420
+ },
421
+ {
422
+ test: () => code.length > 100,
423
+ message: "Generated code is too short",
424
+ },
425
+ {
426
+ test: () => !code.includes("```"),
427
+ message: "Code still contains markdown fences",
428
+ },
429
+ ];
430
+
431
+ for (const check of checks) {
432
+ if (!check.test()) {
433
+ console.error(`❌ Validation failed: ${check.message}`);
434
+ return false;
435
+ }
436
+ }
437
+
438
+ console.log("✅ Generated code validation passed");
439
+ return true;
440
+ };
441
+
442
+ const updateBlocksIndex = async (blockName) => {
443
+ const indexPath = ".southpaw/ui/blocks/index.ts";
444
+
445
+ try {
446
+ let indexContent = "";
447
+
448
+ // Read existing content if file exists
449
+ if (fs.existsSync(indexPath)) {
450
+ indexContent = fs.readFileSync(indexPath, "utf-8");
451
+ } else {
452
+ // Create initial structure
453
+ indexContent = `import dynamic from "next/dynamic";\n\nexport const Blocks = {\n};\n`;
454
+ }
455
+
456
+ // Check if block is already in the index
457
+ if (indexContent.includes(`${blockName}:`)) {
458
+ console.log(`ℹ️ Block '${blockName}' already exists in index`);
459
+ return;
460
+ }
461
+
462
+ // Add the new block to the Blocks object
463
+ const blockEntry = ` ${blockName}: (() => {
464
+ const Comp = dynamic(() => import("./${blockName}"));
465
+ Comp.displayName = "Blocks.${blockName}";
466
+ return Comp;
467
+ })(),`;
468
+
469
+ // Insert before the closing brace
470
+ const updatedContent = indexContent.replace(
471
+ /(\s*)(};?\s*)$/,
472
+ `$1${blockEntry}\n$1$2`
473
+ );
474
+
475
+ fs.writeFileSync(indexPath, updatedContent, "utf-8");
476
+ console.log(`✅ Updated blocks index: ${indexPath}`);
477
+ } catch (error) {
478
+ console.warn(`⚠️ Could not update blocks index: ${error.message}`);
479
+ }
480
+ };
481
+
482
+ const generateBehavior = async () => {
483
+ const name = await input({
484
+ message: "Enter behavior name (e.g. count, todoList)",
485
+ required: true,
486
+ validate: (value) =>
487
+ /^[a-zA-Z0-9_-]+$/.test(value) ||
488
+ "Name must contain only letters, numbers, dashes and underscores",
489
+ });
490
+
491
+ const ok = await confirm({ message: `Create src/behaviors/${name}.ts?` });
492
+ if (!ok) {
493
+ console.log("❌ Behavior generation cancelled!");
494
+ process.exit(1);
495
+ }
496
+
497
+ const filePath = path.join("src", "behaviors", `${name}.ts`);
498
+ if (fs.existsSync(filePath)) {
499
+ console.log(`❌ ${filePath} already exists`);
500
+ process.exit(1);
501
+ }
502
+
503
+ const template = `import { create_behaviors } from "southpaw/modules/behaviors";
504
+
505
+ export default create_behaviors(
506
+ {
507
+ state: {
508
+ // TODO: define initial state
509
+ },
510
+ actions: {
511
+ // TODO: define actions that mutate state and return Promise.resolve()
512
+ },
513
+ },
514
+ {
515
+ when: {
516
+ // "Do something"(arg: string) { return this.store.actions.doSomething(arg); },
517
+ },
518
+ then: {
519
+ // "Some derived value"() { return this.store.state.value; },
520
+ },
521
+ given: {
522
+ // "Some precondition"() { this.store.state = { ... }; return Promise.resolve(); },
523
+ },
524
+ }
525
+ );
526
+ `;
527
+
528
+ fs.writeFileSync(filePath, template, "utf-8");
529
+ console.log(`✅ Behavior created at ${filePath}`);
530
+ };
531
+
532
+ const generatePage = async () => {
533
+ const name = await input({
534
+ message: "Enter page name (without extension)",
535
+ required: true,
536
+ validate: (value) =>
537
+ /^[a-zA-Z0-9_-]+$/.test(value) ||
538
+ "Name must contain only letters, numbers, dashes and underscores",
539
+ });
540
+
541
+ const routePath = await input({
542
+ message: "Enter route path (e.g. /demo, /dashboard)",
543
+ required: true,
544
+ });
545
+
546
+ const ok = await confirm({
547
+ message: `Create src/pages/${name}.tsx for route ${routePath}?`,
548
+ });
549
+ if (!ok) {
550
+ console.log("❌ Page generation cancelled!");
551
+ process.exit(1);
552
+ }
553
+
554
+ const filePath = path.join("src", "pages", `${name}.tsx`);
555
+ if (fs.existsSync(filePath)) {
556
+ console.log(`❌ ${filePath} already exists`);
557
+ process.exit(1);
558
+ }
559
+
560
+ const template = `import { create_page } from "southpaw/modules/page";
561
+ import { UI } from "southpaw/ui";
562
+
563
+ export default create_page({
564
+ name: "${name}",
565
+ description: "${name} page",
566
+ seo: {
567
+ title: "${name}",
568
+ description: "${name} page",
569
+ },
570
+ render() {
571
+ return (
572
+ <UI.flex direction="column" h="100%">
573
+ <Components.topbar page={this} />
574
+ <UI.divider />
575
+ <UI.container py="lg" size="lg">
576
+ <UI.title order={1} text="${name}" />
577
+ <UI.text c="dimmed" text="${routePath}" />
578
+ </UI.container>
579
+ </UI.flex>
580
+ );
581
+ },
582
+ });
583
+ `;
584
+
585
+ fs.writeFileSync(filePath, template, "utf-8");
586
+ console.log(`✅ Page created at ${filePath}`);
587
+ };
588
+
589
+ const [, , mode] = process.argv;
590
+
591
+ switch (mode) {
592
+ case "test":
593
+ await testRoutes();
594
+ break;
595
+ case "generate-block":
596
+ await generateBlock();
597
+ break;
598
+ case "generate-behavior":
599
+ await generateBehavior();
600
+ break;
601
+ case "generate-page":
602
+ await generatePage();
603
+ break;
604
+ default:
605
+ await createProject();
606
+ break;
607
+ }