@gjsify/site 0.64.1-next.247 → 0.64.1-next.249

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -104,7 +104,8 @@ Lives in the root `package.json`, next to `ship` and `flatpak`.
104
104
  | `translations` | Directory of the app's `<lang>.po` catalogues, used when the metainfo lacks a translation. Optional. |
105
105
  | `pageTranslations` | Directory of `<lang>.po` catalogues for the *page's own* strings, overlaying this package's `po/`. Optional, see below. |
106
106
  | `icon` | Icon (SVG or PNG) for the header, hero and favicon; overrides the Flatpak/ship icon. Optional. |
107
- | `downloads` | `[{platform, format, url?, label?, available?}]`: targets the release assets do not cover, e.g. browser stores or a source install. `platform` is an OS or `firefox` / `chromium`; `format` adds `store`, `xpi`, `source`. `url` is required unless `available` is `false`, which lists the target as "Not yet available" and never recommends it. |
107
+ | `components` | `[{id, name, summary?, match?}]`: the separately installed parts of the product, in install order, see below. Optional — without it the dialog is one undivided list. |
108
+ | `downloads` | `[{platform, format, url?, label?, available?, component?}]`: targets the release assets do not cover, e.g. browser stores or a source install. `platform` is an OS or `firefox` / `chromium`; `format` adds `store`, `xpi`, `source`. `url` is required unless `available` is `false`, which lists the target as "Not yet available" and never recommends it. `component` is a `components[].id`. |
108
109
  | `releaseAssets` | `false` ignores the GitHub release assets, so `downloads` is the whole list. Default `true`. |
109
110
  | `support`, `legal` | `support`: `{kind, url}` links, or `{kind: "sepa", name, iban, bic?, reference?}`, a bank transfer shown as IBAN plus an EPC QR code. `legal`: `{label, url}` links. Both empty by default: nothing is invented. |
110
111
 
@@ -112,6 +113,33 @@ A `sepa` entry carries no amount, and its IBAN is checked when the config loads
112
113
  country, alphabet, and the mod-97 checksum — so a typo fails the build instead of printing a QR
113
114
  code that moves money nowhere.
114
115
 
116
+ ### Components
117
+
118
+ Some products are not one download. A desktop app plus the browser extension that talks to it are
119
+ two things a person installs one after the other, and a single flat list of files does not say
120
+ which goes first. `components` names those parts:
121
+
122
+ ```json
123
+ "components": [
124
+ { "id": "app", "name": "App", "summary": "Install and start it once.", "match": "^beifahrer[-_]" },
125
+ { "id": "extension", "name": "Browser extension", "summary": "Then add it to your browser." }
126
+ ]
127
+ ```
128
+
129
+ Each part becomes its own block in the download dialog, in declared order, with its own operating
130
+ system tabs — numbered "Step 1", "Step 2", … as soon as there is more than one. The tab strip is
131
+ labelled `Browser` when every target in that block is a browser, otherwise `Operating system`.
132
+
133
+ `match` is a regular expression on the asset's file name, matched case-insensitively; the first
134
+ component whose `match` hits owns the asset. A component without `match` is the catch-all and
135
+ takes everything left over, which is what a release without a naming scheme for every file needs.
136
+ Entries in `downloads` pick their block by `component` instead, naming a `components[].id` — an
137
+ unknown id fails the build with the known ids listed.
138
+
139
+ The recommendation ("Recommended for your system") is made once per part, so each block
140
+ highlights its own best match, and the big button at the top of the dialog offers the first
141
+ declared component: the thing to install first.
142
+
115
143
  ### Screenshots
116
144
 
117
145
  Nothing to configure: the page shows the metainfo `<screenshots>`, in document order, with their
@@ -953,6 +953,45 @@ h1 {
953
953
  color: var(--fg);
954
954
  }
955
955
 
