UNPKG

b0nes

Version:

Zero-dependency component library and SSR/SSG framework

887 lines (722 loc) 22.9 kB
# b0nes Framework > A zero-dependency SSG/SSR framework with built-in state management and component composition > GitHub: https://github.com/iggydotdev/b0nes > npm: https://www.npmjs.com/package/b0nes > Version: 0.2.0 ## Core Philosophy b0nes is a complete web development toolkit with zero npm dependencies. It provides: - Server-side rendering (SSR) and static site generation (SSG) - Component composition (atoms molecules organisms) - Built-in state management (Store) - Built-in state machines (FSM) with SPA routing - Client-side progressive enhancement - Pure JavaScript + JSDoc for type safety Perfect for: blogs, marketing sites, documentation, landing pages, e-commerce product pages, SaaS applications, SPAs ## Installation ```bash git clone https://github.com/iggydotdev/b0nes.git cd b0nes # No npm install needed! ``` ## Quick Start ```bash # Development server with hot reload npm run dev:watch # Build static site npm run build # Run component tests npm run test # Generate new component npm run generate [atom|molecule|organism] [name] # Install community component npm run install-component [url] ``` ## Component Architecture ### Atomic Design Hierarchy - **Atoms**: Basic elements (button, text, link, input, image, video, etc.) - **Molecules**: Combinations (card, tabs, modal, dropdown) - **Organisms**: Page sections (header, footer, hero, cta) ### Available Components #### Atoms (15 components) - **accordion**: `{ titleSlot, detailsSlot, className, attrs }` - **badge**: `{ slot, className, attrs }` - status indicators/labels - **box**: `{ is, slot, className, attrs }` - flexible container (div/section/article/etc) - **button**: `{ type, slot, className, attrs }` - **divider**: `{ className, attrs }` - horizontal rule - **image**: `{ src, alt, className, attrs }` - **input**: `{ type, className, attrs }` - **link**: `{ url, slot, className, attrs }` - **picture**: `{ slot, className, attrs }` - responsive images with source elements - **source**: `{ type, src, srcset, className, attrs }` - for picture/video elements - **text**: `{ is, slot, className, attrs }` - any text element (p, h1-h6, span, etc.) - **textarea**: `{ className, attrs }` - **video**: `{ src, slot, className, attrs }` #### Molecules (4 components) - **card**: `{ slot, headerSlot, mediaSlot, linkSlot, contentSlot, className, attrs }` - **tabs**: `{ tabs: [{ label, content }], className, attrs }` - requires b0nes.js - **modal**: `{ id, title, slot, className, attrs }` - requires b0nes.js - **dropdown**: `{ trigger, slot, className, attrs }` - requires b0nes.js #### Organisms (4 components) - **header**: `{ slot, className, attrs }` - **footer**: `{ slot, className, attrs }` - **hero**: `{ slot, className, attrs }` - **cta**: `{ slot, className, attrs }` ### Component API Pattern ALL components follow this pattern: ```javascript import { processSlotTrusted } from '../../utils/processSlot.js'; import { normalizeClasses } from '../../utils/normalizeClasses.js'; import { validateProps, validatePropTypes } from '../../utils/componentError.js'; export const componentName = ({ attrs = '', // Raw HTML attributes string className = '', // CSS classes slot, // Content (string or array) // ... component-specific props }) => { // 1. Validate required props validateProps({ slot }, ['slot'], { componentName: 'componentName', componentType: 'atom' }); // 2. Validate prop types validatePropTypes({ attrs, className }, { attrs: 'string', className: 'string' }, { componentName: 'componentName', componentType: 'atom' }); // 3. Process attributes and classes attrs = attrs ? ` ${attrs}` : ''; const classes = normalizeClasses(['base-class', className]); // 4. Process slot content const slotContent = processSlotTrusted(slot); // 5. Return HTML string return `<element class="${classes}"${attrs}>${slotContent}</element>`; }; ``` ## Component Usage ### Direct Usage (standalone) ```javascript import { button } from './components/atoms/button/button.js'; const html = button({ type: 'submit', slot: 'Click Me', className: 'primary' }); // Returns: '<button type="submit" class="btn primary">Click Me</button>' ``` ### Composition Usage (in pages) ```javascript // src/framework/pages/home.js export const components = [ { type: 'organism', name: 'hero', props: { slot: [ { type: 'atom', name: 'text', props: { is: 'h1', slot: 'Welcome' } }, { type: 'atom', name: 'button', props: { slot: 'Get Started' } } ] } } ]; ``` ## Routing ### Static Routes (SSG/SSR) ```javascript // src/framework/routes.js import { URLPattern } from './utils/urlPattern.js'; import { components as homeComponents } from './pages/home.js'; export const routes = [ { name: 'Home', pattern: new URLPattern({ pathname: '/' }), meta: { title: 'Home' }, components: homeComponents } ]; ``` ### Dynamic Routes (SSG/SSR) ```javascript { name: 'Blog Post', pattern: new URLPattern({ pathname: '/blog/:postid' }), meta: { title: 'Blog Post' }, components: blogPostComponents, // Function: (data) => [components] externalData: async () => { // Fetch data for this route return await fetchBlogPost(); } } ``` ## Client-Side Interactivity ### b0nes.js Runtime The framework includes a zero-dependency client-side runtime for progressive enhancement: ```javascript // Automatically loaded from /b0nes.js // Discovers components with data-b0nes attribute // Initializes interactive behaviors window.b0nes = { init(root), // Initialize components destroy(el), // Destroy component instance destroyAll(), // Destroy all components register(name, fn), // Register custom behavior behaviors: {}, // Registered behaviors activeInstances: Set // Active component instances }; ``` ### Interactive Components **Tabs** - Keyboard-accessible tabbed interface ```javascript { type: 'molecule', name: 'tabs', props: { tabs: [ { label: 'Tab 1', content: '<p>Content 1</p>' }, { label: 'Tab 2', content: '<p>Content 2</p>' } ] } } ``` **Modal** - Accessible dialog with focus management ```javascript // Modal component { type: 'molecule', name: 'modal', props: { id: 'my-modal', title: 'Title', slot: '<p>Content</p>' } } // Trigger button { type: 'atom', name: 'button', props: { attrs: 'data-modal-open="my-modal"', slot: 'Open Modal' } } ``` **Dropdown** - Click-to-toggle menu ```javascript { type: 'molecule', name: 'dropdown', props: { trigger: 'Menu', slot: '<a href="#">Item 1</a><a href="#">Item 2</a>' } } ``` ### Disabling Client-Side Runtime ```javascript // In routes.js meta: { title: 'My Page', interactive: false // Don't load b0nes.js } ``` ## State Management ### Store - Redux-style State Management ```javascript import { createStore } from './framework/client/store.js'; const store = createStore({ state: { count: 0 }, actions: { increment: (state) => ({ count: state.count + 1 }), decrement: (state) => ({ count: state.count - 1 }), reset: () => ({ count: 0 }) }, getters: { doubled: (state) => state.count * 2 } }); // Usage store.dispatch('increment'); // Update state const current = store.getState(); // Get state const doubled = store.computed('doubled'); // Get computed value // Subscribe to changes const unsubscribe = store.subscribe((change) => { console.log('State changed:', change); }); ``` ### Advanced Store Features ```javascript // Modules (organize large stores) import { combineModules, createModule } from './framework/client/store.js'; const userModule = createModule({ state: { name: '', email: '' }, actions: { updateProfile: (state, data) => ({ ...state, ...data }) } }); const cartModule = createModule({ state: { items: [] }, actions: { addItem: (state, item) => ({ items: [...state.items, item] }) } }); const store = createStore( combineModules({ user: userModule, cart: cartModule }) ); // Access namespaced store.dispatch('user/updateProfile', { name: 'John' }); store.dispatch('cart/addItem', { id: 1, name: 'Product' }); ``` ### Middleware ```javascript import { loggerMiddleware, persistenceMiddleware } from './framework/client/store.js'; const store = createStore({ state: { cart: [] }, actions: { /* ... */ }, middleware: [ loggerMiddleware, persistenceMiddleware('cart-data') ] }); ``` ## Finite State Machines (FSM) ### Basic FSM ```javascript import { createFSM } from './framework/client/fsm.js'; const authFSM = createFSM({ initial: 'logged-out', states: { 'logged-out': { on: { LOGIN: 'logging-in' } }, 'logging-in': { actions: { onEntry: (context, data) => { console.log('Starting login...'); // Return context updates if needed return { loading: true }; }, onExit: (context, data) => { console.log('Exiting login...'); } }, on: { SUCCESS: 'logged-in', FAILURE: 'logged-out' } }, 'logged-in': { on: { LOGOUT: 'logged-out' } } }, context: { user: null } // Initial context }); // Usage authFSM.send('LOGIN', { username: 'grok' }); // Transition with data authFSM.getState(); // 'logging-in' authFSM.is('logged-in'); // false authFSM.can('LOGOUT'); // false authFSM.getContext(); // { user: null, loading: true } authFSM.getHistory(); // Array of transitions authFSM.updateContext({ user: 'grok' }); // Update without transition authFSM.reset(); // Back to initial // Subscribe to changes const unsubscribe = authFSM.subscribe((transition) => { console.log('Transition:', transition); // { from, to, event, data, timestamp } }); // Visualize console.log(authFSM.toMermaid()); // Mermaid diagram string ``` ### FSM with Guards (Conditional Transitions) ```javascript const checkoutFSM = createFSM({ initial: 'cart', states: { 'cart': { on: { CHECKOUT: (context, data) => context.items.length > 0 ? 'payment' : 'cart' } }, 'payment': { on: { SUCCESS: 'complete' } }, 'complete': {} }, context: { items: [] } }); ``` ### FSM Router - SPA Routing with State Machines ```javascript import { createRouterFSM, connectFSMtoDOM } from './framework/client/fsm.js'; const routes = [ { name: 'start', url: '/demo/fsm/start', template: "<h1>FSM Demo</h1><button data-fsm-event='GOTO_STEP2'>Next</button>", onEnter: (context, data) => console.log('Entered start'), onExit: (context, data) => console.log('Exiting start') }, { name: 'step2', url: '/demo/fsm/step2', template: "<h1>Step 2</h1><button data-fsm-event='GOTO_START'>Back</button>" }, { name: 'success', url: '/demo/fsm/success', template: "<h1>Success!</h1>" } ]; const { fsm, routes: fsmRoutes } = createRouterFSM(routes); // Creates FSM with GOTO_ events // Connect to DOM (handles render, clicks, popstate) const rootEl = document.querySelector('[data-bones-fsm]'); const cleanup = connectFSMtoDOM(fsm, rootEl, routes); // Navigate programmatically fsm.send('GOTO_STEP2'); // Cleanup when done cleanup(); ``` ### FSM Router Example - Multi-Step Form ```javascript const routes = [ { name: 'start', url: '/form/start', template: "<h1>Start</h1><button data-fsm-event='GOTO_STEP2'>Next</button>" }, { name: 'step2', url: '/form/step2', template: "<h1>Step 2</h1><button data-fsm-event='GOTO_START'>Back</button><button data-fsm-event='GOTO_SUCCESS'>Submit</button>" }, { name: 'success', url: '/form/success', template: "<h1>Success!</h1><button data-fsm-event='GOTO_START'>Reset</button>" } ]; const { fsm } = createRouterFSM(routes); connectFSMtoDOM(fsm, document.getElementById('app'), routes); ``` ### FSM + Store Integration ```javascript import { connectStoreToFSM } from './framework/client/store.js'; const store = createStore({ state: { step: 'cart', formData: {} }, actions: { updateFormData: (state, data) => ({ formData: { ...state.formData, ...data } }) } }); const fsm = createFSM({ initial: 'cart', states: { /* ... */ } }); // Sync FSM state to store const disconnect = connectStoreToFSM(store, fsm); ``` ### Composed FSM (Parallel State Machines) ```javascript import { composeFSM } from './framework/client/fsm.js'; const composed = composeFSM({ auth: authFSM, checkout: checkoutFSM }); composed.getAllStates(); // { auth: 'logged-out', checkout: 'cart' } composed.getAllContexts(); // Combined contexts composed.send('auth', 'LOGIN'); // Send to specific machine composed.broadcast('RESET'); // Send to all that can handle it // Subscribe to any transition composed.subscribe((change) => { console.log(change); // { machine: 'auth', from, to, ... } }); ``` ## Key Functions ### compose(components) Converts component tree to HTML strings. ```javascript import { compose } from './framework/compose.js'; const html = compose([ { type: 'atom', name: 'text', props: { is: 'p', slot: 'Hello' } } ]); ``` ### renderPage(content, meta) Wraps content in full HTML document. ```javascript import { renderPage } from './framework/renderPage.js'; const html = renderPage(content, { title: 'My Page', interactive: true // Include b0nes.js (default) }); ``` ### router(url, routes) Matches URL to route definition. ```javascript import { router } from './framework/router.js'; const route = router(new URL('http://localhost/'), routes); // Returns: { params, query, meta, components, ... } ``` ## Component Generator ```bash # Generate new atom npm run generate atom badge # Generate new molecule npm run generate molecule card-list # Generate new organism npm run generate organism sidebar ``` Creates: ``` src/components/atoms/badge/ ├── index.js ├── badge.js └── badge.test.js ``` ## Component Installer Install community components from URLs: ```bash # Install from URL npm run install-component https://example.com/components/my-card # Preview without installing npm run install-component https://example.com/card --dry-run # Force overwrite existing npm run install-component https://example.com/card --force ``` ### Component Manifest Format ```json { "name": "my-card", "version": "1.0.0", "type": "molecule", "description": "A custom card component", "author": "Your Name <you@example.com>", "license": "MIT", "files": { "component": "./my-card.js", "test": "./my-card.test.js", "client": "./molecule.my-card.client.js" }, "dependencies": [], "tags": ["card", "layout"] } ``` ## Testing ### Component Tests ```javascript // component.test.js export const test = () => { const actual = component({ slot: 'Test' }); const expected = '<div class="component">Test</div>'; return actual === expected ? true : console.error({actual, expected}) || false; }; ``` Run tests: ```bash npm run test ``` ## Building & Deployment ### SSG Build ```bash npm run build # Outputs to public/ ``` ### Project Structure After Build ``` public/ ├── index.html # Homepage ├── demo/ └── index.html # Demo page └── blog/ └── [postid]/ └── index.html # Dynamic routes ``` ### Deployment Serve the `public/` directory on any static host: - Netlify: Drag & drop `public/` folder - Vercel: `vercel --prod` - GitHub Pages: Push `public/` to gh-pages branch - Cloudflare Pages: Connect repository ## Utility Functions ### processSlot(slot, options) Handles slot content with HTML escaping for user input. ```javascript import { processSlot, processSlotTrusted } from './components/utils/processSlot.js'; // Escapes HTML (for user input) const safe = processSlot('<script>alert(1)</script>'); // Returns: '&lt;script&gt;alert(1)&lt;/script&gt;' // Trusts HTML (for component content) const trusted = processSlotTrusted('<button>Click</button>'); // Returns: '<button>Click</button>' ``` ### normalizeClasses(classes) Normalizes and escapes CSS class names. ```javascript import { normalizeClasses } from './components/utils/normalizeClasses.js'; normalizeClasses(['btn', 'primary', '', 'large']); // Returns: 'btn primary large' normalizeClasses('btn primary large'); // Returns: 'btn primary large' ``` ### validateProps(props, required, context) Validates required props and throws descriptive errors. ```javascript import { validateProps } from './components/utils/componentError.js'; validateProps( { slot: 'text' }, ['slot', 'url'], { componentName: 'link', componentType: 'atom' } ); // Throws: ComponentError with details about missing 'url' prop ``` ## Important Constraints ### NO CSS Included (By Design) b0nes provides HTML structure only. Users choose their own CSS strategy: - Tailwind CSS - Vanilla CSS - CSS Modules - Any CSS framework This avoids: - Forced design opinions - CSS specificity conflicts - Breaking changes on updates - Bundle bloat ### Security: XSS Protection - `processSlotTrusted()` - For component-rendered HTML (trusted) - `processSlot()` with `escape: true` - For user input (escaped) - `escapeHtml()` / `escapeAttr()` - Available for manual escaping ### NO Client-Side JavaScript Required - Server-rendered HTML works without JavaScript - Progressive enhancement with b0nes.js - Interactive components degrade gracefully ### Zero Dependencies - Entire framework runs on Node.js built-ins only - No npm packages required - No build tools needed (except maybe for production optimization) ## Common Patterns ### Nested Components ```javascript { type: 'organism', name: 'card', props: { slot: [ { type: 'atom', name: 'text', props: { is: 'h2', slot: 'Title' }}, { type: 'atom', name: 'text', props: { is: 'p', slot: 'Content' }} ] } } ``` ### Conditional Rendering ```javascript const components = [ showHeader && { type: 'organism', name: 'header', props: {...} }, { type: 'organism', name: 'hero', props: {...} } ].filter(Boolean); ``` ### Custom Styling ```javascript button({ slot: 'Styled', className: 'btn-primary large', attrs: 'style="background: blue"' }) ``` ### Multi-Step Forms with FSM + Store ```javascript const formFSM = createFSM({ initial: 'step1', states: { step1: { on: { NEXT: 'step2' } }, step2: { on: { NEXT: 'step3', BACK: 'step1' } }, step3: { on: { SUBMIT: 'complete' } } } }); const formStore = createStore({ state: { step1Data: {}, step2Data: {}, step3Data: {} }, actions: { updateStep1: (state, data) => ({ step1Data: { ...state.step1Data, ...data } }) } }); connectStoreToFSM(formStore, formFSM); ``` ### SPAs with FSM Router ```javascript // Define routes const routes = [ { name: 'home', url: '/examples/home', template: '<h1>Home</h1>' }, { name: 'about', url: '/about', template: '<h1>About</h1>' }, { name: 'contact', url: '/contact', template: '<h1>Contact</h1>' } ]; const { fsm } = createRouterFSM(routes); connectFSMtoDOM(fsm, document.getElementById('app'), routes); ``` ## Troubleshooting ### "Component not found in library" - Component must be registered in `atoms/index.js`, `molecules/index.js`, or `organisms/index.js` - Check component name matches exactly ### Tests failing - String comparison is exact (whitespace matters) - Check expected output format carefully ### Build fails - Check `route.components` is array (or function returning array for dynamic routes) - Verify all required props are provided ### Client-side components not working - Check if b0nes.js is loaded (`meta.interactive !== false`) - Verify component has `data-b0nes` attribute - Check browser console for initialization errors ### FSM not rendering anything - FSM is just state management - you must provide a render function - Use `createRouterFSM` helper or implement your own onEntry actions - Templates must be provided in route definitions ## Code Style - Use JSDoc for documentation - Follow atomic design principles - Keep components pure (no side effects) - Validate props at component start - Use `processSlotTrusted()` for component content - Use `normalizeClasses()` for class handling - Return template literals for HTML ## What b0nes Is Complete web development toolkit with zero dependencies SSG/SSR framework with built-in state management Component composition system (atomic design) Progressive enhancement with client-side runtime State machines for flow control AND SPA routing For content-heavy sites and web applications ## What b0nes Is NOT Not a CSS framework (bring your own styles) Not trying to replace React/Vue for complex SPAs Not for real-time applications Not opinionated about styling ## Version Info Current: v0.2.0 Node: >=20.0.0 License: MIT ## Links - GitHub: https://github.com/iggydotdev/b0nes - Issues: https://github.com/iggydotdev/b0nes/issues - npm: https://www.npmjs.com/package/b0nes --- **When helping users with b0nes:** 1. It's a complete toolkit (components + routing + state + FSM) 2. Zero dependencies = suggest pure JS solutions only 3. Components return HTML strings, not JSX 4. FSM for flow control AND SPA routing, Store for data management 5. No CSS included - users choose their own strategy 6. Progressive enhancement pattern (works without JS, better with JS) 7. Point to component generator for new components 8. Encourage atomic design patterns (atoms molecules organisms) 9. FSM needs render functions to actually display UI - it's not magic! 10. FSM Router combines state machines with actual URL/template rendering