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") }} +

Theming

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