solverforge-ui 0.4.0

Frontend component library for SolverForge constraint-optimization applications
Documentation
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>solverforge-ui full surface demo</title>
  <link rel="stylesheet" href="../static/sf/vendor/fontawesome/css/fontawesome.min.css">
  <link rel="stylesheet" href="../static/sf/vendor/fontawesome/css/solid.min.css">
  <link rel="stylesheet" href="../static/sf/vendor/frappe-gantt/frappe-gantt.min.css">
  <link rel="stylesheet" href="../static/sf/sf.css">
  <script src="../static/sf/vendor/split/split.min.js"></script>
  <script src="../static/sf/vendor/frappe-gantt/frappe-gantt.min.js"></script>
  <script src="../static/sf/sf.js"></script>
  <style>
    body { margin: 0; }
    .demo-stack { display: grid; gap: 18px; }
    .demo-card { background: white; border: 1px solid var(--sf-gray-200); border-radius: 12px; box-shadow: var(--sf-shadow-base); padding: 18px; }
    .demo-card h2 { margin: 0 0 12px; }
    .demo-actions { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
    .demo-gantt { height: 520px; }
    .demo-footer-space { height: 32px; }
  </style>
</head>
<body class="sf-app">
  <script>
    var scoreModal;

    function buildPlannerTable() {
      return SF.createTable({
        columns: [
          { label: 'Task' },
          { label: 'Machine' },
          { label: 'Due', align: 'right' }
        ],
        rows: [
          ['ODL-2847', 'FORNO 1', 'Mon 14:00'],
          ['ODL-3012', 'FORNO 2', 'Mon 17:30'],
          ['ODL-1802', 'FORNO 1', 'Tue 09:15']
        ],
        onRowClick: function (index, row) {
          SF.showToast({
            title: 'Row selected',
            message: row[0] + ' on ' + row[1],
            variant: 'success',
            delay: 2500
          });
        }
      });
    }

    function buildRailCard() {
      var card = SF.rail.createCard({
        id: 'furnace-1',
        name: 'FORNO 1',
        labelWidth: 220,
        columns: 5,
        type: 'CAMERA',
        typeStyle: {
          bg: 'rgba(59,130,246,0.15)',
          color: '#1d4ed8',
          border: '1px solid rgba(59,130,246,0.30)'
        },
        gauges: [
          { label: 'Temp', pct: 84, style: 'heat', text: '840 / 1000 C' },
          { label: 'Load', pct: 63, style: 'load', text: '126 / 200 kg' }
        ],
        stats: [
          { label: 'Jobs', value: 12 },
          { label: 'Prod', value: '840 kg' }
        ]
      });

      card.addBlock({
        start: 120,
        end: 340,
        horizon: 600,
        label: 'ODL-2847',
        meta: 'Bianchi',
        color: 'rgba(59,130,246,0.55)',
        borderColor: '#3b82f6'
      });

      SF.rail.addChangeover(card.rail, {
        start: 340,
        end: 385,
        horizon: 600
      });

      card.addBlock({
        start: 385,
        end: 560,
        horizon: 600,
        label: 'ODL-3012',
        meta: 'Rossi',
        color: 'rgba(16,185,129,0.55)',
        borderColor: '#10b981',
        late: true
      });

      return card.el;
    }

    function createGanttPanel() {
      var mount = document.createElement('div');
      mount.className = 'demo-gantt';
      var mounted = false;
      var tasks = [
        {
          id: 'task-1',
          name: 'Design review',
          start: '2026-03-20 09:00',
          end: '2026-03-20 10:30',
          priority: 1,
          custom_class: 'project-color-0 priority-1',
          dependencies: ''
        },
        {
          id: 'task-2',
          name: 'Implementation',
          start: '2026-03-20 10:30',
          end: '2026-03-20 14:00',
          priority: 2,
          custom_class: 'project-color-1 priority-2',
          dependencies: 'task-1'
        },
        {
          id: 'task-3',
          name: 'Validation',
          start: '2026-03-20 14:30',
          end: '2026-03-20 17:00',
          priority: 3,
          custom_class: 'project-color-2 priority-3',
          dependencies: 'task-2'
        }
      ];

      var gantt = SF.gantt.create({
        gridTitle: 'Tasks',
        chartTitle: 'Schedule',
        viewMode: 'Half Day',
        splitSizes: [38, 62],
        columns: [
          { key: 'name', label: 'Task' },
          { key: 'start', label: 'Start' },
          { key: 'end', label: 'End' },
          { key: 'priority', label: 'P', render: function (task) {
            return '<span class="sf-priority-badge priority-' + task.priority + '">P' + task.priority + '</span>';
          } }
        ],
        onTaskClick: function (task) {
          SF.showToast({
            title: 'Gantt task',
            message: task.name,
            variant: 'success',
            delay: 1800
          });
        }
      });

      return {
        mountIfNeeded: function () {
          if (mounted) return;
          if (!mount.isConnected || mount.offsetWidth === 0 || mount.offsetHeight === 0) return;
          gantt.mount(mount);
          gantt.setTasks(tasks);
          mounted = true;
        },
        el: mount
      };
    }

    function buildApiGuide() {
      return SF.createApiGuide({
        endpoints: [
          {
            method: 'POST',
            path: '/schedules',
            description: 'Start a new solving session.',
            curl: 'curl -X POST http://localhost:3000/schedules'
          },
          {
            method: 'GET',
            path: '/schedules/{id}',
            description: 'Fetch the current best solution.',
            curl: 'curl http://localhost:3000/schedules/job-123'
          }
        ]
      });
    }

    function openScoreModal() {
      scoreModal.setBody('<p>Hard: 0</p><p>Soft: -42</p><p>Moves/s: 12,400</p>');
      scoreModal.open();
    }

    document.addEventListener('DOMContentLoaded', function () {
      var ganttPanel = createGanttPanel();

      var header = SF.createHeader({
        logo: '../static/sf/img/ouroboros.svg',
        title: 'Planner123',
        subtitle: 'solverforge-ui fixture',
        tabs: [
          { id: 'overview', label: 'Overview', icon: 'fa-table-columns', active: true },
          { id: 'gantt', label: 'Gantt', icon: 'fa-chart-gantt' },
          { id: 'api', label: 'API', icon: 'fa-plug' }
        ],
        onTabChange: function (id) {
          SF.showTab(id);
          if (id === 'gantt') {
            requestAnimationFrame(function () {
              ganttPanel.mountIfNeeded();
            });
          }
        },
        actions: {
          onSolve: function () {
            statusBar.setSolving(true);
            statusBar.updateScore('0hard/-42soft');
            statusBar.updateMoves(12400);
            SF.showToast({ title: 'Solve started', message: 'Fixture solver state updated', variant: 'success', delay: 2200 });
          },
          onStop: function () {
            statusBar.setSolving(false);
            statusBar.updateMoves(null);
            SF.showToast({ title: 'Solve stopped', message: 'Fixture solver state reset', variant: 'warning', delay: 2200 });
          },
          onAnalyze: openScoreModal
        }
      });
      document.body.prepend(header);

      var statusBar = SF.createStatusBar({
        constraints: [
          { name: 'Machine capacity', type: 'hard' },
          { name: 'Preferred due date', type: 'soft' },
          { name: 'Setup continuity', type: 'soft' }
        ],
        onConstraintClick: function (index) {
          SF.showToast({ title: 'Constraint selected', message: 'Constraint #' + (index + 1), variant: 'success', delay: 1600 });
        }
      });
      header.after(statusBar.el);
      statusBar.updateScore('0hard/0soft');

      scoreModal = SF.createModal({
        title: 'Score Analysis',
        body: '<p>Use the Analyze action to inspect the current score snapshot.</p>',
        footer: [
          SF.createButton({ text: 'Close', variant: 'default', onClick: function () { scoreModal.close(); } })
        ]
      });

      var tabs = SF.createTabs({
        tabs: [
          {
            id: 'overview',
            active: true,
            content: (function () {
              var panel = document.createElement('div');
              panel.className = 'demo-stack';

              var controls = document.createElement('section');
              controls.className = 'demo-card';
              controls.innerHTML = '<h2>Interactive controls</h2>';
              var actions = document.createElement('div');
              actions.className = 'demo-actions';
              actions.appendChild(SF.createButton({ text: 'Show success toast', variant: 'primary', onClick: function () {
                SF.showToast({ title: 'Saved', message: 'Fixture action completed', variant: 'success' });
              } }));
              actions.appendChild(SF.createButton({ text: 'Show error toast', variant: 'danger', onClick: function () {
                SF.showError('Fixture error', 'This is a demo error payload.');
              } }));
              actions.appendChild(SF.createButton({ text: 'Open score modal', variant: 'default', onClick: openScoreModal }));
              controls.appendChild(actions);
              panel.appendChild(controls);

              var railSection = document.createElement('section');
              railSection.className = 'demo-card';
              railSection.innerHTML = '<h2>Rail</h2>';
              railSection.appendChild(SF.rail.createHeader({ label: 'Resource', labelWidth: 220, columns: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'] }));
              railSection.appendChild(buildRailCard());
              panel.appendChild(railSection);

              var tableSection = document.createElement('section');
              tableSection.className = 'demo-card';
              tableSection.innerHTML = '<h2>Table</h2>';
              tableSection.appendChild(buildPlannerTable());
              panel.appendChild(tableSection);

              return panel;
            })()
          },
          {
            id: 'gantt',
            content: (function () {
              var panel = document.createElement('section');
              panel.className = 'demo-card';
              panel.innerHTML = '<h2>Gantt</h2>';
              panel.appendChild(ganttPanel.el);
              return panel;
            })()
          },
          {
            id: 'api',
            content: (function () {
              var panel = document.createElement('section');
              panel.className = 'demo-card';
              panel.innerHTML = '<h2>API Guide</h2>';
              panel.appendChild(buildApiGuide());
              return panel;
            })()
          }
        ]
      });

      var main = document.createElement('main');
      main.className = 'sf-main';
      main.appendChild(tabs.el);
      document.body.appendChild(main);

      requestAnimationFrame(function () {
        ganttPanel.mountIfNeeded();
      });

      document.body.appendChild(SF.createFooter({
        links: [
          { label: 'Demo Index', url: './index.html' },
          { label: 'Repository', url: 'https://github.com/SolverForge/solverforge-ui' }
        ],
        version: 'fixture v0.3.0'
      }));
    });
  </script>
</body>
</html>