decantr 0.1.0 → 0.2.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.

Potentially problematic release.


This version of decantr might be problematic. Click here for more details.

package/cli/art.js ADDED
@@ -0,0 +1,59 @@
1
+ const WINE = '\x1b[35m';
2
+ const DIM = '\x1b[2m';
3
+ const BOLD = '\x1b[1m';
4
+ const RESET = '\x1b[0m';
5
+ const CYAN = '\x1b[36m';
6
+
7
+ const DECANTER = `
8
+ ${WINE} ╭─────╮
9
+ │ │
10
+ ╰──┬──╯
11
+
12
+ ╭──┴──╮
13
+ ╱ ╲
14
+ ╱ ╲
15
+ │ ${RESET}${BOLD}decantr${RESET}${WINE} │
16
+ │ │
17
+ ╲ ╱
18
+ ╰───────╯${RESET}`;
19
+
20
+ const messages = [
21
+ 'Letting the code breathe...',
22
+ 'Decanting your project...',
23
+ 'Swirling the dependencies...',
24
+ 'Pouring the components...',
25
+ 'A fine vintage of JavaScript...',
26
+ 'Uncorking fresh signals...',
27
+ 'Aerating the DOM...',
28
+ 'Notes of CSS on the palate...',
29
+ 'Bottle-aged to perfection...',
30
+ 'Full-bodied, zero dependencies...'
31
+ ];
32
+
33
+ export function art() {
34
+ return DECANTER;
35
+ }
36
+
37
+ export function tagline() {
38
+ return messages[Math.floor(Math.random() * messages.length)];
39
+ }
40
+
41
+ export function welcome(version) {
42
+ return `${art()}
43
+
44
+ ${BOLD}decantr${RESET} ${DIM}v${version}${RESET} ${DIM}— AI-first web framework${RESET}
45
+ ${CYAN}${tagline()}${RESET}
46
+ `;
47
+ }
48
+
49
+ export function success(msg) {
50
+ return `\x1b[32m✓${RESET} ${msg}`;
51
+ }
52
+
53
+ export function info(msg) {
54
+ return `${CYAN}→${RESET} ${msg}`;
55
+ }
56
+
57
+ export function heading(msg) {
58
+ return `\n ${BOLD}${msg}${RESET}\n`;
59
+ }
@@ -2,244 +2,134 @@ import { createInterface } from 'node:readline/promises';
2
2
  import { stdin, stdout } from 'node:process';
3
3
  import { mkdir, writeFile } from 'node:fs/promises';
4
4
  import { join, basename } from 'node:path';
