Public repository browser
+NethSecurity
+Browse folders and download release artifacts from the public repository.
+| Name | +Type | +Size | +Modified | +
|---|
diff --git a/README.rst b/README.rst
index 4807f47b..143c63fa 100644
--- a/README.rst
+++ b/README.rst
@@ -132,6 +132,10 @@ Finally, build the doc: ::
make html
+The standalone package browser is published from ``_extra/browser/`` through
+``html_extra_path``. It is plain HTML, CSS, and JavaScript, so local and Read
+the Docs builds do not require a separate frontend toolchain.
+
Localization workflow
---------------------
diff --git a/_extra/browser/browser.css b/_extra/browser/browser.css
new file mode 100644
index 00000000..469d39ba
--- /dev/null
+++ b/_extra/browser/browser.css
@@ -0,0 +1,402 @@
+:root {
+ color-scheme: light;
+ --pst-color-primary: #0a7d91;
+ --pst-color-primary-bg: #d0ecf1;
+ --pst-color-secondary: #8045e5;
+ --pst-color-secondary-bg: #e0c7ff;
+ --pst-color-accent: #c132af;
+ --pst-color-accent-bg: #f8dff5;
+ --pst-color-text-base: #222832;
+ --pst-color-text-muted: #48566b;
+ --pst-color-border: #d1d5da;
+ --pst-color-link-higher-contrast: #085d6c;
+ --pst-color-background: #fff;
+ --pst-color-on-background: #fff;
+ --pst-color-surface: #f3f4f5;
+ --pst-color-heading: var(--pst-color-text-base);
+ --pst-color-link: var(--pst-color-primary);
+ --pst-color-link-hover: var(--pst-color-secondary);
+ --pst-font-family-base: system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ --pst-font-family-heading: var(--pst-font-family-base);
+ --shell-width: min(88rem, calc(100vw - 2rem));
+ --surface-shadow: 0 0.2rem 0.5rem rgba(34, 40, 50, 0.04);
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ color-scheme: dark;
+ --pst-color-primary-bg: #042c33;
+ --pst-color-secondary-bg: #341a61;
+ --pst-color-accent: #e47fd7;
+ --pst-color-accent-bg: #46123f;
+ --pst-color-text-base: #ced6dd;
+ --pst-color-text-muted: #9ca4af;
+ --pst-color-border: #48566b;
+ --pst-color-link-higher-contrast: #3fb1c5;
+ --pst-color-background: #14181e;
+ --pst-color-on-background: #222832;
+ --pst-color-surface: #29313d;
+ --pst-color-heading: var(--pst-color-text-base);
+ --pst-color-link: var(--pst-color-primary);
+ --pst-color-link-hover: var(--pst-color-secondary);
+ }
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ min-height: 100%;
+ background: var(--pst-color-background);
+}
+
+body {
+ margin: 0;
+ min-width: 320px;
+ min-height: 100vh;
+ background: var(--pst-color-background);
+ color: var(--pst-color-text-base);
+ font-family: var(--pst-font-family-base);
+ font-size: 1rem;
+ line-height: 1.6;
+ font-weight: 400;
+}
+
+button,
+a,
+input {
+ font: inherit;
+}
+
+button {
+ border: 0;
+ cursor: pointer;
+}
+
+a {
+ color: var(--pst-color-link);
+ text-decoration: none;
+}
+
+a:hover {
+ color: var(--pst-color-link-hover);
+}
+
+button:focus-visible,
+a:focus-visible,
+input:focus-visible {
+ outline: 2px solid var(--pst-color-primary);
+ outline-offset: 2px;
+}
+
+#root {
+ width: 100%;
+ min-height: 100vh;
+}
+
+.app-shell {
+ width: var(--shell-width);
+ margin: 0 auto;
+ padding: 1.5rem 0 3rem;
+}
+
+.hero {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 1.5rem;
+ padding-bottom: 1.5rem;
+ border-bottom: 1px solid var(--pst-color-border);
+}
+
+.eyebrow {
+ margin: 0 0 0.35rem;
+ font-size: 0.9rem;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: var(--pst-color-primary);
+}
+
+.hero h1 {
+ margin: 0;
+ font-family: var(--pst-font-family-heading);
+ font-size: clamp(2rem, 4vw, 2.625rem);
+ line-height: 1.1;
+ font-weight: 600;
+ color: var(--pst-color-heading);
+}
+
+.hero-copy {
+ margin: 0.75rem 0 0;
+ max-width: 60ch;
+ color: var(--pst-color-text-muted);
+}
+
+.hero-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ justify-content: flex-end;
+ align-items: flex-start;
+}
+
+.button,
+.crumb,
+.entry-button,
+.entry-link {
+ transition:
+ color 150ms ease,
+ background-color 150ms ease,
+ border-color 150ms ease,
+ box-shadow 150ms ease,
+ transform 150ms ease;
+}
+
+.button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.7rem 1rem;
+ border-radius: 999px;
+ background: var(--pst-color-primary);
+ color: #fff;
+ box-shadow: 0 0.125rem 0.25rem rgba(34, 40, 50, 0.08);
+}
+
+.button:hover {
+ background: var(--pst-color-link-higher-contrast);
+ color: #fff;
+ transform: translateY(-1px);
+}
+
+.button-secondary {
+ background: transparent;
+ color: var(--pst-color-primary);
+ border: 1px solid var(--pst-color-border);
+ box-shadow: none;
+}
+
+.button-secondary:hover {
+ background: var(--pst-color-surface);
+ border-color: var(--pst-color-primary);
+ color: var(--pst-color-link-higher-contrast);
+}
+
+.breadcrumbs {
+ display: flex;
+ align-items: center;
+ gap: 0.45rem;
+ flex-wrap: wrap;
+ margin: 1rem 0 1.1rem;
+ color: var(--pst-color-text-muted);
+ font-size: 0.95rem;
+}
+
+.crumb {
+ padding: 0;
+ background: none;
+ color: var(--pst-color-link);
+ font-weight: 600;
+}
+
+.crumb:hover {
+ color: var(--pst-color-link-hover);
+ text-decoration: underline;
+ text-underline-offset: 0.2em;
+}
+
+.crumb-separator {
+ color: var(--pst-color-text-muted);
+}
+
+.panel {
+ background: var(--pst-color-background);
+ border: 1px solid var(--pst-color-border);
+ border-radius: 0.5rem;
+ box-shadow: var(--surface-shadow);
+ overflow: hidden;
+}
+
+.panel-noscript {
+ margin-top: 2rem;
+ padding: 1.5rem;
+}
+
+.panel-noscript h1 {
+ margin-top: 0;
+}
+
+.toolbar {
+ display: flex;
+ justify-content: flex-end;
+ padding: 1rem 1rem 0;
+}
+
+.filter {
+ width: min(100%, 24rem);
+}
+
+.meta-label {
+ display: block;
+ margin-bottom: 0.35rem;
+ font-size: 0.9rem;
+ font-weight: 600;
+ color: var(--pst-color-text-muted);
+}
+
+.filter-input {
+ width: 100%;
+ padding: 0.75rem 0.9rem;
+ border: 1px solid var(--pst-color-border);
+ border-radius: 0.5rem;
+ background: var(--pst-color-surface);
+ color: var(--pst-color-text-base);
+ outline: none;
+}
+
+.filter-input::placeholder {
+ color: var(--pst-color-text-muted);
+}
+
+.filter-input:focus {
+ border-color: var(--pst-color-primary);
+ background: var(--pst-color-background);
+ box-shadow: 0 0 0 0.2rem rgba(10, 125, 145, 0.15);
+}
+
+.status {
+ min-height: 1.5rem;
+ padding: 0.85rem 1rem 0;
+ color: var(--pst-color-text-muted);
+ font-size: 0.95rem;
+}
+
+.table-wrapper {
+ margin-top: 0.9rem;
+ border-top: 1px solid var(--pst-color-border);
+ overflow-x: auto;
+}
+
+.entries-table {
+ width: 100%;
+ border-collapse: separate;
+ border-spacing: 0;
+ font-size: 0.96rem;
+}
+
+.entries-table thead th {
+ padding: 0.9rem 1rem;
+ text-align: left;
+ background: var(--pst-color-surface);
+ color: var(--pst-color-text-muted);
+ font-weight: 600;
+ border-bottom: 1px solid var(--pst-color-border);
+}
+
+.entries-table tbody tr {
+ background: var(--pst-color-background);
+}
+
+.entries-table tbody tr:nth-child(even) {
+ background: var(--pst-color-surface);
+}
+
+.entries-table tbody tr:hover {
+ background: rgba(10, 125, 145, 0.05);
+}
+
+.entries-table td {
+ padding: 0.95rem 1rem;
+ border-bottom: 1px solid var(--pst-color-border);
+ vertical-align: middle;
+}
+
+.entries-table tbody tr:last-child td {
+ border-bottom: 0;
+}
+
+.entry-row td:first-child {
+ max-width: 50%;
+ word-break: break-word;
+}
+
+.entry-button {
+ padding: 0;
+ background: transparent;
+ border: 0;
+ color: var(--pst-color-link);
+ cursor: pointer;
+ text-align: left;
+ font-weight: 600;
+ text-decoration: none;
+}
+
+.entry-button:hover {
+ color: var(--pst-color-link-hover);
+ text-decoration: underline;
+ text-underline-offset: 0.2em;
+}
+
+.entry-link {
+ color: var(--pst-color-link);
+ text-decoration: none;
+ cursor: pointer;
+ font-weight: 500;
+}
+
+.entry-link:hover {
+ color: var(--pst-color-link-hover);
+ text-decoration: underline;
+ text-underline-offset: 0.2em;
+}
+
+.entry-parent {
+ background: var(--pst-color-surface);
+}
+
+.entry-parent .entry-button {
+ color: var(--pst-color-text-muted);
+}
+
+.entry-parent .entry-button:hover {
+ color: var(--pst-color-primary);
+}
+
+@media (max-width: 720px) {
+ .app-shell {
+ width: min(100vw - 1rem, 88rem);
+ padding-top: 1rem;
+ }
+
+ .hero {
+ flex-direction: column;
+ }
+
+ .hero-actions {
+ width: 100%;
+ justify-content: flex-start;
+ }
+
+ .toolbar {
+ justify-content: stretch;
+ }
+
+ .filter {
+ width: 100%;
+ }
+
+ .entries-table {
+ font-size: 0.9rem;
+ }
+
+ .entries-table th,
+ .entries-table td {
+ padding: 0.75rem 0.85rem;
+ }
+
+ .entry-row td:first-child {
+ max-width: 100%;
+ }
+}
diff --git a/_extra/browser/browser.js b/_extra/browser/browser.js
new file mode 100644
index 00000000..bcb693e9
--- /dev/null
+++ b/_extra/browser/browser.js
@@ -0,0 +1,367 @@
+const root = document.querySelector('#root');
+
+if (!root) {
+ throw new Error('Missing browser root element.');
+}
+
+const config = {
+ bucket: document.body.dataset.bucket || 'nethsecurity',
+ endpoint: (document.body.dataset.endpoint || 'https://ams3.digitaloceanspaces.com').replace(/\/$/, ''),
+ cdnEndpoint: (document.body.dataset.cdnEndpoint || 'https://updates.nethsecurity.nethserver.org').replace(/\/$/, ''),
+ docsHome: document.body.dataset.docsHome || '../index.html',
+ downloadPage: document.body.dataset.downloadPage || '../download.html',
+};
+
+const state = {
+ bucket: config.bucket,
+ endpoint: config.endpoint,
+ cdnEndpoint: config.cdnEndpoint,
+ prefix: '',
+ filter: '',
+ currentFolders: [],
+ currentFiles: [],
+};
+
+root.innerHTML = `
+ Public repository browser Browse folders and download release artifacts from the public repository.NethSecurity
+
+
+
+
+
+
+
+ Name
+ Type
+ Size
+ Modified
+