1/** 2 * CitrusOS Yo-Yo desktop widget â global overlay toggle + physics aux window. 3 * Register path: citrusShellManifest.js â widgets (after yoyo.js) 4 */ 5(function (global) { 6 'use strict'; 7 8 var PREFS_KEY = 'citrusos_widget_prefs_yoyo'; 9 var PHYSICS_AUX_ID = 'yoyo-physics'; 10 11 var SLIDER_SPECS = [ 12 { key: 'gravity', label: 'Gravity', min: 0, max: 1, step: 0.05, decimals: 2 }, 13 { key: 'stiffness', label: 'Stiffness', min: 0.001, max: 0.1, step: 0.001, decimals: 3 }, 14 { key: 'damping', label: 'Damping', min: 0.5, max: 0.99, step: 0.01, decimals: 2 }, 15 { key: 'radius', label: 'Ball size', min: 5, max: 30, step: 1, decimals: 0 }, 16 { key: 'mass', label: 'Ball mass', min: 0.5, max: 5, step: 0.1, decimals: 1 } 17 ]; 18 19 function readGlobalPrefs() { 20 if (global.CitrusPrefs && typeof global.CitrusPrefs.load === 'function') { 21 return global.CitrusPrefs.load(PREFS_KEY); 22 } 23 try { 24 var raw = global.localStorage.getItem(PREFS_KEY); 25 if (!raw) return {}; 26 var parsed = JSON.parse(raw); 27 return parsed && typeof parsed === 'object' ? parsed : {}; 28 } catch (e) { 29 return {}; 30 } 31 } 32 33 function saveGlobalPrefs(partial) { 34 if (global.CitrusPrefs && typeof global.CitrusPrefs.save === 'function') { 35 return global.CitrusPrefs.save(PREFS_KEY, partial); 36 } 37 try { 38 var next = Object.assign({}, readGlobalPrefs(), partial || {}); 39 global.localStorage.setItem(PREFS_KEY, JSON.stringify(next)); 40 return next; 41 } catch (e) { 42 return readGlobalPrefs(); 43 } 44 } 45 46 function getYoyoApi() { 47 return global.CitrusYoyo || null; 48 } 49 50 function readPhysicsFromPrefs(prefs) { 51 var api = getYoyoApi(); 52 var defaults = api && api.DEFAULTS ? api.DEFAULTS : { 53 gravity: 0.4, 54 stiffness: 0.02, 55 damping: 0.95, 56 radius: 12, 57 mass: 2 58 }; 59 prefs = prefs || {}; 60 return { 61 gravity: typeof prefs.gravity === 'number' ? prefs.gravity : defaults.gravity, 62 stiffness: typeof prefs.stiffness === 'number' ? prefs.stiffness : defaults.stiffness, 63 damping: typeof prefs.damping === 'number' ? prefs.damping : defaults.damping, 64 radius: typeof prefs.radius === 'number' ? prefs.radius : defaults.radius, 65 mass: typeof prefs.mass === 'number' ? prefs.mass : defaults.mass 66 }; 67 } 68 69 function physicsToPrefs(physics) { 70 return { 71 gravity: physics.gravity, 72 stiffness: physics.stiffness, 73 damping: physics.damping, 74 radius: physics.radius, 75 mass: physics.mass 76 }; 77 } 78 79 function applyPhysics(physics) { 80 var api = getYoyoApi(); 81 if (!api || typeof api.setPhysics !== 'function') return physics; 82 return api.setPhysics(physics); 83 } 84 85 function applyEnabled(enabled) { 86 var api = getYoyoApi(); 87 if (!api) return !!enabled; 88 if (enabled) { 89 if (typeof api.enable === 'function') api.enable(); 90 } else if (typeof api.disable === 'function') { 91 api.disable(); 92 } 93 return api.isEnabled ? !!api.isEnabled() : !!enabled; 94 } 95 96 function applyCitrusShellYoyo() { 97 if (!global.document.body || !global.document.body.classList.contains('page-citrusos')) return; 98 var api = getYoyoApi(); 99 if (!api) return; 100 var prefs = readGlobalPrefs(); 101 applyPhysics(readPhysicsFromPrefs(prefs)); 102 applyEnabled(!!prefs.enabled); 103 } 104 105 function formatValue(value, decimals) { 106 if (decimals === 0) return String(Math.round(value)); 107 return Number(value).toFixed(decimals); 108 } 109 110 function renderToggleButton(btn, enabled) { 111 if (!btn) return; 112 btn.classList.toggle('citrus-widget-yoyo__btn--active', !!enabled); 113 btn.setAttribute('aria-pressed', enabled ? 'true' : 'false'); 114 btn.innerHTML = 115 '<i class="fa-solid ' + (enabled ? 'fa-circle-stop' : 'fa-circle-notch') + '" aria-hidden="true"></i>' + 116 '<span>' + (enabled ? 'Hide yoyo' : 'Show yoyo') + '</span>'; 117 } 118
119 function renderStatus(el, enabled) { 120 if (!el) return; 121 el.textContent = enabled ? 'Yoyo is out!' : 'Yoyo is tucked away'; 122 el.classList.toggle('citrus-widget-yoyo__status--on', !!enabled); 123 } 124 125 function knobRow(spec, val) { 126 return '<label class="noise-knob" data-yoyo-ui>' + 127 '<span class="noise-knob__meta">' + 128 '<span class="noise-knob__label">' + spec.label + '</span>' + 129 '<span class="noise-knob__value" data-yoyo-val="' + spec.key + '">' + formatValue(val, spec.decimals) + '</span>' + 130 '</span>' + 131 '<input type="range" class="noise-knob__input" data-yoyo-key="' + spec.key + '" ' + 132 'min="' + spec.min + '" max="' + spec.max + '" step="' + spec.step + '" value="' + val + '" ' + 133 'aria-label="' + spec.label + '">' + 134 '</label>'; 135 } 136 137 function mountPhysicsPanel(mountEl, onChange) { 138 if (!mountEl) return function () {}; 139 140 var api = getYoyoApi(); 141 var physics = api && typeof api.getPhysics === 'function' 142 ? api.getPhysics() 143 : readPhysicsFromPrefs(readGlobalPrefs()); 144 145 var rows = SLIDER_SPECS.map(function (spec) { 146 return knobRow(spec, physics[spec.key]); 147 }).join(''); 148 149 var html = '<div class="citrus-yoyo-physics">' + 150 '<div class="noise-knob-grid citrus-yoyo-physics__knobs">' + rows + '</div>' + 151 '<button type="button" class="citrus-tool-btn citrus-yoyo-physics__reset" data-yoyo-reset>Reset to defaults</button>' + 152 '</div>'; 153 154 mountEl.innerHTML = html; 155 156 var sliders = mountEl.querySelectorAll('[data-yoyo-key]'); 157 var valueEls = {}; 158 SLIDER_SPECS.forEach(function (spec) { 159 valueEls[spec.key] = mountEl.querySelector('[data-yoyo-val="' + spec.key + '"]'); 160 }); 161 162 function syncDisplay(nextPhysics) { 163 SLIDER_SPECS.forEach(function (spec) { 164 var slider = mountEl.querySelector('[data-yoyo-key="' + spec.key + '"]'); 165 if (slider) slider.value = String(nextPhysics[spec.key]); 166 if (valueEls[spec.key]) { 167 valueEls[spec.key].textContent = formatValue(nextPhysics[spec.key], spec.decimals); 168 } 169 }); 170 } 171 172 function handleInput(ev) { 173 var key = ev.target.getAttribute('data-yoyo-key'); 174 if (!key) return; 175 var partial = {}; 176 partial[key] = parseFloat(ev.target.value); 177 var next = applyPhysics(Object.assign({}, physics, partial)); 178 physics = next; 179 var spec = SLIDER_SPECS.filter(function (s) { return s.key === key; })[0]; 180 if (spec && valueEls[key]) { 181 valueEls[key].textContent = formatValue(next[key], spec.decimals); 182 } 183 if (typeof onChange === 'function') { 184 onChange(next); 185 } 186 } 187 188 for (var i = 0; i < sliders.length; i++) { 189 sliders[i].addEventListener('input', handleInput); 190 } 191 192 var resetBtn = mountEl.querySelector('[data-yoyo-reset]'); 193 if (resetBtn) { 194 resetBtn.addEventListener('click', function (ev) { 195 ev.stopPropagation(); 196 var apiRef = getYoyoApi(); 197 physics = apiRef && typeof apiRef.resetPhysics === 'function' 198 ? apiRef.resetPhysics() 199 : readPhysicsFromPrefs({}); 200 syncDisplay(physics); 201 if (typeof onChange === 'function') { 202 onChange(physics); 203 } 204 }); 205 } 206 207 return function teardown() { 208 for (var j = 0; j < sliders.length; j++) { 209 sliders[j].removeEventListener('input', handleInput); 210 } 211 }; 212 } 213 214 var PHYSICS_WIN_W = 300; 215 var PHYSICS_WIN_H = 296; 216 217 function openPhysicsWindow(anchorEl, persistPhysics) { 218 var desktop = global.CitrusDesktop; 219 if (!desktop || typeof desktop.openAuxWindow !== 'function') return; 220 221 var shellW = PHYSICS_WIN_W; 222 var shellH = PHYSICS_WIN_H; 223 var rect = anchorEl && anchorEl.getBoundingClientRect 224 ? anchorEl.getBoundingClientRect() 225 : { left: 120, top: 80, right: 220, bottom: 120 }; 226 var posX = Math.min(rect.right + 8, Math.max(8, global.innerWidth - shellW - 8)); 227 var posY = Math.max(8, Math.min(rect.top, global.innerHe
227ight - shellH - 72)); 228 229 var teardownPanel = null; 230 231 desktop.openAuxWindow({ 232 id: PHYSICS_AUX_ID, 233 label: 'Yoyo Physics', 234 icon: 'fa-solid fa-sliders', 235 w: shellW, 236 h: shellH, 237 x: posX, 238 y: posY, 239 nearChat: false, 240 resizable: false, 241 auxClass: 'citrus-window--yoyo-physics', 242 onMount: function (contentEl) { 243 contentEl.classList.add('page-yoyo-physics', 'citrusos-app-active'); 244 teardownPanel = mountPhysicsPanel(contentEl, function (physics) { 245 if (typeof persistPhysics === 'function') { 246 persistPhysics(physics); 247 } else { 248 saveGlobalPrefs(physicsToPrefs(physics)); 249 } 250 }); 251 }, 252 onClose: function () { 253 if (typeof teardownPanel === 'function') { 254 teardownPanel(); 255 } 256 teardownPanel = null; 257 } 258 }); 259 } 260 261 function mount(body, ctx) { 262 body.classList.add('citrus-widget-yoyo'); 263 264 var settings = ctx.settings || {}; 265 var enabled = !!settings.enabled; 266 var physics = readPhysicsFromPrefs(settings); 267 268 applyPhysics(physics); 269 enabled = applyEnabled(enabled); 270 271 body.innerHTML = 272 '<div class="citrus-widget-yoyo__main">' + 273 '<div class="citrus-widget-yoyo__head">' + 274 '<i class="fa-solid fa-circle-notch citrus-widget-yoyo__icon" aria-hidden="true"></i>' + 275 '<span class="citrus-widget-yoyo__status" data-yoyo-status>Yoyo is tucked away</span>' + 276 '</div>' + 277 '<div class="citrus-widget-yoyo__actions">' + 278 '<button type="button" class="citrus-widget-yoyo__btn" data-yoyo-toggle aria-pressed="false"></button>' + 279 '<button type="button" class="citrus-widget-yoyo__btn" data-yoyo-physics>' + 280 '<i class="fa-solid fa-sliders" aria-hidden="true"></i>' + 281 '<span>Physics</span>' + 282 '</button>' + 283 '</div>' + 284 '</div>'; 285 286 var statusEl = body.querySelector('[data-yoyo-status]'); 287 var toggleBtn = body.querySelector('[data-yoyo-toggle]'); 288 var physicsBtn = body.querySelector('[data-yoyo-physics]'); 289 290 function persistAll(partial) { 291 if (typeof ctx.saveSettings === 'function') { 292 ctx.saveSettings(partial); 293 } 294 saveGlobalPrefs(partial); 295 } 296 297 function setEnabled(nextEnabled, persist) { 298 enabled = !!nextEnabled; 299 enabled = applyEnabled(enabled); 300 renderToggleButton(toggleBtn, enabled); 301 renderStatus(statusEl, enabled); 302 if (persist) { 303 persistAll({ enabled: enabled }); 304 } 305 } 306 307 function persistPhysics(nextPhysics) { 308 persistAll(physicsToPrefs(nextPhysics)); 309 } 310 311 renderToggleButton(toggleBtn, enabled); 312 renderStatus(statusEl, enabled); 313 314 if (toggleBtn) { 315 toggleBtn.addEventListener('click', function (ev) { 316 ev.stopPropagation(); 317 setEnabled(!enabled, true); 318 }); 319 } 320 321 if (physicsBtn) { 322 physicsBtn.addEventListener('click', function (ev) { 323 ev.stopPropagation(); 324 openPhysicsWindow(body, persistPhysics); 325 }); 326 } 327 328 return { 329 destroy: function () { /* global yoyo stays on like fish food */ } 330 }; 331 } 332 333 if (global.CitrusWidgetRegistry && typeof global.CitrusWidgetRegistry.register === 'function') { 334 global.CitrusWidgetRegistry.register({ 335 type: 'yoyo', 336 label: 'Yo-Yo', 337 icon: 'fa-solid fa-circle-notch', 338 blurb: 'Toggle the springy yoyo overlay and tune its physics.', 339 defaultSize: { w: 210, h: 118 }, 340 minSize: { w: 190, h: 108 }, 341 resizable: false, 342 mount: mount 343 }); 344 } 345 346 global.CitrusYoyoShell = { 347 apply: applyCitrusShellYoyo 348 }; 349 350 function scheduleShellApply() { 351 if (!global.document.body || !global.document.body.classList.contains('page-citrusos')) return; 352 applyCitrusShellYoyo(); 353 if (global.document.readyState === 'loading') { 354 global.document.addEventListener('DOMContentLoaded', applyCitrusShellYoyo); 355 } 356 } 357 358 scheduleShellApply(); 359})(typeof window !== 'undefined' ? window : this);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.