overplay 4.0.0 → 4.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.2.0 — 2026-10-17
4
+
5
+ - Widget Settings has two tabs: **General** (panel, notifications and the shortcut) and **Appearance** (theme, pins and the comments button).
6
+ - Opening the sidebar with the shortcut no longer shows a focus ring on the Settings button. Focus moves to the panel; Tab still reaches every button.
7
+ - Website: new pages **How It Works** and **MCP Server** in the header. The MCP Server page lists all 20 MCP tools by permission. The New pill on the landing page opens it.
8
+ - Website: Pricing, Docs, How It Works and MCP Server share one page header (label, title, text, buttons).
9
+ - Dashboard: the usage bars on Plan & Billing use the full width. Team rows no longer push the arrow out of the card on hover, and Teams in the sidebar has a two-people icon.
10
+
11
+ ## 4.1.0 — 2026-10-17
12
+
13
+ - New default shortcut to open and close comments: **Alt+C** (**Option+C** on Mac). It works on any keyboard layout. Alt+Shift+C still works, but on Windows Alt+Shift can switch the keyboard language. Settings shows Option on Mac. Set `keyboardShortcut` to keep another key.
14
+ - The update notice in the sidebar footer now offers a prompt for your coding agent: Show Prompt and Copy Prompt. The prompt updates the package, reads the release notes, clears the bundler cache (Vite, Next.js, Nuxt and others) and restarts the dev server. A version bump alone often kept serving the old widget.
15
+ - The update notice no longer shows for the script tag, which loads the server's copy and updates by itself.
16
+ - Dashboard: on the Projects page each team is a panel with its name, project count and New Project button. Project tiles use the brand blue.
17
+ - Website: the MCP section shows a short chat in which a coding agent reads yesterday's comments, fixes one and replies.
18
+ - The public source moves to [github.com/artemsvit/Overplay-Community](https://github.com/artemsvit/Overplay-Community): the widget and the Community server. Links on the site and in the package point there.
19
+
3
20
  ## 4.0.0 — 2026-10-17
4
21
 
5
22
  The package and code now use the Overplay name everywhere. This release has breaking changes.
package/LICENSE CHANGED
@@ -1,5 +1,8 @@
1
1
  MIT License
2
2
 
3
+ This license covers the Overplay widget (the `overplay` npm package) and every file outside
4
+ `service/`. The service in `service/` uses the Functional Source License: see service/LICENSE.md.
5
+
3
6
  Copyright (c) 2026 Overplay contributors
4
7
 
5
8
  Permission is hereby granted, free of charge, to any person obtaining a copy
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  <p align="center">
12
12
  <a href="https://www.npmjs.com/package/overplay"><img src="https://img.shields.io/npm/v/overplay?color=575ce5" alt="npm version"></a>
13
- <a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-575ce5" alt="MIT license"></a>
13
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/widget-MIT-575ce5" alt="Widget: MIT license"></a> <a href="./service/LICENSE.md"><img src="https://img.shields.io/badge/service-FSL--1.1--MIT-575ce5" alt="Service: FSL-1.1-MIT license"></a>
14
14
  <a href="https://overplay.app/"><img src="https://img.shields.io/badge/cloud-overplay.app-575ce5" alt="Cloud"></a>
15
15
  </p>
16
16
 
@@ -110,7 +110,7 @@ Read the [self-hosting guide](docs/SELF_HOSTING.md).
110
110
 
111
111
  | Key | Action |
112
112
  | --- | --- |
113
- | <kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>C</kbd> (<kbd>Option</kbd> on Mac) | Open or close the widget |
113
+ | <kbd>Alt</kbd> + <kbd>C</kbd> (<kbd>Option</kbd> + <kbd>C</kbd> on Mac) | Open or close the widget |
114
114
  | <kbd>Esc</kbd> | Cancel |
115
115
  | <kbd>Enter</kbd> | Place a pin on the focused element |
116
116
 
@@ -132,7 +132,10 @@ The hosted service (Cloudflare Worker + D1) is in [`service/`](service/README.md
132
132
 
133
133
  ## License
134
134
 
135
- [MIT](./LICENSE)
135
+ - **Widget** (the `overplay` npm package: `src/`, `dist/`): [MIT](./LICENSE). Use it on any site, free.
136
+ - **Service** (Cloud and Community: everything in `service/`): [Functional Source License 1.1, MIT Future License](./service/LICENSE.md) (FSL-1.1-MIT). You may run Community for free, for your own company and your clients, and change it. You may not offer it as a competing paid or hosted product. Two years after each release, that release becomes MIT.
137
+
138
+ Code published before this change keeps the MIT license it was published under.
136
139
 
137
140
  ## Connect a coding agent
138
141
 
@@ -421,6 +421,10 @@ function matchesShortcut(event, shortcut) {
421
421
  const code = /^[a-z]$/.test(shortcut.key) ? `Key${shortcut.key.toUpperCase()}` : /^\d$/.test(shortcut.key) ? `Digit${shortcut.key}` : "";
422
422
  return event.key.toLowerCase() === shortcut.key || !!code && event.code === code;
423
423
  }
424
+ function shortcutLabel(value, mac) {
425
+ const names = mac ? { alt: "Option", option: "Option", meta: "Cmd", cmd: "Cmd", ctrl: "Control", control: "Control" } : { alt: "Alt", option: "Alt", meta: "Meta", cmd: "Meta", ctrl: "Ctrl", control: "Ctrl" };
426
+ return value.toLowerCase().split("+").map((part) => part.trim()).filter(Boolean).map((part) => names[part] ?? (part.length === 1 ? part.toUpperCase() : part[0].toUpperCase() + part.slice(1))).join(" + ");
427
+ }
424
428
  function newId() {
425
429
  if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
426
430
  const bytes = crypto.getRandomValues(new Uint8Array(16));
@@ -470,10 +474,26 @@ function newActivity(comments, seen, selfId) {
470
474
  function sameComments(a, b) {
471
475
  return a.length === b.length && JSON.stringify(a) === JSON.stringify(b);
472
476
  }
473
- const version = "4.0.0";
477
+ const version = "4.2.0";
474
478
  const VERSION = version;
475
479
  const PACKAGE = "overplay";
476
- const UPDATE_COMMAND = `npm install ${PACKAGE}@latest`;
480
+ function updatePrompt(current, latest) {
481
+ return `Update the Overplay widget in this project from version ${current} to ${latest}.
482
+
483
+ Steps:
484
+ 1. Find the package manager (look for package-lock.json, pnpm-lock.yaml, yarn.lock or bun.lockb) and update the package "${PACKAGE}" to ${latest}:
485
+ npm install ${PACKAGE}@${latest} (or: pnpm add ${PACKAGE}@${latest}, yarn add ${PACKAGE}@${latest}, bun add ${PACKAGE}@${latest})
486
+ 2. Read the release notes from ${current} to ${latest}: https://github.com/artemsvit/Overplay-Community/blob/main/CHANGELOG.md
487
+ If a release lists breaking changes, update our code to match. Keep serverUrl, projectId and the other options as they are.
488
+ 3. Stop the dev server and clear the bundler cache, so the old widget is not served again:
489
+ - Vite: delete node_modules/.vite
490
+ - Next.js: delete .next
491
+ - Nuxt: delete .nuxt and node_modules/.cache
492
+ - Other tools (webpack, Parcel, Astro, SvelteKit): delete node_modules/.cache, .parcel-cache, .astro or .svelte-kit if they exist.
493
+ 4. Start the dev server again and tell me to hard-reload the browser (Cmd+Shift+R or Ctrl+Shift+R).
494
+ 5. Run the project's type check and build, and fix any errors.
495
+ 6. When you finish, list the files you changed. The update is done when the widget no longer shows "Version ${latest} is available".`;
496
+ }
477
497
  function isNewer(latest, current) {
478
498
  const parse = (v) => {
479
499
  var _a;
@@ -572,6 +592,8 @@ const paths = {
572
592
  arrow: "M12 19V5m-6 6 6-6 6 6",
573
593
  refresh: "M20.5 12a8.5 8.5 0 1 1-2.6-6.1L20.5 8.5M20.5 3.5v5h-5",
574
594
  open: "M7 17 17 7M9 7h8v8",
595
+ copy: "M9 9h11v11H9zM5 15H4V4h11v1",
596
+ sparkle: "M10 3.5c.6 3.9 2.6 5.9 6.5 6.5-3.9.6-5.9 2.6-6.5 6.5-.6-3.9-2.6-5.9-6.5-6.5 3.9-.6 5.9-2.6 6.5-6.5ZM18 14.5c.3 1.9 1.1 2.7 3 3-1.9.3-2.7 1.1-3 3-.3-1.9-1.1-2.7-3-3 1.9-.3 2.7-1.1 3-3Z",
575
597
  trash: "M3 6h18M9 6V4h6v2M5 6l1 14h12l1-14M10 10v6m4-6v6",
576
598
  pin: "M12 21s7-6 7-13A7 7 0 0 0 5 8c0 7 7 13 7 13ZM12 6v4M10 8h4",
577
599
  search: "m21 21-5-5M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Z",
@@ -671,6 +693,10 @@ const tokens = {
671
693
  warning: "#745320",
672
694
  "warning-soft": "#fff8e8",
673
695
  "warning-line": "#ebd59f",
696
+ // AI: a light violet that marks AI features, such as the AI Install prompt. Never for buttons.
697
+ ai: "#6d4fe0",
698
+ "ai-soft": "#f6f3ff",
699
+ "ai-line": "#e3dbff",
674
700
  // Shape. Buttons and chips are pills; cards and windows have soft corners; big page panels have 26px.
675
701
  "radius-sm": "6px",
676
702
  radius: "10px",
@@ -710,6 +736,9 @@ const darkTokens = {
710
736
  warning: "#efc56e",
711
737
  "warning-soft": "#2f2718",
712
738
  "warning-line": "#57472a",
739
+ ai: "#a994ff",
740
+ "ai-soft": "#1d1a2c",
741
+ "ai-line": "#3b3263",
713
742
  "shadow-sm": "0 0 0 1px #ffffff14,0 1px 2px #00000066",
714
743
  shadow: "0 1px 3px #00000066,0 16px 40px -12px #000000b3"
715
744
  };
@@ -752,7 +781,7 @@ const styles = `
752
781
  .menu-note{display:flex;align-items:center;gap:2px;margin:4px 0 0;padding:8px 10px 4px;border-top:1px solid var(--cw-line);font-size:12px;color:var(--cw-muted)}
753
782
  .footer{justify-content:space-between}
754
783
  .footer-note{font-size:13px;color:var(--cw-muted)}
755
- .settings-body{flex:1;min-height:0;overflow:auto;overscroll-behavior:contain;padding:4px 20px 12px}
784
+ .panel:focus{outline:0}.settings-tabs{grid-template-columns:1fr 1fr;margin:0 0 12px}.settings-body{flex:1;min-height:0;overflow:auto;overscroll-behavior:contain;padding:4px 20px 12px}
756
785
  .setting-group{padding:14px 0 6px}
757
786
  .setting-group+.setting-group{border-top:1px solid var(--cw-line)}
758
787
  .setting-group h3{margin:0 0 6px;font-size:12px;font-weight:650;letter-spacing:1.1px;text-transform:uppercase;color:var(--cw-muted)}
@@ -801,7 +830,7 @@ const styles = `
801
830
  .anchor-status .quiet{display:inline-flex;align-items:center;gap:6px;margin-top:10px;padding:6px 11px 6px 9px;font-size:13px;font-weight:600}.anchor-status .quiet svg{width:15px;height:15px;flex-shrink:0}
802
831
  @media(max-width:600px){.panel,.panel.left{inset:0;width:auto;border:0}.panel.placing{visibility:hidden}.panel.behind{visibility:hidden}.launcher{right:16px;bottom:16px}.notice,.notice.beside-panel,.notice.beside-panel.left{left:50%;bottom:24px;width:calc(100vw - 32px);font-size:13px}.compose-hint{display:none}.compose-bar{justify-content:flex-end}.toasts,.toasts.left,.toasts.beside-panel,.toasts.beside-panel.left{left:16px;right:16px;width:auto;max-width:none;bottom:82px}.toasts.top,.toasts.beside-panel.top{top:82px;bottom:auto}.toasts.beside-panel{display:none}.head{padding-top:18px}}
803
832
  .popover.moving{visibility:hidden;pointer-events:none}
804
- .update-bar{display:grid;grid-template-columns:30px 1fr auto;align-items:start;gap:4px 12px;padding:14px 12px 16px 20px;background:var(--cw-accent-soft);border-top-color:var(--cw-accent-line)}.update-icon{display:grid;place-items:center;width:30px;height:30px;border-radius:9px;background:var(--cw-surface);color:var(--cw-accent);box-shadow:var(--cw-shadow-sm)}.update-icon svg{width:16px;height:16px}.update-text{display:flex;flex-direction:column;gap:1px;min-width:0;color:var(--cw-muted);font-size:12.5px;line-height:1.45}.update-text strong{color:var(--cw-ink);font-size:13px;font-weight:650}.update-command{grid-column:1/-1;display:flex;align-items:center;gap:8px;margin:8px 8px 0 0;padding:4px 4px 4px 10px;border:1px solid var(--cw-line);border-radius:9px;background:var(--cw-surface)}.update-command code{flex:1;min-width:0;color:var(--cw-ink);font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.update-bar .quiet.update{padding:5px 12px;border-radius:var(--cw-radius-pill)}
833
+ .update-bar{display:grid;grid-template-columns:30px 1fr auto;align-items:start;gap:4px 12px;padding:14px 12px 16px 20px;background:var(--cw-accent-soft);border-top-color:var(--cw-accent-line)}.update-icon{display:grid;place-items:center;width:30px;height:30px;border-radius:9px;background:var(--cw-surface);color:var(--cw-accent);box-shadow:var(--cw-shadow-sm)}.update-icon svg{width:16px;height:16px}.update-text{display:flex;flex-direction:column;gap:1px;min-width:0;color:var(--cw-muted);font-size:12.5px;line-height:1.45}.update-text strong{color:var(--cw-ink);font-size:13px;font-weight:650}.update-actions{grid-column:2/-1;display:flex;flex-wrap:wrap;gap:8px;margin:8px 8px 0 0}.update-actions .quiet{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;font-size:13px;font-weight:600;border-radius:var(--cw-radius-pill)}.update-actions svg{width:14px;height:14px}.update-toggle svg{color:var(--cw-ai)}.update-prompt{grid-column:1/-1;margin:10px 8px 0 0;max-height:220px;overflow:auto;padding:10px 12px;border:1px solid var(--cw-ai-line);border-radius:10px;background:linear-gradient(135deg,var(--cw-ai-soft),var(--cw-surface));color:var(--cw-ink);font:11.5px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;overflow-wrap:anywhere;user-select:text;-webkit-user-select:text}.update-prompt:focus-visible{outline:2px solid var(--cw-accent);outline-offset:2px}
805
834
  @media(max-width:600px){.search input{font-size:16px}.select{max-width:100%}}
806
835
  @media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
807
836
  `;
@@ -1667,6 +1696,8 @@ class Overlay {
1667
1696
  __publicField(this, "settingsCache", null);
1668
1697
  /** A newer npm version, found when the sidebar first opens. See checkForUpdate(). */
1669
1698
  __publicField(this, "update", null);
1699
+ __publicField(this, "updatePromptOpen", false);
1700
+ __publicField(this, "settingsTab", "general");
1670
1701
  __publicField(this, "updateChecked", false);
1671
1702
  /** The page margin we replaced to make room for the sidebar, restored when it closes. */
1672
1703
  __publicField(this, "pushed", null);
@@ -1884,13 +1915,13 @@ class Overlay {
1884
1915
  });
1885
1916
  });
1886
1917
  var _a, _b, _c;
1887
- this.config = { primaryColor: "#0068f9", position: "bottom-right", keyboardShortcut: "alt+shift+c", enableKeyboardShortcuts: true, ...config };
1918
+ this.config = { primaryColor: "#0068f9", position: "bottom-right", keyboardShortcut: "alt+c", enableKeyboardShortcuts: true, ...config };
1888
1919
  if (!/^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test(this.config.primaryColor ?? "")) {
1889
1920
  throw new Error("primaryColor must be a 3- or 6-digit hex color.");
1890
1921
  }
1891
1922
  if (this.config.theme !== void 0 && !THEMES.includes(this.config.theme)) throw new Error('theme must be "light", "dark" or "system".');
1892
1923
  this.shortcut = parseShortcut(this.config.keyboardShortcut ?? "");
1893
- if (this.config.enableKeyboardShortcuts && !this.shortcut) throw new Error('keyboardShortcut must be a key such as "alt+shift+c".');
1924
+ if (this.config.enableKeyboardShortcuts && !this.shortcut) throw new Error('keyboardShortcut must be a key such as "alt+c".');
1894
1925
  const project = config.projectId || location.hostname || "default";
1895
1926
  this.identityKey = `overplay:${project}:user`;
1896
1927
  this.launcherKey = `overplay:${project}:launcher`;
@@ -2229,7 +2260,11 @@ ${this.currentPage()}`;
2229
2260
  this.visible = true;
2230
2261
  this.render();
2231
2262
  this.trackLayoutMotion();
2232
- (_a = this.shadow.querySelector(".panel button, .launcher")) == null ? void 0 : _a.focus();
2263
+ const panel = this.shadow.querySelector(".panel");
2264
+ if (panel) {
2265
+ panel.tabIndex = -1;
2266
+ panel.focus({ preventScroll: true });
2267
+ } else (_a = this.shadow.querySelector(".launcher")) == null ? void 0 : _a.focus();
2233
2268
  this.checkForUpdate();
2234
2269
  }
2235
2270
  hide() {
@@ -2727,7 +2762,9 @@ ${this.currentPage()}`;
2727
2762
  * Only development hosts check by default: that is where the person reading the notice can run npm.
2728
2763
  */
2729
2764
  checkForUpdate() {
2730
- if (this.updateChecked || !(this.config.updateNotice ?? developmentHost(location.hostname))) return;
2765
+ var _a;
2766
+ const scriptTag = typeof ((_a = globalThis.Overplay) == null ? void 0 : _a.init) === "function";
2767
+ if (this.updateChecked || !(this.config.updateNotice ?? (developmentHost(location.hostname) && !scriptTag))) return;
2731
2768
  this.updateChecked = true;
2732
2769
  void latestVersion().then((latest) => {
2733
2770
  if (this.destroyed || !latest || !isNewer(latest, VERSION)) return;
@@ -2740,19 +2777,24 @@ ${this.currentPage()}`;
2740
2777
  });
2741
2778
  }
2742
2779
  async copyUpdate() {
2780
+ const prompt = updatePrompt(VERSION, this.update ?? "latest");
2743
2781
  let copied = false;
2744
2782
  try {
2745
- await navigator.clipboard.writeText(UPDATE_COMMAND);
2783
+ await navigator.clipboard.writeText(prompt);
2746
2784
  copied = true;
2747
2785
  } catch {
2748
2786
  }
2787
+ if (!copied) {
2788
+ this.updatePromptOpen = true;
2789
+ this.render();
2790
+ }
2749
2791
  this.toast({
2750
2792
  kind: "comment",
2751
2793
  commentId: "",
2752
- author: copied ? `Update command copied` : "Run this to update",
2753
- text: copied ? `Run it in your project, then reload: ${UPDATE_COMMAND}` : UPDATE_COMMAND
2794
+ author: copied ? "Update prompt copied" : "Copy the prompt below",
2795
+ text: copied ? "Paste it into your coding agent. It updates the package, clears the cache and restarts the dev server." : "Select the prompt text and copy it."
2754
2796
  }, true);
2755
- this.announce(copied ? "Update command copied." : `Run ${UPDATE_COMMAND} to update.`);
2797
+ this.announce(copied ? "Update prompt copied." : "Select the prompt text to copy it.");
2756
2798
  }
2757
2799
  /** "Later" hides the notice until the next version comes out. */
2758
2800
  dismissUpdate() {
@@ -3468,6 +3510,10 @@ ${this.currentPage()}`;
3468
3510
  return empty;
3469
3511
  }
3470
3512
  /** A new npm version is the one thing left at the bottom. The account and settings live in the header. */
3513
+ /**
3514
+ * The update notice: an AI prompt that updates the package, clears the bundler cache and restarts the dev
3515
+ * server. A version bump alone often keeps serving the old widget.
3516
+ */
3471
3517
  footer() {
3472
3518
  if (!this.update) return null;
3473
3519
  const footer = element("div", "footer update-bar");
@@ -3475,16 +3521,24 @@ ${this.currentPage()}`;
3475
3521
  const mark = element("span", "update-icon");
3476
3522
  mark.append(icon("refresh"));
3477
3523
  const text = element("div", "update-text");
3478
- text.append(element("strong", "", `Version ${this.update} is available`), element("span", "", `You have ${VERSION}. Update the package, then reload.`));
3479
- const command = element("div", "update-command");
3480
- const code = element("code", "", UPDATE_COMMAND);
3481
- code.title = UPDATE_COMMAND;
3482
- const copy = button(`Copy the command to update to version ${this.update}`, "quiet update", () => {
3524
+ text.append(element("strong", "", `Version ${this.update} is available`), element("span", "", `You have ${VERSION}. Give this prompt to your coding agent to update.`));
3525
+ const actions = element("div", "update-actions");
3526
+ const show = button(this.updatePromptOpen ? "Hide Prompt" : "Show Prompt", "quiet update-toggle", () => {
3527
+ this.updatePromptOpen = !this.updatePromptOpen;
3528
+ this.render();
3529
+ }, "sparkle");
3530
+ show.setAttribute("aria-expanded", String(this.updatePromptOpen));
3531
+ const copy = button("Copy Prompt", "quiet update", () => {
3483
3532
  void this.copyUpdate();
3484
- });
3485
- copy.querySelector("span").textContent = "Copy";
3486
- command.append(code, copy);
3487
- footer.append(mark, text, button("Hide until the next version", "icon-button", () => this.dismissUpdate(), "close"), command);
3533
+ }, "copy");
3534
+ actions.append(show, copy);
3535
+ footer.append(mark, text, button("Hide until the next version", "icon-button", () => this.dismissUpdate(), "close"), actions);
3536
+ if (this.updatePromptOpen) {
3537
+ const prompt = element("pre", "update-prompt", updatePrompt(VERSION, this.update));
3538
+ prompt.tabIndex = 0;
3539
+ prompt.setAttribute("aria-label", "Update prompt for your coding agent");
3540
+ footer.append(prompt);
3541
+ }
3488
3542
  return footer;
3489
3543
  }
3490
3544
  messageHeader(name, timestamp, url, via, words2 = []) {
@@ -3637,14 +3691,26 @@ ${this.currentPage()}`;
3637
3691
  }
3638
3692
  /** Display choices for this project. Each person has their own, saved in this browser. */
3639
3693
  settingsView() {
3640
- var _a;
3641
3694
  const view = this.view("Settings", "settings");
3642
3695
  view.append(this.detailHeading("Settings", "Your choices for this project. Other people keep their own."));
3643
3696
  const current = this.settings, body = element("div", "settings-body");
3644
- const section = (title) => {
3697
+ const tabs = element("div", "tabs settings-tabs");
3698
+ tabs.setAttribute("role", "group");
3699
+ tabs.setAttribute("aria-label", "Settings section");
3700
+ for (const [id, label2] of [["general", "General"], ["appearance", "Appearance"]]) {
3701
+ const tab = button(label2, "tab", () => {
3702
+ this.settingsTab = id;
3703
+ this.render();
3704
+ });
3705
+ tab.setAttribute("aria-pressed", String(this.settingsTab === id));
3706
+ tab.dataset.focus = `settings-tab-${id}`;
3707
+ tabs.append(tab);
3708
+ }
3709
+ body.append(tabs);
3710
+ const section = (title, tab) => {
3645
3711
  const group = element("section", "setting-group");
3646
3712
  group.append(element("h3", "", title));
3647
- body.append(group);
3713
+ if (tab === this.settingsTab) body.append(group);
3648
3714
  return group;
3649
3715
  };
3650
3716
  const row = (group, label2, hint, control) => {
@@ -3680,9 +3746,9 @@ ${this.currentPage()}`;
3680
3746
  control.addEventListener("click", () => this.setSetting(name, !on));
3681
3747
  return control;
3682
3748
  };
3683
- const look = section("Appearance");
3749
+ const look = section("Theme", "appearance");
3684
3750
  row(look, "Theme", current.theme === "system" ? "Follows your device" : "", choice("theme", "Theme", [["light", "Light"], ["dark", "Dark"], ["system", "System"]]));
3685
- const pins = section("Pins");
3751
+ const pins = section("Pins", "appearance");
3686
3752
  const sample = element("div", `pin-sample${current.pinSize === "small" ? " small" : ""}${current.resolvedPins === "gray" ? " resolved-gray" : ""}`);
3687
3753
  sample.setAttribute("aria-hidden", "true");
3688
3754
  sample.style.setProperty("--cw-accent", this.pinColor());
@@ -3706,7 +3772,7 @@ ${this.currentPage()}`;
3706
3772
  row(pins, "Resolved pins", "", choice("resolvedPins", "Resolved pins", [["green", "Green"], ["gray", "Gray"]]));
3707
3773
  row(pins, "Size", "", choice("pinSize", "Pin size", [["regular", "Regular"], ["small", "Small"]]));
3708
3774
  row(pins, "Label", "", choice("pinLabel", "Pin label", [["number", "Number"], ["initials", "Initials"]]));
3709
- const floating = section("Comments button");
3775
+ const floating = section("Comments button", "appearance");
3710
3776
  const buttonSample = element("div", "launcher-sample");
3711
3777
  buttonSample.setAttribute("aria-hidden", "true");
3712
3778
  const mini = element("span", "launcher-mini");
@@ -3728,7 +3794,7 @@ ${this.currentPage()}`;
3728
3794
  }
3729
3795
  const buttonHint = current.buttonColor === "default" ? "Dark. Takes the site color while open." : current.buttonColor === "site" ? "The color of this site" : PIN_COLORS[current.buttonColor].label;
3730
3796
  row(floating, "Color", buttonHint, buttonSwatches).classList.add("wide");
3731
- const panel = section("Panel");
3797
+ const panel = section("Panel", "general");
3732
3798
  const open = element("div", "segmented");
3733
3799
  open.setAttribute("role", "group");
3734
3800
  open.setAttribute("aria-label", "Open comments");
@@ -3742,9 +3808,9 @@ ${this.currentPage()}`;
3742
3808
  }
3743
3809
  row(panel, "Open comments", "", open);
3744
3810
  row(panel, "Push the page aside", "The open panel does not cover the page.", toggle("pushContent", "Push the page aside"));
3745
- const alerts = section("Notifications and keys");
3811
+ const alerts = section("Notifications and keys", "general");
3746
3812
  row(alerts, "New activity", "Show a short notice when others comment or reply.", toggle("notifications", "Show new activity"));
3747
- const keys = ((_a = this.config.keyboardShortcut) == null ? void 0 : _a.split("+").map((key2) => key2.length === 1 ? key2.toUpperCase() : key2[0].toUpperCase() + key2.slice(1)).join(" + ")) ?? "";
3813
+ const keys = shortcutLabel(this.config.keyboardShortcut ?? "", /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent));
3748
3814
  row(alerts, "Keyboard shortcut", this.shortcut ? `${keys} opens and closes comments.` : "This site has no shortcut.", toggle("shortcut", "Keyboard shortcut", !this.shortcut));
3749
3815
  const foot = element("div", "settings-foot");
3750
3816
  foot.append(element("span", "", "Saved in this browser"), button("Reset to Defaults", "quiet", () => this.resetSettings()));