@zvoove/unity-ui 2.22.1 → 2.23.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/README.md +8 -76
- package/bin/cli.mjs +49 -0
- package/bin/commands/config.mjs +68 -0
- package/bin/commands/create.mjs +163 -0
- package/bin/commands/init.mjs +158 -0
- package/bin/commands/rules.mjs +100 -0
- package/bin/commands/skills.mjs +1883 -0
- package/bin/generate-skills.mjs +19 -1903
- package/bin/templates/component.tsx +15 -0
- package/bin/templates/doc.mdx +36 -0
- package/bin/templates/index.ts +2 -0
- package/bin/templates/stories.tsx +15 -0
- package/bin/templates/styled.ts +14 -0
- package/bin/templates/test.tsx +30 -0
- package/bin/templates/types.ts +13 -0
- package/dist/llms.txt +150 -43
- package/dist/theme.css +44 -0
- package/dist/unity-ui.cjs.js +1 -1
- package/dist/unity-ui.css +1 -1
- package/dist/unity-ui.d.ts +575 -24
- package/dist/unity-ui.es.js +762 -389
- package/package.json +9 -6
package/README.md
CHANGED
|
@@ -29,29 +29,9 @@ The `unity-ui` is available as a public package on [npmjs](https://www.npmjs.com
|
|
|
29
29
|
npm i @zvoove/unity-ui
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
> **Note:** The GitHub Packages registry (`@zvoove/unity-ui`) is deprecated and should no longer be used. Use the public npm registry (`@zvoove/unity-ui`) instead.
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
The `unity-ui` is also available on [our private package registry on GitHub](https://github.com/orgs/zvoove-org/packages?repo_name=unity-ui). To install from GitHub Packages, you'll need to [setup a personal access token](https://github.com/settings/tokens) with the `read:packages` permission.
|
|
37
|
-
|
|
38
|
-
Configure the token for `npm`: `npm config set //npm.pkg.github.com/:_authToken $TOKEN`.
|
|
39
|
-
|
|
40
|
-
Then create a `.npmrc` file in the root of your project:
|
|
41
|
-
|
|
42
|
-
```shell
|
|
43
|
-
@zvoove-org:registry=https://npm.pkg.github.com/zvoove-org
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Then install:
|
|
47
|
-
|
|
48
|
-
```shell
|
|
49
|
-
npm i @zvoove-org/unity-ui
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
`@zvoove/unity-ui` (or `@zvoove-org/unity-ui`) has required peer dependencies for `react` and `react-dom`. If your project doesn't have them already, make sure you have installed using at least **v18**
|
|
34
|
+
`@zvoove/unity-ui` has required peer dependencies for `react` and `react-dom`. If your project doesn't have them already, make sure you have installed using at least **v18**
|
|
55
35
|
|
|
56
36
|
## Styles
|
|
57
37
|
|
|
@@ -62,8 +42,8 @@ In case you want to install `tailwindcss` in your project and have access to the
|
|
|
62
42
|
```css
|
|
63
43
|
/* index.css or main.css etc... */
|
|
64
44
|
|
|
65
|
-
@import '@zvoove
|
|
66
|
-
@import '@zvoove
|
|
45
|
+
@import '@zvoove/unity-ui/theme.css';
|
|
46
|
+
@import '@zvoove/unity-ui/unity-ui.css';
|
|
67
47
|
```
|
|
68
48
|
|
|
69
49
|
The `theme.css` file contains all the design tokens used in the project, so you can use them in your styles.
|
|
@@ -71,7 +51,7 @@ The `theme.css` file contains all the design tokens used in the project, so you
|
|
|
71
51
|
After that you can use the components in your project and you can also use the design tokens in your styles. (Check our [Design Tokens](https://main--67c03f013fea08bb2f926e5f.chromatic.com/?path=/docs/design-tokens--docs) section for more information)
|
|
72
52
|
|
|
73
53
|
```jsx
|
|
74
|
-
import { Button } from '@zvoove
|
|
54
|
+
import { Button } from '@zvoove/unity-ui';
|
|
75
55
|
|
|
76
56
|
const App = () => {
|
|
77
57
|
return (
|
|
@@ -89,7 +69,7 @@ export default App;
|
|
|
89
69
|
The `unity-ui` has a dark mode that can be enabled by adding the `dark` data attribute to the root element of your application.
|
|
90
70
|
|
|
91
71
|
```jsx
|
|
92
|
-
import { Button } from '@zvoove
|
|
72
|
+
import { Button } from '@zvoove/unity-ui';
|
|
93
73
|
|
|
94
74
|
const App = () => {
|
|
95
75
|
return (
|
|
@@ -113,62 +93,14 @@ export default Layout;
|
|
|
113
93
|
you can also force a specific component to be in dark mode by adding the `dark` data attribute to the component.
|
|
114
94
|
|
|
115
95
|
```jsx
|
|
116
|
-
import { Button } from '@zvoove
|
|
96
|
+
import { Button } from '@zvoove/unity-ui';
|
|
117
97
|
|
|
118
98
|
<Button data-theme="dark">Click me</Button>;
|
|
119
99
|
```
|
|
120
100
|
|
|
121
101
|
## Fonts
|
|
122
102
|
|
|
123
|
-
Our project uses the `Source Sans` font family
|
|
124
|
-
|
|
125
|
-
```html
|
|
126
|
-
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
127
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
128
|
-
<link
|
|
129
|
-
href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap"
|
|
130
|
-
rel="stylesheet"
|
|
131
|
-
/>
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
## Fonts in NextJS
|
|
135
|
-
|
|
136
|
-
For `NextJS` you can load the fonts using `next/font` like this:
|
|
137
|
-
|
|
138
|
-
```jsx
|
|
139
|
-
import './globals.css';
|
|
140
|
-
import { Source_Sans_3 } from 'next/font/google';
|
|
141
|
-
|
|
142
|
-
const sourceSans3 = Source_Sans_3({
|
|
143
|
-
subsets: ['latin'],
|
|
144
|
-
variable: '--font-source-sans-3',
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
export default async function RootLayout({
|
|
148
|
-
children,
|
|
149
|
-
}: Readonly<{
|
|
150
|
-
children: React.ReactNode;
|
|
151
|
-
}>) {
|
|
152
|
-
return (
|
|
153
|
-
<html lang="de-DE" className={sourceSans3.variable}>
|
|
154
|
-
<body data-theme="light">{children}</body>
|
|
155
|
-
</html>
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
And then you update your `globals.css` with:
|
|
161
|
-
|
|
162
|
-
```css
|
|
163
|
-
@import '@zvoove-org/unity-ui/theme.css';
|
|
164
|
-
@import '@zvoove-org/unity-ui/unity-ui.css';
|
|
165
|
-
|
|
166
|
-
@layer base {
|
|
167
|
-
html {
|
|
168
|
-
font-family: var(--font-source-sans-3);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
```
|
|
103
|
+
Our project uses the `Source Sans 3` font family. The font is bundled with the library via `@font-face` declarations in `theme.css`, loaded from our CDN. No additional setup is required — importing the theme CSS is enough.
|
|
172
104
|
|
|
173
105
|
## Installing (Development)
|
|
174
106
|
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unity UI — Unified CLI
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx unity-ui init Initialize unity-ui.config.mjs
|
|
8
|
+
* npx unity-ui skills [-o .claude] Generate Agent Skills
|
|
9
|
+
* npx unity-ui create <ComponentName> Scaffold a new component
|
|
10
|
+
* npx unity-ui rules [-o .] Generate AI rules (.cursorrules + CLAUDE.md)
|
|
11
|
+
* npx unity-ui --help Show all commands
|
|
12
|
+
*/
|
|
13
|
+
import { runCreate } from './commands/create.mjs';
|
|
14
|
+
import { runInit } from './commands/init.mjs';
|
|
15
|
+
import { runRules } from './commands/rules.mjs';
|
|
16
|
+
import { runSkills } from './commands/skills.mjs';
|
|
17
|
+
import { Command } from 'commander';
|
|
18
|
+
|
|
19
|
+
const program = new Command();
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.name('unity-ui')
|
|
23
|
+
.description('Unity UI Design System CLI')
|
|
24
|
+
.version('1.0.0');
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.command('init')
|
|
28
|
+
.description('Initialize unity-ui.config.mjs with project settings')
|
|
29
|
+
.action(() => runInit());
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.command('skills')
|
|
33
|
+
.description('Generate Agent Skills files from llms.txt')
|
|
34
|
+
.option('-o, --output <dir>', 'Base directory for skills output')
|
|
35
|
+
.action((opts) => runSkills(opts));
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.command('create')
|
|
39
|
+
.description('Scaffold a new component')
|
|
40
|
+
.argument('<name>', 'Component name (PascalCase)')
|
|
41
|
+
.action((name) => runCreate(name));
|
|
42
|
+
|
|
43
|
+
program
|
|
44
|
+
.command('rules')
|
|
45
|
+
.description('Generate AI rules files (.cursorrules + CLAUDE.md)')
|
|
46
|
+
.option('-o, --output <dir>', 'Output directory')
|
|
47
|
+
.action((opts) => runRules(opts));
|
|
48
|
+
|
|
49
|
+
program.parse();
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unity UI — Config Loader
|
|
3
|
+
*
|
|
4
|
+
* Looks for unity-ui.config.mjs in process.cwd().
|
|
5
|
+
* If found, merges with defaults. If not, returns defaults.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { pathToFileURL } from 'url';
|
|
10
|
+
|
|
11
|
+
export const DEFAULTS = {
|
|
12
|
+
components: {
|
|
13
|
+
directory: 'src/components',
|
|
14
|
+
indexFile: 'src/index.ts',
|
|
15
|
+
},
|
|
16
|
+
ai: {
|
|
17
|
+
skills: {
|
|
18
|
+
output: '.claude',
|
|
19
|
+
},
|
|
20
|
+
rules: {
|
|
21
|
+
output: '.',
|
|
22
|
+
targets: ['cursorrules', 'claude'],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function deepMerge(target, source) {
|
|
28
|
+
const result = { ...target };
|
|
29
|
+
for (const key of Object.keys(source)) {
|
|
30
|
+
if (
|
|
31
|
+
source[key] &&
|
|
32
|
+
typeof source[key] === 'object' &&
|
|
33
|
+
!Array.isArray(source[key]) &&
|
|
34
|
+
target[key] &&
|
|
35
|
+
typeof target[key] === 'object' &&
|
|
36
|
+
!Array.isArray(target[key])
|
|
37
|
+
) {
|
|
38
|
+
result[key] = deepMerge(target[key], source[key]);
|
|
39
|
+
} else {
|
|
40
|
+
result[key] = source[key];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Loads unity-ui.config.mjs from process.cwd() and merges with defaults.
|
|
48
|
+
* Returns defaults if no config file is found.
|
|
49
|
+
*/
|
|
50
|
+
export async function loadConfig() {
|
|
51
|
+
const configPath = path.resolve(process.cwd(), 'unity-ui.config.mjs');
|
|
52
|
+
|
|
53
|
+
if (!fs.existsSync(configPath)) {
|
|
54
|
+
return DEFAULTS;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const configUrl = pathToFileURL(configPath).href;
|
|
59
|
+
const mod = await import(configUrl);
|
|
60
|
+
const userConfig = mod.default || {};
|
|
61
|
+
return deepMerge(DEFAULTS, userConfig);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.warn(
|
|
64
|
+
`\x1b[33m⚠️ Failed to load unity-ui.config.mjs: ${err.message}\x1b[0m`
|
|
65
|
+
);
|
|
66
|
+
return DEFAULTS;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unity UI — Component Scaffolder (core logic)
|
|
3
|
+
*
|
|
4
|
+
* Creates a new component folder with all required files
|
|
5
|
+
* from templates.
|
|
6
|
+
*/
|
|
7
|
+
import { loadConfig } from './config.mjs';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
const lowerFirstLetter = (str) => {
|
|
17
|
+
if (!str) return '';
|
|
18
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const checkComponentName = (componentName) => {
|
|
22
|
+
const componentNameRegex = /^[A-Z][a-zA-Z]*$/;
|
|
23
|
+
|
|
24
|
+
if (!componentNameRegex.test(componentName)) {
|
|
25
|
+
console.error(
|
|
26
|
+
'❌ Component name must start with an uppercase letter and contain only letters'
|
|
27
|
+
);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (componentName.includes(' ')) {
|
|
32
|
+
console.error('❌ Component name must not contain spaces');
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return true;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export async function runCreate(componentName) {
|
|
40
|
+
if (!checkComponentName(componentName)) {
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const config = await loadConfig();
|
|
45
|
+
|
|
46
|
+
const componentDir = path.join(
|
|
47
|
+
process.cwd(),
|
|
48
|
+
config.components.directory,
|
|
49
|
+
componentName
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const templatesDir = path.resolve(__dirname, '..', 'templates');
|
|
53
|
+
|
|
54
|
+
const templateMap = {
|
|
55
|
+
[`{name}.tsx`]: 'component.tsx',
|
|
56
|
+
[`{name}.styled.ts`]: 'styled.ts',
|
|
57
|
+
[`{name}.stories.tsx`]: 'stories.tsx',
|
|
58
|
+
[`{name}.test.tsx`]: 'test.tsx',
|
|
59
|
+
[`{name}.types.ts`]: 'types.ts',
|
|
60
|
+
[`{name}.mdx`]: 'doc.mdx',
|
|
61
|
+
[`index.ts`]: 'index.ts',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Load template files
|
|
65
|
+
const loadTemplate = (templateFile, name) => {
|
|
66
|
+
const filePath = path.join(templatesDir, templateFile);
|
|
67
|
+
|
|
68
|
+
if (!fs.existsSync(filePath)) {
|
|
69
|
+
console.error(`❌ Template file not found: \x1b[33m${filePath}\x1b[0m`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
74
|
+
|
|
75
|
+
return content
|
|
76
|
+
.replace(/__COMPONENT_NAME__/g, name)
|
|
77
|
+
.replace(/__STYLES__FUNCTION__/g, lowerFirstLetter(name));
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Create component folder
|
|
81
|
+
if (!fs.existsSync(componentDir)) {
|
|
82
|
+
fs.mkdirSync(componentDir);
|
|
83
|
+
console.log(`✅ Created folder: \x1b[34m${componentName}\x1b[0m`);
|
|
84
|
+
} else {
|
|
85
|
+
console.log(`⚠️ Folder \x1b[33m${componentName}\x1b[0m already exists`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Create files
|
|
89
|
+
Object.entries(templateMap).forEach(([fileNamePattern, templateFile]) => {
|
|
90
|
+
const fileName = fileNamePattern.replace('{name}', componentName);
|
|
91
|
+
const fullPath = path.join(componentDir, fileName);
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(fullPath)) {
|
|
94
|
+
const content = loadTemplate(templateFile, componentName);
|
|
95
|
+
fs.writeFileSync(fullPath, content, 'utf8');
|
|
96
|
+
console.log(`✅ Created file: \x1b[32m${fileName}\x1b[0m`);
|
|
97
|
+
} else {
|
|
98
|
+
console.log(`⚠️ File \x1b[33m${fileName}\x1b[0m already exists`);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const indexFile = path.join(process.cwd(), config.components.indexFile);
|
|
103
|
+
const exportLine = `export * from './components/${componentName}';`;
|
|
104
|
+
|
|
105
|
+
// Add export to index.ts
|
|
106
|
+
if (fs.existsSync(indexFile)) {
|
|
107
|
+
const content = fs.readFileSync(indexFile, 'utf8');
|
|
108
|
+
const lines = content.split('\n').filter((line) => line.trim());
|
|
109
|
+
|
|
110
|
+
// Separate theme import from component exports
|
|
111
|
+
const themeImport = lines.find((line) =>
|
|
112
|
+
line.includes("import './theme.css'")
|
|
113
|
+
);
|
|
114
|
+
const componentExports = lines.filter(
|
|
115
|
+
(line) => line !== themeImport && line.startsWith('export * from')
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (!componentExports.includes(exportLine)) {
|
|
119
|
+
// Add new export and sort alphabetically
|
|
120
|
+
componentExports.push(exportLine);
|
|
121
|
+
const sortedExports = componentExports.sort((a, b) => {
|
|
122
|
+
const getComponentName = (line) => {
|
|
123
|
+
const match = line.match(/from '\.\/components\/([^']+)'/);
|
|
124
|
+
return match ? match[1] : '';
|
|
125
|
+
};
|
|
126
|
+
return getComponentName(a).localeCompare(getComponentName(b));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const finalContent =
|
|
130
|
+
[themeImport, '', ...sortedExports].filter(Boolean).join('\n') + '\n';
|
|
131
|
+
|
|
132
|
+
const finalContentWithEmptyLine = finalContent.replace(
|
|
133
|
+
/(import '\.\/theme\.css';\n)(\n*)/,
|
|
134
|
+
'$1\n'
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
fs.writeFileSync(indexFile, finalContentWithEmptyLine, 'utf8');
|
|
138
|
+
console.log(
|
|
139
|
+
`✅ Added export to \x1b[34m./src/index.ts\x1b[0m (sorted alphabetically)`
|
|
140
|
+
);
|
|
141
|
+
} else {
|
|
142
|
+
console.log(`⚠️ Export already exists in \x1b[33m./src/index.ts\x1b[0m`);
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
console.log(`❌ \x1b[33m./src/index.ts\x1b[0m does not exist.`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log(
|
|
149
|
+
`🎉 Component \x1b[32m<${componentName} />\x1b[0m structure created successfully!`
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Format the newly created component folder
|
|
153
|
+
try {
|
|
154
|
+
execSync(
|
|
155
|
+
`npx prettier --write "${componentDir}" --ignore-glob "**/*.md" --ignore-glob "**/*.mdx"`,
|
|
156
|
+
{ stdio: 'inherit' }
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
console.log(`✅ Prettier formatted: \x1b[32m<${componentName} />\x1b[0m`);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error(`❌ Prettier formatting failed:`, err);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unity UI — Init Command
|
|
3
|
+
*
|
|
4
|
+
* Interactive questionnaire that generates unity-ui.config.mjs.
|
|
5
|
+
*/
|
|
6
|
+
import { DEFAULTS } from './config.mjs';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import readline from 'readline';
|
|
10
|
+
|
|
11
|
+
function createRL() {
|
|
12
|
+
return readline.createInterface({
|
|
13
|
+
input: process.stdin,
|
|
14
|
+
output: process.stdout,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ask(rl, question, defaultValue) {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
rl.question(
|
|
21
|
+
` \x1b[36m?\x1b[0m ${question} \x1b[90m(${defaultValue})\x1b[0m `,
|
|
22
|
+
(answer) => {
|
|
23
|
+
resolve(answer.trim() || defaultValue);
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function askChoice(rl, question, options, defaultValue) {
|
|
30
|
+
const optionsList = options.map((o) => o.label).join(' / ');
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
rl.question(
|
|
33
|
+
` \x1b[36m?\x1b[0m ${question} \x1b[90m(${defaultValue})\x1b[0m [${optionsList}] `,
|
|
34
|
+
(answer) => {
|
|
35
|
+
const trimmed = answer.trim().toLowerCase();
|
|
36
|
+
const match = options.find(
|
|
37
|
+
(o) => o.label.toLowerCase() === trimmed || o.alias === trimmed
|
|
38
|
+
);
|
|
39
|
+
if (match) {
|
|
40
|
+
resolve(match.value);
|
|
41
|
+
} else {
|
|
42
|
+
// Default
|
|
43
|
+
const defaultOption = options.find(
|
|
44
|
+
(o) =>
|
|
45
|
+
o.label.toLowerCase() === defaultValue.toLowerCase() ||
|
|
46
|
+
o.alias === defaultValue.toLowerCase()
|
|
47
|
+
);
|
|
48
|
+
resolve(defaultOption ? defaultOption.value : options[0].value);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function generateConfigContent(config) {
|
|
56
|
+
const targets = config.ai.rules.targets;
|
|
57
|
+
const targetsStr = targets.map((t) => `'${t}'`).join(', ');
|
|
58
|
+
|
|
59
|
+
return `// unity-ui.config.mjs — generated by \`npx unity-ui init\`
|
|
60
|
+
import { defineConfig } from '@zvoove/unity-ui/config';
|
|
61
|
+
|
|
62
|
+
export default defineConfig({
|
|
63
|
+
components: {
|
|
64
|
+
directory: '${config.components.directory}',
|
|
65
|
+
indexFile: '${config.components.indexFile}',
|
|
66
|
+
},
|
|
67
|
+
ai: {
|
|
68
|
+
skills: {
|
|
69
|
+
output: '${config.ai.skills.output}',
|
|
70
|
+
},
|
|
71
|
+
rules: {
|
|
72
|
+
output: '${config.ai.rules.output}',
|
|
73
|
+
targets: [${targetsStr}],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function runInit() {
|
|
81
|
+
const configPath = path.resolve(process.cwd(), 'unity-ui.config.mjs');
|
|
82
|
+
|
|
83
|
+
if (fs.existsSync(configPath)) {
|
|
84
|
+
console.log(
|
|
85
|
+
`\x1b[33m⚠️ unity-ui.config.mjs already exists. Delete it first to re-initialize.\x1b[0m`
|
|
86
|
+
);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log(`\n \x1b[1m🔧 Unity UI — Project Setup\x1b[0m\n`);
|
|
91
|
+
|
|
92
|
+
const rl = createRL();
|
|
93
|
+
|
|
94
|
+
const componentsDir = await ask(
|
|
95
|
+
rl,
|
|
96
|
+
'Where do your components live?',
|
|
97
|
+
DEFAULTS.components.directory
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const indexFile = await ask(
|
|
101
|
+
rl,
|
|
102
|
+
'Where is your main index/barrel file?',
|
|
103
|
+
DEFAULTS.components.indexFile
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const rulesTargets = await askChoice(
|
|
107
|
+
rl,
|
|
108
|
+
'Generate AI rules for which tools?',
|
|
109
|
+
[
|
|
110
|
+
{ label: 'Both', alias: 'b', value: ['cursorrules', 'claude'] },
|
|
111
|
+
{ label: 'Claude', alias: 'c', value: ['claude'] },
|
|
112
|
+
{ label: 'Cursor', alias: 'u', value: ['cursorrules'] },
|
|
113
|
+
{ label: 'None', alias: 'n', value: [] },
|
|
114
|
+
],
|
|
115
|
+
'Both'
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const skillsOutput = await ask(
|
|
119
|
+
rl,
|
|
120
|
+
'Where should Agent Skills be generated?',
|
|
121
|
+
DEFAULTS.ai.skills.output
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const rulesOutput = await ask(
|
|
125
|
+
rl,
|
|
126
|
+
'Where should rules files be generated?',
|
|
127
|
+
DEFAULTS.ai.rules.output
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
rl.close();
|
|
131
|
+
|
|
132
|
+
const config = {
|
|
133
|
+
components: {
|
|
134
|
+
directory: componentsDir,
|
|
135
|
+
indexFile: indexFile,
|
|
136
|
+
},
|
|
137
|
+
ai: {
|
|
138
|
+
skills: { output: skillsOutput },
|
|
139
|
+
rules: { output: rulesOutput, targets: rulesTargets },
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const content = generateConfigContent(config);
|
|
144
|
+
fs.writeFileSync(configPath, content, 'utf8');
|
|
145
|
+
|
|
146
|
+
console.log(`\n \x1b[32m✅ Created unity-ui.config.mjs\x1b[0m\n`);
|
|
147
|
+
console.log(` Components: ${config.components.directory}/`);
|
|
148
|
+
console.log(` Index file: ${config.components.indexFile}`);
|
|
149
|
+
console.log(` Skills dir: ${config.ai.skills.output}/skills/`);
|
|
150
|
+
if (config.ai.rules.targets.length > 0) {
|
|
151
|
+
console.log(
|
|
152
|
+
` Rules: ${config.ai.rules.targets.join(', ')} → ${config.ai.rules.output}/`
|
|
153
|
+
);
|
|
154
|
+
} else {
|
|
155
|
+
console.log(` Rules: disabled`);
|
|
156
|
+
}
|
|
157
|
+
console.log('');
|
|
158
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unity UI — AI Rules Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates .cursorrules and CLAUDE.md files for consuming projects.
|
|
5
|
+
* These files help AI coding assistants understand and correctly use
|
|
6
|
+
* Unity UI components.
|
|
7
|
+
*/
|
|
8
|
+
import { loadConfig } from './config.mjs';
|
|
9
|
+
import { findLlmsTxt } from './skills.mjs';
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
|
|
13
|
+
function generateRulesContent(llmsContent) {
|
|
14
|
+
return `# Unity UI — AI Rules
|
|
15
|
+
|
|
16
|
+
> Auto-generated by \`npx unity-ui rules\`. Do not edit manually.
|
|
17
|
+
> Source: @zvoove/unity-ui llms.txt
|
|
18
|
+
|
|
19
|
+
## General Rules
|
|
20
|
+
|
|
21
|
+
- Always import components from \`@zvoove/unity-ui\`
|
|
22
|
+
- Never recreate components that exist in this library
|
|
23
|
+
- Use responsive props where applicable (single value or breakpoint object)
|
|
24
|
+
- Use design tokens from theme.css — never hardcode colors, spacing, or shadows
|
|
25
|
+
- Use \`tv()\` from tailwind-variants for component styling — never inline conditional classes
|
|
26
|
+
- Dark mode uses \`data-theme="dark"\` attribute, not \`className="dark"\`
|
|
27
|
+
- Default/placeholder texts should be in German
|
|
28
|
+
- Font (Source Sans 3) is bundled via \`@font-face\` in theme.css — no extra setup needed
|
|
29
|
+
|
|
30
|
+
## Setup
|
|
31
|
+
|
|
32
|
+
\`\`\`bash
|
|
33
|
+
npm install @zvoove/unity-ui
|
|
34
|
+
\`\`\`
|
|
35
|
+
|
|
36
|
+
\`\`\`css
|
|
37
|
+
@import '@zvoove/unity-ui/theme.css';
|
|
38
|
+
@import '@zvoove/unity-ui/unity-ui.css';
|
|
39
|
+
\`\`\`
|
|
40
|
+
|
|
41
|
+
## Responsive Props
|
|
42
|
+
|
|
43
|
+
Breakpoints: minimum (0px), mobile (320px), tablet (768px), laptop (1024px), desktop (1440px).
|
|
44
|
+
|
|
45
|
+
\`\`\`tsx
|
|
46
|
+
<Button size="md" />
|
|
47
|
+
<Button size={{ mobile: 'sm', tablet: 'md', desktop: 'lg' }} />
|
|
48
|
+
\`\`\`
|
|
49
|
+
|
|
50
|
+
## Component Reference
|
|
51
|
+
|
|
52
|
+
${llmsContent}
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function runRules({ output: outputDir } = {}) {
|
|
57
|
+
const config = await loadConfig();
|
|
58
|
+
const resolvedDir = path.resolve(
|
|
59
|
+
process.cwd(),
|
|
60
|
+
outputDir || config.ai.rules.output
|
|
61
|
+
);
|
|
62
|
+
const targets = config.ai.rules.targets;
|
|
63
|
+
|
|
64
|
+
const llmsPath = findLlmsTxt();
|
|
65
|
+
if (!llmsPath) {
|
|
66
|
+
console.error(
|
|
67
|
+
'Could not find llms.txt. Make sure @zvoove/unity-ui is installed and built.'
|
|
68
|
+
);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const llmsContent = fs.readFileSync(llmsPath, 'utf8');
|
|
73
|
+
const rulesContent = generateRulesContent(llmsContent);
|
|
74
|
+
|
|
75
|
+
fs.mkdirSync(resolvedDir, { recursive: true });
|
|
76
|
+
|
|
77
|
+
if (targets.includes('cursorrules')) {
|
|
78
|
+
const cursorRulesPath = path.join(resolvedDir, '.cursorrules');
|
|
79
|
+
fs.writeFileSync(cursorRulesPath, rulesContent, 'utf8');
|
|
80
|
+
console.log(
|
|
81
|
+
`✅ Generated ${path.relative(process.cwd(), cursorRulesPath)}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (targets.includes('claude')) {
|
|
86
|
+
const claudeMdPath = path.join(resolvedDir, 'CLAUDE.md');
|
|
87
|
+
fs.writeFileSync(claudeMdPath, rulesContent, 'utf8');
|
|
88
|
+
console.log(`✅ Generated ${path.relative(process.cwd(), claudeMdPath)}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (targets.length > 0) {
|
|
92
|
+
console.log(
|
|
93
|
+
`\n Your AI assistant now knows how to use Unity UI components.\n`
|
|
94
|
+
);
|
|
95
|
+
} else {
|
|
96
|
+
console.log(
|
|
97
|
+
`\n No rule targets configured. Set targets in unity-ui.config.mjs.\n`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|