Skip to content

Commit c8eabe6

Browse files
authored
Merge pull request #37 from SolverForge/issue-18-demos
Add runnable demo fixtures for the shipped UI surface
2 parents e32ed8b + 657ae9f commit c8eabe6

File tree

6 files changed

+500
-1
lines changed

6 files changed

+500
-1
lines changed

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ VERSIONED_JS := static/sf/sf.$(VERSION).js
2929
# ============== Phony Targets ==============
3030
.PHONY: banner help assets build build-release test test-quick test-doc test-unit test-one \
3131
lint fmt fmt-check clippy ci-local pre-release version package-verify \
32-
bump-patch bump-minor bump-major bump-dry \
32+
bump-patch bump-minor bump-major bump-dry demo-serve \
3333
publish-dry publish clean watch
3434

3535
# ============== Default Target ==============
@@ -251,12 +251,19 @@ watch:
251251
@printf "$(ARROW) Watching for changes...\n"
252252
@cargo watch -x build
253253

254+
demo-serve: assets
255+
@printf "$(ARROW) Serving demos at http://localhost:8000/demos/\n"
256+
@python3 -m http.server 8000
257+
254258
# ============== Help ==============
255259

256260
help: banner
257261
@/bin/echo -e "$(CYAN)$(BOLD)Asset Commands:$(RESET)"
258262
@/bin/echo -e " $(GREEN)make assets$(RESET) - Bundle CSS/JS from sources"
259263
@/bin/echo -e ""
264+
@/bin/echo -e "$(CYAN)$(BOLD)Demo Commands:$(RESET)"
265+
@/bin/echo -e " $(GREEN)make demo-serve$(RESET) - Serve the runnable demo fixtures locally"
266+
@/bin/echo -e ""
260267
@/bin/echo -e "$(CYAN)$(BOLD)Build Commands:$(RESET)"
261268
@/bin/echo -e " $(GREEN)make build$(RESET) - Bundle assets + build crate"
262269
@/bin/echo -e " $(GREEN)make build-release$(RESET) - Bundle assets + release build"

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,14 @@ Bundling writes both stable compatibility assets (`static/sf/sf.css`,
516516
`static/sf/sf.js`) and versioned assets (`static/sf/sf.<version>.css`,
517517
`static/sf/sf.<version>.js`).
518518

519+
## Demo Fixtures
520+
521+
Runnable demo fixtures live in `demos/`.
522+
523+
- `demos/full-surface.html` exercises the primary shipped component surface together.
524+
- `demos/rail.html` focuses on resource cards, blocks, gauges, and changeovers.
525+
- `make demo-serve` serves the repository at `http://localhost:8000/demos/` for local validation.
526+
519527
## Acknowledgments
520528

521529
solverforge-ui builds on these excellent open-source projects:

demos/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Demo Fixtures
2+
3+
These fixtures are intended to validate the shipped `solverforge-ui` surface locally.
4+
5+
## Start a local server
6+
7+
From the repository root:
8+
9+
```bash
10+
make demo-serve
11+
```
12+
13+
Then open:
14+
15+
- `http://localhost:8000/demos/`
16+
- `http://localhost:8000/demos/full-surface.html`
17+
- `http://localhost:8000/demos/rail.html`
18+
19+
## Coverage
20+
21+
- `full-surface.html`: header, status bar, tabs, buttons, modal, toast, table, rail, Gantt, API guide, and footer
22+
- `rail.html`: resource header, cards, gauges, blocks, and changeovers

