@dhayalesh/create-ktern-component 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js ADDED
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const inquirer = require('inquirer');
6
+ const ejs = require('ejs');
7
+ const { promisify } = require('util');
8
+
9
+ const copyFile = promisify(fs.copyFile);
10
+ const mkdir = promisify(fs.mkdir);
11
+ const readdir = promisify(fs.readdir);
12
+ const stat = promisify(fs.stat);
13
+ const writeFile = promisify(fs.writeFile);
14
+ const readFile = promisify(fs.readFile);
15
+
16
+ async function copyTemplate(src, dest, data) {
17
+ const stats = await stat(src);
18
+ if (stats.isDirectory()) {
19
+ if (!fs.existsSync(dest)) {
20
+ await mkdir(dest, { recursive: true });
21
+ }
22
+ const files = await readdir(src);
23
+ for (const file of files) {
24
+ await copyTemplate(path.join(src, file), path.join(dest, file), data);
25
+ }
26
+ } else {
27
+ let content = await readFile(src, 'utf8');
28
+ // Simple template replacement if no EJS variables are found or if we want to use EJS
29
+ try {
30
+ content = ejs.render(content, data);
31
+ } catch (err) {
32
+ // If EJS fails (e.g. invalid syntax in non-template files), just use raw content
33
+ }
34
+
35
+ // Remove .template or .tmpl suffix if present in destination filename
36
+ const finalDest = dest.replace(/\.template$/, '').replace(/\.tmpl$/, '');
37
+ await writeFile(finalDest, content);
38
+ }
39
+ }
40
+
41
+ async function run() {
42
+ console.log('šŸš€ Welcome to the KTERN Generator!');
43
+
44
+ const questions = [
45
+ {
46
+ type: 'input',
47
+ name: 'name',
48
+ message: 'What is the project name?',
49
+ validate: (input) => input.length > 0 || 'Project name is required',
50
+ },
51
+ {
52
+ type: 'input',
53
+ name: 'directory',
54
+ message: 'In which directory should the project be created?',
55
+ default: (answers) => path.join(process.cwd(), answers.name),
56
+ },
57
+ {
58
+ type: 'number',
59
+ name: 'port',
60
+ message: 'What port should the service run on?',
61
+ default: 3000,
62
+ },
63
+ ];
64
+
65
+ const answers = await inquirer.prompt(questions);
66
+ const targetDir = path.resolve(answers.directory);
67
+
68
+ if (fs.existsSync(targetDir)) {
69
+ const { overwrite } = await inquirer.prompt([
70
+ {
71
+ type: 'confirm',
72
+ name: 'overwrite',
73
+ message: `Directory ${targetDir} already exists. Overwrite?`,
74
+ default: false,
75
+ },
76
+ ]);
77
+ if (!overwrite) {
78
+ console.log('Exiting...');
79
+ process.exit(0);
80
+ }
81
+ }
82
+
83
+ console.log(`\nCreating project "${answers.name}" in ${targetDir}...`);
84
+
85
+ const templateDir = path.join(__dirname, '../templates');
86
+ await copyTemplate(templateDir, targetDir, {
87
+ name: answers.name,
88
+ port: answers.port,
89
+ // Add more default variables if needed
90
+ });
91
+
92
+ console.log('\nāœ… Project created successfully!');
93
+ console.log(`\nNext steps:`);
94
+ console.log(` cd ${targetDir}`);
95
+ console.log(` npm install`);
96
+ console.log(` npm start`);
97
+ }
98
+
99
+ run().catch((err) => {
100
+ console.error('āŒ Error:', err.message);
101
+ process.exit(1);
102
+ });
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@dhayalesh/create-ktern-component",
3
+ "version": "1.0.0",
4
+ "description": "Standalone KTERN component generator",
5
+ "bin": {
6
+ "create-ktern-component": "./bin/cli.js"
7
+ },
8
+ "dependencies": {
9
+ "inquirer": "^8.2.4",
10
+ "ejs": "^3.1.8"
11
+ },
12
+ "publishConfig": {
13
+ "access": "restricted"
14
+ }
15
+ }
@@ -0,0 +1,58 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { <%= className %> } from './<%= className %>';
3
+
4
+ describe('<%= className %>', () => {
5
+ it('should render successfully', () => {
6
+ render(<<%= className %>>Test</<%= className %>>);
7
+ expect(screen.getByText('Test')).toBeInTheDocument();
8
+ });
9
+
10
+ it('should apply the correct variant class', () => {
11
+ const { rerender } = render(<<%= className %> variant="primary">Test</<%= className %>>);
12
+ const button = screen.getByText('Test');
13
+ expect(button).toHaveClass('bg-blue-600');
14
+
15
+ rerender(<<%= className %> variant="secondary">Test</<%= className %>>);
16
+ expect(button).toHaveClass('bg-purple-600');
17
+
18
+ rerender(<<%= className %> variant="accent">Test</<%= className %>>);
19
+ expect(button).toHaveClass('bg-green-600');
20
+ });
21
+
22
+ it('should apply the correct size class', () => {
23
+ const { rerender } = render(<<%= className %> size="sm">Test</<%= className %>>);
24
+ const button = screen.getByText('Test');
25
+ expect(button).toHaveClass('px-3');
26
+
27
+ rerender(<<%= className %> size="md">Test</<%= className %>>);
28
+ expect(button).toHaveClass('px-4');
29
+
30
+ rerender(<<%= className %> size="lg">Test</<%= className %>>);
31
+ expect(button).toHaveClass('px-6');
32
+ });
33
+
34
+ it('should handle click events', () => {
35
+ const handleClick = vi.fn();
36
+ render(<<%= className %> onClick={handleClick}>Test</<%= className %>>);
37
+
38
+ const button = screen.getByText('Test');
39
+ fireEvent.click(button);
40
+
41
+ expect(handleClick).toHaveBeenCalledTimes(1);
42
+ });
43
+
44
+ it('should be disabled when disabled prop is true', () => {
45
+ render(<<%= className %> disabled>Test</<%= className %>>);
46
+ const button = screen.getByText('Test');
47
+
48
+ expect(button).toBeDisabled();
49
+ expect(button).toHaveClass('opacity-50');
50
+ });
51
+
52
+ it('should apply custom className', () => {
53
+ render(<<%= className %> className="custom-class">Test</<%= className %>>);
54
+ const button = screen.getByText('Test');
55
+
56
+ expect(button).toHaveClass('custom-class');
57
+ });
58
+ });
@@ -0,0 +1,142 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { <%= className %> } from './<%= className %>';
3
+
4
+ /**
5
+ * <%= className %> Component Stories
6
+ *
7
+ * Demonstrates the component in various states and configurations.
8
+ * This follows the Separation of Concerns principle - stories are separate from implementation.
9
+ */
10
+ const meta: Meta<typeof <%= className %>> = {
11
+ title: 'Components/<%= className %>',
12
+ component: <%= className %>,
13
+ tags: ['autodocs'],
14
+ argTypes: {
15
+ variant: {
16
+ control: 'select',
17
+ options: ['primary', 'secondary', 'accent'],
18
+ description: 'Visual variant of the component',
19
+ },
20
+ size: {
21
+ control: 'select',
22
+ options: ['sm', 'md', 'lg'],
23
+ description: 'Size of the component',
24
+ },
25
+ disabled: {
26
+ control: 'boolean',
27
+ description: 'Whether the component is disabled',
28
+ },
29
+ onClick: {
30
+ action: 'clicked',
31
+ description: 'Click event handler',
32
+ },
33
+ },
34
+ };
35
+
36
+ export default meta;
37
+ type Story = StoryObj<typeof <%= className %>>;
38
+
39
+ /**
40
+ * Default state of the component
41
+ */
42
+ export const Default: Story = {
43
+ args: {
44
+ children: 'Click me',
45
+ variant: 'primary',
46
+ size: 'md',
47
+ disabled: false,
48
+ },
49
+ };
50
+
51
+ /**
52
+ * Primary variant
53
+ */
54
+ export const Primary: Story = {
55
+ args: {
56
+ children: 'Primary Button',
57
+ variant: 'primary',
58
+ size: 'md',
59
+ },
60
+ };
61
+
62
+ /**
63
+ * Secondary variant
64
+ */
65
+ export const Secondary: Story = {
66
+ args: {
67
+ children: 'Secondary Button',
68
+ variant: 'secondary',
69
+ size: 'md',
70
+ },
71
+ };
72
+
73
+ /**
74
+ * Accent variant
75
+ */
76
+ export const Accent: Story = {
77
+ args: {
78
+ children: 'Accent Button',
79
+ variant: 'accent',
80
+ size: 'md',
81
+ },
82
+ };
83
+
84
+ /**
85
+ * Small size
86
+ */
87
+ export const Small: Story = {
88
+ args: {
89
+ children: 'Small Button',
90
+ variant: 'primary',
91
+ size: 'sm',
92
+ },
93
+ };
94
+
95
+ /**
96
+ * Large size
97
+ */
98
+ export const Large: Story = {
99
+ args: {
100
+ children: 'Large Button',
101
+ variant: 'primary',
102
+ size: 'lg',
103
+ },
104
+ };
105
+
106
+ /**
107
+ * Disabled state
108
+ */
109
+ export const Disabled: Story = {
110
+ args: {
111
+ children: 'Disabled Button',
112
+ variant: 'primary',
113
+ size: 'md',
114
+ disabled: true,
115
+ },
116
+ };
117
+
118
+ /**
119
+ * All variants side by side
120
+ */
121
+ export const AllVariants: Story = {
122
+ render: () => (
123
+ <div className="flex gap-4">
124
+ <<%= className %> variant="primary">Primary</<%= className %>>
125
+ <<%= className %> variant="secondary">Secondary</<%= className %>>
126
+ <<%= className %> variant="accent">Accent</<%= className %>>
127
+ </div>
128
+ ),
129
+ };
130
+
131
+ /**
132
+ * All sizes side by side
133
+ */
134
+ export const AllSizes: Story = {
135
+ render: () => (
136
+ <div className="flex gap-4 items-center">
137
+ <<%= className %> size="sm">Small</<%= className %>>
138
+ <<%= className %> size="md">Medium</<%= className %>>
139
+ <<%= className %> size="lg">Large</<%= className %>>
140
+ </div>
141
+ ),
142
+ };
@@ -0,0 +1,98 @@
1
+ import React from 'react';
2
+
3
+ /**
4
+ * Props interface for <%= className %> component
5
+ * Following Inversion of Control principle - component behavior is configured via props
6
+ */
7
+ export interface <%= className %>Props {
8
+ /**
9
+ * The content to display inside the component
10
+ */
11
+ children?: React.ReactNode;
12
+
13
+ /**
14
+ * Optional CSS class name for custom styling
15
+ */
16
+ className?: string;
17
+
18
+ /**
19
+ * Variant of the component
20
+ */
21
+ variant?: 'primary' | 'secondary' | 'accent';
22
+
23
+ /**
24
+ * Size of the component
25
+ */
26
+ size?: 'sm' | 'md' | 'lg';
27
+
28
+ /**
29
+ * Whether the component is disabled
30
+ */
31
+ disabled?: boolean;
32
+
33
+ /**
34
+ * Click handler
35
+ */
36
+ onClick?: () => void;
37
+ }
38
+
39
+ /**
40
+ * <%= className %> Component
41
+ *
42
+ * A reusable UI component built with React and Tailwind CSS.
43
+ * Follows Clean Architecture principles:
44
+ * - Dependency Rule: Depends only on React and design tokens (CSS variables)
45
+ * - Separation of Concerns: Presentation logic separated from business logic
46
+ * - Inversion of Control: Behavior configured via props interface
47
+ */
48
+ export const <%= className %>: React.FC<<%= className %>Props> = ({
49
+ children,
50
+ className = '',
51
+ variant = 'primary',
52
+ size = 'md',
53
+ disabled = false,
54
+ onClick,
55
+ }) => {
56
+ // Variant styles - using Tailwind classes that reference design tokens
57
+ const variantClasses = {
58
+ primary: 'bg-blue-600 hover:bg-blue-700 text-white',
59
+ secondary: 'bg-purple-600 hover:bg-purple-700 text-white',
60
+ accent: 'bg-green-600 hover:bg-green-700 text-white',
61
+ };
62
+
63
+ // Size styles
64
+ const sizeClasses = {
65
+ sm: 'px-3 py-1.5 text-sm',
66
+ md: 'px-4 py-2 text-base',
67
+ lg: 'px-6 py-3 text-lg',
68
+ };
69
+
70
+ // Combine classes
71
+ const classes = [
72
+ 'rounded-lg',
73
+ 'font-medium',
74
+ 'transition-all',
75
+ 'duration-200',
76
+ 'focus:outline-none',
77
+ 'focus:ring-2',
78
+ 'focus:ring-offset-2',
79
+ 'focus:ring-offset-gray-900',
80
+ variantClasses[variant],
81
+ sizeClasses[size],
82
+ disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
83
+ className,
84
+ ].join(' ');
85
+
86
+ return (
87
+ <button
88
+ className={classes}
89
+ onClick={onClick}
90
+ disabled={disabled}
91
+ type="button"
92
+ >
93
+ {children}
94
+ </button>
95
+ );
96
+ };
97
+
98
+ export default <%= className %>;
@@ -0,0 +1,15 @@
1
+ # <%= className %>
2
+
3
+ This library was generated with [Nx](https://nx.dev).
4
+
5
+ ## Building
6
+
7
+ Run `nx build <%= projectName %>` to build the library.
8
+
9
+ ## Running unit tests
10
+
11
+ Run `nx test <%= projectName %>` to execute the unit tests via [Vitest](https://vitest.dev/).
12
+
13
+ ## Running Storybook
14
+
15
+ Run `nx storybook <%= projectName %>` to start the Storybook development server.
@@ -0,0 +1,17 @@
1
+ import baseConfig from '../../eslint.config.mjs';
2
+
3
+ export default [
4
+ ...baseConfig,
5
+ {
6
+ files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
7
+ rules: {},
8
+ },
9
+ {
10
+ files: ['**/*.ts', '**/*.tsx'],
11
+ rules: {},
12
+ },
13
+ {
14
+ files: ['**/*.js', '**/*.jsx'],
15
+ rules: {},
16
+ },
17
+ ];
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@ktern/<%= projectName %>",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "devDependencies": {
8
+ "@testing-library/react": "^14.0.0",
9
+ "@testing-library/jest-dom": "^6.1.5",
10
+ "@types/react": "^18.2.0",
11
+ "@types/react-dom": "^18.2.0",
12
+ "vitest": "^1.0.0",
13
+ "@vitest/ui": "^1.0.0",
14
+ "jsdom": "^23.0.0"
15
+ },
16
+ "peerDependencies": {
17
+ "react": "^18.2.0",
18
+ "react-dom": "^18.2.0"
19
+ }
20
+ }
@@ -0,0 +1 @@
1
+ export * from './components/<%= className %>/<%= className %>';
@@ -0,0 +1,50 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /* Design System Tokens */
6
+ :root {
7
+ /* Colors */
8
+ --color-primary: #3b82f6;
9
+ --color-secondary: #8b5cf6;
10
+ --color-accent: #10b981;
11
+ --color-background: #0a0a0a;
12
+ --color-surface: #1a1a1a;
13
+ --color-text: #ffffff;
14
+ --color-text-secondary: #a3a3a3;
15
+
16
+ /* Spacing */
17
+ --spacing-xs: 0.25rem;
18
+ --spacing-sm: 0.5rem;
19
+ --spacing-md: 1rem;
20
+ --spacing-lg: 1.5rem;
21
+ --spacing-xl: 2rem;
22
+
23
+ /* Border Radius */
24
+ --radius-sm: 0.25rem;
25
+ --radius-md: 0.5rem;
26
+ --radius-lg: 0.75rem;
27
+ --radius-xl: 1rem;
28
+
29
+ /* Shadows */
30
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
31
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
32
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
33
+ }
34
+
35
+ /* Base Styles */
36
+ * {
37
+ box-sizing: border-box;
38
+ }
39
+
40
+ body {
41
+ margin: 0;
42
+ padding: 0;
43
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
44
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
45
+ sans-serif;
46
+ -webkit-font-smoothing: antialiased;
47
+ -moz-osx-font-smoothing: grayscale;
48
+ background-color: var(--color-background);
49
+ color: var(--color-text);
50
+ }
@@ -0,0 +1,8 @@
1
+ import '@testing-library/jest-dom/vitest';
2
+ import { cleanup } from '@testing-library/react';
3
+ import { afterEach } from 'vitest';
4
+
5
+ // Cleanup after each test
6
+ afterEach(() => {
7
+ cleanup();
8
+ });
@@ -0,0 +1,25 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "esnext",
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "jsx": "react-jsx",
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "outDir": "../../dist/out-tsc",
10
+ "rootDir": "src",
11
+ "types": ["node", "vitest"],
12
+ "allowSyntheticDefaultImports": true,
13
+ "esModuleInterop": true
14
+ },
15
+ "files": [],
16
+ "include": [],
17
+ "references": [
18
+ {
19
+ "path": "./tsconfig.lib.json"
20
+ },
21
+ {
22
+ "path": "./tsconfig.spec.json"
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../dist/out-tsc",
5
+ "types": ["node"]
6
+ },
7
+ "include": [
8
+ "src/**/*.ts",
9
+ "src/**/*.tsx",
10
+ "src/**/*.js",
11
+ "src/**/*.jsx"
12
+ ],
13
+ "exclude": [
14
+ "src/**/*.spec.ts",
15
+ "src/**/*.test.ts",
16
+ "src/**/*.spec.tsx",
17
+ "src/**/*.test.tsx",
18
+ "src/**/*.spec.js",
19
+ "src/**/*.test.js",
20
+ "src/**/*.spec.jsx",
21
+ "src/**/*.test.jsx",
22
+ "**/*.stories.ts",
23
+ "**/*.stories.js",
24
+ "**/*.stories.jsx",
25
+ "**/*.stories.tsx"
26
+ ]
27
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../dist/out-tsc",
5
+ "types": ["vitest/globals", "vitest/importMeta", "node"]
6
+ },
7
+ "include": [
8
+ "src/**/*.test.ts",
9
+ "src/**/*.spec.ts",
10
+ "src/**/*.test.tsx",
11
+ "src/**/*.spec.tsx",
12
+ "src/**/*.test.js",
13
+ "src/**/*.spec.js",
14
+ "src/**/*.test.jsx",
15
+ "src/**/*.spec.jsx"
16
+ ]
17
+ }
@@ -0,0 +1,49 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
4
+ import dts from 'vite-plugin-dts';
5
+ import * as path from 'path';
6
+
7
+ export default defineConfig({
8
+ root: __dirname,
9
+ cacheDir: '../../node_modules/.vite/<%= projectName %>',
10
+
11
+ plugins: [
12
+ react(),
13
+ nxViteTsPaths(),
14
+ dts({
15
+ entryRoot: 'src',
16
+ tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
17
+ }),
18
+ ],
19
+
20
+ build: {
21
+ outDir: '../../dist/<%= projectRoot %>',
22
+ emptyOutDir: true,
23
+ reportCompressedSize: true,
24
+ commonjsOptions: {
25
+ transformMixedEsModules: true,
26
+ },
27
+ lib: {
28
+ entry: 'src/index.ts',
29
+ name: '<%= className %>',
30
+ fileName: 'index',
31
+ formats: ['es', 'cjs'],
32
+ },
33
+ rollupOptions: {
34
+ external: ['react', 'react-dom', 'react/jsx-runtime'],
35
+ },
36
+ },
37
+
38
+ test: {
39
+ globals: true,
40
+ environment: 'jsdom',
41
+ include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
42
+ setupFiles: ['src/test-setup.ts'],
43
+ reporters: ['default'],
44
+ coverage: {
45
+ reportsDirectory: '../../coverage/<%= projectRoot %>',
46
+ provider: 'v8',
47
+ },
48
+ },
49
+ });