Files
anonymous_github/public/partials/anonymize.htm
T
Thomas DurieuxandClaude Opus 5.5 6226acff80 fix(ui): show a vertical file tree in the reviewer preview
The "What reviewers will see" window now has a sidebar like the explorer's
instead of the one-row file strip: folders first, then files, with the
explorer's tree rows, icons and teal selection on the README. Folders expand
from the loaded file list. The sidebar is 24% of the window (180-240px),
tinted full height, and scrolls apart from the document. Phones hide it and
show the explorer's "Files" button in the file bar. Loading shows skeleton
rows; without a file list the sidebar shows the README only.

Removes the strip fitter and the dead rail/strip CSS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 03:54:56 +00:00

689 lines
72 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div class="anonymize-page h-100" :class="{'mobile-preview-active': mobilePane === 'preview', 'preview-expanded': previewExpanded}">
<!-- ===== STATE 1: No URL — centered input ===== -->
<div class="anonymize-landing" v-show="!workspaceOpen &amp;&amp; !isUpdate">
<div class="anonymize-landing-inner">
<header class="page-head">
<div class="paper-crumbs"><a href="/dashboard">My work</a> &nbsp;/&nbsp; <span class="here">New anonymization</span></div>
<div class="page-head-main"><div>
<h1 class="paper-page-title">New anonymization</h1>
<p class="paper-page-lede">Paste a repository, pull request, or gist URL, or pick one of your repositories. You&rsquo;ll choose the terms to redact next.</p>
</div></div>
<ol class="task-trail flow-trail" aria-label="Anonymization steps"><li aria-current="step">1. Repository</li><li>2. Review and hosting</li><li>3. Review link</li></ol>
</header>
<aside class="task-summary landing-next" aria-labelledby="landing-next-title">
<h2 id="landing-next-title">What happens next</h2>
<ol>
<li><b>Choose terms to redact.</b> Names, affiliations, and emails are replaced in every text file.</li>
<li><b>Pick hosting and expiry.</b> Decide what reviewers may see and when the link stops working.</li>
<li><b>Share the review link.</b> Reviewers open it without an account.</li>
</ol>
</aside>
<div class="form-group mt-4 mb-2">
<div class="source-url-label-row">
<label class="paper-field-label" for="sourceUrl-landing">Source URL</label>
<aside class="repo-access" aria-label="GitHub access" v-if="!sourceUrl &amp;&amp; githubConnections?.appEnabled &amp;&amp; githubConnections.appConnected &amp;&amp; githubConnections.oauthConnected">
<div class="repo-access-switch" role="group" aria-label="Repository connection" v-if="!isUpdate &amp;&amp; detectedType !== 'gist'">
<button type="button" :aria-pressed="githubConnection === 'github-app'" :disabled="githubConnection === 'github-app'" @click="chooseGitHubConnection('github-app')"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> <span class="sr-only">Read-only </span>GitHub App</button>
<button type="button" :aria-pressed="githubConnection === 'oauth'" :disabled="githubConnection === 'oauth'" @click="chooseGitHubConnection('oauth')">Legacy OAuth</button>
</div>
<span class="connection-badge" v-if="isUpdate || detectedType === 'gist'">{{ (detectedType === 'gist' ? githubConnections?.gistConnection === 'github-app' : githubConnection === 'github-app') ? 'Read-only GitHub App' : 'Legacy OAuth' }}</span>
</aside>
</div>
<div class="landing-url-row"><input id="sourceUrl-landing" onblur="this.scrollLeft=0" type="text" class="form-control form-control-lg" placeholder="https://github.com/owner/repository" v-field="{ state: viewState, set: value =&gt; { sourceUrl = value }, value: sourceUrl, form: null, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; }, change: () =&gt; { urlSelected() } }"><button type="button" class="btn btn-primary landing-continue" :aria-disabled="!sourceUrl?.trim() ? 'true' : null" @click="sourceUrl?.trim() && urlSelected()">Continue</button></div>
</div>
<p class="text-danger" role="alert" v-if="sourceUrlError">{{ sourceUrlError }}</p>
<small class="form-text landing-help" style="color: var(--ink-muted);">
Repository, pull request (…/pull/42) and gist (gist.github.com/…) URLs are all accepted.
</small>
<section class="repo-picker" aria-labelledby="repo-picker-title">
<div class="repo-picker-head">
<div class="repo-picker-title">
<h2 id="repo-picker-title">Your repositories</h2>
<span class="repo-picker-count" aria-live="polite" v-if="userReposStatus === 'ready' &amp;&amp; userRepos.length &gt; 0"><template v-if="userRepoFilter &amp;&amp; filteredUserRepos().length !== userRepos.length">{{ filteredUserRepos().length }} of </template>{{ userRepos.length }} {{ userRepos.length === 1 ? 'repository' : 'repositories' }}<span class="repo-picker-more"><span class="repo-picker-more-sep"> · </span>scroll for more</span></span>
</div>
<input type="search" class="form-control repo-picker-filter" placeholder="Filter…" aria-label="Filter your repositories" autocomplete="off" v-model="userRepoFilter" v-if="userReposStatus === 'ready' &amp;&amp; userRepos.length &gt; 0">
</div>
<ul class="repo-picker-list" v-if="userReposStatus === 'loading' || userReposStatus === 'idle'" aria-busy="true" aria-label="Loading your repositories">
<template v-for="(i, index) in [1, 2, 3, 4]"><li class="repo-picker-row is-skeleton" aria-hidden="true"><span class="skeleton skeleton-line" :style="{ width: (34 + i * 9) + '%' }"></span></li></template>
</ul>
<ul class="repo-picker-list" v-if="userReposStatus === 'ready' &amp;&amp; filteredUserRepos().length &gt; 0">
<template v-for="(repo, index) in filteredUserRepos()" :key="repo.id || repo.fullName"><li class="repo-picker-row">
<svg class="project-type-svg" aria-hidden="true"><use href="#project-type-repo"></use></svg>
<span class="repo-picker-name" :title="repo.fullName"><span class="repo-picker-owner">{{ repo.fullName.split('/')[0] }}<span class="repo-picker-slash">/</span></span><span class="repo-picker-repo">{{ repo.fullName.split('/').slice(1).join('/') }}</span></span>
<button type="button" class="btn btn-outline-ink btn-sm repo-picker-select" @click="selectUserRepo(repo)" :aria-label="'Select ' + repo.fullName">Select</button>
</li></template>
</ul>
<p class="repo-picker-state" v-if="userReposStatus === 'ready' &amp;&amp; userRepos.length &gt; 0 &amp;&amp; filteredUserRepos().length === 0">No repository matches “{{ userRepoFilter }}”.</p>
<p class="repo-picker-state" v-if="userReposStatus === 'ready' &amp;&amp; userRepos.length === 0">No repositories found for your GitHub account. Paste a URL above, or grant access below.</p>
<p class="repo-picker-state" role="alert" v-if="userReposStatus === 'error'">Your repositories could not be loaded. <button type="button" class="task-text-button" @click="loadUserRepos(true)">Try again</button></p>
<aside class="repo-access-details" aria-label="GitHub access" v-if="!sourceUrl &amp;&amp; githubConnections?.appEnabled &amp;&amp; githubConnections.appConnected">
<div class="repo-access-main">
<div class="repo-access-controls">
<span class="access-q" v-if="detectedType !== 'gist' &amp;&amp; githubConnection !== 'oauth'">Repository not listed?</span><button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' &amp;&amp; githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button>
<a class="connection-link" href="/connections" v-if="isUpdate || detectedType === 'gist' || githubConnection === 'oauth'">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></a>
</div>
</div>
<div class="repo-access-note"><span v-if="error &amp;&amp; detectedType !== 'gist' &amp;&amp; githubConnection === 'github-app'">Signing in does not grant access to private repositories. Use Manage GitHub access to select this repository, then return here. Organization access may require administrator approval.</span><span><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> Anonymized content is public, even from private repositories.</span></div>
</aside>
</section>
</div>
</div>
<!-- ===== STATE 2: URL provided — form (left) + preview (right) ===== -->
<div class="anonymize-workspace" v-show="workspaceOpen || isUpdate">
<header class="anonymize-topbar">
<div class="anonymize-topbar-inner">
<div class="paper-crumbs"><a href="/dashboard">My work</a> &nbsp;/&nbsp; <span class="here">{{ isUpdate ? 'Edit anonymization' : 'New anonymization' }}</span></div>
<div class="anonymize-topbar-head">
<h1 class="paper-page-title anonymize-topbar-title">
<span v-if="!isUpdate">New anonymization</span>
<span v-if="isUpdate">Edit anonymization</span>
</h1>
<span class="type-badge" v-show="detectedType" :class="{&#x27;type-repo&#x27;: detectedType === &#x27;repo&#x27;, &#x27;type-pr&#x27;: detectedType === &#x27;pr&#x27;, &#x27;type-gist&#x27;: detectedType === &#x27;gist&#x27;}">
{{ detectedType === 'repo' ? 'Repository' : detectedType === 'pr' ? 'Pull request' : 'Gist' }}
</span>
</div>
<p class="paper-page-lede anonymize-topbar-lede">{{ isUpdate ? 'Changes apply to the existing anonymous link; reviewers keep the same URL.' : 'Choose the terms to redact, what reviewers see, and when the link expires.' }}</p>
<ol class="task-trail flow-trail" aria-label="Anonymization steps" v-if="!isUpdate"><li>1. Repository</li><li aria-current="step">2. Review and hosting</li><li>3. Review link</li></ol>
</div>
</header>
<aside class="alert alert-info" role="status" v-if="detectedType === 'gist' &amp;&amp; gistAppRequired">
<p>Reconnect your GitHub account to read this gist. Your draft will be restored when you return.</p>
<button type="button" class="btn btn-ink" @click="connectGistApp()">Connect GitHub App to access gists</button>
</aside>
<aside class="alert alert-info" role="status" v-if="detectedType === 'gist' &amp;&amp; gistOAuthRequired">
<p>Connect GitHub OAuth to access gists. Your account and existing anonymizations stay the same, and your draft will be restored when you return.</p>
<p>GitHub's legacy OAuth connection includes write access to private repositories.</p>
<button type="button" class="btn btn-ink" @click="connectGistOAuth()" v-if="githubConnections?.oauthEnabled">Connect GitHub OAuth to access gists</button>
<p v-else>GitHub OAuth is currently unavailable. Please try again later.</p>
</aside>
<nav class="anonymize-mobile-tabs" role="tablist" aria-label="Anonymization workspace">
<button id="workspace-settings-tab" role="tab" type="button" :aria-selected="mobilePane === 'settings'" aria-controls="workspace-settings" :tabindex="mobilePane === 'settings' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="selectMobilePane('settings')"><span class="tab-label">Settings</span></button>
<button id="workspace-preview-tab" role="tab" type="button" :aria-selected="mobilePane === 'preview'" aria-controls="workspace-preview" :tabindex="mobilePane === 'preview' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="selectMobilePane('preview')"><span class="tab-label">Reviewer preview</span></button>
</nav>
<div class="anonymize-split">
<!-- Form column (left) -->
<div id="workspace-settings" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-settings-tab' : null" class="anonymize-form-col overflow-auto">
<form class="form needs-validation paper-settings-main" name="anonymize" novalidate v-form="viewState"><div class="anonymize-fields">
<section class="paper-settings-section">
<h2 class="paper-section-eyebrow">Source &amp; access</h2>
<div class="form-group">
<div class="source-url-label-row">
<label class="paper-field-label" for="sourceUrl">Source URL</label>
<aside class="repo-access" aria-label="GitHub access" v-if="sourceUrl &amp;&amp; githubConnections?.appEnabled &amp;&amp; githubConnections.appConnected &amp;&amp; githubConnections.oauthConnected">
<div class="repo-access-switch" role="group" aria-label="Repository connection" v-if="!isUpdate &amp;&amp; detectedType !== 'gist'">
<button type="button" :aria-pressed="githubConnection === 'github-app'" :disabled="githubConnection === 'github-app'" @click="chooseGitHubConnection('github-app')"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-shield"></use></svg> <span class="sr-only">Read-only </span>GitHub App</button>
<button type="button" :aria-pressed="githubConnection === 'oauth'" :disabled="githubConnection === 'oauth'" @click="chooseGitHubConnection('oauth')">Legacy OAuth</button>
</div>
<span class="connection-badge" v-if="isUpdate || detectedType === 'gist'">{{ (detectedType === 'gist' ? githubConnections?.gistConnection === 'github-app' : githubConnection === 'github-app') ? 'Read-only GitHub App' : 'Legacy OAuth' }}</span>
</aside>
</div>
<input type="text" class="form-control" name="sourceUrl" id="sourceUrl" onblur="this.scrollLeft=0" :disabled="isUpdate &amp;&amp; detectedType !== &#x27;repo&#x27;" placeholder="Paste a GitHub repo or pull request URL" v-field="{ state: viewState, set: value =&gt; { sourceUrl = value }, value: sourceUrl, form: &quot;anonymize&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; }, change: () =&gt; { urlSelected() } }" :class="{&#x27;is-invalid&#x27;: anonymize?.sourceUrl?.invalid}">
<p class="source-summary" v-if="sourceSummary() &amp;&amp; !anonymize?.sourceUrl?.invalid"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check"></use></svg>{{ sourceSummary() }}</p>
<div class="invalid-feedback" v-show="anonymize?.sourceUrl?.errors?.github">
Please provide a valid GitHub URL.
</div>
<div class="invalid-feedback" v-show="anonymize?.sourceUrl?.errors?.access">
Not accessible. The organization may restrict access.
</div>
<div class="invalid-feedback" v-show="anonymize?.sourceUrl?.errors?.missing">
Does not exist or is not accessible.
</div>
<div class="invalid-feedback" v-show="anonymize?.sourceUrl?.errors?.used">
Already anonymized.
</div>
</div>
<aside class="repo-access-details" aria-label="Previous repository configuration" v-if="!isUpdate &amp;&amp; previousConfigurations.length">
<p role="status">You have already anonymized this repository. Use the existing configuration to keep its anonymous link and avoid creating another repository.</p>
<div class="form-group" v-if="previousConfigurations.length > 1">
<label class="paper-field-label" for="previousConfiguration">Previous anonymization</label>
<select id="previousConfiguration" class="form-control" v-model="previousConfigurationId">
<option v-for="previous in previousConfigurations" :key="previous.repoId" :value="previous.repoId">{{ previous.repoId }}</option>
</select>
</div>
<p v-else>{{ previousConfigurationId }}</p>
<p>You can review and update its settings before saving.</p>
<div class="repo-access-controls">
<a class="btn btn-ink" :href="existingConfigurationUrl()">Use existing configuration</a>
<button type="button" class="btn btn-outline-ink" @click="previousConfigurations = []">Create a separate one</button>
</div>
</aside>
<aside class="repo-access-details" aria-label="GitHub access" v-if="sourceUrl &amp;&amp; githubConnections?.appEnabled &amp;&amp; githubConnections.appConnected">
<div class="repo-access-main">
<div class="repo-access-controls">
<span class="access-q" v-if="detectedType !== 'gist' &amp;&amp; githubConnection !== 'oauth' &amp;&amp; !details">Repository not listed?</span><button type="button" class="btn btn-outline-ink" v-if="detectedType !== 'gist' &amp;&amp; githubConnection !== 'oauth' &amp;&amp; !details" @click="grantGitHubAccess()">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button>
<a class="connection-link" href="/connections" v-if="isUpdate || detectedType === 'gist' || githubConnection === 'oauth'">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></a>
</div>
</div>
<div class="repo-access-note"><span v-if="error &amp;&amp; detectedType !== 'gist' &amp;&amp; githubConnection === 'github-app'">Signing in does not grant access to private repositories. Use Manage GitHub access to select this repository, then return here. Organization access may require administrator approval.</span><span v-if="detectedType !== 'gist' &amp;&amp; githubConnection !== 'oauth' &amp;&amp; !details">Select repositories on GitHub, then return here.</span><span><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> Selected content will be published, including content from private repositories.</span></div>
</aside>
<details class="repair-disclosure" v-if="isUpdate &amp;&amp; detectedType === 'repo' &amp;&amp; role === 'owner' &amp;&amp; sourceUrl === 'https://github.com/' + _originalFullName" :open="!!(error || accessRepairMessage || reconnectRepositoryId)">
<summary>Connection problems?</summary>
<div class="repair-step" v-if="accessRepairAllowed &amp;&amp; githubConnections?.appEnabled &amp;&amp; githubConnection === 'github-app' &amp;&amp; sourceUrl === 'https://github.com/' + _originalFullName">
<p>If this repository became private or its App installation changed, repair its access here. This keeps the same anonymous URL and published commit.</p>
<button type="button" class="btn btn-outline-ink" :disabled="repairingGitHubAccess" @click="repairGitHubAccess()">Repair GitHub access</button>
<button type="button" class="btn btn-outline-ink" @click="grantGitHubAccess()">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button>
<p v-if="accessRepairMessage" role="status">{{ accessRepairMessage }}</p>
</div>
<div class="repair-step">
<p>If you deleted and recreated this repository, reconnect it before choosing a branch.</p>
<button type="button" class="btn btn-outline-ink" @click="reconnectSource()">Reconnect source repository</button>
<p v-if="reconnectRepositoryId" role="status">Source reloaded. Review the branch, commit, and anonymization preview, then save to replace the published snapshot.</p>
</div>
</details>
<div class="form-grid-2" v-if="detectedType === &#x27;repo&#x27;">
<div class="form-group">
<label class="paper-field-label" for="branch">Branch</label>
<div class="input-group">
<select class="form-control" id="branch" name="branch" v-field="{ state: viewState, set: value =&gt; { source.branch = value }, value: source?.branch, form: &quot;anonymize&quot;, options: {} }">
<option v-for="(b, index) in branches" v-text="b?.name" :value="(b?.name)"></option>
</select>
<div class="input-group-append">
<button class="btn btn-icon refresh-branches" type="button" title="Refresh branches" @click="getBranches(true)" aria-label="Refresh branches">
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg>
</button>
</div>
</div>
</div>
<div class="form-group">
<label class="paper-field-label" for="commit">Commit</label>
<input class="form-control" id="commit" :disabled="options.update" name="commit" pattern="[a-fA-Z0-9]{6,}" required v-field="{ state: viewState, set: value =&gt; { source.commit = value }, value: source?.commit, form: &quot;anonymize&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: anonymize?.commit?.invalid}">
<div class="invalid-feedback" v-show="anonymize?.commit?.errors?.pattern || anonymize?.commit?.errors?.required">
The commit SHA is not valid.
</div>
<div class="invalid-feedback" v-show="anonymize?.commit?.errors?.exists">
This commit no longer exists in the repository. Click refresh to get the latest.
</div>
</div>
</div>
<div class="form-check auto-update-check" v-show="detectedType" :class="{ 'is-paid': !liveUpdatesIncluded() }">
<input class="form-check-input" type="checkbox" id="update" name="update" aria-describedby="update-help" v-field="{ state: viewState, set: value =&gt; { options.update = value }, value: options?.update, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="update">Auto-update from GitHub<span class="paid-tag" v-if="!liveUpdatesIncluded()">Paid</span></label>
<small class="form-text text-muted" id="update-help" v-if="liveUpdatesIncluded()">Follows the latest version, at most once an hour.</small>
<small class="form-text text-muted auto-update-paid" id="update-help" v-else>Needs Live updates<template v-if="liveUpdatesPrice()"> · {{ liveUpdatesPrice() }}</template>, a Paper pack or an author subscription. <a href="#repository-hosting" @click.prevent="showHostingOptions()">Hosting options</a></small>
</div>
</section>
<section class="paper-settings-section" v-show="detectedType">
<h2 class="paper-section-eyebrow">Anonymization</h2>
<div class="form-group" v-show="detectedType === &#x27;repo&#x27;">
<label class="paper-field-label" for="repoId">Anonymized repository ID</label>
<input type="text" class="form-control" name="repoId" id="repoId" :disabled="isUpdate" v-field="{ state: viewState, set: value =&gt; { repoId = value }, value: repoId, form: &quot;anonymize&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; } }" :class="{&#x27;is-invalid&#x27;: anonymize?.repoId?.invalid}">
<small class="form-text text-muted anon-url-echo">anonymous.4open.science/<wbr>r/<wbr><span class="anon-url-id">{{ repoId }}</span></small>
<div class="invalid-feedback" v-show="anonymize?.repoId?.errors?.format">ID can only contain letters and numbers.</div>
<div class="invalid-feedback" v-show="anonymize?.repoId?.errors?.used">{{ repoId }} is already used.</div>
</div>
<div class="form-group" v-show="detectedType === &#x27;pr&#x27;">
<label class="paper-field-label" for="pullRequestId">Anonymized pull request ID</label>
<input type="text" class="form-control" name="pullRequestId" id="pullRequestId" :disabled="isUpdate" v-field="{ state: viewState, set: value =&gt; { pullRequestId = value }, value: pullRequestId, form: &quot;anonymize&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; } }" :class="{&#x27;is-invalid&#x27;: anonymize?.pullRequestId?.invalid}">
<small class="form-text text-muted anon-url-echo">anonymous.4open.science/<wbr>pr/<wbr><span class="anon-url-id">{{ pullRequestId }}</span></small>
<div class="invalid-feedback" v-show="anonymize?.pullRequestId?.errors?.format">ID can only contain letters and numbers.</div>
<div class="invalid-feedback" v-show="anonymize?.pullRequestId?.errors?.used">{{ pullRequestId }} is already used.</div>
</div>
<div class="form-group" v-show="detectedType === &#x27;gist&#x27;">
<label class="paper-field-label" for="gistId">Anonymized gist ID</label>
<input type="text" class="form-control" name="gistId" id="gistId" :disabled="isUpdate" v-field="{ state: viewState, set: value =&gt; { gistId = value }, value: gistId, form: &quot;anonymize&quot;, options: { debounce: {default: 1000, blur: 0, click: 0}, updateOn: &#x27;default blur click&#x27; } }" :class="{&#x27;is-invalid&#x27;: anonymize?.gistId?.invalid}">
<small class="form-text text-muted anon-url-echo">anonymous.4open.science/<wbr>gist/<wbr><span class="anon-url-id">{{ gistId }}</span></small>
<div class="invalid-feedback" v-show="anonymize?.gistId?.errors?.format">ID can only contain letters and numbers.</div>
<div class="invalid-feedback" v-show="anonymize?.gistId?.errors?.used">{{ gistId }} is already used.</div>
</div>
<div class="form-group">
<label class="paper-field-label" for="terms">Terms to redact</label>
<textarea class="form-control terms-input" id="terms" name="terms" :rows="Math.min(14, Math.max(4, String(terms || &quot;&quot;).split(&quot;\n&quot;).length + 1))" placeholder="Jane Smith&#10;MIT CSAIL&#10;jane.smith@example.org" v-field="{ state: viewState, set: value =&gt; { terms = value }, value: terms, form: &quot;anonymize&quot;, options: { debounce: 250 } }" :class="{&#x27;is-invalid&#x27;: anonymize?.terms?.invalid}"></textarea>
<small class="form-text text-muted">One term per line.</small><details class="redaction-syntax"><summary>Regex &amp; replacement syntax</summary> <small class="form-text text-muted">Regex is supported. Replaced by <code>{{ site_options?.ANONYMIZATION_MASK }}-[N]</code>, or use <code>term=&gt;replacement</code> to pick your own (e.g. <code>Anonymous=&gt;ABC</code>).</small></details>
<div class="term-hint" v-for="hint in (termHints || []).slice(0, 3)" :key="hint.line">
<span>In <code>{{ hint.term }}</code>, <code>{{ hint.symbols.join(" ") }}</code> {{ hint.symbols.length > 1 ? "are regex symbols" : "is a regex symbol" }}, so it may not match the text as written.</span>
<button type="button" class="btn btn-outline-ink btn-sm" @click="matchTermAsWritten(hint.line)">Match as written</button>
</div>
<div class="term-hint-more" v-if="(termHints || []).length > 3">{{ termHints.length - 3 }} more {{ termHints.length - 3 === 1 ? "term uses" : "terms use" }} regex symbols.</div>
<div class="invalid-feedback" v-show="anonymize?.terms?.errors?.format">Terms are in an invalid format.</div>
</div>
<div class="preserve-row" v-show="detectedType === &#x27;repo&#x27;">
<div class="preserve-row-text">
<span class="paper-field-label" id="preserve-files-label">Unredacted files</span>
<span class="preserve-row-value">{{ preserveSummary() }}</span>
</div>
<button type="button" class="btn btn-outline-ink btn-sm" aria-describedby="preserve-files-label" :disabled="!source?.commit" @click="openPreserveFiles()">{{ (options?.preserveFiles || []).length ? "Edit" : "Choose files" }}</button>
</div>
<dialog id="preserve-files-dialog" class="preserve-dialog" v-if="preserve?.open" aria-labelledby="preserve-files-title" @cancel.prevent="closePreserveFiles()">
<form @submit.prevent="savePreserveFiles()">
<h2 id="preserve-files-title">Leave files unredacted</h2>
<p class="preserve-intro">Checked files are shown exactly as written, for example a bibliography where redacted self-citations would reveal you. File names are still redacted.</p>
<input type="search" id="preserve-files-search" class="form-control" placeholder="Search files" aria-label="Search files" autocomplete="off" v-model="preserve.search">
<div class="preserve-tree">
<p class="preserve-status" v-if="preserve.loading">Loading files from GitHub…</p>
<p class="paper-inline-alert paper-inline-alert-error" role="alert" v-else-if="preserve.error">{{ preserve.error }}</p>
<template v-else>
<div class="preserve-item" v-for="(row, i) in preserveRows()" :key="row.kind + ':' + row.path" :style="{ paddingLeft: (row.depth * 18 + 6) + &quot;px&quot; }">
<button type="button" class="preserve-caret" v-if="row.kind === &#x27;dir&#x27;" :aria-expanded="row.open ? &quot;true&quot; : &quot;false&quot;" :aria-label="(row.open ? &quot;Collapse &quot; : &quot;Expand &quot;) + row.label" @click="togglePreserveDir(row.path)">{{ row.open ? "▾" : "▸" }}</button>
<span class="preserve-caret-space" v-else></span>
<input type="checkbox" :id="&quot;pf-&quot; + i" :aria-label="row.kind === &#x27;dir&#x27; ? &quot;Leave folder &quot; + row.path + &quot; unredacted&quot; : null" :checked="row.state === &#x27;all&#x27;" :indeterminate.prop="row.state === &#x27;some&#x27;" :disabled="row.locked" @change="row.kind === &#x27;dir&#x27; ? togglePreserveFolder(row.path) : togglePreserveFile(row.path)">
<label :for="row.kind === &#x27;file&#x27; ? &quot;pf-&quot; + i : null" class="preserve-name" @click="row.kind === &#x27;dir&#x27; &amp;&amp; togglePreserveDir(row.path)">{{ row.label }}</label>
<span class="preserve-note" v-if="row.note">{{ row.note }}</span>
</div>
<p class="preserve-status" v-if="!preserve.loading &amp;&amp; !preserveRows().length">{{ preserve.search ? "No file matches your search." : "No text files in this commit." }}</p>
</template>
</div>
<p class="preserve-status" v-if="preserve.truncated">This repository has too many files to list them all. Use a pattern for files you cannot find.</p>
<details class="preserve-patterns" :open="preserve.patternsOpen">
<summary>Patterns</summary>
<textarea class="form-control" rows="3" aria-label="Patterns" placeholder="*.bib&#10;paper/appendix/**" v-model="preserve.patterns"></textarea>
<small class="form-text text-muted">One per line, as in .gitignore: <code>*.bib</code> matches in any folder; <code>/refs.bib</code> and <code>paper/refs.bib</code> match from the repository root; a folder name such as <code>drafts/</code> covers everything inside it; <code>!private.bib</code> excludes again.</small>
</details>
<div class="preserve-foot">
<span class="preserve-count" aria-live="polite">{{ preserve.loading || preserve.error ? "" : preserveCount() + (preserveCount() === 1 ? " file" : " files") + " will be shown as written" }}</span>
<span class="preserve-actions">
<button type="button" class="btn btn-outline-ink" @click="closePreserveFiles()">Cancel</button>
<button type="submit" class="btn btn-ink">Save</button>
</span>
</div>
</form>
</dialog>
</section>
<section class="paper-settings-section" v-show="detectedType">
<h2 class="paper-section-eyebrow">Display</h2>
<div v-show="detectedType === &#x27;repo&#x27;">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="link" name="link" v-field="{ state: viewState, set: value =&gt; { options.link = value }, value: options?.link, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="link">Keep links</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="image" name="image" v-field="{ state: viewState, set: value =&gt; { options.image = value }, value: options?.image, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="image">Display images</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="pdf" name="pdf" v-field="{ state: viewState, set: value =&gt; { options.pdf = value }, value: options?.pdf, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="pdf">Display PDFs</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="notebook" name="notebook" v-field="{ state: viewState, set: value =&gt; { options.notebook = value }, value: options?.notebook, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="notebook">Display notebooks</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="page" name="page" v-field="{ state: viewState, set: value =&gt; { options.page = value }, value: options?.page, form: &quot;anonymize&quot;, options: {} }" :disabled="!details?.hasPage || (details?.pageSource &amp;&amp; details?.pageSource?.branch !== source?.branch)">
<label class="form-check-label" for="page">GitHub Pages</label>
<small class="form-text text-muted" v-if="!details?.hasPage">
{{ fmt?.translate('WARNINGS.page_not_enabled_on_repo') }}
</small>
<small class="form-text text-muted" v-if="details?.hasPage &amp;&amp; details?.pageSource?.branch &amp;&amp; source?.branch &amp;&amp; details.pageSource.branch !== source.branch">
{{ fmt?.translate('WARNINGS.page_branch_mismatch', { pageBranch: details?.pageSource?.branch, selectedBranch: source?.branch }) }}
</small>
</div>
</div>
<p class="form-text text-muted" v-if="detectedType === 'repo' &amp;&amp; (options.image || options.pdf)">Images and PDFs are served unchanged. Review their contents and metadata before sharing. <a href="/faq#formats">Supported formats</a></p>
<div v-show="detectedType === &#x27;gist&#x27;">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="title-gist" name="title-gist" v-field="{ state: viewState, set: value =&gt; { options.title = value }, value: options?.title, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="title-gist">Gist description</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="content-gist" name="content-gist" v-field="{ state: viewState, set: value =&gt; { options.content = value }, value: options?.content, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="content-gist">File contents</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="comments-gist" name="comments-gist" v-field="{ state: viewState, set: value =&gt; { options.comments = value }, value: options?.comments, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="comments-gist">Comments</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="username-gist" name="username-gist" v-field="{ state: viewState, set: value =&gt; { options.username = value }, value: options?.username, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="username-gist">Usernames</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="date-gist" name="date-gist" v-field="{ state: viewState, set: value =&gt; { options.date = value }, value: options?.date, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="date-gist">Dates</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="origin-gist" name="origin-gist" v-field="{ state: viewState, set: value =&gt; { options.origin = value }, value: options?.origin, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="origin-gist">Source gist ID</label>
</div>
</div>
<div v-show="detectedType === &#x27;pr&#x27;">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="title" name="title" v-field="{ state: viewState, set: value =&gt; { options.title = value }, value: options?.title, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="title">PR title</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="body" name="body" v-field="{ state: viewState, set: value =&gt; { options.body = value }, value: options?.body, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="body">PR body</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="diff" name="diff" v-field="{ state: viewState, set: value =&gt; { options.diff = value }, value: options?.diff, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="diff">Diff</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="comments" name="comments" v-field="{ state: viewState, set: value =&gt; { options.comments = value }, value: options?.comments, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="comments">Comments</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="username" name="username" v-field="{ state: viewState, set: value =&gt; { options.username = value }, value: options?.username, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="username">Usernames</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="date" name="date" v-field="{ state: viewState, set: value =&gt; { options.date = value }, value: options?.date, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="date">Dates</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="origin" name="origin" v-field="{ state: viewState, set: value =&gt; { options.origin = value }, value: options?.origin, form: &quot;anonymize&quot;, options: {} }">
<label class="form-check-label" for="origin">Project name</label>
</div>
</div>
</section>
<section class="paper-settings-section hosting-settings" id="repository-hosting" v-show="detectedType" aria-labelledby="hosting-heading">
<h2 class="paper-section-eyebrow" id="hosting-heading">Hosting &amp; expiration</h2>
<div class="form-group conference-field">
<label class="paper-field-label" for="conference">Conference code <span class="paper-optional">(optional)</span></label>
<input class="form-control" id="conference" name="conference" v-field="{ state: viewState, set: value =&gt; { conference = value }, value: conference, form: &quot;anonymize&quot;, options: { debounce: { default: 800, blur: 0 }, updateOn: &#x27;default blur&#x27; } }" :class="{&#x27;is-invalid&#x27;: anonymize?.conference?.invalid}">
<p class="conference-hosting" :class="{ 'is-covered': conferenceHosting() }" v-show="conference_data" role="status">
<a target="_blank" :href="safeUrl((conference_data?.url))">{{ conference_data?.name }}</a>
<template v-if="conferenceHosting()"> hosts this repository until {{ fmt?.date(conference_data?.endDate) }}, with automatic updates. You don't pay anything.</template>
<template v-else-if="billing?.enabled &amp;&amp; detectedType === 'repo'"> runs until {{ fmt?.date(conference_data?.endDate) }}. It doesn't pay for hosting, so this repository uses your own allowance.</template>
<template v-else> runs until {{ fmt?.date(conference_data?.endDate) }}.</template>
</p>
<div class="invalid-feedback" v-show="anonymize?.conference?.errors?.activated">The conference is not activated.</div>
<small class="form-text text-muted" v-show="!conference_data">Submitting to a conference? Its code applies the venue's settings, and a paid conference hosts the repository for you.</small>
</div>
<p class="hosting-usage" v-if="detectedType === 'repo' &amp;&amp; billing?.enabled &amp;&amp; !conferenceHosting()">
<span v-if="isUpdate &amp;&amp; role !== 'owner'">Uses the owner's hosting</span>
<span v-else-if="hasAuthorHosting()">Your author subscription is active.</span>
<span v-else-if="hostingQuota?.repository">{{ hostingQuota.repository.used }} of {{ hostingQuota.repository.total }} active repositories · Free</span>
</p>
<p class="hosting-limit auto-update-note" role="status" v-if="detectedType === 'repo' &amp;&amp; options?.update &amp;&amp; !liveUpdatesIncluded()">Automatic updates need a paid option. On free hosting, the link keeps the version you create until you refresh it from My work. Once it exists, add Live updates<template v-if="liveUpdatesPrice()"> ({{ liveUpdatesPrice() }} once)</template> or a Paper pack in <a href="/profile?billing=1">Settings › Hosting &amp; billing</a>, or take an author subscription.</p>
<div class="hosting-payment" v-if="detectedType === 'repo' &amp;&amp; billing?.enabled">
<template v-if="hostingNeedsPayment() &amp;&amp; !billingPending">
<p class="hosting-limit" role="status" v-if="options.expirationMode !== 'never' || hostingQuotaBlocked">{{ options.update &amp;&amp; !hostingQuotaBlocked &amp;&amp; hostingQuota?.repository?.used &lt; hostingQuota?.repository?.total ? 'Automatic updates need a paid option for this repository.' : 'Your free repositories are all in use.' }}</p>
<fieldset class="hosting-options" v-if="billing.checkoutEnabled !== false &amp;&amp; hostingOptionPlans().length">
<legend class="hosting-options-title">{{ hostingOneTime()?.id === 'repo_never' ? 'Keep this link online' : 'Choose how to host this repository' }}</legend>
<label class="hosting-option" v-for="plan in hostingOptionPlans()" :key="plan.id" :class="{ 'is-selected': hostingChoice() === plan.id || (plan.interval &amp;&amp; authorPlans().some(p =&gt; p.id === hostingChoice())) }">
<input type="radio" name="hosting-plan" :value="plan.id" :checked="hostingChoice() === plan.id || (plan.interval &amp;&amp; authorPlans().some(p =&gt; p.id === hostingChoice()))" @change="hostingPlan = plan.interval ? hostingSubscriptionPlan().id : plan.id">
<span class="hosting-option-body"><strong>{{ hostingOptionName(plan) }}</strong><small>{{ hostingOptionBlurb(plan) }}</small>
<span class="hosting-cycle billing-toggle" v-if="plan.interval &amp;&amp; authorPlans().length &gt; 1 &amp;&amp; authorPlans().some(p =&gt; p.id === hostingChoice())" role="radiogroup" aria-label="Billing frequency"><button type="button" role="radio" v-for="cycle in authorPlans()" :key="cycle.id" :aria-checked="hostingSubscriptionPlan().id === cycle.id ? 'true' : 'false'" :class="{ 'is-on': hostingSubscriptionPlan().id === cycle.id }" @click.prevent="hostingCycle = cycle.id; hostingPlan = cycle.id">{{ cycle.interval === 'year' ? 'Yearly' : 'Monthly' }}<span class="billing-toggle-save" v-if="cycle.interval === 'year' &amp;&amp; hostingAnnualSavings()">−{{ hostingAnnualSavings() }}%</span></button></span></span>
<span class="hosting-option-price"><strong>{{ billingAmount(plan.interval ? hostingSubscriptionPlan() : plan) }}</strong><small>{{ plan.interval ? 'per ' + hostingSubscriptionPlan().interval : 'once' }}</small></span>
</label>
</fieldset>
<p v-else role="status">New purchases are temporarily unavailable. Your draft is kept.</p>
<p class="hosting-terms" v-if="authorPlans().some(p =&gt; p.id === hostingChoice())">Renews {{ hostingChoice() === 'author_annual' ? 'yearly' : 'monthly' }} until you cancel. Tax shown at checkout.</p>
<p class="hosting-terms" v-else>One-time payment, nothing renews. Tax shown at checkout.</p>
<details class="capacity-options" v-if="options.expirationMode !== 'never'"><summary>Keep using free hosting</summary><p>Remove a repository you no longer need, or enter the code of a paid conference above. Removing a repository affects its review link.</p><a href="/dashboard">Review my repositories</a><button type="button" class="task-text-button" @click="anonymizeRepo($event)" :disabled="sourceLoading || billingBusy">Recheck capacity and submit</button></details>
</template>
</div>
<h4 class="hosting-subhead" v-if="hostingNeedsPayment() &amp;&amp; !billingPending">Expiration</h4>
<fieldset class="expiration-choices">
<legend class="sr-only">Expiration</legend>
<label class="expiration-choice"><input type="radio" name="expiration-choice" :checked="options.expirationMode !== 'never'" @change="useDatedHosting()" :disabled="billingPending"><span>Expire on a date</span></label>
<div class="dated-options" v-if="options.expirationMode !== 'never'">
<div class="hosting-date" v-if="options?.expirationMode !== 'never'">
<label class="paper-field-label" for="expirationDate">Expires</label>
<input class="form-control" type="date" name="expirationDate" id="expirationDate" required v-field="{ state: viewState, set: value =&gt; { options.expirationDate = value }, value: options?.expirationDate, form: &quot;anonymize&quot;, options: {} }" :class="{&#x27;is-invalid&#x27;: anonymize?.expirationDate?.invalid}" :min="(minExpirationDate)" :max="(maxExpirationDate)">
<p class="date-echo" aria-live="polite" v-if="fmt?.dateEcho(options?.expirationDate)">Expires {{ fmt?.dateEcho(options?.expirationDate) }}</p>
<div class="invalid-feedback" v-show="anonymize?.expirationDate?.errors?.min">Pick a date in the future.</div>
<div class="invalid-feedback" v-show="anonymize?.expirationDate?.errors?.max">Pick a date on or before {{ fmt?.date(maxExpirationDate) }}.</div>
<div class="invalid-feedback" v-show="anonymize?.expirationDate?.errors?.required || anonymize?.expirationDate?.errors?.date">Enter a valid expiration date.</div>
</div>
<div class="hosting-property"><label for="expiration">Then</label><select id="expiration" class="form-control" v-model="options.expirationMode"><option value="remove">Remove content</option><option value="redirect">Redirect to GitHub</option></select></div>
</div>
<label class="expiration-choice" v-if="detectedType === 'repo'"><input type="radio" name="expiration-choice" :checked="options.expirationMode === 'never'" @change="options.expirationMode = 'never'" :disabled="billingPending"><span>Never expire<small v-if="(options.expirationMode !== 'never' || hasAuthorHosting() || repositoryGrants('repo_never').length) &amp;&amp; !(hostingNeedsPayment() &amp;&amp; hostingOptionPlans().length)">{{ neverExpireTag() }}</small></span></label>
</fieldset>
<p class="form-text text-muted" v-if="options.expirationMode === 'redirect'">Visitors will be redirected to the original GitHub source on this date, revealing its identity.</p>
<div v-if="detectedType === 'repo' &amp;&amp; billing?.enabled" class="inline-hosting">
<p v-if="taskValidationError" role="alert" class="text-danger">{{ taskValidationError }}</p>
<p v-if="billingError" role="alert" class="text-danger">{{ billingError }} <button type="button" class="task-text-button" @click="loadBilling()" :disabled="billingBusy">Reload billing</button></p>
<div v-if="billingPending" role="status"><p><strong>Confirming your payment…</strong></p><p class="form-text text-muted">Your draft is saved. Please do not pay again.</p></div>
<p v-else-if="billingNotice" role="status" class="form-text text-muted">{{ billingNotice }}</p>
<p class="hosting-terms" v-else-if="options.expirationMode === 'never' &amp;&amp; !billingPending &amp;&amp; !hasAuthorHosting() &amp;&amp; repositoryGrants('repo_never').length">No scheduled expiration. Paid once for this repository.</p>
<p class="hosting-terms" v-else-if="options.expirationMode === 'never' &amp;&amp; !billingPending">No scheduled expiration while subscribed. A 90-day grace period applies if paid access ends.</p>
<details class="repository-pass" v-if="isUpdate &amp;&amp; role === 'owner' &amp;&amp; !hasAuthorHosting() &amp;&amp; !hostingNeedsPayment() &amp;&amp; repositoryAddonPlans().length"><summary>Pay once for just this repository</summary><div v-for="plan in repositoryAddonPlans()" :key="plan.id"><p><strong>{{ plan.name }}</strong> · {{ billingPrice(plan) }}. {{ plan.description }}</p><button type="button" class="btn btn-outline-ink" :disabled="billingBusy || hostingSaving || sourceLoading || billing.checkoutEnabled === false || billingPending" @click="checkoutRepository(plan.id)">Buy {{ plan.name.toLowerCase() }}</button></div></details>
</div>
</section>
<section class="paper-settings-section" v-show="isUpdate &amp;&amp; detectedType === &#x27;repo&#x27;">
<h2 class="paper-section-eyebrow">Co-authors</h2>
<p class="form-text text-muted" style="margin-bottom: 8px;">
Co-authors can view and edit these settings. They cannot delete the anonymization or manage co-authors.
</p>
<div class="form-group" v-show="role === &#x27;owner&#x27; || role === &#x27;admin&#x27;">
<label class="paper-field-label" for="coauthorSearch">Add a GitHub user</label>
<div style="position: relative;">
<input type="text" id="coauthorSearch" class="form-control" placeholder="Search GitHub username…" autocomplete="off" v-field="{ state: viewState, set: value =&gt; { coauthorSearch = value }, value: coauthorSearch, form: &quot;anonymize&quot;, options: { debounce: 300 }, change: () =&gt; { searchCoauthors() } }">
<div class="dropdown-menu show" style="display: block; max-height: 220px; overflow-y: auto; width: 100%;" v-show="coauthorResults?.length &gt; 0">
<a href="#" class="dropdown-item d-flex align-items-center" v-for="(u, index) in coauthorResults" @click.prevent="addCoauthor(u, $event)">
<img alt="" style="width: 22px; height: 22px; border-radius: 50%; margin-right: 8px;" :src="safeUrl((u?.photo))">
<span v-text="u?.username"></span>
</a>
</div>
</div>
<small class="form-text text-muted" v-show="coauthorError" v-text="coauthorError"></small>
</div>
<div class="coauthor-list">
<div class="coauthor-row d-flex align-items-center" style="padding: 6px 0; gap: 8px;" v-for="(c, index) in coauthors">
<img alt="" style="width: 24px; height: 24px; border-radius: 50%;" v-if="c?.photo" :src="safeUrl((c?.photo))">
<a target="_blank" v-text="c?.username" :href="safeUrl(&quot;https://github.com/&quot; + (c?.username))"></a>
<span class="type-badge type-coauthor">Co-author</span>
<button type="button" class="btn btn-sm btn-icon" style="margin-left: auto;" title="Remove co-author" v-show="role === &#x27;owner&#x27; || role === &#x27;admin&#x27;" @click="removeCoauthor(c)" aria-label="Remove co-author">
<svg class="ui-icon" aria-hidden="true"><use href="#project-icon-close"></use></svg>
</button>
</div>
<div class="form-text text-muted" v-show="!coauthors || coauthors?.length === 0">
No co-authors yet.
</div>
</div>
</section>
<div class="alert alert-danger" role="alert" v-if="error" v-text="error"></div>
</div>
<button type="button" class="btn btn-outline-ink mobile-preview-jump" @click="selectMobilePane('preview')">Review preview <span aria-hidden="true">→</span></button>
<div class="anonymize-submit-bar" v-show="detectedType">
<template v-if="detectedType === 'repo'">
<button type="button" class="btn btn-ink" v-if="billingPending" @click="refreshBilling()" :disabled="billingBusy">Check payment status</button>
<button type="submit" class="btn btn-ink" v-else :disabled="sourceLoading || billingBusy || hostingSaving || billingLoading || (hostingNeedsPayment() &amp;&amp; ((!authorPlans().length &amp;&amp; !hostingOneTime()) || billing?.checkoutEnabled === false))" @click.prevent="submitForm($event, () =&gt; { submitRepository($event) })">{{ hostingSaving ? 'Saving draft…' : hostingNeedsPayment() ? 'Continue to checkout' : isUpdate ? 'Save changes' : 'Create anonymous link' }}</button>
</template>
<button type="submit" :disabled="sourceLoading || !details" class="btn btn-ink" v-if="detectedType === &#x27;pr&#x27; &amp;&amp; !isUpdate" @click.prevent="submitForm($event, () =&gt; { anonymizePullRequest($event) })">
Create anonymous link
</button>
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === &#x27;pr&#x27; &amp;&amp; isUpdate" @click.prevent="submitForm($event, () =&gt; { anonymizePullRequest($event) })">
Save changes
</button>
<button type="submit" :disabled="sourceLoading || !details" class="btn btn-ink" v-if="detectedType === &#x27;gist&#x27; &amp;&amp; !isUpdate" @click.prevent="submitForm($event, () =&gt; { anonymizeGist($event) })">
Create anonymous link
</button>
<button type="submit" :disabled="sourceLoading" class="btn btn-ink" v-if="detectedType === &#x27;gist&#x27; &amp;&amp; isUpdate" @click.prevent="submitForm($event, () =&gt; { anonymizeGist($event) })">
Save changes
</button>
<p class="submit-reason" role="status" v-if="sourceLoading">{{ detectedType === 'pr' ? 'Waiting for the pull request to load from GitHub…' : detectedType === 'gist' ? 'Waiting for the gist to load from GitHub…' : 'Waiting for the repository to load from GitHub…' }}</p>
<p class="submit-reason" role="status" v-else-if="!details &amp;&amp; !isUpdate">Load an accessible source to continue. See the preview for details.</p>
<p class="checkout-return-note" v-if="hostingNeedsPayment() &amp;&amp; !billingPending">You pay in Stripe, then come back here to finish.</p>
<div class="draft-status" role="status" v-if="draftRestored">Your unsaved draft was restored. <button type="button" class="draft-discard" @click="discardDraft()">Discard draft</button></div>
<div class="draft-status" v-else-if="unsavedChanges">{{ draftSaved ? "Draft saved ·" : "Unsaved changes ·" }} <button type="button" class="draft-discard" @click="discardDraft()">{{ isUpdate ? "Restore saved" : "Discard" }}</button></div>
</div>
</form>
</div>
<!-- Preview column (right): "What reviewers will see", a miniature reviewer window -->
<div id="workspace-preview" :role="mobileWorkspace ? 'tabpanel' : null" :aria-labelledby="mobileWorkspace ? 'workspace-preview-tab' : null" class="anonymize-preview-col reviewer-preview" v-if="detectedType">
<div class="rp-caption">
<span class="rp-title">What reviewers will see</span>
<span class="rp-count" v-if="previewStatus === 'ready' &amp;&amp; previewRedactions() &gt; 0" :title="previewRedactions() + ' redacted terms in this preview'"><span class="redact-slot" aria-hidden="true"><i>{{ previewRedactions() }}</i></span> redacted</span>
<span class="rp-count rp-count-none" v-else-if="previewStatus === 'ready' &amp;&amp; !String(terms || '').trim()">No terms yet</span>
<span class="rp-count rp-count-none" v-else-if="previewStatus === 'ready' &amp;&amp; readmePreserved()">README left unredacted</span>
<span class="rp-spacer"></span>
<a class="rp-open is-external" v-if="isUpdate &amp;&amp; previewUrl()" :href="safeUrl(previewPath())" target="_blank" rel="noopener">Open reviewer page <span aria-hidden="true">↗</span></a>
<button type="button" class="rp-open is-toggle" v-else @click="previewExpanded = !previewExpanded" :aria-pressed="!!previewExpanded"><svg class="rp-open-icon" viewBox="0 0 16 16" aria-hidden="true"><path v-if="!previewExpanded" d="M9.5 2.5h4v4M13.5 2.5 9 7M6.5 13.5h-4v-4M2.5 13.5 7 9"/><path v-else d="M13.5 6.5h-4v-4M9.5 6.5 14 2M2.5 9.5h4v4M6.5 9.5 2 14"/></svg>{{ previewExpanded ? 'Back to settings' : 'Expand preview' }}</button>
</div>
<div class="rp-window">
<div class="rp-chrome" aria-hidden="true"><i></i><i></i><i></i><span class="rp-url">{{ previewUrl() || 'anonymous.4open.science' }}</span></div>
<div class="rp-bar" aria-hidden="true"><span class="vb-logo">Anonymous <em>GitHub</em></span><span class="rp-bar-id" v-if="previewId()">{{ previewId() }}</span><span class="rp-bar-kind" v-if="detectedType !== 'repo'">{{ detectedType === 'pr' ? 'Pull request' : 'Gist' }}</span></div>
<div class="rp-body" :class="{ 'has-side': detectedType === 'repo' }">
<!-- Sidebar: a miniature of the explorer's file tree -->
<nav class="rp-side" v-if="detectedType === 'repo'" aria-label="Files reviewers will see">
<ul class="files rp-files" v-if="previewTree().length">
<li v-for="entry in previewTree()" :key="entry.path" class="file" :class="{ folder: entry.dir, open: entry.open, active: entry.readme }" :style="{ '--depth': entry.depth }">
<a v-if="entry.dir" role="button" tabindex="0" :aria-expanded="entry.open ? 'true' : 'false'" :title="entry.path" @click="togglePreviewFolder(entry.path)" @keydown.enter.prevent="togglePreviewFolder(entry.path)" @keydown.space.prevent="togglePreviewFolder(entry.path)"><span class="tree-toggle"></span><span class="tree-icon-folder" aria-hidden="true"><svg viewBox="0 0 14 12" focusable="false"><path d="M1.5 10.5v-9h4l1.5 2h5.5v7z"/></svg></span><span class="tree-name">{{ entry.name }}</span></a>
<a v-else :title="entry.path" :aria-current="entry.readme ? 'page' : null"><span class="tree-spacer"></span><span class="far fa-file tree-icon-file" aria-hidden="true"></span><span class="tree-name">{{ entry.name }}</span></a>
</li>
</ul>
<ul class="rp-files-skel" v-else-if="preserve.loading || (sourceLoading &amp;&amp; !preserve.error)" aria-label="Loading the file list"><li v-for="w in [58, 72, 46, 64, 52, 38, 68, 44]" :key="w"><i class="rp-skel" :style="{ width: w + '%' }"></i></li></ul>
<template v-else>
<ul class="files rp-files" v-if="readme || previewStatus === 'ready'"><li class="file active"><a title="README.md" aria-current="page"><span class="tree-spacer"></span><span class="far fa-file tree-icon-file" aria-hidden="true"></span><span class="tree-name">README.md</span></a></li></ul>
<p class="rp-side-note">Every other file at this commit appears here, with the same redactions.</p>
</template>
</nav>
<div class="rp-doc">
<div class="rp-file-head" v-if="detectedType === 'repo' &amp;&amp; (previewStatus === 'ready' || readme)"><span class="rp-files-btn" aria-hidden="true"><svg class="ui-icon"><use href="#ui-icon-folder"></use></svg>Files<span class="rp-files-count" v-if="previewTreeCount()">{{ previewTreeCount() }}</span></span><span class="rp-file-name">README.md</span><span class="rp-file-meta">Markdown</span></div>
<!-- Loading: skeleton inside the window -->
<div class="rp-state rp-loading" role="status" v-if="previewStatus === 'loading' || (sourceLoading &amp;&amp; previewStatus !== 'ready' &amp;&amp; previewStatus !== 'error')">
<span class="rp-state-text">{{ previewStatus === 'loading' ? 'Updating the anonymized preview…' : (detectedType === 'pr' ? 'Loading the pull request from GitHub…' : detectedType === 'gist' ? 'Loading the gist from GitHub…' : 'Loading the repository from GitHub…') }}</span>
<i class="rp-skel rp-skel-title"></i><i class="rp-skel" style="width:92%"></i><i class="rp-skel" style="width:86%"></i><i class="rp-skel" style="width:58%"></i>
<i class="rp-skel rp-skel-block"></i><i class="rp-skel" style="width:80%"></i><i class="rp-skel" style="width:66%"></i>
</div>
<!-- The source itself could not be loaded -->
<div class="rp-state rp-error" role="alert" v-else-if="!details &amp;&amp; detectedType !== 'repo'">
<strong>Couldn't load {{ sourceSummary() || (detectedType === 'pr' ? 'this pull request' : 'this gist') }}</strong>
<p>{{ error || 'GitHub did not return it. Check that the link is right and that Anonymous GitHub can read it.' }}</p>
<div class="rp-state-actions"><button type="button" class="btn btn-ink btn-sm" @click="urlSelected(true)">Retry</button><button type="button" class="btn btn-outline-ink btn-sm" v-if="detectedType === 'pr' &amp;&amp; githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button><a class="btn btn-outline-ink btn-sm" v-else href="/connections">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></a></div>
</div>
<div class="rp-state rp-error" role="alert" v-else-if="!details &amp;&amp; detectedType === 'repo' &amp;&amp; !isUpdate &amp;&amp; error">
<strong>Couldn't load this repository</strong>
<p>{{ error }}</p>
<div class="rp-state-actions"><button type="button" class="btn btn-ink btn-sm" @click="urlSelected(true)">Retry</button><button type="button" class="btn btn-outline-ink btn-sm" v-if="githubConnection !== 'oauth'" @click="grantGitHubAccess()">Manage GitHub access <svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg></button></div>
</div>
<!-- The anonymized preview failed -->
<div class="rp-state rp-error" role="alert" v-else-if="previewStatus === 'error'">
<strong>Couldn't apply the redactions</strong>
<p>Unable to generate the anonymized preview. The latest redactions have not been verified.</p>
<div class="rp-state-actions"><button class="btn btn-ink btn-sm" type="button" @click="retryPreview()">Retry preview</button></div>
</div>
<div class="rp-state rp-empty" v-else-if="detectedType === 'repo' &amp;&amp; !readme &amp;&amp; previewStatus === 'idle'">
<strong>No README</strong>
<p>Reviewers land on the file list. Open the files you will share and check them before sending the link.</p>
</div>
<div class="anonymize-preview-body markdown-body body" v-else-if="detectedType === 'repo' &amp;&amp; previewStatus === 'ready'" v-html="renderRedacted(sanitize(html_readme))"></div>
<div class="anonymize-preview-body" v-else-if="detectedType === 'gist' &amp;&amp; previewStatus === 'ready'">
<div class="d-flex w-100 justify-content-between align-items-center flex-wrap">
<h2 class="pr-title mb-1">
<span v-if="options?.title">{{ anonymizeGistContent(details?.gist?.description) || 'Untitled gist' }}</span>
<span class="badge" :class="{&#x27;badge-success&#x27;: details?.gist?.isPublic, &#x27;badge-secondary&#x27;: !details?.gist?.isPublic}">
{{ details?.gist?.isPublic ? 'public' : 'secret' }}
</span>
</h2>
<small v-if="options?.date" v-text="fmt?.date(details?.gist?.updatedDate)"></small>
</div>
<small v-if="options?.origin">Gist ID: {{ details?.source?.gistId }}</small>
<small v-if="options?.username &amp;&amp; details?.gist?.ownerLogin">By @{{ anonymizeGistContent(details?.gist?.ownerLogin) }}</small>
<div v-if="options?.content &amp;&amp; previewGistFiles?.length">
<ul class="pr-comments mt-3">
<li class="pr-comment" v-for="(file, index) in previewGistFiles">
<div class="pr-comment-head">
<strong v-text="file?.filename"></strong>
<span class="pr-comment-date" v-if="file?.language">{{ file?.language }}</span>
</div>
<gist-file :file="file" :terms="terms" :options="options"></gist-file>
</li>
</ul>
</div>
<div v-if="options?.comments &amp;&amp; details?.gist?.comments &amp;&amp; details?.gist?.comments?.length">
<h3 class="paper-section-eyebrow mt-3">Comments</h3>
<ul class="pr-comments">
<li class="pr-comment" v-for="(comment, index) in details?.gist?.comments">
<div class="pr-comment-head">
<span class="pr-comment-author" v-if="options?.username">
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user"></use></svg> @<span v-text="anonymizeGistContent(comment?.author)"></span>
</span>
<span class="pr-comment-date" v-if="options?.date" v-text="fmt?.date(comment?.updatedDate)"></span>
</div>
<div class="pr-comment-body" v-if="options?.body">
<markdown :content="anonymizeGistContent(comment?.body)" :options="options" :terms="terms"></markdown>
</div>
</li>
</ul>
</div>
</div>
<div class="anonymize-preview-body" v-else-if="detectedType === 'pr' &amp;&amp; previewStatus === 'ready'">
<div class="d-flex w-100 justify-content-between align-items-center flex-wrap">
<h2 class="pr-title mb-1">
<span v-if="options?.title">{{ anonymizePrContent(details?.pullRequest?.title) }}</span>
<span class="badge" :class="{&#x27;badge-success&#x27;:details?.pullRequest?.merged, &#x27;badge-warning&#x27;:details?.pullRequest?.state==&#x27;open&#x27;, &#x27;badge-danger&#x27;:details?.pullRequest?.state==&#x27;closed&#x27; &amp;&amp; !details?.pullRequest?.merged}">
{{ fmt?.title(details?.pullRequest?.merged ? "merged" : details?.pullRequest?.state) }}
</span>
</h2>
<small v-if="options?.date" v-text="fmt?.date(details?.pullRequest?.updatedDate)"></small>
</div>
<small v-if="options?.origin">Pull Request on {{ details?.pullRequest?.baseRepositoryFullName }}</small>
<div class="pr-body" v-if="options?.body">
<markdown :content="anonymizePrContent(details?.pullRequest?.body)" :options="options" :terms="terms"></markdown>
</div>
<nav class="paper-tabs" role="tablist" v-if="options?.diff || options?.comments">
<button class="paper-tab" type="button" role="tab" v-if="options?.diff" :class="{&#x27;active&#x27;: prTabState?.active == &#x27;diff&#x27;}" id="preview-pr-diff-tab" :aria-selected="prTabState?.active === 'diff'" aria-controls="preview-pr-diff-panel" :tabindex="prTabState?.active === 'diff' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="prTabState.active = &#x27;diff&#x27;">
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-code"></use></svg> Diff
</button>
<button class="paper-tab" type="button" role="tab" v-if="options?.comments" :class="{&#x27;active&#x27;: prTabState?.active == &#x27;comments&#x27;}" id="preview-pr-comments-tab" :aria-selected="prTabState?.active === 'comments'" aria-controls="preview-pr-comments-panel" :tabindex="prTabState?.active === 'comments' ? 0 : -1" @keydown="previewTabKeydown($event)" @click="prTabState.active = &#x27;comments&#x27;">
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-comment"></use></svg>
<span v-text="fmt.plural(details?.pullRequest?.comments?.length, {&#x27;0&#x27;: &#x27;No comments&#x27;, &#x27;one&#x27;: &#x27;1 comment&#x27;, &#x27;other&#x27;: &#x27;{} comments&#x27;})"></span>
</button>
</nav>
<div class="anonymize-preview-status" v-if="!options?.diff &amp;&amp; !options?.comments">Diff and comments are hidden by your display settings.</div>
<div class="paper-tab-content">
<div role="tabpanel" id="preview-pr-diff-panel" aria-labelledby="preview-pr-diff-tab" v-if="options?.diff &amp;&amp; prTabState?.active == &#x27;diff&#x27;">
<div class="pr-diff" v-html="sanitize(fmt?.diff(anonymizePrContent(details?.pullRequest?.diff)))"></div>
</div>
<div role="tabpanel" id="preview-pr-comments-panel" aria-labelledby="preview-pr-comments-tab" v-if="options?.comments &amp;&amp; prTabState?.active == &#x27;comments&#x27;">
<ul class="pr-comments">
<li class="pr-comment" v-for="(comment, index) in details?.pullRequest?.comments">
<div class="pr-comment-head">
<span class="pr-comment-author" v-if="options?.username">
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-user"></use></svg> @<span v-text="anonymizePrContent(comment?.author)"></span>
</span>
<span class="pr-comment-date" v-if="options?.date" v-text="fmt?.date(comment?.updatedDate)"></span>
</div>
<div class="pr-comment-body" v-if="options?.body">
<markdown :content="anonymizePrContent(comment?.body)" :options="options" :terms="terms"></markdown>
</div>
</li>
</ul>
</div>
</div>
</div>
<!-- More below: a fade with a way to keep reading (hidden by CSS once the window is scrolled to the end) -->
<div class="rp-more" v-if="previewStatus === 'ready'">
<a class="rp-more-btn" v-if="isUpdate &amp;&amp; previewUrl()" :href="safeUrl(previewPath())" target="_blank" rel="noopener">Continue reading on the reviewer page <span aria-hidden="true">↗</span></a>
<button type="button" class="rp-more-btn" v-else-if="!previewExpanded" @click="previewExpanded = true">Continue reading in the expanded preview</button>
<span class="rp-more-btn is-hint" v-else aria-hidden="true">Scroll for more</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>