extension-create 4.1.19 → 4.1.21

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.
@@ -3,6 +3,7 @@ declare const NODE_PACKAGE_MANAGERS: readonly ['pnpm', 'yarn', 'bun', 'npm'];
3
3
  type NodePackageManager = (typeof NODE_PACKAGE_MANAGERS)[number];
4
4
  export declare function detectPackageManagerFromEnv(): NodePackageManager;
5
5
  export declare function isDenoRuntime(): boolean;
6
+ export declare function isBunRuntime(): boolean;
6
7
  export type ScaffoldPackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun' | 'deno';
7
8
  export declare function resolveScaffoldPackageManager(): ScaffoldPackageManager;
8
9
  export declare function resolveProjectPackageManager(projectPath: string): ScaffoldPackageManager;
@@ -0,0 +1 @@
1
+ export declare function isUrlProjectName(projectNameInput: string): boolean;
package/dist/module.cjs CHANGED
@@ -136,8 +136,12 @@ function detectPackageManagerFromEnv() {
136
136
  function isDenoRuntime() {
137
137
  return void 0 !== globalThis.Deno || Boolean(process.versions?.deno);
138
138
  }
139
+ function isBunRuntime() {
140
+ return void 0 !== globalThis.Bun || Boolean(process.versions?.bun);
141
+ }
139
142
  function resolveScaffoldPackageManager() {
140
143
  if (isDenoRuntime()) return 'deno';
144
+ if (isBunRuntime()) return 'bun';
141
145
  return detectPackageManagerFromEnv();
142
146
  }
143
147
  function resolveProjectPackageManager(projectPath) {
@@ -145,6 +149,7 @@ function resolveProjectPackageManager(projectPath) {
145
149
  const pinned = readPinnedPackageManager(projectPath);
146
150
  if (pinned) return pinned;
147
151
  if (external_node_fs_namespaceObject.existsSync(external_node_path_namespaceObject.join(projectPath, 'pnpm-workspace.yaml'))) return 'pnpm';
152
+ if (isBunRuntime()) return 'bun';
148
153
  return detectPackageManagerFromEnv();
149
154
  }
150
155
  function readPinnedPackageManager(projectPath) {
@@ -358,6 +363,9 @@ function keepingExistingGitignore(projectName) {
358
363
  function existingRepositoryKept(projectName) {
359
364
  return `${prefix('info')} ${external_pintor_default().blue(projectName)} is already a git repository with history.\nLeft it uncommitted. Review the scaffold and run ${external_pintor_default().blue('git add -A && git commit')} yourself.`;
360
365
  }
366
+ function isUrlProjectName(projectNameInput) {
367
+ return /^https?:\/\//i.test(projectNameInput.trim());
368
+ }
361
369
  const promises_namespaceObject = require("node:fs/promises");
362
370
  async function copyDirectoryWithSymlinks(source, destination) {
363
371
  const entries = await promises_namespaceObject.readdir(source, {
@@ -609,7 +617,7 @@ const NETWORK_TIMEOUT_MS = (()=>{
609
617
  return Number.isFinite(raw) && raw > 0 ? raw : 60000;
610
618
  })();
611
619
  const CODELOAD_BASE = 'https://codeload.github.com/extension-js/examples/zip';
612
- const DEFAULT_TEMPLATES_REF = 'e552f6db6f2862093ba9d7026e0d376f002d95cc';
620
+ const DEFAULT_TEMPLATES_REF = '06d33e35894516a460409dca06d1614e291e6252';
613
621
  const BUNDLED_TEMPLATES = [
614
622
  "javascript"
615
623
  ];
@@ -2135,7 +2143,7 @@ async function extensionCreate(projectNameInput, { cliVersion, template, install
2135
2143
  if (!projectNameInput) throw new Error(noProjectName());
2136
2144
  const templateWasOmitted = null == template || '' === String(template).trim();
2137
2145
  const effectiveTemplate = templateWasOmitted ? DEFAULT_TEMPLATE_NAME : String(template);
2138
- if (projectNameInput.startsWith('http')) throw new Error(noUrlAllowed());
2146
+ if (isUrlProjectName(projectNameInput)) throw new Error(noUrlAllowed());
2139
2147
  const projectPath = external_node_path_namespaceObject.isAbsolute(projectNameInput) ? projectNameInput : external_node_path_namespaceObject.join(process.cwd(), projectNameInput);
2140
2148
  const projectName = external_node_path_namespaceObject.basename(projectPath);
2141
2149
  const updateSuffix = process.env.EXTENSION_CLI_UPDATE_SUFFIX || '';
@@ -1,4 +1,4 @@
1
- export declare const DEFAULT_TEMPLATES_REF = "e552f6db6f2862093ba9d7026e0d376f002d95cc";
1
+ export declare const DEFAULT_TEMPLATES_REF = "06d33e35894516a460409dca06d1614e291e6252";
2
2
  export declare const BUNDLED_TEMPLATES: readonly string[];
3
3
  export declare const DEFAULT_TEMPLATE_NAME = "typescript";
4
4
  export declare const OFFLINE_FALLBACK_TEMPLATE = "javascript";
@@ -2,10 +2,8 @@ import { type ScaffoldPackageManager } from '../lib/package-manager.js';
2
2
  export declare function resolveExtensionBinary(): Promise<string>;
3
3
  export declare function getTemplateAwareScripts(template: string, extensionBinary: string): Record<string, string>;
4
4
  interface OverridePackageJsonOptions {
5
- /** Defaults to `javascript` when omitted (same as `extensionCreate`). */
6
5
  template?: string;
7
6
  cliVersion?: string;
8
- /** The one manager the project uses; resolved from the scaffold when omitted. */
9
7
  packageManager?: ScaffoldPackageManager;
10
8
  }
11
9
  export declare function resolveExtensionDevDependencyVersion(cliVersion?: string): string;
@@ -1,6 +1,6 @@
1
1
  {
2
- "createdWith": "extension-create@4.1.18",
2
+ "createdWith": "extension-create@4.1.20",
3
3
  "template": "typescript",
4
- "source": "https://codeload.github.com/extension-js/examples/zip/e552f6db6f2862093ba9d7026e0d376f002d95cc",
5
- "ref": "e552f6db6f2862093ba9d7026e0d376f002d95cc"
4
+ "source": "https://codeload.github.com/extension-js/examples/zip/06d33e35894516a460409dca06d1614e291e6252",
5
+ "ref": "06d33e35894516a460409dca06d1614e291e6252"
6
6
  }
@@ -9,7 +9,7 @@ Packaging your extension is local and free. Submitting the result to a
9
9
  store is what [extension.dev](https://docs.extension.dev/publish/overview?utm_source=store-md)
10
10
  does, and it sponsors Extension.js.
11
11
 
12
- Last updated: 2026-09-15
12
+ Last updated: 2026-09-17
13
13
 
14
14
  ## Listing
15
15
 
@@ -8,7 +8,7 @@
8
8
  "devDependencies": {
9
9
  "@types/chrome": "^0.0.287",
10
10
  "typescript": "7.0.2",
11
- "extension": "^4.1.18"
11
+ "extension": "^4.1.20"
12
12
  },
13
13
  "scripts": {
14
14
  "dev": "extension dev",
@@ -6,22 +6,61 @@ const isFirefoxLike =
6
6
  import.meta.env.EXTENSION_PUBLIC_BROWSER === 'firefox' ||
7
7
  import.meta.env.EXTENSION_PUBLIC_BROWSER === 'gecko-based'
8
8
 
9
+ const isSafariLike =
10
+ import.meta.env.EXTENSION_PUBLIC_BROWSER === 'safari' ||
11
+ import.meta.env.EXTENSION_PUBLIC_BROWSER === 'webkit-based'
12
+
13
+ // Safari has no side panel surface, so the sidebar page opens in a tab.
14
+ let sidebarTabId: number | undefined
15
+
16
+ function openSidebarTab() {
17
+ const url = chrome.runtime.getURL('sidebar/index.html')
18
+
19
+ const openNewTab = () => {
20
+ chrome.tabs.create({url}, (tab) => {
21
+ sidebarTabId = tab?.id
22
+ })
23
+ }
24
+
25
+ // A repeat click focuses the tab already opened instead of a new copy.
26
+ const knownTabId = sidebarTabId
27
+
28
+ if (knownTabId === undefined) {
29
+ openNewTab()
30
+
31
+ return
32
+ }
33
+
34
+ chrome.tabs.update(knownTabId, {active: true}, () => {
35
+ if (chrome.runtime.lastError) openNewTab()
36
+ })
37
+ }
38
+
9
39
  if (isFirefoxLike) {
40
+ // Firefox refuses sidebarAction.open() outside a user input handler, and a
41
+ // message listener is not one, so the toolbar click is the only route.
10
42
  browser.browserAction.onClicked.addListener(() => {
11
43
  browser.sidebarAction.open()
12
44
  })
45
+ }
13
46
 
14
- browser.runtime.onMessage.addListener((message: any) => {
47
+ if (isSafariLike) {
48
+ // Safari never had setPanelBehavior, so the toolbar click needs a listener.
49
+ chrome.action?.onClicked.addListener(() => {
50
+ openSidebarTab()
51
+ })
52
+
53
+ chrome.runtime.onMessage.addListener((message) => {
15
54
  if (!message || message.type !== 'openSidebar') return
16
55
 
17
- browser.sidebarAction.open()
56
+ openSidebarTab()
18
57
  })
19
58
  }
20
59
 
21
- if (!isFirefoxLike) {
60
+ if (!isFirefoxLike && !isSafariLike) {
22
61
  // setPanelBehavior only affects FUTURE action clicks, registering it
23
62
  // inside onClicked would swallow the first toolbar click.
24
- chrome.sidePanel.setPanelBehavior({openPanelOnActionClick: true})
63
+ chrome.sidePanel?.setPanelBehavior({openPanelOnActionClick: true})
25
64
 
26
65
  // The side panel API only exists in Chromium. Firefox opens the sidebar in
27
66
  // the listener above, so this listener is compiled out of gecko builds.
@@ -32,13 +71,13 @@ if (!isFirefoxLike) {
32
71
  // allowed inside the user gesture that the content-script click carries, and
33
72
  // a tabs.query callback outlives it: the panel then silently refuses to open.
34
73
  // sender.tab is the tab the click came from, so no lookup is needed at all.
35
- chrome.sidePanel.setPanelBehavior({openPanelOnActionClick: true})
74
+ chrome.sidePanel?.setPanelBehavior({openPanelOnActionClick: true})
36
75
 
37
76
  const tabId = sender.tab?.id
38
- if (!chrome.sidePanel.open || tabId === undefined) return
77
+ if (!chrome.sidePanel?.open || tabId === undefined) return
39
78
 
40
79
  try {
41
- chrome.sidePanel.open({tabId})
80
+ chrome.sidePanel?.open({tabId})
42
81
  } catch (error) {
43
82
  console.error(error)
44
83
  }
@@ -1,20 +1,27 @@
1
1
  import logo from '../images/icon.png'
2
2
 
3
+ const isFirefoxLike =
4
+ import.meta.env.EXTENSION_PUBLIC_BROWSER === 'firefox' ||
5
+ import.meta.env.EXTENSION_PUBLIC_BROWSER === 'gecko-based'
6
+
3
7
  export default function createContentApp(): HTMLDivElement {
4
8
  const container = document.createElement('div')
5
9
  container.className = 'content_script'
6
10
 
7
- const pill = document.createElement('button')
8
- pill.type = 'button'
9
- pill.className = 'content_pill'
10
- pill.setAttribute('aria-label', 'Open sidebar')
11
- pill.addEventListener('click', () => {
12
- if (import.meta.env.EXTENSION_PUBLIC_BROWSER === 'firefox') {
13
- browser.runtime.sendMessage({type: 'openSidebar'})
14
- } else {
11
+ // Firefox cannot open a sidebar from a message listener, so the gecko build
12
+ // renders a hint naming the toolbar action instead of a dead control.
13
+ const pill = document.createElement(isFirefoxLike ? 'div' : 'button')
14
+ pill.className = isFirefoxLike
15
+ ? 'content_pill content_pill_static'
16
+ : 'content_pill'
17
+
18
+ if (!isFirefoxLike) {
19
+ ;(pill as HTMLButtonElement).type = 'button'
20
+ pill.setAttribute('aria-label', 'Open sidebar')
21
+ pill.addEventListener('click', () => {
15
22
  chrome.runtime.sendMessage({type: 'openSidebar'})
16
- }
17
- })
23
+ })
24
+ }
18
25
 
19
26
  const img = document.createElement('img')
20
27
  img.className = 'content_pill_logo'
@@ -26,7 +33,9 @@ export default function createContentApp(): HTMLDivElement {
26
33
 
27
34
  const text = document.createElement('span')
28
35
  text.className = 'content_pill_text'
29
- text.textContent = 'Open sidebar'
36
+ text.textContent = isFirefoxLike
37
+ ? 'Use the toolbar icon to open the sidebar'
38
+ : 'Open sidebar'
30
39
 
31
40
  pill.appendChild(img)
32
41
  pill.appendChild(text)
@@ -40,5 +40,6 @@ async function fetchCSS() {
40
40
  const cssUrl = new URL('./styles.css', import.meta.url)
41
41
  const response = await fetch(cssUrl)
42
42
  const text = await response.text()
43
+
43
44
  return response.ok ? text : Promise.reject(text)
44
45
  }
@@ -22,7 +22,7 @@
22
22
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
23
23
  }
24
24
 
25
- .content_pill:hover {
25
+ .content_pill:not(.content_pill_static):hover {
26
26
  background: #11151c;
27
27
  }
28
28
 
@@ -40,6 +40,12 @@
40
40
  font-weight: 600;
41
41
  line-height: 1;
42
42
  font-family:
43
- -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
44
- Arial, "Noto Sans", sans-serif;
43
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
44
+ Arial, 'Noto Sans', sans-serif;
45
+ }
46
+
47
+ /* The gecko build renders this hint in place of the pill, so it must not read
48
+ as a control: no pointer cursor and no hover response. */
49
+ .content_pill_static {
50
+ cursor: default;
45
51
  }
@@ -48,7 +48,8 @@
48
48
  },
49
49
  "firefox:sidebar_action": {
50
50
  "default_panel": "sidebar/index.html",
51
- "default_title": "Side Panel Content"
51
+ "default_title": "Side Panel Content",
52
+ "browser_style": false
52
53
  },
53
54
  "chromium:permissions": [
54
55
  "sidePanel"
@@ -11,7 +11,12 @@
11
11
  body {
12
12
  background-color: var(--sidebar-bg);
13
13
  color: var(--sidebar-text);
14
- height: 100vh;
14
+ font-family:
15
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
16
+ Arial, 'Noto Sans', sans-serif;
17
+ /* The margin insets the panel, so a full-viewport height would push the
18
+ content below centre and overflow the panel by twice the margin. */
19
+ height: calc(100vh - 2 * var(--sidebar-margin));
15
20
  margin: var(--sidebar-margin);
16
21
  border-radius: 6px;
17
22
  display: flex;
@@ -26,11 +31,12 @@ body {
26
31
  align-items: center;
27
32
  padding: 0 1rem;
28
33
  text-align: center;
29
- max-height: 100vh;
34
+ max-height: 100%;
30
35
  overflow-y: auto;
31
36
  }
32
37
 
33
38
  .sidebar_logo {
39
+ display: block;
34
40
  width: 72px;
35
41
  margin: 0 auto 1rem;
36
42
  }
@@ -38,9 +44,6 @@ body {
38
44
  .sidebar_title {
39
45
  font-size: 1.85em;
40
46
  line-height: 1.1;
41
- font-family:
42
- -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
43
- Arial, "Noto Sans", sans-serif;
44
47
  font-weight: 700;
45
48
  margin: 0;
46
49
  text-align: center;
package/package.json CHANGED
@@ -25,7 +25,7 @@
25
25
  "templates"
26
26
  ],
27
27
  "name": "extension-create",
28
- "version": "4.1.19",
28
+ "version": "4.1.21",
29
29
  "description": "The standalone extension creation engine for Extension.js",
30
30
  "author": {
31
31
  "name": "Cezar Augusto",
@@ -45,7 +45,7 @@
45
45
  "watch": "rslib build --watch",
46
46
  "compile": "rslib build",
47
47
  "format": "biome format --write .",
48
- "lint": "biome lint .",
48
+ "lint": "biome lint . && eslint .",
49
49
  "pretest:create": "pnpm compile",
50
50
  "pretest": "pnpm compile",
51
51
  "test": "vitest run",
@@ -6,22 +6,61 @@ const isFirefoxLike =
6
6
  import.meta.env.EXTENSION_PUBLIC_BROWSER === 'firefox' ||
7
7
  import.meta.env.EXTENSION_PUBLIC_BROWSER === 'gecko-based'
8
8
 
9
+ const isSafariLike =
10
+ import.meta.env.EXTENSION_PUBLIC_BROWSER === 'safari' ||
11
+ import.meta.env.EXTENSION_PUBLIC_BROWSER === 'webkit-based'
12
+
13
+ // Safari has no side panel surface, so the sidebar page opens in a tab.
14
+ let sidebarTabId
15
+
16
+ function openSidebarTab() {
17
+ const url = chrome.runtime.getURL('sidebar/index.html')
18
+
19
+ const openNewTab = () => {
20
+ chrome.tabs.create({url}, (tab) => {
21
+ sidebarTabId = tab?.id
22
+ })
23
+ }
24
+
25
+ // A repeat click focuses the tab already opened instead of a new copy.
26
+ const knownTabId = sidebarTabId
27
+
28
+ if (knownTabId === undefined) {
29
+ openNewTab()
30
+
31
+ return
32
+ }
33
+
34
+ chrome.tabs.update(knownTabId, {active: true}, () => {
35
+ if (chrome.runtime.lastError) openNewTab()
36
+ })
37
+ }
38
+
9
39
  if (isFirefoxLike) {
40
+ // Firefox refuses sidebarAction.open() outside a user input handler, and a
41
+ // message listener is not one, so the toolbar click is the only route.
10
42
  browser.browserAction.onClicked.addListener(() => {
11
43
  browser.sidebarAction.open()
12
44
  })
45
+ }
13
46
 
14
- browser.runtime.onMessage.addListener((message) => {
47
+ if (isSafariLike) {
48
+ // Safari never had setPanelBehavior, so the toolbar click needs a listener.
49
+ chrome.action?.onClicked.addListener(() => {
50
+ openSidebarTab()
51
+ })
52
+
53
+ chrome.runtime.onMessage.addListener((message) => {
15
54
  if (!message || message.type !== 'openSidebar') return
16
55
 
17
- browser.sidebarAction.open()
56
+ openSidebarTab()
18
57
  })
19
58
  }
20
59
 
21
- if (!isFirefoxLike) {
60
+ if (!isFirefoxLike && !isSafariLike) {
22
61
  // setPanelBehavior only affects FUTURE action clicks, registering it
23
62
  // inside onClicked would swallow the first toolbar click.
24
- chrome.sidePanel.setPanelBehavior({openPanelOnActionClick: true})
63
+ chrome.sidePanel?.setPanelBehavior({openPanelOnActionClick: true})
25
64
 
26
65
  // The side panel API only exists in Chromium. Firefox opens the sidebar in
27
66
  // the listener above, so this listener is compiled out of gecko builds.
@@ -32,13 +71,13 @@ if (!isFirefoxLike) {
32
71
  // allowed inside the user gesture that the content-script click carries, and
33
72
  // a tabs.query callback outlives it: the panel then silently refuses to open.
34
73
  // sender.tab is the tab the click came from, so no lookup is needed at all.
35
- chrome.sidePanel.setPanelBehavior({openPanelOnActionClick: true})
74
+ chrome.sidePanel?.setPanelBehavior({openPanelOnActionClick: true})
36
75
 
37
76
  const tabId = sender.tab?.id
38
- if (!chrome.sidePanel.open || tabId === undefined) return
77
+ if (!chrome.sidePanel?.open || tabId === undefined) return
39
78
 
40
79
  try {
41
- chrome.sidePanel.open({tabId})
80
+ chrome.sidePanel?.open({tabId})
42
81
  } catch (error) {
43
82
  console.error(error)
44
83
  }
@@ -1,27 +1,31 @@
1
1
  import logo from '../images/icon.png'
2
2
 
3
+ const isFirefoxLike =
4
+ import.meta.env.EXTENSION_PUBLIC_BROWSER === 'firefox' ||
5
+ import.meta.env.EXTENSION_PUBLIC_BROWSER === 'gecko-based'
6
+
3
7
  export default function createContentApp() {
4
8
  const container = document.createElement('div')
5
9
  container.className = 'content_script'
6
10
 
7
- const pill = document.createElement('button')
8
- pill.type = 'button'
9
- pill.className = 'content_pill'
10
- pill.setAttribute('aria-label', 'Open sidebar')
11
- pill.addEventListener('click', () => {
12
- try {
13
- if (
14
- import.meta.env.EXTENSION_PUBLIC_BROWSER === 'firefox' ||
15
- import.meta.env.EXTENSION_PUBLIC_BROWSER === 'gecko-based'
16
- ) {
17
- browser.runtime.sendMessage({type: 'openSidebar'})
18
- } else {
11
+ // Firefox cannot open a sidebar from a message listener, so the gecko build
12
+ // renders a hint naming the toolbar action instead of a dead control.
13
+ const pill = document.createElement(isFirefoxLike ? 'div' : 'button')
14
+ pill.className = isFirefoxLike
15
+ ? 'content_pill content_pill_static'
16
+ : 'content_pill'
17
+
18
+ if (!isFirefoxLike) {
19
+ pill.type = 'button'
20
+ pill.setAttribute('aria-label', 'Open sidebar')
21
+ pill.addEventListener('click', () => {
22
+ try {
19
23
  chrome.runtime.sendMessage({type: 'openSidebar'})
24
+ } catch (error) {
25
+ console.error(error)
20
26
  }
21
- } catch (error) {
22
- console.error(error)
23
- }
24
- })
27
+ })
28
+ }
25
29
 
26
30
  const img = document.createElement('img')
27
31
  img.className = 'content_pill_logo'
@@ -31,7 +35,9 @@ export default function createContentApp() {
31
35
 
32
36
  const text = document.createElement('span')
33
37
  text.className = 'content_pill_text'
34
- text.textContent = 'Open sidebar'
38
+ text.textContent = isFirefoxLike
39
+ ? 'Use the toolbar icon to open the sidebar'
40
+ : 'Open sidebar'
35
41
 
36
42
  pill.appendChild(img)
37
43
  pill.appendChild(text)
@@ -40,5 +40,6 @@ async function fetchCSS() {
40
40
  const cssUrl = new URL('./styles.css', import.meta.url)
41
41
  const response = await fetch(cssUrl)
42
42
  const text = await response.text()
43
+
43
44
  return response.ok ? text : Promise.reject(text)
44
45
  }
@@ -22,7 +22,7 @@
22
22
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
23
23
  }
24
24
 
25
- .content_pill:hover {
25
+ .content_pill:not(.content_pill_static):hover {
26
26
  background: #11151c;
27
27
  }
28
28
 
@@ -40,6 +40,12 @@
40
40
  font-weight: 600;
41
41
  line-height: 1;
42
42
  font-family:
43
- -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
44
- Arial, "Noto Sans", sans-serif;
43
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
44
+ Arial, 'Noto Sans', sans-serif;
45
+ }
46
+
47
+ /* The gecko build renders this hint in place of the pill, so it must not read
48
+ as a control: no pointer cursor and no hover response. */
49
+ .content_pill_static {
50
+ cursor: default;
45
51
  }
@@ -46,7 +46,8 @@
46
46
  },
47
47
  "firefox:sidebar_action": {
48
48
  "default_panel": "sidebar/index.html",
49
- "default_title": "Side Panel Content"
49
+ "default_title": "Side Panel Content",
50
+ "browser_style": false
50
51
  },
51
52
  "chromium:permissions": ["sidePanel"],
52
53
  "background": {
@@ -11,7 +11,12 @@
11
11
  body {
12
12
  background-color: var(--sidebar-bg);
13
13
  color: var(--sidebar-text);
14
- height: 100vh;
14
+ font-family:
15
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
16
+ Arial, 'Noto Sans', sans-serif;
17
+ /* The margin insets the panel, so a full-viewport height would push the
18
+ content below centre and overflow the panel by twice the margin. */
19
+ height: calc(100vh - 2 * var(--sidebar-margin));
15
20
  margin: var(--sidebar-margin);
16
21
  border-radius: 6px;
17
22
  display: flex;
@@ -26,11 +31,12 @@ body {
26
31
  align-items: center;
27
32
  padding: 0 1rem;
28
33
  text-align: center;
29
- max-height: 100vh;
34
+ max-height: 100%;
30
35
  overflow-y: auto;
31
36
  }
32
37
 
33
38
  .sidebar_logo {
39
+ display: block;
34
40
  width: 72px;
35
41
  margin: 0 auto 1rem;
36
42
  }
@@ -38,9 +44,6 @@ body {
38
44
  .sidebar_title {
39
45
  font-size: 1.85em;
40
46
  line-height: 1.1;
41
- font-family:
42
- -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
43
- Arial, "Noto Sans", sans-serif;
44
47
  font-weight: 700;
45
48
  margin: 0;
46
49
  text-align: center;