1/** 2 * CitrusOS desktop widget registry. 3 * 4 * Register new widget types with CitrusWidgetRegistry.register({ ... }). 5 * Add the widget script path to citrusShellManifest.js â widgets (loaded before CitrusWidgets.init). 6 * 7 * User prefs: call ctx.saveSettings({ ... }) from mount(). Settings persist across close/re-add 8 * automatically (key: citrusos_widget_prefs_<type>). Set persistSettings: false for instance-only 9 * widgets (e.g. sticky notes). Override key with globalPrefsKey if needed. 10 */ 11(function (global) { 12 'use strict'; 13 14 /** @type {Record<string, object>} */ 15 var types = Object.create(null); 16 17 var DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; 18 var MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 19 20 function ordinal(n) { 21 var s = ['th', 'st', 'nd', 'rd']; 22 var v = n % 100; 23 return n + (s[(v - 20) % 10] || s[v] || s[0]); 24 } 25 26 function register(def) { 27 if (!def || !def.type || typeof def.mount !== 'function') { 28 console.warn('[CitrusWidgetRegistry] invalid widget def', def); 29 return; 30 } 31 types[def.type] = def; 32 } 33 34 function get(type) { 35 return types[type] || null; 36 } 37 38 function list() { 39 return Object.keys(types).map(function (key) { 40 return types[key]; 41 }); 42 } 43 44 function listGallery() { 45 return list().filter(function (def) { 46 return !def.galleryHidden; 47 }); 48 } 49 50 function loadScriptOnce(src) { 51 var id = 'citrus-widget-script-' + src.replace(/[^\w]+/g, '-'); 52 if (global.document.getElementById(id)) { 53 return Promise.resolve(); 54 } 55 return new Promise(function (resolve, reject) { 56 var script = global.document.createElement('script'); 57 script.id = id; 58 script.src = src; 59 script.onload = function () { resolve(); }; 60 script.onerror = function () { reject(new Error('Failed to load ' + src)); }; 61 global.document.head.appendChild(script); 62 }); 63 } 64 65 /* ---- Starter hint (first visit only; not in gallery) ---- */ 66 register({ 67 type: 'hint', 68 label: 'Getting started', 69 icon: 'fa-solid fa-hand-pointer', 70 galleryHidden: true, 71 persistSettings: false, 72 defaultSize: { w: 220, h: 118 }, 73 minSize: { w: 180, h: 96 }, 74 resizable: false, 75 mount: function (body) { 76 body.classList.add('citrus-widget-hint'); 77 body.innerHTML = 78 '<p class="citrus-widget-hint__lead">' + 79 '<i class="fa-solid fa-hand-pointer citrus-widget-hint__icon" aria-hidden="true"></i>' + 80 'Right-click the desktop' + 81 '</p>' + 82 '<p class="citrus-widget-hint__text">' + 83 'Choose <strong>Widget gallery</strong> or <strong>Add widget</strong> to drop gadgets here.' + 84 '</p>'; 85 return {}; 86 } 87 }); 88 89 /* ---- Clock ---- */ 90 register({ 91 type: 'clock', 92 label: 'Clock', 93 icon: 'fa-solid fa-clock', 94 blurb: 'Analog and digital time with date and greeting.', 95 defaultSize: { w: 200, h: 168 }, 96 minSize: { w: 150, h: 130 }, 97 resizable: true, 98 mount: function (body, ctx) { 99 body.classList.add('citrus-widget-clock'); 100 body.innerHTML = 101 '<div class="citrus-widget-clock__date">' + 102 '<span class="citrus-widget-clock__day" data-clock-day>--</span>' + 103 '<span class="citrus-widget-clock__full" data-clock-full>--</span>' + 104 '</div>' + 105 '<div class="citrus-widget-clock__greeting" data-clock-greeting>good day!</div>' + 106 '<div class="citrus-widget-clock__face">' + 107 '<div class="citrus-widget-clock__analog" data-clock-analog>' + 108 '<div class="citrus-widget-clock__hand citrus-widget-clock__hand--hour"></div>' + 109 '<div class="citrus-widget-clock__hand citrus-widget-clock__hand--minute"></div>' + 110 '</div>' + 111 '<div class="citrus-widget-clock__digital" data-clock-digital>--:--</div>' + 112 '</div>'; 113 114 var dayEl = body.querySelector('[data-clock-day]'); 115 var fullEl = body.querySelector('[data-clock-full]'); 116 var greetingEl = body.querySelector('[data-clock-greeting]'); 117 var digitalEl = body.querySelector('[data-clock-digital]'); 118 var analogEl = body.querySelector('[data-clock-analog]'); 119 var hourHand = analogEl && analogEl.querySelector('.citrus-widget-clock__hand--hour');
120 var minHand = analogEl && analogEl.querySelector('.citrus-widget-clock__hand--minute'); 121 var timers = []; 122 123 function tick() { 124 var now = new Date(); 125 var h = now.getHours(); 126 var m = now.getMinutes(); 127 var hour12 = h % 12 || 12; 128 129 if (digitalEl) { 130 digitalEl.innerHTML = hour12 + '<small class="citrus-widget-clock__min">' + 131 String(m).padStart(2, '0') + '</small>'; 132 } 133 if (analogEl && hourHand && minHand) { 134 hourHand.style.transform = 'rotate(' + ((hour12 * 30) + (m * 0.5)) + 'deg)'; 135 minHand.style.transform = 'rotate(' + (m * 6) + 'deg)'; 136 } 137 } 138 139 function tickDate() { 140 if (dayEl) dayEl.textContent = DAYS[new Date().getDay()]; 141 if (fullEl) { 142 var d = new Date(); 143 fullEl.textContent = MONTHS[d.getMonth()] + ' ' + ordinal(d.getDate()); 144 } 145 if (greetingEl) { 146 var hour = new Date().getHours(); 147 if (hour >= 5 && hour < 12) greetingEl.textContent = 'good morning!'; 148 else if (hour >= 12 && hour < 17) greetingEl.textContent = 'good afternoon!'; 149 else greetingEl.textContent = 'good evening!'; 150 } 151 } 152 153 tick(); 154 tickDate(); 155 timers.push(global.setInterval(tick, 1000)); 156 timers.push(global.setInterval(tickDate, 60000)); 157 158 return { 159 destroy: function () { 160 for (var i = 0; i < timers.length; i++) { 161 global.clearInterval(timers[i]); 162 } 163 } 164 }; 165 } 166 }); 167 168 /* ---- Sticky note (one document per widget) ---- */ 169 register({ 170 type: 'note', 171 label: 'New note', 172 icon: 'fa-solid fa-file-lines', 173 blurb: 'Create a new sticky note and open it on the desktop.', 174 persistSettings: false, 175 defaultSize: { w: 260, h: 180 }, 176 minSize: { w: 180, h: 120 }, 177 resizable: true, 178 galleryAction: 'createNote', 179 mount: function (body, ctx) { 180 body.classList.add('citrus-widget-notes'); 181 var noteId = ctx.settings && ctx.settings.noteId; 182 var notesApi = global.CitrusStickyNotes; 183 if (!noteId || !notesApi) { 184 body.innerHTML = '<p class="citrus-widget-notes__missing">Note not found</p>'; 185 return {}; 186 } 187 188 var note = notesApi.getNote(noteId); 189 if (!note) { 190 body.innerHTML = '<p class="citrus-widget-notes__missing">Note not found</p>'; 191 return {}; 192 } 193 194 if (typeof ctx.setTitle === 'function') { 195 ctx.setTitle(note.title || notesApi.DEFAULT_TITLE); 196 } 197 198 body.innerHTML = 199 '<textarea id="' + ctx.instanceId + '-note" class="citrus-widget-notes__textarea" ' + 200 'placeholder="jot something down..." spellcheck="true" aria-label="' + 201 (note.title || 'Sticky note').replace(/"/g, '"') + '"></textarea>'; 202 203 var textarea = body.querySelector('.citrus-widget-notes__textarea'); 204 if (!textarea) return {}; 205 206 textarea.value = note.text || ''; 207 var saveTimer = null; 208 209 textarea.addEventListener('input', function () { 210 global.clearTimeout(saveTimer); 211 saveTimer = global.setTimeout(function () { 212 var updated = notesApi.updateNote(noteId, { text: textarea.value }); 213 if (updated && typeof ctx.setTitle === 'function') { 214 ctx.setTitle(updated.title); 215 } 216 }, 280); 217 }); 218 textarea.addEventListener('pointerdown', function (ev) { 219 ev.stopPropagation(); 220 }); 221 222 return { 223 destroy: function () { 224 global.clearTimeout(saveTimer); 225 } 226 }; 227 } 228 }); 229 230 global.CitrusWidgetRegistry = { 231 register: register, 232 get: get, 233 list: list, 234 listGallery: listGallery 235 }; 236})(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.