From e8d8027f22532fdd5d1680fa83ddbc58982aa280 Mon Sep 17 00:00:00 2001 From: 360 Date: Sun, 19 Jul 2026 09:32:01 -0400 Subject: [PATCH 1/2] Add pinning for MySite and Canvas apps --- apps.html | 13 ++- assets/css/main.css | 98 +++++++++++++++++++ assets/js/main.js | 222 +++++++++++++++++++++++++++++++++++++------- 3 files changed, 297 insertions(+), 36 deletions(-) diff --git a/apps.html b/apps.html index 3fde8ec..f2499c9 100644 --- a/apps.html +++ b/apps.html @@ -287,18 +287,23 @@

📦 Apps

360Maps
Get directions and search places fast with 360Maps.
+ -
+ + + - +
diff --git a/assets/css/main.css b/assets/css/main.css index 2248725..57848b8 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -1396,3 +1396,101 @@ body.dark .nav-ripple { text-fill-color: currentColor; font-variant-emoji: emoji; } + +/* ============================================================ + PINNED APPS MENU SHORTCUTS + ============================================================ */ +.nav-section { + margin-top: 16px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.nav-section-label { + font-size: 10px; + font-weight: 700; + letter-spacing: .08em; + text-transform: uppercase; + color: var(--mut); + padding: 0 12px 3px; +} +body.dark .nav-section-label { color: var(--mutd); } + +.pinned-app-item { + display: flex; + align-items: center; + gap: 8px; + border: 1px solid rgba(59,130,246,.12); + background: linear-gradient(120deg, rgba(59,130,246,.08), rgba(6,182,212,.05)); +} +body.dark .pinned-app-item { + border-color: rgba(255,255,255,.08); + background: linear-gradient(120deg, rgba(59,130,246,.16), rgba(6,182,212,.08)); +} + +.pinned-app-icon { line-height: 1; } + +.settings-select { + width: 100%; + padding: 9px 10px; + border-radius: 8px; + border: 1px solid var(--br); + background: rgba(255,255,255,.65); + color: var(--txt); + outline: none; +} +body.dark .settings-select { + background: rgba(255,255,255,.06); + color: var(--txtd); +} + +.pinned-apps-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.pinned-app-toggle { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 8px 10px; + border: 1px solid var(--br); + border-radius: 10px; + background: rgba(148,163,184,.1); + color: var(--txt); + font-size: 13px; + cursor: pointer; +} +body.dark .pinned-app-toggle { + background: rgba(255,255,255,.05); + color: var(--txtd); +} + +.app-pin-btn { + position: absolute; + left: 10px; + top: 10px; + z-index: 2; + padding: 4px 9px; + border-radius: 999px; + border: 1px solid var(--br); + background: rgba(255,255,255,.86); + color: var(--txt); + font-size: 11px; + font-weight: 700; + cursor: pointer; + transition: .2s; +} +body.dark .app-pin-btn { + background: rgba(15,23,42,.86); + color: var(--txtd); +} +.app-pin-btn:hover, +.app-pin-btn.pinned { + background: linear-gradient(110deg, var(--a), var(--a2)); + border-color: transparent; + color: #050816; +} diff --git a/assets/js/main.js b/assets/js/main.js index 4d1f66e..42a4110 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -25,6 +25,55 @@ const body = document.body; data-extra attribute, e.g.: ============================================================ */ + +const PINNED_APPS_STORAGE_KEY = "360_pinned_apps"; +const PINNED_APPS_POSITION_KEY = "360_pinned_apps_position"; +const PINNED_APP_CATALOG = [ + { label: "360Vids", href: "/apps/360vids", icon: "🎬" }, + { label: "360Music", href: "/apps/360Music", icon: "🎶" }, + { label: "Zone", href: "/apps/360zone", icon: "🏫" }, + { label: "360Notes", href: "/apps/360Notes", icon: "📖" }, + { label: "360Draw", href: "/apps/360Draw", icon: "🎨" }, + { label: "360Docs", href: "/apps/360Docs", icon: "📃" }, + { label: "360Do", href: "/apps/360Do", icon: "💡" }, + { label: "360Mail", href: "/apps/360mail-claim", icon: "✉️" }, + { label: "360Studio", href: "/apps/360Studio", icon: "🎛️" }, + { label: "360MySite", href: "/apps/360MySite", icon: "🌐" }, + { label: "360Canvas", href: "/apps/360Canvas", icon: "🚀" } +]; + +function safeParseJSON(raw, fallback) { + try { return raw ? JSON.parse(raw) : fallback; } catch { return fallback; } +} + +function normalizeAppHref(href) { + try { return new URL(href, window.location.origin).pathname.replace(/\.html$/, "").replace(/\/+$/, "") || "/"; } + catch { return href.replace(/\.html$/, "").replace(/\/+$/, "") || "/"; } +} + +function getPinnedAppHrefs() { + const saved = safeParseJSON(localStorage.getItem(PINNED_APPS_STORAGE_KEY), []); + return Array.isArray(saved) ? saved.map(normalizeAppHref) : []; +} + +function savePinnedAppHrefs(hrefs) { + const unique = Array.from(new Set(hrefs.map(normalizeAppHref))); + localStorage.setItem(PINNED_APPS_STORAGE_KEY, JSON.stringify(unique)); +} + +function getPinnedApps() { + const pinned = getPinnedAppHrefs(); + return pinned.map(href => PINNED_APP_CATALOG.find(app => normalizeAppHref(app.href) === href)).filter(Boolean); +} + +function getPinnedAppsPosition() { + return localStorage.getItem(PINNED_APPS_POSITION_KEY) || "after-apps"; +} + +function escapeHtml(value) { + return String(value).replace(/[&<>"]/g, ch => ({ "&": "&", "<": "<", ">": ">", '"': """ }[ch])); +} + const SIDEBAR_NAV_ITEMS = [ { label: "Home", href: "/" }, { label: "AI", href: "/ai" }, @@ -47,10 +96,25 @@ function renderSidebar() { try { extraItems = JSON.parse(slot.dataset.extra); } catch {} } - const allItems = SIDEBAR_NAV_ITEMS.concat(extraItems); + const pinnedApps = getPinnedApps(); + const position = getPinnedAppsPosition(); + const appItemHtml = pinnedApps + .map(it => ``) + .join(""); + const pinnedSectionHtml = pinnedApps.length + ? `` + : ""; + + let allItems = SIDEBAR_NAV_ITEMS.concat(extraItems); + if (position === "after-apps") { + const appsIndex = allItems.findIndex(it => it.label === "Apps"); + allItems = appsIndex >= 0 + ? allItems.slice(0, appsIndex + 1).concat(pinnedApps.map(app => ({ ...app, pinned: true })), allItems.slice(appsIndex + 1)) + : allItems.concat(pinnedApps.map(app => ({ ...app, pinned: true }))); + } const navHtml = allItems - .map(it => ``) + .map(it => ``) .join(""); slot.innerHTML = ` @@ -58,7 +122,9 @@ function renderSidebar() {
+ ${position === "top" ? pinnedSectionHtml : ""} + ${position === "bottom" ? pinnedSectionHtml : ""} `; @@ -514,7 +580,9 @@ sidebarToggle?.addEventListener("click", e => { }); /* Settings toggle */ -settingsBtn?.addEventListener("click", e => { +document.addEventListener("click", e => { + const btn = e.target.closest("#settingsBtn"); + if (!btn) return; e.stopPropagation(); settingsPanel?.classList.toggle("open"); updateOverlay(); @@ -542,38 +610,55 @@ document.addEventListener("click", e => { }); /* Nav item navigation */ -navItems.forEach(item => { - item.addEventListener("click", e => { - e.stopPropagation(); +sidebar?.addEventListener("click", e => { + const item = e.target.closest(".nav-item[data-href]"); + if (!item || !sidebar.contains(item)) return; + e.stopPropagation(); - const ripple = document.createElement("span"); - ripple.className = "nav-ripple"; - const rect = item.getBoundingClientRect(); - const size = Math.max(rect.width, rect.height); - ripple.style.width = ripple.style.height = `${size}px`; - ripple.style.left = `${e.clientX - rect.left - size / 2}px`; - ripple.style.top = `${e.clientY - rect.top - size / 2}px`; - item.appendChild(ripple); - ripple.addEventListener("animationend", () => ripple.remove(), { once: true }); - - const href = item.dataset.href; - if (href) { - const current = window.location.pathname || "/"; - const targetPath = new URL(href, window.location.origin).pathname; - if (targetPath !== current) { - const raw = sessionStorage.getItem("navHistory"); - const history = raw ? JSON.parse(raw) : []; - if (!history.length || history[history.length - 1] !== current) history.push(current); - sessionStorage.setItem("navHistory", JSON.stringify(history.slice(-30))); - } - setTimeout(() => { window.location.href = href; }, 140); + const ripple = document.createElement("span"); + ripple.className = "nav-ripple"; + const rect = item.getBoundingClientRect(); + const size = Math.max(rect.width, rect.height); + ripple.style.width = ripple.style.height = `${size}px`; + ripple.style.left = `${e.clientX - rect.left - size / 2}px`; + ripple.style.top = `${e.clientY - rect.top - size / 2}px`; + item.appendChild(ripple); + ripple.addEventListener("animationend", () => ripple.remove(), { once: true }); + + const href = item.dataset.href; + if (href) { + const current = window.location.pathname || "/"; + const targetPath = new URL(href, window.location.origin).pathname; + if (targetPath !== current) { + const raw = sessionStorage.getItem("navHistory"); + const history = raw ? JSON.parse(raw) : []; + if (!history.length || history[history.length - 1] !== current) history.push(current); + sessionStorage.setItem("navHistory", JSON.stringify(history.slice(-30))); } + setTimeout(() => { window.location.href = href; }, 140); + } - sidebar?.classList.remove("open"); - updateOverlay(); - }); + sidebar?.classList.remove("open"); + updateOverlay(); }); +function refreshSidebarAfterPinnedAppChange() { + renderSidebar(); + markActiveNav(); + initPinnedAppsSettings(); + initAppsPagePinButtons(); +} + +function togglePinnedApp(href) { + const normalized = normalizeAppHref(href); + const pinned = getPinnedAppHrefs(); + const next = pinned.includes(normalized) + ? pinned.filter(item => item !== normalized) + : pinned.concat(normalized); + savePinnedAppHrefs(next); + refreshSidebarAfterPinnedAppChange(); +} + Array.from(document.querySelectorAll(".back-btn")).forEach(btn => { btn.addEventListener("click", e => { e.preventDefault(); @@ -748,7 +833,7 @@ if (installBtn) installBtn.onclick = () => deferredPrompt?.prompt(); /* ============================================================ ACTIVE NAV MARK ============================================================ */ -(function markActiveNav() { +function markActiveNav() { const path = location.pathname.replace(/\/+$/, "") || "/"; $$(".nav-item[data-href]").forEach(item => { const href = item.dataset.href.replace(/\/+$/, "") || "/"; @@ -756,6 +841,79 @@ if (installBtn) installBtn.onclick = () => deferredPrompt?.prompt(); const normHref = href.replace(/^(\.\.\/)+/, "/"); item.classList.toggle("active", path === normHref || path.endsWith(normHref)); }); -})(); +} +markActiveNav(); + + +/* ============================================================ + PINNED APPS CUSTOMIZATION + ============================================================ */ +function initPinnedAppsSettings() { + const panel = document.getElementById("settingsPanel"); + if (!panel || panel.querySelector("#pinnedAppsSettings")) return; + + const section = document.createElement("div"); + section.id = "pinnedAppsSettings"; + section.className = "pinned-apps-settings"; + section.innerHTML = ` +

Pinned Apps

+
Menu bar location
+ +
Pinned shortcuts
+
+ `; + panel.appendChild(section); + + const positionSelect = section.querySelector("#pinnedAppsPosition"); + positionSelect.value = getPinnedAppsPosition(); + positionSelect.addEventListener("change", () => { + localStorage.setItem(PINNED_APPS_POSITION_KEY, positionSelect.value); + refreshSidebarAfterPinnedAppChange(); + }); + + const list = section.querySelector("#pinnedAppsList"); + PINNED_APP_CATALOG.forEach(app => { + const row = document.createElement("label"); + row.className = "pinned-app-toggle"; + const checked = getPinnedAppHrefs().includes(normalizeAppHref(app.href)); + row.innerHTML = `${app.icon} ${escapeHtml(app.label)}`; + row.querySelector("input").addEventListener("change", () => togglePinnedApp(app.href)); + list.appendChild(row); + }); +} + +function initAppsPagePinButtons() { + document.querySelectorAll(".app-card[href]").forEach(card => { + const href = normalizeAppHref(card.getAttribute("href")); + const app = PINNED_APP_CATALOG.find(item => normalizeAppHref(item.href) === href); + if (!app) return; + + let btn = card.querySelector(".app-pin-btn"); + if (!btn) { + btn = document.createElement("button"); + btn.type = "button"; + btn.className = "app-pin-btn"; + btn.dataset.href = app.href; + btn.addEventListener("click", event => { + event.preventDefault(); + event.stopPropagation(); + togglePinnedApp(app.href); + }); + card.appendChild(btn); + } + + const pinned = getPinnedAppHrefs().includes(normalizeAppHref(app.href)); + btn.classList.toggle("pinned", pinned); + btn.textContent = pinned ? "★ Pinned" : "☆ Pin"; + btn.setAttribute("aria-label", `${pinned ? "Unpin" : "Pin"} ${app.label} on the menu bar`); + }); +} + +initPinnedAppsSettings(); +initAppsPagePinButtons(); console.log("%c360 V.3.0.0 — main.js loaded.", "color:#4ade80;font-weight:bold;font-size:14px;"); From 73db78a7a530d9e3df3560a55d90733f2b0b67a9 Mon Sep 17 00:00:00 2001 From: 360 Date: Sun, 19 Jul 2026 09:35:35 -0400 Subject: [PATCH 2/2] Resolve pinned app merge conflicts --- apps.html | 13 ++++--------- assets/js/main.js | 30 +++++++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/apps.html b/apps.html index f2499c9..3fde8ec 100644 --- a/apps.html +++ b/apps.html @@ -287,23 +287,18 @@

📦 Apps

360Maps
Get directions and search places fast with 360Maps.
- - - diff --git a/assets/js/main.js b/assets/js/main.js index 42a4110..56c9bab 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -887,11 +887,35 @@ function initPinnedAppsSettings() { } function initAppsPagePinButtons() { - document.querySelectorAll(".app-card[href]").forEach(card => { - const href = normalizeAppHref(card.getAttribute("href")); - const app = PINNED_APP_CATALOG.find(item => normalizeAppHref(item.href) === href); + document.querySelectorAll(".app-card").forEach(card => { + const title = card.querySelector(".app-title")?.textContent?.trim(); + const cardHref = card.getAttribute("href"); + const app = PINNED_APP_CATALOG.find(item => + (cardHref && normalizeAppHref(item.href) === normalizeAppHref(cardHref)) || + item.label === title + ); if (!app) return; + // Some newly launched apps may still be plain Coming Soon cards in older + // markup. Activate them progressively here so users can open and pin them + // without requiring another apps.html merge touchpoint. + if (!cardHref) { + card.style.opacity = ""; + card.style.pointerEvents = ""; + card.setAttribute("role", "link"); + card.setAttribute("tabindex", "0"); + if (!card.dataset.appActivated) { + card.dataset.appActivated = "true"; + card.addEventListener("click", () => { window.location.href = app.href; }); + card.addEventListener("keydown", event => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + window.location.href = app.href; + } + }); + } + } + let btn = card.querySelector(".app-pin-btn"); if (!btn) { btn = document.createElement("button");