demos/full-surface.html

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>solverforge-ui full surface demo</title>
7+
<link rel="stylesheet" href="../static/sf/vendor/fontawesome/css/fontawesome.min.css">
8+
<link rel="stylesheet" href="../static/sf/vendor/fontawesome/css/solid.min.css">
9+
<link rel="stylesheet" href="../static/sf/vendor/frappe-gantt/frappe-gantt.min.css">
10+
<link rel="stylesheet" href="../static/sf/sf.css">
11+
<script src="../static/sf/vendor/split/split.min.js"></script>
12+
<script src="../static/sf/vendor/frappe-gantt/frappe-gantt.min.js"></script>
13+
<script src="../static/sf/sf.js"></script>
14+
<style>
15+
body { margin: 0; }
16+
.demo-stack { display: grid; gap: 18px; }
17+
.demo-card { background: white; border: 1px solid var(--sf-gray-200); border-radius: 12px; box-shadow: var(--sf-shadow-base); padding: 18px; }
18+
.demo-card h2 { margin: 0 0 12px; }
19+
.demo-actions { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
20+
.demo-gantt { height: 520px; }
21+
.demo-footer-space { height: 32px; }
22+
</style>
23+
</head>
24+
<body class="sf-app">
25+
<script>
26+
var scoreModal;
27+
28+
function buildPlannerTable() {
29+
return SF.createTable({
30+
columns: [
31+
{ label: 'Task' },
32+
{ label: 'Machine' },
33+
{ label: 'Due', align: 'right' }
34+
],
35+
rows: [
36+
['ODL-2847', 'FORNO 1', 'Mon 14:00'],
37+
['ODL-3012', 'FORNO 2', 'Mon 17:30'],
38+
['ODL-1802', 'FORNO 1', 'Tue 09:15']
39+
],
40+
onRowClick: function (index, row) {
41+
SF.showToast({
42+
title: 'Row selected',
43+
message: row[0] + ' on ' + row[1],
44+
variant: 'success',
45+
delay: 2500
46+
});
47+
}
48+
});
49+
}
50+
51+
function buildRailCard() {
52+
var card = SF.rail.createCard({
53+
id: 'furnace-1',
54+
name: 'FORNO 1',
55+
labelWidth: 220,
56+
columns: 5,
57+
type: 'CAMERA',
58+
typeStyle: {
59+
bg: 'rgba(59,130,246,0.15)',
60+
color: '#1d4ed8',
61+
border: '1px solid rgba(59,130,246,0.30)'
62+
},
63+
gauges: [
64+
{ label: 'Temp', pct: 84, style: 'heat', text: '840 / 1000 C' },
65+
{ label: 'Load', pct: 63, style: 'load', text: '126 / 200 kg' }
66+
],
67+
stats: [
68+
{ label: 'Jobs', value: 12 },
69+
{ label: 'Prod', value: '840 kg' }
70+
]
71+
});
72+
73+
card.addBlock({
74+
start: 120,
75+
end: 340,
76+
horizon: 600,
77+
label: 'ODL-2847',
78+
meta: 'Bianchi',
79+
color: 'rgba(59,130,246,0.55)',
80+
borderColor: '#3b82f6'
81+
});
82+
83+
SF.rail.addChangeover(card.rail, {
84+
start: 340,
85+
end: 385,
86+
horizon: 600
87+
});
88+
89+
card.addBlock({
90+
start: 385,
91+
end: 560,
92+
horizon: 600,
93+
label: 'ODL-3012',
94+
meta: 'Rossi',
95+
color: 'rgba(16,185,129,0.55)',
96+
borderColor: '#10b981',
97+
late: true
98+
});
99+
100+
return card.el;
101+
}
102+
103+
function buildGantt() {
104+
var mount = document.createElement('div');
105+
mount.className = 'demo-gantt';
106+
107+
var gantt = SF.gantt.create({
108+
gridTitle: 'Tasks',
109+
chartTitle: 'Schedule',
110+
viewMode: 'Half Day',
111+
splitSizes: [38, 62],
112+
columns: [
113+
{ key: 'name', label: 'Task' },
114+
{ key: 'start', label: 'Start' },
115+
{ key: 'end', label: 'End' },
116+
{ key: 'priority', label: 'P', render: function (task) {
117+
return '<span class="sf-priority-badge priority-' + task.priority + '">P' + task.priority + '</span>';
118+
} }
119+
],
120+
onTaskClick: function (task) {
121+
SF.showToast({
122+
title: 'Gantt task',
123+
message: task.name,
124+
variant: 'success',
125+
delay: 1800
126+
});
127+
}
128+
});
129+
130+
gantt.mount(mount);
131+
gantt.setTasks([
132+
{
133+
id: 'task-1',
134+
name: 'Design review',
135+
start: '2026-03-20 09:00',
136+
end: '2026-03-20 10:30',
137+
priority: 1,
138+
custom_class: 'project-color-0 priority-1',
139+
dependencies: ''
140+
},
141+
{
142+
id: 'task-2',
143+
name: 'Implementation',
144+
start: '2026-03-20 10:30',
145+
end: '2026-03-20 14:00',
146+
priority: 2,
147+
custom_class: 'project-color-1 priority-2',
148+
dependencies: 'task-1'
149+
},
150+
{
151+
id: 'task-3',
152+
name: 'Validation',
153+
start: '2026-03-20 14:30',
154+
end: '2026-03-20 17:00',
155+
priority: 3,
156+
custom_class: 'project-color-2 priority-3',
157+
dependencies: 'task-2'
158+
}
159+
]);
160+
161+
return mount;
162+
}
163+
164+
function buildApiGuide() {
165+
return SF.createApiGuide({
166+
endpoints: [
167+
{
168+
method: 'POST',
169+
path: '/schedules',
170+
description: 'Start a new solving session.',
171+
curl: 'curl -X POST http://localhost:3000/schedules'
172+
},
173+
{
174+
method: 'GET',
175+
path: '/schedules/{id}',
176+
description: 'Fetch the current best solution.',
177+
curl: 'curl http://localhost:3000/schedules/job-123'
178+
}
179+
]
180+
});
181+
}
182+
183+
function openScoreModal() {
184+
scoreModal.setBody('<p>Hard: 0</p><p>Soft: -42</p><p>Moves/s: 12,400</p>');
185+
scoreModal.open();
186+
}
187+
188+
document.addEventListener('DOMContentLoaded', function () {
189+
var header = SF.createHeader({
190+
logo: '../static/sf/img/ouroboros.svg',
191+
title: 'Planner123',
192+
subtitle: 'solverforge-ui fixture',
193+
tabs: [
194+
{ id: 'overview', label: 'Overview', icon: 'fa-table-columns', active: true },
195+
{ id: 'gantt', label: 'Gantt', icon: 'fa-chart-gantt' },
196+
{ id: 'api', label: 'API', icon: 'fa-plug' }
197+
],
198+
onTabChange: function (id) { SF.showTab(id); },
199+
actions: {
200+
onSolve: function () {
201+
statusBar.setSolving(true);
202+
statusBar.updateScore('0hard/-42soft');
203+
statusBar.updateMoves(12400);
204+
SF.showToast({ title: 'Solve started', message: 'Fixture solver state updated', variant: 'success', delay: 2200 });
205+
},
206+
onStop: function () {
207+
statusBar.setSolving(false);
208+
statusBar.updateMoves(null);
209+
SF.showToast({ title: 'Solve stopped', message: 'Fixture solver state reset', variant: 'warning', delay: 2200 });
210+
},
211+
onAnalyze: openScoreModal
212+
}
213+
});
214+
document.body.prepend(header);
215+
216+
var statusBar = SF.createStatusBar({
217+
constraints: [
218+
{ name: 'Machine capacity', type: 'hard' },
219+
{ name: 'Preferred due date', type: 'soft' },
220+
{ name: 'Setup continuity', type: 'soft' }
221+
],
222+
onConstraintClick: function (index) {
223+
SF.showToast({ title: 'Constraint selected', message: 'Constraint #' + (index + 1), variant: 'success', delay: 1600 });
224+
}
225+
});
226+
header.after(statusBar.el);
227+
statusBar.updateScore('0hard/0soft');
228+
229+
scoreModal = SF.createModal({
230+
title: 'Score Analysis',
231+
body: '<p>Use the Analyze action to inspect the current score snapshot.</p>',
232+
footer: [
233+
SF.createButton({ text: 'Close', variant: 'default', onClick: function () { scoreModal.close(); } })
234+
]
235+
});
236+
237+
var tabs = SF.createTabs({
238+
tabs: [
239+
{
240+
id: 'overview',
241+
active: true,
242+
content: (function () {
243+
var panel = document.createElement('div');
244+
panel.className = 'demo-stack';
245+
246+
var controls = document.createElement('section');
247+
controls.className = 'demo-card';
248+
controls.innerHTML = '<h2>Interactive controls</h2>';
249+
var actions = document.createElement('div');
250+
actions.className = 'demo-actions';
251+
actions.appendChild(SF.createButton({ text: 'Show success toast', variant: 'primary', onClick: function () {
252+
SF.showToast({ title: 'Saved', message: 'Fixture action completed', variant: 'success' });
253+
} }));
254+
actions.appendChild(SF.createButton({ text: 'Show error toast', variant: 'danger', onClick: function () {
255+
SF.showError('Fixture error', 'This is a demo error payload.');
256+
} }));
257+
actions.appendChild(SF.createButton({ text: 'Open score modal', variant: 'default', onClick: openScoreModal }));
258+
controls.appendChild(actions);
259+
panel.appendChild(controls);
260+
261+
var railSection = document.createElement('section');
262+
railSection.className = 'demo-card';
263+
railSection.innerHTML = '<h2>Rail</h2>';
264+
railSection.appendChild(SF.rail.createHeader({ label: 'Resource', labelWidth: 220, columns: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'] }));
265+
railSection.appendChild(buildRailCard());
266+
panel.appendChild(railSection);
267+
268+
var tableSection = document.createElement('section');
269+
tableSection.className = 'demo-card';
270+
tableSection.innerHTML = '<h2>Table</h2>';
271+
tableSection.appendChild(buildPlannerTable());
272+
panel.appendChild(tableSection);
273+
274+
return panel;
275+
})()
276+
},
277+
{
278+
id: 'gantt',
279+
content: (function () {
280+
var panel = document.createElement('section');
281+
panel.className = 'demo-card';
282+
panel.innerHTML = '<h2>Gantt</h2>';
283+
panel.appendChild(buildGantt());
284+
return panel;
285+
})()
286+
},
287+
{
288+
id: 'api',
289+
content: (function () {
290+
var panel = document.createElement('section');
291+
panel.className = 'demo-card';
292+
panel.innerHTML = '<h2>API Guide</h2>';
293+
panel.appendChild(buildApiGuide());
294+
return panel;
295+
})()
296+
}
297+
]
298+
});
299+
300+
var main = document.createElement('main');
301+
main.className = 'sf-main';
302+
main.appendChild(tabs.el);
303+
document.body.appendChild(main);
304+
305+
document.body.appendChild(SF.createFooter({
306+
links: [
307+
{ label: 'Demo Index', url: './index.html' },
308+
{ label: 'Repository', url: 'https://github.com/SolverForge/solverforge-ui' }
309+
],
310+
version: 'fixture v0.1.0'
311+
}));
312+
});
313+
</script>
314+
</body>
315+
</html>

0 commit comments

Comments
 (0)