# solverforge-ui
Frontend component library for [SolverForge](https://solverforge.org)
constraint-optimization applications. Emerald-themed, zero-framework,
vendor-ready. One line to mount, zero npm, zero webpack.
```rust
// Cargo.toml
solverforge-ui = { path = "../solverforge-ui" }
// main.rs
let app = api::router(state)
.merge(solverforge_ui::routes()) // serves /sf/*
.fallback_service(ServeDir::new("static"));
```
```html
<link rel="stylesheet" href="/sf/vendor/fontawesome/css/fontawesome.min.css">
<link rel="stylesheet" href="/sf/vendor/fontawesome/css/solid.min.css">
<link rel="stylesheet" href="/sf/sf.css">
<script src="/sf/sf.js"></script>
```
That's it. Every asset is compiled into the binary via `include_dir!`.
## Screenshots
**Planner123** — Gantt chart with dependency arrows, project-colored bars, and constraint scoring:

**Furnace Scheduler** — Timeline rail with resource cards, temperature/load gauges, and positioned job blocks:

## Philosophy
Every backend element has a corresponding UI element. The library grows
alongside the solver. When you scaffold a new SolverForge project with
`solverforge new`, it's already wired in.
## Quick Start
```html
<body class="sf-app">
<script>
var backend = SF.createBackend({ type: 'axum' });
var header = SF.createHeader({
logo: '/sf/img/ouroboros.svg',
title: 'My Scheduler',
subtitle: 'by SolverForge',
tabs: [
{ id: 'plan', label: 'Plan', icon: 'fa-list-check', active: true },
{ id: 'gantt', label: 'Gantt', icon: 'fa-chart-gantt' },
],
onTabChange: function (id) { SF.showTab(id); },
actions: {
onSolve: function () { solver.start(); },
onStop: function () { solver.stop(); },
},
});
document.body.prepend(header);
var bar = SF.createStatusBar({ constraints: myConstraints });
header.after(bar.el);
var solver = SF.createSolver({
backend: backend,
statusBar: bar,
onUpdate: function (schedule) { render(schedule); },
});
</script>
</body>
```
## API Reference
### Components
| `SF.createHeader(config)` | `HTMLElement` | Sticky header with logo, title, nav tabs, solve/stop/analyze buttons |
| `SF.createStatusBar(config)` | `{el, updateScore, setSolving, updateMoves, colorDotsFromAnalysis}` | Score display + constraint dot indicators |
| `SF.createButton(config)` | `HTMLButtonElement` | Button with variant/size/icon/shape modifiers |
| `SF.createModal(config)` | `{el, body, open, close, setBody}` | Dialog with emerald gradient header, backdrop, Escape key |
| `SF.createTable(config)` | `HTMLElement` | Data table with headers and row click |
| `SF.createTabs(config)` | `{el, show}` | Tab panel container |
| `SF.createFooter(config)` | `HTMLElement` | Footer with links and version |
| `SF.createApiGuide(config)` | `HTMLElement` | REST API documentation panel |
| `SF.showToast(config)` | `void` | Toast notification (auto-dismiss) |
| `SF.showError(title, detail)` | `void` | Danger toast shorthand |
| `SF.showTab(tabId)` | `void` | Activate a tab panel by ID |
### Timeline Rail
| `SF.rail.createHeader(config)` | `HTMLElement` | Day/period column header above resource cards |
| `SF.rail.createCard(config)` | `{el, rail, addBlock, clearBlocks, setSolving}` | Resource lane with identity, gauges, stats, and block rail |
| `SF.rail.addBlock(rail, config)` | `HTMLElement` | Positioned block (task/job) inside a rail |
| `SF.rail.addChangeover(rail, config)` | `HTMLElement` | Diagonal-striped gap between blocks |
### Gantt (Frappe Gantt)
| `SF.gantt.create(config)` | `{el, mount, setTasks, refresh, changeViewMode, highlightTask, destroy}` | Split-pane Gantt with grid table + Frappe Gantt chart |
### Solver Lifecycle
| `SF.createBackend(config)` | Backend adapter | HTTP or Tauri IPC transport |
| `SF.createSolver(config)` | `{start, stop, isRunning, getJobId}` | SSE state machine with auto status bar updates |
### Utilities
| `SF.score.parseHard(str)` | Extract hard score from `"0hard/-42soft"` |
| `SF.score.parseSoft(str)` | Extract soft score |
| `SF.score.parseMedium(str)` | Extract medium score |
| `SF.score.getComponents(str)` | `{hard, medium, soft}` |
| `SF.score.colorClass(str)` | `"score-green"` / `"score-yellow"` / `"score-red"` |
| `SF.colors.pick(key)` | Tango palette color for any key (cached) |
| `SF.colors.project(index)` | `{main, dark, light}` from 8-color project palette |
| `SF.colors.reset()` | Clear the color cache |
| `SF.escHtml(str)` | HTML-escape a string |
| `SF.el(tag, attrs, ...children)` | DOM element factory |
## Button Variants
```javascript
SF.createButton({ text: 'Solve', variant: 'success' }) // white bg, emerald text
SF.createButton({ text: 'Stop', variant: 'danger' }) // red bg, white text
SF.createButton({ text: 'Save', variant: 'primary' }) // emerald-700 bg
SF.createButton({ text: 'Cancel', variant: 'default' }) // gray border
SF.createButton({ icon: 'fa-gear', variant: 'ghost', circle: true })
SF.createButton({ text: 'Submit', variant: 'primary', pill: true })
SF.createButton({ text: 'Delete', variant: 'danger', outline: true })
SF.createButton({ text: 'Sm', variant: 'primary', size: 'small' })
```
## Timeline Rail
The scheduling hero view. Resource lanes with positioned task blocks,
gauges, stats, heatmaps, and changeover indicators.
```javascript
// Day header
var header = SF.rail.createHeader({
label: 'Resource',
labelWidth: 200,
columns: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
});
container.appendChild(header);
// One card per resource (furnace, vehicle, employee, machine...)
var card = SF.rail.createCard({
id: 'furnace-1',
name: 'FORNO 1',
labelWidth: 200,
columns: 5,
type: 'CAMERA',
typeStyle: { bg: 'rgba(59,130,246,0.15)', color: '#1d4ed8', border: '1px solid rgba(59,130,246,0.3)' },
gauges: [
{ label: 'Temp', pct: 85, style: 'heat', text: '850/1000°C' },
{ label: 'Load', pct: 60, style: 'load', text: '120/200 kg' },
],
stats: [
{ label: 'Jobs', value: 12 },
{ label: 'Production', value: '840 kg' },
],
});
container.appendChild(card.el);
// Add task blocks (positioned by start/end within horizon)
card.addBlock({
start: 120, // minutes from horizon start
end: 360,
horizon: 4800, // total horizon in same units
label: 'ODL-2847',
meta: 'Bianchi',
color: 'rgba(59,130,246,0.6)',
borderColor: '#3b82f6',
late: false,
onHover: function (e, cfg) { /* show tooltip */ },
});
// Changeover gap between blocks
SF.rail.addChangeover(card.rail, { start: 360, end: 400, horizon: 4800 });
// Solving state (breathing emerald glow)
card.setSolving(true);
```
Gauge styles: `heat` (blue→amber→red), `load` (emerald→amber→red), `emerald` (solid green).
## Gantt Chart
Interactive task scheduling with Frappe Gantt. Split-pane layout: task grid
on top, SVG timeline chart on bottom. Drag to reschedule, resize to change
duration, project-colored bars, dependency arrows.
```html
<link rel="stylesheet" href="/sf/vendor/frappe-gantt/frappe-gantt.min.css">
<script src="/sf/vendor/frappe-gantt/frappe-gantt.min.js"></script>
<script src="/sf/vendor/split/split.min.js"></script>
```
```javascript
var gantt = SF.gantt.create({
gridTitle: 'Tasks',
chartTitle: 'Schedule',
viewMode: 'Quarter Day',
splitSizes: [40, 60],
columns: [
{ key: 'name', label: 'Task' },
{ key: 'start', label: 'Start' },
{ key: 'end', label: 'End' },
{ key: 'priority', label: 'P', render: function (t) {
return '<span class="sf-priority-badge priority-' + t.priority + '">P' + t.priority + '</span>';
}},
],
onTaskClick: function (task) { console.log('clicked', task.id); },
onDateChange: function (task, start, end) { console.log('moved', task.id, start, end); },
});
gantt.mount('my-container');
gantt.setTasks([
{
id: 'task-1',
name: 'Design review',
start: '2026-03-15 09:00',
end: '2026-03-15 10:30',
priority: 1,
custom_class: 'project-color-0 priority-1',
dependencies: '',
},
{
id: 'task-2',
name: 'Implementation',
start: '2026-03-15 10:30',
end: '2026-03-15 14:00',
priority: 2,
custom_class: 'project-color-0 priority-2',
dependencies: 'task-1',
},
]);
gantt.changeViewMode('Day');
gantt.highlightTask('task-1');
```
View modes: `Quarter Day`, `Half Day`, `Day`, `Week`, `Month`.
## Backend Adapters
### Axum (default)
```javascript
var backend = SF.createBackend({ type: 'axum', baseUrl: '' });
```
Expects standard SolverForge REST endpoints:
- `POST /schedules` — start solving
- `GET /schedules/{id}` — get solution
- `GET /schedules/{id}/events` — SSE stream
- `GET /schedules/{id}/analyze` — constraint analysis
- `DELETE /schedules/{id}` — stop solving
- `GET /demo-data/{name}` — load demo dataset
### Tauri
```javascript
var backend = SF.createBackend({
type: 'tauri',
invoke: window.__TAURI__.core.invoke,
listen: window.__TAURI__.event.listen,
eventName: 'solver-update',
});
```
### Generic fetch (Rails, etc.)
```javascript
var backend = SF.createBackend({
type: 'fetch',
baseUrl: '/api/v1',
headers: { 'X-CSRF-Token': csrfToken },
});
```
## Optional Modules
### Map (Leaflet)
```html
<link rel="stylesheet" href="/sf/vendor/leaflet/leaflet.css">
<script src="/sf/vendor/leaflet/leaflet.js"></script>
<link rel="stylesheet" href="/sf/modules/sf-map.css">
<script src="/sf/modules/sf-map.js"></script>
```
```javascript
var map = SF.map.create({ container: 'map', center: [45.07, 7.69], zoom: 13 });
map.addVehicleMarker({ lat: 45.07, lng: 7.69, color: '#10b981' });
map.addVisitMarker({ lat: 45.08, lng: 7.70, color: '#3b82f6', icon: 'fa-utensils' });
map.drawRoute({ points: [[45.07, 7.69], [45.08, 7.70]], color: '#10b981' });
map.drawEncodedRoute({ encoded: 'encodedPolylineString', color: '#3b82f6' });
map.fitBounds();
map.highlight('#10b981'); // dim all routes except this color
map.clearHighlight();
SF.map.decodePolyline('_p~iF~ps|U...'); // Google polyline algorithm
```
## Design System
### Colors
| `--sf-emerald-500` | `#10b981` | Primary brand, success states |
| `--sf-emerald-600` | `#059669` | Primary dark |
| `--sf-emerald-700` | `#047857` | Primary buttons, links |
| `--sf-red-600` | `#dc2626` | Danger buttons, hard violations |
| `--sf-amber-500` | `#f59e0b` | Warnings, soft violations |
| `--sf-gray-50` | `#f9fafb` | Backgrounds |
| `--sf-gray-900` | `#111827` | Primary text |
8 project colors for assignment: emerald, blue, purple, amber, pink, cyan, rose, lime.
### Fonts
- **Space Grotesk** (body, headings) — variable weight 300-700, self-hosted WOFF2
- **JetBrains Mono** (code, scores, data) — variable weight 100-800, self-hosted WOFF2
### Spacing
`--sf-space-{0,1,2,3,4,5,6,8,10,12,16}` — 0 to 4rem in quarter-rem increments.
### Shadows
`--sf-shadow-{sm,base,md,lg,xl,2xl}` — elevation scale.
`--sf-shadow-emerald` — colored shadow for branded elements.
### Animations
`sf-spin` / `sf-dot-pulse` / `sf-score-flash` / `sf-dialog-slide-in` / `sf-breathe` / `sf-slide-in` / `sf-fade-in` / `sf-late-glow`
## Project Structure
```
solverforge-ui/
├── Cargo.toml # 2 deps: axum + include_dir
├── src/lib.rs # routes() + asset serving
├── Makefile # make → cats css-src/ + js-src/ into sf.css + sf.js
├── css-src/ # 16 CSS source files (numbered for concat order)
│ ├── 00-tokens.css # design system variables
│ ├── 01-reset.css # box-sizing reset
│ ├── 02-typography.css # @font-face declarations
│ ├── 03-layout.css # .sf-app, .sf-main, tab panels
│ ├── 04-header.css # .sf-header
│ ├── 05-statusbar.css # .sf-statusbar + constraint dots
│ ├── 06-buttons.css # .sf-btn variants
│ ├── 07-modal.css # .sf-modal
│ ├── 08-table.css # .sf-table + constraint analysis table
│ ├── 09-badges.css # .sf-badge variants
│ ├── 10-cards.css # .sf-card, .sf-kpi-card
│ ├── 11-tooltip.css # .sf-tooltip
│ ├── 12-footer.css # .sf-footer
│ ├── 13-scrollbars.css # custom webkit scrollbars
│ ├── 14-animations.css # @keyframes + toast + api guide
│ ├── 15-rail.css # timeline rail, resource cards, blocks
│ └── 16-gantt.css # Frappe Gantt + Split.js layout + bar styling
├── js-src/ # 15 JS source files
│ ├── 00-core.js # SF namespace, escHtml, el()
│ ├── 01-score.js # score parsing
│ ├── 02-colors.js # Tango palette + project colors
│ ├── 03-buttons.js # createButton()
│ ├── 04-header.js # createHeader()
│ ├── 05-statusbar.js # createStatusBar()
│ ├── 06-modal.js # createModal()
│ ├── 07-tabs.js # createTabs(), showTab()
│ ├── 08-table.js # createTable()
│ ├── 09-toast.js # showToast(), showError()
│ ├── 10-backend.js # createBackend() — axum/tauri/fetch
│ ├── 11-solver.js # createSolver() — SSE state machine
│ ├── 12-api-guide.js # createApiGuide(), createFooter()
│ ├── 13-rail.js # timeline rail, resource cards, blocks
│ └── 14-gantt.js # Frappe Gantt wrapper (split pane, grid, chart)
└── static/sf/ # Embedded assets (include_dir!)
├── sf.css # concatenated from css-src/
├── sf.js # concatenated from js-src/
├── img/ # SVG logos (ouroboros, favicon, brand)
├── fonts/ # Space Grotesk + JetBrains Mono WOFF2
├── modules/ # optional: sf-map.js/css
└── vendor/ # FontAwesome 6.5, Leaflet 1.9, Frappe Gantt, Split.js
```
## Integration Paths
| **Axum** | Add crate dep, call `.merge(solverforge_ui::routes())` |
| **Tauri** | Add crate dep, serve via Tauri's asset protocol or custom command |
| **Rails** | Copy `static/sf/` into `public/sf/`, reference in layouts |
| **Any HTTP server** | Copy `static/sf/`, serve as static files |
| **`solverforge new`** | Automatic — wired into generated project |
## Non-Rust Projects
The `static/sf/` directory is self-contained. Copy it, git-submodule it,
or symlink it into any project that serves static files:
```bash
# git submodule
git submodule add https://github.com/solverforge/solverforge-ui vendor/solverforge-ui
ln -s vendor/solverforge-ui/static/sf public/sf
```
## Development
```bash
# Edit source files
vim css-src/06-buttons.css
vim js-src/03-buttons.js
# Rebuild concatenated files
make
# Compile the crate (embeds updated assets)
cargo build
```
## Acknowledgments
solverforge-ui builds on these excellent open-source projects:
| [Font Awesome Free](https://fontawesome.com) | Icons (Solid subset) | CC BY 4.0 (icons), SIL OFL (fonts), MIT (code) | [github](https://github.com/FortAwesome/Font-Awesome) |
| [Frappe Gantt](https://frappe.io/gantt) | Interactive Gantt chart | MIT | [github](https://github.com/frappe/gantt) |
| [Split.js](https://split.js.org) | Resizable split panes | MIT | [github](https://github.com/nathancahill/split) |
| [Leaflet](https://leafletjs.com) | Interactive maps (optional module) | BSD-2-Clause | [github](https://github.com/Leaflet/Leaflet) |
| [Space Grotesk](https://fonts.google.com/specimen/Space+Grotesk) | Body typeface | SIL Open Font License 1.1 | [github](https://github.com/floriankarsten/space-grotesk) |
| [JetBrains Mono](https://www.jetbrains.com/lp/mono/) | Monospace typeface | SIL Open Font License 1.1 | [github](https://github.com/JetBrains/JetBrainsMono) |
| [Axum](https://github.com/tokio-rs/axum) | Rust web framework | MIT | [github](https://github.com/tokio-rs/axum) |
| [include_dir](https://github.com/Michael-F-Bryan/include_dir) | Compile-time file embedding | MIT | [github](https://github.com/Michael-F-Bryan/include_dir) |
## License
Apache-2.0