From 9ab4aa10b2e425df818957ffb2bc5cd6d2df4c51 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 11 Jun 2026 23:15:14 +0100 Subject: Initial commit --- Website/app.js | 231 ++++++++++++++++++++++++++++++++++ Website/banner.png | Bin 0 -> 60292 bytes Website/favicon.ico | Bin 0 -> 77918 bytes Website/index.html | 221 ++++++++++++++++++++++++++++++++ Website/styles.css | 356 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 808 insertions(+) create mode 100644 Website/app.js create mode 100644 Website/banner.png create mode 100644 Website/favicon.ico create mode 100644 Website/index.html create mode 100644 Website/styles.css (limited to 'Website') diff --git a/Website/app.js b/Website/app.js new file mode 100644 index 0000000..b15c8fe --- /dev/null +++ b/Website/app.js @@ -0,0 +1,231 @@ +import { baseLayerLuminance, StandardLuminance } from 'https://unpkg.com/@fluentui/web-components'; + +const LISTING_URL = "{{ listingInfo.Url }}"; + +const PACKAGES = { +{{~ for package in packages ~}} + "{{ package.Name }}": { + name: "{{ package.Name }}", + displayName: "{{ if package.DisplayName; package.DisplayName; end; }}", + description: "{{ if package.Description; package.Description; end; }}", + version: "{{ package.Version }}", + author: { + name: "{{ if package.Author.Name; package.Author.Name; end; }}", + url: "{{ if package.Author.Url; package.Author.Url; end; }}", + }, + dependencies: { + {{~ for dependency in package.Dependencies ~}} + "{{ dependency.Name }}": "{{ dependency.Version }}", + {{~ end ~}} + }, + keywords: [ + {{~ for keyword in package.Keywords ~}} + "{{ keyword }}", + {{~ end ~}} + ], + license: "{{ package.License }}", + licensesUrl: "{{ package.LicensesUrl }}", + }, +{{~ end ~}} +}; + +const setTheme = () => { + const isDarkTheme = () => window.matchMedia("(prefers-color-scheme: dark)").matches; + if (isDarkTheme()) { + baseLayerLuminance.setValueFor(document.documentElement, StandardLuminance.DarkMode); + } else { + baseLayerLuminance.setValueFor(document.documentElement, StandardLuminance.LightMode); + } +} + +(() => { + setTheme(); + + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + setTheme(); + }); + + const packageGrid = document.getElementById('packageGrid'); + + const searchInput = document.getElementById('searchInput'); + searchInput.addEventListener('input', ({ target: { value = '' }}) => { + const items = packageGrid.querySelectorAll('fluent-data-grid-row[row-type="default"]'); + items.forEach(item => { + if (value === '') { + item.style.display = 'grid'; + return; + } + if ( + item.dataset?.packageName?.toLowerCase()?.includes(value.toLowerCase()) || + item.dataset?.packageId?.toLowerCase()?.includes(value.toLowerCase()) + ) { + item.style.display = 'grid'; + } else { + item.style.display = 'none'; + } + }); + }); + + const urlBarHelpButton = document.getElementById('urlBarHelp'); + const addListingToVccHelp = document.getElementById('addListingToVccHelp'); + urlBarHelpButton.addEventListener('click', () => { + addListingToVccHelp.hidden = false; + }); + const addListingToVccHelpClose = document.getElementById('addListingToVccHelpClose'); + addListingToVccHelpClose.addEventListener('click', () => { + addListingToVccHelp.hidden = true; + }); + + const vccListingInfoUrlFieldCopy = document.getElementById('vccListingInfoUrlFieldCopy'); + vccListingInfoUrlFieldCopy.addEventListener('click', () => { + const vccUrlField = document.getElementById('vccListingInfoUrlField'); + vccUrlField.select(); + navigator.clipboard.writeText(vccUrlField.value); + vccUrlFieldCopy.appearance = 'accent'; + setTimeout(() => { + vccUrlFieldCopy.appearance = 'neutral'; + }, 1000); + }); + + const vccAddRepoButton = document.getElementById('vccAddRepoButton'); + vccAddRepoButton.addEventListener('click', () => window.location.assign(`vcc://vpm/addRepo?url=${encodeURIComponent(LISTING_URL)}`)); + + const vccUrlFieldCopy = document.getElementById('vccUrlFieldCopy'); + vccUrlFieldCopy.addEventListener('click', () => { + const vccUrlField = document.getElementById('vccUrlField'); + vccUrlField.select(); + navigator.clipboard.writeText(vccUrlField.value); + vccUrlFieldCopy.appearance = 'accent'; + setTimeout(() => { + vccUrlFieldCopy.appearance = 'neutral'; + }, 1000); + }); + + const rowMoreMenu = document.getElementById('rowMoreMenu'); + const hideRowMoreMenu = e => { + if (rowMoreMenu.contains(e.target)) return; + document.removeEventListener('click', hideRowMoreMenu); + rowMoreMenu.hidden = true; + } + + const rowMenuButtons = document.querySelectorAll('.rowMenuButton'); + rowMenuButtons.forEach(button => { + button.addEventListener('click', e => { + if (rowMoreMenu?.hidden) { + rowMoreMenu.style.top = `${e.clientY + e.target.clientHeight}px`; + rowMoreMenu.style.left = `${e.clientX - 120}px`; + rowMoreMenu.hidden = false; + + const downloadLink = rowMoreMenu.querySelector('#rowMoreMenuDownload'); + const downloadListener = () => { + window.open(e?.target?.dataset?.packageUrl, '_blank'); + } + downloadLink.addEventListener('change', () => { + downloadListener(); + downloadLink.removeEventListener('change', downloadListener); + }); + + setTimeout(() => { + document.addEventListener('click', hideRowMoreMenu); + }, 1); + } + }); + }); + + const packageInfoModal = document.getElementById('packageInfoModal'); + const packageInfoModalClose = document.getElementById('packageInfoModalClose'); + packageInfoModalClose.addEventListener('click', () => { + packageInfoModal.hidden = true; + }); + + // Fluent dialogs use nested shadow-rooted elements, so we need to use JS to style them + const modalControl = packageInfoModal.shadowRoot.querySelector('.control'); + modalControl.style.maxHeight = "90%"; + modalControl.style.transition = 'height 0.2s ease-in-out'; + modalControl.style.overflowY = 'hidden'; + + const packageInfoName = document.getElementById('packageInfoName'); + const packageInfoId = document.getElementById('packageInfoId'); + const packageInfoVersion = document.getElementById('packageInfoVersion'); + const packageInfoDescription = document.getElementById('packageInfoDescription'); + const packageInfoAuthor = document.getElementById('packageInfoAuthor'); + const packageInfoDependencies = document.getElementById('packageInfoDependencies'); + const packageInfoKeywords = document.getElementById('packageInfoKeywords'); + const packageInfoLicense = document.getElementById('packageInfoLicense'); + + const rowAddToVccButtons = document.querySelectorAll('.rowAddToVccButton'); + rowAddToVccButtons.forEach((button) => { + button.addEventListener('click', () => window.location.assign(`vcc://vpm/addRepo?url=${encodeURIComponent(LISTING_URL)}`)); + }); + + const rowPackageInfoButton = document.querySelectorAll('.rowPackageInfoButton'); + rowPackageInfoButton.forEach((button) => { + button.addEventListener('click', e => { + const packageId = e.target.dataset?.packageId; + const packageInfo = PACKAGES?.[packageId]; + if (!packageInfo) { + console.error(`Did not find package ${packageId}. Packages available:`, PACKAGES); + return; + } + + packageInfoName.textContent = packageInfo.displayName; + packageInfoId.textContent = packageId; + packageInfoVersion.textContent = `v${packageInfo.version}`; + packageInfoDescription.textContent = packageInfo.description; + packageInfoAuthor.textContent = packageInfo.author.name; + packageInfoAuthor.href = packageInfo.author.url; + + if ((packageInfo.keywords?.length ?? 0) === 0) { + packageInfoKeywords.parentElement.classList.add('hidden'); + } else { + packageInfoKeywords.parentElement.classList.remove('hidden'); + packageInfoKeywords.innerHTML = null; + packageInfo.keywords.forEach(keyword => { + const keywordDiv = document.createElement('div'); + keywordDiv.classList.add('me-2', 'mb-2', 'badge'); + keywordDiv.textContent = keyword; + packageInfoKeywords.appendChild(keywordDiv); + }); + } + + if (!packageInfo.license?.length && !packageInfo.licensesUrl?.length) { + packageInfoLicense.parentElement.classList.add('hidden'); + } else { + packageInfoLicense.parentElement.classList.remove('hidden'); + packageInfoLicense.textContent = packageInfo.license ?? 'See License'; + packageInfoLicense.href = packageInfo.licensesUrl ?? '#'; + } + + packageInfoDependencies.innerHTML = null; + Object.entries(packageInfo.dependencies).forEach(([name, version]) => { + const depRow = document.createElement('li'); + depRow.classList.add('mb-2'); + depRow.textContent = `${name} @ v${version}`; + packageInfoDependencies.appendChild(depRow); + }); + + packageInfoModal.hidden = false; + + setTimeout(() => { + const height = packageInfoModal.querySelector('.col').clientHeight; + modalControl.style.setProperty('--dialog-height', `${height + 14}px`); + }, 1); + }); + }); + + const packageInfoVccUrlFieldCopy = document.getElementById('packageInfoVccUrlFieldCopy'); + packageInfoVccUrlFieldCopy.addEventListener('click', () => { + const vccUrlField = document.getElementById('packageInfoVccUrlField'); + vccUrlField.select(); + navigator.clipboard.writeText(vccUrlField.value); + vccUrlFieldCopy.appearance = 'accent'; + setTimeout(() => { + vccUrlFieldCopy.appearance = 'neutral'; + }, 1000); + }); + + const packageInfoListingHelp = document.getElementById('packageInfoListingHelp'); + packageInfoListingHelp.addEventListener('click', () => { + addListingToVccHelp.hidden = false; + }); +})(); \ No newline at end of file diff --git a/Website/banner.png b/Website/banner.png new file mode 100644 index 0000000..6609efc Binary files /dev/null and b/Website/banner.png differ diff --git a/Website/favicon.ico b/Website/favicon.ico new file mode 100644 index 0000000..a72240a Binary files /dev/null and b/Website/favicon.ico differ diff --git a/Website/index.html b/Website/index.html new file mode 100644 index 0000000..8bb119c --- /dev/null +++ b/Website/index.html @@ -0,0 +1,221 @@ + + + + + + VCC Listing + + + + + +
+
+ {{~ if listingInfo.BannerImage; ~}} +
+ {{~ end; ~}} +

