# 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: 'Click Me '
```
### 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 1 Item 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 Next ",
onEnter: (context, data) => console.log('Entered start'),
onExit: (context, data) => console.log('Exiting start')
},
{
name: 'step2',
url: '/demo/fsm/step2',
template: "Step 2 Back "
},
{
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 Next "
},
{
name: 'step2',
url: '/form/step2',
template: "Step 2 Back Submit "
},
{
name: 'success',
url: '/form/success',
template: "Success! Reset "
}
];
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('Click ');
// Returns: 'Click '
```
### 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