Visual reference for every component in the library. Each section shows the DOM structure, CSS classes, and how the JS factory wires them together.
Sections in this document follow a simple staging rule:
- Shipped: backed by the current JavaScript API in
js-src/and safe to document as supported behavior. - Planned: useful design or styling direction, but not part of the supported public API yet.
+------------------------------------------------------------------------+
| .sf-header (sticky, 60px, emerald gradient) |
| [logo] [title / subtitle] [nav tabs...] [Solve][Pause][Ana] |
+------------------------------------------------------------------------+
| .sf-statusbar (score | constraint dots | moves/s | status) |
+------------------------------------------------------------------------+
| |
| .sf-main |
| |
| .sf-tab-panel.active |
| +------------------------------------------------------------------+|
| | ||
| | (application content) ||
| | ||
| +------------------------------------------------------------------+|
| |
+------------------------------------------------------------------------+
| .sf-footer (links | version) |
+------------------------------------------------------------------------+
Wrap the page in <body class="sf-app"> to get the flex column layout,
sticky header, and scrollable main area.
+------------------------------------------------------------------------+
| ┌──┐ |
| │🐍│ Furnace Scheduler [Forni] [Ordini] [Gantt] [▶ Solve][❚❚][◉] |
| └──┘ by SolverForge ←─ nav tabs ──→ ←─ actions ──→ |
+------------------------------------------------------------------------+
↑ ↑ ↑
.sf-header-logo .sf-header-brand .sf-header-actions
44×44, white filter .sf-header-title (18px, white) .sf-btn--success (Solve)
.sf-header-subtitle (12px, mono) .sf-btn--default (Pause, hidden)
.sf-btn--danger (Stop, lifecycle visible)
.sf-btn--ghost (Analyze, circle)
JS: SF.createHeader({ logo, title, subtitle, tabs[], onTabChange, actions: { onSolve, onPause, onResume, onCancel, onAnalyze } })
Nav buttons (.sf-nav-btn): semi-transparent white, .active state toggles on click.
+------------------------------------------------------------------------+
| 0hard/-42soft | ●●●●○○●●● | 12,400 moves/s | Solving... |
| ↑ ↑ ↑ ↑ |
| .sf-statusbar- .sf-constraint- moves display status text |
| score dot |
| (green/yellow/ |
| red by score) green = OK |
| red = hard violated (pulses) |
| amber = soft violated (pulses) |
+------------------------------------------------------------------------+
JS: SF.createStatusBar({ header?, constraints[], onConstraintClick })
Returns: { el, bindHeader(header), updateScore(str), setLifecycleState(state), updateMoves(n), colorDotsFromAnalysis(arr) }
Pass header or call bindHeader(header) when the status bar should control a
specific header's lifecycle controls and spinner state. Without a bound header,
setLifecycleState() only updates the status text and moves display.
Variants:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ ▶ Solve │ │ ■ Stop │ │ Primary │ │ Default │ │ Ghost │
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
--success --danger --primary --default --ghost
white bg red-600 bg emerald-700 gray border transparent
emerald text white text white text gray text white text
Sizes: Shapes:
┌────┐ ┌────────┐ ┌──────────┐ ┌────────────────┐ ┌──┐
│ sm │ │ medium │ │ large │ │ pill (rounded) │ │◉ │ circle
└────┘ └────────┘ └──────────┘ └────────────────┘ └──┘
Modifiers:
┌ ─ ─ ─ ─ ─ ─┐ --outline: border only, transparent bg
│ Outlined │ combinable with any variant
└ ─ ─ ─ ─ ─ ─┘
JS: SF.createButton({ text, variant, size, icon, pill, circle, outline, disabled, onClick })
┌────────────────────────────────────────────┐
│ ░░░░░░░░░░ emerald gradient ░░░░░░░░░░ ×│ .sf-modal-header
│ Score Analysis │ .sf-modal-title
├────────────────────────────────────────────┤
│ │
│ .sf-modal-body (scrollable) │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ constraint table / analysis / form │ │
│ └──────────────────────────────────────┘ │
│ │
├────────────────────────────────────────────┤
│ [Close] │ .sf-modal-footer
└────────────────────────────────────────────┘
Backdrop: .sf-modal-overlay (dark, blur)
Animation: scale(0.95) → scale(1), 180ms
JS: SF.createModal({ title, body, footer[], width?, onClose? })
Returns: { el, body, open(), close(), setBody(content) }
┌────────────────────────────────────────────────────────┐
│ NAME │ PROCESS │ PRIORITY │ DUE │ SCORE│ .sf-table th
├──────────────┼────────────┼──────────┼──────────┼──────┤
│ Order #142 │ ┌────────┐ │ ┌──────┐ │ Mon 14:00│ -200 │
│ │ │Tempra │ │ │ HIGH │ │ │ │
│ │ └────────┘ │ └──────┘ │ │ │
├──────────────┼────────────┼──────────┼──────────┼──────┤
│ Order #143 │ ┌────────┐ │ ┌──────┐ │ Tue 09:00│ 0 │
│ │ │Ricott. │ │ │ LOW │ │ │ │ hover: gray-50
│ │ └────────┘ │ └──────┘ │ │ │
└──────────────┴────────────┴──────────┴──────────┴──────┘
.sf-badge .sf-badge
--process --high / --medium / --low
JS: SF.createTable({ columns[], rows[], onRowClick? })
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Tempra │ │ HIGH │ │ HARD │
│ (process) │ │ (priority)│ │ (constraint)│
└────────────┘ └────────────┘ └────────────┘
colored bg/ red bg/text red border
border per amber bg/text amber border
process type green bg/text
┌─────────────┐
│ Forklift │ .sf-badge--skill
│ (skill) │ emerald-50 bg, emerald-700 text
└─────────────┘
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 154 │ │ 3 │ │ 96% │ │ 1,240 │
│ ORDERS │ │ LATE │ │ ON-TIME │ │ WEIGHT │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
.sf-kpi-card .sf-kpi-value.danger
gray-50 bg red for danger
amber for warn
emerald for ok
Wrapped in .sf-kpi-row (flex, gap).
┌──────────────────────────┐
│ ┃ Error × │
│ ┃ Connection refused │
│ ┃ 404: Not Found │
└──────────────────────────┘
↑
.sf-toast--danger
red left border
top-right fixed position
auto-dismiss 10s
Variants: --danger (red), --success (emerald), --warning (amber)
JS: SF.showToast({ title, message, detail?, variant, delay? })
Shorthand: SF.showError(title, detail)
┌─────────────────────┐
│ ODL-2847 │ .sf-tooltip-title
│ Process Tempra │ .sf-tooltip-row
│ Temp 850°C │ .sf-tooltip-key / .sf-tooltip-val
│ Weight 120 kg │
│ Due Mon 14h │
│ ┌───────────────┐ │
│ │ LATE: +45 min │ │ late badge
│ └───────────────┘ │
└─────────────────────┘
fixed position, shadow-xl
opacity transition 150ms
+------------------------------------------------------------------------+
| SolverForge │ Documentation │ GitHub v0.6.1 |
| ↑ links with hover emerald ↑ right-aligned |
+------------------------------------------------------------------------+
JS: SF.createFooter({ links: [{label, url}], version })
┌─────────────────────────────────────────────┐
│ GET /jobs/{id}/snapshot │ .sf-api-section h3
│ Get the latest or requested retained │ (emerald-700 text)
│ snapshot for a job. │
│ ┌───────────────────────────────────┬────┐ │
│ │ curl localhost:7860/jobs/abc/ │Copy│ │ .sf-api-code-block
│ │ snapshot?snapshot_revision=4 │ │ │
│ └───────────────────────────────────┴────┘ │ .sf-copy-btn
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ POST /jobs/{id}/pause │
│ Request an exact runtime-managed pause. │
│ ┌───────────────────────────────────┬────┐ │
│ │ curl -X POST localhost:7860/jobs/ │Copy│ │
│ │ abc/pause │ │ │
│ └───────────────────────────────────┴────┘ │
└─────────────────────────────────────────────┘
JS: SF.createApiGuide({ endpoints: [{ method, path, description, curl }] })
start()
│
├── "progress" → scored metadata update only
│
└── "best_solution" → scored live snapshot update
Startup rule:
the first lifecycle event may be either branch above, but it must already
carry score data.
Retained bootstrap rule:
if startup state is restored from a retained snapshot, do not follow that
bootstrap with an identical duplicate startup "best_solution".
Shipped runtime expectations:
progressupdates score, telemetry, and status only.best_solutionupdates score, telemetry, and the live solution view.best_solutionalways includessolutionplussnapshotRevision.paused,completed,cancelled, andfailedremain authoritative and trigger retained snapshot synchronization before downstream callbacks fire.- HTTP
EventSource.onerroris transport state. Reconnecting errors stay local to the browser transport; a closed stream surfaces throughonErrorand preserves the last authoritative lifecycle, retained job id, score, metadata, and snapshot revision. start()never replaces a retained job. Terminal retained jobs require successfuldelete()cleanup before another solve can start.delete()waits for terminal synchronization, calls backenddeleteJob(), and clears local retained state only after required terminal synchronization and backend cleanup both succeed.COMPLETEDandTERMINATED_BY_CONFIGrequire successful terminal snapshot sync before backend deletion.- Stop remains visible during
CANCELLING, but that state blocks duplicate cancel commands. Activating Stop duringCANCELLINGmay reattach a detached stream listen-only so the UI can observe the terminal event.
.sf-rail-timeline
┌─────────────────────────────────────────────────────────────────────────────┐
│ [1W] [2W] [4W] [Reset] drag to pan · read only │
├───────────────┬─────────────────────────────────────────────────────────────┤
│ Staffing lane │ Mon 20 Tue 21 Wed 22 Thu 23 Fri 24 │ sticky top time header
│ │ 00 06 12 18 00 06 12 18 00 06 12 18 00 06 12 18 ... │ 6-hour tick marks
├───────────────┼─────────────────────────────────────────────────────────────┤
│ Ward East │ ███ summary ███ █ cardio block █ │ overview lane
│ Coverage 92% │ count/open chips + tone mix bar keep dense windows legible │
├───────────────┼─────────────────────────────────────────────────────────────┤
│ Ward West │ ███ summary ███ █ handoff █ │ overview lane
│ Coverage 88% │ overlays stay behind the schedule; focus reveals tooltip │
├───────────────┼─────────────────────────────────────────────────────────────┤
│ Ada │ █ primary █ │
│ Hours 38h │ █ overlap █ │ detailed lane
│ │ track 0 track 1 │ interval packed
├───────────────┼─────────────────────────────────────────────────────────────┤
│ Marco │ █ primary █ │ detailed lane
│ Hours 42h │ █ late coverage █ │
└───────────────┴─────────────────────────────────────────────────────────────┘
sticky left lane labels
hidden native scrollbar
weekend shading behind the axis
JS:
var timeline = SF.rail.createTimeline({
label: 'Staffing lane',
labelWidth: 280,
model: {
axis: {
startMinute: 0,
endMinute: 28 * 1440,
days: buildDays(28),
ticks: buildSixHourTicks(28),
initialViewport: { startMinute: 0, endMinute: 14 * 1440 },
},
lanes: [
{
id: 'ward-east',
label: 'By location · Ward East',
mode: 'overview',
overlays: [{ dayIndex: 5, label: 'Unavailable', tone: 'red' }],
items: [
{
id: 'east-rush',
clusterId: 'east-rush',
startMinute: 360,
endMinute: 1080,
label: 'Monday intake surge',
tone: 'blue',
summary: {
primaryLabel: 'Monday intake surge',
secondaryLabel: 'ER intake · trauma hold · overflow beds',
count: 24,
openCount: 3,
toneSegments: [
{ tone: 'blue', count: 15 },
{ tone: 'amber', count: 6 },
{ tone: 'rose', count: 3 },
],
},
detailItems: [
{ id: 'east-1', startMinute: 360, endMinute: 840, label: 'ER intake', tone: 'blue' },
{ id: 'east-2', startMinute: 420, endMinute: 960, label: 'Trauma hold', tone: 'amber' },
{ id: 'east-3', startMinute: 480, endMinute: 1080, label: 'Overflow beds', tone: 'rose' },
],
},
],
},
{
id: 'employee-ada',
label: 'By employee · Ada',
mode: 'detailed',
items: [
{ id: 'ada-1', startMinute: 2 * 1440 + 360, endMinute: 2 * 1440 + 840, label: 'Primary shift', tone: 'amber' },
{ id: 'ada-2', startMinute: 2 * 1440 + 660, endMinute: 2 * 1440 + 1020, label: 'Handoff overlap', tone: 'amber' },
],
},
],
},
});
timeline.setViewport({ startMinute: 7 * 1440, endMinute: 21 * 1440 });
timeline.expandCluster('ward-east', 'east-rush');
The original createHeader/createCard/addBlock/addChangeover APIs remain
shipped as low-level primitives, but they are no longer the recommended
integration path for dense scheduling UIs.
Shipped dense overview rules:
- overview lanes are for scanability, not raw-label dumps
- additive
summarymetadata can provide explicit headline, count, open state, and tone mix - mixed summarized/raw groups combine explicit summary fields with derived count/open/tone data from the other grouped items
- summary items that override aggregate count must also provide open/tone aggregate data if those signals should be shown
- if
summaryis omitted, the timeline computes a default aggregate summary from grouped detail items - expanded overview clusters keep the aggregate block visible as the collapse affordance
- focus and hover expose the same tooltip content for overview and detailed blocks
.sf-gantt-split
┌──────────────────────────────────────────────────────────────────┐
│ .sf-gantt-pane (grid) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Tasks │ │ .sf-gantt-pane-header
│ ├────────────────────────────────────────────────────────────┤ │
│ │ TASK │ START │ P │ │ │ .sf-gantt-table th
│ ├───────────────────────────────────────────────────────────│ │
│ │▌ Design review │ 09:00 │ P1 │ │ │ .sf-gantt-row
│ │▌ Implementation │ 10:30 │ P2 │ │ │ left border = custom_class
│ │▌ Testing │ 14:00 │ P3 │ │ │ sweep underline on hover
│ └────────────────────────────────────────────────────────────┘ │
├══════════════════════════════════════════════════════════════════┤ ← Split.js gutter (4px, emerald on hover)
│ .sf-gantt-pane (chart) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Schedule [Quarter Day ▾] │ │ .sf-gantt-pane-header
│ ├────────────────────────────────────────────────────────────┤ │
│ │ Frappe Gantt SVG │ │
│ │ ┌───────────────┬──────────────────────────┬─────────┐ │ │
│ │ │ 09:00 10:00 │ 11:00 12:00 13:00 │ 14:00 │ │ │
│ │ ├───────────────┼──────────────────────────┼─────────┤ │ │
│ │ │ ████████████ ┼──→ ██████████████████████│ │ │ │ bars colored by custom_class
│ │ │ Design │ Implementation │ │ │ │ arrows = dependencies
│ │ │ │ │ ████████│ │ │
│ │ │ │ │ Testing │ │ │
│ │ └───────────────┴──────────────────────────┴─────────┘ │ │
│ │ │ │
│ │ hover: brightness(1.12) + drop-shadow │ │
│ │ drag: grab cursor │ │
│ │ resize: ew-resize handles appear on hover │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
JS:
var gantt = SF.gantt.create({
gridTitle: 'Tasks', chartTitle: 'Schedule',
viewMode: 'Quarter Day',
columns: [
{ key: 'name', label: 'Task' },
{ key: 'start', label: 'Start' },
{ key: 'priority', label: 'P', render: fn },
],
onTaskClick: function (task) { ... },
onDateChange: function (task, start, end) { ... },
});
gantt.mount('container');
gantt.setTasks(taskArray);
gantt.changeViewMode('Day');
gantt.highlightTask('task-1');
Requires: /sf/vendor/frappe-gantt/ + /sf/vendor/split/
These helpers remain shipped for custom primitive rail compositions:
.sf-heatmap/.sf-heatmap-segmentviaSF.rail.createHeatmap(config).sf-unassigned-pillviaSF.rail.createUnassignedRail(tasks, onTaskClick)orcard.setUnassigned(items)
Treat them as low-level add-ons, not the canonical dense scheduling entrypoint.
┌─────────────────────────────────────────────────┐
│ │
│ 🏠 depot (sf-marker-vehicle) │
│ ╲ │
│ ╲── route polyline (color per vehicle)│
│ ╲ │
│ ①───②───③ (sf-marker-stop, numbered) │
│ │ │
│ 🍴 visit (sf-marker-visit, icon per type) │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Click to add new visit │ │ .sf-map-hint
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
JS:
var map = SF.map.create({ container, center, zoom });
map.addVehicleMarker({ lat, lng, color });
map.addVisitMarker({ lat, lng, color, icon, assigned });
map.drawRoute({ points, color });
map.drawEncodedRoute({ encoded, color });
map.highlight(vehicleColor);
┌──────────────────────────────────────────────────┐
│ ░░░ Score Analysis ░░░░░░░░░░░░░░░░░░░░░░░░ ×│
├──────────────────────────────────────────────────┤
│ │
│ ● Constraint Type # Score │ .sf-ov-table
│ ───────────────────────────────────────── │
│ 🔴 Unassigned HARD 3 -600 │ .sf-ov-row-violated
│ 🔴 Temp Exceeded HARD 1 -150 │
│ 🟢 Process Compat HARD 0 0 │
│ 🟡 Late Delivery SOFT 12 -840 │
│ 🟡 Changeover Cost SOFT 8 -240 │
│ 🟢 Shift Balance SOFT 0 0 │
│ │
│ Click a row to see individual violations → │
│ │
├──────────────────────────────────────────────────┤
│ [Close] │
└──────────────────────────────────────────────────┘
Emerald (Primary) Semantic
┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐ ┌──────────────────┐
│50││100│200│300│400│500│600│ │ success = 500 │
└──┘└──┘└──┘└──┘└──┘└──┘└──┘ │ primary = 700 │
light ──────────────────→ dark │ danger = red-600│
│ warning = amb-500│
Project Colors (8) └──────────────────┘
┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐
│Em││Bl││Pu││Am││Pk││Cy││Ro││Li│
└──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘
#10b981 #3b82f6 #8b5cf6 #f59e0b ...
Fonts
────────────────────────────────
Body: Space Grotesk (400–700)
Mono: JetBrains Mono (400–700)