basecoat-cli 0.1.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.
@@ -0,0 +1,198 @@
1
+ {#
2
+ Renders a toaster container and individual toast messages.
3
+ Can render the initial container or be used with htmx OOB swaps to add toasts dynamically.
4
+
5
+ @param id {string} [optional] [default="toaster"] - Unique identifier for the toaster container.
6
+ @param toasts {array} [optional] - An array of toast objects to render initially. See the <code>toast()</code> macro for more details.
7
+ @param main_attrs {object} [optional] - Additional HTML attributes for the main toaster container div.
8
+ @param is_fragment {boolean} [optional] [default=false] - If true, renders only the toast elements with hx-swap-oob="beforeend", suitable for htmx responses. Skips script and template inclusion.
9
+ #}
10
+ {% macro toaster(
11
+ id="toaster",
12
+ toasts=[],
13
+ main_attrs=None,
14
+ is_fragment=false
15
+ ) %}
16
+ <div
17
+ id="{{ id }}"
18
+ class="toaster"
19
+ {% for key, value in main_attrs %}
20
+ {{ key }}="{{ value }}"
21
+ {% endfor %}
22
+ {% if is_fragment %}hx-swap-oob="beforeend"{% endif %}
23
+ >
24
+ {% for item in toasts %}
25
+ {{ toast(
26
+ category=item.category,
27
+ title=item.title,
28
+ description=item.description,
29
+ action=item.action,
30
+ cancel=item.cancel
31
+ ) }}
32
+ {% endfor %}
33
+ </div>
34
+
35
+ {% if not is_fragment %}
36
+ <template id="toast-template">
37
+ <div
38
+ class="toast"
39
+ role="status"
40
+ aria-atomic="true"
41
+ x-bind="$toastBindings"
42
+ >
43
+ <div class="toast-content">
44
+ <div class="flex items-center justify-between gap-x-3 p-4 [&>svg]:size-4 [&>svg]:shrink-0 [&>[role=img]]:size-4 [&>[role=img]]:shrink-0 [&>[role=img]>svg]:size-4">
45
+ <template x-if="config.icon">
46
+ <span aria-hidden="true" role="img" x-html="config.icon"></span>
47
+ </template>
48
+ <template x-if="!config.icon && config.category === 'success'">
49
+ {{ toast_icons.success | safe }}
50
+ </template>
51
+ <template x-if="!config.icon && config.category === 'error'">
52
+ {{ toast_icons.error | safe }}
53
+ </template>
54
+ <template x-if="!config.icon && config.category === 'info'">
55
+ {{ toast_icons.info | safe }}
56
+ </template>
57
+ <template x-if="!config.icon && config.category === 'warning'">
58
+ {{ toast_icons.warning | safe }}
59
+ </template>
60
+ <section class="flex-1 flex flex-col gap-0.5 items-start">
61
+ <template x-if="config.title">
62
+ <h2 class="font-medium" x-text="config.title"></h2>
63
+ </template>
64
+ <template x-if="config.description">
65
+ <p class="text-muted-foreground" x-text="config.description"></p>
66
+ </template>
67
+ </section>
68
+ <template x-if="config.action || config.cancel">
69
+ <footer class="flex flex-col gap-1 self-start">
70
+ <template x-if="config.action?.click">
71
+ <button
72
+ type="button"
73
+ class="btn h-6 text-xs px-2.5 rounded-sm"
74
+ @click="executeAction(config.action.click)"
75
+ x-text="config.action.label"
76
+ ></button>
77
+ </template>
78
+ <template x-if="config.action?.url">
79
+ <a
80
+ :href="config.action.url"
81
+ class="btn h-6 text-xs px-2.5 rounded-sm"
82
+ x-text="config.action.label"
83
+ ></a>
84
+ </template>
85
+ <template x-if="config.cancel?.click">
86
+ <button
87
+ type="button"
88
+ class="btn-outline h-6 text-xs px-2.5 rounded-sm"
89
+ @click="executeAction(config.cancel.click)"
90
+ x-text="config.cancel.label"
91
+ ></button>
92
+ </template>
93
+ <template x-if="config.cancel?.url">
94
+ <a
95
+ :href="config.cancel.url"
96
+ class="btn-outline h-6 text-xs px-2.5 rounded-sm"
97
+ x-text="config.cancel.label"
98
+ ></a>
99
+ </template>
100
+ </footer>
101
+ </template>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </template>
106
+ {% endif %}
107
+ {% endmacro %}
108
+
109
+ {#
110
+ Renders a single toast message.
111
+
112
+ @param category {string} [optional] [default="success"] - Type of toast ('success', 'error', 'info', 'warning'). Determines icon and ARIA role.
113
+ @param title {string} [optional] - The main title text of the toast.
114
+ @param description {string} [optional] - The secondary description text.
115
+ @param action {object} [optional] - Defines an action button.
116
+ - label {string}: Button text.
117
+ - click {string}: JavaScript code to execute on click (e.g., '$dispatch(\'custom-event\')').
118
+ - url {string}: URL for an anchor link button.
119
+ @param cancel {object} [optional] - Defines a cancel/dismiss button (similar structure to action).
120
+ - label {string}: Button text.
121
+ - click {string}: JavaScript code to execute on click (e.g., '$dispatch(\'custom-event\')').
122
+ - url {string}: URL for an anchor link button.
123
+ #}
124
+ {% macro toast(
125
+ category="success",
126
+ title="",
127
+ description="",
128
+ action=None,
129
+ cancel=None
130
+ ) %}
131
+ <div
132
+ class="toast"
133
+ role="{{ 'alert' if category == 'error' else 'status' }}"
134
+ aria-atomic="true"
135
+ aria-hidden="false"
136
+ {% if category %}data-category="{{ category }}"{% endif %}
137
+ x-data="toast({
138
+ category: '{{ category }}',
139
+ duration: {{ duration or 'null' }}
140
+ })"
141
+ x-bind="$toastBindings"
142
+ >
143
+ <div class="toast-content">
144
+ <div class="flex items-center justify-between gap-x-3 p-4 [&>svg]:size-4 [&>svg]:shrink-0 [&>[role=img]]:size-4 [&>[role=img]]:shrink-0 [&>[role=img]>svg]:size-4">
145
+ {% if category in ["error", "success", "info", "warning"] %}
146
+ {{ toast_icons[category] | safe }}
147
+ {% endif %}
148
+ <section class="flex-1 flex flex-col gap-0.5 items-start">
149
+ {% if title %}
150
+ <h2 class="font-medium">{{ title }}</h2>
151
+ {% endif %}
152
+ {% if description %}
153
+ <p class="text-muted-foreground">{{ description }}</p>
154
+ {% endif %}
155
+ </section>
156
+ {% if action or cancel %}
157
+ <footer class="flex flex-col gap-1 self-start">
158
+ {% if action %}
159
+ {% if action.click %}
160
+ <button
161
+ type="button"
162
+ class="btn h-6 text-xs px-2.5 rounded-sm"
163
+ @click="{{ action.click }}"
164
+ >{{ action.label }}</button>
165
+ {% elif action.url %}
166
+ <a
167
+ href="{{ action.url }}"
168
+ class="btn h-6 text-xs px-2.5 rounded-sm"
169
+ >{{ action.label }}</a>
170
+ {% endif %}
171
+ {% endif %}
172
+ {% if cancel %}
173
+ {% if cancel.click %}
174
+ <button
175
+ type="button"
176
+ class="btn-outline h-6 text-xs px-2.5 rounded-sm"
177
+ @click="{{ cancel.click }}"
178
+ >{{ cancel.label }}</button>
179
+ {% elif cancel.url %}
180
+ <a
181
+ href="{{ cancel.url }}"
182
+ class="btn-outline h-6 text-xs px-2.5 rounded-sm"
183
+ >{{ toast.cancel.label }}</a>
184
+ {% endif %}
185
+ {% endif %}
186
+ </footer>
187
+ {% endif %}
188
+ </div>
189
+ </div>
190
+ </div>
191
+ {% endmacro %}
192
+
193
+ {% set toast_icons = {
194
+ 'success': '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check-icon lucide-circle-check"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>',
195
+ 'error': '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-x-icon lucide-circle-x"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>',
196
+ 'info': '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info-icon lucide-info"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
197
+ 'warning': '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert-icon lucide-triangle-alert"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>'
198
+ } %}
package/dist/index.js ADDED
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import inquirer from 'inquirer';
5
+ import fs from 'fs-extra';
6
+ import path from 'path';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ const program = new Command();
10
+
11
+ // Attempt to read version from the CLI's own package.json
12
+ let packageVersion = 'unknown';
13
+ try {
14
+ const cliScriptRunningDir = path.dirname(fileURLToPath(import.meta.url)); // .../packages/cli/dist
15
+ const cliPackageRootDir = path.resolve(cliScriptRunningDir, '..'); // .../packages/cli
16
+ const packageJsonPath = path.join(cliPackageRootDir, 'package.json');
17
+ const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
18
+ packageVersion = JSON.parse(packageJsonContent).version;
19
+ } catch (error) {
20
+ console.warn('Could not read CLI package version:', error.message);
21
+ }
22
+
23
+ const DEFAULT_CONFIG = {
24
+ templateEngine: null,
25
+ templateDest: './components/basecoat',
26
+ scriptDest: './static/js/basecoat'
27
+ };
28
+ let config = { ...DEFAULT_CONFIG }; // User-specific config, potentially updated by prompts
29
+
30
+ // Discovers available components from the CLI's bundled assets.
31
+ async function getAvailableComponents() {
32
+ try {
33
+ const cliScriptRunningDir = path.dirname(fileURLToPath(import.meta.url));
34
+ const assetsDir = path.resolve(cliScriptRunningDir, 'assets');
35
+ const jsSourceDir = path.join(assetsDir, 'js');
36
+
37
+ const jsFiles = await fs.readdir(jsSourceDir);
38
+
39
+ return jsFiles
40
+ .filter(file => file.endsWith('.js'))
41
+ .map(file => path.basename(file, '.js'));
42
+ } catch (error) {
43
+ console.error("Error reading component source directories from CLI assets:", error);
44
+ return [];
45
+ }
46
+ }
47
+
48
+ // Ensures necessary configuration (template engine, destination paths) is set, prompting the user if needed.
49
+ async function ensureConfiguration() {
50
+ if (!config.templateEngine) {
51
+ const engineChoice = await inquirer.prompt([
52
+ {
53
+ type: 'list',
54
+ name: 'engine',
55
+ message: 'Which template engine are you using?',
56
+ choices: ['nunjucks', 'jinja'],
57
+ }
58
+ ]);
59
+ config.templateEngine = engineChoice.engine;
60
+ }
61
+
62
+ const pathPrompts = [];
63
+ if (!config.templateDest || config.templateDest === DEFAULT_CONFIG.templateDest) {
64
+ pathPrompts.push({
65
+ type: 'input',
66
+ name: 'templateDest',
67
+ message: 'Where should template files be placed?',
68
+ default: DEFAULT_CONFIG.templateDest,
69
+ });
70
+ }
71
+ if (!config.scriptDest || config.scriptDest === DEFAULT_CONFIG.scriptDest) {
72
+ pathPrompts.push({
73
+ type: 'input',
74
+ name: 'scriptDest',
75
+ message: 'Where should script files be placed?',
76
+ default: DEFAULT_CONFIG.scriptDest,
77
+ });
78
+ }
79
+
80
+ if (pathPrompts.length > 0) {
81
+ const pathChoices = await inquirer.prompt(pathPrompts);
82
+ if (pathChoices.templateDest) config.templateDest = pathChoices.templateDest;
83
+ if (pathChoices.scriptDest) config.scriptDest = pathChoices.scriptDest;
84
+ }
85
+ }
86
+
87
+ // Copies a component's template and script files to the user's project.
88
+ async function addComponent(componentName) {
89
+ console.log(`\nProcessing component: ${componentName}...`);
90
+
91
+ const cliScriptRunningDir = path.dirname(fileURLToPath(import.meta.url));
92
+ const assetsDir = path.resolve(cliScriptRunningDir, 'assets');
93
+ const templateExt = config.templateEngine === 'jinja' ? '.html.jinja' : '.njk';
94
+
95
+ const templateSource = path.join(assetsDir, config.templateEngine, `${componentName}${templateExt}`);
96
+ const scriptSource = path.join(assetsDir, 'js', `${componentName}.js`);
97
+
98
+ const templateDestPath = path.resolve(process.cwd(), config.templateDest, `${componentName}${templateExt}`);
99
+ const scriptDestPath = path.resolve(process.cwd(), config.scriptDest, `${componentName}.js`);
100
+
101
+ try {
102
+ const templateExists = await fs.pathExists(templateSource);
103
+ const scriptExists = await fs.pathExists(scriptSource);
104
+
105
+ if (!templateExists) {
106
+ console.error(` Error: Template file for component '${componentName}' not found in CLI assets. Searched: ${templateSource}`);
107
+ // Optionally, list available template engines or files for that component if helpful
108
+ }
109
+ if (!scriptExists) {
110
+ console.error(` Error: Script file for component '${componentName}' not found in CLI assets. Searched: ${scriptSource}`);
111
+ }
112
+ if (!templateExists || !scriptExists) return; // Skip this component if sources are missing
113
+
114
+ await fs.ensureDir(path.dirname(templateDestPath));
115
+ await fs.ensureDir(path.dirname(scriptDestPath));
116
+
117
+ await fs.copyFile(templateSource, templateDestPath);
118
+ console.log(` -> Copied template to: ${templateDestPath}`);
119
+ await fs.copyFile(scriptSource, scriptDestPath);
120
+ console.log(` -> Copied script to: ${scriptDestPath}`);
121
+
122
+ } catch (error) {
123
+ console.error(` Error processing component ${componentName}:`, error);
124
+ }
125
+ }
126
+
127
+ // Setup main CLI program
128
+ program
129
+ .name('basecoat-cli')
130
+ .description('Add Basecoat components to your project')
131
+ .version(packageVersion);
132
+
133
+ // Define the 'add' command
134
+ program
135
+ .command('add')
136
+ .description('Add one or more Basecoat components to your project')
137
+ .argument('[components...]', 'Names of components to add (e.g., dialog select)')
138
+ .action(async (componentsArg) => {
139
+ let componentsToAdd = []; // Initialize as an empty array
140
+ try {
141
+ if (!componentsArg || componentsArg.length === 0) {
142
+ const availableComponents = await getAvailableComponents();
143
+ if (availableComponents.length === 0) {
144
+ console.error('Error: No components available in the CLI. Build might be corrupted.');
145
+ return;
146
+ }
147
+
148
+ const allComponentsChoice = 'All components';
149
+ const choices = [allComponentsChoice, ...availableComponents];
150
+
151
+ const answers = await inquirer.prompt([
152
+ {
153
+ type: 'list', // Changed from checkbox to list
154
+ name: 'selectedComponent', // Changed name for clarity
155
+ message: 'Which component(s) would you like to add?',
156
+ choices: choices,
157
+ }
158
+ ]);
159
+
160
+ if (answers.selectedComponent === allComponentsChoice) {
161
+ componentsToAdd = availableComponents; // Add all
162
+ } else {
163
+ componentsToAdd = [answers.selectedComponent]; // Add the single selected one
164
+ }
165
+ } else {
166
+ // If components were provided as arguments, use them directly
167
+ // (This part of the logic remains, ensuring componentsArg is an array)
168
+ componentsToAdd = Array.isArray(componentsArg) ? componentsArg : [componentsArg];
169
+ }
170
+
171
+ if (!componentsToAdd || componentsToAdd.length === 0) {
172
+ console.log('No components selected. Exiting.');
173
+ return;
174
+ }
175
+
176
+ await ensureConfiguration(); // Confirm/gather destination paths
177
+
178
+ for (const componentName of componentsToAdd) {
179
+ await addComponent(componentName);
180
+ }
181
+ console.log('\nComponent addition process finished.');
182
+
183
+ } catch (error) {
184
+ if (error.isTtyError || error.constructor?.name === 'ExitPromptError') { // Handle inquirer cancellation
185
+ console.log('\nOperation cancelled by user.');
186
+ } else {
187
+ console.error('\nAn unexpected error occurred during the add command:', error);
188
+ }
189
+ process.exit(1); // Exit with error code for unexpected errors
190
+ }
191
+ });
192
+
193
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "basecoat-cli",
3
+ "version": "0.1.0",
4
+ "description": "Add Basecoat components to your project",
5
+ "author": "hunvreus",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "bin": {
9
+ "basecoat-cli": "./dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist/"
13
+ ],
14
+ "dependencies": {
15
+ "commander": "^13.1.0",
16
+ "fs-extra": "^11.3.0",
17
+ "inquirer": "^12.6.0"
18
+ },
19
+ "keywords": [
20
+ "cli",
21
+ "components",
22
+ "component library",
23
+ "component system",
24
+ "ui",
25
+ "ui kit",
26
+ "shadcn",
27
+ "shadcn/ui",
28
+ "tailwind",
29
+ "tailwindcss",
30
+ "css",
31
+ "html",
32
+ "jinja",
33
+ "nunjucks",
34
+ "alpinejs"
35
+ ],
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/hunvreus/basecoat.git",
39
+ "directory": "packages/cli"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/hunvreus/basecoat/issues"
43
+ },
44
+ "homepage": "https://basecoatui.com/installation#install-cli"
45
+ }