chromosomix 0.18.0 → 0.20.0

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
@@ -1,7 +1,9 @@
1
1
  # chromosomix
2
2
 
3
3
  [![Web app](https://img.shields.io/badge/web%20app-chromosomix.app-1f6feb)](https://chromosomix.app/)
4
+ [![Documentation](https://img.shields.io/badge/docs-doc.chromosomix.app-1f6feb)](https://doc.chromosomix.app/)
4
5
  [![Tests](https://github.com/js2264/chromosomix/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/js2264/chromosomix/actions/workflows/test.yml)
6
+ [![Coverage](https://img.shields.io/endpoint?url=https%3A%2F%2Fchromosomix.app%2Fcoverage%2Fbadge.json)](https://chromosomix.app/coverage/)
5
7
  [![Deploy](https://github.com/js2264/chromosomix/actions/workflows/deploy.yml/badge.svg?branch=main)](https://github.com/js2264/chromosomix/actions/workflows/deploy.yml)
6
8
  [![npm](https://img.shields.io/npm/v/chromosomix?logo=npm)](https://www.npmjs.com/package/chromosomix)
7
9
  [![Release](https://img.shields.io/github/v/release/js2264/chromosomix?logo=github)](https://github.com/js2264/chromosomix/releases)
@@ -25,7 +27,7 @@ chromosomix launches in about a second and reads your data where it lives, on yo
25
27
 
26
28
  - Perform analyses directly in the browser:
27
29
  - 1D analysis tasks: peak calling from a coverage track (yapc, with IDR between replicates), motif search, base composition and GC skew, the origin and terminus of replication, metaplots, track correlation, FRiP over a feature set, signal by group, interval overlap with a shuffle null, ...
28
- - 2D analysis tasks: P(s) (and its four pair orientations), scalogram, diamond insulation, directionality index, HiC score, coverage, cis/trans per chromosome, virtual 4C, compartments annotation and saddle plot, the alignment of a bacterial chromosome's two arms, loops and borders detection, APA, loop ranking, ...
30
+ - 2D analysis tasks: a QC map report (one HTML page, and MultiQC sections from the CLI), P(s) (and its four pair orientations), scalogram, diamond insulation, directionality index, HiC score, TAD annotation (Juicer's Arrowhead), coverage, cis/trans per chromosome, virtual 4C, compartments annotation and saddle plot, the alignment of a bacterial chromosome's two arms, loops and borders detection, APA, loop ranking, ...
29
31
  - 3D analysis tasks: build a 3D model from the contact map (pastis-NB or ShRec3D), compare a 3D model with the contact map, and turn a 3D model into tracks.
30
32
 
31
33
  - Serverless browser & CLI-based: there is no upload, no required preprocessing step and no backend of any kind — the viewer is static files that read standard genome-wide formats directly. Every analysis can be run **interactively**, in the browser, or **programmatically**, with the `chromosomix` command line in a script or a pipeline: the same tasks, with the same parameters, producing the same results. A bottom Console panel in the browser records every run and reports the `chromosomix` command that repeats it.
@@ -46,9 +48,7 @@ chromosomix launches in about a second and reads your data where it lives, on yo
46
48
 
47
49
  1. **No backend or install of any kind.** No tile server, no specific process on a host, no accounts. The viewer is static files; the data are objects on a bucket, or a file on your disk. This is what makes the app free to run and free to archive.
48
50
 
49
- 2. **Nothing is uploaded.** A local file is handed to an air-gapped worker by reference and read in place through the Emscripten filesystem. Nothing of it crosses the network, which is why there is no account system, no storage and no hosting cost. You can open unpublished patient-derived genomic data on a laptop with the network cable out.
50
-
51
- The website does send anonymous telemetry about how it is used — which kinds of file are opened, which analyses run, which errors are met — as words from fixed lists, with no cookie and nothing that identifies you: never a file name, an address or a place on the genome. **About → telemetry** turns it off, and a browser sending Global Privacy Control sends nothing. The desktop app, the static HTML file, the npm package and embeds never send any ([SPECS §31](SPECS.md#31-telemetry)).
51
+ 2. **Nothing is uploaded.** A local file is handed to an air-gapped worker by reference and read in place through the Emscripten filesystem. Nothing crosses the network, which is why there is no account system, no storage, no hosting cost and nothing to review for GDPR. You can open unpublished patient-derived genomic data on a laptop with the network cable out.
52
52
 
53
53
  3. **No pre-parsing of genomic files is required.** Contact maps and 1D genomic files are read in bounded reads, and the rest is pure canvas transforms with **zero** further I/O. That is the central difference from HiGlass, which needs a tile server, a tile cache and a tile compositor to stream contact maps.
54
54
 
@@ -81,6 +81,14 @@ double-clicking and you end up with a fully functional genome browser.
81
81
  It is attached to every [release](https://github.com/js2264/chromosomix/releases)
82
82
  as `chromosomix-<tag>.html`, and `npm run build:standalone` builds it from a clone.
83
83
 
84
+ ### For AI agents
85
+
86
+ The site describes itself to language models at <https://chromosomix.app/llms.txt>, in the shape
87
+ [llmstxt.org](https://llmstxt.org/) gives one, with reference pages generated from the app itself:
88
+ how to write a link to a view, every analysis task with every parameter, the embed protocol, and the
89
+ file, store and workspace formats. <https://chromosomix.app/llms-full.txt> is all of it, and this
90
+ README, in one file.
91
+
84
92
  ## Embedding chromosomix in a page
85
93
 
86
94
  A minimal version of the viewer can be added to any HTML page as an `<iframe>`. Nothing needs to be installed. There is no extra package dependency and no modifications to make to the page's header, just drop the `<iframe>` in. The `src` attribute is the URL of the viewer, with a query string that tells it what to show.
@@ -184,7 +192,9 @@ npm run build:cli && npm link
184
192
 
185
193
  ### Running tasks
186
194
 
187
- Once it is installed, run the following command to see the available tasks and their parameters:
195
+ Once it is installed, run the following command to see the available tasks and their parameters
196
+ (`chromosomix tasks --json` prints the same as one JSON document, with every value a choice takes,
197
+ for a script or an agent):
188
198
 
189
199
  ```bash
190
200
  chromosomix tasks
@@ -232,6 +242,11 @@ chromosomix tasks
232
242
  ## --enzyme (default "none") enzyme
233
243
  ## --sites (default "") sites
234
244
  ##
245
+ ## mapreport
246
+ ## Map report
247
+ ## --base (default "finest") ladder from
248
+ ## --no-balance read the raw counts (balanced by default where the file carries weights)
249
+ ##
235
250
  ## scalogram
236
251
  ## Scalogram
237
252
  ## --quantiles (default "standard") quantiles
@@ -282,6 +297,18 @@ chromosomix tasks
282
297
  ## --minValid (default 0.5) minimum coverage of the line
283
298
  ## --no-balance read the raw counts (balanced by default where the file carries weights)
284
299
  ##
300
+ ## arrowhead
301
+ ## TAD annotation
302
+ ## --sequences sequences
303
+ ## --matrixSize (default 2000) window
304
+ ## --minBlockSize (default 60) smallest lenient domain
305
+ ## --varThreshold (default 0.2) variance at most
306
+ ## --highSignThreshold (default 0.5) sign agreement at least
307
+ ## --maxLowSignThreshold (default 0.4) sign agreement from
308
+ ## --minLowSignThreshold (default 0) down to
309
+ ## --decrementLowSignThreshold (default 0.1) in steps of
310
+ ## --no-balance read the raw counts (balanced by default where the file carries weights)
311
+ ##
285
312
  ## compartments
286
313
  ## Compartments
287
314
  ## --sequences sequences
@@ -494,7 +521,7 @@ chromosomix tasks
494
521
  ## --groupTrack grouping track
495
522
  ## --cut (default "sign") cut it by
496
523
  ## --quantiles (default 4) quantiles
497
- ## --resolution (default 10000) bin size
524
+ ## --resolution (default 0) bin size
498
525
  ## --perFeature (default false) score every feature
499
526
  ##
500
527
  ## overlap
@@ -542,13 +569,17 @@ Each analysis is held to the package it reproduces — cooltools, coolpuppy, chr
542
569
  bedtools and a dozen others — on public data, command for command, in a notebook rendered with every
543
570
  release: <https://chromosomix.app/validation/>, with every number its figures draw in one table beside
544
571
  it, <https://chromosomix.app/validation/validation.txt>; and how long each command takes beside its
545
- reference, <https://chromosomix.app/timings/>. A third notebook holds the app to this command: ten
546
- tasks run in the app, in a headless browser, then from the line its console wrote for each, and every
547
- file the two wrote compared, <https://chromosomix.app/parity/>. And what the commands cost on a machine
572
+ reference, <https://chromosomix.app/timings/>; and how long a map and a track take to draw beside
573
+ juicebox.js and HiGlass, on the same files, <https://chromosomix.app/viewers/>. A fourth notebook holds
574
+ the app to this command: ten
575
+ tasks run in the app, in headless Chromium, Firefox and WebKit, then from the line its console wrote
576
+ for each, and every file the two wrote compared, <https://chromosomix.app/parity/>. And what the commands cost on a machine
548
577
  like yours — the time and the peak memory of 23 analyses run genome-wide, up to a 1.08-billion-contact
549
578
  human map — is <https://chromosomix.app/laptop/>, rendered on a laptop and committed as
550
- `notebooks/laptop.html`; render `notebooks/laptop.qmd` with Quarto from a clone to measure your own: it
551
- downloads the data, runs each analysis once and tabulates them. The app lists all four in
579
+ `notebooks/laptop.html`; render `notebooks/laptop.qmd` from a clone to measure your own —
580
+ `micromamba run -n open2c quarto render notebooks/laptop.qmd`, in the environment
581
+ `notebooks/environment-laptop.yml` makes — and it downloads the data, runs each analysis once and
582
+ tabulates them. The app lists all five in
552
583
  **About → Notebooks**, each with the one line of what it found.
553
584
 
554
585
  Every task writes its results where `--out` points, each named after the file it came from up to
@@ -1,4 +1,4 @@
1
- Third-party notices for chromosomix 0.18.0
1
+ Third-party notices for chromosomix 0.20.0
2
2
 
3
3
  chromosomix itself is released under the GNU General Public License, version 3 or (at your option) any later version, reproduced below and in its repository, https://github.com/js2264/chromosomix.
4
4
  The complete source of this build, and of every release, is at https://github.com/js2264/chromosomix.
@@ -32,7 +32,7 @@ Copied or ported into the source:
32
32
  yapc 0.1 — GPL-3.0
33
33
 
34
34
  ========================================================================
35
- chromosomix 0.18.0 — GPL-3.0-or-later
35
+ chromosomix 0.20.0 — GPL-3.0-or-later
36
36
  https://github.com/js2264/chromosomix
37
37
  ========================================================================
38
38
 
@@ -1,4 +1,4 @@
1
- import{e,f as b,b as g,i as f,n as O,r as R,w as P,a as A,s as I,g as T,c as G,d as M,m as F,h as H,j as _,D as $,k as B,l as N,o as j,p as K,M as z,q as U,t as W,u as X,v as Y,x as q,y as V,S as J,z as Q,A as Z}from"./index-BZqg8J4S.js";function ee(t){const s=new Set(t.map(i=>i.id));for(let i=t.length;;i++){const o=`b${i}-${Math.floor(Math.random()*1e6).toString(36)}`;if(!s.has(o))return o}}let te=0;function ne(t){const s=e("input",{class:"input",type:"text",placeholder:"optional"}),i=e("input",{class:"input",type:"text",placeholder:"https://bucket.s3.amazonaws.com/prefix/ or s3://bucket/prefix/"}),o=e("input",{class:"input",type:"text",placeholder:"eu-west-3"}),l=e("input",{class:"input",type:"text",placeholder:"AKIA…"}),a=e("input",{class:"input",type:"password",placeholder:"secret key"}),c=e("input",{class:"input",type:"password",placeholder:"session token (only for temporary credentials)"}),r=e("p",{class:"hint bucket-error"}),d=e("div",{class:"bucket-keys hidden",id:`bucket-keys-${++te}`},b("region",o,"The bucket's region. Part of the signature, so a wrong one is a 403."),b("access key ID",l),b("secret key",a),b("session token",c,"Only for STS or SSO credentials."));let n=!1;const p=g("additional settings",()=>h(!n),{class:"btn bucket-more"});p.setAttribute("aria-controls",d.id),p.prepend(f("chevron-down",{size:14}));function h(u){n=u,d.classList.toggle("hidden",!u),p.classList.toggle("is-on",u),p.setAttribute("aria-expanded",String(u)),p.replaceChildren(f(u?"chevron-up":"chevron-down",{size:14})),p.append("additional settings")}h(!1);const w=()=>o.value.trim()!==""||l.value.trim()!==""||a.value!=="",L=()=>{const u=O(i.value,{region:o.value});if(u===null){r.textContent="The prefix must be an https:// URL or an s3://bucket/prefix — a bucket reached over http:// would send the signature in clear, and a page served over https:// cannot read one at all.";return}const m=w();if(m&&(o.value.trim()===""||l.value.trim()===""||a.value==="")){r.textContent="Region, access key ID and secret key are all part of the signature.",h(!0);return}const k=R(),x=c.value.trim(),C={id:ee(k),label:s.value.trim()===""?u:s.value.trim(),base:u,...m?{region:o.value.trim(),accessKeyId:l.value.trim(),secretAccessKey:a.value,...x===""?{}:{sessionToken:x}}:{}};P([...k,C]),A(),r.textContent="";for(const E of[s,i,o,l,a,c])E.value="";t.onSaved(C)};return e("div",{class:"bucket-add"},t.heading===void 0?null:e("h3",{text:t.heading}),b("name",s),b("URL",i),e("div",{class:"bucket-more-row"},p),d,r,e("div",{class:"bucket-actions"},g("connect this bucket",L,{class:"btn btn-primary"})))}function oe(t){const s=e("input",{class:"input",type:"text",placeholder:"Run 2024"}),i=e("input",{class:"input",type:"text",placeholder:"https://drive.google.com/drive/folders/…"}),o=e("p",{class:"hint bucket-error"}),l=e("div",{class:"drive-controls"}),a=()=>{o.textContent="";const n=G(i.value);if(n===null){o.textContent="That is not a Drive folder link. Open the folder in Drive and copy the address bar — it looks like https://drive.google.com/drive/folders/<id> — or paste the id on its own. A link to a single file is not a folder; paste that into the file picker instead. Leave the box empty for all of My Drive.";return}const p=s.value.trim(),w=M({label:p!==""?p:n==="root"?"My Drive":n,folderId:n});s.value="",i.value="",A(),t(w.id)},c=e("div",{class:"drive-folder hidden"},l,o),r=()=>{if(!T().signedIn){l.replaceChildren(),o.textContent="",c.classList.add("hidden");return}c.classList.remove("hidden"),l.replaceChildren(e("p",{class:"hint",text:"A folder of your Drive, browsed in the Files panel:"}),b("name",s,"What this folder is called in the Files panel. Yours, not Drive’s"),b("folder link or id",i,"The address bar of the folder open in Drive, or its id. Empty means all of My Drive."),e("div",{class:"bucket-actions"},g("connect this folder",a,{class:"btn btn-primary"})))};r();const d=I(()=>{if(!c.isConnected){d();return}r()});return c}function S(t){return e("pre",{class:"connect-tip-code"},e("code",{text:t}))}let se=0;const ie=6,D=12,re=140;function v(t,s,i){const o=`connect-tip-${++se}`,l=e("div",{class:"connect-tip-box"},...i.map(n=>typeof n=="string"?e("p",{text:n}):n)),a=e("div",{class:"connect-tip",id:o,attrs:{role:"tooltip"}},l),c=e("button",{class:"connect-help",type:"button",text:"?",attrs:{"aria-label":`About ${t}`,"aria-describedby":o,"aria-expanded":"false"}}),r=e("span",{class:"connect-help-wrap"},c,a),d=()=>{const n=c.getBoundingClientRect(),p=document.documentElement.clientWidth;a.style.top=`${n.bottom}px`,a.style.right=`${Math.max(D,p-n.right)}px`,l.style.maxHeight=`${Math.max(re,window.innerHeight-n.bottom-ie-D)}px`};return c.addEventListener("pointerenter",d),c.addEventListener("focus",d),c.addEventListener("click",()=>{d();const n=r.classList.toggle("is-open");c.setAttribute("aria-expanded",String(n))}),c.addEventListener("blur",()=>{r.classList.remove("is-open"),c.setAttribute("aria-expanded","false")}),e("div",{class:"connect-head"},e("h3",{},f(s,{size:15}),e("span",{text:t})),r)}function y(t,s,i,o){return F(t,s,i,{class:"btn btn-mark btn-signin",disabled:o.disabled,...o.reason===void 0?{}:{reason:o.reason}})}function ae(t){const s=e("div",{class:"connect-signin"}),i=e("p",{class:"hint bucket-error"}),o=e("input",{class:"input",type:"text",placeholder:"the code Dropbox showed"}),l=e("a",{class:"hint",text:"the Dropbox page"});l.setAttribute("target","_blank"),l.setAttribute("rel","noopener noreferrer");const a=e("div",{class:"connect-code hidden"},e("p",{class:"hint",text:"Dropbox shows a code instead of sending you back here, because this page has no address to be sent back to. Copy the code into the box and press finish."}),e("p",{class:"hint"},e("span",{text:"If no tab opened, open "}),l,e("span",{text:"."})),b("code",o)),c=()=>{const n=_();if(n.signedIn&&n.expiresAt!==null){s.replaceChildren(e("span",{class:"hint",text:`Signed in until ${le(n.expiresAt)}.`}),g("browse my Dropbox",()=>t($),{class:"btn"}),g("sign out",()=>{B(),a.classList.add("hidden"),o.value=""},{class:"btn"}));return}const p=y(n.expiresAt!==null?"sign in again":"sign in to Dropbox","dropbox",()=>{i.textContent="",p.disabled=!0,N().then(({url:h})=>{l.setAttribute("href",h),window.open(h,"_blank","noopener,noreferrer"),a.classList.remove("hidden")}).catch(h=>{i.textContent=h instanceof Error?h.message:String(h)}).finally(()=>{c()})},{disabled:!n.configured,reason:"This site was built without a Dropbox app key, so there is no app to sign in to."});s.replaceChildren(p,...n.configured?[]:[e("p",{class:"hint",text:"Signing in to Dropbox is not set up on this site: it needs a Dropbox app key, which the site’s maintainer provides as DROPBOX_APP_KEY (docs/deploy.md)."})])};c();const r=()=>{i.textContent="",j(o.value).then(()=>{o.value="",a.classList.add("hidden")}).catch(n=>{i.textContent=n instanceof Error?n.message:String(n)}).finally(()=>{c()})};a.appendChild(e("div",{class:"bucket-actions"},g("finish",r,{class:"btn btn-primary"})));const d=H(()=>{if(!s.isConnected){d();return}c()});return e("div",{class:"connect-section"},v("Dropbox","dropbox",ce()),s,a,i)}function ce(){return["Sign in with your own Dropbox account to browse it in the Files panel.",`The app is given ${K.join(" and ")}: read access to everything in your Dropbox, or to the app’s own folder inside it if the site registered it that way.`,"The token lives only in this tab’s memory — never written to storage, gone when the tab closes, expiring on its own. A file already open keeps reading for the four hours its link lasts. No account information is asked for, so this page does not know whose Dropbox it is."]}function le(t){return new Date(t).toLocaleTimeString()}function ke(t){return new z({title:"Connect a folder",variant:"modal-wide",...t.onClose===void 0?{}:{onClose:t.onClose},body:s=>pe(t,s)})}function de(){const t={local:he,bucket:be,drive:fe,dropbox:s=>ae(s)??e("div")};return U().map(s=>({...s,pane:t[s.id]}))}function pe(t,s){const i=r=>{s(),t.onConnected(r)},o=de(),l=e("div",{class:"connect-pane"}),a=e("div",{class:"connect-providers",attrs:{role:"tablist"}}),c=r=>{const d=o.find(n=>n.id===r)??o[0];d.id;for(const n of a.children){const p=n.getAttribute("data-provider")===d.id;n.classList.toggle("is-on",p),n.setAttribute("aria-selected",String(p))}l.replaceChildren(d.pane(i))};for(const r of o){const d=e("button",{class:"connect-provider",type:"button",attrs:{role:"tab","data-provider":r.id,"aria-selected":"false"}},f(r.mark,{size:22}),e("span",{text:r.label}));d.addEventListener("click",()=>c(r.id)),a.appendChild(d)}return c(t.provider??"local"),e("div",{class:"modal-body bucket-body connect-body"},a,l,ye())}function he(t){const s=W(),i=X(),o=e("p",{class:"hint bucket-error"}),l=async()=>{o.textContent="";const a=await Z();a!==null&&t(a)};return e("div",{class:"connect-section"},v("This machine","hard-drive",ue(s,i)),e("div",{class:"connect-signin"},y(Y(),"hard-drive",()=>void l(),{disabled:!1})),o)}function ue(t,s){return["Read in place, never uploaded. The folder is listed one level at a time in the Files panel, and no byte of it is sent anywhere.",s?"The app keeps permission to list the folder across restarts, and the workspaces you save say which of your files are in it, so reopening one finds them there. Take it back any time under What this browser is remembering (Ctrl+P → Help).":t?"In a browser tab the folder is connected for this visit and kept nowhere, so nobody else at this browser finds it. Installed as an app — or in the desktop app — the folder is kept across restarts, and saved workspaces find their files in it again.":"This browser cannot keep that permission, so what you get is a snapshot of the file list for this visit only — it does not update and it does not survive a reload. Chrome, Edge and the desktop app keep the connection when installed as an app."]}function be(t){return e("div",{class:"connect-section"},v("A bucket","cloud",ge()),ne({onSaved:s=>t(Q(s.id))}))}function ge(){return["An https:// or s3:// prefix on S3, R2, MinIO, Google Cloud Storage or anything else that answers ListObjectsV2. Your browser reads it directly, with no server in between, so the bucket — not this page — decides what may be read. Two things have to be set.","CORS first, without which no read starts at all. Range and Content-Range are the pair that matters: a .mcool is read in pieces and never whole, so a bucket that will not expose them serves files this viewer cannot open. Narrow AllowedOrigins to the site once it works.",S(`[{
1
+ import{e,f as b,b as g,i as f,n as O,r as R,w as P,a as A,s as I,g as T,c as G,d as M,m as F,h as H,j as _,D as $,k as B,l as N,o as j,p as K,M as z,q as U,t as W,u as X,v as Y,x as q,y as V,S as J,z as Q,A as Z}from"./index-Cys9FWNc.js";function ee(t){const s=new Set(t.map(i=>i.id));for(let i=t.length;;i++){const o=`b${i}-${Math.floor(Math.random()*1e6).toString(36)}`;if(!s.has(o))return o}}let te=0;function ne(t){const s=e("input",{class:"input",type:"text",placeholder:"optional"}),i=e("input",{class:"input",type:"text",placeholder:"https://bucket.s3.amazonaws.com/prefix/ or s3://bucket/prefix/"}),o=e("input",{class:"input",type:"text",placeholder:"eu-west-3"}),l=e("input",{class:"input",type:"text",placeholder:"AKIA…"}),a=e("input",{class:"input",type:"password",placeholder:"secret key"}),c=e("input",{class:"input",type:"password",placeholder:"session token (only for temporary credentials)"}),r=e("p",{class:"hint bucket-error"}),d=e("div",{class:"bucket-keys hidden",id:`bucket-keys-${++te}`},b("region",o,"The bucket's region. Part of the signature, so a wrong one is a 403."),b("access key ID",l),b("secret key",a),b("session token",c,"Only for STS or SSO credentials."));let n=!1;const p=g("additional settings",()=>h(!n),{class:"btn bucket-more"});p.setAttribute("aria-controls",d.id),p.prepend(f("chevron-down",{size:14}));function h(u){n=u,d.classList.toggle("hidden",!u),p.classList.toggle("is-on",u),p.setAttribute("aria-expanded",String(u)),p.replaceChildren(f(u?"chevron-up":"chevron-down",{size:14})),p.append("additional settings")}h(!1);const w=()=>o.value.trim()!==""||l.value.trim()!==""||a.value!=="",L=()=>{const u=O(i.value,{region:o.value});if(u===null){r.textContent="The prefix must be an https:// URL or an s3://bucket/prefix — a bucket reached over http:// would send the signature in clear, and a page served over https:// cannot read one at all.";return}const m=w();if(m&&(o.value.trim()===""||l.value.trim()===""||a.value==="")){r.textContent="Region, access key ID and secret key are all part of the signature.",h(!0);return}const k=R(),x=c.value.trim(),C={id:ee(k),label:s.value.trim()===""?u:s.value.trim(),base:u,...m?{region:o.value.trim(),accessKeyId:l.value.trim(),secretAccessKey:a.value,...x===""?{}:{sessionToken:x}}:{}};P([...k,C]),A(),r.textContent="";for(const E of[s,i,o,l,a,c])E.value="";t.onSaved(C)};return e("div",{class:"bucket-add"},t.heading===void 0?null:e("h3",{text:t.heading}),b("name",s),b("URL",i),e("div",{class:"bucket-more-row"},p),d,r,e("div",{class:"bucket-actions"},g("connect this bucket",L,{class:"btn btn-primary"})))}function oe(t){const s=e("input",{class:"input",type:"text",placeholder:"Run 2024"}),i=e("input",{class:"input",type:"text",placeholder:"https://drive.google.com/drive/folders/…"}),o=e("p",{class:"hint bucket-error"}),l=e("div",{class:"drive-controls"}),a=()=>{o.textContent="";const n=G(i.value);if(n===null){o.textContent="That is not a Drive folder link. Open the folder in Drive and copy the address bar — it looks like https://drive.google.com/drive/folders/<id> — or paste the id on its own. A link to a single file is not a folder; paste that into the file picker instead. Leave the box empty for all of My Drive.";return}const p=s.value.trim(),w=M({label:p!==""?p:n==="root"?"My Drive":n,folderId:n});s.value="",i.value="",A(),t(w.id)},c=e("div",{class:"drive-folder hidden"},l,o),r=()=>{if(!T().signedIn){l.replaceChildren(),o.textContent="",c.classList.add("hidden");return}c.classList.remove("hidden"),l.replaceChildren(e("p",{class:"hint",text:"A folder of your Drive, browsed in the Files panel:"}),b("name",s,"What this folder is called in the Files panel. Yours, not Drive’s"),b("folder link or id",i,"The address bar of the folder open in Drive, or its id. Empty means all of My Drive."),e("div",{class:"bucket-actions"},g("connect this folder",a,{class:"btn btn-primary"})))};r();const d=I(()=>{if(!c.isConnected){d();return}r()});return c}function S(t){return e("pre",{class:"connect-tip-code"},e("code",{text:t}))}let se=0;const ie=6,D=12,re=140;function v(t,s,i){const o=`connect-tip-${++se}`,l=e("div",{class:"connect-tip-box"},...i.map(n=>typeof n=="string"?e("p",{text:n}):n)),a=e("div",{class:"connect-tip",id:o,attrs:{role:"tooltip"}},l),c=e("button",{class:"connect-help",type:"button",text:"?",attrs:{"aria-label":`About ${t}`,"aria-describedby":o,"aria-expanded":"false"}}),r=e("span",{class:"connect-help-wrap"},c,a),d=()=>{const n=c.getBoundingClientRect(),p=document.documentElement.clientWidth;a.style.top=`${n.bottom}px`,a.style.right=`${Math.max(D,p-n.right)}px`,l.style.maxHeight=`${Math.max(re,window.innerHeight-n.bottom-ie-D)}px`};return c.addEventListener("pointerenter",d),c.addEventListener("focus",d),c.addEventListener("click",()=>{d();const n=r.classList.toggle("is-open");c.setAttribute("aria-expanded",String(n))}),c.addEventListener("blur",()=>{r.classList.remove("is-open"),c.setAttribute("aria-expanded","false")}),e("div",{class:"connect-head"},e("h3",{},f(s,{size:15}),e("span",{text:t})),r)}function y(t,s,i,o){return F(t,s,i,{class:"btn btn-mark btn-signin",disabled:o.disabled,...o.reason===void 0?{}:{reason:o.reason}})}function ae(t){const s=e("div",{class:"connect-signin"}),i=e("p",{class:"hint bucket-error"}),o=e("input",{class:"input",type:"text",placeholder:"the code Dropbox showed"}),l=e("a",{class:"hint",text:"the Dropbox page"});l.setAttribute("target","_blank"),l.setAttribute("rel","noopener noreferrer");const a=e("div",{class:"connect-code hidden"},e("p",{class:"hint",text:"Dropbox shows a code instead of sending you back here, because this page has no address to be sent back to. Copy the code into the box and press finish."}),e("p",{class:"hint"},e("span",{text:"If no tab opened, open "}),l,e("span",{text:"."})),b("code",o)),c=()=>{const n=_();if(n.signedIn&&n.expiresAt!==null){s.replaceChildren(e("span",{class:"hint",text:`Signed in until ${le(n.expiresAt)}.`}),g("browse my Dropbox",()=>t($),{class:"btn"}),g("sign out",()=>{B(),a.classList.add("hidden"),o.value=""},{class:"btn"}));return}const p=y(n.expiresAt!==null?"sign in again":"sign in to Dropbox","dropbox",()=>{i.textContent="",p.disabled=!0,N().then(({url:h})=>{l.setAttribute("href",h),window.open(h,"_blank","noopener,noreferrer"),a.classList.remove("hidden")}).catch(h=>{i.textContent=h instanceof Error?h.message:String(h)}).finally(()=>{c()})},{disabled:!n.configured,reason:"This site was built without a Dropbox app key, so there is no app to sign in to."});s.replaceChildren(p,...n.configured?[]:[e("p",{class:"hint",text:"Signing in to Dropbox is not set up on this site: it needs a Dropbox app key, which the site’s maintainer provides as DROPBOX_APP_KEY (docs/deploy.md)."})])};c();const r=()=>{i.textContent="",j(o.value).then(()=>{o.value="",a.classList.add("hidden")}).catch(n=>{i.textContent=n instanceof Error?n.message:String(n)}).finally(()=>{c()})};a.appendChild(e("div",{class:"bucket-actions"},g("finish",r,{class:"btn btn-primary"})));const d=H(()=>{if(!s.isConnected){d();return}c()});return e("div",{class:"connect-section"},v("Dropbox","dropbox",ce()),s,a,i)}function ce(){return["Sign in with your own Dropbox account to browse it in the Files panel.",`The app is given ${K.join(" and ")}: read access to everything in your Dropbox, or to the app’s own folder inside it if the site registered it that way.`,"The token lives only in this tab’s memory — never written to storage, gone when the tab closes, expiring on its own. A file already open keeps reading for the four hours its link lasts. No account information is asked for, so this page does not know whose Dropbox it is."]}function le(t){return new Date(t).toLocaleTimeString()}function ke(t){return new z({title:"Connect a folder",variant:"modal-wide",...t.onClose===void 0?{}:{onClose:t.onClose},body:s=>pe(t,s)})}function de(){const t={local:he,bucket:be,drive:fe,dropbox:s=>ae(s)??e("div")};return U().map(s=>({...s,pane:t[s.id]}))}function pe(t,s){const i=r=>{s(),t.onConnected(r)},o=de(),l=e("div",{class:"connect-pane"}),a=e("div",{class:"connect-providers",attrs:{role:"tablist"}}),c=r=>{const d=o.find(n=>n.id===r)??o[0];d.id;for(const n of a.children){const p=n.getAttribute("data-provider")===d.id;n.classList.toggle("is-on",p),n.setAttribute("aria-selected",String(p))}l.replaceChildren(d.pane(i))};for(const r of o){const d=e("button",{class:"connect-provider",type:"button",attrs:{role:"tab","data-provider":r.id,"aria-selected":"false"}},f(r.mark,{size:22}),e("span",{text:r.label}));d.addEventListener("click",()=>c(r.id)),a.appendChild(d)}return c(t.provider??"local"),e("div",{class:"modal-body bucket-body connect-body"},a,l,ye())}function he(t){const s=W(),i=X(),o=e("p",{class:"hint bucket-error"}),l=async()=>{o.textContent="";const a=await Z();a!==null&&t(a)};return e("div",{class:"connect-section"},v("This machine","hard-drive",ue(s,i)),e("div",{class:"connect-signin"},y(Y(),"hard-drive",()=>void l(),{disabled:!1})),o)}function ue(t,s){return["Read in place, never uploaded. The folder is listed one level at a time in the Files panel, and no byte of it is sent anywhere.",s?"The app keeps permission to list the folder across restarts, and the workspaces you save say which of your files are in it, so reopening one finds them there. Take it back any time under What this browser is remembering (Ctrl+P → Help).":t?"In a browser tab the folder is connected for this visit and kept nowhere, so nobody else at this browser finds it. Installed as an app — or in the desktop app — the folder is kept across restarts, and saved workspaces find their files in it again.":"This browser cannot keep that permission, so what you get is a snapshot of the file list for this visit only — it does not update and it does not survive a reload. Chrome, Edge and the desktop app keep the connection when installed as an app."]}function be(t){return e("div",{class:"connect-section"},v("A bucket","cloud",ge()),ne({onSaved:s=>t(Q(s.id))}))}function ge(){return["An https:// or s3:// prefix on S3, R2, MinIO, Google Cloud Storage or anything else that answers ListObjectsV2. Your browser reads it directly, with no server in between, so the bucket — not this page — decides what may be read. Two things have to be set.","CORS first, without which no read starts at all. Range and Content-Range are the pair that matters: a .mcool is read in pieces and never whole, so a bucket that will not expose them serves files this viewer cannot open. Narrow AllowedOrigins to the site once it works.",S(`[{
2
2
  "AllowedOrigins": ["*"],
3
3
  "AllowedMethods": ["GET", "HEAD"],
4
4
  "AllowedHeaders": ["Range", "Content-Type"],
@@ -1 +1 @@
1
- import{e as l,b as v,B as A,i as f,C as O}from"./index-BZqg8J4S.js";const W=700,P=1500,_=500,z=150,y=6,c=14,d=8,$=18,H="Not on screen yet. If it does not appear, go back a step.",q={press:{icon:"mouse-pointer-click",word:"press"},type:{icon:"keyboard",word:"type"},choose:{icon:"chevrons-up-down",word:"choose"},drag:{icon:"move",word:"drag"},look:{icon:"eye",word:"look"}},R='input, textarea, select, canvas, [contenteditable], [role="slider"], [role="listbox"], [role="menu"], [role="grid"], [role="tablist"]';class Q{el;steps;options;veil=l("div",{class:"tour-veil"});ring=l("div",{class:"tour-ring hidden"});blocks=[0,1,2,3].map(()=>l("div",{class:"tour-block hidden"}));card;tail=l("span",{class:"tour-tail",attrs:{"aria-hidden":"true"}});chapterEl=l("span",{class:"tour-chapter"});countEl=l("span",{class:"tour-count"});segments;chipEl=l("span",{class:"tour-chip",attrs:{"aria-hidden":"true"}});badgeEl=l("span",{class:"tour-badge hidden",attrs:{"aria-hidden":"true"}});titleEl=l("h2",{class:"tour-title",id:"tour-title"});textEl=l("p",{class:"tour-text",id:"tour-text"});noteEl=l("p",{class:"tour-note hidden"});agendaEl=l("ol",{class:"tour-agenda hidden"});assistSlot=l("span",{class:"tour-assist"});backButton;nextButton;nextLabel=l("span");index=0;entry=N();target=null;spot=null;clips=[];painted="";chip=null;placed=!1;timer=null;frame=null;soonTimer=null;nudgeTimer=null;running=!1;constructor(t){this.options=t,this.steps=t.steps,this.backButton=v("back",()=>this.previous(),{class:"btn btn-quiet tour-back",title:A("The step before this one","←")}),this.nextButton=l("button",{class:"btn btn-primary tour-next",type:"button"},this.nextLabel,f("arrow-right",{size:14,class:"tour-next-arrow"})),this.nextButton.addEventListener("click",()=>this.next());const i=v("skip tutorial",()=>this.end("skipped"),{class:"btn btn-quiet tour-skip",title:"Leave the tutorial. About picks it up where you left it."});this.segments=B(this.steps).map(e=>{const n=l("span",{class:"tour-segment-fill"}),r=l("span",{class:"tour-segment",attrs:{title:e.chapter}},n);return r.style.flexGrow=String(e.count),{from:e.from,count:e.count,fill:n,el:r}}),this.card=l("div",{class:"tour-card",attrs:{role:"dialog","aria-modal":"false","aria-labelledby":"tour-title","aria-describedby":"tour-text"}},this.tail,l("div",{class:"tour-head"},this.chapterEl,this.countEl,i),l("div",{class:"tour-track",attrs:{"aria-hidden":"true"}},...this.segments.map(e=>e.el)),l("div",{class:"tour-body"},this.badgeEl,l("div",{class:"tour-heading"},this.chipEl,this.titleEl),l("div",{class:"tour-live",attrs:{"aria-live":"polite"}},this.textEl,this.noteEl),this.agendaEl),l("div",{class:"tour-actions"},this.assistSlot,this.backButton,this.nextButton));for(const e of this.blocks){for(const n of["pointerdown","mousedown","click","dblclick","contextmenu","touchstart"])e.addEventListener(n,r=>{r.preventDefault(),r.stopPropagation(),n==="pointerdown"&&this.nudge()});e.addEventListener("wheel",n=>n.stopPropagation(),{passive:!0})}this.el=l("div",{class:"tour"},this.veil,this.ring,...this.blocks,this.card)}get at(){return this.index}start(){if(!this.running){this.running=!0,document.body.appendChild(this.el);for(const t of["click","input","change","keyup","pointerup"])window.addEventListener(t,this.soon,!0);window.addEventListener("keydown",this.onKey),this.timer=setInterval(()=>this.tick(),z),this.loop(),this.go(this.options.start,!1)}}stop(){if(this.running){this.running=!1;for(const t of["click","input","change","keyup","pointerup"])window.removeEventListener(t,this.soon,!0);window.removeEventListener("keydown",this.onKey),this.timer!==null&&clearInterval(this.timer),this.frame!==null&&cancelAnimationFrame(this.frame),this.soonTimer!==null&&clearTimeout(this.soonTimer),this.nudgeTimer!==null&&clearTimeout(this.nudgeTimer),this.timer=null,this.frame=null,this.el.remove()}}go(t,i){let e=Math.max(0,Math.min(this.steps.length-1,t));if(!i)for(;e<this.steps.length-1&&this.isDone(this.steps[e]);)e++;this.index=e;const n=this.steps[e];this.entry=N(),this.entry.was=this.isDone(n),this.target=null,this.spot=null,this.clips=[],this.painted="",n.prepare?.(),this.paintCard(),this.card.classList.remove("is-entering"),this.card.offsetWidth,this.card.classList.add("is-entering"),n.hero===!0&&this.nextButton.focus({preventScroll:!0}),this.options.onStep(n,e),this.tick()}next(){if(this.index>=this.steps.length-1){this.end("finished");return}this.go(this.index+1,!1)}previous(){this.index>0&&this.go(this.index-1,!0)}end(t){const i=this.index;this.stop(),this.options.onEnd(t,i)}isDone(t){try{return t.done?.()===!0}catch{return!1}}waiting(){return this.steps[this.index].done!==void 0&&!this.entry.was&&this.index<this.steps.length-1}onKey=t=>{if(t.defaultPrevented||t.altKey||t.ctrlKey||t.metaKey||t.shiftKey||t.key!=="ArrowRight"&&t.key!=="ArrowLeft"||this.options.yielding?.()===!0)return;const i=t.target;i instanceof Element&&i.closest(R)!==null||(t.key==="ArrowLeft"?(t.preventDefault(),this.previous()):this.waiting()||(t.preventDefault(),this.next()))};soon=()=>{this.soonTimer===null&&(this.soonTimer=setTimeout(()=>{this.soonTimer=null,this.tick()},0))};tick(){if(!this.running)return;const t=this.options.yielding?.()===!0;if(this.el.classList.toggle("is-yielding",t),t)return;const i=this.steps[this.index],e=performance.now(),n=this.isDone(i);if(i.needs!==void 0&&!n&&!L(()=>i.needs?.holds()===!0)){if(this.entry.failingSince??=e,e-this.entry.failingSince>=_){const o=this.steps.findIndex(h=>h.id===i.needs?.back);if(o>=0&&o!==this.index){this.go(o,!1);return}}}else this.entry.failingSince=null;if(i.done!==void 0){n&&!this.entry.was&&(this.entry.doneAt=e),n||(this.entry.doneAt=null),this.entry.was=n;const o=this.index===this.steps.length-1;if(n&&!o&&this.entry.doneAt!==null&&e-this.entry.doneAt>=W){this.next();return}}const r=i.mistake===void 0?null:L(()=>i.mistake?.()??null);r!==this.entry.mistake&&(this.entry.mistake=r,this.blink(r!==null)),this.find(i,e),this.paintCard()}find(t,i){const e=t.target===void 0?null:L(()=>t.target?.()??null),n=e===null?null:I(e)?e.within:e;if(this.spot=e!==null&&I(e)?e.box:null,n!==this.target&&(this.target=n,this.clips=n===null?[]:Y(n)),n!==null){if(!this.entry.scrolled){this.entry.scrolled=!0;const r=this.lit(),o=M(n.getBoundingClientRect()),h=this.spot===null?o:w(this.spot,o);if(h!==null&&(r===null||K(r)<K(h)-1)){const a=h.bottom-h.top>window.innerHeight*.6;n.scrollIntoView({block:a?"start":"center",inline:"nearest"})}}t.focus===!0&&!this.entry.focused&&n instanceof HTMLElement&&(this.entry.focused=!0,n.focus({preventScroll:!0})),this.lit()!==null&&(this.entry.seenAt=i)}}lit(){const t=F(this.target,this.clips);return t===null||this.spot===null?t:w(t,this.spot)}paintCard(){const t=this.steps[this.index],i=this.index===this.steps.length-1,e=t.hero===!0,n=t.done!==void 0&&this.entry.was;g(this.chapterEl,t.chapter),g(this.countEl,`step ${this.index+1} of ${this.steps.length}`);for(const a of this.segments){const b=`${Math.max(0,Math.min(1,(this.index-a.from+1)/a.count))*100}%`;a.fill.style.width!==b&&(a.fill.style.width=b)}g(this.titleEl,t.title),this.paintText(t.text),this.card.dataset.step!==t.id&&(this.card.dataset.step=t.id),this.card.classList.toggle("is-hero",e),this.el.classList.toggle("is-done",n),this.el.classList.toggle("is-waiting",!e&&this.waiting()),this.paintChip(e?"":n?"done":t.action??(t.done===void 0?"look":"press")),this.badgeEl.classList.toggle("hidden",!(e&&i)),e&&i&&this.badgeEl.childElementCount===0&&this.badgeEl.append(f("check",{size:26})),this.paintAgenda(t);const r=t.target!==void 0&&t.done!==void 0&&!this.entry.was&&this.lit()===null&&performance.now()-this.entry.seenAt>=P;if(g(this.noteEl,r?t.missing??H:""),this.noteEl.classList.toggle("hidden",!r),this.assistSlot.dataset.step!==t.id){this.assistSlot.dataset.step=t.id;const a=t.assist;this.assistSlot.replaceChildren(...a===void 0?[]:[v(a.label,()=>a.run(),{class:"btn btn-quiet",title:a.title})])}this.backButton.disabled=this.index===0,this.backButton.classList.toggle("hidden",this.index===0);const o=this.waiting();g(this.nextLabel,t.next??(i?"finish":o?"skip step":"next"));const h=o?"btn btn-quiet tour-next":"btn btn-primary tour-next";this.nextButton.className!==h&&(this.nextButton.className=h),this.nextButton.classList.toggle("is-last",i),this.nextButton.title=o?"Go on without doing this step. The steps after it may need it.":i?"Close the tutorial":A("The next step","→")}paintChip(t){if(t===this.chip)return;if(this.chip=t,this.chipEl.classList.toggle("hidden",t===""),this.chipEl.classList.toggle("is-done",t==="done"),t===""){this.chipEl.replaceChildren();return}const i=t==="done"?null:q[t];this.chipEl.replaceChildren(f(i===null?"check":i.icon,{size:14})),this.chipEl.title=i===null?"done":i.word}paintAgenda(t){const i=t.agenda===!0;if(this.agendaEl.classList.toggle("hidden",!i),!i||this.agendaEl.dataset.step===t.id)return;this.agendaEl.dataset.step=t.id;const e=B(this.steps).filter(n=>this.steps.slice(n.from,n.from+n.count).some(r=>r.hero!==!0));this.agendaEl.replaceChildren(...e.map(n=>l("li",{text:n.chapter})))}paintText(t){const i=typeof t=="string"?t:JSON.stringify(t);this.textEl.dataset.text!==i&&(this.textEl.dataset.text=i,this.textEl.replaceChildren(...(typeof t=="string"?[t]:t).flatMap(e=>{if(typeof e=="string")return[document.createTextNode(e)];const n=l("code",{class:"tour-name",text:e.name});return e.copy===!0?[n,G(e.name,n)]:[n]})))}blink(t){for(const i of this.textEl.querySelectorAll(".tour-name"))i.classList.toggle("is-wrong",t),i.classList.remove("is-blinking"),t&&(i.offsetWidth,i.classList.add("is-blinking"))}loop(){this.frame=requestAnimationFrame(()=>{this.running&&(this.place(),this.loop())})}place(){const i=this.steps[this.index].hero===!0,e=i?null:this.lit(),n=window.innerWidth,r=window.innerHeight,o=e===null?null:{left:Math.max(0,e.left-y),top:Math.max(0,e.top-y),right:Math.min(n,e.right+y),bottom:Math.min(r,e.bottom+y)},h=this.card.offsetWidth,a=this.card.offsetHeight,x=(i?"hero:":"")+(o===null?`none:${n}:${r}:${h}:${a}`:`${Math.round(o.left)}:${Math.round(o.top)}:${Math.round(o.right)}:${Math.round(o.bottom)}:${n}:${r}:${h}:${a}`);if(x===this.painted)return;this.painted=x;const b=o!==null&&this.ring.classList.contains("hidden");this.veil.classList.toggle("hidden",o!==null),this.ring.classList.toggle("hidden",o===null);for(const p of this.blocks)p.classList.toggle("hidden",o===null);if(o!==null){D(this.ring,b,()=>m(this.ring,o));const[p,k,C,E]=this.blocks;m(p,{left:0,top:0,right:n,bottom:o.top}),m(k,{left:o.right,top:o.top,right:n,bottom:o.bottom}),m(C,{left:0,top:o.bottom,right:n,bottom:r}),m(E,{left:0,top:o.top,right:o.left,bottom:o.bottom})}const T={width:h,height:a},S={width:n,height:r},u=i?V(T,S):J(T,o,S);if(D(this.card,!this.placed,()=>{this.card.style.left=`${Math.round(u.left)}px`,this.card.style.top=`${Math.round(u.top)}px`}),this.placed=!0,this.card.dataset.side!==u.side&&(this.card.dataset.side=u.side),o!==null){const p=u.side==="below"||u.side==="above",k=p?(o.left+o.right)/2-u.left:(o.top+o.bottom)/2-u.top,E=Math.max($,Math.min((p?h:a)-$,k));this.card.style.setProperty("--tail-at",`${Math.round(E)}px`)}}nudge(){this.ring.classList.remove("is-nudged"),this.ring.offsetWidth,this.ring.classList.add("is-nudged"),this.nudgeTimer!==null&&clearTimeout(this.nudgeTimer),this.nudgeTimer=setTimeout(()=>this.ring.classList.remove("is-nudged"),700)}}function U(s,t){let i=s.findIndex(e=>e.id===t);if(i<0)return 0;for(;i>0&&s[i-1].chapter===s[i].chapter;)i--;return i}function B(s){const t=[];return s.forEach((i,e)=>{const n=t[t.length-1];n!==void 0&&n.chapter===i.chapter?n.count++:t.push({chapter:i.chapter,from:e,count:1})}),t}function N(){return{was:!1,doneAt:null,seenAt:performance.now(),failingSince:null,scrolled:!1,focused:!1,mistake:null}}function I(s){return!(s instanceof Element)}function g(s,t){s.textContent!==t&&(s.textContent=t)}function G(s,t){const i=`Copy ${s}`,e=l("button",{class:"tour-copy",type:"button",title:i,attrs:{"aria-label":i}},f("copy",{size:13}));let n=null;return e.addEventListener("click",()=>{O(s).then(()=>{e.classList.add("is-copied"),e.replaceChildren(f("check",{size:13})),e.title="Copied",n!==null&&clearTimeout(n),n=setTimeout(()=>{e.classList.remove("is-copied"),e.replaceChildren(f("copy",{size:13})),e.title=i},1400)},()=>{window.getSelection()?.selectAllChildren(t),e.title="Selected: copy it with the keyboard"})}),e}function L(s){try{return s()}catch{return null}}function D(s,t,i){if(!t){i();return}s.classList.add("is-snapping"),i(),s.offsetWidth,s.classList.remove("is-snapping")}function M(s){return{left:s.left,top:s.top,right:s.right,bottom:s.bottom}}function K(s){return Math.max(0,s.right-s.left)*Math.max(0,s.bottom-s.top)}function m(s,t){s.style.left=`${t.left}px`,s.style.top=`${t.top}px`,s.style.width=`${Math.max(0,t.right-t.left)}px`,s.style.height=`${Math.max(0,t.bottom-t.top)}px`}function Y(s){const t=[];for(let i=s.parentElement;i!==null&&i!==document.body;i=i.parentElement){const e=window.getComputedStyle(i);(e.overflowX!=="visible"||e.overflowY!=="visible")&&t.push(i)}return t}function X(s,t){if(!s.isConnected)return null;const i=s.getBoundingClientRect();if(i.width<=0||i.height<=0||s instanceof HTMLElement&&window.getComputedStyle(s).visibility==="hidden")return null;let e=M(i);for(const n of t)if(e=w(e,M(n.getBoundingClientRect())),e===null)return null;return w(e,{left:0,top:0,right:window.innerWidth,bottom:window.innerHeight})}function F(s,t){return s===null?null:X(s,t)}function w(s,t){const i={left:Math.max(s.left,t.left),top:Math.max(s.top,t.top),right:Math.min(s.right,t.right),bottom:Math.min(s.bottom,t.bottom)};return i.right-i.left<1||i.bottom-i.top<1?null:i}function J(s,t,i){const e=h=>Math.max(d,Math.min(i.width-s.width-d,h)),n=h=>Math.max(d,Math.min(i.height-s.height-d,h));if(t===null)return{left:e((i.width-s.width)/2),top:n(i.height-s.height-3*c),side:"none"};const r=(t.left+t.right)/2-s.width/2,o=(t.top+t.bottom)/2-s.height/2;return t.bottom+c+s.height<=i.height-d?{left:e(r),top:t.bottom+c,side:"below"}:t.top-c-s.height>=d?{left:e(r),top:t.top-c-s.height,side:"above"}:t.right+c+s.width<=i.width-d?{left:t.right+c,top:n(o),side:"right"}:t.left-c-s.width>=d?{left:t.left-c-s.width,top:n(o),side:"left"}:{left:e(i.width-s.width-2*c),top:n(i.height-s.height-2*c),side:"over"}}function V(s,t){return{left:Math.max(d,(t.width-s.width)/2),top:Math.max(d,(t.height-s.height)/2-t.height*.06),side:"none"}}export{Q as Tour,J as cardPlace,Y as clipsOf,V as heroPlace,B as partsOf,U as resumeIndex,X as shownBox};
1
+ import{e as l,b as v,B as A,i as f,C as O}from"./index-Cys9FWNc.js";const W=700,P=1500,_=500,z=150,y=6,c=14,d=8,$=18,H="Not on screen yet. If it does not appear, go back a step.",q={press:{icon:"mouse-pointer-click",word:"press"},type:{icon:"keyboard",word:"type"},choose:{icon:"chevrons-up-down",word:"choose"},drag:{icon:"move",word:"drag"},look:{icon:"eye",word:"look"}},R='input, textarea, select, canvas, [contenteditable], [role="slider"], [role="listbox"], [role="menu"], [role="grid"], [role="tablist"]';class Q{el;steps;options;veil=l("div",{class:"tour-veil"});ring=l("div",{class:"tour-ring hidden"});blocks=[0,1,2,3].map(()=>l("div",{class:"tour-block hidden"}));card;tail=l("span",{class:"tour-tail",attrs:{"aria-hidden":"true"}});chapterEl=l("span",{class:"tour-chapter"});countEl=l("span",{class:"tour-count"});segments;chipEl=l("span",{class:"tour-chip",attrs:{"aria-hidden":"true"}});badgeEl=l("span",{class:"tour-badge hidden",attrs:{"aria-hidden":"true"}});titleEl=l("h2",{class:"tour-title",id:"tour-title"});textEl=l("p",{class:"tour-text",id:"tour-text"});noteEl=l("p",{class:"tour-note hidden"});agendaEl=l("ol",{class:"tour-agenda hidden"});assistSlot=l("span",{class:"tour-assist"});backButton;nextButton;nextLabel=l("span");index=0;entry=N();target=null;spot=null;clips=[];painted="";chip=null;placed=!1;timer=null;frame=null;soonTimer=null;nudgeTimer=null;running=!1;constructor(t){this.options=t,this.steps=t.steps,this.backButton=v("back",()=>this.previous(),{class:"btn btn-quiet tour-back",title:A("The step before this one","←")}),this.nextButton=l("button",{class:"btn btn-primary tour-next",type:"button"},this.nextLabel,f("arrow-right",{size:14,class:"tour-next-arrow"})),this.nextButton.addEventListener("click",()=>this.next());const i=v("skip tutorial",()=>this.end("skipped"),{class:"btn btn-quiet tour-skip",title:"Leave the tutorial. About picks it up where you left it."});this.segments=B(this.steps).map(e=>{const n=l("span",{class:"tour-segment-fill"}),r=l("span",{class:"tour-segment",attrs:{title:e.chapter}},n);return r.style.flexGrow=String(e.count),{from:e.from,count:e.count,fill:n,el:r}}),this.card=l("div",{class:"tour-card",attrs:{role:"dialog","aria-modal":"false","aria-labelledby":"tour-title","aria-describedby":"tour-text"}},this.tail,l("div",{class:"tour-head"},this.chapterEl,this.countEl,i),l("div",{class:"tour-track",attrs:{"aria-hidden":"true"}},...this.segments.map(e=>e.el)),l("div",{class:"tour-body"},this.badgeEl,l("div",{class:"tour-heading"},this.chipEl,this.titleEl),l("div",{class:"tour-live",attrs:{"aria-live":"polite"}},this.textEl,this.noteEl),this.agendaEl),l("div",{class:"tour-actions"},this.assistSlot,this.backButton,this.nextButton));for(const e of this.blocks){for(const n of["pointerdown","mousedown","click","dblclick","contextmenu","touchstart"])e.addEventListener(n,r=>{r.preventDefault(),r.stopPropagation(),n==="pointerdown"&&this.nudge()});e.addEventListener("wheel",n=>n.stopPropagation(),{passive:!0})}this.el=l("div",{class:"tour"},this.veil,this.ring,...this.blocks,this.card)}get at(){return this.index}start(){if(!this.running){this.running=!0,document.body.appendChild(this.el);for(const t of["click","input","change","keyup","pointerup"])window.addEventListener(t,this.soon,!0);window.addEventListener("keydown",this.onKey),this.timer=setInterval(()=>this.tick(),z),this.loop(),this.go(this.options.start,!1)}}stop(){if(this.running){this.running=!1;for(const t of["click","input","change","keyup","pointerup"])window.removeEventListener(t,this.soon,!0);window.removeEventListener("keydown",this.onKey),this.timer!==null&&clearInterval(this.timer),this.frame!==null&&cancelAnimationFrame(this.frame),this.soonTimer!==null&&clearTimeout(this.soonTimer),this.nudgeTimer!==null&&clearTimeout(this.nudgeTimer),this.timer=null,this.frame=null,this.el.remove()}}go(t,i){let e=Math.max(0,Math.min(this.steps.length-1,t));if(!i)for(;e<this.steps.length-1&&this.isDone(this.steps[e]);)e++;this.index=e;const n=this.steps[e];this.entry=N(),this.entry.was=this.isDone(n),this.target=null,this.spot=null,this.clips=[],this.painted="",n.prepare?.(),this.paintCard(),this.card.classList.remove("is-entering"),this.card.offsetWidth,this.card.classList.add("is-entering"),n.hero===!0&&this.nextButton.focus({preventScroll:!0}),this.options.onStep(n,e),this.tick()}next(){if(this.index>=this.steps.length-1){this.end("finished");return}this.go(this.index+1,!1)}previous(){this.index>0&&this.go(this.index-1,!0)}end(t){const i=this.index;this.stop(),this.options.onEnd(t,i)}isDone(t){try{return t.done?.()===!0}catch{return!1}}waiting(){return this.steps[this.index].done!==void 0&&!this.entry.was&&this.index<this.steps.length-1}onKey=t=>{if(t.defaultPrevented||t.altKey||t.ctrlKey||t.metaKey||t.shiftKey||t.key!=="ArrowRight"&&t.key!=="ArrowLeft"||this.options.yielding?.()===!0)return;const i=t.target;i instanceof Element&&i.closest(R)!==null||(t.key==="ArrowLeft"?(t.preventDefault(),this.previous()):this.waiting()||(t.preventDefault(),this.next()))};soon=()=>{this.soonTimer===null&&(this.soonTimer=setTimeout(()=>{this.soonTimer=null,this.tick()},0))};tick(){if(!this.running)return;const t=this.options.yielding?.()===!0;if(this.el.classList.toggle("is-yielding",t),t)return;const i=this.steps[this.index],e=performance.now(),n=this.isDone(i);if(i.needs!==void 0&&!n&&!L(()=>i.needs?.holds()===!0)){if(this.entry.failingSince??=e,e-this.entry.failingSince>=_){const o=this.steps.findIndex(h=>h.id===i.needs?.back);if(o>=0&&o!==this.index){this.go(o,!1);return}}}else this.entry.failingSince=null;if(i.done!==void 0){n&&!this.entry.was&&(this.entry.doneAt=e),n||(this.entry.doneAt=null),this.entry.was=n;const o=this.index===this.steps.length-1;if(n&&!o&&this.entry.doneAt!==null&&e-this.entry.doneAt>=W){this.next();return}}const r=i.mistake===void 0?null:L(()=>i.mistake?.()??null);r!==this.entry.mistake&&(this.entry.mistake=r,this.blink(r!==null)),this.find(i,e),this.paintCard()}find(t,i){const e=t.target===void 0?null:L(()=>t.target?.()??null),n=e===null?null:I(e)?e.within:e;if(this.spot=e!==null&&I(e)?e.box:null,n!==this.target&&(this.target=n,this.clips=n===null?[]:Y(n)),n!==null){if(!this.entry.scrolled){this.entry.scrolled=!0;const r=this.lit(),o=M(n.getBoundingClientRect()),h=this.spot===null?o:w(this.spot,o);if(h!==null&&(r===null||K(r)<K(h)-1)){const a=h.bottom-h.top>window.innerHeight*.6;n.scrollIntoView({block:a?"start":"center",inline:"nearest"})}}t.focus===!0&&!this.entry.focused&&n instanceof HTMLElement&&(this.entry.focused=!0,n.focus({preventScroll:!0})),this.lit()!==null&&(this.entry.seenAt=i)}}lit(){const t=F(this.target,this.clips);return t===null||this.spot===null?t:w(t,this.spot)}paintCard(){const t=this.steps[this.index],i=this.index===this.steps.length-1,e=t.hero===!0,n=t.done!==void 0&&this.entry.was;g(this.chapterEl,t.chapter),g(this.countEl,`step ${this.index+1} of ${this.steps.length}`);for(const a of this.segments){const b=`${Math.max(0,Math.min(1,(this.index-a.from+1)/a.count))*100}%`;a.fill.style.width!==b&&(a.fill.style.width=b)}g(this.titleEl,t.title),this.paintText(t.text),this.card.dataset.step!==t.id&&(this.card.dataset.step=t.id),this.card.classList.toggle("is-hero",e),this.el.classList.toggle("is-done",n),this.el.classList.toggle("is-waiting",!e&&this.waiting()),this.paintChip(e?"":n?"done":t.action??(t.done===void 0?"look":"press")),this.badgeEl.classList.toggle("hidden",!(e&&i)),e&&i&&this.badgeEl.childElementCount===0&&this.badgeEl.append(f("check",{size:26})),this.paintAgenda(t);const r=t.target!==void 0&&t.done!==void 0&&!this.entry.was&&this.lit()===null&&performance.now()-this.entry.seenAt>=P;if(g(this.noteEl,r?t.missing??H:""),this.noteEl.classList.toggle("hidden",!r),this.assistSlot.dataset.step!==t.id){this.assistSlot.dataset.step=t.id;const a=t.assist;this.assistSlot.replaceChildren(...a===void 0?[]:[v(a.label,()=>a.run(),{class:"btn btn-quiet",title:a.title})])}this.backButton.disabled=this.index===0,this.backButton.classList.toggle("hidden",this.index===0);const o=this.waiting();g(this.nextLabel,t.next??(i?"finish":o?"skip step":"next"));const h=o?"btn btn-quiet tour-next":"btn btn-primary tour-next";this.nextButton.className!==h&&(this.nextButton.className=h),this.nextButton.classList.toggle("is-last",i),this.nextButton.title=o?"Go on without doing this step. The steps after it may need it.":i?"Close the tutorial":A("The next step","→")}paintChip(t){if(t===this.chip)return;if(this.chip=t,this.chipEl.classList.toggle("hidden",t===""),this.chipEl.classList.toggle("is-done",t==="done"),t===""){this.chipEl.replaceChildren();return}const i=t==="done"?null:q[t];this.chipEl.replaceChildren(f(i===null?"check":i.icon,{size:14})),this.chipEl.title=i===null?"done":i.word}paintAgenda(t){const i=t.agenda===!0;if(this.agendaEl.classList.toggle("hidden",!i),!i||this.agendaEl.dataset.step===t.id)return;this.agendaEl.dataset.step=t.id;const e=B(this.steps).filter(n=>this.steps.slice(n.from,n.from+n.count).some(r=>r.hero!==!0));this.agendaEl.replaceChildren(...e.map(n=>l("li",{text:n.chapter})))}paintText(t){const i=typeof t=="string"?t:JSON.stringify(t);this.textEl.dataset.text!==i&&(this.textEl.dataset.text=i,this.textEl.replaceChildren(...(typeof t=="string"?[t]:t).flatMap(e=>{if(typeof e=="string")return[document.createTextNode(e)];const n=l("code",{class:"tour-name",text:e.name});return e.copy===!0?[n,G(e.name,n)]:[n]})))}blink(t){for(const i of this.textEl.querySelectorAll(".tour-name"))i.classList.toggle("is-wrong",t),i.classList.remove("is-blinking"),t&&(i.offsetWidth,i.classList.add("is-blinking"))}loop(){this.frame=requestAnimationFrame(()=>{this.running&&(this.place(),this.loop())})}place(){const i=this.steps[this.index].hero===!0,e=i?null:this.lit(),n=window.innerWidth,r=window.innerHeight,o=e===null?null:{left:Math.max(0,e.left-y),top:Math.max(0,e.top-y),right:Math.min(n,e.right+y),bottom:Math.min(r,e.bottom+y)},h=this.card.offsetWidth,a=this.card.offsetHeight,x=(i?"hero:":"")+(o===null?`none:${n}:${r}:${h}:${a}`:`${Math.round(o.left)}:${Math.round(o.top)}:${Math.round(o.right)}:${Math.round(o.bottom)}:${n}:${r}:${h}:${a}`);if(x===this.painted)return;this.painted=x;const b=o!==null&&this.ring.classList.contains("hidden");this.veil.classList.toggle("hidden",o!==null),this.ring.classList.toggle("hidden",o===null);for(const p of this.blocks)p.classList.toggle("hidden",o===null);if(o!==null){D(this.ring,b,()=>m(this.ring,o));const[p,k,C,E]=this.blocks;m(p,{left:0,top:0,right:n,bottom:o.top}),m(k,{left:o.right,top:o.top,right:n,bottom:o.bottom}),m(C,{left:0,top:o.bottom,right:n,bottom:r}),m(E,{left:0,top:o.top,right:o.left,bottom:o.bottom})}const T={width:h,height:a},S={width:n,height:r},u=i?V(T,S):J(T,o,S);if(D(this.card,!this.placed,()=>{this.card.style.left=`${Math.round(u.left)}px`,this.card.style.top=`${Math.round(u.top)}px`}),this.placed=!0,this.card.dataset.side!==u.side&&(this.card.dataset.side=u.side),o!==null){const p=u.side==="below"||u.side==="above",k=p?(o.left+o.right)/2-u.left:(o.top+o.bottom)/2-u.top,E=Math.max($,Math.min((p?h:a)-$,k));this.card.style.setProperty("--tail-at",`${Math.round(E)}px`)}}nudge(){this.ring.classList.remove("is-nudged"),this.ring.offsetWidth,this.ring.classList.add("is-nudged"),this.nudgeTimer!==null&&clearTimeout(this.nudgeTimer),this.nudgeTimer=setTimeout(()=>this.ring.classList.remove("is-nudged"),700)}}function U(s,t){let i=s.findIndex(e=>e.id===t);if(i<0)return 0;for(;i>0&&s[i-1].chapter===s[i].chapter;)i--;return i}function B(s){const t=[];return s.forEach((i,e)=>{const n=t[t.length-1];n!==void 0&&n.chapter===i.chapter?n.count++:t.push({chapter:i.chapter,from:e,count:1})}),t}function N(){return{was:!1,doneAt:null,seenAt:performance.now(),failingSince:null,scrolled:!1,focused:!1,mistake:null}}function I(s){return!(s instanceof Element)}function g(s,t){s.textContent!==t&&(s.textContent=t)}function G(s,t){const i=`Copy ${s}`,e=l("button",{class:"tour-copy",type:"button",title:i,attrs:{"aria-label":i}},f("copy",{size:13}));let n=null;return e.addEventListener("click",()=>{O(s).then(()=>{e.classList.add("is-copied"),e.replaceChildren(f("check",{size:13})),e.title="Copied",n!==null&&clearTimeout(n),n=setTimeout(()=>{e.classList.remove("is-copied"),e.replaceChildren(f("copy",{size:13})),e.title=i},1400)},()=>{window.getSelection()?.selectAllChildren(t),e.title="Selected: copy it with the keyboard"})}),e}function L(s){try{return s()}catch{return null}}function D(s,t,i){if(!t){i();return}s.classList.add("is-snapping"),i(),s.offsetWidth,s.classList.remove("is-snapping")}function M(s){return{left:s.left,top:s.top,right:s.right,bottom:s.bottom}}function K(s){return Math.max(0,s.right-s.left)*Math.max(0,s.bottom-s.top)}function m(s,t){s.style.left=`${t.left}px`,s.style.top=`${t.top}px`,s.style.width=`${Math.max(0,t.right-t.left)}px`,s.style.height=`${Math.max(0,t.bottom-t.top)}px`}function Y(s){const t=[];for(let i=s.parentElement;i!==null&&i!==document.body;i=i.parentElement){const e=window.getComputedStyle(i);(e.overflowX!=="visible"||e.overflowY!=="visible")&&t.push(i)}return t}function X(s,t){if(!s.isConnected)return null;const i=s.getBoundingClientRect();if(i.width<=0||i.height<=0||s instanceof HTMLElement&&window.getComputedStyle(s).visibility==="hidden")return null;let e=M(i);for(const n of t)if(e=w(e,M(n.getBoundingClientRect())),e===null)return null;return w(e,{left:0,top:0,right:window.innerWidth,bottom:window.innerHeight})}function F(s,t){return s===null?null:X(s,t)}function w(s,t){const i={left:Math.max(s.left,t.left),top:Math.max(s.top,t.top),right:Math.min(s.right,t.right),bottom:Math.min(s.bottom,t.bottom)};return i.right-i.left<1||i.bottom-i.top<1?null:i}function J(s,t,i){const e=h=>Math.max(d,Math.min(i.width-s.width-d,h)),n=h=>Math.max(d,Math.min(i.height-s.height-d,h));if(t===null)return{left:e((i.width-s.width)/2),top:n(i.height-s.height-3*c),side:"none"};const r=(t.left+t.right)/2-s.width/2,o=(t.top+t.bottom)/2-s.height/2;return t.bottom+c+s.height<=i.height-d?{left:e(r),top:t.bottom+c,side:"below"}:t.top-c-s.height>=d?{left:e(r),top:t.top-c-s.height,side:"above"}:t.right+c+s.width<=i.width-d?{left:t.right+c,top:n(o),side:"right"}:t.left-c-s.width>=d?{left:t.left-c-s.width,top:n(o),side:"left"}:{left:e(i.width-s.width-2*c),top:n(i.height-s.height-2*c),side:"over"}}function V(s,t){return{left:Math.max(d,(t.width-s.width)/2),top:Math.max(d,(t.height-s.height)/2-t.height*.06),side:"none"}}export{Q as Tour,J as cardPlace,Y as clipsOf,V as heroPlace,B as partsOf,U as resumeIndex,X as shownBox};