@oddessentials/agent-guild 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -124,6 +124,11 @@ vX.Y.Z** switches over when you are ready. The version badge opens
124
124
 
125
125
  <img src="docs/images/whats-new.webp" alt="The What's new panel listing the release notes of each version">
126
126
 
127
+ **Keep notes at hand.** **Notes** in the top bar opens a notepad for prompts,
128
+ commands and to-dos, even while a terminal is open. It saves up to 100,000
129
+ characters as you type and keeps them in this browser, never on the manager;
130
+ every tab of the page shows the same notes.
131
+
127
132
  **Appearance.** The Appearance menu in the top bar picks a skin and light or
128
133
  dark mode, without a reload. **Guild** is the default fantasy look;
129
134
  **Professional** is a plain business look with no characters; **Orbital**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oddessentials/agent-guild",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Launch and watch AI coding-assistant terminal sessions from one local web page. A bundled session manager owns the terminals so the page can close and reconnect.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/web/app.js CHANGED
@@ -16,6 +16,7 @@ const GITHUB_ACCOUNT_KEY = 'agentGuild.githubAccount';
16
16
  const CLONE_PARENT_KEY = 'agentGuild.cloneParent';
17
17
  const SESSION_ORDER_KEY = 'agentGuild.sessionOrder';
18
18
  const SOUND_KEY = 'agentGuild.sound';
19
+ const NOTES_KEY = 'agentGuild.notes';
19
20
  const RELEASES_URL = 'https://github.com/oddessentials/agent-guild/releases';
20
21
  const isMac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
21
22
  const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
