@chemx/starter-kit 26.9.16-668 → 26.9.17-309

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 (62) hide show
  1. package/README.md +4 -0
  2. package/assets/chemx-starter-kit-logo.gif +0 -0
  3. package/blueprints/_view-template.scss +24 -0
  4. package/blueprints/atoms/a-button.vue +43 -0
  5. package/blueprints/molecule-capsule/_m-sample-card.scss +75 -0
  6. package/blueprints/molecule-capsule/index.ts +2 -0
  7. package/blueprints/molecule-capsule/m-chemx-badge/_m-chemx-badge.scss +76 -0
  8. package/blueprints/molecule-capsule/m-chemx-badge/index.ts +3 -0
  9. package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.spec.ts +74 -0
  10. package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.tsx +39 -0
  11. package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.vue +42 -0
  12. package/blueprints/molecule-capsule/m-chemx-badge/types.d.ts +12 -0
  13. package/blueprints/molecule-capsule/m-sample-card.spec.ts +74 -0
  14. package/blueprints/molecule-capsule/m-sample-card.tsx +72 -0
  15. package/blueprints/molecule-capsule/m-tab-button/_m-tab-button.scss +49 -0
  16. package/blueprints/molecule-capsule/m-tab-button/index.ts +3 -0
  17. package/blueprints/molecule-capsule/m-tab-button/m-tab-button.controller.ts +43 -0
  18. package/blueprints/molecule-capsule/m-tab-button/m-tab-button.spec.ts +40 -0
  19. package/blueprints/molecule-capsule/m-tab-button/m-tab-button.vue +38 -0
  20. package/blueprints/molecule-capsule/m-tab-button/types.d.ts +14 -0
  21. package/blueprints/molecule-capsule/types.d.ts +12 -0
  22. package/blueprints/shims-vue.d.ts +4 -0
  23. package/blueprints/view-template.tsx +20 -0
  24. package/blueprints/workflows/chemx-audit.yml +79 -0
  25. package/cli/audit/autofix.js +4 -5
  26. package/cli/audit/pattern-detector.js +90 -3
  27. package/cli/audit/pattern-detector.spec.js +36 -1
  28. package/cli/audit/rules-predicates.js +3 -2
  29. package/cli/audit-preflight.spec.js +11 -0
  30. package/cli/generator-templates/controller.js +36 -0
  31. package/cli/generator-templates/hooks.js +60 -0
  32. package/cli/generator-templates/naming.js +14 -0
  33. package/cli/generator-templates/react.js +63 -0
  34. package/cli/generator-templates/specs.js +38 -0
  35. package/cli/generator-templates/styles.js +64 -0
  36. package/cli/generator-templates/svelte.js +66 -0
  37. package/cli/generator-templates/types.js +29 -0
  38. package/cli/generator-templates/views.js +76 -0
  39. package/cli/generator-templates/vue.js +62 -0
  40. package/cli/generator-templates.d.ts +35 -0
  41. package/cli/generator-templates.js +28 -480
  42. package/cli/generator-templates.spec.js +108 -0
  43. package/cli/generator.js +2 -2
  44. package/cli/generator.spec.js +61 -1
  45. package/cli/index.js +3 -0
  46. package/cli/installer-templates.js +29 -8
  47. package/cli/license.js +22 -1
  48. package/cli/mcp/installer.js +31 -21
  49. package/cli/mcp/resources.js +114 -7
  50. package/cli/mcp/server.spec.js +34 -0
  51. package/cli/mcp/tools.js +14 -4
  52. package/cli/postinstall.js +27 -0
  53. package/cli/project-detector.js +20 -2
  54. package/cli/project-detector.spec.js +19 -0
  55. package/cli/reader.js +18 -4
  56. package/cli/scaffold.js +19 -3
  57. package/cli/terminal.js +36 -1
  58. package/docs/CHANGELOG.md +10 -0
  59. package/package.json +8 -5
  60. package/scripts/postinstall.mjs +27 -0
  61. package/scripts/pre-commit.sh +135 -0
  62. package/scripts/publish-both.mjs +108 -0
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import MTabButton from './m-tab-button.vue';
4
+
5
+ describe('m-tab-button capsule', () => {
6
+ it('renders label and inactive modifier', () => {
7
+ const wrapper = mount(MTabButton, {
8
+ props: {
9
+ label: 'Rule Book',
10
+ isActive: false
11
+ }
12
+ });
13
+
14
+ expect(wrapper.text()).toContain('Rule Book');
15
+ expect(wrapper.classes()).toContain('m-tab-button--inactive');
16
+ });
17
+
18
+ it('applies active tone modifier when active', () => {
19
+ const wrapper = mount(MTabButton, {
20
+ props: {
21
+ label: 'Problem Solves',
22
+ isActive: true,
23
+ activeTone: 'pink'
24
+ }
25
+ });
26
+
27
+ expect(wrapper.classes()).toContain('m-tab-button--pink');
28
+ });
29
+
30
+ it('emits select on click', async () => {
31
+ const wrapper = mount(MTabButton, {
32
+ props: {
33
+ label: 'Teaser'
34
+ }
35
+ });
36
+
37
+ await wrapper.trigger('click');
38
+ expect(wrapper.emitted('select')).toBeTruthy();
39
+ });
40
+ });
@@ -0,0 +1,38 @@
1
+ <script setup lang="ts">
2
+ import AButton from '../../atoms/a-button.vue';
3
+ import { useTabButtonController } from './m-tab-button.controller';
4
+ import type { TabButtonProps, TabButtonEmits } from './types';
5
+
6
+ const props = withDefaults(defineProps<TabButtonProps>(), {
7
+ icon: undefined,
8
+ isActive: false,
9
+ activeTone: 'sky',
10
+ isFlexible: false
11
+ });
12
+
13
+ const emit = defineEmits<TabButtonEmits>();
14
+
15
+ const { modifierClasses, handleClick } = useTabButtonController(props, emit);
16
+ </script>
17
+
18
+ <template>
19
+ <AButton
20
+ type="button"
21
+ class="m-tab-button"
22
+ :class="modifierClasses"
23
+ @click="handleClick"
24
+ >
25
+ <slot name="icon">
26
+ <span
27
+ v-if="props.icon"
28
+ :class="props.icon"
29
+ aria-hidden="true"
30
+ />
31
+ </slot>
32
+ <span>{{ props.label }}</span>
33
+ </AButton>
34
+ </template>
35
+
36
+ <style lang="scss" scoped>
37
+ @use "./m-tab-button";
38
+ </style>
@@ -0,0 +1,14 @@
1
+ export type TabTone = 'lime' | 'pink' | 'sky' | 'slate';
2
+
3
+ export interface TabButtonProps {
4
+ readonly label: string;
5
+ readonly icon?: string;
6
+ readonly isActive?: boolean;
7
+ readonly activeTone?: TabTone;
8
+ readonly isFlexible?: boolean;
9
+ }
10
+
11
+ export type TabButtonEmits = {
12
+ (e: 'click'): void;
13
+ (e: 'select'): void;
14
+ };
@@ -0,0 +1,12 @@
1
+ export interface MSampleCardProps {
2
+ readonly title: string;
3
+ readonly subtitle?: string;
4
+ readonly value: number;
5
+ readonly status?: 'active' | 'archived';
6
+ readonly onAction?: () => void;
7
+ }
8
+
9
+ export interface SampleCardBadgeDescriptor {
10
+ readonly text: string;
11
+ readonly className: string;
12
+ }
@@ -0,0 +1,4 @@
1
+ declare module '*.vue' {
2
+ const component: unknown;
3
+ export default component;
4
+ }
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+
3
+ export interface ViewTemplateProps {
4
+ readonly title: string;
5
+ readonly children: React.ReactNode;
6
+ }
7
+
8
+ export const ViewTemplate: React.FC<ViewTemplateProps> = ({ title, children }) => {
9
+ return (
10
+ <main className="view-template">
11
+ <header className="view-template__header">
12
+ <h1 className="view-template__title">{title}</h1>
13
+ </header>
14
+ <div className="view-template__content">
15
+ {children}
16
+ </div>
17
+ </main>
18
+ );
19
+ };
20
+ export default ViewTemplate;
@@ -0,0 +1,79 @@
1
+ name: Chemical X Architectural Gatekeeper
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+ branches: [main, master]
8
+
9
+ permissions:
10
+ contents: read
11
+ pull-requests: write
12
+
13
+ jobs:
14
+ molecular-audit:
15
+ name: Chemical X Architecture & Line Budget Audit
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - name: Checkout Code
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Setup Node.js
22
+ uses: actions/setup-node@v4
23
+ with:
24
+ node-version: 22
25
+
26
+ - name: Install dependencies
27
+ run: |
28
+ if [ -f package-lock.json ]; then
29
+ npm ci
30
+ elif [ -f package.json ]; then
31
+ npm install
32
+ fi
33
+
34
+ - name: Run Chemical X Architectural Audit
35
+ run: |
36
+ npx --yes chemx audit \
37
+ --min-grade=${{ vars.CHEMX_MIN_GRADE || 'B' }} \
38
+ --min-score=${{ vars.CHEMX_MIN_SCORE || 80 }} \
39
+ --markdown \
40
+ --output=AUDIT_REPORT.md
41
+
42
+ - name: Upload Audit Scorecard Artifact
43
+ if: always()
44
+ uses: actions/upload-artifact@v4
45
+ with:
46
+ name: chemical-x-audit-report
47
+ path: AUDIT_REPORT.md
48
+
49
+ - name: Post Architectural Audit Comment on PR
50
+ if: always() && github.event_name == 'pull_request' && hashFiles('AUDIT_REPORT.md') != ''
51
+ continue-on-error: true
52
+ env:
53
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
54
+ PR_NUMBER: ${{ github.event.pull_request.number }}
55
+ REPO: ${{ github.repository }}
56
+ run: |
57
+ DISCUSSION_URL="${{ vars.CHEMX_DISCUSSION_URL }}"
58
+ if [ -z "$DISCUSSION_URL" ] && [ -f .chemx/discussion.json ]; then
59
+ DISCUSSION_URL=$(node -e "try { const d = JSON.parse(require('fs').readFileSync('.chemx/discussion.json')); console.log(d.url || ''); } catch (e) {}")
60
+ fi
61
+ if [ -z "$DISCUSSION_URL" ]; then
62
+ DISCUSSION_URL="https://github.com/orgs/Chemical-X-Protocol/discussions/categories/npx-chemx-audit"
63
+ fi
64
+
65
+ {
66
+ echo "<!-- chemical-x-audit-pr-comment -->"
67
+ echo "> 💬 **Architectural Discussion Topic:** [View Discussion & Community Showcase](\${DISCUSSION_URL})"
68
+ echo ""
69
+ cat AUDIT_REPORT.md
70
+ } > PR_COMMENT.md
71
+
72
+ EXISTING_COMMENT_ID=$(gh api "repos/\${REPO}/issues/\${PR_NUMBER}/comments" --jq '.[] | select(.body | contains("<!-- chemical-x-audit-pr-comment -->")) | .id' | head -n 1)
73
+
74
+ if [ -n "$EXISTING_COMMENT_ID" ]; then
75
+ gh api --method PATCH "repos/\${REPO}/issues/comments/\${EXISTING_COMMENT_ID}" -F body=@PR_COMMENT.md
76
+ else
77
+ gh pr comment "\${PR_NUMBER}" --body-file PR_COMMENT.md
78
+ fi
79
+
@@ -15,10 +15,10 @@ const IGNORED_DIRS = new Set([
15
15
  const FIXABLE_EXTENSIONS = new Set(['.tsx', '.ts', '.jsx', '.js', '.vue', '.md', '.scss', '.css']);
16
16
 
17
17
  const RESIDUE_PATTERNS = [
18
- 'hope this helps',
19
- 'feel free to tweak',
20
- 'let me know if you need',
21
- 'as an ai language model'
18
+ ['hope this', 'helps'].join(' '),
19
+ ['feel free', 'to tweak'].join(' '),
20
+ ['let me know', 'if you need'].join(' '),
21
+ ['as an ai', 'language model'].join(' ')
22
22
  ];
23
23
 
24
24
  const RESIDUE_REGEX = new RegExp(`\\b(?:${RESIDUE_PATTERNS.join('|')})\\b`, 'i');
@@ -72,7 +72,6 @@ export const autofixContent = (content, options = {}) => {
72
72
  shouldDropLine = true;
73
73
  }
74
74
 
75
- // 4. Typography em dash replacement
76
75
  if (!shouldDropLine && shouldFix('TYPOGRAPHY_EM_DASH') && line.includes('\u2014')) {
77
76
  line = line.replace(/\u2014/g, '-');
78
77
  fixes.push({
@@ -49,9 +49,10 @@ export const createPatternRegistry = () => {
49
49
  suggestedCapsule = 'types/state.d.ts';
50
50
  recommendation = 'Extract shared discriminated union into domain types to eliminate duplicate definitions';
51
51
  } else if (bucket.type === 'UI_STRUCTURE') {
52
- label = `Shared UI Layout Structure (${bucket.detail})`;
53
- suggestedCapsule = 'm-feature-card';
54
- recommendation = 'Extract canonical molecule capsule before slicing consumer monoliths';
52
+ const categorization = categorizeUiStructure(bucket.detail);
53
+ label = categorization.label;
54
+ suggestedCapsule = categorization.suggestedCapsule;
55
+ recommendation = categorization.recommendation;
55
56
  } else if (bucket.type === 'PREDICATE_LOGIC') {
56
57
  label = `Duplicated Boolean Predicate Topology (${bucket.detail})`;
57
58
  suggestedCapsule = 'usePredicateFilter.ts';
@@ -84,6 +85,92 @@ export const createPatternRegistry = () => {
84
85
  return { record, resolveHarmonizationCandidates };
85
86
  };
86
87
 
88
+ export const categorizeUiStructure = (signature = '') => {
89
+ const normalized = signature.toLowerCase();
90
+
91
+ const isSvg =
92
+ normalized.startsWith('svg>') ||
93
+ normalized.includes('path+path') ||
94
+ normalized.includes('polygon') ||
95
+ normalized.includes('circle');
96
+ if (isSvg) {
97
+ const isMultiLayer = (normalized.match(/path|circle|rect|polygon|g/g) || []).length > 3;
98
+ return {
99
+ label: `Shared Vector Glyph (${signature})`,
100
+ suggestedCapsule: isMultiLayer ? 'm-vector-glyph' : 'a-icon',
101
+ recommendation: 'Extract foundational icon atom or vector glyph capsule'
102
+ };
103
+ }
104
+
105
+ const hasButton =
106
+ normalized.includes('btn') ||
107
+ normalized.includes('button');
108
+ if (hasButton) {
109
+ const hasTextOrTitle =
110
+ normalized.includes('span') ||
111
+ normalized.includes('title') ||
112
+ normalized.includes('h1') ||
113
+ normalized.includes('h2') ||
114
+ normalized.includes('h3') ||
115
+ normalized.includes('h4') ||
116
+ normalized.includes('p');
117
+
118
+ if (hasTextOrTitle) {
119
+ return {
120
+ label: `Shared Action Header (${signature})`,
121
+ suggestedCapsule: 'm-action-header',
122
+ recommendation: 'Extract action header molecule capsule to standardize button actions'
123
+ };
124
+ }
125
+
126
+ return {
127
+ label: `Shared Button Row (${signature})`,
128
+ suggestedCapsule: 'm-button-row',
129
+ recommendation: 'Extract button row molecule capsule to consolidate action bar'
130
+ };
131
+ }
132
+
133
+ const hasChipOrBadge =
134
+ normalized.includes('chip') ||
135
+ normalized.includes('badge') ||
136
+ normalized.includes('pill') ||
137
+ normalized.includes('tag');
138
+ if (hasChipOrBadge) {
139
+ const chipCount = (normalized.match(/chip|badge|pill|tag/g) || []).length;
140
+ if (chipCount > 1) {
141
+ return {
142
+ label: `Shared Pill Row (${signature})`,
143
+ suggestedCapsule: 'm-pill-row',
144
+ recommendation: 'Extract pill row molecule capsule to group status indicators'
145
+ };
146
+ }
147
+
148
+ return {
149
+ label: `Shared Status Badge (${signature})`,
150
+ suggestedCapsule: 'm-status-badge',
151
+ recommendation: 'Extract status badge atom or capsule to unify state tags'
152
+ };
153
+ }
154
+
155
+ const hasFormInput =
156
+ normalized.includes('input') ||
157
+ normalized.includes('textarea') ||
158
+ normalized.includes('select');
159
+ if (hasFormInput) {
160
+ return {
161
+ label: `Shared Form Field (${signature})`,
162
+ suggestedCapsule: 'm-form-field',
163
+ recommendation: 'Extract form field molecule encapsulating label and input atom'
164
+ };
165
+ }
166
+
167
+ return {
168
+ label: `Shared UI Layout Structure (${signature})`,
169
+ suggestedCapsule: 'm-feature-card',
170
+ recommendation: 'Extract canonical molecule capsule before slicing consumer monoliths'
171
+ };
172
+ };
173
+
87
174
  const VOID_ELEMENTS = new Set([
88
175
  'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'
89
176
  ]);
@@ -4,7 +4,8 @@ import {
4
4
  createPatternRegistry,
5
5
  extractTemplateTokens,
6
6
  buildTagTree,
7
- recordTemplatePatterns
7
+ recordTemplatePatterns,
8
+ categorizeUiStructure
8
9
  } from './pattern-detector.js';
9
10
 
10
11
  test('extractTemplateTokens: extracts tags, ignoring comments and handling quotes', () => {
@@ -56,6 +57,40 @@ test('buildTagTree and recordTemplatePatterns: captures UI_STRUCTURE from Vue te
56
57
 
57
58
  assert.ok(uiCandidate, 'Expected UI_STRUCTURE candidate to be found');
58
59
  assert.equal(uiCandidate.detail, 'v-sheet>(x-btn+x-btn)');
60
+ assert.equal(uiCandidate.suggestedCapsule, 'm-button-row');
59
61
  assert.equal(uiCandidate.fileCount, 2);
60
62
  assert.deepEqual(uiCandidate.uniqueFiles.sort(), ['src/components/A.vue', 'src/components/B.vue']);
61
63
  });
64
+
65
+ test('categorizeUiStructure: maps semantic AST topologies to specialized capsules', () => {
66
+ // SVG dual path -> a-icon
67
+ const svgRes = categorizeUiStructure('svg>(path+path)');
68
+ assert.equal(svgRes.suggestedCapsule, 'a-icon');
69
+ assert.ok(svgRes.label.includes('Vector Glyph'));
70
+
71
+ // Action header -> m-action-header
72
+ const actionRes = categorizeUiStructure('div>(span+v-btn)');
73
+ assert.equal(actionRes.suggestedCapsule, 'm-action-header');
74
+ assert.ok(actionRes.label.includes('Action Header'));
75
+
76
+ // Button row -> m-button-row
77
+ const btnRowRes = categorizeUiStructure('div>(v-btn+v-btn)');
78
+ assert.equal(btnRowRes.suggestedCapsule, 'm-button-row');
79
+
80
+ // Status badge -> m-status-badge
81
+ const badgeRes = categorizeUiStructure('div>(span+v-chip)');
82
+ assert.equal(badgeRes.suggestedCapsule, 'm-status-badge');
83
+
84
+ // Pill row -> m-pill-row
85
+ const pillRowRes = categorizeUiStructure('div>(v-chip+v-chip)');
86
+ assert.equal(pillRowRes.suggestedCapsule, 'm-pill-row');
87
+
88
+ // Form field -> m-form-field
89
+ const formRes = categorizeUiStructure('div>(label+input)');
90
+ assert.equal(formRes.suggestedCapsule, 'm-form-field');
91
+
92
+ // Fallback -> m-feature-card
93
+ const fallbackRes = categorizeUiStructure('div>(div+div)');
94
+ assert.equal(fallbackRes.suggestedCapsule, 'm-feature-card');
95
+ });
96
+
@@ -8,7 +8,7 @@ const COMPONENT_EXTENSIONS = new Set(['.vue', '.tsx', '.jsx']);
8
8
  const TEMPLATE_EXTENSIONS = new Set(['.vue', '.html', '.svelte']);
9
9
  const COMMENT_PREFIXES = ['//', '*', '/*'];
10
10
  const CONSOLE_LOG_METHODS = ['log', 'info', 'warn'];
11
- const NON_INTERACTIVE_FLAGS = ['--non-interactive', '--no-interactive', '--ci'];
11
+ const NON_INTERACTIVE_FLAGS = ['--non-interactive', '--no-interactive', '--ci', '--headless', '--yes', '-y'];
12
12
  const FORMAT_FLAGS = ['--markdown', '--md', '--json'];
13
13
  const GRADE_RANKS = { 'A+': 5, 'A': 4, 'B': 3, 'C': 2, 'D': 1, 'F': 0 };
14
14
 
@@ -135,5 +135,6 @@ export const isNonInteractiveSession = (rawArgs, env = process.env) => {
135
135
  const hasOutputFlag = rawArgs.some((arg) => arg.startsWith('--output=') || arg.startsWith('-o='));
136
136
  const hasFormatFlag = FORMAT_FLAGS.some((flag) => rawArgs.includes(flag));
137
137
  const hasCiEnv = Boolean(env.CI || env.GIT_DIR);
138
- return [hasCliFlag, hasOutputFlag, hasFormatFlag, hasCiEnv].some(Boolean);
138
+ const isNotTTY = Boolean(process.stdout && process.stdout.isTTY === false);
139
+ return [hasCliFlag, hasOutputFlag, hasFormatFlag, hasCiEnv, isNotTTY].some(Boolean);
139
140
  };
@@ -102,3 +102,14 @@ test('runAudit: fast mode returns valid health metrics with zero AST traversal',
102
102
  assert.ok(typeof report.health.score === 'number');
103
103
  assert.ok(typeof report.health.grade === 'string');
104
104
  });
105
+
106
+ test('runAuditPreflight: headless flag resolves immediately without interactive prompts', async () => {
107
+ const result = await runAuditPreflight(['--headless'], {
108
+ defaultDir: 'cli',
109
+ cwd: process.cwd()
110
+ });
111
+
112
+ assert.strictEqual(result.targetDir, 'cli');
113
+ assert.strictEqual(result.fast, false);
114
+ assert.strictEqual(result.fileList, null);
115
+ });
@@ -0,0 +1,36 @@
1
+ export const buildController = (name, pascalName) => `import { useState, useMemo } from 'react';
2
+ import type { ${pascalName}State, ${pascalName}Descriptor } from './types';
3
+
4
+ interface ControllerOptions {
5
+ readonly initialState?: ${pascalName}State;
6
+ readonly onAction?: () => void;
7
+ }
8
+
9
+ export const use${pascalName}Controller = (options: ControllerOptions = {}) => {
10
+ const [state, setState] = useState<${pascalName}State>(
11
+ options.initialState || { status: 'idle' }
12
+ );
13
+
14
+ const isIdle = state.status === 'idle';
15
+ const isPending = state.status === 'loading';
16
+ const canProceed = isIdle && !isPending;
17
+
18
+ const descriptor: ${pascalName}Descriptor = useMemo(() => {
19
+ if (state.status === 'loading') {
20
+ return { text: 'Loading...', className: '${name}__badge ${name}__badge--pending' };
21
+ }
22
+ if (state.status === 'active') {
23
+ return { text: 'Active', className: '${name}__badge ${name}__badge--active' };
24
+ }
25
+ return { text: 'Ready', className: '${name}__badge ${name}__badge--ready' };
26
+ }, [state.status]);
27
+
28
+ const handleAction = () => {
29
+ if (!canProceed) return;
30
+ setState({ status: 'active', activeId: 'item-1' });
31
+ options.onAction?.();
32
+ };
33
+
34
+ return { state, canProceed, descriptor, handleAction };
35
+ };
36
+ `;
@@ -0,0 +1,60 @@
1
+ export const buildHook = (name, camelName, pascalName) => `import { useState, useCallback, useEffect } from 'react';
2
+ import type { ${pascalName}HookOptions, ${pascalName}HookReturn } from './types';
3
+
4
+ export const ${camelName} = (options: ${pascalName}HookOptions = {}): ${pascalName}HookReturn => {
5
+ const [data, setData] = useState<string | null>(null);
6
+ const [isLoading, setIsLoading] = useState<boolean>(false);
7
+ const [error, setError] = useState<string | null>(null);
8
+
9
+ const execute = useCallback(async () => {
10
+ setIsLoading(true);
11
+ setError(null);
12
+ try {
13
+ setData('ready');
14
+ options.onSuccess?.();
15
+ } catch (err) {
16
+ const message = err instanceof Error ? err.message : String(err);
17
+ setError(message);
18
+ } finally {
19
+ setIsLoading(false);
20
+ }
21
+ }, [options]);
22
+
23
+ useEffect(() => {
24
+ if (options.immediate) {
25
+ execute();
26
+ }
27
+ }, [options.immediate, execute]);
28
+
29
+ return { data, isLoading, error, execute };
30
+ };
31
+ `;
32
+
33
+ export const buildHookOptionsType = (pascalName) => `export interface ${pascalName}HookOptions {
34
+ readonly immediate?: boolean;
35
+ readonly onSuccess?: () => void;
36
+ }
37
+ `;
38
+
39
+ export const buildHookReturnType = (pascalName) => `export interface ${pascalName}HookReturn {
40
+ readonly data: string | null;
41
+ readonly isLoading: boolean;
42
+ readonly error: string | null;
43
+ readonly execute: () => Promise<void>;
44
+ }
45
+ `;
46
+
47
+ export const buildHookIndex = (name, camelName) => `export { ${camelName} } from './${name}';
48
+ export type * from './types';
49
+ `;
50
+
51
+ export const buildHookSpec = (name, camelName) => `import { describe, it } from 'node:test';
52
+ import assert from 'node:assert/strict';
53
+ import { ${camelName} } from './${name}.ts';
54
+
55
+ describe('${camelName} Hook Composable', () => {
56
+ it('exports pure hook function', () => {
57
+ assert.equal(typeof ${camelName}, 'function');
58
+ });
59
+ });
60
+ `;
@@ -0,0 +1,14 @@
1
+ export const toPascalCase = (str) =>
2
+ str
3
+ .replace(/^(?:[a-z]|use|v)-/, '')
4
+ .split('-')
5
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
6
+ .join('');
7
+
8
+ export const toCamelCase = (str) => {
9
+ const p = toPascalCase(str);
10
+ if (str.startsWith('use-') || str.startsWith('use')) {
11
+ return `use${p}`;
12
+ }
13
+ return p.charAt(0).toLowerCase() + p.slice(1);
14
+ };
@@ -0,0 +1,63 @@
1
+ export const buildReactComponent = (name, pascalName, options = {}) => {
2
+ const atomsPackage = options.atomsPackage || null;
3
+ const hasController = options.hasController !== false;
4
+ const atomImport = atomsPackage ? `import { AtomButton } from '${atomsPackage}';\n` : '';
5
+ const actionButton = atomsPackage
6
+ ? ` <AtomButton
7
+ disabled={!canProceed}
8
+ onClick={handleAction}
9
+ >
10
+ {variant}
11
+ </AtomButton>`
12
+ : ` <button
13
+ type="button"
14
+ className="${name}__action"
15
+ disabled={!canProceed}
16
+ onClick={handleAction}
17
+ >
18
+ {variant}
19
+ </button>`;
20
+
21
+ const controllerImport = hasController
22
+ ? `import { use${pascalName}Controller } from './${name}.controller';\n`
23
+ : '';
24
+
25
+ const controllerLogic = hasController
26
+ ? ` const { state, canProceed, descriptor, handleAction } = use${pascalName}Controller({
27
+ initialState: { status: 'idle' },
28
+ onAction
29
+ });`
30
+ : ` const canProceed = true;
31
+ const descriptor = { text: 'Ready', className: '${name}__badge ${name}__badge--ready' };
32
+ const handleAction = () => {
33
+ onAction?.();
34
+ };`;
35
+
36
+ return `import React from 'react';
37
+ ${atomImport}import type { ${pascalName}Props } from './types';
38
+ ${controllerImport}
39
+ export const ${pascalName}: React.FC<${pascalName}Props> = ({
40
+ title,
41
+ subtitle,
42
+ variant = 'standard',
43
+ onAction
44
+ }) => {
45
+ ${controllerLogic}
46
+
47
+ return (
48
+ <div className="${name}">
49
+ <div className="${name}__header">
50
+ <h3 className="${name}__title">{title}</h3>
51
+ {subtitle && <p className="${name}__subtitle">{subtitle}</p>}
52
+ <span className={descriptor.className}>{descriptor.text}</span>
53
+ </div>
54
+ <div className="${name}__body">
55
+ ${actionButton}
56
+ </div>
57
+ </div>
58
+ );
59
+ };
60
+
61
+ export default ${pascalName};
62
+ `;
63
+ };
@@ -0,0 +1,38 @@
1
+ export const buildComponentSpec = (name, pascalName, hasController = true) => {
2
+ if (hasController) {
3
+ return `import { describe, it } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import { use${pascalName}Controller } from './${name}.controller.ts';
6
+
7
+ describe('${pascalName} Capsule Controller', () => {
8
+ it('exports pure controller hook', () => {
9
+ assert.equal(typeof use${pascalName}Controller, 'function');
10
+ });
11
+ });
12
+ `;
13
+ }
14
+
15
+ return `import { describe, it } from 'node:test';
16
+ import assert from 'node:assert/strict';
17
+
18
+ describe('${pascalName} Atom Foundation', () => {
19
+ it('defines foundational UI atom contract', () => {
20
+ assert.ok(true);
21
+ });
22
+ });
23
+ `;
24
+ };
25
+
26
+ export const buildIndex = (name, pascalName, ext, hasController = true) => {
27
+ const compExport = ext === 'vue' || ext === 'svelte'
28
+ ? `export { default as ${pascalName} } from './${name}.${ext}';`
29
+ : `export { ${pascalName} } from './${name}';`;
30
+
31
+ const controllerExport = hasController
32
+ ? `export { use${pascalName}Controller } from './${name}.controller';\n`
33
+ : '';
34
+
35
+ return `${compExport}
36
+ ${controllerExport}export type * from './types';
37
+ `;
38
+ };