tfrere HF Staff Cursor commited on
Commit
2b90d0d
·
1 Parent(s): 44626d0

feat(publisher): editor pill shows who you're signed in as

Browse files

Upgrade the editor quick-link from a bare pencil icon to a
proper pill that contextualises the action:

┌──────────────────────────────────────────────────────┐
│ [avatar] Signed in as @tfrere [✎ Edit article] │
└──────────────────────────────────────────────────────┘

The visitor sees who they're authenticated as before clicking,
which makes the "would I be writing to the real file?" question
self-answering. Useful when multiple HF accounts share the same
browser session, or when a collaborator wants to confirm they're
not editing on someone else's behalf.

Implementation:

- Render placeholder content (\"@you\" + empty <img>) so the
layout doesn't shift when /api/auth/status comes back. The
pill is still \`hidden\` by default - no flash for anonymous
visitors.
- Inline script fills the <img>.src (preferring
user.avatarUrl, falling back to /api/users/<name>/avatar)
and the @handle , then drops the \`hidden\` attribute.
- New \`.edit-pill\` styles: rounded pill, page-bg fill, primary
CTA chip on the right, soft shadow, primary-tinted border on
hover. Avatar uses the same 24px round shape as the article's
HfUser cards for visual consistency.
- Still hidden under 1100px (same breakpoint as #theme-toggle).

Snapshot refreshed.

Co-authored-by: Cursor <cursoragent@cursor.com>

backend/src/publisher/html-renderer.ts CHANGED
@@ -198,16 +198,25 @@ ${renderPrimaryColorOverride(meta.primaryHue)}
198
  </button>
199
 
200
  <!--
201
- Quick link to the editor, shown only for visitors whose
202
  /api/auth/status response carries canEdit=true. Hidden by
203
- default so non-editors never see a flash. Same shape as the
204
- theme toggle, mirrored to the right edge of the viewport.
 
 
205
  -->
206
- <a id="edit-link" href="/editor" aria-label="Open the editor" title="Open the editor" hidden>
207
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
208
- <path d="M12 20h9"/>
209
- <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
210
- </svg>
 
 
 
 
 
 
 
211
  </a>
212
 
213
  <!-- Mobile TOC toggle -->
@@ -713,25 +722,38 @@ ${renderPrimaryColorOverride(meta.primaryHue)}
713
  });
714
 
715
  // ----------------------------------------------------------------
716
- // Editor quick-link
717
  // ----------------------------------------------------------------
718
  // The published page lives on the same Space as the editor, so we
719
  // can probe /api/auth/status to know if the current visitor has
720
- // edit rights. When they do, surface a small "open editor" pencil
721
- // top-right; otherwise the button stays \`hidden\` and is removed
722
- // on the next layout pass (no flicker for read-only visitors).
723
  //
724
  // A network failure (offline preview, exported HTML opened from
725
- // disk, ...) is silently ignored - the button just stays hidden.
726
  (function() {
727
  var link = document.getElementById('edit-link');
728
  if (!link) return;
729
  fetch('/api/auth/status', { credentials: 'same-origin' })
730
  .then(function(r) { return r.ok ? r.json() : null; })
731
  .then(function(data) {
732
- if (data && data.canEdit) {
733
- link.removeAttribute('hidden');
 
 
 
 
 
 
 
 
 
 
 
 
734
  }
 
735
  })
736
  .catch(function() {});
737
  })();
 
198
  </button>
199
 
200
  <!--
201
+ Editor quick-access pill, shown only for visitors whose
202
  /api/auth/status response carries canEdit=true. Hidden by
203
+ default so non-editors never see a flash. The fields are
204
+ filled by the inline script at the bottom of <body>; we use
205
+ placeholder content so layout doesn't shift when the data
206
+ arrives.
207
  -->
208
+ <a id="edit-link" href="/editor" class="edit-pill" hidden>
209
+ <img class="edit-pill__avatar" alt="" width="24" height="24" referrerpolicy="no-referrer">
210
+ <span class="edit-pill__text">
211
+ Signed in as <strong class="edit-pill__name">@you</strong>
212
+ </span>
213
+ <span class="edit-pill__cta">
214
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
215
+ <path d="M12 20h9"/>
216
+ <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
217
+ </svg>
218
+ Edit article
219
+ </span>
220
  </a>
221
 
222
  <!-- Mobile TOC toggle -->
 
722
  });
723
 
724
  // ----------------------------------------------------------------
725
+ // Editor quick-access pill
726
  // ----------------------------------------------------------------
727
  // The published page lives on the same Space as the editor, so we
728
  // can probe /api/auth/status to know if the current visitor has
729
+ // edit rights. When they do, fill in their avatar/handle and
730
+ // surface the pill top-right. Otherwise the pill stays hidden
731
+ // (no flash for anonymous / read-only visitors).
732
  //
733
  // A network failure (offline preview, exported HTML opened from
734
+ // disk, ...) is silently ignored - the pill just stays hidden.
735
  (function() {
736
  var link = document.getElementById('edit-link');
737
  if (!link) return;
738
  fetch('/api/auth/status', { credentials: 'same-origin' })
739
  .then(function(r) { return r.ok ? r.json() : null; })
740
  .then(function(data) {
741
+ if (!data || !data.canEdit) return;
742
+ var u = data.user || {};
743
+ var handle = u.name || 'editor';
744
+ var avatar = link.querySelector('.edit-pill__avatar');
745
+ var nameEl = link.querySelector('.edit-pill__name');
746
+ if (avatar) {
747
+ var src = u.avatarUrl ||
748
+ ('https://huggingface.co/api/users/' +
749
+ encodeURIComponent(handle) + '/avatar');
750
+ avatar.src = src;
751
+ avatar.alt = handle + ' avatar';
752
+ }
753
+ if (nameEl) {
754
+ nameEl.textContent = '@' + handle;
755
  }
756
+ link.removeAttribute('hidden');
757
  })
758
  .catch(function() {});
759
  })();
backend/tests/__snapshots__/html-renderer-snapshot.test.ts.snap CHANGED
@@ -126,16 +126,25 @@ exports[`snapshot - full render > matches snapshot for a typical article 1`] = `
126
  </button>
127
 
128
  <!--
129
- Quick link to the editor, shown only for visitors whose
130
  /api/auth/status response carries canEdit=true. Hidden by
131
- default so non-editors never see a flash. Same shape as the
132
- theme toggle, mirrored to the right edge of the viewport.
 
 
133
  -->
134
- <a id="edit-link" href="/editor" aria-label="Open the editor" title="Open the editor" hidden>
135
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
136
- <path d="M12 20h9"/>
137
- <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
138
- </svg>
 
 
 
 
 
 
 
139
  </a>
140
 
141
  <!-- Mobile TOC toggle -->
@@ -656,25 +665,38 @@ exports[`snapshot - full render > matches snapshot for a typical article 1`] = `
656
  });
657
 
658
  // ----------------------------------------------------------------
659
- // Editor quick-link
660
  // ----------------------------------------------------------------
661
  // The published page lives on the same Space as the editor, so we
662
  // can probe /api/auth/status to know if the current visitor has
663
- // edit rights. When they do, surface a small "open editor" pencil
664
- // top-right; otherwise the button stays \`hidden\` and is removed
665
- // on the next layout pass (no flicker for read-only visitors).
666
  //
667
  // A network failure (offline preview, exported HTML opened from
668
- // disk, ...) is silently ignored - the button just stays hidden.
669
  (function() {
670
  var link = document.getElementById('edit-link');
671
  if (!link) return;
672
  fetch('/api/auth/status', { credentials: 'same-origin' })
673
  .then(function(r) { return r.ok ? r.json() : null; })
674
  .then(function(data) {
675
- if (data && data.canEdit) {
676
- link.removeAttribute('hidden');
 
 
 
 
 
 
 
 
 
 
 
 
677
  }
 
678
  })
679
  .catch(function() {});
680
  })();
 
126
  </button>
127
 
128
  <!--
129
+ Editor quick-access pill, shown only for visitors whose
130
  /api/auth/status response carries canEdit=true. Hidden by
131
+ default so non-editors never see a flash. The fields are
132
+ filled by the inline script at the bottom of <body>; we use
133
+ placeholder content so layout doesn't shift when the data
134
+ arrives.
135
  -->
136
+ <a id="edit-link" href="/editor" class="edit-pill" hidden>
137
+ <img class="edit-pill__avatar" alt="" width="24" height="24" referrerpolicy="no-referrer">
138
+ <span class="edit-pill__text">
139
+ Signed in as <strong class="edit-pill__name">@you</strong>
140
+ </span>
141
+ <span class="edit-pill__cta">
142
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
143
+ <path d="M12 20h9"/>
144
+ <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
145
+ </svg>
146
+ Edit article
147
+ </span>
148
  </a>
149
 
150
  <!-- Mobile TOC toggle -->
 
665
  });
666
 
667
  // ----------------------------------------------------------------
668
+ // Editor quick-access pill
669
  // ----------------------------------------------------------------
670
  // The published page lives on the same Space as the editor, so we
671
  // can probe /api/auth/status to know if the current visitor has
672
+ // edit rights. When they do, fill in their avatar/handle and
673
+ // surface the pill top-right. Otherwise the pill stays hidden
674
+ // (no flash for anonymous / read-only visitors).
675
  //
676
  // A network failure (offline preview, exported HTML opened from
677
+ // disk, ...) is silently ignored - the pill just stays hidden.
678
  (function() {
679
  var link = document.getElementById('edit-link');
680
  if (!link) return;
681
  fetch('/api/auth/status', { credentials: 'same-origin' })
682
  .then(function(r) { return r.ok ? r.json() : null; })
683
  .then(function(data) {
684
+ if (!data || !data.canEdit) return;
685
+ var u = data.user || {};
686
+ var handle = u.name || 'editor';
687
+ var avatar = link.querySelector('.edit-pill__avatar');
688
+ var nameEl = link.querySelector('.edit-pill__name');
689
+ if (avatar) {
690
+ var src = u.avatarUrl ||
691
+ ('https://huggingface.co/api/users/' +
692
+ encodeURIComponent(handle) + '/avatar');
693
+ avatar.src = src;
694
+ avatar.alt = handle + ' avatar';
695
+ }
696
+ if (nameEl) {
697
+ nameEl.textContent = '@' + handle;
698
  }
699
+ link.removeAttribute('hidden');
700
  })
701
  .catch(function() {});
702
  })();
frontend/src/styles/_layout.css CHANGED
@@ -138,44 +138,82 @@
138
  transform: scale(0.92);
139
  }
140
 
141
- /* Editor quick-link (published page only). Mirrors the theme toggle
142
- on the opposite side. The button itself ships with `hidden` and is
143
- revealed by inline JS in the published page only when
144
- /api/auth/status returns canEdit=true. */
145
- #edit-link {
 
 
146
  position: absolute;
147
  top: var(--spacing-4);
148
  right: var(--spacing-4);
149
  margin: 0;
150
  z-index: var(--z-overlay);
151
- width: 40px;
152
- height: 40px;
153
- border-radius: 50%;
 
 
154
  border: 1px solid var(--border-color);
155
  background: var(--page-bg);
156
- box-shadow: 0 2px 12px rgba(0,0,0,.08);
157
- display: flex;
158
- align-items: center;
159
- justify-content: center;
160
- padding: 0;
161
  color: var(--text-color);
162
  text-decoration: none;
 
 
 
163
  transition: transform 150ms ease, box-shadow 150ms ease,
164
- background 150ms ease;
165
  }
166
- #edit-link:hover {
167
- background: color-mix(in srgb, var(--primary-color) 12%, var(--page-bg));
 
168
  }
169
- #edit-link:active {
170
- transform: scale(0.92);
171
  }
172
- #edit-link[hidden] {
173
  display: none;
174
  }
175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  @media (max-width: 1100px) {
177
  #theme-toggle,
178
- #edit-link {
179
  display: none;
180
  }
181
  }
 
138
  transform: scale(0.92);
139
  }
140
 
141
+ /* Editor quick-access pill (published page only). The element ships
142
+ with `hidden` and the inline script in the published page reveals
143
+ it only when /api/auth/status returns canEdit=true, after also
144
+ filling in the avatar / handle. We split the rules across the
145
+ element + a class so the JS can target the id (it's stable across
146
+ page exports) while the styles live on the class. */
147
+ #edit-link.edit-pill {
148
  position: absolute;
149
  top: var(--spacing-4);
150
  right: var(--spacing-4);
151
  margin: 0;
152
  z-index: var(--z-overlay);
153
+ display: inline-flex;
154
+ align-items: center;
155
+ gap: 10px;
156
+ padding: 4px 4px 4px 6px;
157
+ border-radius: 999px;
158
  border: 1px solid var(--border-color);
159
  background: var(--page-bg);
 
 
 
 
 
160
  color: var(--text-color);
161
  text-decoration: none;
162
+ box-shadow: 0 2px 12px rgba(0,0,0,.08);
163
+ font-size: 13px;
164
+ line-height: 1.2;
165
  transition: transform 150ms ease, box-shadow 150ms ease,
166
+ border-color 150ms ease;
167
  }
168
+ #edit-link.edit-pill:hover {
169
+ border-color: color-mix(in srgb, var(--primary-color) 50%, var(--border-color));
170
+ box-shadow: 0 4px 16px rgba(0,0,0,.12);
171
  }
172
+ #edit-link.edit-pill:active {
173
+ transform: scale(0.98);
174
  }
175
+ #edit-link.edit-pill[hidden] {
176
  display: none;
177
  }
178
 
179
+ .edit-pill__avatar {
180
+ width: 24px;
181
+ height: 24px;
182
+ border-radius: 50%;
183
+ object-fit: cover;
184
+ flex-shrink: 0;
185
+ background: var(--surface-bg);
186
+ }
187
+
188
+ .edit-pill__text {
189
+ color: var(--muted-color);
190
+ white-space: nowrap;
191
+ }
192
+ .edit-pill__name {
193
+ color: var(--text-color);
194
+ font-weight: 600;
195
+ }
196
+
197
+ .edit-pill__cta {
198
+ display: inline-flex;
199
+ align-items: center;
200
+ gap: 5px;
201
+ padding: 4px 10px;
202
+ border-radius: 999px;
203
+ background: var(--primary-color);
204
+ color: #fff;
205
+ font-weight: 600;
206
+ font-size: 12px;
207
+ white-space: nowrap;
208
+ flex-shrink: 0;
209
+ }
210
+ .edit-pill__cta svg {
211
+ stroke: currentColor;
212
+ }
213
+
214
  @media (max-width: 1100px) {
215
  #theme-toggle,
216
+ #edit-link.edit-pill {
217
  display: none;
218
  }
219
  }