feat: rebuild the landing page around the product

The home page led with a text hero, a boxed stats strip, a numbered
how-it-works list, a trust card and three Bootstrap featurettes with
postage-stamp screenshots. It now shows the product first and stays
flat:

- Hero: copy on the left, the anonymize form in a browser frame on the
  right, one primary CTA plus a link to a real anonymized repository,
  and three reassurance points (free, read-only, expires).
- Proof and live stats collapse into hairline strips with no cards;
  the stat bars keep their daily-delta charts and the "+N today" line.
- Before/after example gets a section head and shows the actual XXXX-n
  masks, each labelled for screen readers.
- "What you get" is one large screenshot with three vertical tabs
  (Anonymize, Review, Manage) instead of three columns. Tabs are real
  buttons with tablist/tab/tabpanel roles, roving tabindex and arrow-key
  navigation; the description and link sit outside the button. Signed-out
  visitors are sent to sign in rather than to a dashboard that redirects.
- FAQ teaser with the three questions that come up most, linking into
  the FAQ anchors.
- Phone layout: tabs become a strip above the screenshot with the
  selected description below it, stats go two-up, panes stack, the CTA
  fills the width, and horizontal overflow is clipped so the hero bleed
  can never cause sideways scrolling.

Removes the dead how-it-works, trust, featurette and metrics styles and
the legacy mobile hero font overrides that fought the new sizes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
tdurieuxandClaude Fable 5.1 committed 2026-09-09 12:21:02 +02:00
1 parent b9189953d3
commit 88d8225f00
7 files changed
+887 -561

No files matched your search

