diff --git a/i18n/english.js b/i18n/english.js index 85a4f833..9d8adf11 100644 --- a/i18n/english.js +++ b/i18n/english.js @@ -218,7 +218,9 @@ const ui = { childOf: "child of", parentOf: "parent of", unlocked: "unlocked", - locked: "locked" + locked: "locked", + switchPayload: "Switch payload", + removeFromCache: "Remove from cache" }, search: { packagesCache: "Packages available in the cache", diff --git a/i18n/french.js b/i18n/french.js index 419ca2f3..904b082b 100644 --- a/i18n/french.js +++ b/i18n/french.js @@ -218,7 +218,9 @@ const ui = { childOf: "enfant de", parentOf: "parent de", unlocked: "Déverrouillé", - locked: "Verrouillé" + locked: "Verrouillé", + switchPayload: "Changer de payload", + removeFromCache: "Retirer du cache" }, search: { packagesCache: "Packages disponibles dans le cache", diff --git a/public/components/navigation/navigation.css b/public/components/navigation/navigation.css index 80134c97..e9979ff4 100644 --- a/public/components/navigation/navigation.css +++ b/public/components/navigation/navigation.css @@ -76,23 +76,3 @@ nav#aside>ul li.bottom-nav { margin-top: auto; } -#search-nav { - z-index: 30; - display: flex; - justify-content: center; - align-items: center; - position: absolute; - height: 30px; - left: 50px; - padding-left: 20px; - max-width: calc(100vw - 70px); - box-sizing: border-box; - background: var(--primary); - transform: skewX(-20deg); - box-shadow: 2px 1px 10px #26107f7a; -} - -body.dark #search-nav { - background: var(--dark-theme-primary-color); -} - diff --git a/public/components/drill-breadcrumb/drill-breadcrumb.js b/public/components/network-breadcrumb/network-breadcrumb.js similarity index 51% rename from public/components/drill-breadcrumb/drill-breadcrumb.js rename to public/components/network-breadcrumb/network-breadcrumb.js index 1b538cb7..24e48156 100644 --- a/public/components/drill-breadcrumb/drill-breadcrumb.js +++ b/public/components/network-breadcrumb/network-breadcrumb.js @@ -3,18 +3,28 @@ import { LitElement, html, css, nothing } from "lit"; // Import Internal Dependencies import { EVENTS } from "../../core/events.js"; +import { currentLang } from "../../common/utils.js"; -class DrillBreadcrumb extends LitElement { +class NetworkBreadcrumb extends LitElement { static styles = css` :host { + --breadcrumb-bg: var(--primary); + --breadcrumb-shadow: 2px 1px 10px rgb(38 16 127 / 48%); + --breadcrumb-dropdown-bg: var(--primary-darker); + --breadcrumb-dropdown-border: rgb(255 255 255 / 20%); + --breadcrumb-dropdown-hover: rgb(255 255 255 / 15%); + --breadcrumb-header-color: rgb(255 255 255 / 50%); + --breadcrumb-separator-bg: rgb(255 255 255 / 15%); + position: absolute; - top: 38px; + top: 8px; left: 10px; z-index: 20; display: flex; align-items: center; gap: 4px; - background: rgb(10 10 20 / 72%); + background: var(--breadcrumb-bg); + box-shadow: var(--breadcrumb-shadow); border-radius: 6px; padding: 4px 10px; font-family: mononoki, monospace; @@ -22,6 +32,16 @@ class DrillBreadcrumb extends LitElement { color: #fff; } + :host-context(body.dark) { + --breadcrumb-bg: rgb(10 10 20 / 72%); + --breadcrumb-shadow: none; + --breadcrumb-dropdown-bg: rgb(10 10 20 / 95%); + --breadcrumb-dropdown-border: rgb(255 255 255 / 15%); + --breadcrumb-dropdown-hover: rgb(255 255 255 / 10%); + --breadcrumb-header-color: rgb(255 255 255 / 40%); + --breadcrumb-separator-bg: rgb(255 255 255 / 10%); + } + :host([hidden]) { display: none !important; } @@ -72,14 +92,11 @@ class DrillBreadcrumb extends LitElement { .dropdown { position: absolute; top: calc(100% + 6px); - left: 50%; - transform: translateX(-50%); - background: rgb(10 10 20 / 95%); - border: 1px solid rgb(255 255 255 / 15%); + left: 0; + background: var(--breadcrumb-dropdown-bg); + border: 1px solid var(--breadcrumb-dropdown-border); border-radius: 6px; padding: 4px 0; - min-width: 180px; - max-height: 260px; overflow-y: auto; z-index: 30; box-shadow: 0 4px 16px rgb(0 0 0 / 50%); @@ -97,17 +114,73 @@ class DrillBreadcrumb extends LitElement { } .dropdown button:hover { - background: rgb(255 255 255 / 10%); + background: var(--breadcrumb-dropdown-hover); color: #fff; text-decoration: none; } + + .dropdown-header { + display: block; + padding: 4px 12px 3px; + font-size: 10px; + color: var(--breadcrumb-header-color); + text-transform: uppercase; + letter-spacing: 0.08em; + cursor: default; + } + + .dropdown-separator { + height: 1px; + background: var(--breadcrumb-separator-bg); + margin: 3px 0 4px; + } + + .root-switcher-wrapper { + position: static; + } + + .root-switcher { + opacity: 0.55; + font-size: 11px; + padding: 0 3px; + } + + .root-switcher:hover { + opacity: 1; + text-decoration: none; + } + + .root-entry { + display: inline-flex; + align-items: center; + gap: 5px; + } + + .root-label { + color: rgb(255 255 255 / 85%); + } + + .remove-btn { + opacity: 0.3; + font-size: 14px; + line-height: 1; + padding: 0 1px; + } + + .remove-btn:hover { + opacity: 1; + color: #ff6b6b; + text-decoration: none; + } `; static properties = { root: { type: Object }, stack: { type: Array }, siblings: { type: Array }, - _openDropdown: { state: true } + packages: { type: Array }, + _openDropdown: { state: true }, + _rootSwitcherOpen: { state: true } }; constructor() { @@ -115,7 +188,9 @@ class DrillBreadcrumb extends LitElement { this.root = null; this.stack = []; this.siblings = []; + this.packages = []; this._openDropdown = null; + this._rootSwitcherOpen = false; } connectedCallback() { @@ -129,13 +204,17 @@ class DrillBreadcrumb extends LitElement { } updated() { - this.hidden = this.stack.length === 0 || this.root === null; + this.hidden = this.root === null; } #handleDocumentClick = () => { if (this._openDropdown !== null) { this._openDropdown = null; } + + if (this._rootSwitcherOpen) { + this._rootSwitcherOpen = false; + } }; #handleReset() { @@ -170,13 +249,68 @@ class DrillBreadcrumb extends LitElement { })); } + #toggleRootSwitcher(event) { + event.stopPropagation(); + + this._rootSwitcherOpen = !this._rootSwitcherOpen; + } + + #handleRootSwitch(spec, event) { + event.stopPropagation(); + + this._rootSwitcherOpen = false; + this.dispatchEvent(new CustomEvent(EVENTS.ROOT_SWITCH, { + detail: { spec }, + bubbles: true, + composed: true + })); + } + + #handleRootRemove(event) { + event.stopPropagation(); + + this.dispatchEvent(new CustomEvent(EVENTS.ROOT_REMOVE, { + bubbles: true, + composed: true + })); + } + render() { - if (this.stack.length === 0 || this.root === null) { + if (this.root === null) { return nothing; } + const otherPackages = this.packages ?? []; + const isInDrill = this.stack.length > 0; + const i18n = window.i18n[currentLang()]; + return html` - + ${otherPackages.length > 0 ? html` + + + ${this._rootSwitcherOpen ? html` + + ` : nothing} + + ` : nothing} + + ${isInDrill + ? html`` + : html`${this.root.name}@${this.root.version}` + } + + ${this.stack.map((entry, stackIndex) => { const siblingList = this.siblings?.[stackIndex] ?? []; const hasSiblings = siblingList.length > 0; @@ -212,4 +346,4 @@ class DrillBreadcrumb extends LitElement { } } -customElements.define("drill-breadcrumb", DrillBreadcrumb); +customElements.define("network-breadcrumb", NetworkBreadcrumb); diff --git a/public/components/package-navigation/package-navigation.js b/public/components/package-navigation/package-navigation.js deleted file mode 100644 index a6ac733c..00000000 --- a/public/components/package-navigation/package-navigation.js +++ /dev/null @@ -1,260 +0,0 @@ -// Import Third-party Dependencies -import { LitElement, html, css, nothing } from "lit"; -import { repeat } from "lit/directives/repeat.js"; - -// Import Internal Dependencies -import * as utils from "../../common/utils.js"; -import "../icon/icon.js"; - -/** - * @typedef {Object} PackageMetadata - * @property {string} spec - Package spec (e.g. "package@1.0.0") - * @property {string} scanType - Type of scan ("cwd" for local) - * @property {string} locationOnDisk - Path to the package on disk - * @property {number} lastUsedAt - Timestamp of last usage - * @property {string | null} integrity - Package integrity hash - */ - -class PackageNavigation extends LitElement { - static styles = css` - b,p { - margin: 0; - padding: 0; - border: 0; - font: inherit; - font-size: 100%; - } - - :host { - z-index: 30; - display: flex; - justify-content: center; - align-items: center; - height: 30px; - left: 50px; - padding-left: 20px; - max-width: calc(100vw - 70px); - box-sizing: border-box; - background: var(--primary); - box-shadow: 2px 1px 10px #26107f7a; - } - - :host-context(body.dark) { - background: var(--dark-theme-primary-color); - } - - .packages { - height: 30px; - display: flex; - background: var(--primary); - } - - .packages > .package { - height: 30px; - font-family: mononoki; - display: flex; - align-items: center; - background: linear-gradient(to right, rgb(55 34 175 / 100%) 0%, rgb(87 74 173 / 100%) 50%, rgb(59 110 205) 100%); - padding: 0 10px; - border-right: 2px solid #0f041a; - text-shadow: 1px 1px 10px #000; - color: #def7ff; - } - - :host-context(body.dark) .packages > .package { - background: linear-gradient(to right, rgb(11 3 31 / 100%) 0%, rgb(11 3 31 / 80%) 50%, rgb(11 3 31 / 60%) 100%); - } - - .packages > .package > * { - transform: skewX(20deg); - } - - .packages > .package:first-child { - padding-left: 10px; - } - - .packages > .package:not(.active):hover { - background: linear-gradient(to right, rgb(55 34 175 / 100%) 1%, rgb(68 121 218) 100%); - color: #defff9; - cursor: pointer; - } - - :host-context(body.dark) .packages > .package:not(.active):hover { - background: linear-gradient(to right, rgb(11 3 31 / 70%) 1%, rgb(11 3 31 / 50%) 100%); - } - - .packages > .package.active { - background: linear-gradient(to right, rgb(55 34 175 / 100%) 0%, rgb(87 74 173 / 100%) 50%, rgb(59 110 205) 100%); - } - - .packages > .package.active > b { - background: var(--secondary); - } - - .packages > .package.active > .remove { - display: block; - } - - .packages > .package > b:last-of-type:not(:first-of-type) { - background: #f57c00; - } - - .packages > .package > b { - font-weight: bold; - font-size: 12px; - margin-left: 5px; - background: var(--secondary-darker); - padding: 3px 5px; - border-radius: 2px; - font-family: Roboto; - letter-spacing: 1px; - } - - .add { - height: 30px; - font-size: 20px; - border: none; - background: var(--secondary-darker); - cursor: pointer; - padding: 0 7px; - transition: 0.2s all ease; - color: #def7ff; - } - - .add:hover { - background: var(--secondary); - cursor: pointer; - } - - .add > i { - transform: skewX(20deg); - } - - button.remove { - display: none; - border: none; - position: relative; - cursor: pointer; - color: #fff5dc; - background: #ff3434e2; - margin-left: 10px; - border-radius: 50%; - line-height: 16px; - text-shadow: 1px 1px 10px #000; - font-weight: bold; - width: 20px; - } - - button.remove:hover { - cursor: pointer; - background: #ff5353e2; - } - `; - - static properties = { - /** - * Array of package metadata objects - * @type {PackageMetadata[]} - */ - metadata: { type: Array }, - /** - * Currently active package spec - * @type {string} - */ - activePackage: { type: String } - }; - - constructor() { - super(); - /** @type {PackageMetadata[]} */ - this.metadata = []; - /** @type {string} */ - this.activePackage = ""; - } - - /** - * Check if there are at least 2 packages - * @returns {boolean} - */ - get #hasAtLeast2Packages() { - return this.metadata.length > 1; - } - - /** - * Handle click on a package to select it - * @param {string} spec - */ - #handlePackageClick(spec) { - if (this.activePackage !== spec) { - window.socket.commands.search(spec); - } - } - - /** - * Handle click on remove button - * @param {Event} event - * @param {string} packageName - */ - #handleRemoveClick(event, packageName) { - event.stopPropagation(); - window.socket.commands.remove(packageName); - } - - #handleAddClick() { - window.navigation.setNavByName("search--view"); - } - - /** - * Render a single package element - * @param {PackageMetadata} param0 - * @returns {import("lit").TemplateResult} - */ - #renderPackage({ spec, scanType }) { - const isLocal = scanType === "cwd"; - const { name, version } = utils.parseNpmSpec(spec); - const isActive = spec === this.activePackage; - - return html` -
-

${name}

- v${version} - ${isLocal ? html`local` : nothing} - ${this.#hasAtLeast2Packages - ? html` - - ` - : nothing} -
- `; - } - - render() { - if (this.metadata.length === 0) { - return nothing; - } - - return html` -
- ${repeat( - this.metadata, - (pkg) => pkg.spec, - (pkg) => this.#renderPackage(pkg) - )} -
- - `; - } -} - -customElements.define("package-navigation", PackageNavigation); diff --git a/public/core/events.js b/public/core/events.js index c1f7c421..04421754 100644 --- a/public/core/events.js +++ b/public/core/events.js @@ -13,5 +13,7 @@ export const EVENTS = { SEARCH_COMMAND_INIT: "search-command-init", DRILL_RESET: "drill-reset", DRILL_BACK: "drill-back", - DRILL_SWITCH: "drill-switch" + DRILL_SWITCH: "drill-switch", + ROOT_SWITCH: "root-switch", + ROOT_REMOVE: "root-remove" }; diff --git a/public/core/search-nav.js b/public/core/search-nav.js deleted file mode 100644 index 7468e3bc..00000000 --- a/public/core/search-nav.js +++ /dev/null @@ -1,42 +0,0 @@ -// Import Internal Dependencies -import { createDOMElement } from "../common/utils.js"; -import "../components/package-navigation/package-navigation.js"; - -// CONSTANTS -const kSearchShortcut = navigator.userAgent.includes("Mac") ? "⌘K" : "Ctrl+K"; - -export function initSearchNav( - data, - options = {} -) { - const { - initFromZero = true - } = options; - - const searchNavElement = document.getElementById("search-nav"); - if (!searchNavElement) { - throw new Error("Unable to found search navigation"); - } - - if (initFromZero) { - searchNavElement.innerHTML = ""; - const element = document.createElement("package-navigation"); - searchNavElement.appendChild( - element - ); - element.metadata = data; - element.activePackage = data.length > 0 ? data[0].spec : ""; - } - - if (document.getElementById("search-shortcut-hint") === null) { - document.body.appendChild( - createDOMElement("div", { - classList: ["search-shortcut-hint"], - attributes: { id: "search-shortcut-hint" }, - childs: [ - createDOMElement("kbd", { text: kSearchShortcut }) - ] - }) - ); - } -} diff --git a/public/css/light.css b/public/css/light.css index c6cda34c..85646711 100644 --- a/public/css/light.css +++ b/public/css/light.css @@ -15,6 +15,7 @@ --dark-theme-accent-darker: #261c66; --dark-theme-accent-color: #8d7afc; --dark-theme-gray: #191a38; + } body { @@ -28,4 +29,5 @@ body { body.dark { color: white; background: var(--dark-theme-gray); + } diff --git a/public/main.js b/public/main.js index 6ffc12f0..92e3d5b2 100644 --- a/public/main.js +++ b/public/main.js @@ -13,14 +13,16 @@ import "./components/search-command/search-command.js"; import { Settings } from "./components/views/settings/settings.js"; import { HomeView } from "./components/views/home/home.js"; import "./components/views/search/search.js"; -import "./components/drill-breadcrumb/drill-breadcrumb.js"; +import "./components/network-breadcrumb/network-breadcrumb.js"; import { NetworkNavigation } from "./core/network-navigation.js"; import { i18n } from "./core/i18n.js"; -import { initSearchNav } from "./core/search-nav.js"; import * as utils from "./common/utils.js"; import { EVENTS } from "./core/events.js"; import { WebSocketClient } from "./websocket.js"; +// CONSTANTS +const kSearchShortcut = navigator.userAgent.includes("Mac") ? "⌘K" : "Ctrl+K"; + let secureDataSet; let nsn; let homeView; @@ -42,7 +44,17 @@ document.addEventListener("DOMContentLoaded", async() => { // update searchview after window.i18n is set searchview.requestUpdate(); - drillBreadcrumb = document.querySelector("drill-breadcrumb"); + document.body.appendChild( + utils.createDOMElement("div", { + classList: ["search-shortcut-hint"], + attributes: { id: "search-shortcut-hint" }, + childs: [ + utils.createDOMElement("kbd", { text: kSearchShortcut }) + ] + }) + ); + + drillBreadcrumb = document.querySelector("network-breadcrumb"); drillBreadcrumb.addEventListener(EVENTS.DRILL_RESET, resetDrill); drillBreadcrumb.addEventListener(EVENTS.DRILL_BACK, function handleDrillBack(event) { drillBackTo(event.detail.index); @@ -52,6 +64,22 @@ document.addEventListener("DOMContentLoaded", async() => { drillStack.length = stackIndex; drillInto(nodeId); }); + drillBreadcrumb.addEventListener(EVENTS.ROOT_SWITCH, function handleRootSwitch(event) { + window.socket.commands.search(event.detail.spec); + }); + drillBreadcrumb.addEventListener(EVENTS.ROOT_REMOVE, function handleRootRemove() { + const specToRemove = window.activePackage; + const nextPackage = drillBreadcrumb.packages[0]; + if (nextPackage) { + window.socket.commands.search(nextPackage.spec); + } + else { + window.navigation.hideMenu("network--view"); + window.navigation.hideMenu("home--view"); + window.navigation.setNavByName("search--view"); + } + window.socket.commands.remove(specToRemove); + }); await init(); window.dispatchEvent( @@ -85,7 +113,6 @@ async function onSocketPayload(event) { window.activePackage = name + "@" + version; await init({ navigateToNetworkView: true }); - initSearchNav(payload, { initFromZero: false }); dispatchSearchCommandInit(); } @@ -112,11 +139,8 @@ async function onSocketInitOrReload(event) { await init(); } - const navCache = cache.slice(0, 3); - const overflowCache = cache.slice(3); - - initSearchNav(navCache); - searchview.cachedSpecs = overflowCache; + drillBreadcrumb.packages = cache.filter((pkg) => pkg.spec !== window.activePackage); + searchview.cachedSpecs = cache; searchview.reset(); dispatchSearchCommandInit(); } @@ -222,6 +246,9 @@ function updateDrillBreadcrumb() { name: rootEntry.name, version: rootEntry.version }; + drillBreadcrumb.packages = window.cachedSpecs.filter( + (pkg) => pkg.spec !== window.activePackage + ); drillBreadcrumb.stack = drillStack.map((nodeId) => { const entry = secureDataSet.linker.get(nodeId); @@ -297,17 +324,6 @@ async function init(options = {}) { window.navigation.setNavByName("network--view"); } - // update search nav - const pkgs = document.querySelectorAll("#search-nav .packages > .package"); - for (const pkg of pkgs) { - if (pkg.dataset.name.startsWith(window.activePackage)) { - pkg.classList.add("active"); - } - else { - pkg.classList.remove("active"); - } - } - PackageInfo.close(); console.log("[INFO] Node-Secure is ready!"); diff --git a/views/index.html b/views/index.html index b16cb4d4..84e35e22 100644 --- a/views/index.html +++ b/views/index.html @@ -64,8 +64,6 @@ -
- +

[[=z.token('network.unlocked')]]