@manufosela/system-capabilities

@manufosela/system-capabilities

Browser and system feature detection utility

system-capabilities

Browser and system feature detection utility

Quick usage

import { supports, getCapabilitiesSync } from '@manufosela/system-capabilities';

if (supports('webgl')) {
  // init WebGL
}

const caps = getCapabilitiesSync();

Browser Information

System Information

Screen Information

Network Connection

User Preferences

Capabilities

Demo code (CodePen-ready HTML, CSS, JS)
HTML (html)
<div class="top-links">
    <a href="https://manufosela.dev/utils/">Utils catalog</a>
    <a href="https://manufosela.dev/">Main catalog</a>
    <a href="https://github.com/manufosela/utils/tree/main/packages/system-capabilities">Source</a>
  </div>
        <demo-theme-toggle></demo-theme-toggle>
  <h1>system-capabilities</h1>
  <p class="subtitle">Browser and system feature detection utility</p>

  <div class="section">
    <h2>Quick usage</h2>
    <pre class="code-block"><code>import { supports, getCapabilitiesSync } from '@manufosela/system-capabilities';

if (supports('webgl')) {
  // init WebGL
}

const caps = getCapabilitiesSync();
</code></pre>
  </div>

  <div class="section">
    <h2>Browser Information</h2>
    <div class="browser-info" id="browserInfo"></div>
  </div>

  <div class="section">
    <h2>System Information</h2>
    <div class="browser-info" id="systemInfo"></div>
  </div>

  <div class="section">
    <h2>Screen Information</h2>
    <div class="screen-info" id="screenInfo"></div>
  </div>

  <div class="section">
    <h2>Network Connection</h2>
    <div class="connection-info" id="connectionInfo"></div>
  </div>

  <div class="section">
    <h2>User Preferences</h2>
    <div class="preferences" id="preferences"></div>
  </div>

  <div class="section">
    <h2>Capabilities</h2>
    <div id="capabilities"></div>
  </div>
CSS (css)
:root {
  --bg: #0c0f14;
  --bg-elevated: #141923;
  --bg-panel: #171d28;
  --border: #262f3f;
  --text: #f4f6fb;
  --text-muted: #a7b0c2;
  --text-dim: #7d879b;
  --accent: #ff8a3d;
  --accent-strong: #ff6a00;
  --accent-soft: rgba(255, 138, 61, 0.16);
  --shadow: 0 20px 50px rgba(5, 8, 14, 0.45);
  --radius-lg: 22px;
  --radius-md: 14px;
  --radius-sm: 10px;
  --max-width: 1160px;
}

