openvibers/openvibe.tools Commits

Font Awesome out, the OpenVibe glyphs in (plan L54 K4) (#171)

OP OpenVibers committed 12 days ago · 74e342c9f3 · 88 files, +364 −322

The owner, 2026-11-01: "get rid of Font Awesome completely and just use our own
openvibe.design system". Every app's link to cdnjs' all.min.css is gone; the icons
come from the OpenVibe glyphs (openvibe-shared/glyphs) and the brand logos
(openvibe-shared/brands), which the Shared navbar loads for the page. cdnjs leaves
every app's CSP: none of them used it for anything else.

Every fa- name the apps named that no glyph draws is now one that is drawn: the
point-of-interest categories (gas-pump -> store, bus -> train, shower -> faucet,
bed -> moon, book-skull -> book, backpack -> briefcase, shield-heart -> shield,
temperature-half -> sun, charging-station -> bolt, smoking -> smog,
hospital -> kit-medical, binoculars -> eye, cloud-sun-rain -> cloud-rain,
openstreetmap -> map, crosshairs -> location-dot, satellite -> satellite-dish,
globe-americas -> earth-americas, street-view -> map-location-dot,
magnifying-glass-location -> magnifying-glass, exclamation-triangle ->
triangle-exclamation, diamond-turn-right -> route, spider -> bug,
dumbbell -> weight-hanging, toilet -> restroom, archway -> door-open,
tent-arrow-down-to-line -> campground, car-side -> square-parking, chair -> couch,
warehouse -> box-open, martini-glass -> mug-hot, beer-mug-empty -> mug-saucer,
ring -> gem, wine-bottle -> flask, building-shield -> shield-halved,
building-circle-xmark -> circle-xmark, drumstick-bite -> fish, wheat-awn -> seedling,
carrot -> leaf, glass-water -> mug-saucer, oil-can -> droplet, boxes-stacked -> cubes,
bowl-food -> utensils, temperature-low -> snowflake, syringe -> briefcase-medical,
vial -> flask-vial, stethoscope -> kit-medical, baby -> child,
cash-register -> money-bill, house-medical -> house, dumpster -> trash,
fire-flame-simple -> fire, hotel -> couch, people-roof -> people-group,
prescription -> suitcase-medical, church -> house-chimney, vest -> shirt,
caravan -> car, hill-rockslide -> mountain, sailboat -> water, bicycle -> road,
truck-monster -> car, horse -> paw, ship -> water, anchor -> map-pin,
cloud-moon-rain -> cloud-moon), and the app pages (fingerprint -> id-card,
barcode -> code, calendar-check -> calendar-days, code-compare -> arrows-left-right,
paragraph -> align-left, location-crosshairs -> location-dot, building -> city,
address-book -> address-card, magnifying-glass-arrow-right -> magnifying-glass,
share -> arrow-up-from-bracket, code-merge -> code-fork, note-sticky -> paste,
crop -> scissors, crop-simple -> scissors, bezier-curve -> shapes,
arrow-rotate-left -> rotate-left, bars-staggered -> sliders, repeat -> arrows-rotate,
podcast -> microphone, broom -> eraser, dragon -> gamepad,
magnifying-glass-chart -> magnifying-glass, face-smile-beam -> face-smile,
sort-alpha-down -> arrow-down-short-wide, braille -> grip, text-slash -> font,
calculator -> table-cells, brackets-curly -> code, landmark -> gavel,
hands-holding-child -> child, markdown -> code, cloud-upload-alt -> cloud-arrow-up,
asterisk -> star, house-lock -> house, file-signature -> file-pen, handcuffs -> lock,
phone-volume -> phone, app-store -> mobile-screen, arrow-down-a-z ->
arrow-down-wide-short, face-sad-tear -> face-grin-squint-tears,
face-angry -> face-grin-squint, person-shrugging -> question,
face-surprise -> face-grin-stars, face-grin-tongue -> face-laugh,
face-grin-wink -> face-grin, tools -> screwdriver-wrench,
arrow-down-up-across-line -> arrows-up-down-left-right, pepper-hot -> fire,
sort-alpha-up -> arrow-down-wide-short, shuffle -> right-left,
retweet -> arrow-right-arrow-left, right-long -> arrow-right, superscript -> arrow-up,
grip-lines -> grip-vertical, star-of-david -> star, chess -> gamepad,
certificate -> ranking-star, sunglasses -> eye, fill-drip -> droplet,
sort -> arrow-down-wide-short, stamp -> signature, lock-open -> unlock,
apple-whole -> utensils, toggle-on -> circle-check).

The text app's ice.html drew its marker with Font Awesome's font; it is the OpenVibe
chevron now, painted in the text's colour with a CSS mask.

The app lock files move to openvibe-shared v4.73.0, the version the apps ask for:
v4.56.0 has no ov-glyphs.js.

test/no-font-awesome.test.js holds it there: no page links Font Awesome, nothing
draws with its font, and every fa- name is drawn.

Changes

ddiff --git a/apps/audio/package-lock.json b/apps/audio/package-lock.json
iindex 9137134..e64f348 100644
---- a/apps/audio/package-lock.json
++++ b/apps/audio/package-lock.json
@@@ -22,7 +22,7 @@
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
"pg": "^8.23.0",
"uuid": "^11.1.0"
}
@@@ -1330,9 +1330,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "4.56.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
-- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",
++ "version": "4.73.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
++ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
ddiff --git a/apps/audio/public/index.html b/apps/audio/public/index.html
iindex 120484a..eea81eb 100644
---- a/apps/audio/public/index.html
++++ b/apps/audio/public/index.html
@@@ -20,7 +20,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
@@@ -45,10 +44,10 @@
<button class="tool-tab" data-tool="normalize"><i class="fa-solid fa-sliders"></i> Normalize</button>
<button class="tool-tab" data-tool="fade"><i class="fa-solid fa-volume-low"></i> Fade</button>
<button class="tool-tab" data-tool="bass"><i class="fa-solid fa-volume-high"></i> Bass</button>
-- <button class="tool-tab" data-tool="equalizer"><i class="fa-solid fa-bars-staggered"></i> EQ</button>
++ <button class="tool-tab" data-tool="equalizer"><i class="fa-solid fa-sliders"></i> EQ</button>
<button class="tool-tab" data-tool="voice"><i class="fa-solid fa-user-astronaut"></i> Voice FX</button>
<button class="tool-tab" data-tool="echo"><i class="fa-solid fa-tower-broadcast"></i> Echo</button>
-- <button class="tool-tab" data-tool="reverb"><i class="fa-solid fa-church"></i> Reverb</button>
++ <button class="tool-tab" data-tool="reverb"><i class="fa-solid fa-house-chimney"></i> Reverb</button>
<button class="tool-tab" data-tool="extract"><i class="fa-solid fa-music"></i> Extract</button>
<button class="tool-tab" data-tool="ringtone"><i class="fa-solid fa-bell"></i> Ringtone</button>
</nav>
@@@ -303,7 +302,7 @@
<i class="fa-solid fa-download"></i> Download
</a>
<button class="btn btn-secondary" id="another-btn">
-- <i class="fa-solid fa-arrow-rotate-left"></i> Process Another
++ <i class="fa-solid fa-rotate-left"></i> Process Another
</button>
</div>
</section>
@@@ -332,7 +331,7 @@
<span class="cross-info"><span class="cross-name">OpenVibe.Live</span><span class="cross-desc">Live streaming platform</span></span>
</a>
<a class="cross-card" href="https://openvibe.games">
-- <span class="cross-icon"><i class="fa-solid fa-dragon"></i></span>
++ <span class="cross-icon"><i class="fa-solid fa-gamepad"></i></span>
<span class="cross-info"><span class="cross-name">Scraplandia</span><span class="cross-desc">Multiplayer RPG</span></span>
</a>
<a class="cross-card" href="https://maps.openvibe.tools">
ddiff --git a/apps/audio/public/js/app.js b/apps/audio/public/js/app.js
iindex 4624709..8954a1c 100644
---- a/apps/audio/public/js/app.js
++++ b/apps/audio/public/js/app.js
@@@ -648,7 +648,7 @@
{ label: 'Format Conversion', icon: 'fa-solid fa-arrows-rotate', ids: ['mp3', 'wav', 'flac', 'ogg', 'm4a', 'aac', 'opus', 'wma', 'aiff', 'ac3'] },
{ label: 'Audio Editing', icon: 'fa-solid fa-scissors', ids: ['trim', 'merge', 'fade', 'loop', 'silence', 'stereo', 'normalize', 'speed', 'reverse'] },
{ label: 'Audio Effects', icon: 'fa-solid fa-wand-magic-sparkles', ids: ['pitch', 'bass', 'equalizer', 'echo', 'reverb', 'chorus', 'distortion', 'compressor', 'bitcrusher', 'voice', 'noise'] },
-- { label: 'Extraction & Analysis', icon: 'fa-solid fa-magnifying-glass-chart', ids: ['extract', 'vocal', 'waveform', 'metadata'] },
++ { label: 'Extraction & Analysis', icon: 'fa-solid fa-magnifying-glass', ids: ['extract', 'vocal', 'waveform', 'metadata'] },
{ label: 'Specialized', icon: 'fa-solid fa-star', ids: ['ringtone', 'podcast'] },
];
ddiff --git a/apps/audio/server/domain-map.js b/apps/audio/server/domain-map.js
iindex 5db09d5..c773557 100644
---- a/apps/audio/server/domain-map.js
++++ b/apps/audio/server/domain-map.js
@@@ -122,7 +122,7 @@ const DOMAIN_MAP = {
},
'loop.openvibe.tools': {
toolId: 'loop', brandName: 'OpenVibeLoop', defaultOp: 'loop',
-- faIcon: 'fa-repeat',
++ faIcon: 'fa-arrows-rotate',
seoTitle: 'OpenVibeLoop — Loop Audio Online',
seoDescription: 'Loop audio files a set number of times. Create repeated versions of any sound. Online audio looper.',
},
@@@ -134,7 +134,7 @@ const DOMAIN_MAP = {
},
'equalizer.openvibe.tools': {
toolId: 'equalizer', brandName: 'OpenVibeEQ', defaultOp: 'equalizer',
-- faIcon: 'fa-bars-staggered',
++ faIcon: 'fa-sliders',
seoTitle: 'OpenVibeEQ — Online Audio Equalizer',
seoDescription: 'Apply equalizer presets to audio files. Boost bass, treble, vocals and more. Online EQ tool.',
},
@@@ -174,7 +174,7 @@ const DOMAIN_MAP = {
},
'podcast.openvibe.tools': {
toolId: 'podcast', brandName: 'OpenVibePodcast', defaultOp: 'podcast',
-- faIcon: 'fa-podcast',
++ faIcon: 'fa-microphone',
seoTitle: 'OpenVibePodcast — Optimize Audio for Podcasts Online',
seoDescription: 'Optimize audio for podcast publishing. Normalize loudness, compress dynamics, convert to podcast-ready format.',
},
@@@ -186,7 +186,7 @@ const DOMAIN_MAP = {
},
'noise.openvibe.tools': {
toolId: 'noise', brandName: 'OpenVibeNoise', defaultOp: 'noise',
-- faIcon: 'fa-broom',
++ faIcon: 'fa-eraser',
seoTitle: 'OpenVibeNoise — Reduce Background Noise Online',
seoDescription: 'Remove background noise from audio recordings. Clean up interviews, podcasts, and voice memos. Online tool.',
},
@@@ -204,7 +204,7 @@ const DOMAIN_MAP = {
},
'reverb.openvibe.tools': {
toolId: 'reverb', brandName: 'OpenVibeReverb', defaultOp: 'reverb',
-- faIcon: 'fa-church',
++ faIcon: 'fa-house-chimney',
seoTitle: 'OpenVibeReverb — Add Reverb to Audio Online',
seoDescription: 'Add room reverb, hall, cathedral, and plate reverb effects to audio. Online reverb tool.',
},
ddiff --git a/apps/audio/server/index.js b/apps/audio/server/index.js
iindex 058dabe..c0163e8 100644
---- a/apps/audio/server/index.js
++++ b/apps/audio/server/index.js
@@@ -95,9 +95,9 @@ app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
-- scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://openvibe.network"],
-- styleSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
-- fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
++ scriptSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://openvibe.network"],
++ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
++ fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "blob:", jobsRuntime.mediaOrigin()], // job result previews are 302s to Media
mediaSrc: ["'self'", "blob:", jobsRuntime.mediaOrigin()],
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
ddiff --git a/apps/audio/server/tools/index.js b/apps/audio/server/tools/index.js
iindex 108bd48..0d1da77 100644
---- a/apps/audio/server/tools/index.js
++++ b/apps/audio/server/tools/index.js
@@@ -117,7 +117,7 @@ const TOOLS = {
id: 'loop',
label: 'Loop',
description: 'Loop audio multiple times',
-- faIcon: 'fa-repeat',
++ faIcon: 'fa-arrows-rotate',
accepts: AUDIO_FORMATS,
category: 'editing',
handler: loop,
@@@ -135,7 +135,7 @@ const TOOLS = {
id: 'equalizer',
label: 'Equalizer',
description: 'Apply EQ presets',
-- faIcon: 'fa-bars-staggered',
++ faIcon: 'fa-sliders',
accepts: AUDIO_FORMATS,
category: 'effects',
handler: equalizer,
@@@ -180,7 +180,7 @@ const TOOLS = {
id: 'podcast',
label: 'Podcast',
description: 'Optimize for podcast publishing',
-- faIcon: 'fa-podcast',
++ faIcon: 'fa-microphone',
accepts: AUDIO_FORMATS,
category: 'specialized',
handler: podcast,
@@@ -198,7 +198,7 @@ const TOOLS = {
id: 'noise',
label: 'Noise Reduce',
description: 'Reduce background noise',
-- faIcon: 'fa-broom',
++ faIcon: 'fa-eraser',
accepts: AUDIO_FORMATS,
category: 'editing',
handler: noise,
@@@ -225,7 +225,7 @@ const TOOLS = {
id: 'reverb',
label: 'Reverb',
description: 'Add reverb effect',
-- faIcon: 'fa-church',
++ faIcon: 'fa-house-chimney',
accepts: AUDIO_FORMATS,
category: 'effects',
handler: reverb,
ddiff --git a/apps/docs/package-lock.json b/apps/docs/package-lock.json
iindex be1aec2..6628931 100644
---- a/apps/docs/package-lock.json
++++ b/apps/docs/package-lock.json
@@@ -21,7 +21,7 @@
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
"pdf-lib": "^1.17.1",
"pg": "^8.23.0",
"sharp": "^0.33.5",
@@@ -1758,9 +1758,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "4.56.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
-- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",
++ "version": "4.73.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
++ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
ddiff --git a/apps/docs/public/index.html b/apps/docs/public/index.html
iindex 6f9b79d..8522c91 100644
---- a/apps/docs/public/index.html
++++ b/apps/docs/public/index.html
@@@ -20,7 +20,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
@@@ -40,8 +39,8 @@
<button class="tool-tab" data-tool="split"><i class="fa-solid fa-scissors"></i> Split</button>
<button class="tool-tab" data-tool="compress"><i class="fa-solid fa-compress"></i> Compress</button>
<button class="tool-tab" data-tool="rotate"><i class="fa-solid fa-rotate"></i> Rotate</button>
-- <button class="tool-tab" data-tool="reorder"><i class="fa-solid fa-sort"></i> Reorder</button>
-- <button class="tool-tab" data-tool="watermark"><i class="fa-solid fa-stamp"></i> Watermark</button>
++ <button class="tool-tab" data-tool="reorder"><i class="fa-solid fa-arrow-down-wide-short"></i> Reorder</button>
++ <button class="tool-tab" data-tool="watermark"><i class="fa-solid fa-signature"></i> Watermark</button>
<button class="tool-tab" data-tool="img2pdf"><i class="fa-solid fa-file-image"></i> IMG→PDF</button>
<button class="tool-tab" data-tool="pdf2img"><i class="fa-solid fa-image"></i> PDF→IMG</button>
</nav>
@@@ -208,7 +207,7 @@
<i class="fa-solid fa-download"></i> Download
</a>
<button class="btn btn-secondary" id="another-btn">
-- <i class="fa-solid fa-arrow-rotate-left"></i> Process Another
++ <i class="fa-solid fa-rotate-left"></i> Process Another
</button>
</div>
</section>
ddiff --git a/apps/docs/public/js/app.js b/apps/docs/public/js/app.js
iindex d7558a9..ef56731 100644
---- a/apps/docs/public/js/app.js
++++ b/apps/docs/public/js/app.js
@@@ -671,10 +671,10 @@
{ host: 'splitpdf.openvibe.tools', name: 'SplitPDF', icon: 'fa-scissors', desc: 'Split by page range' },
{ host: 'compresspdf.openvibe.tools', name: 'CompressPDF', icon: 'fa-compress', desc: 'Reduce file size' },
{ host: 'rotatepdf.openvibe.tools', name: 'RotatePDF', icon: 'fa-rotate', desc: 'Rotate PDF pages' },
-- { host: 'reorderpdf.openvibe.tools', name: 'ReorderPDF', icon: 'fa-sort', desc: 'Rearrange pages' },
-- { host: 'watermarkpdf.openvibe.tools', name: 'WatermarkPDF', icon: 'fa-stamp', desc: 'Add watermarks' },
++ { host: 'reorderpdf.openvibe.tools', name: 'ReorderPDF', icon: 'fa-arrow-down-wide-short', desc: 'Rearrange pages' },
++ { host: 'watermarkpdf.openvibe.tools', name: 'WatermarkPDF', icon: 'fa-signature', desc: 'Add watermarks' },
{ host: 'protectpdf.openvibe.tools', name: 'ProtectPDF', icon: 'fa-lock', desc: 'Password protect' },
-- { host: 'unlockpdf.openvibe.tools', name: 'UnlockPDF', icon: 'fa-lock-open', desc: 'Remove password' },
++ { host: 'unlockpdf.openvibe.tools', name: 'UnlockPDF', icon: 'fa-unlock', desc: 'Remove password' },
{ host: 'image2pdf.openvibe.tools', name: 'Image2PDF', icon: 'fa-file-image', desc: 'Images to PDF' },
{ host: 'jpg2pdf.openvibe.tools', name: 'JPG2PDF', icon: 'fa-file-image', desc: 'JPG to PDF' },
{ host: 'png2pdf.openvibe.tools', name: 'PNG2PDF', icon: 'fa-file-image', desc: 'PNG to PDF' },
ddiff --git a/apps/docs/server/domain-map.js b/apps/docs/server/domain-map.js
iindex 261110a..eca7e59 100644
---- a/apps/docs/server/domain-map.js
++++ b/apps/docs/server/domain-map.js
@@@ -50,13 +50,13 @@ const DOMAIN_MAP = {
},
'reorderpdf.openvibe.tools': {
toolId: 'reorder', brandName: 'ReorderPDF', defaultOp: 'reorder',
-- faIcon: 'fa-sort',
++ faIcon: 'fa-arrow-down-wide-short',
seoTitle: 'ReorderPDF — Rearrange PDF Pages Online',
seoDescription: 'Drag and drop to reorder pages in your PDF. Remove unwanted pages. Online PDF page organizer.',
},
'watermarkpdf.openvibe.tools': {
toolId: 'watermark', brandName: 'WatermarkPDF', defaultOp: 'watermark',
-- faIcon: 'fa-stamp',
++ faIcon: 'fa-signature',
seoTitle: 'WatermarkPDF — Add Watermarks to PDFs Online',
seoDescription: 'Add text or image watermarks to PDF documents. Customize position, opacity, rotation, and font. Online.',
},
@@@ -68,7 +68,7 @@ const DOMAIN_MAP = {
},
'unlockpdf.openvibe.tools': {
toolId: 'unlock', brandName: 'UnlockPDF', defaultOp: 'unlock',
-- faIcon: 'fa-lock-open',
++ faIcon: 'fa-unlock',
seoTitle: 'UnlockPDF — Remove PDF Password Online',
seoDescription: 'Remove password protection from PDFs. Unlock PDF files for editing and printing. Online.',
},
ddiff --git a/apps/docs/server/index.js b/apps/docs/server/index.js
iindex 023536c..7d43522 100644
---- a/apps/docs/server/index.js
++++ b/apps/docs/server/index.js
@@@ -112,9 +112,9 @@ app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
-- scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://openvibe.network"],
-- styleSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
-- fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
++ scriptSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://openvibe.network"],
++ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
++ fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "blob:"],
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
connectSrc: ["'self'", "https://openvibe.network", "https://*.openvibe.tools", "https://openvibe.events"],
ddiff --git a/apps/docs/server/tools/index.js b/apps/docs/server/tools/index.js
iindex 12b4e1e..97f20db 100644
---- a/apps/docs/server/tools/index.js
++++ b/apps/docs/server/tools/index.js
@@@ -60,7 +60,7 @@ const TOOLS = {
id: 'reorder',
label: 'Reorder Pages',
description: 'Rearrange or remove pages in a PDF',
-- faIcon: 'fa-sort',
++ faIcon: 'fa-arrow-down-wide-short',
accepts: ['pdf'],
multiFile: false,
handler: reorder,
@@@ -69,7 +69,7 @@ const TOOLS = {
id: 'watermark',
label: 'Watermark',
description: 'Add text watermarks to PDF pages',
-- faIcon: 'fa-stamp',
++ faIcon: 'fa-signature',
accepts: ['pdf'],
multiFile: false,
handler: watermark,
@@@ -88,7 +88,7 @@ const TOOLS = {
id: 'unlock',
label: 'Unlock PDF',
description: 'Remove password from a PDF',
-- faIcon: 'fa-lock-open',
++ faIcon: 'fa-unlock',
accepts: ['pdf'],
multiFile: false,
requires: [qpdf], // decryption
ddiff --git a/apps/food/package-lock.json b/apps/food/package-lock.json
iindex 026775a..841a56d 100644
---- a/apps/food/package-lock.json
++++ b/apps/food/package-lock.json
@@@ -17,7 +17,7 @@
"helmet": "^7.1.0",
"iovalkey": "^0.4.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
"pg": "^8.23.0"
}
},
@@@ -1129,9 +1129,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "4.56.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
-- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",
++ "version": "4.73.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
++ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
ddiff --git a/apps/food/public/index.html b/apps/food/public/index.html
iindex 7cd735a..439a8bb 100644
---- a/apps/food/public/index.html
++++ b/apps/food/public/index.html
@@@ -32,7 +32,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600&family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css">
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/src/toastify.min.css">
@@@ -45,7 +44,7 @@
<div class="loc-search">
<i class="fa-solid fa-location-dot"></i>
<input type="text" id="loc-input" placeholder="Enter your city or zip code...">
-- <button id="btn-gps" title="Use GPS"><i class="fa-solid fa-crosshairs"></i></button>
++ <button id="btn-gps" title="Use GPS"><i class="fa-solid fa-location-dot"></i></button>
<button id="btn-loc-search" class="btn-primary"><i class="fa-solid fa-magnifying-glass"></i> Find Food</button>
</div>
</div>
@@@ -57,7 +56,7 @@
<button class="tab active" data-tab="food-banks"><i class="fa-solid fa-hand-holding-heart"></i> Food Banks</button>
<button class="tab" data-tab="stores"><i class="fa-solid fa-cart-shopping"></i> Stores</button>
<button class="tab" data-tab="meal-plan"><i class="fa-solid fa-calendar-days"></i> Meal Planner</button>
-- <button class="tab" data-tab="food-db"><i class="fa-solid fa-apple-whole"></i> Food Database</button>
++ <button class="tab" data-tab="food-db"><i class="fa-solid fa-utensils"></i> Food Database</button>
<button class="tab" data-tab="map"><i class="fa-solid fa-map"></i> Map</button>
</nav>
@@@ -117,7 +116,7 @@
<input type="number" id="mp-days" value="3" min="1" max="7">
</div>
<div class="control-group">
-- <label><i class="fa-solid fa-toggle-on"></i> Options</label>
++ <label><i class="fa-solid fa-circle-check"></i> Options</label>
<div class="toggle-row">
<label class="toggle"><input type="checkbox" id="mp-camp" checked> <span>Camp-friendly</span></label>
<label class="toggle"><input type="checkbox" id="mp-shelf"> <span>Shelf-stable only</span></label>
@@@ -131,7 +130,7 @@
<!-- Food Database Panel -->
<section id="food-db" class="panel">
<div class="panel-header">
-- <h2><i class="fa-solid fa-apple-whole"></i> Budget Food Database</h2>
++ <h2><i class="fa-solid fa-utensils"></i> Budget Food Database</h2>
<p class="panel-desc">Browse 80+ budget items with prices from 5 stores, nutrition info, and camp-friendliness ratings</p>
</div>
<div class="db-controls">
@@@ -141,12 +140,12 @@
</div>
<div class="db-filters">
<button class="chip active" data-group="all">All</button>
-- <button class="chip" data-group="protein"><i class="fa-solid fa-drumstick-bite"></i> Protein</button>
-- <button class="chip" data-group="grains"><i class="fa-solid fa-wheat-awn"></i> Grains</button>
-- <button class="chip" data-group="vegetables"><i class="fa-solid fa-carrot"></i> Veggies</button>
-- <button class="chip" data-group="fruits"><i class="fa-solid fa-apple-whole"></i> Fruits</button>
-- <button class="chip" data-group="dairy"><i class="fa-solid fa-glass-water"></i> Dairy</button>
-- <button class="chip" data-group="fats"><i class="fa-solid fa-oil-can"></i> Fats</button>
++ <button class="chip" data-group="protein"><i class="fa-solid fa-fish"></i> Protein</button>
++ <button class="chip" data-group="grains"><i class="fa-solid fa-seedling"></i> Grains</button>
++ <button class="chip" data-group="vegetables"><i class="fa-solid fa-leaf"></i> Veggies</button>
++ <button class="chip" data-group="fruits"><i class="fa-solid fa-utensils"></i> Fruits</button>
++ <button class="chip" data-group="dairy"><i class="fa-solid fa-mug-saucer"></i> Dairy</button>
++ <button class="chip" data-group="fats"><i class="fa-solid fa-droplet"></i> Fats</button>
</div>
<div class="db-toggles">
<label class="toggle"><input type="checkbox" id="db-camp"> <span>Camp-friendly</span></label>
ddiff --git a/apps/food/public/js/app.js b/apps/food/public/js/app.js
iindex 32540d7..1ba4283 100644
---- a/apps/food/public/js/app.js
++++ b/apps/food/public/js/app.js
@@@ -71,7 +71,7 @@
</div>
${b.description ? `<div class="card-desc">${esc(b.description)}</div>` : ''}
<div class="card-actions">
-- ${b.lat && b.lon ? `<a class="card-btn" href="https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(b.lat)},${encodeURIComponent(b.lon)}" target="_blank"><i class="fa-solid fa-diamond-turn-right"></i> Directions</a>` : ''}
++ ${b.lat && b.lon ? `<a class="card-btn" href="https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(b.lat)},${encodeURIComponent(b.lon)}" target="_blank"><i class="fa-solid fa-route"></i> Directions</a>` : ''}
${site ? `<a class="card-btn" href="${esc(site)}" target="_blank" rel="noopener noreferrer"><i class="fa-solid fa-globe"></i> Website</a>` : ''}
</div>
</div>`; }).join('');
@@@ -101,7 +101,7 @@
${s.phone ? `<span><i class="fa-solid fa-phone"></i> ${esc(s.phone)}</span>` : ''}
</div>
<div class="card-actions">
-- ${s.lat && s.lon ? `<a class="card-btn" href="https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(s.lat)},${encodeURIComponent(s.lon)}" target="_blank"><i class="fa-solid fa-diamond-turn-right"></i> Directions</a>` : ''}
++ ${s.lat && s.lon ? `<a class="card-btn" href="https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(s.lat)},${encodeURIComponent(s.lon)}" target="_blank"><i class="fa-solid fa-route"></i> Directions</a>` : ''}
</div>
</div>`;
}).join('');
ddiff --git a/apps/food/server/index.js b/apps/food/server/index.js
iindex 6ff51e2..ed0bf37 100644
---- a/apps/food/server/index.js
++++ b/apps/food/server/index.js
@@@ -85,9 +85,9 @@ app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
-- scriptSrc: ["'self'", "'unsafe-inline'", 'cdn.jsdelivr.net', 'unpkg.com', 'cdnjs.cloudflare.com', 'fonts.googleapis.com', 'https://openvibe.network'],
-- styleSrc: ["'self'", "'unsafe-inline'", 'cdn.jsdelivr.net', 'unpkg.com', 'cdnjs.cloudflare.com', 'fonts.googleapis.com', 'fonts.gstatic.com'],
-- fontSrc: ["'self'", 'fonts.gstatic.com', 'cdnjs.cloudflare.com'],
++ scriptSrc: ["'self'", "'unsafe-inline'", 'cdn.jsdelivr.net', 'unpkg.com', 'fonts.googleapis.com', 'https://openvibe.network'],
++ styleSrc: ["'self'", "'unsafe-inline'", 'cdn.jsdelivr.net', 'unpkg.com', 'fonts.googleapis.com', 'fonts.gstatic.com'],
++ fontSrc: ["'self'", 'fonts.gstatic.com'],
imgSrc: ["'self'", 'data:', 'image.tmdb.org', '*.tile.openstreetmap.org', '*.basemaps.cartocdn.com', 'server.arcgisonline.com'],
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
connectSrc: ["'self'", 'nominatim.openstreetmap.org', 'https://openvibe.network', 'https://openvibe.events'],
ddiff --git a/apps/gateway/package-lock.json b/apps/gateway/package-lock.json
iindex 845f616..13a2fb0 100644
---- a/apps/gateway/package-lock.json
++++ b/apps/gateway/package-lock.json
@@@ -21,7 +21,7 @@
"jsonwebtoken": "^9.0.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
"pg": "^8.23.0",
"zxing-wasm": "3.1.5"
}
@@@ -1229,9 +1229,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "4.56.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
-- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",
++ "version": "4.73.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
++ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
ddiff --git a/apps/gateway/public/audio.html b/apps/gateway/public/audio.html
iindex c4f3114..cac1d21 100644
---- a/apps/gateway/public/audio.html
++++ b/apps/gateway/public/audio.html
@@@ -25,7 +25,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script src="/shared/theme-loader.js" defer></script>
<style>
:root {
@@@ -141,10 +140,10 @@
const AUDIO_TOOLS = {
'convert': { name: 'Converter', domain: 'convert.audio.openvibe.tools', icon: 'fa-arrow-right-arrow-left', category: 'Convert' },
'trim': { name: 'Trim', domain: 'trim.openvibe.tools', icon: 'fa-scissors', category: 'Edit' },
-- 'merge': { name: 'Merge', domain: 'merge.openvibe.tools', icon: 'fa-code-merge', category: 'Mix' },
++ 'merge': { name: 'Merge', domain: 'merge.openvibe.tools', icon: 'fa-code-fork', category: 'Mix' },
'pitch': { name: 'Pitch', domain: 'pitch.openvibe.tools', icon: 'fa-wave-square', category: 'Effects' },
'speed': { name: 'Speed', domain: 'speed.openvibe.tools', icon: 'fa-gauge-high', category: 'Edit' },
-- 'reverb': { name: 'Reverb', domain: 'reverb.openvibe.tools', icon: 'fa-church', category: 'Effects' },
++ 'reverb': { name: 'Reverb', domain: 'reverb.openvibe.tools', icon: 'fa-house-chimney', category: 'Effects' },
'eq': { name: 'Equalizer', domain: 'equalizer.openvibe.tools', icon: 'fa-sliders', category: 'Effects' },
'vocal': { name: 'Vocal Extract', domain: 'vocal.openvibe.tools', icon: 'fa-microphone-lines', category: 'Extract' },
};
ddiff --git a/apps/gateway/public/dev.html b/apps/gateway/public/dev.html
iindex b32e1f1..dbbbc24 100644
---- a/apps/gateway/public/dev.html
++++ b/apps/gateway/public/dev.html
@@@ -25,7 +25,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
--<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
:root{--bg:var(--bg-primary,#0b0c12);--bg2:var(--bg-secondary,#12131c);--bg3:var(--bg-tertiary,#1a1b28);--card:var(--bg-card,rgba(22,24,36,.85));--tx:var(--text-primary,#f1f4fb);--tx2:var(--text-secondary,#a0a8c4);--tx3:var(--text-muted,#5e6580);--ac:var(--accent,#a78bfa);--acl:var(--accent-light,#c4b5fd);--brd:var(--border,rgba(255,255,255,.07));--r:16px}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
@@@ -178,12 +177,12 @@ html:{n:'HTML Formatter',i:'fa-file-code',c:'data',d:'HTML formatter, minifier &
base64:{n:'Base64 Encoder & Decoder',i:'fa-lock',c:'enc',d:'Base64 encode & decode',p:'Hello, World!',m:['Encode','Decode']},
url:{n:'URL Encoder & Decoder',i:'fa-link',c:'enc',d:'URL encode, decode & parser',p:'https://openvibe.tools/search?q=hello world&lang=en#results',m:['Encode','Decode','Parse']},
jwt:{n:'JWT Decoder',i:'fa-key',c:'enc',d:'JWT decoder \u2014 header, payload & expiry',p:'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhvYm8iLCJpYXQiOjE1MTYyMzkwMjJ9.4S5J5Z5mSp5F8Kv3K5qFJpQ5Y5jF5p5t5m5k5e5h5d4',m:['Decode']},
--uuid:{n:'UUID Generator',i:'fa-fingerprint',c:'enc',d:'UUID v4 generator & validator',p:'',m:['Generate','Bulk 10','Bulk 100','Validate']},
++uuid:{n:'UUID Generator',i:'fa-id-card',c:'enc',d:'UUID v4 generator & validator',p:'',m:['Generate','Bulk 10','Bulk 100','Validate']},
hash:{n:'Hash Generator',i:'fa-hashtag',c:'enc',d:'SHA-256, SHA-1 & SHA-512 hash generator',p:'Hello, World!',m:['SHA-256','SHA-1','SHA-512']},
--hex:{n:'Hex Converter',i:'fa-barcode',c:'enc',d:'Hex encoder / decoder',p:'Hello',m:['To Hex','From Hex']},
++hex:{n:'Hex Converter',i:'fa-code',c:'enc',d:'Hex encoder / decoder',p:'Hello',m:['To Hex','From Hex']},
entities:{n:'String Escape Tool',i:'fa-shield-halved',c:'enc',d:'HTML, JS & URL escape / unescape',p:'<script>alert("hello")<\/script>',m:['HTML Enc','HTML Dec','JS Esc','JS Unesc','URL Enc','URL Dec']},
timestamp:{n:'Unix Timestamp Converter',i:'fa-clock',c:'time',d:'Unix timestamp \u2194 date converter',p:'',m:['Now','To Date','To Unix']},
--cron:{n:'Cron Expression Parser',i:'fa-calendar-check',c:'time',d:'Cron expression parser & next runs',p:'*/15 9-17 * * 1-5',m:['Parse']},
++cron:{n:'Cron Expression Parser',i:'fa-calendar-days',c:'time',d:'Cron expression parser & next runs',p:'*/15 9-17 * * 1-5',m:['Parse']},
beautify:{n:'Code Beautifier',i:'fa-wand-magic-sparkles',c:'qual',d:'Beautify JS, CSS, HTML & JSON',p:'{"a":1,"b":[2,3],"c":{"d":true}}',m:['JSON','HTML','CSS']},
jsminify:{n:"JavaScript Minifier",i:'fa-compress',c:'qual',d:"Shrink JavaScript with a real parser, not regex",p:"function greet(name) {\n // say hello\n const message = \"Hello, \" + name + \"!\";\n console.log(message);\n return message;\n}\ngreet(\"OpenVibe\");",m:["Minify", "Minify + mangle", "Whitespace only"]},
jsformat:{n:"JavaScript Beautifier",i:'fa-wand-magic-sparkles',c:'qual',d:"Turn minified or messy JavaScript into readable code",p:"function greet(n){const m=\"Hello, \"+n+\"!\";console.log(m);return m}greet(\"OpenVibe\");",m:["Beautify"]},
@@@ -198,10 +197,10 @@ jsonstringify:{n:"JSON Stringify",i:'fa-quote-right',c:'data',d:"Turn JSON into
html2md:{n:"HTML to Markdown",i:'fa-right-left',c:'data',d:"Convert HTML into clean Markdown",p:"\u003ch1>Hello World\u003c/h1>\n<p>This is <strong>bold</strong>, <em>italic</em> and a <a href=\"https://openvibe.network\">link</a>.</p>\n<ul><li>Item one</li><li>Item two</li></ul>\n<pre><code>code block</code></pre>",m:["Convert"]},
md2html:{n:"Markdown to HTML",i:'fa-right-left',c:'data',d:"Convert Markdown into HTML you can paste anywhere",p:"# Hello World\n\nThis is **bold** and *italic* with a [link](https://openvibe.network).\n\n- Item one\n- Item two\n\n> A quote\n\n`inline code`",m:["To HTML", "Preview"]},
minify:{n:'Code Minifier',i:'fa-compress',c:'qual',d:'Minify JS, CSS, HTML & JSON',p:'{\n "name": "openvibe",\n "version": 1\n}',m:['JSON','HTML','CSS']},
--codediff:{n:'Diff Checker',i:'fa-code-compare',c:'qual',d:'Side-by-side text diff checker',p:'Line one\nLine two\nLine three\nLine four',p2:'Line one\nLine 2 modified\nLine three\nLine five\nLine six',dual:1,m:['Compare']},
++codediff:{n:'Diff Checker',i:'fa-arrows-left-right',c:'qual',d:'Side-by-side text diff checker',p:'Line one\nLine two\nLine three\nLine four',p2:'Line one\nLine 2 modified\nLine three\nLine five\nLine six',dual:1,m:['Compare']},
regex:{n:'Regex Tester',i:'fa-magnifying-glass',c:'qual',d:'Regex tester with match highlighting',p:'\\b[A-Z][a-z]+\\b',p2:'Hello World from OpenVibe.Tools today',dual:1,m:['Test']},
slugify:{n:'Slug Generator',i:'fa-link',c:'qual',d:'URL slug generator',p:'Hello World! This is a Test -- 2024',m:['Generate']},
--lorem:{n:'Lorem Ipsum Generator',i:'fa-paragraph',c:'qual',d:'Lorem ipsum generator',p:'',m:['1 Para','3 Para','5 Para','10 Sent','50 Words']},
++lorem:{n:'Lorem Ipsum Generator',i:'fa-align-left',c:'qual',d:'Lorem ipsum generator',p:'',m:['1 Para','3 Para','5 Para','10 Sent','50 Words']},
curlconvert:{n:'Online curl',i:'fa-terminal',c:'http',d:'Parse curl \u2192 fetch, Python, Node.js',p:"curl -X POST https://api.example.com/users -H 'Content-Type: application/json' -H 'Authorization: Bearer tok123' -d '{\"name\":\"openvibe\"}'",m:['To fetch','To Python','To Node','Parsed']},
webhook:{n:'Webhook Tester',i:'fa-satellite-dish',c:'http',d:'Webhook request inspector bin',p:'',m:['Create Bin']},
color:{n:'Color Converter',i:'fa-palette',c:'fe',d:'Color picker & HEX/RGB/HSL converter',p:'#a78bfa',m:['Convert']},
@@@ -287,7 +286,7 @@ function showTool(){
if(!framed){
let ah='<button class="ab ab-go" onclick="run()"><i class="fa-solid fa-play"></i> Run</button>';
ah+='<button class="ab ab-s" onclick="copyOut()"><i class="fa-solid fa-copy"></i> Copy</button>';
-- ah+='<button class="ab ab-s" onclick="saveAsPaste(this)" title="Save the output as an unlisted paste on OpenVibe.Community"><i class="fa-solid fa-note-sticky"></i> Save as paste</button>';
++ ah+='<button class="ab ab-s" onclick="saveAsPaste(this)" title="Save the output as an unlisted paste on OpenVibe.Community"><i class="fa-solid fa-paste"></i> Save as paste</button>';
ah+='<button class="ab ab-s" onclick="clr()"><i class="fa-solid fa-trash"></i> Clear</button>';
ah+='<button class="ab ab-s" onclick="sample()"><i class="fa-solid fa-flask"></i> Sample</button>';
$('acts').innerHTML=ah;
ddiff --git a/apps/gateway/public/img.html b/apps/gateway/public/img.html
iindex 32bd9b8..5b6743c 100644
---- a/apps/gateway/public/img.html
++++ b/apps/gateway/public/img.html
@@@ -25,7 +25,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script src="/shared/theme-loader.js" defer></script>
<style>
:root {
@@@ -142,7 +141,7 @@
'convert': { name: 'Converter', domain: 'convert.openvibe.tools', icon: 'fa-arrow-right-arrow-left', category: 'Convert' },
'compress': { name: 'Compress', domain: 'compress.openvibe.tools', icon: 'fa-compress', category: 'Optimize' },
'resize': { name: 'Resize', domain: 'resize.openvibe.tools', icon: 'fa-expand', category: 'Resize' },
-- 'crop': { name: 'Crop', domain: 'crop.openvibe.tools', icon: 'fa-crop', category: 'Edit' },
++ 'crop': { name: 'Crop', domain: 'crop.openvibe.tools', icon: 'fa-scissors', category: 'Edit' },
'png': { name: 'PNG', domain: 'png.openvibe.tools', icon: 'fa-image', category: 'Format' },
'jpg': { name: 'JPG', domain: 'jpg.openvibe.tools', icon: 'fa-image', category: 'Format' },
'webp': { name: 'WebP', domain: 'webp.openvibe.tools', icon: 'fa-image', category: 'Format' },
ddiff --git a/apps/gateway/public/net.html b/apps/gateway/public/net.html
iindex 7249ad1..368c1a2 100644
---- a/apps/gateway/public/net.html
++++ b/apps/gateway/public/net.html
@@@ -25,7 +25,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
:root {
--bg-primary: #0b0c12;
@@@ -671,21 +670,21 @@
net: { name: 'Network Tools', icon: 'fa-network-wired', cat: 'hub', endpoint: null, placeholder: 'Enter a domain, IP, or URL…' },
lookup: { name: 'Domain & IP Lookup', icon: 'fa-magnifying-glass', cat: 'hub', endpoint: '/lookup', placeholder: 'Domain, IP, or URL — we\'ll look up everything', btnText: 'Lookup All' },
// IP / Identity
-- myip: { name: 'What Is My IP', icon: 'fa-location-crosshairs', cat: 'ip', endpoint: '/myip', placeholder: null, auto: true },
++ myip: { name: 'What Is My IP', icon: 'fa-location-dot', cat: 'ip', endpoint: '/myip', placeholder: null, auto: true },
ip: { name: 'IP Address Lookup', icon: 'fa-at', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
ipv4: { name: 'IPv4 Lookup & CIDR Calculator', icon: 'fa-hashtag', cat: 'ip', endpoint: '/ipv4', placeholder: 'IPv4 address, CIDR block (e.g. 192.168.1.0/24) or domain' },
ipv6: { name: 'IPv6 Lookup', icon: 'fa-code', cat: 'ip', endpoint: '/ipv6', placeholder: 'IPv6 address or domain' },
geoip: { name: 'IP Geolocation', icon: 'fa-earth-americas', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
hostname: { name: 'Hostname Lookup', icon: 'fa-server', cat: 'ip', endpoint: '/rdns', placeholder: 'IP address to resolve hostname' },
-- isp: { name: 'ISP Lookup', icon: 'fa-building', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
++ isp: { name: 'ISP Lookup', icon: 'fa-city', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
asn: { name: 'ASN Lookup', icon: 'fa-diagram-project', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
rdns: { name: 'Reverse DNS Lookup', icon: 'fa-rotate-left', cat: 'ip', endpoint: '/rdns', placeholder: 'IP address for reverse DNS' },
-- whois: { name: 'Whois Lookup', icon: 'fa-address-book', cat: 'ip', endpoint: '/rdap', placeholder: 'Domain or IP' },
++ whois: { name: 'Whois Lookup', icon: 'fa-address-card', cat: 'ip', endpoint: '/rdap', placeholder: 'Domain or IP' },
rdap: { name: 'RDAP Lookup', icon: 'fa-id-card', cat: 'ip', endpoint: '/rdap', placeholder: 'Domain or IP for RDAP lookup' },
// DNS
dns: { name: 'DNS Lookup', icon: 'fa-sitemap', cat: 'dns', endpoint: '/dns', placeholder: 'Domain to query' },
dig: { name: 'Online dig', icon: 'fa-terminal', cat: 'dns', endpoint: '/dns', placeholder: 'Domain (dig-style query)', dnsTypes: true },
-- nslookup: { name: 'NSLookup', icon: 'fa-magnifying-glass-arrow-right', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for nslookup' },
++ nslookup: { name: 'NSLookup', icon: 'fa-magnifying-glass', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for nslookup' },
dnspropagation: { name: 'DNS Propagation Checker', icon: 'fa-globe', cat: 'dns', endpoint: '/dnsprop', placeholder: 'Domain to compare across public resolvers', propTypes: true },
mx: { name: 'MX Record Lookup', icon: 'fa-envelope', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for MX records', defaultTypes: 'MX' },
txt: { name: 'TXT Record Lookup', icon: 'fa-file-lines', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for TXT records', defaultTypes: 'TXT' },
@@@ -699,7 +698,7 @@
mtr: { name: 'MTR Report', icon: 'fa-chart-line', cat: 'diag', endpoint: '/mtr', placeholder: 'Host for an MTR report (10 probes a hop, from our server)', wait: 'about 15 seconds' },
port: { name: 'Port Checker', icon: 'fa-door-open', cat: 'diag', endpoint: '/port', placeholder: 'Host to scan ports' },
headers: { name: 'HTTP Header Checker', icon: 'fa-list', cat: 'diag', endpoint: '/headers', placeholder: 'URL or domain' },
-- redirects: { name: 'Redirect Checker', icon: 'fa-share', cat: 'diag', endpoint: '/redirects', placeholder: 'URL to follow redirects' },
++ redirects: { name: 'Redirect Checker', icon: 'fa-arrow-up-from-bracket', cat: 'diag', endpoint: '/redirects', placeholder: 'URL to follow redirects' },
ssl: { name: 'SSL Certificate Checker', icon: 'fa-lock', cat: 'diag', endpoint: '/ssl', placeholder: 'Domain for SSL check' },
curl: { name: 'Online curl', icon: 'fa-download', cat: 'diag', endpoint: '/headers', placeholder: 'URL' },
httpstatus: { name: 'HTTP Status Codes', icon: 'fa-circle-check', cat: 'diag', endpoint: '/headers', placeholder: 'URL to check' },
@@@ -715,7 +714,7 @@
};
const CATS = {
-- ip: { label: 'IP / Identity / Ownership', icon: 'fa-fingerprint' },
++ ip: { label: 'IP / Identity / Ownership', icon: 'fa-id-card' },
dns: { label: 'DNS Tools', icon: 'fa-sitemap' },
diag: { label: 'Active Diagnostics', icon: 'fa-satellite-dish' },
mail: { label: 'Email & Reputation', icon: 'fa-envelope' },
ddiff --git a/apps/gateway/server/dev/config.js b/apps/gateway/server/dev/config.js
iindex 91ad32c..86c886d 100644
---- a/apps/gateway/server/dev/config.js
++++ b/apps/gateway/server/dev/config.js
@@@ -31,22 +31,22 @@ const DEV_TOOLS = [
{ id: 'base64', subdomain: 'base64', name: 'OpenVibeBase64', icon: 'fa-lock', desc: 'Base64 encode & decode text and files', category: 'encoding' },
{ id: 'url', subdomain: 'url', name: 'OpenVibeURL', icon: 'fa-link', desc: 'URL encode, decode & component parser', category: 'encoding' },
{ id: 'jwt', subdomain: 'jwt', name: 'OpenVibeJWT', icon: 'fa-key', desc: 'JWT decoder — inspect header, payload & expiry', category: 'encoding' },
-- { id: 'uuid', subdomain: 'uuid', name: 'OpenVibeUUID', icon: 'fa-fingerprint', desc: 'UUID v4 generator, bulk generate & validator', category: 'encoding' },
++ { id: 'uuid', subdomain: 'uuid', name: 'OpenVibeUUID', icon: 'fa-id-card', desc: 'UUID v4 generator, bulk generate & validator', category: 'encoding' },
{ id: 'hash', subdomain: 'hash', name: 'OpenVibeHash', icon: 'fa-hashtag', desc: 'SHA-1, SHA-256 & SHA-512 hash generator', category: 'encoding' },
-- { id: 'hex', subdomain: 'hex', name: 'OpenVibeHex', icon: 'fa-barcode', desc: 'Hex encoder/decoder & binary converter', category: 'encoding' },
++ { id: 'hex', subdomain: 'hex', name: 'OpenVibeHex', icon: 'fa-code', desc: 'Hex encoder/decoder & binary converter', category: 'encoding' },
{ id: 'escape', subdomain: 'entities', name: 'OpenVibeEscape', icon: 'fa-shield-halved', desc: 'HTML, JavaScript & URL escape/unescape', category: 'encoding' },
// Time & Scheduling
{ id: 'timestamp', subdomain: 'timestamp', name: 'OpenVibeTimestamp', icon: 'fa-clock', desc: 'Unix timestamp ↔ date converter with timezones', category: 'time' },
-- { id: 'cron', subdomain: 'cron', name: 'OpenVibeCron', icon: 'fa-calendar-check', desc: 'Cron expression parser with next run calculator', category: 'time' },
++ { id: 'cron', subdomain: 'cron', name: 'OpenVibeCron', icon: 'fa-calendar-days', desc: 'Cron expression parser with next run calculator', category: 'time' },
// Code Quality
{ id: 'beautify', subdomain: 'beautify', name: 'OpenVibeBeautify', icon: 'fa-wand-magic-sparkles', desc: 'Beautify & format JS, HTML, CSS, JSON & SQL', category: 'quality' },
{ id: 'minify', subdomain: 'minify', name: 'OpenVibeMinify', icon: 'fa-compress', desc: 'Minify JS, HTML, CSS & JSON', category: 'quality' },
-- { id: 'diff', subdomain: 'codediff', name: 'OpenVibeDiff', icon: 'fa-code-compare', desc: 'Side-by-side text & code diff checker', category: 'quality' },
++ { id: 'diff', subdomain: 'codediff', name: 'OpenVibeDiff', icon: 'fa-arrows-left-right', desc: 'Side-by-side text & code diff checker', category: 'quality' },
{ id: 'regex', subdomain: 'regex', name: 'OpenVibeRegex', icon: 'fa-magnifying-glass', desc: 'Regex tester with match highlighting & capture groups', category: 'quality' },
{ id: 'slug', subdomain: 'slugify', name: 'OpenVibeSlug', icon: 'fa-link', desc: 'URL slug generator from any text', category: 'quality' },
-- { id: 'lorem', subdomain: 'lorem', name: 'OpenVibeLorem', icon: 'fa-paragraph', desc: 'Lorem ipsum & placeholder text generator', category: 'quality' },
++ { id: 'lorem', subdomain: 'lorem', name: 'OpenVibeLorem', icon: 'fa-align-left', desc: 'Lorem ipsum & placeholder text generator', category: 'quality' },
// HTTP & API
{ id: 'curl', subdomain: 'curlconvert', name: 'OpenVibeCurl', icon: 'fa-terminal', desc: 'Parse curl commands & convert to fetch, Python, Node.js', category: 'http' },
ddiff --git a/apps/gateway/server/index.js b/apps/gateway/server/index.js
iindex b9add1e..e2b4870 100644
---- a/apps/gateway/server/index.js
++++ b/apps/gateway/server/index.js
@@@ -130,9 +130,9 @@ app.use(helmet({
directives: {
defaultSrc: ["'self'"],
// 'wasm-unsafe-eval': the barcode scanner's ZXing reader is WebAssembly (codes.html), served from /vendor here.
-- scriptSrc: ["'self'", "'unsafe-inline'", "'wasm-unsafe-eval'", "https://openvibe.network", "https://openvibe.live", "cdnjs.cloudflare.com", "cdn.jsdelivr.net", "fonts.googleapis.com", "https://static.cloudflareinsights.com"],
-- styleSrc: ["'self'", "'unsafe-inline'", "cdnjs.cloudflare.com", "fonts.googleapis.com", "fonts.gstatic.com"],
-- fontSrc: ["'self'", "fonts.gstatic.com", "cdnjs.cloudflare.com"],
++ scriptSrc: ["'self'", "'unsafe-inline'", "'wasm-unsafe-eval'", "https://openvibe.network", "https://openvibe.live", "cdn.jsdelivr.net", "fonts.googleapis.com", "https://static.cloudflareinsights.com"],
++ styleSrc: ["'self'", "'unsafe-inline'", "fonts.googleapis.com", "fonts.gstatic.com"],
++ fontSrc: ["'self'", "fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "blob:", "https://openvibe.network", "https://openvibe.media"],
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
connectSrc: ["'self'", "https://cloudflareinsights.com", "https://openvibe.network", "https://openvibe.tools", "https://*.openvibe.tools", "https://openvibe.media", "https://openvibe.live", "https://openvibe.games", "https://openvibe.events"],
ddiff --git a/apps/gateway/server/net/config.js b/apps/gateway/server/net/config.js
iindex 60a7a76..cfefdce 100644
---- a/apps/gateway/server/net/config.js
++++ b/apps/gateway/server/net/config.js
@@@ -94,21 +94,21 @@ const NET_TOOLS = [
{ id: 'net', subdomain: 'net', name: 'Net.OpenVibe', icon: 'fa-network-wired', desc: 'Network & Internet Diagnostics Hub', hub: true },
{ id: 'lookup', subdomain: 'lookup', name: 'OpenVibeLookup', icon: 'fa-magnifying-glass', desc: 'All-in-one domain/IP lookup supertool', endpoint: '/lookup' },
// IP / Identity
-- { id: 'myip', subdomain: 'myip', name: 'OpenVibeMyIP', icon: 'fa-location-crosshairs', desc: 'Your public IP, ISP, location & reverse DNS', endpoint: '/myip' },
++ { id: 'myip', subdomain: 'myip', name: 'OpenVibeMyIP', icon: 'fa-location-dot', desc: 'Your public IP, ISP, location & reverse DNS', endpoint: '/myip' },
{ id: 'ip', subdomain: 'ip', name: 'OpenVibeIP', icon: 'fa-at', desc: 'IP address lookup & details (IPv4 & IPv6)', endpoint: '/ip' },
{ id: 'ipv4', subdomain: 'ipv4', name: 'OpenVibeIPv4', icon: 'fa-hashtag', desc: 'IPv4 address lookup, class, CIDR calculator', endpoint: '/ipv4' },
{ id: 'ipv6', subdomain: 'ipv6', name: 'OpenVibeIPv6', icon: 'fa-code', desc: 'IPv6 address lookup, expansion, type detection', endpoint: '/ipv6' },
{ id: 'geoip', subdomain: 'geoip', name: 'OpenVibeGeoIP', icon: 'fa-earth-americas', desc: 'IP geolocation — country, city, timezone, map', endpoint: '/ip' },
{ id: 'hostname', subdomain: 'hostname', name: 'OpenVibeHostname', icon: 'fa-server', desc: 'Hostname & reverse DNS lookup', endpoint: '/rdns' },
-- { id: 'isp', subdomain: 'isp', name: 'OpenVibeISP', icon: 'fa-building', desc: 'ISP & organization info from IP', endpoint: '/ip' },
++ { id: 'isp', subdomain: 'isp', name: 'OpenVibeISP', icon: 'fa-city', desc: 'ISP & organization info from IP', endpoint: '/ip' },
{ id: 'asn', subdomain: 'asn', name: 'OpenVibeASN', icon: 'fa-diagram-project', desc: 'Autonomous System Number lookup', endpoint: '/ip' },
{ id: 'rdns', subdomain: 'rdns', name: 'OpenVibeReverseDNS', icon: 'fa-rotate-left', desc: 'Reverse DNS (PTR) lookup for IPv4 & IPv6', endpoint: '/rdns' },
-- { id: 'whois', subdomain: 'whois', name: 'OpenVibeWhois', icon: 'fa-address-book', desc: 'Whois domain registration lookup', endpoint: '/rdap' },
++ { id: 'whois', subdomain: 'whois', name: 'OpenVibeWhois', icon: 'fa-address-card', desc: 'Whois domain registration lookup', endpoint: '/rdap' },
{ id: 'rdap', subdomain: 'rdap', name: 'OpenVibeRDAP', icon: 'fa-id-card', desc: 'RDAP — modern registration data lookup', endpoint: '/rdap' },
// DNS
{ id: 'dns', subdomain: 'dns', name: 'OpenVibeDNS', icon: 'fa-sitemap', desc: 'DNS record lookup — A, AAAA, MX, TXT, and more', endpoint: '/dns' },
{ id: 'dig', subdomain: 'dig', name: 'OpenVibeDig', icon: 'fa-terminal', desc: 'Online dig command — query any DNS record', endpoint: '/dns' },
-- { id: 'nslookup', subdomain: 'nslookup', name: 'OpenVibeNSLookup', icon: 'fa-magnifying-glass-arrow-right', desc: 'NSLookup — simple DNS query tool', endpoint: '/dns' },
++ { id: 'nslookup', subdomain: 'nslookup', name: 'OpenVibeNSLookup', icon: 'fa-magnifying-glass', desc: 'NSLookup — simple DNS query tool', endpoint: '/dns' },
{ id: 'dnspropagation', subdomain: 'dnspropagation', name: 'OpenVibeDNSPropagation', icon: 'fa-globe', desc: 'Check DNS propagation across world regions', endpoint: '/dnsprop' },
{ id: 'mx', subdomain: 'mx', name: 'OpenVibeMX', icon: 'fa-envelope', desc: 'MX record lookup — mail server discovery', endpoint: '/dns' },
{ id: 'txt', subdomain: 'txt', name: 'OpenVibeTXT', icon: 'fa-file-lines', desc: 'TXT record lookup — SPF, DKIM, verification', endpoint: '/dns' },
@@@ -122,7 +122,7 @@ const NET_TOOLS = [
{ id: 'mtr', subdomain: 'mtr', name: 'OpenVibeMTR', icon: 'fa-chart-line', desc: 'MTR — combined ping + traceroute analysis', endpoint: '/mtr' },
{ id: 'port', subdomain: 'port', name: 'OpenVibePortCheck', icon: 'fa-door-open', desc: 'Port scanner — check if ports are open', endpoint: '/port' },
{ id: 'headers', subdomain: 'headers', name: 'OpenVibeHeaders', icon: 'fa-list', desc: 'HTTP headers checker — security & cache analysis', endpoint: '/headers' },
-- { id: 'redirects', subdomain: 'redirects', name: 'OpenVibeRedirects', icon: 'fa-share', desc: 'Redirect chain tracer — follow all hops', endpoint: '/redirects' },
++ { id: 'redirects', subdomain: 'redirects', name: 'OpenVibeRedirects', icon: 'fa-arrow-up-from-bracket', desc: 'Redirect chain tracer — follow all hops', endpoint: '/redirects' },
{ id: 'ssl', subdomain: 'ssl', name: 'OpenVibeSSL', icon: 'fa-lock', desc: 'SSL/TLS certificate checker & chain inspector', endpoint: '/ssl' },
{ id: 'curl', subdomain: 'curl', name: 'OpenVibeCurl', icon: 'fa-download', desc: 'Online curl — HTTP request tester', endpoint: '/headers' },
{ id: 'httpstatus', subdomain: 'httpstatus', name: 'OpenVibeHTTPStatus', icon: 'fa-circle-check', desc: 'HTTP status code reference & checker', endpoint: '/headers' },
ddiff --git a/apps/img/package-lock.json b/apps/img/package-lock.json
iindex 16267d9..398f289 100644
---- a/apps/img/package-lock.json
++++ b/apps/img/package-lock.json
@@@ -21,7 +21,7 @@
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
"pg": "^8.23.0",
"sharp": "^0.33.5",
"to-ico": "^1.1.5",
@@@ -2264,9 +2264,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "4.56.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
-- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",
++ "version": "4.73.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
++ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
ddiff --git a/apps/img/public/index.html b/apps/img/public/index.html
iindex 21da3ed..7ef84d5 100644
---- a/apps/img/public/index.html
++++ b/apps/img/public/index.html
@@@ -20,7 +20,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
@@@ -39,7 +38,7 @@
<button class="tool-tab active" data-tool="convert"><i class="fa-solid fa-arrows-rotate"></i> Convert</button>
<button class="tool-tab" data-tool="compress"><i class="fa-solid fa-compress"></i> Compress</button>
<button class="tool-tab" data-tool="resize"><i class="fa-solid fa-up-right-and-down-left-from-center"></i> Resize</button>
-- <button class="tool-tab" data-tool="crop"><i class="fa-solid fa-crop-simple"></i> Crop</button>
++ <button class="tool-tab" data-tool="crop"><i class="fa-solid fa-scissors"></i> Crop</button>
</nav>
<!-- Upload Zone -->
@@@ -152,7 +151,7 @@
<i class="fa-solid fa-download"></i> Download
</a>
<button class="btn btn-secondary" id="another-btn">
-- <i class="fa-solid fa-arrow-rotate-left"></i> Process Another
++ <i class="fa-solid fa-rotate-left"></i> Process Another
</button>
</div>
</section>
ddiff --git a/apps/img/public/js/app.js b/apps/img/public/js/app.js
iindex 7b80c14..b6d76e8 100644
---- a/apps/img/public/js/app.js
++++ b/apps/img/public/js/app.js
@@@ -519,14 +519,14 @@
{ host: 'webp.openvibe.tools', name: 'OpenVibeWebP', icon: 'fa-file-image', desc: 'Convert to WebP' },
{ host: 'avif.openvibe.tools', name: 'OpenVibeAVIF', icon: 'fa-file-image', desc: 'Convert to AVIF' },
{ host: 'heic.openvibe.tools', name: 'OpenVibeHEIC', icon: 'fa-file-image', desc: 'HEIC Converter' },
-- { host: 'svg.openvibe.tools', name: 'OpenVibeSVG', icon: 'fa-bezier-curve', desc: 'SVG Converter' },
++ { host: 'svg.openvibe.tools', name: 'OpenVibeSVG', icon: 'fa-shapes', desc: 'SVG Converter' },
{ host: 'gif.openvibe.tools', name: 'OpenVibeGIF', icon: 'fa-film', desc: 'GIF Converter' },
{ host: 'ico.openvibe.tools', name: 'OpenVibeICO', icon: 'fa-icons', desc: 'ICO Favicon Maker' },
{ host: 'tiff.openvibe.tools', name: 'OpenVibeTIFF', icon: 'fa-file-image', desc: 'Convert to TIFF' },
{ host: 'bmp.openvibe.tools', name: 'OpenVibeBMP', icon: 'fa-file-image', desc: 'Convert to BMP' },
{ host: 'compress.openvibe.tools', name: 'OpenVibeCompress', icon: 'fa-compress', desc: 'Compress Images' },
{ host: 'resize.openvibe.tools', name: 'OpenVibeResize', icon: 'fa-up-right-and-down-left-from-center', desc: 'Resize Images' },
-- { host: 'crop.openvibe.tools', name: 'OpenVibeCrop', icon: 'fa-crop-simple', desc: 'Crop Images' },
++ { host: 'crop.openvibe.tools', name: 'OpenVibeCrop', icon: 'fa-scissors', desc: 'Crop Images' },
{ host: 'convert.openvibe.tools', name: 'OpenVibeConvert', icon: 'fa-arrows-rotate', desc: 'Format Converter' },
{ host: 'favicon.openvibe.tools', name: 'OpenVibeFavicon', icon: 'fa-icons', desc: 'Favicon Generator' },
];
ddiff --git a/apps/img/server/domain-map.js b/apps/img/server/domain-map.js
iindex d4abcbf..d45c252 100644
---- a/apps/img/server/domain-map.js
++++ b/apps/img/server/domain-map.js
@@@ -60,7 +60,7 @@ const DOMAIN_MAP = {
},
'svg.openvibe.tools': {
toolId: 'svg', brandName: 'OpenVibeSVG', defaultOp: 'convert', defaultFormat: 'png',
-- faIcon: 'fa-bezier-curve',
++ faIcon: 'fa-shapes',
seoTitle: 'OpenVibeSVG — Convert SVG Images Online',
seoDescription: 'Convert SVG to PNG, JPG, WebP. Convert bitmap images to SVG vector traces. Online SVG converter.',
},
@@@ -104,7 +104,7 @@ const DOMAIN_MAP = {
},
'crop.openvibe.tools': {
toolId: 'crop', brandName: 'OpenVibeCrop', defaultOp: 'crop',
-- faIcon: 'fa-crop-simple',
++ faIcon: 'fa-scissors',
seoTitle: 'OpenVibeCrop — Crop Images Online',
seoDescription: 'Crop images by custom dimensions or preset aspect ratios (16:9, 4:3, 1:1). Online image cropper.',
},
ddiff --git a/apps/img/server/index.js b/apps/img/server/index.js
iindex 6c68cc0..4c17c0c 100644
---- a/apps/img/server/index.js
++++ b/apps/img/server/index.js
@@@ -101,9 +101,9 @@ app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
-- scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://openvibe.network"],
-- styleSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
-- fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
++ scriptSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://openvibe.network"],
++ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
++ fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "blob:", jobsRuntime.mediaOrigin()], // job result previews are 302s to Media
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
connectSrc: ["'self'", "https://openvibe.network", "https://*.openvibe.tools", "https://openvibe.events"],
ddiff --git a/apps/img/server/tools/index.js b/apps/img/server/tools/index.js
iindex 762d207..5bfc9dd 100644
---- a/apps/img/server/tools/index.js
++++ b/apps/img/server/tools/index.js
@@@ -43,7 +43,7 @@ const TOOLS = {
id: 'crop',
label: 'Crop',
description: 'Crop images to specific areas or aspect ratios',
-- faIcon: 'fa-crop-simple',
++ faIcon: 'fa-scissors',
accepts: ['png', 'jpg', 'jpeg', 'webp', 'avif', 'tiff', 'bmp', 'gif', 'heic', 'heif', 'svg'],
outputs: null, // same as input
handler: crop,
ddiff --git a/apps/maps/package-lock.json b/apps/maps/package-lock.json
iindex aef9599..6873a9a 100644
---- a/apps/maps/package-lock.json
++++ b/apps/maps/package-lock.json
@@@ -21,7 +21,7 @@
"iovalkey": "^0.4.0",
"node-cache": "^5.1.2",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
"pg": "^8.23.0"
}
},
@@@ -1446,9 +1446,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "4.56.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
-- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",
++ "version": "4.73.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
++ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
ddiff --git a/apps/maps/public/index.html b/apps/maps/public/index.html
iindex e70efa2..427e1a2 100644
---- a/apps/maps/public/index.html
++++ b/apps/maps/public/index.html
@@@ -35,8 +35,6 @@
</script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600&family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
-- <!-- Font Awesome 6 -->
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Leaflet -->
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css">
@@@ -73,7 +71,7 @@
</div>
<div class="splash-sources">
<span><i class="fa-solid fa-database"></i> RIDB</span>
-- <span><i class="fa-brands fa-openstreetmap"></i> OSM</span>
++ <span><i class="fa-solid fa-map"></i> OSM</span>
<span><i class="fa-solid fa-campground"></i> FreeCampsites</span>
<span><i class="fa-solid fa-globe"></i> iOverlander</span>
<span><i class="fa-solid fa-bridge"></i> Bridges</span>
@@@ -135,7 +133,7 @@
<div class="search-box">
<i class="fa-solid fa-magnifying-glass search-icon"></i>
<input type="text" id="search-input" placeholder="Address, city, or coordinates..." autocomplete="off">
-- <button id="btn-my-location" title="Use GPS"><i class="fa-solid fa-crosshairs"></i></button>
++ <button id="btn-my-location" title="Use GPS"><i class="fa-solid fa-location-dot"></i></button>
<select id="search-radius" title="Search radius">
<option value="5">5 mi</option>
<option value="10">10 mi</option>
@@@ -152,8 +150,8 @@
<button id="btn-food" class="header-btn" title="Food Banks & Grocery"><i class="fa-solid fa-utensils"></i></button>
<button id="btn-favorites" class="header-btn" title="Favorites"><i class="fa-solid fa-heart"></i></button>
<button id="btn-add-spot" class="header-btn" title="Add Custom Spot"><i class="fa-solid fa-plus"></i></button>
-- <button id="btn-guide" class="header-btn" title="Survival Guide"><i class="fa-solid fa-book-skull"></i></button>
-- <button id="btn-stats" class="header-btn" title="Statistics"><i class="fa-solid fa-chart-pie"></i></button>
++ <button id="btn-guide" class="header-btn" title="Survival Guide"><i class="fa-solid fa-book"></i></button>
++ <button id="btn-stats" class="header-btn" title="Statistics"><i class="fa-solid fa-chart-column"></i></button>
<button id="btn-theme" class="header-btn" title="Toggle theme"><i class="fa-solid fa-moon"></i></button>
</div>
</header>
@@@ -187,7 +185,7 @@
</select>
<button id="btn-heatmap" class="filter-tool-btn" title="Stealth heatmap"><i class="fa-solid fa-fire"></i></button>
<button id="btn-crime-heatmap" class="filter-tool-btn" title="Crime/sketch heatmap"><i class="fa-solid fa-skull-crossbones"></i></button>
-- <button id="btn-satellite" class="filter-tool-btn" title="Satellite view"><i class="fa-solid fa-satellite"></i></button>
++ <button id="btn-satellite" class="filter-tool-btn" title="Satellite view"><i class="fa-solid fa-satellite-dish"></i></button>
</div>
</div>
@@@ -214,7 +212,7 @@
<div class="mini-stat"><i class="fa-solid fa-database"></i> 17+ Sources</div>
<div class="mini-stat"><i class="fa-solid fa-map-pin"></i> 100+ Spots</div>
<div class="mini-stat"><i class="fa-solid fa-cloud-sun"></i> Live Weather</div>
-- <div class="mini-stat"><i class="fa-solid fa-globe-americas"></i> North America</div>
++ <div class="mini-stat"><i class="fa-solid fa-earth-americas"></i> North America</div>
</div>
</div>
<div id="results-list"></div>
@@@ -224,11 +222,11 @@
<div id="map-container">
<div id="map"></div>
<div class="map-ctrl-group">
-- <button id="btn-map-locate" class="map-ctrl-btn" title="My location"><i class="fa-solid fa-street-view"></i></button>
++ <button id="btn-map-locate" class="map-ctrl-btn" title="My location"><i class="fa-solid fa-map-location-dot"></i></button>
<button id="btn-map-fullscreen" class="map-ctrl-btn" title="Fullscreen"><i class="fa-solid fa-expand"></i></button>
</div>
<button id="btn-search-area" class="search-area-btn hidden">
-- <i class="fa-solid fa-magnifying-glass-location"></i> Search This Area
++ <i class="fa-solid fa-magnifying-glass"></i> Search This Area
</button>
</div>
</main>
@@@ -278,7 +276,7 @@
<button class="food-tab active" data-tab="food-banks"><i class="fa-solid fa-hand-holding-heart"></i> Food Banks</button>
<button class="food-tab" data-tab="stores"><i class="fa-solid fa-cart-shopping"></i> Stores</button>
<button class="food-tab" data-tab="meal-plan"><i class="fa-solid fa-calendar-days"></i> Meal Plan</button>
-- <button class="food-tab" data-tab="food-db"><i class="fa-solid fa-apple-whole"></i> Food Database</button>
++ <button class="food-tab" data-tab="food-db"><i class="fa-solid fa-utensils"></i> Food Database</button>
</div>
<div id="food-banks" class="food-panel active"></div>
<div id="stores" class="food-panel hidden"></div>
@@@ -353,15 +351,15 @@
<div id="guide-modal" class="modal hidden">
<div class="modal-content modal-lg">
<div class="modal-header">
-- <h2><i class="fa-solid fa-book-skull"></i> Survival Guide</h2>
++ <h2><i class="fa-solid fa-book"></i> Survival Guide</h2>
<button class="modal-close"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="modal-body">
<div class="guide-tabs">
<button class="guide-tab active" data-guide="stealth"><i class="fa-solid fa-eye-slash"></i> Stealth</button>
<button class="guide-tab" data-guide="rules"><i class="fa-solid fa-gavel"></i> Rules</button>
-- <button class="guide-tab" data-guide="gear"><i class="fa-solid fa-backpack"></i> Gear</button>
-- <button class="guide-tab" data-guide="safety"><i class="fa-solid fa-shield-heart"></i> Safety</button>
++ <button class="guide-tab" data-guide="gear"><i class="fa-solid fa-briefcase"></i> Gear</button>
++ <button class="guide-tab" data-guide="safety"><i class="fa-solid fa-shield"></i> Safety</button>
<button class="guide-tab" data-guide="fire"><i class="fa-solid fa-fire"></i> Fire</button>
<button class="guide-tab" data-guide="firstaid"><i class="fa-solid fa-kit-medical"></i> First Aid</button>
</div>
@@@ -374,7 +372,7 @@
<div id="stats-modal" class="modal hidden">
<div class="modal-content modal-md">
<div class="modal-header">
-- <h2><i class="fa-solid fa-chart-pie"></i> Statistics</h2>
++ <h2><i class="fa-solid fa-chart-column"></i> Statistics</h2>
<button class="modal-close"><i class="fa-solid fa-xmark"></i></button>
</div>
<div class="modal-body">
ddiff --git a/apps/maps/public/js/app.js b/apps/maps/public/js/app.js
iindex 1f38e89..387feb6 100644
---- a/apps/maps/public/js/app.js
++++ b/apps/maps/public/js/app.js
@@@ -74,7 +74,7 @@
return 'default';
}
function typeIcon(tc) {
-- return { dispersed:'fa-tree',campground:'fa-campground',forest:'fa-leaf',urban:'fa-city',services:'fa-hands-helping',
++ return { dispersed:'fa-tree',campground:'fa-campground',forest:'fa-leaf',urban:'fa-city',services:'fa-handshake',
water:'fa-faucet-drip',bridge:'fa-bridge',cover:'fa-umbrella',shelter:'fa-house-chimney',restroom:'fa-restroom',
woods:'fa-tree',sketch:'fa-skull-crossbones',default:'fa-map-pin' }[tc]||'fa-map-pin';
}
@@@ -420,7 +420,7 @@
toast(`Found ${state.locations.length} locations near ${query}`);
} catch (err) {
-- setStatus(`<i class="fa-solid fa-exclamation-triangle"></i> ${esc(err.message)}`);
++ setStatus(`<i class="fa-solid fa-triangle-exclamation"></i> ${esc(err.message)}`);
toast(err.message, 'error');
$('#results-list').innerHTML = `<div class="muted-text"><i class="fa-solid fa-triangle-exclamation"></i> ${esc(err.message)}</div>`;
} finally {
@@@ -551,12 +551,12 @@
${loc.amenities?.length?`<div class="detail-section"><h3><i class="fa-solid fa-list-check"></i> Amenities</h3><div class="result-tags">${loc.amenities.map(a=>`<span class="result-tag"><i class="fa-solid fa-check"></i> ${esc(a)}</span>`).join('')}</div></div>`:''}
<div class="detail-section"><h3><i class="fa-solid fa-link"></i> Actions</h3>
<div class="detail-actions">
-- <a class="detail-btn" href="https://www.google.com/maps/dir/?api=1&destination=${num(loc.lat)},${num(loc.lon)}" target="_blank"><i class="fa-solid fa-diamond-turn-right"></i> Directions</a>
++ <a class="detail-btn" href="https://www.google.com/maps/dir/?api=1&destination=${num(loc.lat)},${num(loc.lon)}" target="_blank"><i class="fa-solid fa-route"></i> Directions</a>
<a class="detail-btn" href="https://www.google.com/maps/@${num(loc.lat)},${num(loc.lon)},15z" target="_blank"><i class="fa-solid fa-map"></i> Google Maps</a>
<button class="detail-btn" id="btn-copy-coords"><i class="fa-solid fa-copy"></i> Copy Coords</button>
</div>
</div>
-- <div class="detail-section"><h3><i class="fa-solid fa-note-sticky"></i> Notes</h3>
++ <div class="detail-section"><h3><i class="fa-solid fa-paste"></i> Notes</h3>
<div class="detail-note-area">
<textarea id="detail-note" placeholder="Your notes...">${esc(noteText)}</textarea>
<button class="detail-btn primary" id="btn-save-note"><i class="fa-solid fa-floppy-disk"></i> Save</button>
@@@ -618,7 +618,7 @@
if (!w || !state.searchCenter) { c.innerHTML = '<p class="muted-text">Search for a location first.</p>'; show($('#weather-modal')); return; }
let html = '<div class="detail-section">';
if (w.current) {
-- html += `<h3><i class="fa-solid fa-temperature-half"></i> Current Conditions</h3>`;
++ html += `<h3><i class="fa-solid fa-sun"></i> Current Conditions</h3>`;
if (w.current.temperature != null) html += `<div class="detail-info-row"><span class="label">Temperature</span><span class="value">${Math.round(w.current.temperature)}°F</span></div>`;
if (w.current.description) html += `<div class="detail-info-row"><span class="label">Conditions</span><span class="value">${esc(w.current.description)}</span></div>`;
if (w.current.windSpeed != null) html += `<div class="detail-info-row"><span class="label">Wind</span><span class="value">${w.current.windSpeed} mph</span></div>`;
@@@ -855,7 +855,7 @@
<li><strong>City/County Parks</strong> — Camping usually prohibited.</li>
<li><strong>Private Land</strong> — Need owner permission.</li>
<li><strong>Walmart/Rest Stops</strong> — Varies by location.</li></ul></div>`,
-- gear: `<div class="detail-section"><h3><i class="fa-solid fa-backpack"></i> Gear Checklist</h3><ul>
++ gear: `<div class="detail-section"><h3><i class="fa-solid fa-briefcase"></i> Gear Checklist</h3><ul>
<li><strong>Shelter:</strong> Dark bivy/tarp/tent</li>
<li><strong>Sleep:</strong> 20°F bag + insulated pad</li>
<li><strong>Light:</strong> Red headlamp</li>
@@@ -866,7 +866,7 @@
<li><strong>First Aid:</strong> Kit + emergency blanket + whistle</li>
<li><strong>Tools:</strong> Multi-tool, paracord 50ft, trash bags</li>
<li><strong>Comms:</strong> Charged phone + battery. PLB for remote</li></ul></div>`,
-- safety: `<div class="detail-section"><h3><i class="fa-solid fa-shield-heart"></i> Safety</h3><ul>
++ safety: `<div class="detail-section"><h3><i class="fa-solid fa-shield"></i> Safety</h3><ul>
<li><strong>Tell someone</strong> your plans</li>
<li><strong>Wildlife:</strong> Hang food 200ft from camp in bear country</li>
<li><strong>Rivers</strong> rise fast with rain</li>
ddiff --git a/apps/maps/server/index.js b/apps/maps/server/index.js
iindex d23079b..103d3dc 100644
---- a/apps/maps/server/index.js
++++ b/apps/maps/server/index.js
@@@ -78,12 +78,12 @@ app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
-- scriptSrc: ["'self'", "'unsafe-inline'", "unpkg.com", "cdn.jsdelivr.net", "cdnjs.cloudflare.com", "static.cloudflareinsights.com", "https://openvibe.network"],
-- styleSrc: ["'self'", "'unsafe-inline'", "unpkg.com", "cdn.jsdelivr.net", "cdnjs.cloudflare.com", "fonts.googleapis.com"],
++ scriptSrc: ["'self'", "'unsafe-inline'", "unpkg.com", "cdn.jsdelivr.net", "static.cloudflareinsights.com", "https://openvibe.network"],
++ styleSrc: ["'self'", "'unsafe-inline'", "unpkg.com", "cdn.jsdelivr.net", "fonts.googleapis.com"],
imgSrc: ["'self'", "data:", "blob:", "*.tile.openstreetmap.org", "*.basemaps.cartocdn.com", "server.arcgisonline.com", "image.tmdb.org", "*.wp.com"],
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
connectSrc: ["'self'", "nominatim.openstreetmap.org", "api.weather.gov", "api.open-meteo.com", "*.tile.openstreetmap.org", "*.basemaps.cartocdn.com", "server.arcgisonline.com", "https://openvibe.network", "https://openvibe.events"],
-- fontSrc: ["'self'", "fonts.gstatic.com", "cdnjs.cloudflare.com", "cdn.jsdelivr.net"],
++ fontSrc: ["'self'", "fonts.gstatic.com", "cdn.jsdelivr.net"],
frameSrc: ["'none'"],
scriptSrcAttr: ["'unsafe-inline'"],
},
@@@ -199,8 +199,8 @@ function getSourceDefs(lat, lon, radius) {
{ name: 'Woods', icon: 'fa-tree', fn: () => woods.findWoods(lat, lon, radius) },
{ name: 'Waterways', icon: 'fa-water', fn: () => waterways.findWaterways(lat, lon, radius) },
{ name: 'NPS', icon: 'fa-mountain-sun', fn: () => nps.search(lat, lon, radius, config.npsApiKey) },
-- { name: 'OpenChargeMap', icon: 'fa-charging-station', fn: () => openchargemap.search(lat, lon, radius, config.openChargeMapKey) },
-- { name: 'WebScraper', icon: 'fa-spider', fn: () => scraper.search(lat, lon, radius) },
++ { name: 'OpenChargeMap', icon: 'fa-bolt', fn: () => openchargemap.search(lat, lon, radius, config.openChargeMapKey) },
++ { name: 'WebScraper', icon: 'fa-bug', fn: () => scraper.search(lat, lon, radius) },
{ name: 'Rain Cover', icon: 'fa-umbrella', fn: () => cover.findCover(lat, lon, radius) },
{ name: 'Crime Intel', icon: 'fa-skull-crossbones', fn: () => crimedata.findSketchAreas(lat, lon, radius) },
{ name: 'Harm Reduction', icon: 'fa-suitcase-medical', fn: () => harmreduction.findHarmReduction(lat, lon, radius) },
ddiff --git a/apps/maps/server/sources/bathrooms.js b/apps/maps/server/sources/bathrooms.js
iindex 950da9f..2ce4cbf 100644
---- a/apps/maps/server/sources/bathrooms.js
++++ b/apps/maps/server/sources/bathrooms.js
@@@ -21,17 +21,17 @@ const BATHROOM_TYPES = {
public: { icon: 'fa-restroom', label: 'Public Restroom', color: '#3b82f6' },
park: { icon: 'fa-tree', label: 'Park Restroom', color: '#22c55e' },
library: { icon: 'fa-book', label: 'Library', color: '#8b5cf6' },
-- gas_station: { icon: 'fa-gas-pump', label: 'Gas Station', color: '#f59e0b' },
++ gas_station: { icon: 'fa-store', label: 'Gas Station', color: '#f59e0b' },
restaurant: { icon: 'fa-utensils', label: 'Restaurant/Café', color: '#ef4444' },
mall: { icon: 'fa-store', label: 'Store/Mall', color: '#ec4899' },
-- transit: { icon: 'fa-bus', label: 'Transit Station', color: '#06b6d4' },
-- community: { icon: 'fa-building', label: 'Community Building', color: '#6366f1' },
-- shower: { icon: 'fa-shower', label: 'Shower Facility', color: '#14b8a6' },
-- gym_shower: { icon: 'fa-dumbbell', label: 'Gym Shower', color: '#8b5cf6' },
++ transit: { icon: 'fa-train', label: 'Transit Station', color: '#06b6d4' },
++ community: { icon: 'fa-city', label: 'Community Building', color: '#6366f1' },
++ shower: { icon: 'fa-faucet', label: 'Shower Facility', color: '#14b8a6' },
++ gym_shower: { icon: 'fa-weight-hanging', label: 'Gym Shower', color: '#8b5cf6' },
pool_shower: { icon: 'fa-person-swimming', label: 'Pool/Sports Shower', color: '#0ea5e9' },
hot_spring: { icon: 'fa-hot-tub-person', label: 'Hot Spring', color: '#f97316' },
water: { icon: 'fa-faucet-drip', label: 'Drinking Water', color: '#0ea5e9' },
-- unknown: { icon: 'fa-toilet', label: 'Restroom', color: '#94a3b8' },
++ unknown: { icon: 'fa-restroom', label: 'Restroom', color: '#94a3b8' },
};
// ─── Classify bathroom type from tags/name ─────────────────────────
ddiff --git a/apps/maps/server/sources/cover.js b/apps/maps/server/sources/cover.js
iindex 82e3d61..7e4ec33 100644
---- a/apps/maps/server/sources/cover.js
++++ b/apps/maps/server/sources/cover.js
@@@ -24,15 +24,15 @@ const { haversine, overpassQuery } = require('./utils');
// ── Cover Type Definitions ──────────────────────────────────────────
const COVER_TYPES = {
covered_walkway: { label: 'Covered Walkway', icon: 'fa-road', color: '#6366f1', stealth: 2 },
-- arcade: { label: 'Arcade / Colonnade', icon: 'fa-archway', color: '#8b5cf6', stealth: 2 },
-- canopy: { label: 'Canopy / Awning', icon: 'fa-tent-arrow-down-to-line', color: '#f59e0b', stealth: 2 },
-- carport: { label: 'Carport', icon: 'fa-car-side', color: '#64748b', stealth: 3 },
++ arcade: { label: 'Arcade / Colonnade', icon: 'fa-door-open', color: '#8b5cf6', stealth: 2 },
++ canopy: { label: 'Canopy / Awning', icon: 'fa-campground', color: '#f59e0b', stealth: 2 },
++ carport: { label: 'Carport', icon: 'fa-square-parking', color: '#64748b', stealth: 3 },
porch: { label: 'Building Porch', icon: 'fa-house-chimney', color: '#f97316', stealth: 2 },
covered_market: { label: 'Covered Market', icon: 'fa-store', color: '#ec4899', stealth: 1 },
-- covered_seating: { label: 'Covered Seating', icon: 'fa-chair', color: '#14b8a6', stealth: 1 },
++ covered_seating: { label: 'Covered Seating', icon: 'fa-couch', color: '#14b8a6', stealth: 1 },
covered_platform: { label: 'Covered Platform', icon: 'fa-train', color: '#06b6d4', stealth: 1 },
grandstand: { label: 'Grandstand / Bleachers', icon: 'fa-flag', color: '#3b82f6', stealth: 3 },
-- loading_dock: { label: 'Loading Dock / Bay', icon: 'fa-warehouse', color: '#94a3b8', stealth: 3 },
++ loading_dock: { label: 'Loading Dock / Bay', icon: 'fa-box-open', color: '#94a3b8', stealth: 3 },
covered_area: { label: 'Covered Area', icon: 'fa-cloud-rain', color: '#22c55e', stealth: 2 },
curated: { label: 'Known Cover Spot', icon: 'fa-umbrella', color: '#a855f7', stealth: 3 },
};
ddiff --git a/apps/maps/server/sources/crimedata.js b/apps/maps/server/sources/crimedata.js
iindex c395bca..b00391b 100644
---- a/apps/maps/server/sources/crimedata.js
++++ b/apps/maps/server/sources/crimedata.js
@@@ -18,29 +18,29 @@ const { haversine, overpassQuery } = require('./utils');
// Higher weight = stronger signal of "sketchy" area
const GRIT_INDICATORS = {
// Nightlife / vice
-- bar: { query: '["amenity"="bar"]', weight: 2, label: 'Bar', icon: 'fa-martini-glass' },
++ bar: { query: '["amenity"="bar"]', weight: 2, label: 'Bar', icon: 'fa-mug-hot' },
nightclub: { query: '["amenity"="nightclub"]', weight: 3, label: 'Nightclub', icon: 'fa-music' },
-- pub: { query: '["amenity"="pub"]', weight: 1, label: 'Pub', icon: 'fa-beer-mug-empty' },
++ pub: { query: '["amenity"="pub"]', weight: 1, label: 'Pub', icon: 'fa-mug-saucer' },
strip_club: { query: '["amenity"="stripclub"]', weight: 4, label: 'Strip Club', icon: 'fa-mask' },
casino: { query: '["amenity"="casino"]', weight: 3, label: 'Casino', icon: 'fa-dice' },
adult_shop: { query: '["shop"="erotic"]', weight: 3, label: 'Adult Shop', icon: 'fa-store' },
// Commerce grit
-- pawn_shop: { query: '["shop"="pawnbroker"]', weight: 3, label: 'Pawn Shop', icon: 'fa-ring' },
++ pawn_shop: { query: '["shop"="pawnbroker"]', weight: 3, label: 'Pawn Shop', icon: 'fa-gem' },
payday_loan: { query: '["shop"="money_lender"]', weight: 3, label: 'Payday Lender', icon: 'fa-money-bill' },
-- liquor_store: { query: '["shop"="alcohol"]', weight: 2, label: 'Liquor Store', icon: 'fa-wine-bottle' },
++ liquor_store: { query: '["shop"="alcohol"]', weight: 2, label: 'Liquor Store', icon: 'fa-flask' },
tattoo_shop: { query: '["shop"="tattoo"]', weight: 1, label: 'Tattoo Parlor', icon: 'fa-pen-nib' },
-- vape_shop: { query: '["shop"="e-cigarette"]', weight: 1, label: 'Vape/Smoke Shop', icon: 'fa-smoking' },
-- tobacco_shop: { query: '["shop"="tobacco"]', weight: 1, label: 'Tobacco Shop', icon: 'fa-smoking' },
++ vape_shop: { query: '["shop"="e-cigarette"]', weight: 1, label: 'Vape/Smoke Shop', icon: 'fa-smog' },
++ tobacco_shop: { query: '["shop"="tobacco"]', weight: 1, label: 'Tobacco Shop', icon: 'fa-smog' },
// Social services (indicates street population density)
homeless_shelter:{ query: '["social_facility"="shelter"]["social_facility:for"~"homeless|underprivileged"]', weight: 2, label: 'Homeless Shelter', icon: 'fa-house-chimney' },
food_bank: { query: '["social_facility"="food_bank"]', weight: 1, label: 'Food Bank', icon: 'fa-utensils' },
// Law enforcement (indicates high-incident areas)
-- police: { query: '["amenity"="police"]', weight: 1, label: 'Police Station', icon: 'fa-building-shield' },
++ police: { query: '["amenity"="police"]', weight: 1, label: 'Police Station', icon: 'fa-shield-halved' },
// Transportation (transient hubs)
-- bus_station: { query: '["amenity"="bus_station"]', weight: 1, label: 'Bus Station', icon: 'fa-bus' },
++ bus_station: { query: '["amenity"="bus_station"]', weight: 1, label: 'Bus Station', icon: 'fa-train' },
// Abandoned / sketchy structures
-- abandoned: { query: '["abandoned"="yes"]', weight: 3, label: 'Abandoned Building', icon: 'fa-building-circle-xmark' },
-- disused: { query: '["disused"="yes"]["building"]', weight: 2, label: 'Disused Building', icon: 'fa-building' },
++ abandoned: { query: '["abandoned"="yes"]', weight: 3, label: 'Abandoned Building', icon: 'fa-circle-xmark' },
++ disused: { query: '["disused"="yes"]["building"]', weight: 2, label: 'Disused Building', icon: 'fa-city' },
};
// ── Curated WA Sketchy / High-Activity Zones ────────────────────────
ddiff --git a/apps/maps/server/sources/grocery.js b/apps/maps/server/sources/grocery.js
iindex 3fe9a4d..b0dc946 100644
---- a/apps/maps/server/sources/grocery.js
++++ b/apps/maps/server/sources/grocery.js
@@@ -73,42 +73,42 @@ const STORES = {
const FOOD_GROUPS = {
protein: {
name: 'Protein',
-- icon: 'fa-drumstick-bite',
++ icon: 'fa-fish',
color: '#9b59b6',
dailyMin: 50, // grams
examples: 'Chicken, eggs, beans, peanut butter, canned tuna',
},
grains: {
name: 'Grains',
-- icon: 'fa-wheat-awn',
++ icon: 'fa-seedling',
color: '#e67e22',
dailyMin: 170, // grams (6 oz equiv)
examples: 'Rice, bread, pasta, oatmeal, tortillas',
},
vegetables: {
name: 'Vegetables',
-- icon: 'fa-carrot',
++ icon: 'fa-leaf',
color: '#27ae60',
dailyMin: 300, // grams (~2.5 cups)
examples: 'Carrots, potatoes, canned corn, frozen mixed veggies',
},
fruits: {
name: 'Fruits',
-- icon: 'fa-apple-whole',
++ icon: 'fa-utensils',
color: '#e74c3c',
dailyMin: 200, // grams (~2 cups)
examples: 'Bananas, apples, oranges, canned fruit',
},
dairy: {
name: 'Dairy',
-- icon: 'fa-glass-water',
++ icon: 'fa-mug-saucer',
color: '#3498db',
dailyMin: 720, // ml (~3 cups)
examples: 'Milk, cheese, yogurt',
},
fats: {
name: 'Fats & Oils',
-- icon: 'fa-oil-can',
++ icon: 'fa-droplet',
color: '#f1c40f',
dailyMin: 25, // grams
examples: 'Cooking oil, butter, peanut butter, avocado',
@@@ -864,9 +864,9 @@ function formatOSMAddress(tags) {
// ─── Find Food Banks, Soup Kitchens, Community Fridges ─────────────
const FOOD_BANK_TYPES = {
-- food_bank: { label: 'Food Bank', icon: 'fa-boxes-stacked', color: '#ef4444' },
-- soup_kitchen: { label: 'Soup Kitchen', icon: 'fa-bowl-food', color: '#f97316' },
-- food_sharing: { label: 'Community Fridge', icon: 'fa-temperature-low', color: '#06b6d4' },
++ food_bank: { label: 'Food Bank', icon: 'fa-cubes', color: '#ef4444' },
++ soup_kitchen: { label: 'Soup Kitchen', icon: 'fa-utensils', color: '#f97316' },
++ food_sharing: { label: 'Community Fridge', icon: 'fa-snowflake', color: '#06b6d4' },
give_box: { label: 'Free Pantry Box', icon: 'fa-box-open', color: '#8b5cf6' },
};
ddiff --git a/apps/maps/server/sources/harmreduction.js b/apps/maps/server/sources/harmreduction.js
iindex d0cee42..3a01c47 100644
---- a/apps/maps/server/sources/harmreduction.js
++++ b/apps/maps/server/sources/harmreduction.js
@@@ -18,18 +18,18 @@ const { haversine, overpassQuery } = require('./utils');
// HARM REDUCTION CATEGORIES
// ═══════════════════════════════════════════════════════════════════
const HR_TYPES = {
-- needle_exchange: { label: 'Syringe Exchange', icon: 'fa-syringe', color: '#dc2626' },
++ needle_exchange: { label: 'Syringe Exchange', icon: 'fa-briefcase-medical', color: '#dc2626' },
condom_dist: { label: 'Condom Distribution', icon: 'fa-heart', color: '#ec4899' },
naloxone: { label: 'Naloxone / Narcan', icon: 'fa-kit-medical', color: '#f97316' },
harm_reduction: { label: 'Harm Reduction Center', icon: 'fa-hand-holding-heart', color: '#8b5cf6' },
addiction_counsel: { label: 'Addiction Counselling', icon: 'fa-comments', color: '#3b82f6' },
-- hiv_testing: { label: 'HIV / STI Testing', icon: 'fa-vial', color: '#ef4444' },
-- sexual_health: { label: 'Sexual Health Clinic', icon: 'fa-stethoscope', color: '#0891b2' },
-- family_planning: { label: 'Family Planning', icon: 'fa-baby', color: '#a855f7' },
-- vending_health: { label: 'Health Vending Machine', icon: 'fa-cash-register', color: '#22c55e' },
++ hiv_testing: { label: 'HIV / STI Testing', icon: 'fa-flask-vial', color: '#ef4444' },
++ sexual_health: { label: 'Sexual Health Clinic', icon: 'fa-kit-medical', color: '#0891b2' },
++ family_planning: { label: 'Family Planning', icon: 'fa-child', color: '#a855f7' },
++ vending_health: { label: 'Health Vending Machine', icon: 'fa-money-bill', color: '#22c55e' },
outreach: { label: 'Outreach Program', icon: 'fa-people-arrows', color: '#10b981' },
-- rehab: { label: 'Rehab / Recovery', icon: 'fa-house-medical', color: '#6366f1' },
-- social_health: { label: 'Community Health Services', icon: 'fa-hospital', color: '#0ea5e9' },
++ rehab: { label: 'Rehab / Recovery', icon: 'fa-house', color: '#6366f1' },
++ social_health: { label: 'Community Health Services', icon: 'fa-kit-medical', color: '#0ea5e9' },
};
// ═══════════════════════════════════════════════════════════════════
ddiff --git a/apps/maps/server/sources/ioverlander.js b/apps/maps/server/sources/ioverlander.js
iindex bdebced..e80565e 100644
---- a/apps/maps/server/sources/ioverlander.js
++++ b/apps/maps/server/sources/ioverlander.js
@@@ -15,16 +15,16 @@ const CATEGORIES = {
'Informal Campsite': { type: 'Informal Camp', stealth: 4, icon: 'fa-campground' },
'Established Campground': { type: 'Campground', stealth: 2, icon: 'fa-campground' },
'Overnight Parking': { type: 'Overnight Parking', stealth: 3, icon: 'fa-parking' },
-- 'Rest Area': { type: 'Rest Area', stealth: 2, icon: 'fa-bed' },
-- 'Gas Station': { type: 'Services', stealth: 1, icon: 'fa-gas-pump' },
++ 'Rest Area': { type: 'Rest Area', stealth: 2, icon: 'fa-moon' },
++ 'Gas Station': { type: 'Services', stealth: 1, icon: 'fa-store' },
'Water': { type: 'Water Source', stealth: 0, icon: 'fa-faucet-drip' },
-- 'Dump Station': { type: 'Services', stealth: 0, icon: 'fa-dumpster' },
-- 'Propane': { type: 'Services', stealth: 0, icon: 'fa-fire-flame-simple' },
++ 'Dump Station': { type: 'Services', stealth: 0, icon: 'fa-trash' },
++ 'Propane': { type: 'Services', stealth: 0, icon: 'fa-fire' },
'Mechanic': { type: 'Services', stealth: 0, icon: 'fa-wrench' },
'Wifi': { type: 'Services', stealth: 0, icon: 'fa-wifi' },
'Laundry': { type: 'Services', stealth: 0, icon: 'fa-shirt' },
-- 'Hostel': { type: 'Shelter', stealth: 1, icon: 'fa-bed' },
-- 'Hotel': { type: 'Shelter', stealth: 1, icon: 'fa-hotel' },
++ 'Hostel': { type: 'Shelter', stealth: 1, icon: 'fa-moon' },
++ 'Hotel': { type: 'Shelter', stealth: 1, icon: 'fa-couch' },
};
/**
ddiff --git a/apps/maps/server/sources/openchargemap.js b/apps/maps/server/sources/openchargemap.js
iindex f6d01b3..eb62100 100644
---- a/apps/maps/server/sources/openchargemap.js
++++ b/apps/maps/server/sources/openchargemap.js
@@@ -89,7 +89,7 @@ async function search(lat, lon, radiusMiles, apiKey) {
distanceMiles: Math.round(dist * 10) / 10,
type: 'EV Charging',
source: 'OpenChargeMap',
-- sourceIcon: 'fa-charging-station',
++ sourceIcon: 'fa-bolt',
reservable: false,
url: `https://openchargemap.org/site/poi/details/${s.ID}`,
fee: isFree ? 'Free' : (s.UsageCost || 'Check station'),
ddiff --git a/apps/maps/server/sources/resources.js b/apps/maps/server/sources/resources.js
iindex 60df459..1b21d4e 100644
---- a/apps/maps/server/sources/resources.js
++++ b/apps/maps/server/sources/resources.js
@@@ -18,9 +18,9 @@ const { haversine, OVERPASS_URL, overpassQuery } = require('./utils');
// ═══════════════════════════════════════════════════════════════════
const RESOURCE_TYPES = {
library: { label: 'Library', icon: 'fa-book', color: '#8b5cf6' },
-- shower: { label: 'Public Shower', icon: 'fa-shower', color: '#06b6d4' },
++ shower: { label: 'Public Shower', icon: 'fa-faucet', color: '#06b6d4' },
laundry: { label: 'Laundromat', icon: 'fa-shirt', color: '#f59e0b' },
-- community: { label: 'Community Center', icon: 'fa-people-roof', color: '#ec4899' },
++ community: { label: 'Community Center', icon: 'fa-people-group', color: '#ec4899' },
water: { label: 'Drinking Water', icon: 'fa-faucet-drip', color: '#3b82f6' },
food_bank: { label: 'Food Bank', icon: 'fa-utensils', color: '#ef4444' },
social: { label: 'Social Services', icon: 'fa-hand-holding-heart', color: '#10b981' },
@@@ -30,19 +30,19 @@ const RESOURCE_TYPES = {
bottle_return: { label: 'Bottle Return', icon: 'fa-recycle', color: '#22c55e' },
clinic: { label: 'Free Clinic', icon: 'fa-kit-medical', color: '#dc2626' },
homeless_shelter: { label: 'Homeless Shelter', icon: 'fa-house-chimney', color: '#7c3aed' },
-- hospital: { label: 'Hospital / ER', icon: 'fa-hospital', color: '#be123c' },
-- pharmacy: { label: 'Pharmacy', icon: 'fa-prescription', color: '#0891b2' },
++ hospital: { label: 'Hospital / ER', icon: 'fa-kit-medical', color: '#be123c' },
++ pharmacy: { label: 'Pharmacy', icon: 'fa-suitcase-medical', color: '#0891b2' },
post_office: { label: 'Post Office', icon: 'fa-envelope', color: '#4338ca' },
thrift_store: { label: 'Thrift / Free Store', icon: 'fa-shirt', color: '#a16207' },
public_bookcase: { label: 'Little Free Library', icon: 'fa-book-open', color: '#92400e' },
water_point: { label: 'Water Point (Tank)', icon: 'fa-faucet', color: '#0284c7' },
-- place_of_worship: { label: 'Church / Worship', icon: 'fa-church', color: '#6366f1' },
-- bus_station: { label: 'Bus / Transit Stop', icon: 'fa-bus', color: '#0ea5e9' },
-- gas_station: { label: 'Gas Station', icon: 'fa-gas-pump', color: '#f43f5e' },
-- clothing_bank: { label: 'Clothing Bank', icon: 'fa-vest', color: '#d946ef' },
++ place_of_worship: { label: 'Church / Worship', icon: 'fa-house-chimney', color: '#6366f1' },
++ bus_station: { label: 'Bus / Transit Stop', icon: 'fa-train', color: '#0ea5e9' },
++ gas_station: { label: 'Gas Station', icon: 'fa-store', color: '#f43f5e' },
++ clothing_bank: { label: 'Clothing Bank', icon: 'fa-shirt', color: '#d946ef' },
day_shelter: { label: 'Day Shelter / Drop-In',icon: 'fa-sun', color: '#fb923c' },
pet_friendly: { label: 'Pet-Friendly Shelter', icon: 'fa-paw', color: '#84cc16' },
-- warming_center: { label: 'Warming / Cooling', icon: 'fa-temperature-half', color: '#ef4444' },
++ warming_center: { label: 'Warming / Cooling', icon: 'fa-sun', color: '#ef4444' },
};
// ═══════════════════════════════════════════════════════════════════
ddiff --git a/apps/maps/server/sources/scraper.js b/apps/maps/server/sources/scraper.js
iindex 1df92b7..4280f32 100644
---- a/apps/maps/server/sources/scraper.js
++++ b/apps/maps/server/sources/scraper.js
@@@ -195,11 +195,11 @@ out center body;`;
stealthRating = 2;
} else if (tags.amenity === 'rest_area' || tags.highway === 'rest_area') {
type = 'Rest Area';
-- icon = 'fa-bed';
++ icon = 'fa-moon';
stealthRating = 3;
} else if (tags.tourism === 'caravan_site') {
type = 'RV/Caravan Site';
-- icon = 'fa-caravan';
++ icon = 'fa-car';
stealthRating = 3;
}
ddiff --git a/apps/maps/server/sources/static-data.js b/apps/maps/server/sources/static-data.js
iindex 2ad1cde..ba09479 100644
---- a/apps/maps/server/sources/static-data.js
++++ b/apps/maps/server/sources/static-data.js
@@@ -50,8 +50,8 @@ const locations = [
{ name: 'Stillaguamish River – South Fork at Jim Creek', lat: 48.1350, lon: -121.9700, type: 'Swimming Hole', source: 'Built-in DB', sourceIcon: 'fa-person-swimming', description: 'South Fork Stillaguamish swimming access near Jim Creek area. Forest road pulloffs lead to river. Cold, clean water. Rock pools.', stealthRating: 4, fee: 'Free', amenities: ['River Swimming', 'Clean Water', 'Forest Setting', 'Rock Pools'] },
// ── Arlington/Smokey Point Services & Resources ──
{ name: 'Arlington Community Food Bank', lat: 48.1684, lon: -122.1465, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-utensils', description: 'Free food bank at 19118 63rd Ave NE. Open Tue/Wed/Fri 12:30-3:30 PM. Fresh produce, canned goods, bread, dairy. No questions asked. Serves North Snohomish County.', stealthRating: 0, fee: 'Free', amenities: ['Food Bank', 'Fresh Produce', 'No ID Required'] },
-- { name: 'Village Community Services – Smokey Point', lat: 48.1547, lon: -122.1864, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-hands-helping', description: 'Social services at 3210 Smokey Point Dr (Royal Pacific Bldg). Emergency assistance, food, clothing, case management. Mon-Fri 8AM-5PM. Serves homeless individuals.', stealthRating: 0, fee: 'Free', amenities: ['Emergency Aid', 'Food', 'Clothing', 'Case Management'] },
-- { name: 'VOA Arlington Resource Center', lat: 48.1951, lon: -122.1269, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-hands-helping', description: 'Volunteers of America resource center at 103 E 3rd St in downtown Arlington. Housing assistance, case management, community resources. Mon-Fri 9AM-5PM.', stealthRating: 0, fee: 'Free', amenities: ['Housing Help', 'Case Management', 'Community Resources'] },
++ { name: 'Village Community Services – Smokey Point', lat: 48.1547, lon: -122.1864, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-handshake', description: 'Social services at 3210 Smokey Point Dr (Royal Pacific Bldg). Emergency assistance, food, clothing, case management. Mon-Fri 8AM-5PM. Serves homeless individuals.', stealthRating: 0, fee: 'Free', amenities: ['Emergency Aid', 'Food', 'Clothing', 'Case Management'] },
++ { name: 'VOA Arlington Resource Center', lat: 48.1951, lon: -122.1269, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-handshake', description: 'Volunteers of America resource center at 103 E 3rd St in downtown Arlington. Housing assistance, case management, community resources. Mon-Fri 9AM-5PM.', stealthRating: 0, fee: 'Free', amenities: ['Housing Help', 'Case Management', 'Community Resources'] },
{ name: 'Salt of the Earth Food Blessing', lat: 48.1548, lon: -122.1856, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-utensils', description: 'Free food distribution at 3310 Smokey Point Dr. Faith-based food blessing program. Open select days. No ID or proof of need required.', stealthRating: 0, fee: 'Free', amenities: ['Food Distribution', 'No ID Required'] },
{ name: 'Walmart Supercenter – Smokey Point', lat: 48.1507, lon: -122.1746, type: 'Urban Stealth', source: 'Built-in DB', sourceIcon: 'fa-map-pin', description: 'Walmart at 4010 172nd St NE near Smokey Point. 24-hour access. Restrooms, car camping in lot (ask mgmt). Hot food deli, pharmacy, supplies.', stealthRating: 2, fee: 'Free', amenities: ['24hr Restrooms', 'Car Camping', 'Supplies', 'Pharmacy', 'Hot Food'] },
{ name: 'Grocery Outlet – Arlington', lat: 48.2008, lon: -122.1259, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-map-pin', description: 'Discount grocery at 123 E Burke Ave in downtown Arlington. Cheap food, produce, essentials. Walking distance from Centennial Trail.', stealthRating: 0, fee: 'Discount', amenities: ['Cheap Food', 'Downtown', 'Near Trail'] },
@@@ -141,7 +141,7 @@ const locations = [
{ name: 'Centennial Trail Bridge – Stillaguamish', lat: 48.1975, lon: -122.1200, type: 'Bridge', source: 'Built-in DB', sourceIcon: 'fa-bridge', description: 'RIVER BRIDGE: Centennial Trail pedestrian/bike bridge over Stillaguamish River near downtown Arlington. Great views. River access from trail.', stealthRating: 2, fee: 'Free', amenities: ['Pedestrian Bridge', 'Trail Access', 'River Views'] },
// ── Arlington/Smokey Point – SHOWERS ──
-- { name: 'Planet Fitness – Smokey Point', lat: 48.1510, lon: -122.1793, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-shower', description: 'Planet Fitness gym at 3713 172nd St NE. $10/month membership gives 24/7 access to showers, lockers, WiFi. Clean facilities. Day pass sometimes available.', stealthRating: 0, fee: '$10/mo', amenities: ['24/7 Showers', 'WiFi', 'Lockers', 'Climate Control'] },
++ { name: 'Planet Fitness – Smokey Point', lat: 48.1510, lon: -122.1793, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-faucet', description: 'Planet Fitness gym at 3713 172nd St NE. $10/month membership gives 24/7 access to showers, lockers, WiFi. Clean facilities. Day pass sometimes available.', stealthRating: 0, fee: '$10/mo', amenities: ['24/7 Showers', 'WiFi', 'Lockers', 'Climate Control'] },
{ name: 'Cascade Valley Hospital – ER Lobby', lat: 48.1967, lon: -122.1192, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-map-pin', description: 'Cascade Valley Hospital ER lobby is 24/7 accessible. Warm waiting area, restrooms, water fountain. Social workers available during business hours for housing/resource referrals.', stealthRating: 0, fee: 'Free lobby', amenities: ['24hr Lobby', 'Restrooms', 'Water', 'Warm', 'Social Worker'] },
// ── Arlington – FOREST AREAS & NATURE (near 43rd Ave NE) ──
ddiff --git a/apps/maps/server/sources/terrain.js b/apps/maps/server/sources/terrain.js
iindex 5602f6a..3b1317b 100644
---- a/apps/maps/server/sources/terrain.js
++++ b/apps/maps/server/sources/terrain.js
@@@ -42,7 +42,7 @@ async function getBulkElevation(coords) {
function classifyElevation(elev) {
if (elev === null) return { class: 'unknown', label: 'Unknown', icon: 'fa-question' };
if (elev < 100) return { class: 'lowland', label: 'Lowland/Valley', icon: 'fa-water', note: 'Near sea level. May flood in winter.' };
-- if (elev < 500) return { class: 'foothills', label: 'Foothills', icon: 'fa-hill-rockslide', note: 'Moderate elevation. Good year-round camping.' };
++ if (elev < 500) return { class: 'foothills', label: 'Foothills', icon: 'fa-mountain', note: 'Moderate elevation. Good year-round camping.' };
if (elev < 1500) return { class: 'mountain', label: 'Mountain', icon: 'fa-mountain', note: 'Mountain terrain. Snow possible Oct-May.' };
if (elev < 2500) return { class: 'alpine', label: 'High Mountain', icon: 'fa-mountain-sun', note: 'High altitude. Snow likely Nov-Jun. Be prepared for weather.' };
return { class: 'alpine-extreme', label: 'Alpine/Extreme', icon: 'fa-mountain-sun', note: 'Very high altitude. Extreme conditions. Summer access only.' };
ddiff --git a/apps/maps/server/sources/usfs.js b/apps/maps/server/sources/usfs.js
iindex a53ff30..f900fae 100644
---- a/apps/maps/server/sources/usfs.js
++++ b/apps/maps/server/sources/usfs.js
@@@ -28,14 +28,14 @@ const ACTIVITY_MAP = {
'Picnicking': { type: 'USFS Picnic Area', icon: 'fa-utensils', stealth: 2 },
'Fishing': { type: 'Fishing Access', icon: 'fa-fish', stealth: 3 },
'Swimming': { type: 'Swimming Area', icon: 'fa-water', stealth: 2 },
-- 'Boating': { type: 'Boat Launch', icon: 'fa-sailboat', stealth: 2 },
-- 'Mountain Biking': { type: 'MTB Trailhead', icon: 'fa-bicycle', stealth: 3 },
++ 'Boating': { type: 'Boat Launch', icon: 'fa-water', stealth: 2 },
++ 'Mountain Biking': { type: 'MTB Trailhead', icon: 'fa-road', stealth: 3 },
'Winter Sports': { type: 'Winter Sports Area', icon: 'fa-snowflake', stealth: 2 },
-- 'OHV/Off-Highway Driving': { type: 'OHV Area', icon: 'fa-truck-monster', stealth: 4 },
-- 'Horseback Riding': { type: 'Equestrian Trailhead', icon: 'fa-horse', stealth: 3 },
-- 'Wildlife Viewing': { type: 'Wildlife Viewing', icon: 'fa-binoculars', stealth: 3 },
++ 'OHV/Off-Highway Driving': { type: 'OHV Area', icon: 'fa-car', stealth: 4 },
++ 'Horseback Riding': { type: 'Equestrian Trailhead', icon: 'fa-paw', stealth: 3 },
++ 'Wildlife Viewing': { type: 'Wildlife Viewing', icon: 'fa-eye', stealth: 3 },
'Visitor Centers': { type: 'Visitor Center', icon: 'fa-info-circle', stealth: 1 },
-- 'Nature Viewing': { type: 'Nature Viewing', icon: 'fa-binoculars', stealth: 3 },
++ 'Nature Viewing': { type: 'Nature Viewing', icon: 'fa-eye', stealth: 3 },
'Interpretive Programs': { type: 'Interpretive Site', icon: 'fa-book-open', stealth: 1 },
'Climbing': { type: 'Climbing Area', icon: 'fa-mountain', stealth: 4 },
'Water Sports': { type: 'Water Sports', icon: 'fa-water', stealth: 2 },
ddiff --git a/apps/maps/server/sources/waterways.js b/apps/maps/server/sources/waterways.js
iindex d3d26fd..efce796 100644
---- a/apps/maps/server/sources/waterways.js
++++ b/apps/maps/server/sources/waterways.js
@@@ -46,8 +46,8 @@ const WATERWAY_TYPES = {
ford: { label: 'Ford / River Crossing',icon: 'fa-road', color: '#0891b2', stealth: 4 },
fishing: { label: 'Fishing Area', icon: 'fa-fish', color: '#059669', stealth: 3 },
swimming: { label: 'Swimming Area', icon: 'fa-person-swimming', color: '#14b8a6', stealth: 3 },
-- boat_launch: { label: 'Boat Launch / Slipway',icon: 'fa-ship', color: '#0d9488', stealth: 2 },
-- pier: { label: 'Pier / Dock', icon: 'fa-anchor', color: '#64748b', stealth: 2 },
++ boat_launch: { label: 'Boat Launch / Slipway',icon: 'fa-water', color: '#0d9488', stealth: 2 },
++ pier: { label: 'Pier / Dock', icon: 'fa-map-pin', color: '#64748b', stealth: 2 },
wetland: { label: 'Wetland', icon: 'fa-water', color: '#84cc16', stealth: 3 },
river_area: { label: 'River Area', icon: 'fa-water', color: '#2563eb', stealth: 4 },
beach: { label: 'Beach', icon: 'fa-umbrella-beach', color: '#fbbf24', stealth: 3 },
ddiff --git a/apps/maps/server/sources/weather.js b/apps/maps/server/sources/weather.js
iindex ce2b0bf..8e730f0 100644
---- a/apps/maps/server/sources/weather.js
++++ b/apps/maps/server/sources/weather.js
@@@ -31,7 +31,7 @@ const WMO_CODES = {
73: { description: 'Moderate snow', icon: 'fa-snowflake', cls: 'snow' },
75: { description: 'Heavy snow', icon: 'fa-snowflake', cls: 'snow' },
77: { description: 'Snow grains', icon: 'fa-snowflake', cls: 'snow' },
-- 80: { description: 'Light showers', icon: 'fa-cloud-sun-rain', cls: 'rain' },
++ 80: { description: 'Light showers', icon: 'fa-cloud-rain', cls: 'rain' },
81: { description: 'Moderate showers', icon: 'fa-cloud-showers-heavy', cls: 'rain' },
82: { description: 'Violent showers', icon: 'fa-cloud-showers-heavy', cls: 'rain' },
85: { description: 'Light snow showers', icon: 'fa-snowflake', cls: 'snow' },
@@@ -47,7 +47,7 @@ function decodeWMO(code, isDay) {
let icon = entry.icon;
if (!isDay && icon === 'fa-sun') icon = 'fa-moon';
if (!isDay && icon === 'fa-cloud-sun') icon = 'fa-cloud-moon';
-- if (!isDay && icon === 'fa-cloud-sun-rain') icon = 'fa-cloud-moon-rain';
++ if (!isDay && icon === 'fa-cloud-rain') icon = 'fa-cloud-moon';
return { ...entry, icon };
}
ddiff --git a/apps/text/package-lock.json b/apps/text/package-lock.json
iindex b663396..375a3ad 100644
---- a/apps/text/package-lock.json
++++ b/apps/text/package-lock.json
@@@ -18,7 +18,7 @@
"helmet": "^7.1.0",
"iovalkey": "^0.4.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
"pg": "^8.23.0"
}
},
@@@ -1130,9 +1130,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "4.56.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
-- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",
++ "version": "4.73.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
++ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
ddiff --git a/apps/text/public/ascii.html b/apps/text/public/ascii.html
iindex 4cb6a75..7ced646 100644
---- a/apps/text/public/ascii.html
++++ b/apps/text/public/ascii.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
</head>
ddiff --git a/apps/text/public/badge.html b/apps/text/public/badge.html
iindex 7d4c7cd..348ae26 100644
---- a/apps/text/public/badge.html
++++ b/apps/text/public/badge.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<style>
@@@ -47,7 +46,7 @@
<body>
<div class="tool-wrap" style="max-width: 1000px;">
<div class="page-header">
-- <h1><i class="fas fa-certificate"></i> Badge &amp; Sticker Maker</h1>
++ <h1><i class="fas fa-ranking-star"></i> Badge &amp; Sticker Maker</h1>
<p class="subtitle">Design badges, stickers, and labels with custom shapes, colors, and fonts.</p>
</div>
ddiff --git a/apps/text/public/binary.html b/apps/text/public/binary.html
iindex 27edde5..8a326d1 100644
---- a/apps/text/public/binary.html
++++ b/apps/text/public/binary.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
</head>
ddiff --git a/apps/text/public/bio.html b/apps/text/public/bio.html
iindex ccad3ad..91045d2 100644
---- a/apps/text/public/bio.html
++++ b/apps/text/public/bio.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
</head>
@@@ -42,7 +41,7 @@
<div class="btn-row">
<a href="https://fancy.openvibe.tools" class="btn btn-secondary"><i class="fas fa-wand-magic-sparkles"></i> Fancy Text</a>
<a href="https://symbols.openvibe.tools" class="btn btn-secondary"><i class="fas fa-icons"></i> Symbols</a>
-- <a href="https://kaomoji.openvibe.tools" class="btn btn-secondary"><i class="fas fa-face-smile-beam"></i> Kaomoji</a>
++ <a href="https://kaomoji.openvibe.tools" class="btn btn-secondary"><i class="fas fa-face-smile"></i> Kaomoji</a>
</div>
</div>
ddiff --git a/apps/text/public/braille.html b/apps/text/public/braille.html
iindex 531deb5..5e33e66 100644
---- a/apps/text/public/braille.html
++++ b/apps/text/public/braille.html
@@@ -18,14 +18,13 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
</head>
<body>
<div class="tool-wrap">
<div class="page-header">
-- <h1><i class="fas fa-braille"></i> Braille Translator</h1>
++ <h1><i class="fas fa-grip"></i> Braille Translator</h1>
<p class="subtitle">Convert text to Braille Unicode dots and decode Braille back to text.</p>
</div>
@@@ -45,7 +44,7 @@
</div>
<div class="tool-section">
-- <h2><i class="fas fa-braille"></i> Output</h2>
++ <h2><i class="fas fa-grip"></i> Output</h2>
<div id="output" class="output-box mono" style="font-size: 1.4rem; letter-spacing: 0.2em;"></div>
<div class="btn-row">
<button class="btn btn-primary" onclick="OpenVibeUI.copyText(document.getElementById('output').textContent)"><i class="fas fa-copy"></i> Copy</button>
ddiff --git a/apps/text/public/bubble.html b/apps/text/public/bubble.html
iindex 556b637..2b22244 100644
---- a/apps/text/public/bubble.html
++++ b/apps/text/public/bubble.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
</head>
ddiff --git a/apps/text/public/case.html b/apps/text/public/case.html
iindex 1f45037..82bd598 100644
---- a/apps/text/public/case.html
++++ b/apps/text/public/case.html
@@@ -18,14 +18,13 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
</head>
<body>
<div class="tool-wrap">
<div class="page-header">
-- <h1><i class="fas fa-text-slash"></i> Case Converter</h1>
++ <h1><i class="fas fa-font"></i> Case Converter</h1>
<p class="subtitle">Instantly convert text between 14 different case styles. Click any result to copy.</p>
</div>
ddiff --git a/apps/text/public/clean.html b/apps/text/public/clean.html
iindex 9f2f7b3..274b627 100644
---- a/apps/text/public/clean.html
++++ b/apps/text/public/clean.html
@@@ -18,14 +18,13 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
</head>
<body>
<div class="tool-wrap">
<div class="page-header">
-- <h1><i class="fas fa-broom"></i> Text Cleaner</h1>
++ <h1><i class="fas fa-eraser"></i> Text Cleaner</h1>
<p class="subtitle">Clean up messy text with one click. Fix quotes, strip invisible characters, normalize spacing.</p>
</div>
ddiff --git a/apps/text/public/compare.html b/apps/text/public/compare.html
iindex ac250dc..26134f5 100644
---- a/apps/text/public/compare.html
++++ b/apps/text/public/compare.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<style>
@@@ -38,7 +37,7 @@
<body>
<div class="tool-wrap" style="max-width: 1000px;">
<div class="page-header">
-- <h1><i class="fas fa-code-compare"></i> Text Compare</h1>
++ <h1><i class="fas fa-arrows-left-right"></i> Text Compare</h1>
<p class="subtitle">Paste two texts side by side and see the differences highlighted line by line.</p>
</div>
ddiff --git a/apps/text/public/count.html b/apps/text/public/count.html
iindex fa1bc8c..2764190 100644
---- a/apps/text/public/count.html
++++ b/apps/text/public/count.html
@@@ -18,14 +18,13 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
</head>
<body>
<div class="tool-wrap">
<div class="page-header">
-- <h1><i class="fas fa-calculator"></i> Text Counter</h1>
++ <h1><i class="fas fa-table-cells"></i> Text Counter</h1>
<p class="subtitle">Real-time character, word, sentence, and paragraph counting with reading & speaking time.</p>
</div>
ddiff --git a/apps/text/public/escape.html b/apps/text/public/escape.html
iindex ea9b905..e2caca4 100644
---- a/apps/text/public/escape.html
++++ b/apps/text/public/escape.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<style>
@@@ -62,7 +61,7 @@
<script>
OpenVibeUI.initPage();
-- const ICONS = { htmlEscape: 'fa-code', htmlUnescape: 'fa-code', urlEncode: 'fa-link', urlDecode: 'fa-link', base64Encode: 'fa-lock', base64Decode: 'fa-unlock', jsonStringify: 'fa-brackets-curly', jsonParse: 'fa-brackets-curly', cssEscape: 'fa-paintbrush', regexEscape: 'fa-asterisk', unicodeEscape: 'fa-globe', hexEncode: 'fa-hashtag' };
++ const ICONS = { htmlEscape: 'fa-code', htmlUnescape: 'fa-code', urlEncode: 'fa-link', urlDecode: 'fa-link', base64Encode: 'fa-lock', base64Decode: 'fa-unlock', jsonStringify: 'fa-code', jsonParse: 'fa-code', cssEscape: 'fa-paintbrush', regexEscape: 'fa-star', unicodeEscape: 'fa-globe', hexEncode: 'fa-hashtag' };
const TRANSFORMS = Object.entries(TextOpenVibeEngine.escapes).map(([id, e]) => ({ id, label: e.label, icon: ICONS[id], fn: e.fn }));
const grid = document.getElementById('grid');
ddiff --git a/apps/text/public/fancy.html b/apps/text/public/fancy.html
iindex 4fc202f..9832854 100644
---- a/apps/text/public/fancy.html
++++ b/apps/text/public/fancy.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
</head>
ddiff --git a/apps/text/public/ice.html b/apps/text/public/ice.html
iindex 5eebf1a..a6983f7 100644
---- a/apps/text/public/ice.html
++++ b/apps/text/public/ice.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<style>
@@@ -117,12 +116,16 @@
}
.scenario summary::-webkit-details-marker { display: none; }
.scenario summary::after {
-- content: '\f054';
-- font-family: 'Font Awesome 6 Free';
-- font-weight: 900;
-- font-size: 0.75rem;
++ /* The OpenVibe chevron (openvibe.design/icons/chevron), painted in the text's colour. */
++ content: '';
++ width: 0.75rem;
++ height: 0.75rem;
++ flex: none;
margin-left: auto;
color: var(--text-muted);
++ background-color: currentColor;
++ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 6 6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat;
++ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 6 6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat;
transition: transform 0.2s;
}
.scenario[open] summary::after { transform: rotate(90deg); }
@@@ -187,7 +190,7 @@
<!-- ═══ TAB: KNOW YOUR RIGHTS ═══ -->
<div class="tab-content active" id="tab-rights">
<div class="tool-section">
-- <h2><i class="fas fa-landmark"></i> Your Constitutional Rights</h2>
++ <h2><i class="fas fa-gavel"></i> Your Constitutional Rights</h2>
<p style="color:var(--text-secondary); margin-bottom:16px;">Regardless of immigration status, the U.S. Constitution guarantees these rights to all people on U.S. soil.</p>
<div class="rights-grid">
@@@ -200,7 +203,7 @@
<p>You have the right to speak with an attorney. If detained, say: <strong>"I want to speak with a lawyer before answering any questions."</strong> You have the right to make a phone call. Immigration detainees must be given a list of free legal service providers.</p>
</div>
<div class="rights-card">
-- <h3><i class="fas fa-house-lock"></i> Protection in Your Home</h3>
++ <h3><i class="fas fa-house"></i> Protection in Your Home</h3>
<p>ICE generally <strong>cannot enter your home without a judicial warrant</strong> signed by a judge. An ICE administrative warrant (Form I-200) does NOT give them permission to enter. You are not required to open the door. Ask them to slip the warrant under the door.</p>
</div>
<div class="rights-card">
@@@ -208,7 +211,7 @@
<p>You can refuse a search of yourself, your car, or your home. Say clearly: <strong>"I do not consent to a search."</strong> This does not guarantee they won't search, but it protects your legal rights.</p>
</div>
<div class="rights-card">
-- <h3><i class="fas fa-file-signature"></i> Don't Sign Anything</h3>
++ <h3><i class="fas fa-file-pen"></i> Don't Sign Anything</h3>
<p><strong>Do not sign any documents</strong> without reading them carefully and speaking to a lawyer first. Signing voluntary departure papers waives your right to appear before an immigration judge and could result in a 10-year bar on re-entry.</p>
</div>
<div class="rights-card">
@@@ -266,7 +269,7 @@
</details>
<details class="scenario">
-- <summary><i class="fas fa-building"></i> ICE at Your Workplace</summary>
++ <summary><i class="fas fa-city"></i> ICE at Your Workplace</summary>
<div class="scenario-body">
<ul>
<li>Your employer should ask to see a <strong>judicial warrant</strong> before allowing ICE into non-public areas.</li>
@@@ -292,7 +295,7 @@
</details>
<details class="scenario">
-- <summary><i class="fas fa-handcuffs"></i> If You Are Detained</summary>
++ <summary><i class="fas fa-lock"></i> If You Are Detained</summary>
<div class="scenario-body">
<ul>
<li>Stay calm and repeat: <em>"I wish to remain silent. I want to speak with a lawyer."</em></li>
@@@ -348,7 +351,7 @@
<!-- ═══ TAB: RESOURCES ═══ -->
<div class="tab-content" id="tab-resources">
<div class="tool-section">
-- <h2><i class="fas fa-phone-volume"></i> Emergency Hotlines</h2>
++ <h2><i class="fas fa-phone"></i> Emergency Hotlines</h2>
<div class="emergency-grid">
<div class="emergency-card">
<div class="em-icon red"><i class="fas fa-phone"></i></div>
@@@ -365,7 +368,7 @@
</div>
</div>
<div class="emergency-card">
-- <div class="em-icon green"><i class="fas fa-hands-holding-child"></i></div>
++ <div class="em-icon green"><i class="fas fa-child"></i></div>
<div class="em-info">
<h4>RAICES Texas</h4>
<p><a href="tel:1-800-732-4237">1-800-732-4237</a> — Free legal help</p>
@@@ -414,9 +417,9 @@
<div class="resource-links">
<a class="resource-link" href="https://www.immigrationadvocates.org/legaldirectory/" target="_blank" rel="noopener"><i class="fas fa-search"></i> Find Free Legal Help Near You</a>
<a class="resource-link" href="https://www.aclu.org/know-your-rights/immigrants-rights" target="_blank" rel="noopener"><i class="fas fa-shield-halved"></i> ACLU Know Your Rights</a>
-- <a class="resource-link" href="https://www.raicestexas.org/know-your-rights/" target="_blank" rel="noopener"><i class="fas fa-hands-holding-child"></i> RAICES Texas</a>
++ <a class="resource-link" href="https://www.raicestexas.org/know-your-rights/" target="_blank" rel="noopener"><i class="fas fa-child"></i> RAICES Texas</a>
<a class="resource-link" href="https://unitedwedream.org/our-work/deportation-defense/know-your-rights/" target="_blank" rel="noopener"><i class="fas fa-fist-raised"></i> United We Dream</a>
-- <a class="resource-link" href="https://www.nilc.org/" target="_blank" rel="noopener"><i class="fas fa-landmark"></i> National Immigration Law Center</a>
++ <a class="resource-link" href="https://www.nilc.org/" target="_blank" rel="noopener"><i class="fas fa-gavel"></i> National Immigration Law Center</a>
<a class="resource-link" href="https://cliniclegal.org/" target="_blank" rel="noopener"><i class="fas fa-scale-balanced"></i> CLINIC Legal</a>
<a class="resource-link" href="https://www.informedimmigrant.com/" target="_blank" rel="noopener"><i class="fas fa-book"></i> Informed Immigrant</a>
</div>
@@@ -426,7 +429,7 @@
<h2><i class="fas fa-mobile-screen"></i> Apps & Tools</h2>
<div class="rights-grid">
<div class="rights-card">
-- <h3><i class="fab fa-app-store"></i> Know Your Rights App (NAKASEC)</h3>
++ <h3><i class="fas fa-mobile-screen"></i> Know Your Rights App (NAKASEC)</h3>
<p>Mobile app for immigrants to understand their legal rights. Available on iOS.</p>
<div class="rights-lang"><span class="lang-tag">English</span><span class="lang-tag">Español</span><span class="lang-tag">한국어</span></div>
</div>
ddiff --git a/apps/text/public/index.html b/apps/text/public/index.html
iindex d2a13d8..7d877de 100644
---- a/apps/text/public/index.html
++++ b/apps/text/public/index.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
</head>
@@@ -69,7 +68,7 @@
<span class="card-domain">ascii.openvibe.tools</span>
</a>
<a href="https://case.openvibe.tools" class="tool-card">
-- <div class="card-icon"><i class="fas fa-text-slash"></i></div>
++ <div class="card-icon"><i class="fas fa-font"></i></div>
<h3>Case Converter</h3>
<p>UPPER, lower, Title, camelCase, snake_case, kebab-case, PascalCase, mOcKiNg, and more.</p>
<span class="card-domain">case.openvibe.tools</span>
@@@ -87,13 +86,13 @@
<span class="card-domain">bubble.openvibe.tools</span>
</a>
<a href="https://count.openvibe.tools" class="tool-card">
-- <div class="card-icon"><i class="fas fa-calculator"></i></div>
++ <div class="card-icon"><i class="fas fa-table-cells"></i></div>
<h3>Text Counter</h3>
<p>Count characters, words, sentences, paragraphs, bytes. Reading time and speaking time estimates.</p>
<span class="card-domain">count.openvibe.tools</span>
</a>
<a href="https://clean.openvibe.tools" class="tool-card">
-- <div class="card-icon"><i class="fas fa-broom"></i></div>
++ <div class="card-icon"><i class="fas fa-eraser"></i></div>
<h3>Text Cleaner</h3>
<p>Fix smart quotes, remove invisible characters, strip Zalgo, normalize spacing, clean HTML tags.</p>
<span class="card-domain">clean.openvibe.tools</span>
@@@ -105,7 +104,7 @@
<span class="card-domain">reversetext.openvibe.tools</span>
</a>
<a href="https://kaomoji.openvibe.tools" class="tool-card">
-- <div class="card-icon"><i class="fas fa-face-smile-beam"></i></div>
++ <div class="card-icon"><i class="fas fa-face-smile"></i></div>
<h3>Kaomoji & Emoticons</h3>
<p>Browse and copy Japanese text emoticons sorted by mood: happy, sad, angry, love, shrug, lenny, and more.</p>
<span class="card-domain">kaomoji.openvibe.tools</span>
@@@ -141,13 +140,13 @@
<span class="card-domain">binary.openvibe.tools</span>
</a>
<a href="https://markdown.openvibe.tools" class="tool-card">
-- <div class="card-icon"><i class="fab fa-markdown"></i></div>
++ <div class="card-icon"><i class="fas fa-code"></i></div>
<h3>Markdown Preview</h3>
<p>Write Markdown on the left, see rendered HTML on the right. Export as HTML.</p>
<span class="card-domain">markdown.openvibe.tools</span>
</a>
<a href="https://sort.openvibe.tools" class="tool-card">
-- <div class="card-icon"><i class="fas fa-sort-alpha-down"></i></div>
++ <div class="card-icon"><i class="fas fa-arrow-down-short-wide"></i></div>
<h3>Sort & Dedupe Lines</h3>
<p>Sort lines alphabetically, numerically, or randomly. Remove duplicates instantly.</p>
<span class="card-domain">sort.openvibe.tools</span>
@@@ -191,7 +190,7 @@
<span class="card-domain">wordmark.openvibe.tools</span>
</a>
<a href="https://badge.openvibe.tools" class="tool-card">
-- <div class="card-icon"><i class="fas fa-certificate"></i></div>
++ <div class="card-icon"><i class="fas fa-ranking-star"></i></div>
<h3>Badge & Sticker Maker</h3>
<p>Create badges, stickers, and labels with rounded shapes, bold text, and vibrant colors.</p>
<span class="card-domain">badge.openvibe.tools</span>
ddiff --git a/apps/text/public/json.html b/apps/text/public/json.html
iindex df01785..d4dd24f 100644
---- a/apps/text/public/json.html
++++ b/apps/text/public/json.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<style>
@@@ -65,7 +64,7 @@
<button class="btn btn-primary" onclick="beautify()"><i class="fas fa-wand-magic-sparkles"></i> Beautify</button>
<button class="btn btn-secondary" onclick="minify()"><i class="fas fa-compress"></i> Minify</button>
<button class="btn btn-secondary" onclick="OpenVibeUI.copyText(lastFormatted)"><i class="fas fa-copy"></i> Copy Output</button>
-- <button class="btn btn-secondary" onclick="sortKeys()"><i class="fas fa-arrow-down-a-z"></i> Sort Keys</button>
++ <button class="btn btn-secondary" onclick="sortKeys()"><i class="fas fa-arrow-down-wide-short"></i> Sort Keys</button>
</div>
</div>
ddiff --git a/apps/text/public/kaomoji.html b/apps/text/public/kaomoji.html
iindex 22a1114..89be787 100644
---- a/apps/text/public/kaomoji.html
++++ b/apps/text/public/kaomoji.html
@@@ -18,14 +18,13 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
</head>
<body>
<div class="tool-wrap" style="max-width: 1000px;">
<div class="page-header">
-- <h1><i class="fas fa-face-smile-beam"></i> Kaomoji</h1>
++ <h1><i class="fas fa-face-smile"></i> Kaomoji</h1>
<p class="subtitle">Japanese text emoticons sorted by mood. Click any to copy. (◕‿◕)</p>
</div>
@@@ -44,11 +43,11 @@
OpenVibeUI.initPage();
const container = document.getElementById('categories');
const moodIcons = {
-- happy:'fa-face-smile-beam', sad:'fa-face-sad-tear', angry:'fa-face-angry',
-- love:'fa-heart', shrug:'fa-person-shrugging', surprise:'fa-face-surprise',
-- cool:'fa-sunglasses', silly:'fa-face-grin-tongue', animals:'fa-paw',
++ happy:'fa-face-smile', sad:'fa-face-grin-squint-tears', angry:'fa-face-grin-squint',
++ love:'fa-heart', shrug:'fa-question', surprise:'fa-face-grin-stars',
++ cool:'fa-eye', silly:'fa-face-laugh', animals:'fa-paw',
music:'fa-music', tableFlip:'fa-table', tableUnflip:'fa-table',
-- lenny:'fa-face-grin-wink', fight:'fa-hand-fist', sparkle:'fa-sparkles',
++ lenny:'fa-face-grin', fight:'fa-hand-fist', sparkle:'fa-sparkles',
};
for (const [mood, items] of Object.entries(TextOpenVibeEngine.kaomoji)) {
ddiff --git a/apps/text/public/logo-hub.html b/apps/text/public/logo-hub.html
iindex 3158014..f53c54c 100644
---- a/apps/text/public/logo-hub.html
++++ b/apps/text/public/logo-hub.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
</head>
@@@ -53,7 +52,7 @@
<!-- Tools grid -->
<div class="section-header">
-- <i class="fas fa-tools"></i>
++ <i class="fas fa-screwdriver-wrench"></i>
<h2>Design Tools</h2>
</div>
<div class="tool-grid">
@@@ -70,7 +69,7 @@
<span class="card-domain">wordmark.openvibe.tools</span>
</a>
<a href="https://badge.openvibe.tools" class="tool-card">
-- <div class="card-icon"><i class="fas fa-certificate"></i></div>
++ <div class="card-icon"><i class="fas fa-ranking-star"></i></div>
<h3>Badge & Sticker Maker</h3>
<p>Create rounded badges, stickers, and labels. Choose background shapes, colors, and text styles.</p>
<span class="card-domain">badge.openvibe.tools</span>
ddiff --git a/apps/text/public/markdown.html b/apps/text/public/markdown.html
iindex 59611ad..87afe2b 100644
---- a/apps/text/public/markdown.html
++++ b/apps/text/public/markdown.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<style>
@@@ -49,7 +48,7 @@
<body>
<div class="tool-wrap" style="max-width: 1100px;">
<div class="page-header">
-- <h1><i class="fab fa-markdown"></i> Markdown Preview</h1>
++ <h1><i class="fas fa-code"></i> Markdown Preview</h1>
<p class="subtitle">Write Markdown on the left, see it rendered on the right. Export as HTML.</p>
</div>
ddiff --git a/apps/text/public/morse.html b/apps/text/public/morse.html
iindex 437f6b0..b9f3ef6 100644
---- a/apps/text/public/morse.html
++++ b/apps/text/public/morse.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
</head>
ddiff --git a/apps/text/public/nickname.html b/apps/text/public/nickname.html
iindex cacf5ea..eb2b81d 100644
---- a/apps/text/public/nickname.html
++++ b/apps/text/public/nickname.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
</head>
ddiff --git a/apps/text/public/peso.html b/apps/text/public/peso.html
iindex c9dd776..cdb7efa 100644
---- a/apps/text/public/peso.html
++++ b/apps/text/public/peso.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<style>
@@@ -136,7 +135,7 @@
</div>
<div class="swap-row">
-- <button class="swap-btn" onclick="swapCurrencies()" title="Swap"><i class="fas fa-arrow-down-up-across-line"></i></button>
++ <button class="swap-btn" onclick="swapCurrencies()" title="Swap"><i class="fas fa-arrows-up-down-left-right"></i></button>
</div>
<div class="currency-box" id="box-bottom">
ddiff --git a/apps/text/public/reverse.html b/apps/text/public/reverse.html
iindex 8beecd9..e946361 100644
---- a/apps/text/public/reverse.html
++++ b/apps/text/public/reverse.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
</head>
ddiff --git a/apps/text/public/slang.html b/apps/text/public/slang.html
iindex 24ea6bc..4398f1f 100644
---- a/apps/text/public/slang.html
++++ b/apps/text/public/slang.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<style>
@@@ -115,7 +114,7 @@
<body>
<div class="tool-wrap">
<div class="page-header">
-- <h1><i class="fas fa-pepper-hot"></i> Mexican Slang</h1>
++ <h1><i class="fas fa-fire"></i> Mexican Slang</h1>
<p class="subtitle">Real Mexican Spanish — the words they don't teach you in class. ¡No mames, güey!</p>
</div>
ddiff --git a/apps/text/public/slug.html b/apps/text/public/slug.html
iindex a20aed3..bc56739 100644
---- a/apps/text/public/slug.html
++++ b/apps/text/public/slug.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
</head>
ddiff --git a/apps/text/public/sort.html b/apps/text/public/sort.html
iindex 37f2e36..1a88b80 100644
---- a/apps/text/public/sort.html
++++ b/apps/text/public/sort.html
@@@ -18,14 +18,13 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
</head>
<body>
<div class="tool-wrap">
<div class="page-header">
-- <h1><i class="fas fa-sort-alpha-down"></i> Sort & Dedupe Lines</h1>
++ <h1><i class="fas fa-arrow-down-short-wide"></i> Sort & Dedupe Lines</h1>
<p class="subtitle">Sort lines alphabetically or reverse. Remove duplicate lines instantly.</p>
</div>
@@@ -41,11 +40,11 @@ banana
elderberry</textarea>
</div>
<div class="btn-row">
-- <button class="btn btn-primary" onclick="sortAsc()"><i class="fas fa-sort-alpha-down"></i> Sort A→Z</button>
-- <button class="btn btn-secondary" onclick="sortDesc()"><i class="fas fa-sort-alpha-up"></i> Sort Z→A</button>
-- <button class="btn btn-secondary" onclick="shuffle()"><i class="fas fa-shuffle"></i> Shuffle</button>
++ <button class="btn btn-primary" onclick="sortAsc()"><i class="fas fa-arrow-down-short-wide"></i> Sort A→Z</button>
++ <button class="btn btn-secondary" onclick="sortDesc()"><i class="fas fa-arrow-down-wide-short"></i> Sort Z→A</button>
++ <button class="btn btn-secondary" onclick="shuffle()"><i class="fas fa-right-left"></i> Shuffle</button>
<button class="btn btn-secondary" onclick="dedupe()"><i class="fas fa-filter"></i> Remove Duplicates</button>
-- <button class="btn btn-ghost" onclick="reverseLines()"><i class="fas fa-retweet"></i> Reverse Order</button>
++ <button class="btn btn-ghost" onclick="reverseLines()"><i class="fas fa-arrow-right-arrow-left"></i> Reverse Order</button>
</div>
</div>
ddiff --git a/apps/text/public/spanish.html b/apps/text/public/spanish.html
iindex 1536936..b841f2d 100644
---- a/apps/text/public/spanish.html
++++ b/apps/text/public/spanish.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<style>
ddiff --git a/apps/text/public/symbols.html b/apps/text/public/symbols.html
iindex ff70198..9e30d89 100644
---- a/apps/text/public/symbols.html
++++ b/apps/text/public/symbols.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
</head>
@@@ -56,11 +55,11 @@
const collected = document.getElementById('collected');
const categoryIcons = {
-- arrows: 'fa-right-long', stars: 'fa-star', hearts: 'fa-heart', music: 'fa-music',
-- math: 'fa-superscript', currency: 'fa-dollar-sign', bullets: 'fa-list',
-- boxes: 'fa-square', lines: 'fa-grip-lines', dividers: 'fa-minus',
-- check: 'fa-check', zodiac: 'fa-star-of-david', weather: 'fa-cloud-sun',
-- gaming: 'fa-chess', hands: 'fa-hand', faces: 'fa-face-smile',
++ arrows: 'fa-arrow-right', stars: 'fa-star', hearts: 'fa-heart', music: 'fa-music',
++ math: 'fa-arrow-up', currency: 'fa-dollar-sign', bullets: 'fa-list',
++ boxes: 'fa-square', lines: 'fa-grip-vertical', dividers: 'fa-minus',
++ check: 'fa-check', zodiac: 'fa-star', weather: 'fa-cloud-sun',
++ gaming: 'fa-gamepad', hands: 'fa-hand', faces: 'fa-face-smile',
};
for (const [name, syms] of Object.entries(TextOpenVibeEngine.symbols)) {
ddiff --git a/apps/text/public/thumbnail.html b/apps/text/public/thumbnail.html
iindex a5834fc..8ef624a 100644
---- a/apps/text/public/thumbnail.html
++++ b/apps/text/public/thumbnail.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<style>
@@@ -73,7 +72,7 @@
<h3><i class="fas fa-image"></i> Background</h3>
<label class="upload-btn" id="uploadBtn">
<input type="file" id="fileInput" accept="image/*">
-- <i class="fas fa-cloud-upload-alt"></i> Upload Background Image
++ <i class="fas fa-cloud-arrow-up"></i> Upload Background Image
</label>
<div class="ctrl-row" style="margin-top: .75rem;">
<label>BG Color</label>
ddiff --git a/apps/text/public/title.html b/apps/text/public/title.html
iindex 1b906e6..041e89d 100644
---- a/apps/text/public/title.html
++++ b/apps/text/public/title.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<style>
@@@ -83,7 +82,7 @@
</div>
<div class="control-group">
-- <h3><i class="fas fa-fill-drip"></i> Colors</h3>
++ <h3><i class="fas fa-droplet"></i> Colors</h3>
<div class="control-row">
<label>Text Color</label>
<input type="color" id="color" value="#ffffff">
ddiff --git a/apps/text/public/transparent.html b/apps/text/public/transparent.html
iindex 5c58491..9f9cbd3 100644
---- a/apps/text/public/transparent.html
++++ b/apps/text/public/transparent.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<style>
ddiff --git a/apps/text/public/unicode.html b/apps/text/public/unicode.html
iindex 59227b4..fee39ff 100644
---- a/apps/text/public/unicode.html
++++ b/apps/text/public/unicode.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<style>
ddiff --git a/apps/text/public/watermark.html b/apps/text/public/watermark.html
iindex cfce3fa..2a5e6a0 100644
---- a/apps/text/public/watermark.html
++++ b/apps/text/public/watermark.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<style>
@@@ -56,7 +55,7 @@
<div>
<div class="drop-zone" id="dropZone" onclick="document.getElementById('fileInput').click()">
<input type="file" id="fileInput" accept="image/*">
-- <i class="fas fa-cloud-upload-alt" style="font-size: 2rem; color: var(--text-muted); margin-bottom: .5rem; display: block;"></i>
++ <i class="fas fa-cloud-arrow-up" style="font-size: 2rem; color: var(--text-muted); margin-bottom: .5rem; display: block;"></i>
<p style="color: var(--text-secondary); margin: 0;">Click or drag an image here</p>
</div>
<div class="wm-preview" id="previewArea" style="margin-top: 1.5rem;">
@@@ -80,7 +79,7 @@
</div>
<div class="ctrl-group">
-- <h3><i class="fas fa-crosshairs"></i> Position</h3>
++ <h3><i class="fas fa-location-dot"></i> Position</h3>
<div class="pos-grid" id="posGrid">
<button class="pos-btn" data-pos="top-left">↖ Top Left</button>
<button class="pos-btn" data-pos="top-center">↑ Top</button>
ddiff --git a/apps/text/public/wordmark.html b/apps/text/public/wordmark.html
iindex ecbf08d..2976487 100644
---- a/apps/text/public/wordmark.html
++++ b/apps/text/public/wordmark.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<style>
ddiff --git a/apps/text/public/zalgo.html b/apps/text/public/zalgo.html
iindex af70080..f0987ea 100644
---- a/apps/text/public/zalgo.html
++++ b/apps/text/public/zalgo.html
@@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
</head>
@@@ -60,12 +59,12 @@
<div class="btn-row">
<button class="btn btn-primary" onclick="copyOutput()"><i class="fas fa-copy"></i> Copy</button>
<button class="btn btn-secondary" onclick="regenerate()"><i class="fas fa-rotate"></i> Regenerate</button>
-- <button class="btn btn-ghost" onclick="OpenVibeUI.shareText(lastOutput)"><i class="fas fa-share"></i> Share</button>
++ <button class="btn btn-ghost" onclick="OpenVibeUI.shareText(lastOutput)"><i class="fas fa-arrow-up-from-bracket"></i> Share</button>
</div>
</div>
<div class="tool-section">
-- <h2><i class="fas fa-broom"></i> Need to clean Zalgo?</h2>
++ <h2><i class="fas fa-eraser"></i> Need to clean Zalgo?</h2>
<p style="color: var(--text-secondary); margin-bottom: 12px;">Paste Zalgo text and strip all the combining characters.</p>
<div class="input-group">
<textarea id="cleanInput" placeholder="Paste Zalgo text to clean..." rows="3"></textarea>
ddiff --git a/apps/text/server/index.js b/apps/text/server/index.js
iindex dc37d7a..d767823 100644
---- a/apps/text/server/index.js
++++ b/apps/text/server/index.js
@@@ -64,9 +64,9 @@ app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
-- scriptSrc: ["'self'", "'unsafe-inline'", "cdnjs.cloudflare.com", "cdn.jsdelivr.net", "fonts.googleapis.com", "https://openvibe.network"],
-- styleSrc: ["'self'", "'unsafe-inline'", "cdnjs.cloudflare.com", "fonts.googleapis.com", "fonts.gstatic.com"],
-- fontSrc: ["'self'", "fonts.gstatic.com", "cdnjs.cloudflare.com"],
++ scriptSrc: ["'self'", "'unsafe-inline'", "cdn.jsdelivr.net", "fonts.googleapis.com", "https://openvibe.network"],
++ styleSrc: ["'self'", "'unsafe-inline'", "fonts.googleapis.com", "fonts.gstatic.com"],
++ fontSrc: ["'self'", "fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "blob:"],
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
connectSrc: ["'self'", "https://openvibe.network", "https://api.exchangerate-api.com", "https://openvibe.events"],
ddiff --git a/apps/yt/package-lock.json b/apps/yt/package-lock.json
iindex 949d26c..bf3ddd6 100644
---- a/apps/yt/package-lock.json
++++ b/apps/yt/package-lock.json
@@@ -19,7 +19,7 @@
"iovalkey": "^0.4.0",
"jsonwebtoken": "^9.0.2",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
"pg": "^8.23.0",
"uuid": "^11.1.0"
}
@@@ -1005,9 +1005,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "4.56.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
-- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",
++ "version": "4.73.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
++ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
ddiff --git a/apps/yt/public/index.html b/apps/yt/public/index.html
iindex a380b08..56755e5 100644
---- a/apps/yt/public/index.html
++++ b/apps/yt/public/index.html
@@@ -17,7 +17,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
@@@ -90,7 +89,7 @@
<i class="fa-solid fa-download"></i> Save File
</a>
<button class="btn btn-secondary" id="new-btn">
-- <i class="fa-solid fa-arrow-rotate-left"></i> Download Another
++ <i class="fa-solid fa-rotate-left"></i> Download Another
</button>
</div>
</section>
@@@ -99,7 +98,7 @@
<section class="error-section" id="error-section" style="display:none">
<div class="error-icon"><i class="fa-solid fa-circle-exclamation"></i></div>
<p id="error-msg"></p>
-- <button class="btn btn-secondary" id="retry-btn"><i class="fa-solid fa-arrow-rotate-left"></i> Try Again</button>
++ <button class="btn btn-secondary" id="retry-btn"><i class="fa-solid fa-rotate-left"></i> Try Again</button>
</section>
</div>
ddiff --git a/apps/yt/server/index.js b/apps/yt/server/index.js
iindex 238439a..1eb117f 100644
---- a/apps/yt/server/index.js
++++ b/apps/yt/server/index.js
@@@ -79,9 +79,9 @@ app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
-- scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://openvibe.network", "https://static.cloudflareinsights.com"],
-- styleSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
-- fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
++ scriptSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://openvibe.network", "https://static.cloudflareinsights.com"],
++ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
++ fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "https://i.ytimg.com", "https://img.youtube.com", "https://*.ggpht.com", "https://openvibe.network", "https://openvibe.media"],
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
connectSrc: ["'self'", "https://openvibe.network", "https://openvibe.tools", "https://*.openvibe.tools", "https://cloudflareinsights.com", "https://openvibe.events"],
ddiff --git a/test/no-font-awesome.test.js b/test/no-font-awesome.test.js
nnew file mode 100644
iindex 0000000..bdfe709
---- /dev/null
++++ b/test/no-font-awesome.test.js
@@@ -0,0 +1,83 @@
++'use strict';
++// No Font Awesome (plan L54 K4; the owner, 2026-11-01: "get rid of Font Awesome completely and just use our own
++// openvibe.design system"). OpenVibe.Tools's icons are the OpenVibe glyphs (openvibe-shared/glyphs, drawn in the browser by
++// /shared/ov-glyphs.js, which the Shared navbar loads on a page that names an icon) and the brand logos from
++// openvibe-shared/brands. This holds it there:
++// - no page or stylesheet links Font Awesome, and nothing draws with Font Awesome's font;
++// - every page that names an icon loads the navbar or the glyph drop-in;
++// - every fa- name OpenVibe.Tools uses is one the glyphs or the brands draw (a name nobody draws would show nothing).
++// node test/no-font-awesome.test.js
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const ROOT = path.join(__dirname, '..');
++// openvibe-shared is pinned per app here (each app has its own node_modules), so the glyphs come from one of them.
++const sharedDir = fs.existsSync(path.join(ROOT, 'node_modules', 'openvibe-shared'))
++ ? path.join(ROOT, 'node_modules', 'openvibe-shared')
++ : path.join(ROOT, 'apps', 'gateway', 'node_modules', 'openvibe-shared');
++const glyphs = require(path.join(sharedDir, 'glyphs'));
++const brands = require(path.join(sharedDir, 'brands'));
++
++const walk = (dir, ok, out = []) => {
++ if (!fs.existsSync(dir)) return out;
++ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
++ if (e.name === 'node_modules' || e.name === 'vendor' || e.name.startsWith('.')) continue;
++ const p = path.join(dir, e.name);
++ if (e.isDirectory()) walk(p, ok, out); else if (ok.test(e.name)) out.push(p);
++ }
++ return out;
++};
++const rel = (p) => path.relative(ROOT, p);
++
++// The pages (whole HTML files) and the code that draws them.
++const PAGE_ROOTS = ["apps"];
++const CODE_ROOTS = ["apps"];
++const LAYOUTS = []; // the server-rendered pages: their layout brings the navbar or the glyphs
++
++// A Font Awesome stylesheet, wherever it is written.
++const FA_CSS = /fontawesome[^"'\s]*\.css|font-awesome[^"'\s]*\.css|cdnjs\.cloudflare\.com\/ajax\/libs\/font-awesome/i;
++
++const pages = [];
++for (const r of PAGE_ROOTS) pages.push(...walk(path.join(ROOT, r), /\.html$/));
++for (const p of pages) {
++ const html = fs.readFileSync(p, 'utf8');
++ assert.ok(!/fontawesome|font-awesome/i.test(html), `${rel(p)} links Font Awesome`);
++ // A whole page (with a <head>; fragments are parts of a page) that names an icon loads what draws it.
++ if (/<head[\s>]/i.test(html) && /\bfa-[a-z]/.test(html) && !/\/shared\/navbar|ov-glyphs\.js/.test(html)) {
++ assert.fail(`${rel(p)} has fa- icons but loads neither the navbar nor /shared/ov-glyphs.js`);
++ }
++}
++
++// Nothing draws with Font Awesome's font.
++for (const r of CODE_ROOTS) {
++ for (const p of walk(path.join(ROOT, r), /\.css$/)) {
++ assert.ok(!/font-family:\s*["']?Font ?Awesome/i.test(fs.readFileSync(p, 'utf8')), `${rel(p)} draws with Font Awesome's font`);
++ }
++ for (const p of walk(path.join(ROOT, r), /\.(html|js|mjs|cjs)$/)) {
++ const t = fs.readFileSync(p, 'utf8');
++ assert.ok(!/['"]\d+px FontAwesome|Font Awesome 6 Free/.test(t), `${rel(p)} draws with Font Awesome's font`);
++ assert.ok(!FA_CSS.test(t), `${rel(p)} links Font Awesome`);
++ }
++}
++
++// The server-rendered pages get the glyphs from the Shared navbar (shell.page) or from the drop-in.
++for (const f of LAYOUTS) {
++ const t = fs.readFileSync(path.join(ROOT, f), 'utf8');
++ assert.ok(/shell\.page|ov-glyphs\.js|\/shared\/navbar/.test(t), `${f} renders pages without the navbar or /shared/ov-glyphs.js`);
++}
++
++// Every fa- name in the pages, the scripts and the server views is drawn by a glyph or a brand.
++const MOD = /^(solid|regular|light|thin|duotone|brands|sharp|classic|fw|lg|xl|xs|sm|2xs|2xl|\d+x|li|ul|border|inverse|pull-left|pull-right|rotate-\d+|rotate-by|flip-horizontal|flip-vertical|flip-both|width-auto|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|bounce|fade|flip|shake|stack|stack-1x|stack-2x|layers|layers-text|layers-counter|swap-opacity|stack-text)$/;
++const files = [];
++for (const r of CODE_ROOTS) files.push(...walk(path.join(ROOT, r), /\.(html|js)$/));
++const missing = new Map();
++for (const f of files) {
++ for (const m of fs.readFileSync(f, 'utf8').matchAll(/\bfa-([a-z0-9]+(?:-[a-z0-9]+)*)\b/g)) {
++ const n = m[1];
++ if (MOD.test(n) || glyphs.fromFa(n) || brands.fromFa(n)) continue;
++ if (!missing.has(n)) missing.set(n, rel(f));
++ }
++}
++assert.deepStrictEqual([...missing].map(([n, f]) => `fa-${n} (${f})`), [], 'every icon this site names is drawn by the OpenVibe glyphs or brands');
++
++console.log(`no font awesome: ${pages.length} pages, ${files.length} files, every fa- name drawn`);