956
+ /* One block per part of the product, in the order the parts have to be installed in. */
957
+ .component + .component {
958
+ margin-top: 1.25rem;
959
+ border-top: 1px solid var(--line);
960
+ }
961
+
962
+ .component-head {
963
+ padding: 1rem 1.25rem 0;
964
+ }
965
+
966
+ .component-title {
967
+ display: flex;
968
+ align-items: center;
969
+ gap: 0.5rem;
970
+ margin: 0;
971
+ font-size: 1rem;
972
+ }
973
+
974
+ .component-step {
975
+ display: inline-flex;
976
+ flex: none;
977
+ align-items: center;
978
+ justify-content: center;
979
+ width: 1.5rem;
980
+ height: 1.5rem;
981
+ border-radius: 999px;
982
+ background: var(--accent);
983
+ color: var(--accent-fg);
984
+ font-size: 0.8rem;
985
+ font-variant-numeric: tabular-nums;
986
+ font-weight: 600;
987
+ }
988
+
989
+ .component-summary {
990
+ margin: 0.4rem 0 0;
991
+ color: var(--muted);
992
+ font-size: 0.85rem;
993
+ }
994
+
956
995
  .os-tabs {
957
996
  display: flex;
958
997
  gap: 0.35rem;
@@ -989,7 +1028,7 @@ h1 {
989
1028
  padding: 0 1.25rem;
990
1029
  }
991
1030
 
992
- .os-group h3 {
1031
+ .os-title {
993
1032
  display: flex;
994
1033
  align-items: center;
995
1034
  gap: 0.5rem;
@@ -998,7 +1037,7 @@ h1 {
998
1037
  }
999
1038
 
1000
1039
  /* With tabs visible the group heading only repeats the selected tab. */
1001
- .os-tabs:not([hidden]) ~ .os-groups h3 {
1040
+ .os-tabs:not([hidden]) ~ .os-groups .os-title {
1002
1041
  display: none;
1003
1042
  }
1004
1043
 
@@ -1 +1 @@
1
- function enhanceDialog(e,t){let n=e.querySelector(`.dialog-close`);n&&(n.hidden=!1,n.addEventListener(`click`,()=>e.close())),t?.addEventListener(`click`,t=>{t.preventDefault(),e.showModal()}),e.addEventListener(`click`,t=>{t.target===e&&e.close()})}const e={linux:`x86_64`,darwin:`arm64`,win32:`x86_64`,android:`arm64`,web:`noarch`};function detectOs(e){let t=`${e.platform??``} ${e.userAgent??``}`;if(/android/i.test(t))return`android`;if(/iphone|ipad|ipod|\bios\b/i.test(t))return`web`;if(/mac|darwin/i.test(t))return`darwin`;if(/win/i.test(t))return`win32`;if(/linux|x11|bsd|cros|chrome ?os/i.test(t))return`linux`}function detectArch(e){if(e.architecture)return e.bitness===`32`?void 0:/arm/i.test(e.architecture)?`arm64`:/x86|amd/i.test(e.architecture)?`x86_64`:void 0;let t=e.userAgent??``;if(/arm64|aarch64/i.test(t))return`arm64`;if(/x86_64|x86-64|amd64|win64|wow64|\bx64\b/i.test(t))return`x86_64`}function detectBrowser(e){let t=e.userAgent??``;if(/firefox|fxios/i.test(t))return`firefox`;if(e.brands?.some(e=>/chromium|chrome|edge|opera|brave/i.test(e))||/chrome|chromium|crios|edg(a|ios)?\/|opr\//i.test(t))return`chromium`}function detectVisitorPlatform(t){let n=detectOs(t),r=detectArch(t);return{os:n,browser:detectBrowser(t),arch:r??(n?e[n]:void 0),archAssumed:r===void 0}}const t=[`firefox`,`chromium`];function isBrowserList(e){return e.length>0&&e.every(e=>t.includes(e.os))}function inGroup(e,n){return t.includes(n)?{...e,browser:n}:{...e,os:n}}function recommendTarget(e,t){let n=isBrowserList(e)?t.browser:t.os;if(!n)return;let r=e.filter(e=>e.os===n&&e.available!==!1),fits=e=>e.arch===`noarch`||e.arch===t.arch;return r.find(e=>e.recommended&&fits(e))??r.find(fits)??r.find(e=>e.recommended)??r[0]}async function readHints(){let e=navigator.userAgentData,t={platform:e?.platform,userAgent:navigator.userAgent,brands:e?.brands?.map(e=>e.brand)};if(e?.getHighEntropyValues)try{let n=await e.getHighEntropyValues([`architecture`,`bitness`]);t.architecture=n.architecture,t.bitness=n.bitness}catch{}return t}function readRows(e){return[...e.querySelectorAll(`li.target`)].flatMap(e=>{let t=e.querySelector(`a`),n=e.dataset.os,r=e.dataset.arch;return!t||!n||!r?[]:[{os:n,arch:r,recommended:e.dataset.recommended!==void 0,url:t.href,cta:e.dataset.cta??t.textContent?.trim()??``,size:e.dataset.size??``}]})}var AppDownload=class extends HTMLElement{connectedCallback(){let e=this.querySelector(`dialog`),t=this.querySelector(`a.download-cta`);if(!e||!t)return;this.dataset.enhanced=`true`,enhanceDialog(e,this.querySelector(`.download-toggle`));let n=readRows(this),r=[...this.querySelectorAll(`.os-group`)],i=[...this.querySelectorAll(`.os-tab`)],a=this.querySelector(`.os-tabs`),o=i.length>1;a&&o&&(a.hidden=!1);let s={os:void 0,arch:void 0,browser:void 0,archAssumed:!0},show=e=>{for(let t of r)t.hidden=o&&t.dataset.os!==e;for(let t of i){let n=t.dataset.os===e;t.setAttribute(`aria-selected`,String(n)),t.tabIndex=n?0:-1}},offer=e=>{t.href=e.url,t.dataset.os=e.os,t.dataset.arch=e.arch;let n=t.querySelector(`.download-label`);n&&(n.textContent=e.cta);let r=document.querySelector(`.badge-size`),i=r?.querySelector(`.badge-value`);if(r&&i){i.textContent=e.size,r.hidden=e.size===``;let t=r.closest(`.badges`);if(t){let e=[...t.children].filter(e=>!e.hidden).length;t.style.setProperty(`--badge-count`,String(e)),t.toggleAttribute(`data-odd`,e%2==1)}}},select=e=>{show(e);let t=recommendTarget(n,inGroup(s,e));t&&offer(t)},c=i.find(e=>e.getAttribute(`aria-selected`)===`true`)?.dataset.os;c&&show(c);for(let e of i)e.addEventListener(`click`,()=>{let t=e.dataset.os;t&&select(t)});a?.addEventListener(`keydown`,e=>{let t=i.indexOf(document.activeElement);if(t===-1)return;let n={ArrowRight:t+1,ArrowLeft:t-1,Home:0,End:i.length-1}[e.key];if(n===void 0)return;e.preventDefault();let r=i[(n+i.length)%i.length];r?.focus(),r?.click()}),readHints().then(e=>{s=detectVisitorPlatform(e);let t=recommendTarget(n,s);t&&(show(t.os),offer(t))})}};customElements.define(`app-download`,AppDownload);function enhanceCopy(e){let t=e.querySelector(`span`),n=t?.textContent??``;navigator.clipboard&&(e.hidden=!1,e.addEventListener(`click`,async()=>{try{await navigator.clipboard.writeText(e.dataset.copy??``)}catch{return}t&&(t.textContent=e.dataset.done??n),setTimeout(()=>{t&&(t.textContent=n)},2e3)}))}var AppSupport=class extends HTMLElement{connectedCallback(){this.dataset.enhanced=`true`;let e=this.querySelector(`dialog`);e&&enhanceDialog(e,this.querySelector(`.support-cta`));for(let e of this.querySelectorAll(`.sepa-copy`))enhanceCopy(e)}};customElements.define(`app-support`,AppSupport);var ReleaseNotes=class extends HTMLElement{connectedCallback(){let e=this.querySelector(`.release-body`),t=this.querySelector(`.release-more`);if(!e||!t)return;this.dataset.enhanced=`true`;let n=this.dataset.more??t.textContent??``,r=this.dataset.less??n;if(e.classList.add(`clamped`),e.scrollHeight<=e.clientHeight){e.classList.remove(`clamped`);return}t.hidden=!1,t.textContent=n,t.setAttribute(`aria-expanded`,`false`),t.addEventListener(`click`,()=>{let i=e.classList.toggle(`clamped`);t.textContent=i?n:r,t.setAttribute(`aria-expanded`,String(!i))})}};customElements.define(`release-notes`,ReleaseNotes);var ScreenshotCarousel=class extends HTMLElement{connectedCallback(){let e=this.querySelector(`.carousel-track`),t=[...this.querySelectorAll(`.slide`)];if(!e||t.length===0)return;this.dataset.enhanced=`true`,e.tabIndex=0;let n=matchMedia(`(prefers-reduced-motion: reduce)`),r=[...this.querySelectorAll(`.carousel-dot`)],i=this.querySelector(`.carousel-prev`),a=this.querySelector(`.carousel-next`),o=[this.querySelector(`.carousel-dots`),i,a];if(t.length>1)for(let e of o)e&&(e.hidden=!1);let s=0,clamp=e=>Math.max(0,Math.min(e,t.length-1)),goTo=r=>{let i=t[clamp(r)];if(!i)return;let a=i.offsetLeft-e.offsetLeft-(e.clientWidth-i.clientWidth)/2;e.scrollTo({left:a,behavior:n.matches?`auto`:`smooth`})},mark=e=>{s=e;for(let[t,n]of r.entries())n.setAttribute(`aria-current`,String(t===e));i&&(i.disabled=e===0),a&&(a.disabled=e===t.length-1)};mark(0),i?.addEventListener(`click`,()=>goTo(s-1)),a?.addEventListener(`click`,()=>goTo(s+1));for(let[e,t]of r.entries())t.addEventListener(`click`,()=>goTo(e));let arrowStep=e=>e===`ArrowRight`?1:e===`ArrowLeft`?-1:0;e.addEventListener(`keydown`,e=>{let t=arrowStep(e.key);t!==0&&(e.preventDefault(),goTo(s+t))});let c=new IntersectionObserver(e=>{for(let n of e)n.isIntersecting&&mark(t.indexOf(n.target))},{root:e,threshold:.6});for(let e of t)c.observe(e);let l=this.querySelector(`.lightbox`),u=l?.querySelector(`figure`);if(!l||!u)return;let open=e=>{let n=t[clamp(e)]?.querySelector(`figure`);if(!n)return;s=clamp(e);let r=[...n.children].filter(e=>!e.classList.contains(`slide-expand`));u.replaceChildren(...r.map(e=>e.cloneNode(!0))),l.open||l.showModal()};for(let[e,n]of t.entries()){let t=n.querySelector(`.slide-expand`);t&&(t.hidden=!1,t.addEventListener(`click`,()=>open(e)))}enhanceDialog(l,null),l.addEventListener(`keydown`,e=>{let t=arrowStep(e.key);t!==0&&(e.preventDefault(),open(s+t),goTo(s))})}};customElements.define(`screenshot-carousel`,ScreenshotCarousel);
1
+ function enhanceDialog(e,t){let n=e.querySelector(`.dialog-close`);n&&(n.hidden=!1,n.addEventListener(`click`,()=>e.close())),t?.addEventListener(`click`,t=>{t.preventDefault(),e.showModal()}),e.addEventListener(`click`,t=>{t.target===e&&e.close()})}const e={linux:`x86_64`,darwin:`arm64`,win32:`x86_64`,android:`arm64`,web:`noarch`};function detectOs(e){let t=`${e.platform??``} ${e.userAgent??``}`;if(/android/i.test(t))return`android`;if(/iphone|ipad|ipod|\bios\b/i.test(t))return`web`;if(/mac|darwin/i.test(t))return`darwin`;if(/win/i.test(t))return`win32`;if(/linux|x11|bsd|cros|chrome ?os/i.test(t))return`linux`}function detectArch(e){if(e.architecture)return e.bitness===`32`?void 0:/arm/i.test(e.architecture)?`arm64`:/x86|amd/i.test(e.architecture)?`x86_64`:void 0;let t=e.userAgent??``;if(/arm64|aarch64/i.test(t))return`arm64`;if(/x86_64|x86-64|amd64|win64|wow64|\bx64\b/i.test(t))return`x86_64`}function detectBrowser(e){let t=e.userAgent??``;if(/firefox|fxios/i.test(t))return`firefox`;if(e.brands?.some(e=>/chromium|chrome|edge|opera|brave/i.test(e))||/chrome|chromium|crios|edg(a|ios)?\/|opr\//i.test(t))return`chromium`}function detectVisitorPlatform(t){let n=detectOs(t),r=detectArch(t);return{os:n,browser:detectBrowser(t),arch:r??(n?e[n]:void 0),archAssumed:r===void 0}}const t=[`firefox`,`chromium`];function isBrowserList(e){return e.length>0&&e.every(e=>t.includes(e.os))}function inGroup(e,n){return t.includes(n)?{...e,browser:n}:{...e,os:n}}function recommendTarget(e,t){let n=isBrowserList(e)?t.browser:t.os;if(!n)return;let r=e.filter(e=>e.os===n&&e.available!==!1),fits=e=>e.arch===`noarch`||e.arch===t.arch;return r.find(e=>e.recommended&&fits(e))??r.find(fits)??r.find(e=>e.recommended)??r[0]}async function readHints(){let e=navigator.userAgentData,t={platform:e?.platform,userAgent:navigator.userAgent,brands:e?.brands?.map(e=>e.brand)};if(e?.getHighEntropyValues)try{let n=await e.getHighEntropyValues([`architecture`,`bitness`]);t.architecture=n.architecture,t.bitness=n.bitness}catch{}return t}function readRows(e){return[...e.querySelectorAll(`li.target`)].flatMap(e=>{let t=e.querySelector(`a`),n=e.dataset.os,r=e.dataset.arch;return!t||!n||!r?[]:[{os:n,arch:r,recommended:e.dataset.recommended!==void 0,url:t.href,cta:e.dataset.cta??t.textContent?.trim()??``,size:e.dataset.size??``}]})}function showSize(e){let t=document.querySelector(`.badge-size`),n=t?.querySelector(`.badge-value`);if(!t||!n)return;n.textContent=e,t.hidden=e===``;let r=t.closest(`.badges`);if(!r)return;let i=[...r.children].filter(e=>!e.hidden).length;r.style.setProperty(`--badge-count`,String(i)),r.toggleAttribute(`data-odd`,i%2==1)}var ComponentSection=class{onSelect;rows;groups;tabs;tabBar;grouped;constructor(e,t){this.onSelect=t,this.rows=readRows(e),this.groups=[...e.querySelectorAll(`.os-group`)],this.tabs=[...e.querySelectorAll(`.os-tab`)],this.tabBar=e.querySelector(`.os-tabs`),this.grouped=this.tabs.length>1,this.tabBar&&this.grouped&&(this.tabBar.hidden=!1);let n=this.tabs.find(e=>e.getAttribute(`aria-selected`)===`true`)?.dataset.os;n&&this.show(n);for(let e of this.tabs)e.addEventListener(`click`,()=>{let t=e.dataset.os;t&&this.select(t,this.platform)});this.tabBar?.addEventListener(`keydown`,e=>{let t=this.tabs.indexOf(document.activeElement);if(t===-1)return;let n=this.tabs.length-1,r={ArrowRight:t+1,ArrowLeft:t-1,Home:0,End:n}[e.key];if(r===void 0)return;e.preventDefault();let i=this.tabs[(r+this.tabs.length)%this.tabs.length];i?.focus(),i?.click()})}platform={os:void 0,arch:void 0,browser:void 0,archAssumed:!0};show(e){for(let t of this.groups)t.hidden=this.grouped&&t.dataset.os!==e;for(let t of this.tabs){let n=t.dataset.os===e;t.setAttribute(`aria-selected`,String(n)),t.tabIndex=n?0:-1}}select(e,t){this.show(e);let n=recommendTarget(this.rows,inGroup(t,e));n&&this.onSelect(n)}detected(e){this.platform=e;let t=recommendTarget(this.rows,e);t&&(this.show(t.os),this.onSelect(t))}},AppDownload=class extends HTMLElement{connectedCallback(){let e=this.querySelector(`dialog`),t=this.querySelector(`a.download-cta`);if(!e||!t)return;this.dataset.enhanced=`true`,enhanceDialog(e,this.querySelector(`.download-toggle`));let offer=e=>{t.href=e.url,t.dataset.os=e.os,t.dataset.arch=e.arch;let n=t.querySelector(`.download-label`);n&&(n.textContent=e.cta),showSize(e.size)},n=[...this.querySelectorAll(`section.component`)],r=(n.length>0?n:[this]).map((e,t)=>new ComponentSection(e,t===0?offer:()=>{}));readHints().then(e=>{let t=detectVisitorPlatform(e);for(let e of r)e.detected(t)})}};customElements.define(`app-download`,AppDownload);function enhanceCopy(e){let t=e.querySelector(`span`),n=t?.textContent??``;navigator.clipboard&&(e.hidden=!1,e.addEventListener(`click`,async()=>{try{await navigator.clipboard.writeText(e.dataset.copy??``)}catch{return}t&&(t.textContent=e.dataset.done??n),setTimeout(()=>{t&&(t.textContent=n)},2e3)}))}var AppSupport=class extends HTMLElement{connectedCallback(){this.dataset.enhanced=`true`;let e=this.querySelector(`dialog`);e&&enhanceDialog(e,this.querySelector(`.support-cta`));for(let e of this.querySelectorAll(`.sepa-copy`))enhanceCopy(e)}};customElements.define(`app-support`,AppSupport);var ReleaseNotes=class extends HTMLElement{connectedCallback(){let e=this.querySelector(`.release-body`),t=this.querySelector(`.release-more`);if(!e||!t)return;this.dataset.enhanced=`true`;let n=this.dataset.more??t.textContent??``,r=this.dataset.less??n;if(e.classList.add(`clamped`),e.scrollHeight<=e.clientHeight){e.classList.remove(`clamped`);return}t.hidden=!1,t.textContent=n,t.setAttribute(`aria-expanded`,`false`),t.addEventListener(`click`,()=>{let i=e.classList.toggle(`clamped`);t.textContent=i?n:r,t.setAttribute(`aria-expanded`,String(!i))})}};customElements.define(`release-notes`,ReleaseNotes);var ScreenshotCarousel=class extends HTMLElement{connectedCallback(){let e=this.querySelector(`.carousel-track`),t=[...this.querySelectorAll(`.slide`)];if(!e||t.length===0)return;this.dataset.enhanced=`true`,e.tabIndex=0;let n=matchMedia(`(prefers-reduced-motion: reduce)`),r=[...this.querySelectorAll(`.carousel-dot`)],i=this.querySelector(`.carousel-prev`),a=this.querySelector(`.carousel-next`),o=[this.querySelector(`.carousel-dots`),i,a];if(t.length>1)for(let e of o)e&&(e.hidden=!1);let s=0,clamp=e=>Math.max(0,Math.min(e,t.length-1)),goTo=r=>{let i=t[clamp(r)];if(!i)return;let a=i.offsetLeft-e.offsetLeft-(e.clientWidth-i.clientWidth)/2;e.scrollTo({left:a,behavior:n.matches?`auto`:`smooth`})},mark=e=>{s=e;for(let[t,n]of r.entries())n.setAttribute(`aria-current`,String(t===e));i&&(i.disabled=e===0),a&&(a.disabled=e===t.length-1)};mark(0),i?.addEventListener(`click`,()=>goTo(s-1)),a?.addEventListener(`click`,()=>goTo(s+1));for(let[e,t]of r.entries())t.addEventListener(`click`,()=>goTo(e));let arrowStep=e=>e===`ArrowRight`?1:e===`ArrowLeft`?-1:0;e.addEventListener(`keydown`,e=>{let t=arrowStep(e.key);t!==0&&(e.preventDefault(),goTo(s+t))});let c=new IntersectionObserver(e=>{for(let n of e)n.isIntersecting&&mark(t.indexOf(n.target))},{root:e,threshold:.6});for(let e of t)c.observe(e);let l=this.querySelector(`.lightbox`),u=l?.querySelector(`figure`);if(!l||!u)return;let open=e=>{let n=t[clamp(e)]?.querySelector(`figure`);if(!n)return;s=clamp(e);let r=[...n.children].filter(e=>!e.classList.contains(`slide-expand`));u.replaceChildren(...r.map(e=>e.cloneNode(!0))),l.open||l.showModal()};for(let[e,n]of t.entries()){let t=n.querySelector(`.slide-expand`);t&&(t.hidden=!1,t.addEventListener(`click`,()=>open(e)))}enhanceDialog(l,null),l.addEventListener(`keydown`,e=>{let t=arrowStep(e.key);t!==0&&(e.preventDefault(),open(s+t),goTo(s))})}};customElements.define(`screenshot-carousel`,ScreenshotCarousel);
@@ -47,6 +47,11 @@ export function configMsgids(config) {
47
47
  for (const download of config.downloads)
48
48
  if (download.label)
49
49
  msgids.add(download.label);
50
+ for (const component of config.components) {
51
+ msgids.add(component.name);
52
+ if (component.summary)
53
+ msgids.add(component.summary);
54
+ }
50
55
  return [...msgids];
51
56
  }
52
57
  /** Every msgid in a `.po`/`.pot` file, translated or not, header excluded. */
@@ -20,6 +20,8 @@ export interface RenderContext {
20
20
  ogImage?: string;
21
21
  }
22
22
  type Translate = (msgid: string) => string;
23
+ /** What the download dialog reads: the targets, the catalogue and the locale it formats sizes in. */
24
+ export type DownloadContext = Pick<RenderContext, 'data' | 't' | 'lang'>;
23
25
  export declare function localized(text: Localized, lang: string): string;
24
26
  export declare function pageUrl(data: SiteData, lang: string): string;
25
27
  export declare function absolute(data: SiteData, rootRelative: string): string;
@@ -34,6 +36,15 @@ export declare function languageName(code: string): string;
34
36
  export declare function ctaLabel(target: DownloadTarget, t: Translate): string;
35
37
  /** A window shot brings its own shadow; ours would draw a second one around it. */
36
38
  export declare function frameAttribute(ctx: Pick<RenderContext, 'images'>, screenshot: Screenshot): 'none' | undefined;
39
+ /**
40
+ * The split button plus the dialog holding every target. Both are server-rendered: without
41
+ * JavaScript the dialog stays an open panel below the button (see `site.css`), with it the
42
+ * component turns the same markup into a modal and swaps the button for the detected platform.
43
+ *
44
+ * The button offers the part that is installed first; the dialog lists every part in that order,
45
+ * so a product made of an app and the extension it pairs with reads as the two steps it is.
46
+ */
47
+ export declare function downloadWidget(ctx: DownloadContext): string;
37
48
  /**
38
49
  * The facts row under the hero. A badge whose fact is missing is left out, not faked, and the row
39
50
  * tells CSS how many it ended up with so the columns divide the width evenly.
@@ -176,6 +176,33 @@ function defaultTarget(targets) {
176
176
  available.find((target) => target.recommended) ??
177
177
  available[0]);
178
178
  }
179
+ /**
180
+ * The targets of the part a person installs FIRST, which is the one the primary button offers. A
181
+ * product declaring no components has exactly one part, so the whole list is that part.
182
+ */
183
+ function primaryTargets(data) {
184
+ const first = data.config.components[0];
185
+ if (!first)
186
+ return data.downloads.targets;
187
+ const own = data.downloads.targets.filter((target) => target.component === first.id);
188
+ return own.length > 0 ? own : data.downloads.targets;
189
+ }
190
+ /**
191
+ * The dialog's blocks, in install order. A component nobody built a file for is left out rather
192
+ * than shown empty, so a release that is missing an artefact says so by absence.
193
+ */
194
+ function componentBlocks(data) {
195
+ const targets = data.downloads.targets;
196
+ const declared = data.config.components;
197
+ if (declared.length === 0)
198
+ return [{ id: '', targets }];
199
+ const blocks = declared.flatMap((part) => {
200
+ const own = targets.filter((target) => target.component === part.id);
201
+ return own.length > 0 ? [{ id: part.id, name: part.name, summary: part.summary, targets: own }] : [];
202
+ });
203
+ const rest = targets.filter((target) => !declared.some((part) => part.id === target.component));
204
+ return rest.length > 0 ? [...blocks, { id: '', targets: rest }] : blocks;
205
+ }
179
206
  /**
180
207
  * One row of the download dialog. The data attributes carry everything the client needs to put a
181
208
  * row on the primary button, so it never has to build a translated string itself.
@@ -205,15 +232,66 @@ ${size ? `<span class="target-size">${esc(size)}</span>` : ''}`;
205
232
  ${target.available ? `<a href="${esc(target.url)}">\n${inner}\n</a>` : `<span class="target-link">\n${inner}\n</span>`}
206
233
  </li>`;
207
234
  }
235
+ /** A component id as an HTML id fragment; a configuration's own ids are not guaranteed to be safe. */
236
+ function blockSlug(block, index) {
237
+ const slug = block.id
238
+ .toLowerCase()
239
+ .replace(/[^a-z0-9]+/g, '-')
240
+ .replace(/^-|-$/g, '');
241
+ return slug || `part-${index + 1}`;
242
+ }
243
+ /**
244
+ * One part of the product in the dialog: its heading, then one tab per platform it is built for.
245
+ * The tabs live inside the block, so the extension's tabs are browsers while the app's are
246
+ * operating systems — the two parts are picked by different facts about the visitor.
247
+ *
248
+ * `step` numbers the part when there is more than one, because the order they are listed in is the
249
+ * order they have to be installed in.
250
+ */
251
+ function componentSection(block, slug, step, ctx) {
252
+ const { t } = ctx;
253
+ const selected = defaultTarget(block.targets)?.os;
254
+ const groups = PLATFORM_ORDER.flatMap((os) => {
255
+ const own = block.targets.filter((target) => target.os === os);
256
+ return own.length > 0 ? [[os, own]] : [];
257
+ });
258
+ // A nested block already has a heading of its own, so its platforms are one level deeper.
259
+ const level = block.name ? 'h4' : 'h3';
260
+ const tabs = groups.map(([os]) => `<button class="os-tab" type="button" role="tab" id="os-tab-${esc(slug)}-${esc(os)}" aria-controls="os-group-${esc(slug)}-${esc(os)}" data-os="${esc(os)}" aria-selected="${os === selected}">${icon(PLATFORM_ICONS[os])}<span>${esc(osName(os, t))}</span></button>`);
261
+ const sections = groups.map(([os, own,]) => `<section class="os-group" id="os-group-${esc(slug)}-${esc(os)}" role="tabpanel" aria-labelledby="os-tab-${esc(slug)}-${esc(os)}" data-os="${esc(os)}">
262
+ <${level} class="os-title">${icon(PLATFORM_ICONS[os])}<span>${esc(osName(os, t))}</span></${level}>
263
+ <ul>
264
+ ${own.map((target) => targetItem(target, ctx)).join('\n')}
265
+ </ul>
266
+ ${os === 'darwin' ? `<p class="os-note">${esc(t('Apple Silicon is assumed. Pick the Intel build if your Mac has an Intel processor.'))}</p>` : ''}
267
+ </section>`);
268
+ const head = block.name
269
+ ? `<div class="component-head">
270
+ <h3 class="component-title">${step === undefined ? '' : `<span class="component-step">${esc(String(step))}</span>`}<span>${esc(t(block.name))}</span></h3>
271
+ ${block.summary ? `<p class="component-summary">${esc(t(block.summary))}</p>` : ''}
272
+ </div>`
273
+ : '';
274
+ // An extension's tabs are browsers; naming them "Operating system" would mislead a screen reader.
275
+ const tabsLabel = groups.every(([os]) => isBrowserPlatform(os)) ? t('Browser') : t('Operating system');
276
+ return `<section class="component"${attrs({ 'data-component': block.id || undefined })}>
277
+ ${head}
278
+ <div class="os-tabs" role="tablist" aria-label="${esc(tabsLabel)}" hidden>${tabs.join('')}</div>
279
+ <div class="os-groups">
280
+ ${sections.join('\n')}
281
+ </div>
282
+ </section>`;
283
+ }
208
284
  /**
209
285
  * The split button plus the dialog holding every target. Both are server-rendered: without
210
286
  * JavaScript the dialog stays an open panel below the button (see `site.css`), with it the
211
287
  * component turns the same markup into a modal and swaps the button for the detected platform.
288
+ *
289
+ * The button offers the part that is installed first; the dialog lists every part in that order,
290
+ * so a product made of an app and the extension it pairs with reads as the two steps it is.
212
291
  */
213
- function downloadWidget(ctx) {
292
+ export function downloadWidget(ctx) {
214
293
  const { data, t } = ctx;
215
- const targets = data.downloads.targets;
216
- const primary = defaultTarget(targets);
294
+ const primary = defaultTarget(primaryTargets(data));
217
295
  if (!primary) {
218
296
  // Nothing downloadable yet: point at the releases, or at the source when there are none.
219
297
  const [href, label] = data.downloads.hasReleases
@@ -223,18 +301,10 @@ function downloadWidget(ctx) {
223
301
  <a class="button download-cta" href="${esc(href)}">${icon('download')}<span class="download-label">${esc(label)}</span></a>
224
302
  </div>`;
225
303
  }
226
- const groups = PLATFORM_ORDER.flatMap((os) => {
227
- const own = targets.filter((target) => target.os === os);
228
- return own.length > 0 ? [[os, own]] : [];
229
- });
230
- const tabs = groups.map(([os]) => `<button class="os-tab" type="button" role="tab" id="os-tab-${esc(os)}" aria-controls="os-group-${esc(os)}" data-os="${esc(os)}" aria-selected="${os === primary.os}">${icon(PLATFORM_ICONS[os])}<span>${esc(osName(os, t))}</span></button>`);
231
- const sections = groups.map(([os, own,]) => `<section class="os-group" id="os-group-${esc(os)}" role="tabpanel" aria-labelledby="os-tab-${esc(os)}" data-os="${esc(os)}">
232
- <h3>${icon(PLATFORM_ICONS[os])}<span>${esc(osName(os, t))}</span></h3>
233
- <ul>
234
- ${own.map((target) => targetItem(target, ctx)).join('\n')}
235
- </ul>
236
- ${os === 'darwin' ? `<p class="os-note">${esc(t('Apple Silicon is assumed. Pick the Intel build if your Mac has an Intel processor.'))}</p>` : ''}
237
- </section>`);
304
+ const blocks = componentBlocks(data);
305
+ const named = blocks.filter((block) => block.name).length;
306
+ let step = 0;
307
+ const sections = blocks.map((block, index) => componentSection(block, blockSlug(block, index), block.name && named > 1 ? ++step : undefined, ctx));
238
308
  return `<app-download class="download">
239
309
  <div class="download-split">
240
310
  <a class="button download-cta" href="${esc(primary.url)}"${attrs({ 'data-os': primary.os, 'data-arch': primary.arch })}>${icon('download')}<span class="download-label">${esc(ctaLabel(primary, t))}</span></a>
@@ -245,8 +315,7 @@ ${os === 'darwin' ? `<p class="os-note">${esc(t('Apple Silicon is assumed. Pick
245
315
  <h2 id="all-downloads-title">${esc(t('All download options'))}</h2>
246
316
  <button class="dialog-close" type="button" aria-label="${esc(t('Close'))}" hidden>${icon('close')}</button>
247
317
  </div>
248
- <div class="os-tabs" role="tablist" aria-label="${esc(t('Operating system'))}" hidden>${tabs.join('')}</div>
249
- <div class="os-groups">
318
+ <div class="components">
250
319
  ${sections.join('\n')}
251
320
  </div>
252
321
  ${data.downloads.hasReleases ? `<p class="dialog-foot"><a href="${esc(data.downloads.releasesUrl)}">${esc(t('All releases on GitHub'))}${icon('external', { class: 'link-external' })}</a></p>` : ''}
@@ -276,7 +345,8 @@ export function badges(ctx) {
276
345
  const number = new Intl.NumberFormat(lang, { notation: 'compact', maximumFractionDigits: 1 });
277
346
  const targets = data.downloads.targets;
278
347
  const specs = [];
279
- const primary = defaultTarget(targets);
348
+ // The badge belongs to the primary button, so it reports the part that button offers.
349
+ const primary = defaultTarget(primaryTargets(data));
280
350
  if (primary) {
281
351
  // Not every target reports a size; the client swaps this one as the reader picks another.
282
352
  const size = sizeLabel(primary.size, lang);
@@ -70,7 +70,29 @@ const FORMATS = [
70
70
  'xpi',
71
71
  'source',
72
72
  ];
73
- function parseDownloads(raw) {
73
+ /**
74
+ * `gjsify.site.components`: the parts of the product a person installs one after another. `match`
75
+ * arrives as a string because this is JSON, and is compiled case-insensitively — release asset
76
+ * names differ in case between the formats that build them.
77
+ */
78
+ function parseComponents(raw) {
79
+ return objectList(raw, 'components').map((entry, index) => {
80
+ const where = `gjsify.site.components[${index}]`;
81
+ const id = stringField(entry, 'id', where);
82
+ const name = stringField(entry, 'name', where);
83
+ const summary = entry.summary === undefined ? undefined : stringField(entry, 'summary', where);
84
+ if (entry.match === undefined)
85
+ return { id, name, summary };
86
+ const pattern = stringField(entry, 'match', where);
87
+ try {
88
+ return { id, name, summary, match: new RegExp(pattern, 'i') };
89
+ }
90
+ catch (cause) {
91
+ throw new Error(`${where}.match is not a valid regular expression: ${pattern}`, { cause });
92
+ }
93
+ });
94
+ }
95
+ function parseDownloads(raw, components) {
74
96
  return objectList(raw, 'downloads').map((entry, index) => {
75
97
  const where = `gjsify.site.downloads[${index}]`;
76
98
  const platform = stringField(entry, 'platform', where);
@@ -92,7 +114,12 @@ function parseDownloads(raw) {
92
114
  const label = entry.label;
93
115
  if (label !== undefined && (typeof label !== 'string' || label === ''))
94
116
  throw new Error(`${where}.label must be a non-empty string`);
95
- return { platform, format, url, label, available };
117
+ const component = entry.component === undefined ? undefined : stringField(entry, 'component', where);
118
+ // A typo here would silently drop the row into the unnamed rest of the dialog.
119
+ if (component !== undefined && !components.some((part) => part.id === component)) {
120
+ throw new Error(`${where}.component "${component}" is not one of gjsify.site.components: ${components.map((part) => part.id).join(', ') || '(none declared)'}`);
121
+ }
122
+ return { platform, format, url, label, available, component };
96
123
  });
97
124
  }
98
125
  function parseReleaseAssets(raw) {
@@ -118,6 +145,7 @@ export function parseSiteConfig(manifest) {
118
145
  if (!languages.includes(defaultLanguage))
119
146
  throw new Error(`gjsify.site.defaultLanguage "${defaultLanguage}" is not in languages`);
120
147
  const url = requireString(raw, 'url');
148
+ const components = parseComponents(raw);
121
149
  return {
122
150
  metainfo: requireString(raw, 'metainfo'),
123
151
  base: normalizeBase(requireString(raw, 'base')),
@@ -131,7 +159,8 @@ export function parseSiteConfig(manifest) {
131
159
  translations: optionalString(raw, 'translations'),
132
160
  pageTranslations: optionalString(raw, 'pageTranslations'),
133
161
  icon: optionalString(raw, 'icon'),
134
- downloads: parseDownloads(raw),
162
+ components,
163
+ downloads: parseDownloads(raw, components),
135
164
  releaseAssets: parseReleaseAssets(raw),
136
165
  support: objectList(raw, 'support').map((entry, index) => parseSupport(entry, `gjsify.site.support[${index}]`)),
137
166
  legal: objectList(raw, 'legal').map((entry, index) => ({
@@ -1,4 +1,4 @@
1
- import type { DownloadTarget, ReleaseAsset, SiteDownloadConfig, TargetArch, TargetFormat, TargetPlatform } from './types.js';
1
+ import type { DownloadTarget, ReleaseAsset, SiteComponent, SiteDownloadConfig, TargetArch, TargetFormat, TargetPlatform } from './types.js';
2
2
  /** Browser extensions are listed after the OS builds; a page usually has only one kind. */
3
3
  export declare const PLATFORM_ORDER: readonly TargetPlatform[];
4
4
  /** Platforms that are browsers: their targets are picked by the visitor's browser, not OS. */
@@ -14,6 +14,13 @@ export declare function classifyAsset(asset: ReleaseAsset): {
14
14
  format: TargetFormat;
15
15
  arch: TargetArch;
16
16
  } | null;
17
+ /**
18
+ * Which part of the product a release asset installs. The first component whose `match` accepts
19
+ * the file name wins, so declaration order is the tie-breaker; a component without a `match` is
20
+ * the catch-all for everything the earlier ones left. Undefined when no component claims the file,
21
+ * which is also the single-component case where the dialog shows no headings at all.
22
+ */
23
+ export declare function componentOf(name: string, components: readonly SiteComponent[]): string | undefined;
17
24
  export interface DownloadSources {
18
25
  assets: readonly ReleaseAsset[];
19
26
  /** Flathub application id, when the app is published there. */
@@ -24,6 +31,12 @@ export interface DownloadSources {
24
31
  webAppUrl?: string;
25
32
  /** `gjsify.site.downloads`: targets no release asset supplies, e.g. a store page. */
26
33
  configured?: readonly SiteDownloadConfig[];
34
+ /** `gjsify.site.components`, in install order; empty when the product is one thing to download. */
35
+ components?: readonly SiteComponent[];
27
36
  }
28
- /** Builds the sorted target list and marks the first available target of every platform as recommended. */
37
+ /**
38
+ * Builds the sorted target list and marks the first available target of every platform as
39
+ * recommended — once per component, because a person installs each part of the product in turn and
40
+ * every part needs its own first choice.
41
+ */
29
42
  export declare function buildDownloadTargets(sources: DownloadSources): DownloadTarget[];
@@ -118,6 +118,15 @@ function rank(order, value) {
118
118
  const index = order.indexOf(value);
119
119
  return index === -1 ? order.length : index;
120
120
  }
121
+ /**
122
+ * Which part of the product a release asset installs. The first component whose `match` accepts
123
+ * the file name wins, so declaration order is the tie-breaker; a component without a `match` is
124
+ * the catch-all for everything the earlier ones left. Undefined when no component claims the file,
125
+ * which is also the single-component case where the dialog shows no headings at all.
126
+ */
127
+ export function componentOf(name, components) {
128
+ return components.find((part) => part.match === undefined || part.match.test(name))?.id;
129
+ }
121
130
  /**
122
131
  * What the store install costs to download. Flathub's own number is the honest one; the Flatpak
123
132
  * bundle of the same release is close enough to stand in when the API did not answer.
@@ -128,14 +137,24 @@ function storeSize(sources) {
128
137
  const bundle = sources.assets.find((asset) => classifyAsset(asset)?.format === 'flatpak');
129
138
  return bundle?.size;
130
139
  }
131
- /** Builds the sorted target list and marks the first available target of every platform as recommended. */
140
+ /**
141
+ * Builds the sorted target list and marks the first available target of every platform as
142
+ * recommended — once per component, because a person installs each part of the product in turn and
143
+ * every part needs its own first choice.
144
+ */
132
145
  export function buildDownloadTargets(sources) {
133
146
  const targets = [];
147
+ const components = sources.components ?? [];
148
+ const order = components.map((part) => part.id);
149
+ // A store install and the live web build have no file name to match on; they are the product
150
+ // itself, which is the first component in install order.
151
+ const primaryComponent = components[0]?.id;
134
152
  if (sources.flathub) {
135
153
  targets.push({
136
154
  os: 'linux',
137
155
  arch: 'noarch',
138
156
  format: 'flathub',
157
+ component: primaryComponent,
139
158
  label: defaultLabel('linux', 'flathub'),
140
159
  url: `https://flathub.org/apps/${sources.flathub}`,
141
160
  available: true,
@@ -149,6 +168,7 @@ export function buildDownloadTargets(sources) {
149
168
  continue;
150
169
  targets.push({
151
170
  ...kind,
171
+ component: componentOf(asset.name, components),
152
172
  label: defaultLabel(kind.os, kind.format),
153
173
  url: asset.downloadUrl,
154
174
  available: true,
@@ -162,6 +182,7 @@ export function buildDownloadTargets(sources) {
162
182
  os: 'web',
163
183
  arch: 'noarch',
164
184
  format: 'web',
185
+ component: primaryComponent,
165
186
  label: defaultLabel('web', 'web'),
166
187
  url: sources.webAppUrl,
167
188
  available: true,
@@ -173,20 +194,23 @@ export function buildDownloadTargets(sources) {
173
194
  os: entry.platform,
174
195
  arch: 'noarch',
175
196
  format: entry.format,
197
+ component: entry.component,
176
198
  label: entry.label ?? defaultLabel(entry.platform, entry.format),
177
199
  url: entry.available ? (entry.url ?? '') : '',
178
200
  available: entry.available,
179
201
  recommended: false,
180
202
  });
181
203
  }
182
- targets.sort((a, b) => rank(PLATFORM_ORDER, a.os) - rank(PLATFORM_ORDER, b.os) ||
204
+ targets.sort((a, b) => rank(order, a.component ?? '') - rank(order, b.component ?? '') ||
205
+ rank(PLATFORM_ORDER, a.os) - rank(PLATFORM_ORDER, b.os) ||
183
206
  rank(FORMAT_RANK[a.os], a.format) - rank(FORMAT_RANK[b.os], b.format) ||
184
207
  rank(ARCH_RANK[a.os], a.arch) - rank(ARCH_RANK[b.os], b.arch));
185
208
  const seen = new Set();
186
209
  for (const target of targets) {
187
- if (!target.available || seen.has(target.os))
210
+ const key = `${target.component ?? ''}:${target.os}`;
211
+ if (!target.available || seen.has(key))
188
212
  continue;
189
- seen.add(target.os);
213
+ seen.add(key);
190
214
  target.recommended = true;
191
215
  }
192
216
  return targets;
@@ -151,6 +151,7 @@ export async function loadSiteData(options) {
151
151
  targets: buildDownloadTargets({
152
152
  assets: config.releaseAssets ? (latestRelease?.assets ?? []) : [],
153
153
  configured: config.downloads,
154
+ components: config.components,
154
155
  flathub: config.flathub,
155
156
  flathubSize,
156
157
  webAppUrl: config.webApp ? `${config.base}${config.webApp.replace(/^\//, '')}` : undefined,
@@ -41,6 +41,25 @@ export interface SiteLegalLink {
41
41
  label: string;
42
42
  url: string;
43
43
  }
44
+ /**
45
+ * One part of the product a person installs separately — a desktop app, the browser extension it
46
+ * pairs with, a command line build. Declared in `gjsify.site.components`, in the order they are
47
+ * installed, which is the order the download dialog explains them in.
48
+ */
49
+ export interface SiteComponent {
50
+ /** Referenced by `gjsify.site.downloads[].component`; also the `data-component` attribute. */
51
+ id: string;
52
+ /** English heading, translated through the website catalogue. */
53
+ name: string;
54
+ /** One sentence saying what this part is for and why it is needed. */
55
+ summary?: string;
56
+ /**
57
+ * Release assets whose file name matches belong to this component. Anchor it, because the
58
+ * first matching component in declaration order wins; a component without one takes whatever
59
+ * is left, so the catch-all goes last.
60
+ */
61
+ match?: RegExp;
62
+ }
44
63
  /** One entry of `gjsify.site.downloads`: a target the release assets cannot supply (a store page, the install docs). */
45
64
  export interface SiteDownloadConfig {
46
65
  platform: TargetPlatform;
@@ -51,6 +70,8 @@ export interface SiteDownloadConfig {
51
70
  label?: string;
52
71
  /** False lists the target as "not yet available" without a link. Default true. */
53
72
  available: boolean;
73
+ /** Id of the `gjsify.site.components` entry this target installs. */
74
+ component?: string;
54
75
  }
55
76
  /** The `gjsify.site` block of the root package.json, with defaults applied. */
56
77
  export interface SiteConfig {
@@ -75,6 +96,11 @@ export interface SiteConfig {
75
96
  pageTranslations?: string;
76
97
  /** Repo-relative app icon; defaults to the Flatpak or `ship` icon of the root package.json. */
77
98
  icon?: string;
99
+ /**
100
+ * The separately installed parts of the product, in install order. Empty for the common case of
101
+ * one thing to download, which the dialog then lists without any component heading.
102
+ */
103
+ components: SiteComponent[];
78
104
  /** Extra download targets, added to the ones derived from the latest release. */
79
105
  downloads: SiteDownloadConfig[];
80
106
  /** False ignores the latest release's assets, so `downloads` is the whole list. Default true. */
@@ -107,6 +133,8 @@ export interface DownloadTarget {
107
133
  os: TargetPlatform;
108
134
  arch: TargetArch;
109
135
  format: TargetFormat;
136
+ /** Id of the `SiteComponent` this target installs; empty when the product has only one part. */
137
+ component?: string;
110
138
  /** English label; the page translates it through the website catalogue. */
111
139
  label: string;
112
140
  /** Empty when the target is not available yet. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/site",
3
- "version": "0.64.1-next.247",
3
+ "version": "0.64.1-next.249",
4
4
  "description": "Static landing page generator for a GNOME-style app, rendered from its AppStream metainfo — downloads, screenshots, release notes, support links, no JavaScript required.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -54,26 +54,26 @@
54
54
  "license": "MIT",
55
55
  "dependencies": {
56
56
  "@girs/gdkpixbuf-2.0": "5.4.0",
57
- "@gjsify/console": "0.64.1-next.247",
58
- "@gjsify/crypto": "0.64.1-next.247",
59
- "@gjsify/fs": "0.64.1-next.247",
60
- "@gjsify/module": "0.64.1-next.247",
61
- "@gjsify/os": "0.64.1-next.247",
62
- "@gjsify/path": "0.64.1-next.247",
63
- "@gjsify/process": "0.64.1-next.247",
64
- "@gjsify/url": "0.64.1-next.247",
57
+ "@gjsify/console": "0.64.1-next.249",
58
+ "@gjsify/crypto": "0.64.1-next.249",
59
+ "@gjsify/fs": "0.64.1-next.249",
60
+ "@gjsify/module": "0.64.1-next.249",
61
+ "@gjsify/os": "0.64.1-next.249",
62
+ "@gjsify/path": "0.64.1-next.249",
63
+ "@gjsify/process": "0.64.1-next.249",
64
+ "@gjsify/url": "0.64.1-next.249",
65
65
  "fast-xml-parser": "^5.11.2",
66
66
  "qrcode-generator": "^2.0.4"
67
67
  },
68
68
  "devDependencies": {
69
- "@gjsify/cli": "0.64.1-next.247",
69
+ "@gjsify/cli": "0.64.1-next.249",
70
70
  "@gjsify/node-gi": "file:../../node-gi/node-gi",
71
- "@gjsify/unit": "0.64.1-next.247",
71
+ "@gjsify/unit": "0.64.1-next.249",
72
72
  "@types/node": "^25.9.2",
73
73
  "typescript": "^6.0.3"
74
74
  },
75
75
  "gjsify": {
76
76
  "tier": 2
77
77
  },
78
- "gitHead": "0865008d02b3a10af8152061095378aabe6b9d5d"
78
+ "gitHead": "2e501133bfbe80e44506deef30e911e0c551502f"
79
79
  }
package/po/de.po CHANGED
@@ -111,18 +111,21 @@ msgstr "Empfohlen"
111
111
  msgid "Not yet available"
112
112
  msgstr "Noch nicht verfügbar"
113
113
 
114
- msgid "All releases"
115
- msgstr "Alle Versionen"
116
-
117
114
  msgid "Apple Silicon is assumed. Pick the Intel build if your Mac has an Intel processor."
118
115
  msgstr "Apple Silicon wird angenommen. Für Macs mit Intel-Prozessor die Intel-Variante wählen."
119
116
 
120
- msgid "All download options"
121
- msgstr "Alle Download-Optionen"
117
+ msgid "Browser"
118
+ msgstr "Browser"
122
119
 
123
120
  msgid "Operating system"
124
121
  msgstr "Betriebssystem"
125
122
 
123
+ msgid "All releases"
124
+ msgstr "Alle Versionen"
125
+
126
+ msgid "All download options"
127
+ msgstr "Alle Download-Optionen"
128
+
126
129
  msgid "All releases on GitHub"
127
130
  msgstr "Alle Versionen auf GitHub"
128
131
 
package/po/site.pot CHANGED
@@ -110,18 +110,21 @@ msgstr ""
110
110
  msgid "Not yet available"
111
111
  msgstr ""
112
112
 
113
- msgid "All releases"
114
- msgstr ""
115
-
116
113
  msgid "Apple Silicon is assumed. Pick the Intel build if your Mac has an Intel processor."
117
114
  msgstr ""
118
115
 
119
- msgid "All download options"
116
+ msgid "Browser"
120
117
  msgstr ""
121
118
 
122
119
  msgid "Operating system"
123
120
  msgstr ""
124
121
 
122
+ msgid "All releases"
123
+ msgstr ""
124
+
125
+ msgid "All download options"
126
+ msgstr ""
127
+
125
128
  msgid "All releases on GitHub"
126
129
  msgstr ""
127
130