PageSourceSearch

https://pupwife.com/scripts/desktop/citrusWidgets.js

js pupwife.com collected 2026-09-30 15:04:39 UTC 24,972 bytes, 716 lines download raw bytes

1/**
2 * CitrusOS desktop widgets — placement, persistence, gallery, lifecycle.
3 */
4(function (global) {
5    'use strict';
6
7    var STORAGE_KEY = 'citrusos_widgets_v1';
8    var STARTER_FLAG_KEY = 'citrusos_widgets_starter_v1';
9    var zCounter = 3;
10    var focusedId = null;
11    var restoring = false;
12
13    /** @type {Record<string, object>} */
14    var instances = {};
15    /** @type {Record<string, object>} */
16    var controllers = {};
17
18    var els = {};
19
20    function qs(sel) { return document.querySelector(sel); }
21
22    function uid() {
23        return 'w-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 7);
24    }
25
26    function readStore() {
27        try {
28            var raw = localStorage.getItem(STORAGE_KEY);
29            if (raw === null || raw === undefined) return null;
30            var parsed = JSON.parse(raw);
31            return Array.isArray(parsed) ? parsed : null;
32        } catch (e) {
33            return null;
34        }
35    }
36
37    function writeStore(list) {
38        try {
39            localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
40        } catch (e) { /* ignore */ }
41    }
42
43    function getShellSize() {
44        var shell = els.layer && els.layer.parentElement;
45        if (shell) {
46            return { w: shell.clientWidth, h: shell.clientHeight };
47        }
48        return { w: global.innerWidth, h: global.innerHeight - 52 };
49    }
50
51    function clampBounds(entry) {
52        var shell = getShellSize();
53        var minW = 120;
54        var minH = 80;
55        var w = Math.max(minW, entry.w || minW);
56        var h = Math.max(minH, entry.h || minH);
57        var x = Math.max(8, Math.min(entry.x || 8, Math.max(8, shell.w - w - 8)));
58        var y = Math.max(8, Math.min(entry.y || 8, Math.max(8, shell.h - h - 8)));
59        return { x: x, y: y, w: w, h: h };
60    }
61
62    function buildStarterPack() {
63        var shell = getShellSize();
64        var pad = 16;
65        var w = 220;
66        var h = 118;
67        var rightX = Math.max(pad, shell.w - w - pad);
68        return [
69            { type: 'hint', x: rightX, y: pad, w: w, h: h }
70        ];
71    }
72
73    function migrateLayoutEntry(entry) {
74        if (!entry || entry.type !== 'notes') return entry;
75        if (!global.CitrusStickyNotes || typeof global.CitrusStickyNotes.createNote !== 'function') {
76            return null;
77        }
78        var text = entry.settings && typeof entry.settings.text === 'string' ? entry.settings.text : '';
79        var note = global.CitrusStickyNotes.createNote({ text: text });
80        var next = Object.assign({}, entry);
81        next.type = 'note';
82        next.settings = { noteId: note.id };
83        return next;
84    }
85
86    function normalizeLayoutEntry(entry) {
87        if (!entry) return null;
88        if (entry.type === 'notes') {
89            entry = migrateLayoutEntry(entry);
90        }
91        if (!entry) return null;
92        if (entry.type === 'note') {
93            var noteId = entry.settings && entry.settings.noteId;
94            if (!noteId) return null;
95            if (global.CitrusStickyNotes && !global.CitrusStickyNotes.getNote(noteId)) {
96                return null;
97            }
98        }
99        return entry;
100    }
101
102    function loadLayout() {
103        var saved = readStore();
104        if (saved !== null) {
105            return saved.map(function (entry) {
106                var next = normalizeLayoutEntry(Object.assign({}, entry));
107                if (!next) return null;
108                if (!next.id) next.id = uid();
109                return next;
110            }).filter(Boolean);
111        }
112
113        try {
114            if (!localStorage.getItem(STARTER_FLAG_KEY)) {
115                localStorage.setItem(STARTER_FLAG_KEY, '1');
116            }
117        } catch (e) { /* ignore */ }
118
119        return buildStarterPack().map(function (entry) {
120            var next = Object.assign({}, entry);
121            next.id = uid();
122            return next;
123        });
124    }
125
126    function serializeLayout() {
127        var list = [];
128        for (var id in instances) {
129            if (!Object.prototype.hasOwnProperty.call(instances, id)) continue;
130            var inst = instances[id];
131            list.push({
132                id: inst.id,
133                type: inst.type,
134                x: inst.x,
135                y: inst.y,
136                w: inst.w,
137                h: inst.h,
138                settings: inst.settings || {}
139            });
140        }
141        return list;
142    }
143
144    function saveLayout() {
145        if (restoring) return;
146        writeStore(serializeLayout());
147    }
148
149    function showToast(message) {
150        if (global.SiteContextMenu && typeof global.SiteContextMenu.showToast === 'function') {
151            global.SiteContextMenu.showToast(message);
152            return;
153        }
154        if (global.CitrusDesktop) { /* citrusDesktop has private showToast */ }
155    }
156
157    function getTypeDef(type) {
158        return global.CitrusWidgetRegistry && global.CitrusWidgetRegistry.get
159            ? global.CitrusWidgetRegistry.get(type)
160            : null;
161    }
162
163    function focusWidget(id) {
164        if (!instances[id] || !instances[id].el) return;
165        focusedId = id;
166        zCounter += 1;
167        instances[id].el.style.zIndex = String(zCounter);
168        var all = els.layer.querySelectorAll('.citrus-widget');
169        for (var i = 0; i < all.length; i++) {
170            all[i].classList.toggle('citrus-widget--focused', all[i].getAttribute('data-widget-id') === id);
171        }
172    }
173
174    function destroyController(id) {
175        if (controllers[id] && typeof controllers[id].destroy === 'function') {
176            controllers[id].destroy();
177        }
178        delete controllers[id];
179    }
180
181    function loadWidgetGlobalPrefs(def) {
182        if (!global.CitrusPrefs) {
183            if (!def || !def.globalPrefsKey) return {};
184            try {
185                var raw = localStorage.getItem(def.globalPrefsKey);
186                if (!raw) return {};
187                var parsed = JSON.parse(raw);
188                return parsed && typeof parsed === 'object' ? parsed : {};
189            } catch (e) {
190                return {};
191            }
192        }
193        var key = global.CitrusPrefs.resolveWidgetPrefsKey(def);
194        return key ? global.CitrusPrefs.load(key) : {};
195    }
196
197    function saveWidgetGlobalPrefs(def, partial) {
198        if (!partial) return;
199        if (global.CitrusPrefs) {
200            var prefsKey = global.CitrusPrefs.resolveWidgetPrefsKey(def);
201            if (prefsKey) global.CitrusPrefs.save(prefsKey, partial);
202            return;
203        }
204        if (!def || !def.globalPrefsKey) return;
205        try {
206            var next = Object.assign({}, loadWidgetGlobalPrefs(def), partial || {});
207            localStorage.setItem(def.globalPrefsKey, JSON.stringify(next));
208        } catch (e) { /* ignore */ }
209    }
210
211    function mountWidgetBody(inst) {
212        destroyController(inst.id);
213        var def = getTypeDef(inst.type);
214        if (!def || !inst.bodyEl) return;
215
216        var mergedSettings = Object.assign({}, loadWidgetGlobalPrefs(def), inst.settings || {});
217
218        var ctx = {
219            instanceId: inst.id,
220            settings: mergedSettings,
221            saveSettings: function (partial) {
222                inst.settings = Object.assign({}, inst.settings || {}, partial || {});
223                saveWidgetGlobalPrefs(def, partial);
224                saveLayout();
225            },
226            setTitle: function (text) {
227                if (inst.titleEl) inst.titleEl.textContent = text || inst.type;
228            }
229        };
230
231        controllers[inst.id] = def.mount(inst.bodyEl, ctx) || null;
232    }
233
234    function createWidgetEl(inst) {
235        var def = getTypeDef(inst.type);
236        if (!def) return null;
237
238        var shell = document.createElement('div');
239        shell.className = 'citrus-widget';
240        shell.setAttribute('data-widget-id', inst.id);
241        shell.setAttribute('data-widget-type', inst.type);
242        shell.style.left = inst.x + 'px';
243        shell.style.top = inst.y + 'px';
244        shell.style.width = inst.w + 'px';
245        shell.style.height = inst.h + 'px';
246        shell.style.zIndex = String(++zCounter);
247
248        var titlebar = document.createElement('div');
249        titlebar.className = 'citrus-widget__titlebar';
250
251        var icon = document.createElement('i');
252        icon.className = (def.icon || 'fa-solid fa-puzzle-piece') + ' citrus-widget__icon';
253        icon.setAttribute('aria-hidden', 'true');
254
255        var title = document.createElement('span');
256        title.className = 'citrus-widget__title';
257        var titleText = def.label || inst.type;
258        if (inst.type === 'note' && inst.settings && inst.settings.noteId && global.CitrusStickyNotes) {
259            var noteDoc = global.CitrusStickyNotes.getNote(inst.settings.noteId);
260            if (noteDoc && noteDoc.title) titleText = noteDoc.title;
261        }
262        title.textContent = titleText;
263
264        var controls = document.createElement('div');
265        controls.className = 'citrus-widget__controls';
266
267        var btnClose = document.createElement('button');
268        btnClose.type = 'button';
269        btnClose.className = 'citrus-widget-btn citrus-widget-btn--close';
270        btnClose.setAttribute('aria-label', inst.type === 'note' ? 'Close note' : 'Remove widget');
271        btnClose.innerHTML = '<i class="fa-solid fa-xmark" aria-hidden="true"></i>';
272        btnClose.addEventListener('click', function (ev) {
273            ev.stopPropagation();
274            removeWidget(inst.id);
275        });
276        btnClose.addEventListener('pointerdown', function (ev) { ev.stopPropagation(); });
277
278        controls.appendChild(btnClose);
279        titlebar.appendChild(icon);
280        titlebar.appendChild(title);
281        titlebar.appendChild(controls);
282
283        var body = document.createElement('div');
284        body.className = 'citrus-widget__body';
285
286        shell.appendChild(titlebar);
287        shell.appendChild(body);
288
289        if (def.resizable !== false) {
290            var grip = document.createElement('div');
291            grip.className = 'citrus-widget__resize';
292            grip.setAttribute('aria-hidden', 'true');
293            shell.appendChild(grip);
294            enableWidgetResize(shell, grip, inst.id);
295        }
296
297        enableWidgetDrag(shell, titlebar, inst.id);
298
299        shell.addEventListener('pointerdown', function () {
300            focusWidget(inst.id);
301        });
302
303        inst.el = shell;
304        inst.bodyEl = body;
305        inst.titleEl = title;
306        return shell;
307    }
308
309    function findInstanceByNoteId(noteId) {
310        if (!noteId) return null;
311        for (var id in instances) {
312            if (!Object.prototype.hasOwnProperty.call(instances, id)) continue;
313            var inst = instances[id];
314            if (inst.type === 'note' && inst.settings && inst.settings.noteId === noteId) {
315                return inst;
316            }
317        }
318        return null;
319    }
320
321    function enableWidgetDrag(shell, handle, id) {
322        var dragging = false;
323        var sx = 0;
324        var sy = 0;
325        var ox = 0;
326        var oy = 0;
327
328        handle.addEventListener('pointerdown', function (ev) {
329            if (ev.button !== 0) return;
330            if (ev.target.closest('.citrus-widget__controls')) return;
331            dragging = true;
332            sx = ev.clientX;
333            sy = ev.clientY;
334            ox = parseInt(shell.style.left, 10) || 0;
335            oy = parseInt(shell.style.top, 10) || 0;
336            handle.setPointerCapture(ev.pointerId);
337            focusWidget(id);
338            ev.preventDefault();
339        });
340
341        handle.addEventListener('pointermove', function (ev) {
342            if (!dragging) return;
343            var bounds = clampBounds({
344                x: ox + ev.clientX - sx,
345                y: oy + ev.clientY - sy,
346                w: shell.offsetWidth,
347                h: shell.offsetHeight
348            });
349            shell.style.left = bounds.x + 'px';
350            shell.style.top = bounds.y + 'px';
351        });
352
353        handle.addEventListener('pointerup', function (ev) {
354            if (dragging) {
355                var inst = instances[id];
356                if (inst) {
357                    inst.x = parseInt(shell.style.left, 10) || 0;
358                    inst.y = parseInt(shell.style.top, 10) || 0;
359                    saveLayout();
360                }
361            }
362            dragging = false;
363            if (handle.hasPointerCapture(ev.pointerId)) handle.releasePointerCapture(ev.pointerId);
364        });
365    }
366
367    function enableWidgetResize(shell, grip, id) {
368        var def = getTypeDef(instances[id] && instances[id].type);
369        var minW = (def && def.minSize && def.minSize.w) || 140;
370        var minH = (def && def.minSize && def.minSize.h) || 90;
371        var resizing = false;
372        var sx = 0;
373        var sy = 0;
374        var sw = 0;
375        var sh = 0;
376
377        grip.addEventListener('pointerdown', function (ev) {
378            if (ev.button !== 0) return;
379            resizing = true;
380            sx = ev.clientX;
381            sy = ev.clientY;
382            sw = shell.offsetWidth;
383            sh = shell.offsetHeight;
384            grip.setPointerCapture(ev.pointerId);
385            focusWidget(id);
386            ev.stopPropagation();
387        });
388
389        grip.addEventListener('pointermove', function (ev) {
390            if (!resizing) return;
391            var nw = Math.max(minW, sw + ev.clientX - sx);
392            var nh = Math.max(minH, sh + ev.clientY - sy);
393            var bounds = clampBounds({
394                x: parseInt(shell.style.left, 10) || 0,
395                y: parseInt(shell.style.top, 10) || 0,
396                w: nw,
397                h: nh
398            });
399            shell.style.width = bounds.w + 'px';
400            shell.style.height = bounds.h + 'px';
401        });
402
403        grip.addEventListener('pointerup', function (ev) {
404            if (resizing) {
405                var inst = instances[id];
406                if (inst) {
407                    inst.w = shell.offsetWidth;
408                    inst.h = shell.offsetHeight;
409                    saveLayout();
410                }
411            }
412            resizing = false;
413            if (grip.hasPointerCapture(ev.pointerId)) grip.releasePointerCapture(ev.pointerId);
414        });
415    }
416
417    function spawnInstance(entry, silent) {
418        var def = getTypeDef(entry.type);
419        if (!def) return null;
420
421        var bounds = clampBounds({
422            x: entry.x,
423            y: entry.y,
424            w: entry.w || (def.defaultSize && def.defaultSize.w) || 200,
425            h: entry.h || (def.defaultSize && def.defaultSize.h) || 140
426        });
427
428        var inst = {
429            id: entry.id || uid(),
430            type: entry.type,
431            x: bounds.x,
432            y: bounds.y,
433            w: bounds.w,
434            h: bounds.h,
435            settings: entry.settings || {},
436            el: null,
437            bodyEl: null
438        };
439
440        var el = createWidgetEl(inst);
441        if (!el) return null;
442
443        instances[inst.id] = inst;
444        els.layer.appendChild(el);
445        mountWidgetBody(inst);
446
447        if (!silent) saveLayout();
448        return inst;
449    }
450
451    function addWidget(type, opts) {
452        opts = opts || {};
453        var def = getTypeDef(type);
454        if (!def) {
455            showToast('Unknown widget type');
456            return null;
457        }
458        if (def.galleryHidden) {
459            return null;
460        }
461
462        if (type === 'note') {
463            if (!opts.settings || !opts.settings.noteId) {
464                return createNoteAndOpen(opts);
465            }
466            var existingNote = findInstanceByNoteId(opts.settings.noteId);
467            if (existingNote) {
468                focusWidget(existingNote.id);
469                showToast('Note already open');
470                return existingNote;
471            }
472        }
473
474        var shell = getShellSize();
475        var defaultSize = def.defaultSize || { w: 200, h: 140 };
476        var offset = Object.keys(instances).length * 18;
477        var entry = {
478            type: type,
479            x: opts.x != null ? opts.x : Math.max(16, shell.w - defaultSize.w - 24 - offset),
480            y: opts.y != null ? opts.y : 16 + offset,
481            w: opts.w || defaultSize.w,
482            h: opts.h || defaultSize.h,
483            settings: opts.settings || {}
484        };
485
486        var inst = spawnInstance(entry, false);
487        if (inst) {
488            focusWidget(inst.id);
489            if (type !== 'note') {
490                showToast((def.label || type) + ' added');
491            }
492        }
493        return inst;
494    }
495
496    function createNoteAndOpen(opts) {
497        opts = opts || {};
498        if (!global.CitrusStickyNotes || typeof global.CitrusStickyNotes.createNote !== 'function') {
499            showToast('Notes unavailable');
500            return null;
501        }
502        var note = global.CitrusStickyNotes.createNote({
503            text: typeof opts.text === 'string' ? opts.text : ''
504        });
505        return addWidget('note', {
506            x: opts.x,
507            y: opts.y,
508            w: opts.w,
509            h: opts.h,
510            settings: { noteId: note.id }
511        });
512    }
513
514    function openNote(noteId, opts) {
515        if (!noteId) return null;
516        if (!global.CitrusStickyNotes || !global.CitrusStickyNotes.getNote(noteId)) {
517            showToast('Note not found');
518            return null;
519        }
520        opts = opts || {};
521        opts.settings = { noteId: noteId };
522        return addWidget('note', opts);
523    }
524
525    function closeNoteWidgets(noteId) {
526        if (!noteId) return;
527        var toClose = [];
528        for (var id in instances) {
529            if (!Object.prototype.hasOwnProperty.call(instances, id)) continue;
530            var inst = instances[id];
531            if (inst.type === 'note' && inst.settings && inst.settings.noteId === noteId) {
532                toClose.push(id);
533            }
534        }
535        for (var i = 0; i < toClose.length; i++) {
536            removeWidget(toClose[i], { silentToast: true });
537        }
538    }
539
540    function removeWidget(id, opts) {
541        opts = opts || {};
542        var inst = instances[id];
543        if (!inst) return;
544
545        destroyController(id);
546        if (inst.el) inst.el.remove();
547        delete instances[id];
548        if (focusedId === id) focusedId = null;
549        saveLayout();
550        if (!opts.silentToast) {
551            showToast(inst.type === 'note' ? 'Note closed' : 'Widget removed');
552        }
553    }
554
555    function resetLayout() {
556        for (var id in instances) {
557            if (Object.prototype.hasOwnProperty.call(instances, id)) {
558                destroyController(id);
559            }
560        }
561        instances = {};
562        if (els.layer) els.layer.innerHTML = '';
563
564        try {
565            localStorage.removeItem(STORAGE_KEY);
566            localStorage.removeItem(STARTER_FLAG_KEY);
567        } catch (e) { /* ignore */ }
568
569        restoring = true;
570        var pack = buildStarterPack().map(function (entry) {
571            var next = Object.assign({}, entry);
572            next.id = uid();
573            return next;
574        });
575        for (var i = 0; i < pack.length; i++) {
576            spawnInstance(pack[i], true);
577        }
578        restoring = false;
579        saveLayout();
580        showToast('Widgets reset to starter pack');
581    }
582
583    function closeGallery() {
584        if (!els.gallery) return;
585        els.gallery.hidden = true;
586        els.gallery.setAttribute('aria-hidden', 'true');
587    }
588
589    function openGallery() {
590        if (!els.gallery) buildGallery();
591        renderGalleryCards();
592        els.gallery.hidden = false;
593        els.gallery.removeAttribute('aria-hidden');
594        var firstBtn = els.gallery.querySelector('.citrus-widget-gallery__card-btn');
595        if (firstBtn) firstBtn.focus();
596    }
597
598    function buildGallery() {
599        var overlay = document.createElement('div');
600        overlay.id = 'citrus-widget-gallery';
601        overlay.className = 'citrus-widget-gallery';
602        overlay.hidden = true;
603        overlay.setAttribute('role', 'dialog');
604        overlay.setAttribute('aria-modal', 'true');
605        overlay.setAttribute('aria-label', 'Widget gallery');
606        overlay.innerHTML =
607            '<div class="citrus-widget-gallery__panel">' +
608                '<div class="citrus-widget-gallery__header">' +
609                    '<h2 class="citrus-widget-gallery__title">' +
610                        '<i class="fa-solid fa-puzzle-piece" aria-hidden="true"></i> Widget gallery' +
611                    '</h2>' +
612                    '<button type="button" class="citrus-widget-gallery__close" aria-label="Close gallery">' +
613                        '<i class="fa-solid fa-xmark" aria-hidden="true"></i>' +
614                    '</button>' +
615                '</div>' +
616                '<p class="citrus-widget-gallery__intro">Pick a gadget to drop on your desktop. Drag and resize after adding.</p>' +
617                '<div class="citrus-widget-gallery__grid" data-gallery-grid></div>' +
618            '</div>';
619
620        document.body.appendChild(overlay);
621        els.gallery = overlay;
622        els.galleryGrid = overlay.querySelector('[data-gallery-grid]');
623
624        overlay.querySelector('.citrus-widget-gallery__close').addEventListener('click', closeGallery);
625        overlay.addEventListener('click', function (ev) {
626            if (ev.target === overlay) closeGallery();
627        });
628        global.document.addEventListener('keydown', function (ev) {
629            if (ev.key === 'Escape' && els.gallery && !els.gallery.hidden) closeGallery();
630        });
631    }
632
633    function renderGalleryCards() {
634        if (!els.galleryGrid || !global.CitrusWidgetRegistry) return;
635        els.galleryGrid.innerHTML = '';
636
637        var defs = global.CitrusWidgetRegistry.listGallery
638            ? global.CitrusWidgetRegistry.listGallery()
639            : global.CitrusWidgetRegistry.list().filter(function (def) { return !def.galleryHidden; });
640        for (var i = 0; i < defs.length; i++) {
641            (function (def) {
642                var card = document.createElement('article');
643                card.className = 'citrus-widget-gallery__card';
644                card.innerHTML =
645                    '<div class="citrus-widget-gallery__card-icon">' +
646                        '<i class="' + (def.icon || 'fa-solid fa-puzzle-piece') + '" aria-hidden="true"></i>' +
647                    '</div>' +
648                    '<div class="citrus-widget-gallery__card-body">' +
649                        '<h3 class="citrus-widget-gallery__card-title">' + (def.label || def.type) + '</h3>' +
650                        '<p class="citrus-widget-gallery__card-blurb">' + (def.blurb || '') + '</p>' +
651                    '</div>' +
652                    '<button type="button" class="citrus-widget-gallery__card-btn">Add</button>';
653
654                card.querySelector('.citrus-widget-gallery__card-btn').addEventListener('click', function () {
655                    if (def.galleryAction === 'createNote' || def.type === 'note') {
656                        createNoteAndOpen();
657                    } else {
658                        addWidget(def.type);
659                    }
660                    closeGallery();
661                });
662                els.galleryGrid.appendChild(card);
663            })(defs[i]);
664        }
665    }
666
667    function getWidgetContextTarget(target) {
668        if (!target || !target.closest) return null;
669        var el = target.closest('.citrus-widget');
670        if (!el) return null;
671        var id = el.getAttribute('data-widget-id');
672        return id ? { id: id, el: el } : null;
673    }
674
675    function initFromStorage() {
676        if (!els.layer) return;
677        var hadSavedLayout = readStore() !== null;
678        restoring = true;
679        var layout = loadLayout();
680        for (var i = 0; i < layout.length; i++) {
681            spawnInstance(layout[i], true);
682        }
683        restoring = false;
684        if (!hadSavedLayout) saveLayout();
685    }
686
687    function init() {
688        els.layer = qs('#citrusos-widgets');
689        if (!els.layer) return;
690
691        initFromStorage();
692
693        // Desktop icons keep their saved pixel positions when the viewport changes
694        // (e.g. opening browser devtools). Widgets match that: no reclamp-on-resize.
695    }
696
697    global.CitrusWidgets = {
698        init: init,
699        addWidget: addWidget,
700        openNote: openNote,
701        createNoteAndOpen: createNoteAndOpen,
702        closeNoteWidgets: closeNoteWidgets,
703        removeWidget: removeWidget,
704        openGallery: openGallery,
705        closeGallery: closeGallery,
706        resetLayout: resetLayout,
707        focusWidget: focusWidget,
708        getWidgetContextTarget: getWidgetContextTarget,
709        listTypes: function () {
710            if (!global.CitrusWidgetRegistry) return [];
711            return global.CitrusWidgetRegistry.listGallery
712                ? global.CitrusWidgetRegistry.listGallery()
713                : global.CitrusWidgetRegistry.list().filter(function (def) { return !def.galleryHidden; });
714        }
715    };
716})(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.