+ {{~ listingInfo.Name ~}} +

+ {{~ if listingInfo.Description; ~}} +
{{ listingInfo.Description }}
+ {{~ end; ~}} +
+ {{~ if listingInfo.Author.Email; ~}} + + {{ listingInfo.Author.Email }} + + {{~ end; ~}} + + {{~ if listingInfo.InfoLink.Url ~}} + + {{~ end; ~}} +
+
+
+ + + Add to VCC + + + + + Copy + + + How to add a listing to your VCC + + + + + + +
+ +
+ +
+ + + + + + + Name + + + Type + + + + + {{~ for package in packages ~}} + + +
+
{{ package.DisplayName }}
+
{{ package.Description }}
+
{{ package.Name }}
+
+
+ + {{ package.Type }} + + + Add to VCC + + + + + + + + + + + +
+ {{~ end ~}} +
+
+ {{~ if listingInfo.InfoLink.Url ~}} + + {{~ end; ~}} +
+ + + diff --git a/Website/styles.css b/Website/styles.css new file mode 100644 index 0000000..ce999b0 --- /dev/null +++ b/Website/styles.css @@ -0,0 +1,356 @@ +:root { + color-scheme: light dark; +} + +* { + box-sizing: border-box; +} + +body { + padding: 0; + margin: 0; + min-width: 100vw; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + color: var(--neutral-foreground-rest); +} + +.hidden { + display: none !important; +} + +.row { + display: flex; + flex-direction: row; +} + +.col { + display: flex; + flex-direction: column; +} + +.content { + max-width: 1000px; + width: 100%; + margin: 0 auto; +} + +.align-items-center { + align-items: center; +} + +.justify-content-between { + justify-content: space-between; +} + +.justify-content-end { + justify-content: flex-end; +} + +h1 { + margin-bottom: 0.5rem; +} + +.caption1 { + font-size: 1rem; + color: var(--neutral-foreground-hover); +} + +.caption2 { + font-size: 0.8rem; + margin-top: 0.25rem; + color: var(--neutral-foreground-rest); +} + +.packages { + margin: 0.5rem 0 1rem 0; + max-width: 90%; + padding: 0.25rem; + display: flex; + flex: 1; +} + +#packageGrid { + overflow-y: auto; + width: 100%; + max-height: 40rem; +} + +.packages .packageName { + font-size: 1.1rem; + font-weight: 600; + margin: 0.25rem 0; +} + +.searchBlock { + margin-top: 1rem; + width: 100%; + max-width: 90%; +} + +.searchBlock .root { + width: 100%; +} + +#searchInput { + width: 100%; +} + +.vccUrlField { + min-width: 450px; + max-width: 90%; + flex-grow:1; +} + +#addListingToVccHelp { + z-index: 11; +} + +#packageInfoModal { + z-index: 10; +} + +#rowMoreMenu { + top: 0; + left: 0; + position: absolute; + z-index: 10; +} + +#rowMoreMenu a { + display: block; + text-decoration: none; + color: var(--neutral-foreground-rest); +} + +.bannerImage { + aspect-ratio: 5 / 1; + border-radius: 6px; + max-width: 90%; + width: 100%; + background-size: cover; + background-position: center; + background-repeat: no-repeat; + margin-bottom: 0.25rem; +} + +.badge { + border-radius: 4px; + padding: 0.25rem 0.5rem; + background-color: var(--neutral-fill-hover); +} + +.m-0 { + margin: 0; +} + +.m-1 { + margin: 0.25rem; +} + +.m-2 { + margin: 0.5rem; +} + +.m-3 { + margin: 0.75rem; +} + +.m-4 { + margin: 1rem; +} + +.m-5 { + margin: 2rem; +} + +.mt-1 { + margin-top: 0.25rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.mt-3 { + margin-top: 0.75rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.mt-5 { + margin-top: 2rem; +} + +.mb-1 { + margin-bottom: 0.25rem; +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.mb-3 { + margin-bottom: 0.75rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.mb-5 { + margin-bottom: 2rem; +} + +.ms-1 { + margin-left: 0.25rem; +} + +.ms-2 { + margin-left: 0.5rem; +} + +.ms-3 { + margin-left: 0.75rem; +} + +.ms-4 { + margin-left: 1rem; +} + +.ms-5 { + margin-left: 2rem; +} + +.me-1 { + margin-right: 0.25rem; +} + +.me-2 { + margin-right: 0.5rem; +} + +.me-3 { + margin-right: 0.75rem; +} + +.me-4 { + margin-right: 1rem; +} + +.me-5 { + margin-right: 2rem; +} + +.p-1 { + padding: 0.25rem; +} + +.p-2 { + padding: 0.5rem; +} + +.p-3 { + padding: 0.75rem; +} + +.p-4 { + padding: 1rem; +} + +.p-5 { + padding: 2rem; +} + +.pt-1 { + padding-top: 0.25rem; +} + +.pt-2 { + padding-top: 0.5rem; +} + +.pt-3 { + padding-top: 0.75rem; +} + +.pt-4 { + padding-top: 1rem; +} + +.pt-5 { + padding-top: 2rem; +} + +.pb-1 { + padding-bottom: 0.25rem; +} + +.pb-2 { + padding-bottom: 0.5rem; +} + +.pb-3 { + padding-bottom: 0.75rem; +} + +.pb-4 { + padding-bottom: 1rem; +} + +.pb-5 { + padding-bottom: 2rem; +} + +.ps-1 { + padding-left: 0.25rem; +} + +.ps-2 { + padding-left: 0.5rem; +} + +.ps-3 { + padding-left: 0.75rem; +} + +.ps-4 { + padding-left: 1rem; +} + +.ps-5 { + padding-left: 2rem; +} + +.pe-1 { + padding-right: 0.25rem; +} + +.pe-2 { + padding-right: 0.5rem; +} + +.pe-3 { + padding-right: 0.75rem; +} + +.pe-4 { + padding-right: 1rem; +} + +.pe-5 { + padding-right: 2rem; +} + +.w-100 { + width: 100%; +} + +.flex-1 { + flex: 1; +} \ No newline at end of file -- cgit v1.2.3