+2 -2
View File
@@ -1,6 +1,6 @@
{
"core.min.js": "core.6332b3c288.min.js",
"vendor.min.js": "vendor.81d3dd59f8.min.js",
"vendor.min.js": "vendor.21205fda63.min.js",
"mermaid.min.js": "mermaid.f848a72d16.min.js",
"all.min.css": "all.6d950dfd4d.min.css"
"all.min.css": "all.08e150389d.min.css"
}
+1 -1
View File
@@ -6,4 +6,4 @@
*/:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unsLine truncated
* Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
*/.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:solid .08em #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-webkit-transform:scale(1,-1);transform:scale(1,-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1,-1);transform:scale(-1,-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-rotate-90{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{contentLine truncated
*/.fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:solid .08em #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-webkit-transform:scale(1,-1);transform:scale(1,-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1,-1);transform:scale(-1,-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-rotate-90{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{contentLine truncated
+549 -326
View File
File diff suppressed because it is too large. Load diff
+206 -230
View File
@@ -1,257 +1,233 @@
<div class="paper-landing">
<!-- Hero -->
<!-- Hero: copy on the left, the product on the right -->
<section class="paper-hero">
<div class="paper-eyebrow">For double-blind peer review</div>
<h1 class="hero-title">
Share the code,<br />
<span class="accent">not the author.</span>
</h1>
<p class="hero-subtitle">
Anonymous GitHub makes a read-only mirror of your repository, pull
request, or gist with every trace of identity removed &mdash; so you can attach a
link to your submission without breaking anonymity.
</p>
<div class="paper-hero-copy">
<div class="paper-eyebrow">For double-blind peer review</div>
<h1 class="hero-title">
Share the code,<br />
<span class="accent">not the author.</span>
</h1>
<p class="hero-subtitle">
Anonymous GitHub makes a read-only mirror of your repository, pull
request, or gist with every trace of identity removed, so you can
attach a link to your submission without breaking anonymity.
</p>
<div class="paper-cta-row">
<span ng-if="!user">
<a href="/github/login" target="_self" class="btn-hero">
<i class="fab fa-github mr-2"></i>Sign in with GitHub
<div class="paper-cta-row">
<a ng-if="!user" href="/github/login" target="_self" class="btn-hero">
<i class="fab fa-github mr-2" aria-hidden="true"></i>Sign in with GitHub
</a>
</span>
<span ng-if="user">
<a href="/anonymize" class="btn-hero">
<a ng-if="user" href="/anonymize" class="btn-hero">
Anonymize a repository &rarr;
</a>
</span>
<a href="/anonymize" ng-if="user" class="btn-hero-ghost">
Anonymize a pull request
</a>
<span class="meta">Free &middot; Open source &middot; Self-hostable</span>
<a
class="paper-link-arrow"
target="_self"
href="https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/"
>See an anonymized repository &rarr;</a
>
</div>
<ul class="paper-reassure">
<li>Free and open source</li>
<li>Read-only: never writes to your repos</li>
<li>Expires on the date you set</li>
</ul>
</div>
<!-- Before / after -->
<div class="paper-preview" aria-label="Before and after example">
<div class="pane">
<div class="pane-header">
<span class="pane-label">Before &middot; your repo</span>
<span class="pane-url">github.com/jane-smith/DeepLearnUtils</span>
<div class="paper-hero-shot">
<div class="paper-frame">
<div class="paper-frame-bar" aria-hidden="true">
<span class="dots"><i></i><i></i><i></i></span>
<span class="url">anonymous.4open.science/anonymize</span>
</div>
<div class="pane-body">
<div><span class="tok">#</span> <strong>DeepLearnUtils</strong></div>
<div>Developed by Jane Smith at MIT CSAIL.</div>
<div>See Smith et&nbsp;al., &ldquo;Attention That Works,&rdquo; 2026.</div>
<div>Contact: jane.smith@mit.edu</div>
</div>
</div>
<div class="pane">
<div class="pane-header">
<span class="pane-label">After &middot; anonymous mirror</span>
<span class="pane-url">anonymous.4open.science/r/paper-7f2a</span>
</div>
<div class="pane-body">
<div><span class="tok">#</span> <strong>DeepLearnUtils</strong></div>
<div>Developed by <span class="redact">Jane Smith</span> at <span class="redact">MIT CSAIL</span>.</div>
<div>See <span class="redact">Smith et al.</span>, &ldquo;Attention That Works,&rdquo; 2026.</div>
<div>Contact: <span class="redact">jane.smith@mit.edu</span></div>
</div>
</div>
</div>
</section>
<!-- Stats strip -->
<section class="paper-stats" id="metrics">
<div class="paper-stats-inner">
<div class="paper-stats-meta">
<div class="paper-stats-meta-left">LIVE &middot; LAST 60 DAYS</div>
<div class="paper-stats-meta-right">
updated daily &middot;
<span class="paper-stats-dot"></span> in sync
</div>
</div>
<div class="paper-stats-grid">
<div class="paper-stat-card" ng-repeat="card in cards track by card.key">
<div class="paper-stat-value">{{card.total | bigNum}}</div>
<div class="paper-stat-label">{{card.label}}</div>
<svg class="paper-stat-bars" preserveAspectRatio="none"
ng-attr-viewBox="0 0 {{history[card.key].viewW}} 36"
ng-if="history[card.key].bars.length > 1">
<rect ng-repeat="b in history[card.key].bars track by $index"
ng-attr-x="{{b.x}}" ng-attr-y="{{b.y}}"
ng-attr-width="{{b.w}}" ng-attr-height="{{b.h}}"
ng-class="{'is-latest': $last}"/>
</svg>
<div class="paper-stat-delta" ng-if="history[card.key].bars.length > 1">
<span class="paper-stat-today">+{{history[card.key].deltaToday | number}} today</span>
<span class="paper-stat-sep">&middot;</span>
<span class="paper-stat-pct"
ng-class="{'is-up': history[card.key].isUp, 'is-down': !history[card.key].isUp}">
<span class="paper-stat-arrow">{{history[card.key].isUp ? '▲' : '▼'}}</span>
{{history[card.key].pctAbs}}%
</span>
</div>
</div>
</div>
</div>
</section>
<!-- How it works -->
<section class="paper-how" id="usage">
<div>
<div class="paper-eyebrow">How it works</div>
<h2 class="paper-how-heading">
Three steps,<br />about a minute.
</h2>
</div>
<div>
<div class="paper-step">
<div class="step-num">01</div>
<div>
<div class="step-title">Authenticate</div>
<div class="step-desc">
<a href="/github/login" target="_self">Sign in with GitHub</a> to
access your dashboard. Anonymous GitHub only ever reads your
repositories &mdash; we never push, modify, or delete anything.
GitHub may display a broader scope at sign-in because its OAuth
scopes don't offer read-only access to private repositories
(<a href="/faq#permissions">see FAQ</a>).
</div>
</div>
</div>
<div class="paper-step">
<div class="step-num">02</div>
<div>
<div class="step-title">Configure</div>
<div class="step-desc">
Point us at a repo, branch, commit, or pull request. List the
terms to redact (names, affiliation, emails). Set an expiry.
</div>
</div>
</div>
<div class="paper-step">
<div class="step-num">03</div>
<div>
<div class="step-title">Share</div>
<div class="step-desc">
You get a stable link with a randomised slug. Reviewers browse,
diff, and download &mdash; never seeing who you are.
</div>
</div>
</div>
<div class="paper-step" style="border-top: none;">
<div class="step-num">&nbsp;</div>
<div>
<div class="step-desc">
Example:
<a
target="_self"
href="https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/"
>anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389</a>
</div>
</div>
</div>
</div>
</section>
<!-- Trust row -->
<section class="paper-trust">
<div class="paper-trust-inner">
<div>
<div class="paper-eyebrow">Trusted by the community</div>
<div class="paper-trust-lede">
Used by authors submitting to major software-engineering and systems
venues.
</div>
</div>
<div class="paper-trust-item">
<div class="short">ICSE</div>
<div class="long">International Conference on Software Engineering</div>
</div>
<div class="paper-trust-item">
<div class="short">FSE</div>
<div class="long">Foundations of Software Engineering</div>
</div>
<div class="paper-trust-item">
<div class="short">OOPSLA</div>
<div class="long">
Object-Oriented Programming, Systems, Languages, and Applications
</div>
</div>
</div>
</section>
<!-- Featurettes — existing content, kept but restyled -->
<main class="container home-content" id="about">
<div class="row featurette align-items-center">
<div class="col-md-7 order-md-2">
<h2 class="featurette-heading">Double-anonymous</h2>
<p class="featurette-lead">
Anonymize your GitHub repository with options to remove links,
images, or specific terms. Keep full control &mdash; set an
expiration date to make your repository unavailable after review.
</p>
</div>
<div class="col-md-5 order-md-1">
<img
width="500"
loading="lazy"
src="/imgs/anonymize.png"
class="featurette-image img-fluid mx-auto"
alt="Anonymize form"
width="2880"
height="1800"
alt="The anonymize form with a live README preview showing the redactions"
/>
</div>
</div>
</section>
<hr class="featurette-divider" />
<!-- Proof -->
<section class="paper-proof paper-wrap" aria-label="Venues">
<div class="paper-eyebrow">Trusted by authors at</div>
<div class="paper-proof-venues">
<span>ICSE</span><i aria-hidden="true">&middot;</i>
<span>FSE</span><i aria-hidden="true">&middot;</i>
<span>OOPSLA</span>
</div>
</section>
<div class="row featurette align-items-center">
<div class="col-md-7">
<h2 class="featurette-heading">Explorer</h2>
<p class="featurette-lead">
Reviewers can browse your repository with highlighted source code,
rendered PDFs, images, and notebooks.
<a href="https://pages.github.com">GitHub Pages</a> is also supported.
<!-- Live stats -->
<section class="paper-stats paper-wrap" id="metrics" aria-label="Usage over the last 60 days">
<div class="paper-stats-meta">
<div class="paper-stats-meta-left">Live &middot; last 60 days</div>
<div class="paper-stats-meta-right">
<span class="paper-stats-dot" aria-hidden="true"></span> updated daily
</div>
</div>
<div class="paper-stats-grid">
<div class="paper-stat-card" ng-repeat="card in cards track by card.key">
<div class="paper-stat-value">{{card.total | bigNum}}</div>
<div class="paper-stat-label">{{card.label}}</div>
<svg class="paper-stat-bars" preserveAspectRatio="none" aria-hidden="true"
ng-attr-viewBox="0 0 {{history[card.key].viewW}} 36"
ng-if="history[card.key].bars.length > 1">
<rect ng-repeat="b in history[card.key].bars track by $index"
ng-attr-x="{{b.x}}" ng-attr-y="{{b.y}}"
ng-attr-width="{{b.w}}" ng-attr-height="{{b.h}}"
ng-class="{'is-latest': $last}"/>
</svg>
<div class="paper-stat-delta" ng-if="history[card.key].bars.length > 1">
+{{history[card.key].deltaToday | number}} today
</div>
</div>
</div>
</section>
<!-- Before / after -->
<section class="paper-section-head paper-wrap">
<div>
<div class="paper-eyebrow">Before &middot; after</div>
<h2>The same repository, <em>minus you.</em></h2>
</div>
<p>
Names, affiliations, emails, and any term you list are replaced across
the README, source files, notebooks, and PDF links. Reviewers browse the
real thing, not a zip.
</p>
</section>
<div class="paper-preview paper-wrap" aria-label="Before and after example">
<div class="pane">
<div class="pane-header">
<span class="pane-label">Before &middot; your repo</span>
<span class="pane-url">github.com/jane-smith/DeepLearnUtils</span>
</div>
<div class="pane-body">
<div><span class="tok">#</span> <strong>DeepLearnUtils</strong></div>
<div>Developed by Jane Smith at MIT CSAIL.</div>
<div>See Smith et&nbsp;al., &ldquo;Attention That Works,&rdquo; 2026.</div>
<div>Contact: jane.smith@mit.edu</div>
</div>
</div>
<div class="pane">
<div class="pane-header">
<span class="pane-label">After &middot; anonymous mirror</span>
<span class="pane-url">anonymous.4open.science/r/paper-7f2a</span>
</div>
<div class="pane-body">
<div><span class="tok">#</span> <strong>DeepLearnUtils</strong></div>
<div>Developed by <span class="redact" role="img" aria-label="redacted author name">XXXX-1</span> at <span class="redact" role="img" aria-label="redacted affiliation">XXXX-2</span>.</div>
<div>See <span class="redact" role="img" aria-label="redacted author name">XXXX-1</span> et&nbsp;al., &ldquo;Attention That Works,&rdquo; 2026.</div>
<div>Contact: <span class="redact" role="img" aria-label="redacted email address">XXXX-3</span></div>
</div>
</div>
</div>
<!-- What you get: one screenshot, three tabs -->
<section class="paper-section-head paper-wrap" id="about">
<div>
<div class="paper-eyebrow">What you get</div>
<h2>Anonymize, review, <em>manage.</em></h2>
</div>
<p>
Every mirror is read-only and passes through the same filter, from the
README to a PDF's link targets. Monitor it, update it, or remove it the
day the decision is out.
</p>
</section>
<section class="paper-features paper-wrap" aria-label="Product features">
<div class="paper-features-list" role="tablist" aria-label="Features" aria-orientation="vertical">
<div
class="paper-feature"
ng-repeat="f in features track by f.key"
ng-class="{'is-on': feature === f.key}"
>
<button
type="button"
class="paper-feature-tab"
role="tab"
id="feature-tab-{{f.key}}"
aria-controls="feature-panel-{{f.key}}"
aria-selected="{{feature === f.key}}"
tabindex="{{feature === f.key ? 0 : -1}}"
ng-click="selectFeature(f.key)"
ng-keydown="featureKeydown($event, $index)"
>
<span class="paper-eyebrow">{{f.num}} &middot; {{f.eyebrow}}</span>
<span class="paper-feature-title">{{f.title}} <em>{{f.accent}}</em></span>
</button>
<div class="paper-feature-more">
<span class="paper-feature-title paper-feature-title-m" aria-hidden="true">{{f.title}} <em>{{f.accent}}</em></span>
<p>{{f.text}}</p>
<a class="paper-link-arrow" ng-href="{{featureHref(f)}}" ng-attr-target="{{featureTarget(f)}}">{{f.cta}} &rarr;</a>
</div>
</div>
</div>
<div class="paper-features-stage">
<div
class="paper-feature-panel"
ng-repeat="f in features track by f.key"
ng-show="feature === f.key"
id="feature-panel-{{f.key}}"
role="tabpanel"
aria-labelledby="feature-tab-{{f.key}}"
>
<div class="paper-frame">
<div class="paper-frame-bar" aria-hidden="true">
<span class="dots"><i></i><i></i><i></i></span>
<span class="url">{{f.url}}</span>
</div>
<div class="paper-crop paper-crop-{{f.key}}">
<img ng-src="{{f.img}}" alt="{{f.alt}}" loading="lazy" width="2880" height="1800" />
</div>
</div>
</div>
</div>
</section>
<!-- FAQ teaser -->
<section class="paper-qa paper-wrap" aria-labelledby="qa-heading">
<div>
<div class="paper-eyebrow">Before you sign in</div>
<h2 id="qa-heading">The questions that <em>come up most.</em></h2>
<a class="paper-link-arrow" href="/faq">Read the full FAQ &rarr;</a>
</div>
<div>
<div class="q">
<a href="/faq#howWork">Why does GitHub ask for write access at sign-in?</a>
<p>
GitHub's OAuth scopes have no read-only option for private
repositories. Anonymous GitHub only ever reads your repositories; it
never pushes, modifies, or deletes anything.
</p>
</div>
<div class="col-md-5">
<img
width="500"
loading="lazy"
src="/imgs/explorer.png"
class="featurette-image img-fluid mx-auto"
alt="Repository explorer"
/>
</div>
</div>
<hr class="featurette-divider" />
<div class="row featurette align-items-center">
<div class="col-md-7 order-md-2">
<h2 class="featurette-heading">Manage</h2>
<p class="featurette-lead">
Monitor views, edit configuration, remove or update your repository
&mdash; all from a clean dashboard.
<div class="q">
<a href="/faq#formats">Which file formats are supported?</a>
<p>
Source code with syntax highlighting, Markdown, Jupyter notebooks,
PDFs, images, and GitHub Pages sites. Everything passes through the
same redaction filter.
</p>
</div>
<div class="col-md-5 order-md-1">
<img
width="500"
loading="lazy"
src="/imgs/dashboard.png"
class="featurette-image img-fluid mx-auto"
alt="Dashboard"
/>
<div class="q">
<a href="/faq#expiration">What happens when an anonymized repository expires?</a>
<p>
You choose: the mirror is removed, or visitors are redirected to the
original GitHub repository once the review is over.
</p>
</div>
</div>
</main>
</section>
<footer class="paper-footer">
<div class="paper-footer-inner">
<div class="paper-footer-brand">
<div class="paper-footer-mark">Anonymous <em>GitHub</em></div>
<p class="paper-footer-tag">
A read-only, identity-stripped mirror of your repository &mdash;
built for double-blind peer review.
A read-only, identity-stripped mirror of your repository, built for
double-blind peer review.
</p>
</div>
<div class="paper-footer-col">
@@ -263,14 +239,14 @@
</div>
<div class="paper-footer-col">
<div class="paper-footer-head">Project</div>
<a href="https://github.com/tdurieux/anonymous_github/" target="_blank">Source code</a>
<a href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener">Source code</a>
<a href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener">Report a bug</a>
<a href="https://github.com/sponsors/tdurieux/" target="_blank">Sponsor</a>
<a href="https://github.com/sponsors/tdurieux/" target="_blank" rel="noopener">Sponsor</a>
</div>
<div class="paper-footer-col">
<div class="paper-footer-head">Contact</div>
<a href="mailto:tdurieux@anonymous.4open.science">Email maintainer</a>
<a href="https://github.com/tdurieux" target="_blank">@tdurieux</a>
<a href="https://github.com/tdurieux" target="_blank" rel="noopener">@tdurieux</a>
</div>
</div>
<div class="paper-footer-rule"></div>
+83 -1
View File
@@ -1386,7 +1386,9 @@ angular
"$scope",
"$http",
"$location",
function ($scope, $http, $location) {
"$window",
"$timeout",
function ($scope, $http, $location, $window, $timeout) {
if ($scope.user && !$scope.user.status) {
$location.url("/dashboard");
}
@@ -1396,6 +1398,86 @@ angular
}
});
// "What you get": one screenshot, three tabs. The selected key drives
// both the expanded description and the visible panel.
$scope.features = [
{
key: "anonymize",
num: "01",
eyebrow: "Anonymize",
title: "Double-anonymous,",
accent: "your rules.",
text:
"Choose what reviewers may see: links, images, PDFs, notebooks, GitHub Pages. Add your own terms, with regex if you need it, and pick the expiration date.",
cta: "Start an anonymization",
href: "/anonymize",
url: "anonymous.4open.science/anonymize",
img: "/imgs/anonymize.png",
alt: "The anonymize form: source repository, terms to redact, options and a live README preview",
},
{
key: "review",
num: "02",
eyebrow: "Review",
title: "Reviewers browse",
accent: "the real thing.",
text:
"Highlighted source code, rendered PDFs, images, and notebooks, in a familiar file explorer. GitHub Pages is also supported.",
cta: "Open the example",
href: "https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/",
target: "_self",
url: "anonymous.4open.science/r/840c8c57-…",
img: "/imgs/explorer.png",
alt: "The repository explorer with a file tree and a rendered README",
},
{
key: "manage",
num: "03",
eyebrow: "Manage",
title: "One dashboard,",
accent: "until the decision.",
text:
"Monitor views, edit configuration, remove or update your repository. Program chairs can group submissions under a conference with one shared expiry.",
cta: "Open the dashboard",
href: "/dashboard",
needsUser: true,
url: "anonymous.4open.science/dashboard",
img: "/imgs/dashboard.png",
alt: "The dashboard listing anonymized repositories with status, views and expiry",
},
];
$scope.feature = $scope.features[0].key;
$scope.selectFeature = function (key) {
$scope.feature = key;
};
// Signed-out visitors cannot open the dashboard; send them to sign in.
$scope.featureHref = function (f) {
return f.needsUser && !$scope.user ? "/github/login" : f.href;
};
$scope.featureTarget = function (f) {
return f.needsUser && !$scope.user ? "_self" : f.target || undefined;
};
$scope.featureKeydown = function ($event, index) {
const step = {
ArrowDown: 1,
ArrowRight: 1,
ArrowUp: -1,
ArrowLeft: -1,
Home: "first",
End: "last",
}[$event.key];
if (step === undefined) return;
$event.preventDefault();
const n = $scope.features.length;
const next =
step === "first" ? 0 : step === "last" ? n - 1 : (index + step + n) % n;
$scope.feature = $scope.features[next].key;
$timeout(() => {
const el = $window.document.getElementById("feature-tab-" + $scope.feature);
if (el) el.focus();
});
};
$scope.cards = [
{ key: "repositories", total: 0, label: "repositories anonymized" },
{ key: "users", total: 0, label: "researchers" },
+1 -1
View File
@@ -1,2 +1,2 @@
function markedMermaid(e){return{extensions:[{name:"mermaid",level:"block",start(e){return e.match(/^```mermaid/m)?.index},tokenizer(e,t){e=/^```mermaid\n([\s\S]*?)\n```/.exec(e);if(e)return{type:"mermaid",raw:e[0],text:e[1].trim()}},renderer(e){const t="mermaid-"+Math.random().toString(36).substr(2,9);e=`<div class="mermaid" id="${t}">${e.text}</div>`;return"undefined"==typeof mermaid&&"function"==typeof window.loadMermaid&&window.loadMermaid(),setTimeout(()=>{if("undefined"!=typeof mermaid){window.mermaidInitialized||(mermaid.initialize({startOnLoad:!1,theme:"default",securityLevel:"strict"}),window.mermaidInitialized=!0);try{var e=document.getElementById(t);e&&!e.getAttribute("data-processed")&&(mermaid.init(void 0,e),e.setAttribute("data-processed","true"))}catch(e){console.error("Mermaid rendering error:",e)}}},100),e}}]}}!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=e.pdfjsLib=t():"function"==typeof define&&define.amd?define("pdfjs-dist/build/pdf",[],()=>e.pdfjsLib=t()):"object"==typeof exports?exports["pdfjs-dist/build/pdf"]=e.pdfjsLib=t():e["pdfjs-dist/build/pdf"]=e.pdfjsLib=t()}(globalThis,()=>(()=>{"use strict";var __webpack_modules__=[,(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.VerbosityLevel=t.Util=t.UnknownErrorException=t.UnexpectedResponseException=t.TextRenderingMode=t.RenderingIntentFlag=t.PromiseCapability=t.PermissionFlag=t.PasswordResponses=t.PasswordException=t.PageActionEventType=t.OPS=t.MissingPDFException=t.MAX_IMAGE_SIZE_TO_CACHE=t.LINE_FACTOR=t.LINE_DESCENT_FACTOR=t.InvalidPDFException=t.ImageKind=t.IDENTITY_MATRIX=t.FormatError=t.FeatureTest=t.FONT_IDENTITY_MATRIX=t.DocumentActionEventType=t.CMapCompressionType=t.BaseException=t.BASELINE_FACTOR=t.AnnotationType=t.AnnotationReplyType=t.AnnotationPrefix=t.AnnotationMode=t.AnnotationFlag=t.AnnotationFieldFlag=t.AnnotationEditorType=t.AnnotationEditorPrefix=t.AnnotationEditorParamsType=t.AnnotationBorderStyleType=t.AnnotationActionEventType=t.AbortException=void 0,t.assert=function(e,t){e||s(t)},t.bytesToString=h,t.createValidAbsoluteUrl=function(e){let t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:null,i=2<arguments.length&&void 0!==arguments[2]?arguments[2]:null;if(e)try{if(i&&"string"==typeof e){if(i.addDefaultProtocol&&e.startsWith("www.")){const t=e.match(/\./g);2<=t?.length&&(e="http://"+e)}if(i.tryConvertEncoding)try{e=p(e)}catch{}}var n=t?new URL(e,t):new URL(e);if(function(e){switch(e?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return 1;default:return}}(n))return n}catch{}return null},t.getModificationDate=function(){var e=0<arguments.length&&void 0!==arguments[0]?arguments[0]:new Date;return[e.getUTCFullYear().toString(),(e.getUTCMonth()+1).toString().padStart(2,"0"),e.getUTCDate().toString().padStart(2,"0"),e.getUTCHours().toString().padStart(2,"0"),e.getUTCMinutes().toString().padStart(2,"0"),e.getUTCSeconds().toString().padStart(2,"0")].join("")},t.getUuid=function(){if("undefined"!=typeof crypto&&"function"==typeof crypto?.randomUUID)return crypto.randomUUID();var t=new Uint8Array(32);if("undefined"!=typeof crypto&&"function"==typeof crypto?.getRandomValues)crypto.getRandomValues(t);else for(let e=0;e<32;e++)t[e]=Math.floor(255*Math.random());return h(t)},t.getVerbosityLevel=function(){return r},t.info=function(e){r>=n.INFOS&&console.log("Info: "+e)},t.isArrayBuffer=function(e){return"object"==typeof e&&void 0!==e?.byteLength},t.isArrayEqual=function(i,n){if(i.length!==n.length)return!1;for(let e=0,t=i.length;e<t;e++)if(i[e]!==n[e])return!1;return!0},t.isNodeJS=void 0,t.normalizeUnicode=function(e){return m||(m=/([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc-\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa-\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu,g=new Map([["ſt","ſt"]])),e.replaceAll(m,(e,t,i)=>t?t.normalize("NFKC"):g.get(i))},t.objectFromMap=function(e){var t,i,n=Object.create(null);for([t,i]of e)n[t]=i;return n},t.objectSize=function(e){return Object.keys(e).length},t.setVerbosityLevel=function(e){Number.isInteger(e)&&(r=e)},t.shadow=a,t.string32=function(e){return String.fromCharCode(e>>24&255,e>>16&255,e>>8&255,255&e)},t.stringToBytes=c,t.stringToPDFString=function(i){if("ï"<=i[0]){let e;if("þ"===i[0]&&"ÿ"===i[1]?e="utf-16be":"ÿ"===i[0]&&"þ"===i[1]?e="utf-16le":"ï"===i[0]&&"»"===i[1]&&"¿"===i[2]&&(e="utf-8"),e)try{var t=new TextDecoder(e,{fatal:!0}),n=c(i);return t.decode(n)}catch(i){o(`stringToPDFString: "${i}".`)}}var r=[];for(let e=0,t=i.length;e<t;e++){var s=u[i.charCodeAt(e)];r.push(s?String.fromCharCode(s):i.charAt(e))}return r.join("")},t.stringToUTF8String=p,t.unreachable=s,t.utf8StringToString=function(e){return unescape(encodeURIComponent(e))},t.warn=o,i(2),i(84),i(86),i(87),i(89),i(93),i(101),i(102),i(105),i(107),i(Line truncated
`}setFont(e){var t=this.current,i=this.commonObjs.get(e[0]);let n=e[1],r=(t.font=i,!this.embedFonts||i.missingFile||this.embeddedFonts[i.loadedName]||(this.addFontStyle(i),this.embeddedFonts[i.loadedName]=i),t.fontMatrix=i.fontMatrix||v.FONT_IDENTITY_MATRIX,"normal");i.black?r="900":i.bold&&(r="bold");e=i.italic?"italic":"normal";n<0?(n=-n,t.fontDirection=-1):t.fontDirection=1,t.fontSize=n,t.fontFamily=i.loadedName,t.fontWeight=r,t.fontStyle=e,t.tspan=this.svgFactory.createElement("svg:tspan"),t.tspan.setAttributeNS(null,"y",b(-t.y)),t.xcoords=[],t.ycoords=[]}endText(){var e=this.current;e.textRenderingMode&v.TextRenderingMode.ADD_TO_PATH_FLAG&&e.txtElement?.hasChildNodes()&&(e.element=e.txtElement,this.clip("nonzero"),this.endPath())}setLineWidth(e){0<e&&(this.current.lineWidth=e)}setLineCap(e){this.current.lineCap=r[e]}setLineJoin(e){this.current.lineJoin=s[e]}setMiterLimit(e){this.current.miterLimit=e}setStrokeAlpha(e){this.current.strokeAlpha=e}setStrokeRGBColor(e,t,i){this.current.strokeColor=v.Util.makeHexColor(e,t,i)}setFillAlpha(e){this.current.fillAlpha=e}setFillRGBColor(e,t,i){this.current.fillColor=v.Util.makeHexColor(e,t,i),this.current.tspan=this.svgFactory.createElement("svg:tspan"),this.current.xcoords=[],this.current.ycoords=[]}setStrokeColorN(e){this.current.strokeColor=this._makeColorN_Pattern(e)}setFillColorN(e){this.current.fillColor=this._makeColorN_Pattern(e)}shadingFill(e){var{width:t,height:i}=this.viewport,n=v.Util.inverseTransform(this.transformMatrix),[t,i,n,r]=v.Util.getAxialAlignedBoundingBox([0,0,t,i],n),s=this.svgFactory.createElement("svg:rect");s.setAttributeNS(null,"x",t),s.setAttributeNS(null,"y",i),s.setAttributeNS(null,"width",n-t),s.setAttributeNS(null,"height",r-i),s.setAttributeNS(null,"fill",this._makeShadingPattern(e)),this.current.fillAlpha<1&&s.setAttributeNS(null,"fill-opacity",this.current.fillAlpha),this._ensureTransformGroup().append(s)}_makeColorN_Pattern(e){return"TilingPattern"===e[0]?this._makeTilingPattern(e):this._makeShadingPattern(e)}_makeTilingPattern(e){var t=e[1],i=e[2],n=e[3]||v.IDENTITY_MATRIX,[r,s,o,a]=e[4],l=e[5],h=e[6],e=e[7],c="shading"+m++,[r,s,o,a]=v.Util.normalizeRect([...v.Util.applyTransform([r,s],n),...v.Util.applyTransform([o,a],n)]),[d,u]=v.Util.singularValueDecompose2dScale(n),l=l*d,d=h*u,h=this.svgFactory.createElement("svg:pattern"),u=(h.setAttributeNS(null,"id",c),h.setAttributeNS(null,"patternUnits","userSpaceOnUse"),h.setAttributeNS(null,"width",l),h.setAttributeNS(null,"height",d),h.setAttributeNS(null,"x",""+r),h.setAttributeNS(null,"y",""+s),this.svg),l=this.transformMatrix,d=this.current.fillColor,p=this.current.strokeColor,o=this.svgFactory.create(o-r,a-s);if(this.svg=o,this.transformMatrix=n,2===e){const e=v.Util.makeHexColor(...t);this.current.fillColor=e,this.current.strokeColor=e}return this.executeOpTree(this.convertOpList(i)),this.svg=u,this.transformMatrix=l,this.current.fillColor=d,this.current.strokeColor=p,h.append(o.childNodes[0]),this.defs.append(h),`url(#${c})`}_makeShadingPattern(t){switch((t="string"==typeof t?this.objs.get(t):t)[0]){case"RadialAxial":var i="shading"+m++,n=t[3];let e;switch(t[1]){case"axial":const h=t[4],v=t[5];(e=this.svgFactory.createElement("svg:linearGradient")).setAttributeNS(null,"id",i),e.setAttributeNS(null,"gradientUnits","userSpaceOnUse"),e.setAttributeNS(null,"x1",h[0]),e.setAttributeNS(null,"y1",h[1]),e.setAttributeNS(null,"x2",v[0]),e.setAttributeNS(null,"y2",v[1]);break;case"radial":var r=t[4],s=t[5],o=t[6],a=t[7];(e=this.svgFactory.createElement("svg:radialGradient")).setAttributeNS(null,"id",i),e.setAttributeNS(null,"gradientUnits","userSpaceOnUse"),e.setAttributeNS(null,"cx",s[0]),e.setAttributeNS(null,"cy",s[1]),e.setAttributeNS(null,"r",a),e.setAttributeNS(null,"fx",r[0]),e.setAttributeNS(null,"fy",r[1]),e.setAttributeNS(null,"fr",o);break;default:throw new Error("Unknown RadialAxial type: "+t[1])}for(const t of n){var l=this.svgFactory.createElement("svg:stop");l.setAttributeNS(null,"offset",t[0]),l.setAttributeNS(null,"stop-color",t[1]),e.append(l)}return this.defs.append(e),`url(#${i})`;case"Mesh":return(0,v.warn)("Unimplemented pattern Mesh"),null;case"Dummy":return"hotpink";default:throw new Error("Unknown IR type: "+t[0])}}setDash(e,t){this.current.dashArray=e,this.current.dashPhase=t}constructPath(e,t){const i=this.current;let n=i.x,r=i.y,s=[],o=0;for(const i of e)switch(0|i){case v.OPS.rectangle:n=t[o++],r=t[o++];const e=n+t[o++],i=r+t[o++];s.push("M",b(n),b(r),"L",b(e),b(r),"L",b(e),b(i),"L",b(n),b(i),"Z");break;case v.OPS.moveTo:n=t[o++],r=t[o++],s.push("M",b(n),b(r));break;case v.OPS.lineTo:n=t[o++],r=t[o++],s.push("L",b(n),b(r));break;case v.OPS.curveTo:n=t[o+4],r=t[o+5],s.push("C",b(t[o]),b(t[o+1]),b(t[o+2]),b(t[o+3]),b(n),b(r)),o+=6;break;case v.OPS.curveTo2:s.push("C",b(n),b(r),b(t[o]),b(t[o+1]),b(t[o+2]),b(t[o+3])),n=t[o+2],r=t[o+3],o+=4;break;case v.OPS.curveTo3:n=t[o+2],r=t[o+3],s.push("C",b(t[o]),b(t[o+1]),b(n),b(r),b(n),b(r)),o+=4;break;case v.OPS.closeP Line truncated
`}setFont(e){var t=this.current,i=this.commonObjs.get(e[0]);let n=e[1],r=(t.font=i,!this.embedFonts||i.missingFile||this.embeddedFonts[i.loadedName]||(this.addFontStyle(i),this.embeddedFonts[i.loadedName]=i),t.fontMatrix=i.fontMatrix||v.FONT_IDENTITY_MATRIX,"normal");i.black?r="900":i.bold&&(r="bold");e=i.italic?"italic":"normal";n<0?(n=-n,t.fontDirection=-1):t.fontDirection=1,t.fontSize=n,t.fontFamily=i.loadedName,t.fontWeight=r,t.fontStyle=e,t.tspan=this.svgFactory.createElement("svg:tspan"),t.tspan.setAttributeNS(null,"y",b(-t.y)),t.xcoords=[],t.ycoords=[]}endText(){var e=this.current;e.textRenderingMode&v.TextRenderingMode.ADD_TO_PATH_FLAG&&e.txtElement?.hasChildNodes()&&(e.element=e.txtElement,this.clip("nonzero"),this.endPath())}setLineWidth(e){0<e&&(this.current.lineWidth=e)}setLineCap(e){this.current.lineCap=r[e]}setLineJoin(e){this.current.lineJoin=s[e]}setMiterLimit(e){this.current.miterLimit=e}setStrokeAlpha(e){this.current.strokeAlpha=e}setStrokeRGBColor(e,t,i){this.current.strokeColor=v.Util.makeHexColor(e,t,i)}setFillAlpha(e){this.current.fillAlpha=e}setFillRGBColor(e,t,i){this.current.fillColor=v.Util.makeHexColor(e,t,i),this.current.tspan=this.svgFactory.createElement("svg:tspan"),this.current.xcoords=[],this.current.ycoords=[]}setStrokeColorN(e){this.current.strokeColor=this._makeColorN_Pattern(e)}setFillColorN(e){this.current.fillColor=this._makeColorN_Pattern(e)}shadingFill(e){var{width:t,height:i}=this.viewport,n=v.Util.inverseTransform(this.transformMatrix),[t,i,n,r]=v.Util.getAxialAlignedBoundingBox([0,0,t,i],n),s=this.svgFactory.createElement("svg:rect");s.setAttributeNS(null,"x",t),s.setAttributeNS(null,"y",i),s.setAttributeNS(null,"width",n-t),s.setAttributeNS(null,"height",r-i),s.setAttributeNS(null,"fill",this._makeShadingPattern(e)),this.current.fillAlpha<1&&s.setAttributeNS(null,"fill-opacity",this.current.fillAlpha),this._ensureTransformGroup().append(s)}_makeColorN_Pattern(e){return"TilingPattern"===e[0]?this._makeTilingPattern(e):this._makeShadingPattern(e)}_makeTilingPattern(e){var t=e[1],i=e[2],n=e[3]||v.IDENTITY_MATRIX,[r,s,o,a]=e[4],l=e[5],h=e[6],e=e[7],c="shading"+m++,[r,s,o,a]=v.Util.normalizeRect([...v.Util.applyTransform([r,s],n),...v.Util.applyTransform([o,a],n)]),[d,u]=v.Util.singularValueDecompose2dScale(n),l=l*d,d=h*u,h=this.svgFactory.createElement("svg:pattern"),u=(h.setAttributeNS(null,"id",c),h.setAttributeNS(null,"patternUnits","userSpaceOnUse"),h.setAttributeNS(null,"width",l),h.setAttributeNS(null,"height",d),h.setAttributeNS(null,"x",""+r),h.setAttributeNS(null,"y",""+s),this.svg),l=this.transformMatrix,d=this.current.fillColor,p=this.current.strokeColor,o=this.svgFactory.create(o-r,a-s);if(this.svg=o,this.transformMatrix=n,2===e){const e=v.Util.makeHexColor(...t);this.current.fillColor=e,this.current.strokeColor=e}return this.executeOpTree(this.convertOpList(i)),this.svg=u,this.transformMatrix=l,this.current.fillColor=d,this.current.strokeColor=p,h.append(o.childNodes[0]),this.defs.append(h),`url(#${c})`}_makeShadingPattern(t){switch((t="string"==typeof t?this.objs.get(t):t)[0]){case"RadialAxial":var i="shading"+m++,n=t[3];let e;switch(t[1]){case"axial":const h=t[4],v=t[5];(e=this.svgFactory.createElement("svg:linearGradient")).setAttributeNS(null,"id",i),e.setAttributeNS(null,"gradientUnits","userSpaceOnUse"),e.setAttributeNS(null,"x1",h[0]),e.setAttributeNS(null,"y1",h[1]),e.setAttributeNS(null,"x2",v[0]),e.setAttributeNS(null,"y2",v[1]);break;case"radial":var r=t[4],s=t[5],o=t[6],a=t[7];(e=this.svgFactory.createElement("svg:radialGradient")).setAttributeNS(null,"id",i),e.setAttributeNS(null,"gradientUnits","userSpaceOnUse"),e.setAttributeNS(null,"cx",s[0]),e.setAttributeNS(null,"cy",s[1]),e.setAttributeNS(null,"r",a),e.setAttributeNS(null,"fx",r[0]),e.setAttributeNS(null,"fy",r[1]),e.setAttributeNS(null,"fr",o);break;default:throw new Error("Unknown RadialAxial type: "+t[1])}for(const t of n){var l=this.svgFactory.createElement("svg:stop");l.setAttributeNS(null,"offset",t[0]),l.setAttributeNS(null,"stop-color",t[1]),e.append(l)}return this.defs.append(e),`url(#${i})`;case"Mesh":return(0,v.warn)("Unimplemented pattern Mesh"),null;case"Dummy":return"hotpink";default:throw new Error("Unknown IR type: "+t[0])}}setDash(e,t){this.current.dashArray=e,this.current.dashPhase=t}constructPath(e,t){const i=this.current;let n=i.x,r=i.y,s=[],o=0;for(const i of e)switch(0|i){case v.OPS.rectangle:n=t[o++],r=t[o++];const e=n+t[o++],i=r+t[o++];s.push("M",b(n),b(r),"L",b(e),b(r),"L",b(e),b(i),"L",b(n),b(i),"Z");break;case v.OPS.moveTo:n=t[o++],r=t[o++],s.push("M",b(n),b(r));break;case v.OPS.lineTo:n=t[o++],r=t[o++],s.push("L",b(n),b(r));break;case v.OPS.curveTo:n=t[o+4],r=t[o+5],s.push("C",b(t[o]),b(t[o+1]),b(t[o+2]),b(t[o+3]),b(n),b(r)),o+=6;break;case v.OPS.curveTo2:s.push("C",b(n),b(r),b(t[o]),b(t[o+1]),b(t[o+2]),b(t[o+3])),n=t[o+2],r=t[o+3],o+=4;break;case v.OPS.curveTo3:n=t[o+2],r=t[o+3],s.push("C",b(t[o]),b(t[o+1]),b(n),b(r),b(n),b(r)),o+=4;break;case v.OPS.closeP Line truncated
+45
View File
@@ -140,6 +140,51 @@ describe("frontend production regressions", function () {
h.scope.saveDefault(); h.requests.at(-1).reject({ data: { error: "not_connected" } }); await h.flush();
expect(h.scope.error).to.equal("ERRORS.not_connected");
});
describe("landing page features", function () {
const home = fs.readFileSync(path.join(__dirname, "..", "public", "partials", "home.htm"), "utf8");
function landing(user) {
const h = harness(); h.scope.user = user;
const focused = []; h.focused = focused;
const win = { document: { getElementById: id => ({ focus: () => focused.push(id) }) } };
const timeout = fn => fn();
h.defs.homeController.at(-1)(h.scope, h.http, { url() {} }, win, timeout);
return h;
}
it("selects the first feature and switches on click", function () {
const h = landing(null);
expect(h.scope.feature).to.equal("anonymize");
h.scope.selectFeature("manage"); expect(h.scope.feature).to.equal("manage");
});
it("moves between tabs with the arrow keys, wrapping and moving focus", function () {
const h = landing(null); let prevented = 0;
const key = k => ({ key: k, preventDefault: () => prevented++ });
h.scope.featureKeydown(key("ArrowDown"), 0); expect(h.scope.feature).to.equal("review");
h.scope.featureKeydown(key("ArrowUp"), 1); expect(h.scope.feature).to.equal("anonymize");
h.scope.featureKeydown(key("ArrowUp"), 0); expect(h.scope.feature).to.equal("manage");
h.scope.featureKeydown(key("End"), 0); expect(h.scope.feature).to.equal("manage");
h.scope.featureKeydown(key("Home"), 2); expect(h.scope.feature).to.equal("anonymize");
h.scope.featureKeydown(key("Tab"), 0); expect(h.scope.feature).to.equal("anonymize");
expect(prevented).to.equal(5);
expect(h.focused).to.deep.equal(["review", "anonymize", "manage", "manage", "anonymize"].map(k => "feature-tab-" + k));
});
it("sends signed-out visitors to sign in instead of the dashboard", function () {
const manage = f => f.key === "manage";
const out = landing(null);
expect(out.scope.featureHref(out.scope.features.find(manage))).to.equal("/github/login");
expect(out.scope.featureTarget(out.scope.features.find(manage))).to.equal("_self");
const signedIn = landing({ username: "jane" });
expect(signedIn.scope.featureHref(signedIn.scope.features.find(manage))).to.equal("/dashboard");
expect(signedIn.scope.featureTarget(signedIn.scope.features.find(manage))).to.equal(undefined);
});
it("marks the tabs up as tabs and keeps links out of the buttons", function () {
expect(home).to.match(/<button[^>]*class="paper-feature-tab"[^>]*role="tab"/);
expect(home).to.match(/role="tablist"/);
expect(home).to.match(/role="tabpanel"[^>]*aria-labelledby="feature-tab-\{\{f\.key\}\}"/);
const button = home.slice(home.indexOf('class="paper-feature-tab"'), home.indexOf("</button>"));
expect(button).to.not.include("<a ");
expect(home).to.not.match(/href="#"/);
});
});
it("keeps the conference end date after the start across December", function () {
class December extends Date { constructor(...args) { super(...(args.length ? args : ["2026-12-15T12:00:00Z"])); } }
const h = harness(December); h.scope.user = {};