# 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 `${slotContent}`; }; ``` ## 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: '' ``` ### 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: '

Content 1

' }, { label: 'Tab 2', content: '

Content 2

' } ] } } ``` **Modal** - Accessible dialog with focus management ```javascript // Modal component { type: 'molecule', name: 'modal', props: { id: 'my-modal', title: 'Title', slot: '

Content

' } } // 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: 'Item 1Item 2' } } ``` ### 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: "

FSM Demo

", onEnter: (context, data) => console.log('Entered start'), onExit: (context, data) => console.log('Exiting start') }, { name: 'step2', url: '/demo/fsm/step2', template: "

Step 2

" }, { name: 'success', url: '/demo/fsm/success', template: "

Success!

" } ]; 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: "

Start

" }, { name: 'step2', url: '/form/step2', template: "

Step 2

" }, { name: 'success', url: '/form/success', template: "

Success!

" } ]; 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 ", "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 = '
Test
'; 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(''); // Returns: '<script>alert(1)</script>' // Trusts HTML (for component content) const trusted = processSlotTrusted(''); // Returns: '' ``` ### 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: '

Home

' }, { name: 'about', url: '/about', template: '

About

' }, { name: 'contact', url: '/contact', template: '

Contact

' } ]; 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