* {
      box-sizing: border-box;
    }

    h1 {
      color: var(--accent);
      margin-bottom: 0.5rem;
    }

    .subtitle {
      color: var(--text-muted);
      margin-bottom: 2rem;
    }

    .section {
      background: var(--bg-elevated);
      border-radius: 8px;
      padding: 1.5rem;
      margin-bottom: 1.5rem;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }

    .section h2 {
      margin: 0 0 1rem;
      color: var(--text);
      font-size: 1.25rem;
      border-bottom: 2px solid var(--accent);
      padding-bottom: 0.5rem;
    }

    .browser-info {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
      gap: 1rem;
    }

    .info-item {
      text-align: center;
      padding: 1rem;
      background: var(--bg-panel);
      border-radius: 4px;
    }

    .info-item .label {
      font-size: 0.75rem;
      color: var(--text-muted);
      text-transform: uppercase;
      margin-bottom: 0.25rem;
    }

    .info-item .value {
      font-size: 1.1rem;
      font-weight: 600;
      color: var(--text);
    }

    .capabilities-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
      gap: 0.5rem;
    }

    .capability {
      display: flex;
      align-items: center;
      gap: 0.5rem;
      padding: 0.5rem 0.75rem;
      background: var(--bg-panel);
      border-radius: 4px;
      font-size: 0.875rem;
    }

    .capability .indicator {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      flex-shrink: 0;
    }

    .capability .indicator.supported {
      background: #28a745;
    }

    .capability .indicator.not-supported {
      background: #dc3545;
    }

    .capability .indicator.loading {
      background: #ffc107;
      animation: pulse 1s infinite;
    }

    @keyframes pulse {
      0%, 100% { opacity: 1; }
      50% { opacity: 0.5; }
    }

    .capability .name {
      color: var(--text);
    }

    .screen-info {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
      gap: 1rem;
    }

    .connection-info {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
      gap: 1rem;
    }

    .preferences {
      display: flex;
      gap: 1rem;
      flex-wrap: wrap;
    }

    .preference {
      padding: 0.75rem 1rem;
      border-radius: 4px;
      font-size: 0.875rem;
    }

    .preference.active {
      background: #d4edda;
      color: #155724;
    }

    .preference.inactive {
      background: var(--bg-panel);
      color: var(--text-muted);
    }

    .category-title {
      font-size: 0.875rem;
      color: var(--text-muted);
      text-transform: uppercase;
      margin: 1rem 0 0.5rem;
      padding-top: 0.5rem;
      border-top: 1px solid var(--border);
    }

    .category-title:first-child {
      margin-top: 0;
      padding-top: 0;
      border-top: none;
    }

    .top-links {
      display: flex;
      gap: 0.5rem;
      flex-wrap: wrap;
      margin-bottom: 1rem;
    }

    .top-links a {
      font-size: 0.8rem;
      color: var(--text-muted);
      text-decoration: none;
      border: 1px solid var(--border);
      padding: 0.35rem 0.75rem;
      border-radius: 999px;
    }

    .code-block {
      background: #0b0f1a;
      border: 1px solid #2b3247;
      border-radius: 8px;
      padding: 12px;
      color: #d6d9e6;
      font-family: "JetBrains Mono", "Courier New", monospace;
      font-size: 0.85rem;
      white-space: pre-wrap;
      word-break: break-word;
    }
  






:root {
  --bg: #0f1117;
  --bg-2: #151a26;
  --bg-spot-1: #1a2136;
  --bg-spot-2: #1d1b34;
  --card: #1c2233;
  --text: #f3f6ff;
  --muted: #b8c0d9;
  --line: #2b3247;
  --accent: #ffb000;
  --accent-2: #00d0ff;
  --surface: #0b0f1a;
  --code-bg: #0b0f1a;
  --code-text: #d6d9e6;
}

:root.light {
  --bg: #f5f5f7;
  --bg-2: #ffffff;
  --bg-spot-1: #f8e9d0;
  --bg-spot-2: #e8eef8;
  --card: #ffffff;
  --text: #1d1d1f;
  --muted: #6b7280;
  --line: #e5e7eb;
  --accent: #ffb000;
  --accent-2: #00a7d6;
  --surface: #f3f4f6;
  --code-bg: #111827;
  --code-text: #f9fafb;
}

:root.dark {
  --bg: #0f1117;
  --bg-2: #151a26;
  --bg-spot-1: #1a2136;
  --bg-spot-2: #1d1b34;
  --card: #1c2233;
  --text: #f3f6ff;
  --muted: #b8c0d9;
  --line: #2b3247;
  --accent: #ffb000;
  --accent-2: #00d0ff;
  --surface: #0b0f1a;
  --code-bg: #0b0f1a;
  --code-text: #d6d9e6;
}

h1 {
  color: var(--accent) !important;
}

.top-links a,
.topbar a {
  color: var(--muted) !important;
  border-color: var(--line) !important;
}

.top-links a:hover,
.topbar a:hover {
  color: var(--text) !important;
}

.card,
.section,
.demo-section,
.panel {
  background: var(--card) !important;
  border-color: var(--line) !important;
  color: var(--text) !important;
}

.code-block,
pre,
code,
.output,
.current-url,
.event-log,
.result-card,
.log {
  background: var(--code-bg) !important;
  color: var(--code-text) !important;
  border-color: var(--line) !important;
}

input,
select,
textarea {
  background: var(--surface) !important;
  color: var(--text) !important;
  border-color: var(--line) !important;
}