@@ -63,7 +64,7 @@ const state = {
63
64
  // ---- storage (may be unavailable, e.g. blocked site data) -----------------
64
65
 
65
66
  function load(key) { try { return localStorage.getItem(key); } catch { return null; } }
66
- function save(key, value) { try { value === null ? localStorage.removeItem(key) : localStorage.setItem(key, value); } catch { /* ignore */ } }
67
+ function save(key, value) { try { value === null ? localStorage.removeItem(key) : localStorage.setItem(key, value); return true; } catch { return false; } }
67
68
 
68
69
  // ---- helpers --------------------------------------------------------------
69
70
 
@@ -87,6 +88,12 @@ function toast(message, ms = 5000, action = null) {
87
88
  toastTimer = setTimeout(() => { el.hidden = true; }, ms);
88
89
  }
89
90
 
91
+ /**
92
+ * Handles a click, but not the second click of a double-click, which lands on whatever the first one
93
+ * opened or uncovered: Close over Notes, or Stop manager under a panel's Close.
94
+ */
95
+ const firstClick = (run) => (e) => { if (e.detail < 2) run(); };
96
+
90
97
  function setConnection(kind, label) {
91
98
  const el = $('connection');
92
99
  el.className = `connection ${kind}`;
@@ -294,6 +301,99 @@ function placeAppearanceMenu() {
294
301
  menu.style.right = `${Math.max(8, Math.round(innerWidth - box.right))}px`;
295
302
  }
296
303
 
304
+ // ---- notes ----------------------------------------------------------------
305
+
306
+ /** Notes share the page's storage with every other setting, so they stay far below the browser's limit for it. */
307
+ const NOTES_LIMIT = 100000;
308
+ const NOTES_STATUS = {
309
+ saved: 'Saved in this browser as you type',
310
+ long: `Not saved: notes hold up to ${NOTES_LIMIT.toLocaleString('en-US')} characters. Shorten them to save.`,
311
+ refused: 'Not saved: the browser’s storage for this page is full or turned off. Copy what you need before you close the page.',
312
+ };
313
+ /**
314
+ * `saved`: the notes as this page last read or wrote them in storage. `status`: whether the text in the
315
+ * panel is saved, too `long` or `refused`; `shown`: the status the line under the title shows.
316
+ * `pressedOutside`: the last press on the panel was on its backdrop.
317
+ */
318
+ const notesView = { saved: '', status: 'saved', shown: 'saved', pressedOutside: false };
319
+
320
+ /** Shows notes another tab saved since this page last read or wrote them. Saved notes win over text this page could not save. */
321
+ function refreshNotes() {
322
+ let text;
323
+ // Unlike load(), a read that fails is not taken for empty notes.
324
+ try { text = localStorage.getItem(NOTES_KEY) ?? ''; } catch { return; }
325
+ if (text === notesView.saved) return;
326
+ notesView.saved = text;
327
+ notesView.status = 'saved';
328
+ $('notes-text').value = text;
329
+ renderNotesStatus();
330
+ guardLeaving();
331
+ }
332
+
333
+ /** Saves the notes on every change; emptying them removes the saved copy. */
334
+ function saveNotes() {
335
+ const text = $('notes-text').value;
336
+ if (text.length > NOTES_LIMIT) notesView.status = 'long';
337
+ else if (!save(NOTES_KEY, text || null)) notesView.status = 'refused';
338
+ else {
339
+ notesView.status = 'saved';
340
+ notesView.saved = text;
341
+ }
342
+ renderNotesStatus();
343
+ guardLeaving();
344
+ }
345
+
346
+ /** The line under the title is a live region, so it is rewritten only when the status changes. */
347
+ function renderNotesStatus() {
348
+ if (notesView.shown === notesView.status) return;
349
+ notesView.shown = notesView.status;
350
+ const sub = $('notes-sub');
351
+ sub.textContent = NOTES_STATUS[notesView.status];
352
+ sub.classList.toggle('warn', notesView.status !== 'saved');
353
+ }
354
+
355
+ /** Another tab saved the notes, or cleared this page's storage. */
356
+ function notesStored(e) {
357
+ if (e.key === NOTES_KEY || e.key === null) refreshNotes();
358
+ }
359
+
360
+ function openNotes() {
361
+ hideTip();
362
+ refreshNotes();
363
+ // Saving an empty note changes nothing, but shows at once when the browser keeps nothing.
364
+ if (notesView.status === 'saved' && !$('notes-text').value) saveNotes();
365
+ const dialog = $('notes');
366
+ if (!dialog.open) dialog.showModal();
367
+ $('notes-text').focus();
368
+ }
369
+
370
+ function closeNotes() {
371
+ if ($('notes').open) $('notes').close();
372
+ }
373
+
374
+ /** Focus goes back to the Notes button in every browser, never to the page itself. */
375
+ function notesClosed() {
376
+ $('notes-open').focus();
377
+ }
378
+
379
+ /** The second press of a double-click on Notes lands in the panel, where it must not take the focus from the text. */
380
+ const keepFocus = (e) => { if (e.detail > 1 && e.target !== $('notes-text')) e.preventDefault(); };
381
+
382
+ /** Whether a press or click on the panel was on its backdrop, outside the sheet. */
383
+ function isOutsideNotes(e) {
384
+ const box = e.currentTarget.getBoundingClientRect();
385
+ return e.target === e.currentTarget && (e.clientX < box.left || e.clientX >= box.right || e.clientY < box.top || e.clientY >= box.bottom);
386
+ }
387
+
388
+ function notesPressed(e) {
389
+ notesView.pressedOutside = isOutsideNotes(e);
390
+ }
391
+
392
+ /** Only a press and a release both outside close the notes: a drag into or out of them selects text. */
393
+ function notesClicked(e) {
394
+ if (notesView.pressedOutside && isOutsideNotes(e)) closeNotes();
395
+ }
396
+
297
397
  // ---- upgrading the manager ------------------------------------------------
298
398
 
299
399
  function renderUpgrade() {
@@ -2858,7 +2958,9 @@ function confirmLeaving(event) {
2858
2958
 
2859
2959
  function guardLeaving() {
2860
2960
  const running = state.connected && [...state.sessions.values()].some((s) => s.status === 'running');
2861
- if (running) addEventListener('beforeunload', confirmLeaving);
2961
+ // Notes that could not be saved are lost with the page.
2962
+ const unsaved = notesView.status !== 'saved' && $('notes-text').value !== '';
2963
+ if (running || unsaved) addEventListener('beforeunload', confirmLeaving);
2862
2964
  else removeEventListener('beforeunload', confirmLeaving);
2863
2965
  }
2864
2966
 
@@ -3761,6 +3863,16 @@ document.addEventListener('keydown', (e) => {
3761
3863
  }, true);
3762
3864
  addEventListener('scroll', () => { if (tipFor) hideTip(); }, true);
3763
3865
  addEventListener('resize', () => { if (tipFor) hideTip(); });
3866
+ $('notes-open').addEventListener('click', firstClick(openNotes));
3867
+ $('notes-close').addEventListener('click', firstClick(closeNotes));
3868
+ $('notes').addEventListener('mousedown', keepFocus);
3869
+ $('notes').addEventListener('pointerdown', notesPressed);
3870
+ $('notes').addEventListener('click', notesClicked);
3871
+ $('notes').addEventListener('close', notesClosed);
3872
+ $('notes-text').addEventListener('input', saveNotes);
3873
+ addEventListener('storage', notesStored);
3874
+ // On load, and again for a page back from the back/forward cache, which may have missed another tab's notes.
3875
+ addEventListener('pageshow', refreshNotes);
3764
3876
  $('news-all').addEventListener('click', openNews);
3765
3877
  $('news-close').addEventListener('click', closeNews);
3766
3878
  $('news-fresh').addEventListener('click', showFreshNews);
@@ -3843,8 +3955,8 @@ $('models-more').addEventListener('click', () => {
3843
3955
  renderModels();
3844
3956
  $('models-list').children[before]?.querySelector('.model-toggle')?.focus();
3845
3957
  });
3846
- $('stop-manager').addEventListener('click', () => stopManager());
3847
- $('restart-manager').addEventListener('click', () => stopManager({ restart: true }));
3958
+ $('stop-manager').addEventListener('click', firstClick(() => stopManager()));
3959
+ $('restart-manager').addEventListener('click', firstClick(() => stopManager({ restart: true })));
3848
3960
  $('copy-command').addEventListener('click', copyCommand);
3849
3961
  $('upgrade').addEventListener('click', upgradeManager);
3850
3962
  $('appearance-menu').addEventListener('change', (e) => {
package/web/index.html CHANGED
@@ -41,6 +41,7 @@
41
41
  <label class="choice" title="Plays when the session manager stops, remains unavailable after a brief recovery check, or a new Agent Guild update is discovered. Click or type on this page to allow playback."><input id="sound" type="checkbox" name="sound"><span class="sound-alerts">Alert sounds</span></label>
42
42
  </fieldset>
43
43
  </div>
44
+ <button id="notes-open" class="btn notes-open" type="button" aria-haspopup="dialog">Notes</button>
44
45
  <button id="restart-manager" class="btn restart-manager" type="button" hidden>Restart manager</button>
45
46
  <button id="stop-manager" class="btn danger stop-manager" type="button" hidden title="Stop the session manager. This ends every session.">Stop manager</button>
46
47
  </div>
@@ -179,6 +180,19 @@
179
180
  <p id="changelog-status" class="news-status" hidden></p>
180
181
  </dialog>
181
182
 
183
+ <dialog id="notes" class="news-panel notes-panel" aria-labelledby="notes-title">
184
+ <div class="news-bar">
185
+ <div class="news-heading">
186
+ <h2 id="notes-title">Notes</h2>
187
+ <p id="notes-sub" class="sub" aria-live="polite">Saved in this browser as you type</p>
188
+ </div>
189
+ <button id="notes-close" class="btn" type="button">Close</button>
190
+ </div>
191
+ <div class="news-body">
192
+ <textarea id="notes-text" class="notes-text" aria-labelledby="notes-title" aria-describedby="notes-sub" autocomplete="off" placeholder="Prompts, commands, to-dos: anything you want to keep at hand"></textarea>
193
+ </div>
194
+ </dialog>
195
+
182
196
  <dialog id="history" class="models history" aria-labelledby="history-title">
183
197
  <div class="models-bar">
184
198
  <span id="history-icon" class="provider-icon small"></span>
@@ -243,7 +243,7 @@
243
243
  }
244
244
  .btn.primary:hover { filter: brightness(1.08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), inset 0 0 0 1px rgba(233, 196, 122, 0.6), 0 5px 16px rgba(40, 70, 30, 0.35); }
245
245
  .btn.danger:hover { border-color: var(--danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 14%, transparent); }
246
- .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select { background: var(--panel-strong); }
246
+ .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { background: var(--panel-strong); }
247
247
 
248
248
  .models, .news-panel, .auth, .news-latest, .appearance-menu { background: var(--panel-strong); backdrop-filter: blur(16px); }
249
249
  .models, .news-panel { box-shadow: var(--dialog-shadow); }
@@ -362,7 +362,7 @@
362
362
  .btn.primary { border-color: #6a8f3a; background: linear-gradient(#55924a, #35662f); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), inset 0 0 0 1px rgba(233, 196, 122, 0.3), 0 0 16px rgba(110, 170, 80, 0.3); }
363
363
  .btn.danger { color: #f59a8c; }
364
364
  .btn.danger:hover { border-color: #b8402f; box-shadow: 0 0 12px rgba(240, 123, 106, 0.3); }
365
- .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select { border-color: #4a3824; background: rgba(10, 7, 4, 0.7); }
365
+ .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { border-color: #4a3824; background: rgba(10, 7, 4, 0.7); }
366
366
 
367
367
  .models::backdrop, .news-panel::backdrop { background: rgba(6, 4, 2, 0.66); }
368
368
  .terminal-panel { border-color: #5a4226; box-shadow: 0 0 0 1px rgba(233, 196, 122, 0.06), 0 30px 80px rgba(0, 0, 0, 0.7); }
@@ -244,7 +244,7 @@
244
244
  }
245
245
  .btn.primary:hover { filter: brightness(1.07); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22), 0 6px 20px rgba(45, 107, 69, 0.36); }
246
246
  .btn.danger:hover { border-color: var(--danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 14%, transparent); }
247
- .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select { background: var(--paper-strong); }
247
+ .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { background: var(--paper-strong); }
248
248
 
249
249
  .models, .news-panel, .auth, .news-latest, .appearance-menu { background: var(--paper-strong); backdrop-filter: blur(16px); }
250
250
  .models, .news-panel { box-shadow: var(--dialog-shadow); }
@@ -371,7 +371,7 @@
371
371
  .btn.primary { background: linear-gradient(135deg, #4a8c5e, #2a6340); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 18px rgba(80, 160, 105, 0.35); }
372
372
  .btn.danger { color: #ff9a8e; }
373
373
  .btn.danger:hover { border-color: #b8443a; box-shadow: 0 0 12px rgba(240, 122, 108, 0.3); }
374
- .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select { border-color: #2e4a3d; background: rgba(4, 10, 8, 0.7); }
374
+ .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { border-color: #2e4a3d; background: rgba(4, 10, 8, 0.7); }
375
375
 
376
376
  .models::backdrop, .news-panel::backdrop { background: rgba(2, 6, 5, 0.66); }
377
377
  .terminal-panel { border-color: #2e4a3d; box-shadow: 0 0 0 1px rgba(140, 210, 160, 0.06), 0 30px 80px rgba(0, 0, 0, 0.7); }
@@ -376,7 +376,7 @@
376
376
  .btn.primary:hover { border-color: var(--gold); filter: brightness(1.08); }
377
377
  .btn.danger { color: #ff9a94; }
378
378
  .btn.danger:hover { border-color: #b8524d; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), 0 0 12px rgba(239, 107, 102, 0.3); }
379
- .cwd input, .auth input { border-color: #4a3d25; background: rgba(12, 13, 17, 0.8); }
379
+ .cwd input, .auth input, .notes-text { border-color: #4a3d25; background: rgba(12, 13, 17, 0.8); }
380
380
  .auth {
381
381
  background: var(--card-bg);
382
382
  border: 1px solid #4a3d25; box-shadow: inset 0 0 0 1px rgba(232, 196, 124, 0.06), 0 8px 24px rgba(0, 0, 0, 0.35);
@@ -480,7 +480,7 @@
480
480
  }
481
481
  .btn.primary:hover { border-color: #b8892f; filter: brightness(1.08); }
482
482
  .btn.danger:hover { border-color: #c4625d; }
483
- .cwd input, .auth input, .history-tools input[type="search"], .history-form input {
483
+ .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .notes-text {
484
484
  border-color: #dccba5; background: #fffdf8; box-shadow: inset 0 1px 2px rgba(70, 48, 12, 0.06);
485
485
  }
486
486
  .auth { border-color: #e4d6b6; box-shadow: var(--card-shadow); }
@@ -246,7 +246,7 @@
246
246
  }
247
247
  .btn.primary:hover { filter: brightness(1.08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 6px 20px rgba(122, 79, 232, 0.4); }
248
248
  .btn.danger:hover { border-color: var(--danger); box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 14%, transparent); }
249
- .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select { background: var(--glass-strong); }
249
+ .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { background: var(--glass-strong); }
250
250
 
251
251
  .models, .news-panel, .auth, .news-latest, .appearance-menu { background: var(--glass-strong); backdrop-filter: blur(16px); }
252
252
  .models, .news-panel { box-shadow: var(--dialog-shadow); }
@@ -367,7 +367,7 @@
367
367
  .btn.primary { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22), 0 0 18px rgba(106, 92, 240, 0.45); }
368
368
  .btn.danger { color: #ff8c9c; }
369
369
  .btn.danger:hover { border-color: #c23a52; box-shadow: 0 0 12px rgba(255, 107, 129, 0.3); }
370
- .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select { border-color: #26355c; background: rgba(4, 7, 16, 0.7); }
370
+ .cwd input, .auth input, .history-tools input[type="search"], .history-form input, .github-fields input, .github-fields select, .notes-text { border-color: #26355c; background: rgba(4, 7, 16, 0.7); }
371
371
 
372
372
  .models::backdrop, .news-panel::backdrop { background: rgba(2, 4, 10, 0.66); }
373
373
  .terminal-panel { border-color: #2a3a66; box-shadow: 0 0 0 1px rgba(120, 170, 255, 0.06), 0 30px 80px rgba(0, 0, 0, 0.7); }
package/web/styles.css CHANGED
@@ -29,6 +29,7 @@
29
29
  --icon-github: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z'/%3E%3C/svg%3E");
30
30
  --icon-info: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 10.8v6.2' stroke-width='2.4'/%3E%3Ccircle cx='12' cy='7.3' r='1.2' fill='black' stroke='none'/%3E%3C/svg%3E");
31
31
  --icon-grip: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black'%3E%3Ccircle cx='9' cy='5.5' r='1.9'/%3E%3Ccircle cx='15' cy='5.5' r='1.9'/%3E%3Ccircle cx='9' cy='12' r='1.9'/%3E%3Ccircle cx='15' cy='12' r='1.9'/%3E%3Ccircle cx='9' cy='18.5' r='1.9'/%3E%3Ccircle cx='15' cy='18.5' r='1.9'/%3E%3C/svg%3E");
32
+ --icon-notes: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 4.5h14v9.5l-5.5 5.5H5z'/%3E%3Cpath d='M13.5 19.5V14H19M8.5 9h7M8.5 12.5h4'/%3E%3C/svg%3E");
32
33
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
33
34
  font-size: 15px;
34
35
  color-scheme: light;
@@ -336,13 +337,14 @@ main { max-width: 1200px; margin: 0 auto; padding: 26px 16px 56px; display: grid
336
337
  .btn.danger { color: var(--danger); }
337
338
  .btn.danger:hover { border-color: var(--danger); }
338
339
  .btn:disabled { opacity: 0.6; cursor: progress; }
339
- .stop-manager, .restart-manager, .appearance, .upgrade { white-space: nowrap; }
340
- .appearance::before, .appearance-menu .choice > span::before {
340
+ .stop-manager, .restart-manager, .appearance, .notes-open, .upgrade { white-space: nowrap; }
341
+ .appearance::before, .notes-open::before, .appearance-menu .choice > span::before {
341
342
  content: ""; display: inline-block; width: 1em; height: 1em; margin-right: 0.4em; vertical-align: -0.15em;
342
343
  background: currentColor; mask: var(--icon-sun) center / contain no-repeat;
343
344
  }
344
345
  :root[data-theme="dark"] .appearance::before, .appearance-menu .choice > .mode-dark::before { mask-image: var(--icon-moon); }
345
346
  .appearance-menu .choice > .sound-alerts::before { mask-image: var(--icon-sound); }
347
+ .notes-open::before { mask-image: var(--icon-notes); }
346
348
  .appearance::after { margin-left: 0.45em; transform: rotate(90deg); transition: transform 0.15s var(--ease); }
347
349
  .appearance:has(+ :popover-open)::after { transform: rotate(-90deg); }
348
350
  .appearance-menu {
@@ -547,6 +549,20 @@ a.news-link:visited { color: var(--muted); }
547
549
  .changelog-changes li::marker { color: var(--muted); }
548
550
  .changelog-changes code { font-size: 0.8rem; padding: 0 4px; border-radius: 4px; background: var(--surface-2); }
549
551
 
552
+ /* Notes: the News side sheet, holding one text field that skins style with their other fields. */
553
+ .notes-panel .news-body { display: flex; flex-direction: column; padding-top: 12px; }
554
+ .notes-text {
555
+ flex: 1; min-height: 8em; margin: 0; padding: 10px 12px; resize: none;
556
+ border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text);
557
+ font: inherit; font-size: 0.9rem; line-height: 1.55;
558
+ }
559
+ .notes-text:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
560
+ .notes-panel .sub.warn { color: var(--warn); }
561
+ @media (pointer: coarse) {
562
+ /* iOS zooms in on a field with smaller text when it takes focus. */
563
+ .notes-text { font-size: 16px; }
564
+ }
565
+
550
566
  .history-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 10px 14px 0; }
551
567
  .history-tools input[type="search"] {
552
568
  flex: 1 1 220px; min-width: 0; padding: 7px 10px; border-radius: var(--radius); border: 1px solid var(--border);