Appearance studio panel — font slider, custom accent, density (incl. dense), full chat width, separate button & switch styles.
window.ggbro. This is the complete plugin.
{
"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"]
}
/**
* 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' });
}
});
})();
:root {
--ggbro-plugin-theme-pulse: 1;
}
/* Soft nudge on primary buttons when this pack is enabled */
.gg-btn-primary {
letter-spacing: 0.02em;
}
# 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