button {
  background: linear-gradient(120deg, var(--accent), #ff6a00) !important;
  color: #111 !important;
}

footer {
  color: var(--muted) !important;
}

footer a {
  color: var(--text) !important;
}
JS (js)
import {
      getBrowserInfo,
      getCapabilities,
      getCapabilitiesSync,
      getDeviceMemory,
      getHardwareConcurrency,
      getConnectionInfo,
      getScreenInfo,
      prefersReducedMotion,
      prefersDarkMode,
      isStandalone
    } from "https://esm.sh/@manufosela/system-capabilities";

    // Browser Info
    const browserInfo = getBrowserInfo();
    const browserInfoEl = document.getElementById('browserInfo');
    browserInfoEl.innerHTML = `
      <div class="info-item">
        <div class="label">Browser</div>
        <div class="value">${browserInfo.name}</div>
      </div>
      <div class="info-item">
        <div class="label">Version</div>
        <div class="value">${browserInfo.version}</div>
      </div>
      <div class="info-item">
        <div class="label">Engine</div>
        <div class="value">${browserInfo.engine}</div>
      </div>
      <div class="info-item">
        <div class="label">OS</div>
        <div class="value">${browserInfo.os}</div>
      </div>
      <div class="info-item">
        <div class="label">OS Version</div>
        <div class="value">${browserInfo.osVersion}</div>
      </div>
      <div class="info-item">
        <div class="label">Mobile</div>
        <div class="value">${browserInfo.isMobile ? 'Yes' : 'No'}</div>
      </div>
    `;

    // System Info
    const memory = getDeviceMemory();
    const cores = getHardwareConcurrency();
    const systemInfoEl = document.getElementById('systemInfo');
    systemInfoEl.innerHTML = `
      <div class="info-item">
        <div class="label">Device Memory</div>
        <div class="value">${memory ? memory + ' GB' : 'N/A'}</div>
      </div>
      <div class="info-item">
        <div class="label">CPU Cores</div>
        <div class="value">${cores || 'N/A'}</div>
      </div>
      <div class="info-item">
        <div class="label">Standalone Mode</div>
        <div class="value">${isStandalone() ? 'Yes' : 'No'}</div>
      </div>
    `;

    // Screen Info
    const screenData = getScreenInfo();
    const screenInfoEl = document.getElementById('screenInfo');
    screenInfoEl.innerHTML = `
      <div class="info-item">
        <div class="label">Resolution</div>
        <div class="value">${screenData.width} x ${screenData.height}</div>
      </div>
      <div class="info-item">
        <div class="label">Available</div>
        <div class="value">${screenData.availWidth} x ${screenData.availHeight}</div>
      </div>
      <div class="info-item">
        <div class="label">Color Depth</div>
        <div class="value">${screenData.colorDepth} bit</div>
      </div>
      <div class="info-item">
        <div class="label">Pixel Ratio</div>
        <div class="value">${screenData.pixelRatio}x</div>
      </div>
      <div class="info-item">
        <div class="label">Orientation</div>
        <div class="value">${screenData.orientation}</div>
      </div>
    `;

    // Connection Info
    const connection = getConnectionInfo();
    const connectionInfoEl = document.getElementById('connectionInfo');
    if (connection) {
      connectionInfoEl.innerHTML = `
        <div class="info-item">
          <div class="label">Type</div>
          <div class="value">${connection.effectiveType}</div>
        </div>
        <div class="info-item">
          <div class="label">Downlink</div>
          <div class="value">${connection.downlink} Mbps</div>
        </div>
        <div class="info-item">
          <div class="label">RTT</div>
          <div class="value">${connection.rtt} ms</div>
        </div>
        <div class="info-item">
          <div class="label">Save Data</div>
          <div class="value">${connection.saveData ? 'Yes' : 'No'}</div>
        </div>
      `;
    } else {
      connectionInfoEl.innerHTML = '<p style="color: var(--text-muted);">Network Information API not available</p>';
    }

    // User Preferences
    const preferencesEl = document.getElementById('preferences');
    preferencesEl.innerHTML = `
      <div class="preference ${prefersReducedMotion() ? 'active' : 'inactive'}">
        Reduced Motion: ${prefersReducedMotion() ? 'Yes' : 'No'}
      </div>
      <div class="preference ${prefersDarkMode() ? 'active' : 'inactive'}">
        Dark Mode: ${prefersDarkMode() ? 'Yes' : 'No'}
      </div>
    `;

    // Capabilities
    const capabilitiesEl = document.getElementById('capabilities');
    const syncCaps = getCapabilitiesSync();

    // Group capabilities by category
    const categories = {
      'Graphics': ['webgl', 'webgl2', 'canvas', 'offscreenCanvas', 'webAnimations'],
      'Communication': ['webrtc', 'websockets', 'fetch'],
      'Workers': ['serviceWorker', 'webWorkers', 'sharedWorkers'],
      'Notifications': ['pushNotifications', 'notifications'],
      'Location & Sensors': ['geolocation', 'deviceOrientation', 'deviceMotion'],
      'Input': ['touch', 'pointerEvents', 'gamepad'],
      'Storage': ['indexedDB', 'localStorage', 'sessionStorage', 'cookies'],
      'Media': ['webm', 'mediaRecorder', 'mediaSession', 'speechRecognition', 'speechSynthesis'],
      'Device APIs': ['bluetooth', 'usb', 'midi', 'vibration', 'batteryStatus', 'networkInfo', 'wakeLock'],
      'Security': ['crypto'],
      'Clipboard & Share': ['clipboard', 'share'],
      'Display': ['fullscreen', 'pictureInPicture'],
      'Observers': ['intersectionObserver', 'resizeObserver', 'mutationObserver'],
      'Web Components': ['customElements', 'shadowDOM'],
      'ES Features': ['modules', 'dynamicImport'],
      'Image Formats (async)': ['webp', 'avif']
    };

    let html = '';
    for (const [category, features] of Object.entries(categories)) {
      html += `<div class="category-title">${category}</div>`;
      html += '<div class="capabilities-grid">';
      for (const feature of features) {
        const isAsync = ['webp', 'avif'].includes(feature);
        const supported = syncCaps[feature];
        html += `
          <div class="capability" data-feature="${feature}">
            <span class="indicator ${isAsync ? 'loading' : (supported ? 'supported' : 'not-supported')}"></span>
            <span class="name">${feature}</span>
          </div>
        `;
      }
      html += '</div>';
    }
    capabilitiesEl.innerHTML = html;

    // Load async capabilities
    getCapabilities().then(fullCaps => {
      ['webp', 'avif'].forEach(feature => {
        const el = document.querySelector(`[data-feature="${feature}"] .indicator`);
        if (el) {
          el.classList.remove('loading');
          el.classList.add(fullCaps[feature] ? 'supported' : 'not-supported');
        }
      });
    });
  

class DemoThemeToggle extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.handleClick = this.handleClick.bind(this);
  }

  connectedCallback() {
    const saved = localStorage.getItem('utils-demo-theme');
    document.documentElement.classList.remove('dark');
    document.documentElement.classList.toggle('light', saved === 'light');

    this.render();
  }

  render() {
    const isLight = document.documentElement.classList.contains('light');
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: inline-flex;
        }
        .toggle {
          display: inline-flex;
          align-items: center;
          border: 1px solid var(--line);
          border-radius: 999px;
          overflow: hidden;
          background: var(--surface);
        }
        button {
          border: none;
          background: transparent;
          color: var(--muted);
          padding: 6px 12px;
          font-size: 0.8rem;
          cursor: pointer;
          font-family: "Space Grotesk", "Trebuchet MS", Arial, sans-serif;
        }
        button.active {
          background: linear-gradient(120deg, var(--accent), #ff6a00);
          color: #111;
          font-weight: 600;
        }
      </style>
      <div class="toggle" role="group" aria-label="Theme toggle">
        <button class="${isLight ? 'active' : ''}" data-theme="light">Light</button>
        <button class="${isLight ? '' : 'active'}" data-theme="dark">Dark</button>
      </div>
    `;

    this.shadowRoot.querySelectorAll('button').forEach((btn) => {
      btn.addEventListener('click', this.handleClick);
    });
  }

  handleClick(event) {
    const theme = event.currentTarget.dataset.theme;
    const isLight = theme === 'light';
    document.documentElement.classList.toggle('light', isLight);
    document.documentElement.classList.remove('dark');
    localStorage.setItem('utils-demo-theme', isLight ? 'light' : 'dark');
    this.render();
  }
}

customElements.define('demo-theme-toggle', DemoThemeToggle);