ggbro Plugins

Theme Pulse

com.ggbro.plugin.theme-pulse · v4.0.4 · ui.toast, theme.read, theme.write, ui.panel, ui.sound, storage

Appearance studio panel — font slider, custom accent, density (incl. dense), full chat width, separate button & switch styles.

Sandbox example — full pack source. Everything below runs in the plugin iframe via window.ggbro. This is the complete plugin.

manifest.json

Raw
{
  "id": "com.ggbro.plugin.theme-pulse",
  "name": "Theme Pulse",
  "version": "4.0.4",
  "author": "ggbro",
  "description": "Appearance studio panel — font slider, custom accent, density (incl. dense), full chat width, separate button & switch styles.",
  "main": "plugin.js",
  "styles": "styles.css",
  "permissions": ["ui.toast", "theme.read", "theme.write", "ui.panel", "ui.sound", "storage"]
}

plugin.js

Raw
/**
 * Theme Pulse v4.0.2 — sliders, color, progress, quick looks via setTheme.
 * switchStyle is independent of buttonStyle.
 */
(async function () {
  var lastKey = '';
  var celebrate = true;
  var soundOn = true;
  var snapshot = {
    accent: '—',
    accentPreset: '—',
    accentCustom: '#5865f2',
    bgPreset: '—',
    fontSize: 14,
    radius: '—',
    density: '—',
    buttonStyle: '—',
    switchStyle: '—',
    chatWidth: '—',
    msgAlign: '—',
  };
  var changeCount = 0;

  try {
    celebrate = ((await ggbro.storage.get('celebrate')).value || '1') !== '0';
    soundOn = ((await ggbro.storage.get('soundOn')).value || '1') !== '0';
    changeCount = parseInt((await ggbro.storage.get('changes')).value || '0', 10) || 0;
  } catch (e) {
    ggbro.log('storage', e && e.message);
  }

  function keyOf(t) {
    return [
      t.accent,
      t.accentPreset,
      t.accentCustom,
      t.bgPreset,
      t.fontSize,
      t.radius,
      t.density,
      t.buttonStyle,
      t.switchStyle,
      t.chatWidth,
      t.msgAlign,
    ].join('|');
  }

  function applyTheme(t) {
    var fs = parseInt(String((t && t.fontSize) || '14').replace(/px/i, ''), 10);
    if (!fs || fs < 12) fs = 14;
    if (fs > 20) fs = 20;
    snapshot = {
      accent: (t && t.accent) || '—',
      accentPreset: (t && t.accentPreset) || '—',
      accentCustom: (t && t.accentCustom) || snapshot.accentCustom || '#5865f2',
      bgPreset: (t && t.bgPreset) || '—',
      fontSize: fs,
      radius: (t && t.radius) || '—',
      density: (t && t.density) || '—',
      buttonStyle: (t && t.buttonStyle) || '—',
      switchStyle: (t && t.switchStyle) || '—',
      chatWidth: (t && t.chatWidth) || '—',
      msgAlign: (t && t.msgAlign) || '—',
    };
  }

  async function paint() {
    await ggbro.ui.registerPanel({
      id: 'pulse',
      title: 'Theme Pulse',
      schema: [
        {
          type: 'notice',
          id: 'tip',
          text: 'Live Appearance controls — also open Apps → Theme for the full studio.',
          tone: 'info',
        },
        { type: 'section', id: 's0', title: 'Live theme' },
        { type: 'text', id: 'accent', label: 'Accent', value: snapshot.accent },
        { type: 'text', id: 'preset', label: 'Accent preset', value: snapshot.accentPreset },
        { type: 'text', id: 'bg', label: 'Background', value: snapshot.bgPreset },
        { type: 'text', id: 'radius', label: 'Radius', value: snapshot.radius },
        { type: 'text', id: 'density', label: 'Density', value: snapshot.density },
        { type: 'text', id: 'btn', label: 'Button style', value: snapshot.buttonStyle },
        { type: 'text', id: 'sw', label: 'Switch style', value: snapshot.switchStyle },
        { type: 'text', id: 'width', label: 'Chat width', value: snapshot.chatWidth },
        { type: 'text', id: 'align', label: 'Msg align', value: snapshot.msgAlign },
        { type: 'progress', id: 'changesBar', label: 'Changes noticed', value: Math.min(changeCount, 40), max: 40 },
        { type: 'text', id: 'changes', label: 'Change count', value: String(changeCount) },
        { type: 'section', id: 's1', title: 'Tune' },
        {
          type: 'slider',
          id: 'fontSize',
          label: 'Font size (px)',
          value: snapshot.fontSize,
          min: 12,
          max: 20,
          step: 1,
        },
        {
          type: 'color',
          id: 'customAccent',
          label: 'Custom accent',
          value: snapshot.accentCustom || '#5865f2',
        },
        {
          type: 'select',
          id: 'radiusPick',
          label: 'Corner radius',
          value: snapshot.radius !== '—' ? snapshot.radius : 'soft',
          options: [
            { value: 'sharp', label: 'Sharp' },
            { value: 'soft', label: 'Soft' },
            { value: 'round', label: 'Round' },
          ],
        },
        {
          type: 'select',
          id: 'densityPick',
          label: 'Density',
          value: snapshot.density !== '—' ? snapshot.density : 'cozy',
          options: [
            { value: 'dense', label: 'Dense' },
            { value: 'compact', label: 'Compact' },
            { value: 'cozy', label: 'Cozy' },
            { value: 'comfy', label: 'Comfy' },
          ],
        },
        {
          type: 'select',
          id: 'buttonPick',
          label: 'Button style',
          value: snapshot.buttonStyle !== '—' ? snapshot.buttonStyle : 'soft',
          options: [
            { value: 'filled', label: 'Filled' },
            { value: 'soft', label: 'Soft' },
            { value: 'outline', label: 'Outline' },
          ],
        },
        {
          type: 'select',
          id: 'switchPick',
          label: 'Switch style',
          value: snapshot.switchStyle !== '—' ? snapshot.switchStyle : 'filled',
          options: [
            { value: 'filled', label: 'Filled' },
            { value: 'soft', label: 'Soft' },
            { value: 'outline', label: 'Outline' },
          ],
        },
        { type: 'section', id: 's2', title: 'Quick looks' },
        {
          type: 'select',
          id: 'look',
          label: 'Apply look',
          value: 'keep',
          options: [
            { value: 'keep', label: '— pick —' },
            { value: 'soft-cyan', label: 'Soft cyan · round' },
            { value: 'outline-pink', label: 'Outline pink · round' },
            { value: 'filled-wide', label: 'Filled · wide center' },
            { value: 'compact', label: 'Compact · sharp' },
          ],
        },
        { type: 'section', id: 's3', title: 'Options' },
        { type: 'toggle', id: 'celebrate', label: 'Toast when theme changes', value: celebrate },
        { type: 'toggle', id: 'sound', label: 'Sound on theme change', value: soundOn },
        { type: 'button', id: 'pulse', label: 'Pulse accent' },
        { type: 'button', id: 'reset', label: 'Reset change counter' },
      ],
    });
  }

  try {
    var theme = await ggbro.getTheme();
    applyTheme(theme);
    lastKey = keyOf(theme);
  } catch (err) {
    ggbro.log('init', err && err.message);
  }

  await paint();
  await ggbro.toast({
    message: 'Theme Pulse — buttons & switches styled separately.',
    type: 'info',
  });

  ggbro.onThemeChange(function (theme) {
    var next = keyOf(theme);
    applyTheme(theme);
    paint().catch(function () {});
    if (next === lastKey) return;
    lastKey = next;
    changeCount += 1;
    ggbro.storage.set('changes', String(changeCount)).catch(function () {});
    if (soundOn) ggbro.playSound({ id: 'airBurst' }).catch(function () {});
    if (celebrate) {
      var label = theme.accentPreset || theme.accent || 'updated';
      ggbro
        .toast({
          message: 'Lookin’ fresh — theme is now ' + label + '.',
          type: 'success',
        })
        .catch(function () {});
    }
  });

  var LOOKS = {
    'soft-cyan': {
      accentPreset: 'cyan',
      radius: 'round',
      buttonStyle: 'soft',
      switchStyle: 'soft',
      density: 'cozy',
      chatWidth: 'normal',
      msgAlign: 'start',
    },
    'outline-pink': {
      accentPreset: 'pink',
      radius: 'round',
      buttonStyle: 'outline',
      switchStyle: 'outline',
      density: 'comfy',
      chatWidth: 'narrow',
      msgAlign: 'center',
    },
    'filled-wide': {
      accentPreset: 'blurple',
      radius: 'soft',
      buttonStyle: 'filled',
      switchStyle: 'filled',
      density: 'cozy',
      chatWidth: 'full',
      msgAlign: 'start',
    },
    compact: {
      accentPreset: 'orange',
      radius: 'sharp',
      buttonStyle: 'filled',
      switchStyle: 'soft',
      density: 'compact',
      chatWidth: 'narrow',
      msgAlign: 'start',
    },
  };

  function applyPatch(patch) {
    return ggbro
      .setTheme(patch)
      .then(function () {
        if (soundOn) return ggbro.playSound({ id: 'boing' });
      })
      .catch(function (e) {
        ggbro.log('setTheme', e && e.message);
        ggbro.toast({ message: 'Theme change failed', type: 'error' }).catch(function () {});
      });
  }

  ggbro.onEvent(function (event, payload) {
    if (event !== 'panelAction' || !payload) return;
    if (payload.fieldId === 'celebrate') {
      celebrate = !!payload.value;
      ggbro.storage.set('celebrate', celebrate ? '1' : '0').catch(function () {});
      return;
    }
    if (payload.fieldId === 'sound') {
      soundOn = !!payload.value;
      ggbro.storage.set('soundOn', soundOn ? '1' : '0').catch(function () {});
      return;
    }
    if (payload.fieldId === 'fontSize') {
      var fs = Math.max(12, Math.min(20, Number(payload.value) || 14));
      applyPatch({ fontSize: fs });
      return;
    }
    if (payload.fieldId === 'customAccent') {
      var hex = String(payload.value || '').trim();
      if (/^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/.test(hex)) {
        applyPatch({ accentPreset: 'custom', accentCustom: hex });
      }
      return;
    }
    if (payload.fieldId === 'radiusPick') {
      applyPatch({ radius: String(payload.value || 'soft') });
      return;
    }
    if (payload.fieldId === 'densityPick') {
      applyPatch({ density: String(payload.value || 'cozy') });
      return;
    }
    if (payload.fieldId === 'buttonPick') {
      applyPatch({ buttonStyle: String(payload.value || 'soft') });
      return;
    }
    if (payload.fieldId === 'switchPick') {
      applyPatch({ switchStyle: String(payload.value || 'filled') });
      return;
    }
    if (payload.fieldId === 'look' && payload.value && LOOKS[payload.value]) {
      applyPatch(LOOKS[payload.value]);
      return;
    }
    if (payload.fieldId === 'pulse') {
      var order = ['blurple', 'green', 'pink', 'orange', 'cyan'];
      var idx = order.indexOf(snapshot.accentPreset);
      var next = order[(idx >= 0 ? idx + 1 : 0) % order.length];
      applyPatch({ accentPreset: next });
      return;
    }
    if (payload.fieldId === 'reset') {
      changeCount = 0;
      ggbro.storage.set('changes', '0').then(paint);
      ggbro.toast({ message: 'Change counter reset', type: 'warning' });
    }
  });
})();

styles.css

Raw
:root {
  --ggbro-plugin-theme-pulse: 1;
}

/* Soft nudge on primary buttons when this pack is enabled */
.gg-btn-primary {
  letter-spacing: 0.02em;
}

README.md

Raw
# Theme Pulse (v3)

Official sample for **theme.read** + **theme.write**.

- Live Appearance snapshot in a Settings panel
- Quick looks via `ggbro.setTheme({ … })`
- Optional CSS chrome hooks (`.gg-btn-primary`)
- Celebrate toasts + sounds on theme change

See https://ggbro.app/developers/plugins#theme