diff --git a/README.md b/README.md index ad53740..4487c84 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ SF.createButton({ text: 'Stop', variant: 'danger' }) // red bg, white tex 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: 'Settings', icon: 'fa-gear', variant: 'ghost', iconOnly: 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' }) @@ -184,6 +185,10 @@ var card = SF.rail.createCard({ columns: 5, type: 'CAMERA', typeStyle: { bg: 'rgba(59,130,246,0.15)', color: '#1d4ed8', border: '1px solid rgba(59,130,246,0.3)' }, + badges: [ + 'TEMPRA', + { label: 'HOT', style: { bg: 'rgba(239,68,68,0.12)', color: '#b91c1c', border: '1px solid rgba(239,68,68,0.3)' } }, + ], gauges: [ { label: 'Temp', pct: 85, style: 'heat', text: '850/1000°C' }, { label: 'Load', pct: 60, style: 'load', text: '120/200 kg' }, @@ -216,6 +221,7 @@ card.setSolving(true); ``` Gauge styles: `heat` (blue→amber→red), `load` (emerald→amber→red), `emerald` (solid green). +`badges` accepts either strings or `{ label, style }` objects for extra resource metadata. ## Gantt Chart diff --git a/WIREFRAME.md b/WIREFRAME.md index eedaf95..8db1a8a 100644 --- a/WIREFRAME.md +++ b/WIREFRAME.md @@ -312,6 +312,7 @@ var header = SF.rail.createHeader({ var card = SF.rail.createCard({ id: 'furnace-1', name: 'FORNO 1', labelWidth: 200, type: 'CAMERA', typeStyle: { bg: 'rgba(59,130,246,0.15)', color: '#1d4ed8' }, + badges: ['TEMPRA', { label: 'HOT', style: { bg: 'rgba(239,68,68,0.12)', color: '#b91c1c' } }], columns: 5, gauges: [ { label: 'Temp', pct: 85, style: 'heat', text: '850/1000°C' }, diff --git a/js-src/03-buttons.js b/js-src/03-buttons.js index 6a80ae2..5deaafd 100644 --- a/js-src/03-buttons.js +++ b/js-src/03-buttons.js @@ -32,7 +32,7 @@ btn.appendChild(icon); } - if (config.text && !config.circle) { + if (config.text && !config.circle && !config.iconOnly) { btn.appendChild(document.createTextNode(config.text)); } @@ -46,6 +46,8 @@ if (config.ariaLabel) { btn.setAttribute('aria-label', config.ariaLabel); + } else if (config.iconOnly && config.text) { + btn.setAttribute('aria-label', config.text); } else if (config.icon && !config.text) { btn.setAttribute('aria-label', config.icon.replace(/fa-/, '').replace(/-/g, ' ')); } diff --git a/js-src/13-rail.js b/js-src/13-rail.js index ed9fcc5..32da3bd 100644 --- a/js-src/13-rail.js +++ b/js-src/13-rail.js @@ -65,6 +65,27 @@ } meta.appendChild(badge); } + var badges = Array.isArray(config.badges) + ? config.badges + : config.badges + ? [config.badges] + : []; + if (badges.length) { + badges.forEach(function (entry) { + if (!entry) return; + if (typeof entry === 'string') { + meta.appendChild(sf.el('span', { className: 'sf-resource-type-badge' }, entry)); + return; + } + var extraBadge = sf.el('span', { className: 'sf-resource-type-badge' }, entry.label || ''); + if (entry.style) { + extraBadge.style.background = entry.style.bg || ''; + extraBadge.style.color = entry.style.color || ''; + extraBadge.style.border = entry.style.border || ''; + } + meta.appendChild(extraBadge); + }); + } identity.appendChild(meta); } resHeader.appendChild(identity); @@ -130,6 +151,7 @@ horizon: config.heatmap.horizon || 1, label: config.heatmap.label, segments: config.heatmap.segments, + labelWidth: labelWidth, }; heatmapCfg.railConfig = config; var heatmap = sf.rail.createHeatmap(heatmapCfg); @@ -185,6 +207,7 @@ if (!config || !config.segments || !Array.isArray(config.segments) || config.segments.length === 0) return null; var heatmap = sf.el('div', { className: 'sf-heatmap' }); + heatmap.style.gridTemplateColumns = (config.labelWidth || 200) + 'px 1fr'; var label = sf.el('div', { className: 'sf-heatmap-label' }, config.label || ''); heatmap.appendChild(label); diff --git a/static/sf/sf.js b/static/sf/sf.js index a23d41d..fbbe110 100644 --- a/static/sf/sf.js +++ b/static/sf/sf.js @@ -215,7 +215,7 @@ const SF = (function () { btn.appendChild(icon); } - if (config.text && !config.circle) { + if (config.text && !config.circle && !config.iconOnly) { btn.appendChild(document.createTextNode(config.text)); } @@ -229,6 +229,8 @@ const SF = (function () { if (config.ariaLabel) { btn.setAttribute('aria-label', config.ariaLabel); + } else if (config.iconOnly && config.text) { + btn.setAttribute('aria-label', config.text); } else if (config.icon && !config.text) { btn.setAttribute('aria-label', config.icon.replace(/fa-/, '').replace(/-/g, ' ')); } @@ -1220,6 +1222,27 @@ const SF = (function () { } meta.appendChild(badge); } + var badges = Array.isArray(config.badges) + ? config.badges + : config.badges + ? [config.badges] + : []; + if (badges.length) { + badges.forEach(function (entry) { + if (!entry) return; + if (typeof entry === 'string') { + meta.appendChild(sf.el('span', { className: 'sf-resource-type-badge' }, entry)); + return; + } + var extraBadge = sf.el('span', { className: 'sf-resource-type-badge' }, entry.label || ''); + if (entry.style) { + extraBadge.style.background = entry.style.bg || ''; + extraBadge.style.color = entry.style.color || ''; + extraBadge.style.border = entry.style.border || ''; + } + meta.appendChild(extraBadge); + }); + } identity.appendChild(meta); } resHeader.appendChild(identity); @@ -1285,6 +1308,7 @@ const SF = (function () { horizon: config.heatmap.horizon || 1, label: config.heatmap.label, segments: config.heatmap.segments, + labelWidth: labelWidth, }; heatmapCfg.railConfig = config; var heatmap = sf.rail.createHeatmap(heatmapCfg); @@ -1340,6 +1364,7 @@ const SF = (function () { if (!config || !config.segments || !Array.isArray(config.segments) || config.segments.length === 0) return null; var heatmap = sf.el('div', { className: 'sf-heatmap' }); + heatmap.style.gridTemplateColumns = (config.labelWidth || 200) + 'px 1fr'; var label = sf.el('div', { className: 'sf-heatmap-label' }, config.label || ''); heatmap.appendChild(label); diff --git a/tests/button-rail-knobs.test.js b/tests/button-rail-knobs.test.js new file mode 100644 index 0000000..4bdef34 --- /dev/null +++ b/tests/button-rail-knobs.test.js @@ -0,0 +1,65 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); +const vm = require('node:vm'); + +const { createDom } = require('./support/fake-dom'); + +const ROOT = path.resolve(__dirname, '..'); + +function loadSf(files, overrides = {}) { + const { document, window, Node } = createDom(); + const context = vm.createContext({ + console, + document, + window, + Node, + setTimeout, + clearTimeout, + ...overrides, + }); + + files.forEach((file) => { + const source = fs.readFileSync(path.join(ROOT, file), 'utf8'); + vm.runInContext(source, context, { filename: file }); + }); + + return { SF: context.window.SF, document }; +} + +test('iconOnly buttons keep an accessible label without rendering text content', () => { + const { SF } = loadSf(['js-src/00-core.js', 'js-src/03-buttons.js']); + + const button = SF.createButton({ + text: 'Settings', + icon: 'fa-gear', + iconOnly: true, + }); + + assert.equal(button.textContent, ''); + assert.equal(button.attributes['aria-label'], 'Settings'); +}); + +test('rail card badges accept a single string badge and preserve heatmap alignment', () => { + const { SF } = loadSf(['js-src/00-core.js', 'js-src/13-rail.js']); + + const card = SF.rail.createCard({ + name: 'Kiln 1', + badges: 'TEMPRA', + labelWidth: 220, + columns: 4, + heatmap: { + label: 'Load', + horizon: 100, + segments: [{ start: 0, end: 25, color: '#0f0' }], + }, + }); + + const badges = card.el.querySelectorAll('.sf-resource-type-badge'); + const heatmap = card.el.querySelector('.sf-heatmap'); + + assert.equal(badges.length, 1); + assert.equal(badges[0].textContent, 'TEMPRA'); + assert.equal(heatmap.style.gridTemplateColumns, '220px 1fr'); +});