diff --git a/cli/index.js b/cli/index.js
index 947bc7e..21e913e 100755
--- a/cli/index.js
+++ b/cli/index.js
@@ -1,60 +1,175 @@
#!/usr/bin/env node
-// ^^^ Makes the script executable
import { Command } from 'commander';
-// We'll add fs-extra and inquirer later
+import inquirer from 'inquirer';
+import fs from 'fs-extra';
+import path from 'path';
const program = new Command();
+let packageVersion = 'unknown';
+try {
+ const cliScriptDir = path.dirname(new URL(import.meta.url).pathname);
+ const packageJsonPath = path.resolve(cliScriptDir, '..', 'package.json');
+ const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
+ packageVersion = JSON.parse(packageJsonContent).version;
+} catch (error) {
+ console.warn('Could not read package version:', error.message);
+}
+
+const DEFAULT_CONFIG = {
+ templateEngine: null,
+ templateDest: './components/basecoat',
+ scriptDest: './static/js/basecoat'
+};
+let config = { ...DEFAULT_CONFIG };
+
+async function getAvailableComponents() {
+ try {
+ const packageRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
+ const jsSourceDir = path.join(packageRoot, 'src', 'js');
+ const jsFiles = await fs.readdir(jsSourceDir);
+ return jsFiles
+ .filter(file => file.endsWith('.js'))
+ .map(file => path.basename(file, '.js'));
+ } catch (error) {
+ console.error("Error reading component source directories:", error);
+ return [];
+ }
+}
+
+async function ensureConfiguration() {
+ console.log('\nChecking configuration...');
+
+ if (!config.templateEngine) {
+ const engineChoice = await inquirer.prompt([
+ {
+ type: 'list',
+ name: 'engine',
+ message: 'Which template engine are you using?',
+ choices: ['nunjucks', 'jinja'],
+ }
+ ]);
+ config.templateEngine = engineChoice.engine;
+ }
+
+ const pathChoices = await inquirer.prompt([
+ {
+ type: 'input',
+ name: 'templateDest',
+ message: 'Where should template files be placed?',
+ default: './',
+ },
+ {
+ type: 'input',
+ name: 'scriptDest',
+ message: 'Where should script files be placed?',
+ default: './',
+ }
+ ]);
+ config.templateDest = pathChoices.templateDest;
+ config.scriptDest = pathChoices.scriptDest;
+
+ console.log(`Configuration set: Engine=${config.templateEngine}, Templates=${config.templateDest}, Scripts=${config.scriptDest}`);
+}
+
console.log('Basecoat CLI');
program
.name('basecoat')
.description('Add Basecoat components to your project')
- .version('0.1.0'); // TODO: Get version from package.json
+ .version(packageVersion);
program
.command('add')
.description('Add one or more components to your project')
- .argument('[components...]', 'Names of components to add') // Optional arg for now
- .action(async (components) => {
- if (!components || components.length === 0) {
- console.log('Please specify which component(s) to add, or run add without args for a list (TODO).');
- // TODO: Implement interactive component selection if no args
- return;
+ .argument('[components...]', 'Names of components to add')
+ .action(async (componentsArg) => {
+ let componentsToAdd = componentsArg;
+ try {
+ if (!componentsToAdd || componentsToAdd.length === 0) {
+ const availableComponents = await getAvailableComponents();
+ if (availableComponents.length === 0) {
+ console.error('Error: Could not find any components.');
+ return;
+ }
+
+ const answers = await inquirer.prompt([
+ {
+ type: 'checkbox',
+ name: 'selectedComponents',
+ message: 'Which component(s) would you like to add?',
+ choices: availableComponents,
+ validate: (input) => input.length > 0 ? true : 'Please select at least one component.'
+ }
+ ]);
+
+ componentsToAdd = answers.selectedComponents;
+ }
+
+ if (!componentsToAdd || componentsToAdd.length === 0) {
+ console.log('No components selected. Exiting.');
+ return;
+ }
+
+ console.log(`Attempting to add: ${componentsToAdd.join(', ')}`);
+
+ await ensureConfiguration();
+
+ for (const componentName of componentsToAdd) {
+ await addComponent(componentName);
+ }
+
+ console.log('\nComponent addition process finished.');
+ } catch (error) {
+ if (error.isTtyError || error.constructor.name === 'ExitPromptError') {
+ console.log('\nOperation cancelled.');
+ } else {
+ console.error('\nAn unexpected error occurred:', error);
+ }
+ process.exit(1);
}
-
- console.log(`Attempting to add: ${components.join(', ')}`);
-
- for (const componentName of components) {
- await addComponent(componentName);
- }
-
- console.log('\nComponent addition process finished.');
});
-// Placeholder function for adding a single component
async function addComponent(componentName) {
- console.log(`\nProcessing component: ${componentName}`);
+ console.log(`\nProcessing component: ${componentName}...`);
- // 1. Check if config file exists (TODO)
- // 2. If not, run init command/ask questions (TODO)
- // 3. Read config (template engine, paths) (TODO)
- // 4. Determine source file paths based on component name and config (TODO)
- // 5. Check if source files exist in the package (TODO)
- // 6. Copy files to destination paths using fs-extra (TODO)
+ // 1. Determine source paths (relative to package root)
+ const cliScriptDir = path.dirname(new URL(import.meta.url).pathname); // Get directory of the current script file
+ // Go UP from cli/, then DOWN into src/, then the specific folder
+ const templateExt = config.templateEngine === 'jinja' ? '.html.jinja' : '.njk';
+ const templateSource = path.resolve(cliScriptDir, '..', 'src', config.templateEngine, `${componentName}${templateExt}`);
+ const scriptSource = path.resolve(cliScriptDir, '..', 'src', 'js', `${componentName}.js`);
- console.log(` -> Placeholder: Logic to copy ${componentName} files would go here.`);
+ // 2. Determine destination paths (relative to user's current working directory)
+ const templateDestPath = path.resolve(process.cwd(), config.templateDest, `${componentName}${templateExt}`);
+ const scriptDestPath = path.resolve(process.cwd(), config.scriptDest, `${componentName}.js`);
+
+ try {
+ // 3. Check if source files exist
+ const templateExists = await fs.pathExists(templateSource);
+ const scriptExists = await fs.pathExists(scriptSource);
+
+ if (!templateExists || !scriptExists) {
+ console.error(` Error: Source files for component '${componentName}' not found. Searched:`);
+ if (!templateExists) console.error(` - ${templateSource}`);
+ if (!scriptExists) console.error(` - ${scriptSource}`);
+ return; // Skip this component
+ }
+
+ // 4. Ensure destination directories exist
+ await fs.ensureDir(path.dirname(templateDestPath));
+ await fs.ensureDir(path.dirname(scriptDestPath));
+
+ // 5. Copy files
+ await fs.copyFile(templateSource, templateDestPath);
+ console.log(` -> Copied template to: ${templateDestPath}`);
+ await fs.copyFile(scriptSource, scriptDestPath);
+ console.log(` -> Copied script to: ${scriptDestPath}`);
+
+ } catch (error) {
+ console.error(` Error processing component ${componentName}:`, error);
+ }
}
-// TODO: Implement init command
-// program
-// .command('init')
-// .description('Initialize basecoat configuration')
-// .action(async () => {
-// console.log('Running init...');
-// // Ask config questions
-// // Save config file
-// });
-
program.parse(process.argv);
\ No newline at end of file
diff --git a/docs/css/styles.css b/docs/css/styles.css
index 641a99f..4847ecb 100644
--- a/docs/css/styles.css
+++ b/docs/css/styles.css
@@ -1,3 +1,5 @@
+@source "../../src/css/basecoat.css";
+
@import url("https://fonts.googleapis.com/css2?family=Poppins&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Lora&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Fira+Code&display=swap");
@@ -5,12 +7,13 @@
@import url("https://fonts.googleapis.com/css2?family=Inter&display=swap");
@import "tailwindcss";
-@import "basecoat";
+@import "../../src/css/basecoat.css";
@import "./custom.css";
@theme {
--font-sans: 'Inter';
--font-mono: 'JetBrains Mono';
+
}
-/* @import "./theme-orange.css"; */
\ No newline at end of file
+/* @import "./theme-orange.css"; */
diff --git a/docs/src/_includes/layouts/base.njk b/docs/src/_includes/layouts/base.njk
index 9a7f08b..0581636 100644
--- a/docs/src/_includes/layouts/base.njk
+++ b/docs/src/_includes/layouts/base.njk
@@ -49,6 +49,7 @@
+
{% endset %}
{{ code_block(code, "jinja") }}
+
diff --git a/src/css/basecoat.css b/src/css/basecoat.css
index e85ced9..a7bd279 100644
--- a/src/css/basecoat.css
+++ b/src/css/basecoat.css
@@ -38,7 +38,8 @@
--scrollbar-width: 6px;
--scrollbar-radius: 6px;
--sidebar-width: 16rem;
- --chevron-down-icon: url('data:image/svg+xml;utf8,'); /* --muted-foreground + 50% opacity */
+ --chevron-down-icon: url('data:image/svg+xml;utf8,'); /* --muted-foreground */
+ --chevron-down-icon-50: url('data:image/svg+xml;utf8,'); /* --muted-foreground + 50% opacity */
--check-icon: url('data:image/svg+xml;utf8,'); /* --muted-foreground */
}
@@ -78,7 +79,8 @@
--scrollbar-thumb: rgba(255, 255, 255, 0.3);
--scrollbar-width: 6px;
--scrollbar-radius: 6px;
- --chevron-down-icon: url('data:image/svg+xml;utf8,'); /* --muted-foreground + 50% opacity */
+ --chevron-down-icon: url('data:image/svg+xml;utf8,'); /* --muted-foreground */
+ --chevron-down-icon-50: url('data:image/svg+xml;utf8,'); /* --muted-foreground + 50% opacity */
--check-icon: url('data:image/svg+xml;utf8,');/* --muted-foreground */
color-scheme: dark;
}
@@ -404,6 +406,7 @@
> header {
@apply @container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6;
+ /* TODO: CLEAN has-data-[slot=card-action] */
h2 {
@apply leading-none font-semibold;
@@ -429,7 +432,7 @@
&:checked:after {
@apply content-[''] block size-3.5 bg-primary-foreground;
- @apply mask-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWNoZWNrLWljb24gbHVjaWRlLWNoZWNrIj48cGF0aCBkPSJNMjAgNiA5IDE3bC01LTUiLz48L3N2Zz4=')] mask-size-[0.875rem] mask-no-repeat mask-center;
+ @apply mask-[image:var(--check-icon)] mask-size-[0.875rem] mask-no-repeat mask-center;
}
}
}
@@ -739,7 +742,7 @@
.form select,
.select {
@apply appearance-none border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent pl-3 pr-9 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 h-9;
- @apply bg-[image:var(--chevron-down-icon)] bg-no-repeat bg-position-[center_right_0.75rem] bg-size-[1rem];
+ @apply bg-[image:var(--chevron-down-icon-50)] bg-no-repeat bg-position-[center_right_0.75rem] bg-size-[1rem];
}
}
@@ -906,4 +909,109 @@
}
}
}
+}
+
+/* Sidebar */
+.sidebar {
+ @apply bg-sidebar text-sidebar-foreground flex flex-col w-(--sidebar-width) fixed inset-y-0 transition-transform ease-in-out z-50;
+ /* h-svh */
+
+ &:not([data-side]),
+ &[data-side=left] {
+ @apply left-0 border-r aria-[hidden=true]:-translate-x-full;
+
+ & + * {
+ @apply relative ml-(--sidebar-width) transition-[margin] ease-in-out;
+ }
+ &[aria-hidden=true] + * {
+ @apply ml-0;
+ }
+ }
+ &[data-side=right] {
+ @apply right-0 border-l aria-[hidden=true]:translate-x-full;
+
+ & + * {
+ @apply relative mr-(--sidebar-width) transition-[margin] ease-in-out;
+ }
+ &[aria-hidden=true] + * {
+ @apply mr-0;
+ }
+ }
+
+
+ > header,
+ > footer {
+ @apply flex flex-col gap-2 p-2;
+ }
+ [role=separator] {
+ @apply border-sidebar-border mx-2 w-auto;
+ }
+ > section {
+ @apply flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto;
+
+ > [role=group] {
+ @apply relative flex w-full min-w-0 flex-col p-2;
+ }
+
+ h3 {
+ @apply text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0;
+ }
+
+ /* GROUP CONTENT: w-full text-sm */
+ ul {
+ @apply flex w-full min-w-0 flex-col gap-1;
+
+ li {
+ /* group/menu-item */
+ @apply relative;
+
+ > a,
+ > details > summary {
+ /* peer/menu-button */
+ @apply flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0;
+
+ &:not([data-variant]),
+ &[data-variant=default] {
+ @apply hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
+ }
+ &[data-variant=outline] {
+ @apply bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))];
+ }
+ &:not([data-size]),
+ &[data-size=default] {
+ @apply h-8 text-sm;
+ }
+ &[data-size=sm] {
+ @apply h-7 text-xs;
+ }
+ &[data-size=lg] {
+ @apply h-12 text-sm group-data-[collapsible=icon]:p-0!;
+ }
+ }
+
+ > details {
+ &:not([open]) {
+ > summary {
+ &::after {
+ @apply -rotate-90;
+ }
+ }
+ }
+ > summary {
+ &::after {
+ @apply content-[''] block size-3.5 bg-primary ml-auto transition-transform ease-linear;
+ @apply mask-[image:var(--chevron-down-icon)] mask-size-[1rem] mask-no-repeat mask-center;
+ }
+ }
+ &::details-content {
+ @apply px-3.5;
+ }
+ }
+ }
+
+ ul {
+ @apply border-sidebar-border flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5 w-full;
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/src/jinja/dialog.html.jinja b/src/jinja/dialog.html.jinja
index ff8c8c2..a528ac4 100644
--- a/src/jinja/dialog.html.jinja
+++ b/src/jinja/dialog.html.jinja
@@ -84,7 +84,7 @@
{{ key }}="{{ value }}"
{% endfor %}
>
- {% if caller %}{{ caller() }}{% endif %}
+ {{ caller() }}
{% endif %}
{% if footer %}
diff --git a/src/jinja/dropdown-menu.html.jinja b/src/jinja/dropdown-menu.html.jinja
index fcf05d3..cdaa0bf 100644
--- a/src/jinja/dropdown-menu.html.jinja
+++ b/src/jinja/dropdown-menu.html.jinja
@@ -50,7 +50,7 @@
{% endfor %}
>
diff --git a/src/jinja/popover.html.jinja b/src/jinja/popover.html.jinja
index b805f51..2b2bda9 100644
--- a/src/jinja/popover.html.jinja
+++ b/src/jinja/popover.html.jinja
@@ -49,7 +49,7 @@
{{ key }}="{{ value }}"
{% endfor %}
>
- {% if caller %}{{ caller() }}{% endif %}
+ {{ caller() if caller }}
{% endmacro %}
diff --git a/src/jinja/select.html.jinja b/src/jinja/select.html.jinja
index ed7a336..72b2e5e 100644
--- a/src/jinja/select.html.jinja
+++ b/src/jinja/select.html.jinja
@@ -91,7 +91,7 @@
{{ key }}="{{ value }}"
{% endfor %}
>
- {% if caller %}{{ caller() }}{% endif %}
+ {{ caller() if caller }}
{% if name is defined %}
diff --git a/src/js/sidebar.js b/src/js/sidebar.js
new file mode 100644
index 0000000..9be7cd3
--- /dev/null
+++ b/src/js/sidebar.js
@@ -0,0 +1,27 @@
+window.basecoat = window.basecoat || {};
+window.basecoat.registerSidebar= function(Alpine) {
+ if (Alpine.components && Alpine.components.sidebar) return;
+
+ Alpine.data('sidebar', (initialOpen = true) => ({
+ id: null,
+ open: true,
+
+ init() {
+ this.id = this.$el.id;
+ this.open = initialOpen;
+ },
+
+ $main: {
+ '@sidebar:open.window'(e) { if (!this.id || (e.detail && e.detail.id === this.id)) this.open = true },
+ '@sidebar:close.window'(e) { if (!this.id || (e.detail && e.detail.id === this.id)) this.open = false },
+ '@sidebar:toggle.window'(e) {
+ if (!this.id || (e.detail && e.detail.id === this.id)) {
+ this.open = !this.open;
+ console.log('toggle', e);
+ }
+ },
+ ':aria-hidden'() { return !this.open },
+ ':inert'() { return !this.open },
+ },
+ }));
+};
\ No newline at end of file
diff --git a/src/nunjucks/dialog.njk b/src/nunjucks/dialog.njk
index ff61ef9..fa21c81 100644
--- a/src/nunjucks/dialog.njk
+++ b/src/nunjucks/dialog.njk
@@ -84,7 +84,7 @@
{{ key }}="{{ value }}"
{% endfor %}
>
- {% if caller %}{{ caller() }}{% endif %}
+ {{ caller() }}
{% endif %}
{% if footer %}
diff --git a/src/nunjucks/dropdown-menu.njk b/src/nunjucks/dropdown-menu.njk
index e183ba8..bb32357 100644
--- a/src/nunjucks/dropdown-menu.njk
+++ b/src/nunjucks/dropdown-menu.njk
@@ -50,7 +50,7 @@
{% endfor %}
>
diff --git a/src/nunjucks/popover.njk b/src/nunjucks/popover.njk
index aa8e30a..355f9e6 100644
--- a/src/nunjucks/popover.njk
+++ b/src/nunjucks/popover.njk
@@ -49,7 +49,7 @@
{{ key }}="{{ value }}"
{% endfor %}
>
- {% if caller %}{{ caller() }}{% endif %}
+ {{ caller() if caller }}
{% endmacro %}
diff --git a/src/nunjucks/select.njk b/src/nunjucks/select.njk
index 66463c9..6161777 100644
--- a/src/nunjucks/select.njk
+++ b/src/nunjucks/select.njk
@@ -91,7 +91,7 @@
{{ key }}="{{ value }}"
{% endfor %}
>
- {% if caller %}{{ caller() }}{% endif %}
+ {{ caller() if caller }}
{% if name is defined %}
diff --git a/src/nunjucks/sidebar.njk b/src/nunjucks/sidebar.njk
new file mode 100644
index 0000000..8d2baef
--- /dev/null
+++ b/src/nunjucks/sidebar.njk
@@ -0,0 +1,101 @@
+{% macro render_menu_items(items, level=0, parent_id="") %}
+
+{% endmacro %}
+
+{% macro sidebar(
+ id=None,
+ label="Sidebar navigation",
+ open=true,
+ side=None,
+ header=None,
+ footer=None,
+ menu=None,
+ main_attrs={},
+ header_attrs={},
+ content_attrs={},
+ footer_attrs={},
+ content_wrapper_attrs=None
+) %}
+
+{% endmacro %}
\ No newline at end of file