5
-
6
- const templates = {
7
- packageJson: (name) => JSON.stringify({
8
- name,
9
- version: '0.1.0',
10
- type: 'module',
11
- scripts: {
12
- dev: 'decantr dev',
13
- build: 'decantr build',
14
- test: 'decantr test'
15
- },
16
- dependencies: {
17
- decantr: '^0.1.0'
18
- }
19
- }, null, 2),
20
-
21
- config: (name, routerMode, port) => JSON.stringify({
22
- $schema: 'https://decantr.ai/schemas/config.v1.json',
23
- name,
24
- router: routerMode,
25
- dev: { port },
26
- build: { outDir: 'dist', inline: false, sourcemap: false }
27
- }, null, 2),
28
-
29
- indexHtml: (name) => `<!DOCTYPE html>
30
- <html lang="en">
31
- <head>
32
- <meta charset="UTF-8">
33
- <meta name="viewport" content="width=device-width,initial-scale=1">
34
- <title>${name}</title>
35
- <style>:root{--c0:#111;--c1:#2563eb;--c2:#f8fafc;--c3:#e2e8f0;--c4:#94a3b8;--c5:#475569;--c6:#1e293b;--c7:#f59e0b;--c8:#10b981;--c9:#ef4444}*{margin:0;box-sizing:border-box}body{font-family:system-ui,-apple-system,sans-serif;color:var(--c0);background:var(--c2)}</style>
36
- </head>
37
- <body>
38
- <div id="app"></div>
39
- <script type="module" src="/src/app.js"></script>
40
- </body>
41
- </html>`,
42
-
43
- appJs: (routerMode, includeCounter) => `import { h, mount } from 'decantr/core';
44
- import { createRouter, link } from 'decantr/router';
45
- import { Home } from './pages/home.js';
46
- import { About } from './pages/about.js';
47
-
48
- const router = createRouter({
49
- mode: '${routerMode}',
50
- routes: [
51
- { path: '/', component: Home },
52
- { path: '/about', component: About }
53
- ]
54
- });
55
-
56
- function App() {
57
- return h('div', null,
58
- h('nav', { class: 'flex gap4 p4 bg1' },
59
- link({ href: '/', class: 'fg2 nounder' }, 'Home'),
60
- link({ href: '/about', class: 'fg2 nounder' }, 'About')
61
- ),
62
- router.outlet()
63
- );
64
- }
65
-
66
- mount(document.getElementById('app'), App);
67
- `,
68
-
69
- homeJs: (includeCounter) => `import { h } from 'decantr/core';
70
- ${includeCounter ? "import { Counter } from '../components/counter.js';\n" : ''}
71
- /** @returns {HTMLElement} */
72
- export function Home() {
73
- return h('main', { class: 'p8' },
74
- h('h1', { class: 't32 bold' }, 'Welcome to Decantr'),
75
- h('p', { class: 'py4 fg5' }, 'AI-first web framework. Zero dependencies.')${includeCounter ? ',\n h(\'div\', { class: \'py4\' }, Counter({ initial: 0 }))' : ''}
76
- );
77
- }
78
- `,
79
-
80
- aboutJs: () => `import { h } from 'decantr/core';
81
-
82
- /** @returns {HTMLElement} */
83
- export function About() {
84
- return h('main', { class: 'p8' },
85
- h('h1', { class: 't32 bold' }, 'About'),
86
- h('p', { class: 'py4 fg5' }, 'Built with Decantr — the AI-first framework.')
87
- );
88
- }
89
- `,
90
-
91
- counterJs: () => `import { h, text } from 'decantr/core';
92
- import { createSignal } from 'decantr/state';
93
-
94
- /**
95
- * @param {{ initial?: number }} props
96
- * @returns {HTMLElement}
97
- */
98
- export function Counter({ initial = 0 } = {}) {
99
- const [count, setCount] = createSignal(initial);
100
-
101
- return h('div', { class: 'flex gap2 p4 aic' },
102
- h('button', { onclick: () => setCount(c => c - 1), class: 'p2 px4 r2 bg1 fg2 pointer b0 t16' }, '-'),
103
- h('span', { class: 'p2 t20 bold' }, text(() => String(count()))),
104
- h('button', { onclick: () => setCount(c => c + 1), class: 'p2 px4 r2 bg1 fg2 pointer b0 t16' }, '+')
105
- );
106
- }
107
- `,
108
-
109
- counterTestJs: () => `import { describe, it, assert, render, fire, flush } from 'decantr/test';
110
- import { Counter } from '../src/components/counter.js';
111
-
112
- describe('Counter', () => {
113
- it('renders with initial value', () => {
114
- const { container } = render(() => Counter({ initial: 5 }));
115
- assert.ok(container.textContent.includes('5'));
116
- });
117
-
118
- it('increments on + click', async () => {
119
- const { container, getByText } = render(() => Counter({ initial: 0 }));
120
- fire(getByText('+'), 'click');
121
- await flush();
122
- assert.ok(container.textContent.includes('1'));
123
- });
124
-
125
- it('decrements on - click', async () => {
126
- const { container, getByText } = render(() => Counter({ initial: 5 }));
127
- fire(getByText('-'), 'click');
128
- await flush();
129
- assert.ok(container.textContent.includes('4'));
130
- });
131
- });
132
- `,
133
-
134
- manifest: (name, routerMode) => JSON.stringify({
135
- $schema: 'https://decantr.ai/schemas/manifest.v1.json',
136
- version: '0.1.0',
137
- name,
138
- router: routerMode,
139
- entrypoint: 'src/app.js',
140
- shell: 'public/index.html',
141
- mountTarget: '#app',
142
- components: '.decantr/components.json',
143
- routes: '.decantr/routes.json',
144
- state: '.decantr/state.json'
145
- }, null, 2),
146
-
147
- components: (includeCounter) => JSON.stringify({
148
- $schema: 'https://decantr.ai/schemas/components.v1.json',
149
- components: includeCounter ? [{
150
- id: 'counter',
151
- file: 'src/components/counter.js',
152
- exportName: 'Counter',
153
- description: 'Increment/decrement counter with display',
154
- props: [{ name: 'initial', type: 'number', default: 0, required: false }],
155
- signals: ['count'],
156
- effects: [],
157
- children: false
158
- }] : []
159
- }, null, 2),
160
-
161
- routes: (routerMode) => JSON.stringify({
162
- $schema: 'https://decantr.ai/schemas/routes.v1.json',
163
- mode: routerMode,
164
- routes: [
165
- { path: '/', component: 'home', file: 'src/pages/home.js', exportName: 'Home', title: 'Home' },
166
- { path: '/about', component: 'about', file: 'src/pages/about.js', exportName: 'About', title: 'About' }
167
- ]
168
- }, null, 2),
169
-
170
- state: (includeCounter) => JSON.stringify({
171
- $schema: 'https://decantr.ai/schemas/state.v1.json',
172
- signals: includeCounter ? [{
173
- id: 'count',
174
- file: 'src/components/counter.js',
175
- type: 'number',
176
- initial: 0,
177
- usedBy: ['Counter']
178
- }] : [],
179
- effects: [],
180
- memos: []
181
- }, null, 2)
182
- };
5
+ import { welcome, success, info, heading } from '../art.js';
6
+ import { packageJson, configJson, indexHtml, manifest } from '../templates/shared.js';
7
+ import { dashboardFiles } from '../templates/dashboard.js';
8
+ import { landingFiles } from '../templates/landing.js';
9
+ import { demoFiles } from '../templates/demo.js';
183
10
 
