OXYGENHUB

Copy buttons are a glyph joined to their field, with a Copied! tick; a drop's Embed and Clone line up (closes #338) 84e3c09656b2

Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 094073a9919d
When
· 2026-10-26 16:44 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
84e3c09656b2741d9b7f1f9e4772f3fdd4724f85e7da30e517e24db79ed4c3d1
Depends on
166f6e9bf4f1 27babfa16580 2cbf6f348595 42fa95540468 43e8ca33bc5d 47e374020b59 4edcf24025cf 61ecbd299f22 64caed18236a 66fd9d6b1452 749d9a5fcd9f 7b17d570d7ee 83d5052bf76c 8ac2aa4ca6d1 8bf3010760d7 a7aa76ac52d7 b349e15a3eab b5668108487f d23464c0ac60 ffb67a81188c
Depended on by
42c77f6515c2
In bookmarks
diffusion-pin-bookmark main proposal-list-fast
In tags
v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.7 v0.9.8 v0.9.9
Edits
33 atoms, 0 tree operations, 12 files

Browse the tree at this change

modified CHANGELOG.md +1 −0
…
6 unchanged lines
7 7
8 8
- **An agent follows its owner's newest identity (oxygen#82).** A keyless certificate's answer now carries `author_chain`: the grant owner's newest linked identity and its predecessors back to the grant's key, so an agent home with `author.for` authors as the owner's current identity, not one several rotations old.
9 9
- **The profile page's actions are one tidy group (#328).** Edit profile, Projects and Drops sat on two rows, touching each other and the panel below; they are now one evenly spaced block, Edit profile across the top and the two pages side by side.
10 +
- **Copy buttons are a glyph joined to their field (#338).** Every copy button is now a clipboard glyph on the end of the box it copies, with the tooltip "Copy to clipboard"; pressed, it turns to a tick saying "Copied!" for two seconds. A drop's Embed and Clone boxes line up with each other.
10 11
11 12
- **Changelog entries no longer block a landing (oxygen#145).** Two proposals that each add a line at the same place in a file marked `merge=union` in `.oxyattributes` (`CHANGELOG.md merge=union`) used to leave the second unlandable where conflicts are refused. When a proposal's only conflicts are such adjacent insertions, landing it now adds the resolution, both sides kept, the earlier change's first, authored by whoever lands it and signed by the hub as their delegate. Any other conflict blocks as before.
12 13
…
345 unchanged lines
modified assets/github.css +8 −0
…
211 unchanged lines
212 212
:root[data-look="github"] .clone-pop[open] > summary.go.primary { background: var(--gh-primary-hover); }
213 213
:root[data-look="github"] :is(.panel, form.card) > h2:first-child,
214 214
:root[data-look="github"] :is(.panel, form.card) > input[type=hidden]:first-child + h2 { border-radius: 6px 6px 0 0; font-size: 16px; }
215 +
216 +
/* Copy buttons (#338) keep their glyph shape in this look. */
217 +
:root[data-look="github"] button.copy-btn { padding: 0; width: 32px; min-height: 30px; background: var(--gh-btn); color: var(--mute); box-shadow: none; text-transform: none; }
218 +
:root[data-look="github"] button.copy-btn:hover { background: var(--gh-btn-hover); color: var(--ink); }
219 +
:root[data-look="github"] button.copy-btn.copied { color: var(--ok); }
220 +
:root[data-look="github"] .clone > button.copy-btn, :root[data-look="github"] .drop-clone > button.copy-btn { border-radius: 0 6px 6px 0; }
221 +
:root[data-look="github"] .cmds > button.copy-btn { width: 28px; min-height: 28px; border-radius: 6px; }
222 +
:root[data-look="github"] :is(.clone, .drop-clone) > input { border-top-right-radius: 0; border-bottom-right-radius: 0; }
modified assets/hub.js +12 −2
…
301 unchanged lines
302 302
    Object.keys(byId).forEach(function (id) { var el = document.getElementById(id); if (el) io.observe(el); });
303 303
  }
304 304
305 -
  /* Copy buttons. */
305 +
  /* Copy buttons. A glyph button (#338) says "Copy to clipboard", then
306 +
     shows a tick and "Copied!" for two seconds; a worded one says so. */
306 307
  document.querySelectorAll("[data-copy]").forEach(function (b) {
308 +
    var glyph = b.classList.contains("copy-btn"), timer;
309 +
    if (glyph) { b.setAttribute("data-tip", "Copy to clipboard"); b.title = ""; }
307 310
    b.addEventListener("click", function () {
308 311
      navigator.clipboard.writeText(b.getAttribute("data-copy")).then(function () {
309 -
        var t = b.textContent; b.textContent = "copied"; setTimeout(function () { b.textContent = t; }, 1200);
312 +
        clearTimeout(timer);
313 +
        if (glyph) {
314 +
          b.classList.add("copied"); b.setAttribute("data-tip", "Copied!"); b.setAttribute("aria-label", "Copied!");
315 +
          timer = setTimeout(function () { b.classList.remove("copied"); b.setAttribute("data-tip", "Copy to clipboard"); b.setAttribute("aria-label", "Copy to clipboard"); }, 2000);
316 +
        } else {
317 +
          var t = b.getAttribute("data-label") || b.textContent; b.setAttribute("data-label", t);
318 +
          b.textContent = "copied"; timer = setTimeout(function () { b.textContent = t; }, 1200);
319 +
        }
310 320
      });
311 321
    });
312 322
  });
…
657 unchanged lines
modified templates/change.html +1 −1
…
12 unchanged lines
13 13
    <dt>via</dt><dd><span class="mute" aria-hidden="true">↳</span> <span class="tag{% if v.revoked %} bad{% endif %}" title="{{ v.title() }}">agent</span> <img class="avatar xs" src="/avatar/agent/{{ v.name }}" alt="" width="16" height="16"> {{ v.name }}{% if !v.email.is_empty() %} <span class="mute">&lt;{{ v.email }}&gt;</span>{% endif %} <span class="mute small">· {{ v.delegation() }} · key {{ v.short }}</span></dd>{% endif %}
14 14
    <dt>When</dt><dd><time title="{{ c.view.when_ms|when }}">{{ c.view.when_ms|ago }}</time> · {{ c.view.when_ms|when }}</dd>
15 15
    <dt>Signature</dt><dd>{% call m::sig(c.view.signature) %} {{ c.view.signature.title() }}</dd>
16 -
    <dt>Hash</dt><dd><code class="full">{{ c.view.hash }}</code> <button class="small" data-copy="{{ c.view.hash }}">copy</button></dd>
16 +
    <dt>Hash</dt><dd><code class="full">{{ c.view.hash }}</code> <button type="button" class="copy-btn" data-copy="{{ c.view.hash }}" aria-label="Copy to clipboard"></button></dd>
17 17
    <dt>Depends on</dt><dd>{% if c.deps.is_empty() %}<span class="mute">nothing: a root change</span>{% else %}{% for d in c.deps %}<a class="chip" href="{{ repo.url }}/change/{{ d.hash }}" title="{{ d.title }}">{{ d.short }}</a> {% endfor %}{% endif %}</dd>
18 18
    <dt>Depended on by</dt><dd>{% if c.dependents.is_empty() %}<span class="mute">nothing yet</span>{% else %}{% for d in c.dependents %}<a class="chip" href="{{ repo.url }}/change/{{ d.hash }}" title="{{ d.title }}">{{ d.short }}</a> {% endfor %}{% endif %}</dd>
19 19
    <dt>In bookmarks</dt><dd>{% for b in c.in_bookmarks %}<a class="lbl lbl-bm" href="{{ repo.url }}/tree/{{ b }}">{{ b }}</a> {% endfor %}{% if c.in_bookmarks.is_empty() %}<span class="mute">none</span>{% endif %}</dd>
…
9 unchanged lines
modified templates/drop.html +2 −2
…
12 unchanged lines
13 13
  {% if signed_in || can_edit || can_delete || can_fork %}<div class="drop-actions">{% if signed_in %}<form class="inline" method="post" action="{{ repo.url }}/star"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button type="submit" title="{% if starred %}Take your star back{% else %}Star this drop{% endif %}" aria-pressed="{% if starred %}true{% else %}false{% endif %}">{% if starred %}★ STARRED{% else %}☆ STAR{% endif %} {{ stars }}</button></form>{% endif %}{% if can_fork %}<form class="inline" method="post" action="{{ repo.url }}/fork"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button type="submit" title="A copy of this drop, history and all, as a drop of yours">FORK</button></form>{% endif %}{% if can_edit %}<a class="go" href="{{ repo.url }}/edit">EDIT</a>{% endif %}{% if can_delete %}<form class="inline" method="post" action="{{ repo.url }}/delete" data-confirm="Delete this drop and its history? Its address stops working."><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button type="submit" class="danger-btn">DELETE</button></form>{% endif %}</div>{% endif %}
14 14
</div>
15 15
{% if secret %}<div class="strip">Secret: this drop is listed nowhere, but anyone with its address can read it.</div>{% endif %}
16 -
<div class="drop-clone"><label for="embed">Embed</label><input type="text" id="embed" class="code" readonly value="{{ embed }}" data-select><button type="button" class="small" data-copy="{{ embed }}">copy</button></div>
17 -
<div class="drop-clone"><label for="clone">Clone</label><input type="text" id="clone" class="code" readonly value="oxy clone {{ clone_url }}" data-select><button type="button" class="small" data-copy="oxy clone {{ clone_url }}">copy</button></div>
16 +
<div class="drop-fields"><div class="drop-clone"><label for="embed">Embed</label><input type="text" id="embed" class="code" readonly value="{{ embed }}" data-select><button type="button" class="copy-btn" data-copy="{{ embed }}" aria-label="Copy to clipboard"></button></div>
17 +
<div class="drop-clone"><label for="clone">Clone</label><input type="text" id="clone" class="code" readonly value="oxy clone {{ clone_url }}" data-select><button type="button" class="copy-btn" data-copy="oxy clone {{ clone_url }}" aria-label="Copy to clipboard"></button></div></div>
18 18
{% if files.is_empty() %}<p class="empty">No files.</p>{% endif %}
19 19
{% for f in files %}
20 20
<section class="filebox drop-filebox" id="{{ f.anchor }}">
…
37 unchanged lines
modified templates/flow_run.html +1 −1
…
25 unchanged lines
26 26
  <ul class="plain">{% for a in approvals %}<li class="approval st-{{ a.state }}"><strong>{{ a.job }}</strong>
27 27
    {% if a.state == "waiting" %}waits for approval by someone with the <em>{{ a.by }}</em> role{% if a.others_only %}, other than {{ run.actor }}{% endif %}.
28 28
      {% if a.sealed %}<p class="small">It takes an approval signed with your own key. In a checkout of {{ repo.owner }}/{{ repo.name }}:</p>
29 -
      <div class="clone"><input type="text" readonly value="{{ a.command }}" data-select aria-label="The command that approves it"><button type="button" class="small" data-copy="{{ a.command }}">copy</button></div>{% endif %}
29 +
      <div class="clone"><input type="text" readonly value="{{ a.command }}" data-select aria-label="The command that approves it"><button type="button" class="copy-btn" data-copy="{{ a.command }}" aria-label="Copy to clipboard"></button></div>{% endif %}
30 30
      {% if a.may_decide() %}<form class="inline" method="post" action="{{ repo.url }}/flow/{{ run.number }}/jobs/{{ a.job }}/decide"><input type="hidden" name="_csrf" value="{{ shell.csrf }}">{% if !a.sealed %}<button type="submit" name="action" value="approve" class="small">APPROVE</button> {% endif %}<button type="submit" name="action" value="reject" class="small danger-btn">REJECT</button></form>
31 31
      {% else %}<span class="small mute">{{ a.why_not() }}</span>{% endif %}
32 32
    {% else if a.state == "approved" %}approved by {{ a.who }} <time title="{{ a.when_ms|when }}">{{ a.when_ms|ago }}</time>{% if !a.seal.is_empty() %}, <span class="sig sig-valid">◈</span> sealed with their key (approval <code title="{{ a.seal }}">{{ a.seal|short }}</code>){% else if a.sealed %}{% endif %}.
…
40 unchanged lines
modified templates/help.html +8 −8
…
33 unchanged lines
34 34
  <h2>Your identity</h2>
35 35
  <p>Oxygen changes are signed. Your identity is a key pair (Ed25519 with ML-DSA-65) kept under <code>~/.config/oxy</code>; the public part travels with your changes.</p>
36 36
  {% let cmd = "oxy config --user author.name \"Your Name\"\noxy config --user author.email [email protected]\noxy identity create\noxy config --user core.sign change" %}
37 -
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button class="small" data-copy="{{ cmd }}">copy</button></div>
37 +
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button type="button" class="copy-btn" data-copy="{{ cmd }}" aria-label="Copy to clipboard"></button></div>
38 38
  <p>Link the key to your account under {% if shell.user.is_some() %}<a href="/settings/keys">Settings › Keys</a>{% else %}Settings › Keys{% endif %}: the page gives you a challenge to sign with <code>oxy identity sign-challenge</code>, which proves you hold the key. Linked keys show on your profile, and the hub can then tell which signed changes are yours.</p>
39 39
</section>
40 40
…
6 unchanged lines
47 47
  {% endif %}
48 48
  <p>From a checkout, add the repository as a remote, give it your token, and push:</p>
49 49
  {% let cmd = q.connect(self.repo_url().as_str()) %}
50 -
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button class="small" data-copy="{{ cmd }}">copy</button></div>
50 +
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button type="button" class="copy-btn" data-copy="{{ cmd }}" aria-label="Copy to clipboard"></button></div>
51 51
  <p>An empty repository takes its identity from the first push, so every later clone shares it. <code>oxy push {{ q.remote }}</code> sends the current bookmark; <code>oxy push {{ q.remote }} --bookmark NAME</code> another one. Tags travel with pushes.</p>
52 52
  <p class="small mute">A Git history comes across with <code>oxy git import --branch main path/to/git-checkout NAME</code>: every commit becomes a change, and the result can be pushed like any other repository.</p>
53 53
</section>
…
1 unchanged lines
55 55
<section class="panel" id="clone">
56 56
  <h2>Cloning</h2>
57 57
  {% let cmd = self.clone_cmd() %}
58 -
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button class="small" data-copy="{{ cmd }}">copy</button></div>
58 +
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button type="button" class="copy-btn" data-copy="{{ cmd }}" aria-label="Copy to clipboard"></button></div>
59 59
  <p>Public repositories clone without a token. For private ones, and to push back, set <code>oxy config remote.origin.token YOUR-TOKEN</code>{% if q.ca_url.is_some() %} and <code>oxy config remote.origin.ca {{ q.ca_file }}</code>{% endif %} in the clone. <code>oxy pull origin</code> brings in what others pushed; there is no merge commit, because Oxygen changes combine by themselves.</p>
60 60
</section>
61 61
…
17 unchanged lines
79 79
  <h2>Letting the hub act for you</h2>
80 80
  <p>Edits in the browser and landings from the proposal page are changes too, and they must be signed. The hub signs them as your <strong>delegate</strong>, only if you allow it. Allowing it is a one-off step:</p>
81 81
  {% let cmd = self.delegate_cmd() %}
82 -
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button class="small" data-copy="{{ cmd }}">copy</button></div>
82 +
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button type="button" class="copy-btn" data-copy="{{ cmd }}" aria-label="Copy to clipboard"></button></div>
83 83
  <p>That makes a new version of your identity: export it in the same checkout (<code>oxy identity export</code>) and upload that file under Settings › Keys, signing the challenge there too, so the hub sees the delegation. The hub's key file is not the one to upload. You can withdraw it at any time with <code>oxy identity delegate remove</code>; a repository's trust policy still decides whether a delegated change may update a bookmark.</p>
84 84
</section>
85 85
…
4 unchanged lines
90 90
  <p>A keyless agent holds no key at all. You give it a <strong>grant</strong>: a token your own key signs, valid for a time you choose. Whenever the agent signs, it makes a fresh key, the hub certifies it for ten minutes under your grant, and the key is thrown away; a stolen home holds nothing that signs by itself.</p>
91 91
  <p>On your side, name the agent and how long it may act for you (the token is printed once):</p>
92 92
  {% let cmd = "oh agent grant claude --for 30d --scope changes,seal,refs" %}
93 -
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button class="small" data-copy="{{ cmd }}">copy</button></div>
93 +
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button type="button" class="copy-btn" data-copy="{{ cmd }}" aria-label="Copy to clipboard"></button></div>
94 94
  <p>Give the agent its own oxy home (never your own), and keep the token there; it is read from standard input, so it stays out of the shell's history:</p>
95 95
  {% let cmd = self.agent_home_cmd() %}
96 -
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button class="small" data-copy="{{ cmd }}">copy</button></div>
96 +
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button type="button" class="copy-btn" data-copy="{{ cmd }}" aria-label="Copy to clipboard"></button></div>
97 97
  <p>The agent then commits and pushes with <code>OXY_HOME</code> set to its home. Its changes are authored as you and sealed <code>via agent:claude@{{ who }}</code>, which history, proposals and your profile show. <code>oh agent list</code> shows your grants; <code>oh agent revoke ID</code> ends one at once, and a grant ends by itself when its time is up.</p>
98 98
  <h3>Where it may push</h3>
99 99
  <p>A repository with a trust policy accepts an agent's work only if the policy names it. Grant the agent's subject, with this hub as the issuer, from a checkout of the repository:</p>
100 100
  {% let cmd = self.subject_grant_cmd() %}
101 -
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button class="small" data-copy="{{ cmd }}">copy</button></div>
101 +
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button type="button" class="copy-btn" data-copy="{{ cmd }}" aria-label="Copy to clipboard"></button></div>
102 102
  <p>A repository without a policy takes any signed change its token may push.</p>
103 103
  <h3>Agents with a key of their own</h3>
104 104
  <p>An agent that keeps its own identity acts for you through a <strong>delegation</strong>: from your checkout, <code>oxy identity delegate add agent.identity --scope changes --expires 7d</code>, then link your identity again under {% if shell.user.is_some() %}<a href="/settings/keys">Settings › Keys</a>{% else %}Settings › Keys{% endif %}. <code>oxy identity delegate remove KEY</code> withdraws it. Keyless grants are simpler and leave nothing behind.</p>
…
10 unchanged lines
115 115
  <p>Pipelines run in <strong>OxygenFlow</strong>. Put a pipeline in <code>.flow/pipeline.kdl</code> and push: each job runs in its own microVM with no network, and its result shows on the repository's Flow tab and as a check named <code>flow/JOB</code> on proposals. A site administrator enables Flow for a repository.</p>
116 116
  <p>Instead of a badge, a README can carry a <strong>state stamp</strong>:</p>
117 117
  {% let cmd = "[![flow](.flow/stamp)](.flow/run)" %}
118 -
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button class="small" data-copy="{{ cmd }}">copy</button></div>
118 +
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button type="button" class="copy-btn" data-copy="{{ cmd }}" aria-label="Copy to clipboard"></button></div>
119 119
  <p>It shows the result for the state you are looking at (a tag shows the tag's result), names that state, and carries each job's receipt hash.</p>
120 120
</section>
121 121
…
50 unchanged lines
modified templates/overview.html +2 −2
…
27 unchanged lines
28 28
        <summary class="go small primary" aria-haspopup="true"><span aria-hidden="true">&lt;&gt;</span> Code</summary>
29 29
        <div class="clone-panel" role="group" aria-label="Clone or download this repository">
30 30
          <label for="clone-cmd" class="small mute">Clone with oxy</label>
31 -
          <div class="clone"><input id="clone-cmd" type="text" readonly value="oxy clone {{ repo.clone_url }}" data-select><button type="button" class="small" data-copy="oxy clone {{ repo.clone_url }}">copy</button></div>
31 +
          <div class="clone"><input id="clone-cmd" type="text" readonly value="oxy clone {{ repo.clone_url }}" data-select><button type="button" class="copy-btn" data-copy="oxy clone {{ repo.clone_url }}" aria-label="Copy to clipboard"></button></div>
32 32
          <p class="small mute">The URL alone: <code>{{ repo.clone_url }}</code></p>
33 33
          {% if let Some(git) = repo.git_url %}
34 34
          <label for="git-clone-cmd" class="small mute">Clone with Git (a read-only mirror)</label>
35 -
          <div class="clone"><input id="git-clone-cmd" type="text" readonly value="git clone {{ git }}" data-select><button type="button" class="small" data-copy="git clone {{ git }}">copy</button></div>
35 +
          <div class="clone"><input id="git-clone-cmd" type="text" readonly value="git clone {{ git }}" data-select><button type="button" class="copy-btn" data-copy="git clone {{ git }}" aria-label="Copy to clipboard"></button></div>
36 36
          <p class="small mute">For tools that only speak Git. Push with oxy: the mirror refuses pushes.</p>
37 37
          {% endif %}
38 38
          {% if quickstart.is_none() %}
…
80 unchanged lines
modified templates/quickstart.html +3 −3
…
6 unchanged lines
7 7
8 8
  <h3>Push an existing checkout</h3>
9 9
  {% let cmd = q.connect(repo.clone_url) %}
10 -
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button class="small" data-copy="{{ cmd }}">copy</button></div>
10 +
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button type="button" class="copy-btn" data-copy="{{ cmd }}" aria-label="Copy to clipboard"></button></div>
11 11
12 12
  <h3>Start from nothing</h3>
13 13
  {% let cmd = q.fresh(repo.name) %}
14 -
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button class="small" data-copy="{{ cmd }}">copy</button></div>
14 +
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button type="button" class="copy-btn" data-copy="{{ cmd }}" aria-label="Copy to clipboard"></button></div>
15 15
  <p class="small">Then run the commands above.</p>
16 16
17 17
  <h3>Bring a Git branch</h3>
18 18
  {% let cmd = q.git_import(repo.name) %}
19 -
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button class="small" data-copy="{{ cmd }}">copy</button></div>
19 +
  <div class="cmds"><pre><code>{{ cmd }}</code></pre><button type="button" class="copy-btn" data-copy="{{ cmd }}" aria-label="Copy to clipboard"></button></div>
20 20
  <p class="small">Then run the commands above. Every Git commit becomes a change.</p>
21 21
22 22
  {% if repo.can(crate::auth::Access::Write) %}<p>Or <a href="{{ repo.url }}/newfile/{{ repo.default_bookmark }}">create the first file</a> in the browser.</p>{% endif %}
…
4 unchanged lines
modified templates/security_contact.html +1 −1
…
2 unchanged lines
3 3
{% block content %}
4 4
<div class="head"><h1>Report a security problem</h1></div>
5 5
<section class="panel prose">
6 -
{% if !email.is_empty() %}<p>Write to <strong><span class="secret">{{ email }}</span></strong> <button type="button" class="small" data-copy="{{ email }}">copy</button></p>{% endif %}
6 +
{% if !email.is_empty() %}<p>Write to <strong><span class="secret">{{ email }}</span></strong> <button type="button" class="copy-btn" data-copy="{{ email }}" aria-label="Copy to clipboard"></button></p>{% endif %}
7 7
{{ body_html|safe }}
8 8
<p class="small mute">This page is also named in <a href="/.well-known/security.txt">security.txt</a>.</p>
9 9
</section>
…
1 unchanged lines
modified templates/style.css +24 −0
…
1449 unchanged lines
1450 1450
.drop-clone { display: flex; align-items: center; gap: 8px; margin: 0 0 16px; font-size: 13px; }
1451 1451
.drop-clone label { color: var(--mute); }
1452 1452
.drop-clone input { flex: 0 1 640px; min-width: 0; }
1453 +
1454 +
/* Copy buttons (#338): a copy glyph joined to the end of its field, or in
1455 +
   the corner of a command block; "Copy to clipboard" on hover, a tick and
1456 +
   "Copied!" for two seconds after a click (hub.js). */
1457 +
:root { --icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5'%3E%3Crect x='5.25' y='1.25' width='9.5' height='9.5' rx='1.75'/%3E%3Cpath d='M3.5 5.25h-.75c-.83 0-1.5.67-1.5 1.5v6.5c0 .83.67 1.5 1.5 1.5h6.5c.83 0 1.5-.67 1.5-1.5v-.75'/%3E%3C/svg%3E"); --icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8.5l3.25 3.25L13 4.75'/%3E%3C/svg%3E"); }
1458 +
button.copy-btn { position: relative; display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; width: 32px; min-height: 30px; padding: 0; margin: 0; background: var(--panel); border: 1px solid var(--line); color: var(--mute); letter-spacing: normal; text-transform: none; }
1459 +
button.copy-btn::before { content: ""; width: 16px; height: 16px; background: currentColor; -webkit-mask: var(--icon-copy) center / 16px no-repeat; mask: var(--icon-copy) center / 16px no-repeat; }
1460 +
button.copy-btn:hover { background: var(--soft); color: var(--ink); }
1461 +
button.copy-btn.copied, button.copy-btn.copied:hover { color: var(--ok); }
1462 +
button.copy-btn.copied::before { -webkit-mask-image: var(--icon-check); mask-image: var(--icon-check); }
1463 +
button.copy-btn[data-tip]:hover::after, button.copy-btn[data-tip]:focus-visible::after, button.copy-btn.copied::after {
1464 +
  content: attr(data-tip); position: absolute; bottom: calc(100% + 6px); right: -4px; z-index: 30; white-space: nowrap; pointer-events: none;
1465 +
  background: var(--ink); color: var(--bg); font-size: 12px; font-weight: 500; line-height: 1.4; padding: 3px 8px; border-radius: 6px;
1466 +
}
1467 +
.clone, .drop-clone { gap: 0; }
1468 +
.clone > input, .clone > code, .drop-clone > input { margin: 0; border-top-right-radius: 0; border-bottom-right-radius: 0; }
1469 +
.clone > button.copy-btn, .drop-clone > button.copy-btn { align-self: stretch; border-left: 0; border-radius: 0 6px 6px 0; }
1470 +
.cmds { position: relative; }
1471 +
.cmds pre { padding-right: 46px; }
1472 +
.cmds > button.copy-btn { position: absolute; top: 6px; right: 6px; width: 28px; min-height: 28px; border-radius: 6px; }
1473 +
/* A drop's Embed and Clone: labels in one column, the fields lined up in the next. */
1474 +
.drop-fields { display: grid; grid-template-columns: max-content minmax(0, 680px) auto; row-gap: 10px; margin: 0 0 16px; }
1475 +
.drop-fields .drop-clone { display: grid; grid-template-columns: subgrid; grid-column: 1 / -1; margin: 0; }
1476 +
.drop-fields .drop-clone label { align-self: center; padding-right: 12px; }
1453 1477
.drop-filebox article.doc { max-width: none; margin: 0; border: 0; }
1454 1478
/* A drop framed on another site (#280): its files and a link back. */
1455 1479
body.drop-embedded { margin: 0; padding: 8px; }
…
14 unchanged lines
modified templates/wiki.html +1 −1
…
44 unchanged lines
45 45
  </section>{% endif %}
46 46
  {% if let Some(s) = sidebar %}<section class="side-sec"><div class="doc wiki-side">{{ s|safe }}</div></section>{% endif %}
47 47
  <section class="side-sec"><div class="side-head"><h3>Clone this wiki</h3></div>
48 -
    <div class="clone"><input type="text" readonly value="oxy clone {{ shell.base_url }}/{{ repo.owner }}/{{ repo.name }}.wiki" data-select aria-label="Clone command"><button type="button" class="small" data-copy="oxy clone {{ shell.base_url }}/{{ repo.owner }}/{{ repo.name }}.wiki">copy</button></div>
48 +
    <div class="clone"><input type="text" readonly value="oxy clone {{ shell.base_url }}/{{ repo.owner }}/{{ repo.name }}.wiki" data-select aria-label="Clone command"><button type="button" class="copy-btn" data-copy="oxy clone {{ shell.base_url }}/{{ repo.owner }}/{{ repo.name }}.wiki" aria-label="Copy to clipboard"></button></div>
49 49
  </section>
50 50
</aside>
51 51
</div>
…
2 unchanged lines