184
11
  async function ask(rl, question, defaultVal) {
185
- const answer = await rl.question(`${question} (${defaultVal}): `);
12
+ const answer = await rl.question(` ${question} (${defaultVal}): `);
186
13
  return answer.trim() || defaultVal;
187
14
  }
188
15
 
189
- async function askChoice(rl, question, options, defaultVal) {
190
- console.log(`\n${question}`);
191
- options.forEach((opt, i) => console.log(` ${i + 1}) ${opt}`));
192
- const answer = await rl.question(`Choose [${defaultVal}]: `);
16
+ async function askChoice(rl, question, options, defaultIdx = 0) {
17
+ console.log(heading(question));
18
+ options.forEach((opt, i) => {
19
+ const marker = i === defaultIdx ? '\x1b[36m>' : ' ';
20
+ console.log(` ${marker} ${i + 1}) ${opt.label}${opt.desc ? ` \x1b[2m— ${opt.desc}\x1b[0m` : ''}\x1b[0m`);
21
+ });
22
+ const answer = await rl.question(` Choose [${defaultIdx + 1}]: `);
193
23
  const idx = parseInt(answer.trim()) - 1;
194
- return (idx >= 0 && idx < options.length) ? options[idx] : defaultVal;
24
+ return (idx >= 0 && idx < options.length) ? options[idx].value : options[defaultIdx].value;
195
25
  }
196
26
 
197
27
  export async function run() {
198
28
  const rl = createInterface({ input: stdin, output: stdout });
199
29
  const cwd = process.cwd();
200
30
 
201
- console.log('\n decantr init — Create a new project\n');
31
+ console.log(welcome('0.2.0'));
202
32
 
203
33
  try {
34
+ // 1. Project name
204
35
  const name = await ask(rl, 'Project name?', basename(cwd));
205
- const routerMode = await askChoice(rl, 'Router mode?', ['history', 'hash'], 'history');
206
- const includeCounterAnswer = await askChoice(rl, 'Include example counter component?', ['yes', 'no'], 'yes');
207
- const includeCounter = includeCounterAnswer === 'yes';
36
+
37
+ // 2. Project type (or demo mode)
38
+ const projectType = await askChoice(rl, 'Project type?', [
39
+ { label: 'Dashboard', desc: 'Sidebar, header, data tables', value: 'dashboard' },
40
+ { label: 'Landing Page', desc: 'Hero, features, pricing', value: 'landing' },
41
+ { label: "Don't ask me, just show me!", desc: 'Demo showcase of everything', value: 'demo' }
42
+ ]);
43
+
44
+ // 3. Color theme
45
+ const theme = await askChoice(rl, 'Color theme?', [
46
+ { label: 'Light', value: 'light' },
47
+ { label: 'Dark', value: 'dark' },
48
+ { label: 'AI', desc: 'Deep purples & cyans', value: 'ai' },
49
+ { label: 'Nature', desc: 'Earthy greens', value: 'nature' },
50
+ { label: 'Pastel', desc: 'Soft pinks', value: 'pastel' },
51
+ { label: 'Spice', desc: 'Warm oranges', value: 'spice' },
52
+ { label: 'Monochromatic', desc: 'Pure grayscale', value: 'mono' }
53
+ ]);
54
+
55
+ // 4. Design style
56
+ const style = await askChoice(rl, 'Design style?', [
57
+ { label: 'Glassmorphism', desc: 'Frosted glass, blur', value: 'glass' },
58
+ { label: 'Claymorphism', desc: 'Soft, puffy, rounded', value: 'clay' },
59
+ { label: 'Minimal', desc: 'Clean lines, no effects', value: 'flat' },
60
+ { label: 'Neobrutalism', desc: 'Bold borders, offset shadows', value: 'brutalist' },
61
+ { label: 'Skeuomorphic', desc: 'Gradients, 3D depth', value: 'skeuo' },
62
+ { label: 'Monochromatic', desc: 'Black & white elegance', value: 'mono' },
63
+ { label: 'Hand-drawn', desc: 'Wobbly borders, sketchy', value: 'sketchy' }
64
+ ], 2);
65
+
66
+ // 5. Router mode
67
+ const router = await askChoice(rl, 'Router mode?', [
68
+ { label: 'History', desc: 'Clean URLs (needs server)', value: 'history' },
69
+ { label: 'Hash', desc: 'Works everywhere', value: 'hash' }
70
+ ]);
71
+
72
+ // 6. Icons
73
+ const iconsChoice = await askChoice(rl, 'Icon library?', [
74
+ { label: 'None', desc: 'Skip for now', value: 'none' },
75
+ { label: 'Material Icons', desc: 'Google Material Design', value: 'material' },
76
+ { label: 'Lucide', desc: 'Beautiful open-source icons', value: 'lucide' }
77
+ ]);
78
+
79
+ let icons = null;
80
+ let iconDelivery = null;
81
+ if (iconsChoice !== 'none') {
82
+ icons = iconsChoice;
83
+ iconDelivery = await askChoice(rl, 'Icon delivery?', [
84
+ { label: 'CDN', desc: 'Link tag, no install', value: 'cdn' },
85
+ { label: 'npm', desc: 'Install as dependency', value: 'npm' }
86
+ ]);
87
+ }
88
+
89
+ // 7. Port
208
90
  const port = parseInt(await ask(rl, 'Dev server port?', '3000'));
209
91
 
92
+ const opts = { name, projectType, theme, style, router, icons, iconDelivery, port };
93
+
94
+ console.log(heading('Decanting your project...'));
95
+
210
96
  // Create directories
211
- const dirs = ['src/pages', 'src/components', 'public', '.decantr', 'test'];
97
+ const dirs = ['public', '.decantr', 'test', 'src/pages', 'src/components'];
98
+ if (projectType === 'landing') dirs.push('src/sections');
212
99
  for (const dir of dirs) {
213
100
  await mkdir(join(cwd, dir), { recursive: true });
214
101
  }
215
102
 
216
- // Write files
103
+ // Shared files
217
104
  const files = [
218
- ['package.json', templates.packageJson(name)],
219
- ['decantr.config.json', templates.config(name, routerMode, port)],
220
- ['public/index.html', templates.indexHtml(name)],
221
- ['src/app.js', templates.appJs(routerMode, includeCounter)],
222
- ['src/pages/home.js', templates.homeJs(includeCounter)],
223
- ['src/pages/about.js', templates.aboutJs()],
224
- ['.decantr/manifest.json', templates.manifest(name, routerMode)],
225
- ['.decantr/components.json', templates.components(includeCounter)],
226
- ['.decantr/routes.json', templates.routes(routerMode)],
227
- ['.decantr/state.json', templates.state(includeCounter)]
105
+ ['package.json', packageJson(name)],
106
+ ['decantr.config.json', configJson(opts)],
107
+ ['public/index.html', indexHtml(opts)],
108
+ ['.decantr/manifest.json', manifest(opts)]
228
109
  ];
229
110
 
230
- if (includeCounter) {
231
- files.push(['src/components/counter.js', templates.counterJs()]);
232
- files.push(['test/counter.test.js', templates.counterTestJs()]);
111
+ // Project-type files
112
+ let typeFiles;
113
+ if (projectType === 'dashboard') {
114
+ typeFiles = dashboardFiles(opts);
115
+ } else if (projectType === 'landing') {
116
+ typeFiles = landingFiles(opts);
117
+ } else {
118
+ typeFiles = demoFiles(opts);
233
119
  }
120
+ files.push(...typeFiles);
234
121
 
122
+ // Write all files
235
123
  for (const [path, content] of files) {
236
124
  await writeFile(join(cwd, path), content + '\n');
125
+ console.log(' ' + success(path));
237
126
  }
238
127
 
239
- console.log(`\n Project "${name}" created successfully!\n`);
240
- console.log(' Next steps:');
241
- console.log(' npm install');
242
- console.log(' npx decantr dev\n');
128
+ console.log(heading('Your project is ready!'));
129
+ console.log(info('Next steps:'));
130
+ console.log(` npm install`);
131
+ console.log(` npx decantr dev`);
132
+ console.log('');
243
133
 
244
134
  } finally {
245
135
  rl.close();
package/cli/index.js CHANGED
@@ -18,21 +18,24 @@ switch (command) {
18
18
  case 'test':
19
19
  await import('./commands/test.js').then(m => m.run());
20
20
  break;
21
- default:
21
+ default: {
22
+ const { art } = await import('./art.js');
23
+ console.log(art());
22
24
  console.log(`
23
- decantr v0.1.0 — AI-first web framework
25
+ \x1b[1mdecantr\x1b[0m v0.2.0 \x1b[2m— AI-first web framework\x1b[0m
24
26
 
25
- Commands:
27
+ \x1b[1mCommands:\x1b[0m
26
28
  init Create a new decantr project
27
29
  dev Start development server
28
30
  build Build for production
29
31
  test Run tests
30
32
 
31
- Usage:
33
+ \x1b[1mUsage:\x1b[0m
32
34
  npx decantr init
33
35
  npx decantr dev
34
36
  npx decantr build
35
37
  npx decantr test [--watch]
36
38
  `);
37
39
  break;
40
+ }
38
41
  }