pinokiod 8.0.78 → 8.0.80

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.
@@ -6,7 +6,8 @@
6
6
  href: <link location to open>,
7
7
  target: <target for window.open()>,
8
8
  features: <windowFeatures>,
9
- audio: <play audio if true>,
9
+ type: <notification type>,
10
+ silent: <disable the notification sound if true>,
10
11
  }
11
12
  }
12
13
  (*/
@@ -1,7 +1,6 @@
1
1
  const fs = require("fs")
2
2
  const path = require("path")
3
3
  const {
4
- SIZE_THRESHOLD,
5
4
  isCandidateFileSize,
6
5
  SHA256_RE,
7
6
  ENTRY_BATCH_SIZE
@@ -276,7 +275,7 @@ class AutomaticScans {
276
275
  if (!collected.size) this.discardChangedPaths(app)
277
276
  }
278
277
 
279
- async candidatePolicy() {
278
+ async candidatePolicy(app = null) {
280
279
  const cached = this.vault.lastScanCache &&
281
280
  typeof this.vault.lastScanCache.get === "function"
282
281
  ? this.vault.lastScanCache.get("")
@@ -284,13 +283,13 @@ class AutomaticScans {
284
283
  const scan = cached || (this.vault.registry
285
284
  ? await this.vault.registry.scanFor()
286
285
  : null)
287
- const threshold = scan ? Number(scan.candidate_min_bytes) : NaN
286
+ const threshold = app
287
+ ? await this.vault.candidateSizeForApp(app)
288
+ : await this.vault.candidateSizeSetting(null)
288
289
  const hasLinkableDevices = !!(
289
290
  scan && Array.isArray(scan.linkable_devices))
290
291
  return {
291
- threshold: Number.isFinite(threshold) && threshold >= 0
292
- ? threshold
293
- : SIZE_THRESHOLD,
292
+ threshold,
294
293
  linkability_known: hasLinkableDevices,
295
294
  linkable_devices: new Set(hasLinkableDevices
296
295
  ? scan.linkable_devices
@@ -1039,7 +1038,7 @@ class AutomaticScans {
1039
1038
  unstable_hashes: 0
1040
1039
  }
1041
1040
  markStage("policy-read")
1042
- const policy = await this.candidatePolicy()
1041
+ const policy = await this.candidatePolicy(app)
1043
1042
  const threshold = await this.candidateThreshold(policy)
1044
1043
  const linkableDevices = policy.linkable_devices
1045
1044
  const paths = [...new Set(active.paths || [])].filter((filePath) =>
@@ -1899,6 +1899,25 @@ class Vault {
1899
1899
  }
1900
1900
  return this.runMutation(() =>
1901
1901
  this.removeExternalSource(payload.source_id))
1902
+ case "set_candidate_size": {
1903
+ if (payload.scope_id != null &&
1904
+ typeof payload.scope_id !== "string") {
1905
+ return { error: "Choose a valid scan scope." }
1906
+ }
1907
+ const scopeId = typeof payload.scope_id === "string" && payload.scope_id
1908
+ ? payload.scope_id
1909
+ : null
1910
+ if (scopeId) {
1911
+ const source = this.scanSource(scopeId)
1912
+ if (!source || source.kind !== "app") {
1913
+ return { error: "That app is no longer available." }
1914
+ }
1915
+ }
1916
+ return this.setCandidateSizeSetting(
1917
+ scopeId,
1918
+ payload.candidate_size
1919
+ )
1920
+ }
1902
1921
  case "scan": {
1903
1922
  const scanSource = payload.scope_id
1904
1923
  ? this.scanSource(payload.scope_id)
@@ -1913,22 +1932,21 @@ class Vault {
1913
1932
  code: "global_scan_required"
1914
1933
  }
1915
1934
  }
1916
- let threshold = this.sizeThreshold
1917
- if (payload.candidate_size != null) {
1918
- if (!CANDIDATE_SIZE_OPTIONS.includes(payload.candidate_size)) {
1919
- return { error: "Choose a valid minimum file size." }
1920
- }
1921
- threshold = payload.candidate_size
1922
- }
1935
+ const settingScopeId = scanSource && scanSource.kind === "app"
1936
+ ? scanSource.id
1937
+ : null
1938
+ const threshold = await this.candidateSizeSetting(settingScopeId)
1923
1939
  return this.startScan(payload.scope_id || null, threshold)
1924
1940
  }
1925
1941
  case "cancel_scan":
1926
1942
  return this.cancelScan()
1927
- case "find_folders":
1943
+ case "find_folders": {
1944
+ const threshold = await this.candidateSizeSetting(null)
1928
1945
  return this.startFolderDiscovery(
1929
1946
  payload.path,
1930
- payload.candidate_size
1947
+ threshold
1931
1948
  )
1949
+ }
1932
1950
  case "cancel_find_folders":
1933
1951
  return this.cancelFolderDiscovery()
1934
1952
  case "clear_find_folders":
@@ -3115,6 +3133,33 @@ class Vault {
3115
3133
  return scoped
3116
3134
  }
3117
3135
 
3136
+ async candidateSizeSetting(scopeId = null) {
3137
+ const setting = await this.registry.scanSetting(scopeId)
3138
+ const minimum = setting && Number(setting.candidate_min_bytes)
3139
+ if (CANDIDATE_SIZE_OPTIONS.includes(minimum)) return minimum
3140
+ return SIZE_THRESHOLD
3141
+ }
3142
+
3143
+ async setCandidateSizeSetting(scopeId = null, candidateMinBytes) {
3144
+ if (!Number.isSafeInteger(candidateMinBytes) ||
3145
+ !CANDIDATE_SIZE_OPTIONS.includes(candidateMinBytes)) {
3146
+ return { error: "Choose a valid minimum file size." }
3147
+ }
3148
+ const setting = await this.registry.setScanSetting(
3149
+ scopeId,
3150
+ candidateMinBytes
3151
+ )
3152
+ return {
3153
+ scope_id: scopeId || null,
3154
+ candidate_min_bytes: setting.candidate_min_bytes,
3155
+ updated_at: setting.updated_at
3156
+ }
3157
+ }
3158
+
3159
+ candidateSizeForApp(app) {
3160
+ return this.candidateSizeSetting(sourceId("app", app))
3161
+ }
3162
+
3118
3163
  sourceCountMaps(summaryRows) {
3119
3164
  const counts = {
3120
3165
  all: {},
@@ -3391,13 +3436,10 @@ class Vault {
3391
3436
  const lastScan = await this.scanForScope(scopeId)
3392
3437
  const globalScan = scopeId ? await this.scanForScope(null) : lastScan
3393
3438
  const globalScanReady = this.globalScanIsReady(globalScan)
3394
- const publishedCandidateMinimum = globalScan
3395
- ? Number(globalScan.candidate_min_bytes)
3396
- : NaN
3397
- const globalCandidateMinimum = Number.isFinite(
3398
- publishedCandidateMinimum) && publishedCandidateMinimum >= 0
3399
- ? publishedCandidateMinimum
3400
- : SIZE_THRESHOLD
3439
+ const candidateMinimum = await this.candidateSizeSetting(scopeId)
3440
+ const globalCandidateMinimum = scopeId
3441
+ ? await this.candidateSizeSetting(null)
3442
+ : candidateMinimum
3401
3443
  const before = lastScan && Number.isFinite(lastScan.bytes_total)
3402
3444
  ? lastScan.bytes_total
3403
3445
  : 0
@@ -3435,6 +3477,7 @@ class Vault {
3435
3477
  const result = {
3436
3478
  enabled: true,
3437
3479
  global_scan_ready: globalScanReady,
3480
+ candidate_min_bytes: candidateMinimum,
3438
3481
  global_candidate_min_bytes: globalCandidateMinimum,
3439
3482
  mode: this.mode,
3440
3483
  scan: this.scanStatus(),
@@ -172,6 +172,8 @@ for (const method of [
172
172
  "removeAutomaticAppScanApp",
173
173
  "setAutomaticAppScanState",
174
174
  "scanFor",
175
+ "scanSetting",
176
+ "setScanSetting",
175
177
  "removeScan",
176
178
  "beginScan",
177
179
  "abortScan",
@@ -306,6 +306,13 @@ class RegistryCore {
306
306
  FROM automatic_app_scans
307
307
  WHERE state = 'paused';
308
308
 
309
+ CREATE TABLE IF NOT EXISTS minimum_size_settings (
310
+ scope_id TEXT PRIMARY KEY,
311
+ candidate_min_bytes INTEGER NOT NULL CHECK (
312
+ candidate_min_bytes >= 0
313
+ ),
314
+ updated_at INTEGER NOT NULL
315
+ );
309
316
  `)
310
317
  const automaticScanColumns = new Set(this.database.prepare(
311
318
  "PRAGMA table_info(automatic_app_scans)"
@@ -2670,6 +2677,38 @@ class RegistryCore {
2670
2677
  })
2671
2678
  }
2672
2679
 
2680
+ scanSetting(scopeId = null) {
2681
+ const row = this.database.prepare(`
2682
+ SELECT candidate_min_bytes, updated_at
2683
+ FROM minimum_size_settings
2684
+ WHERE scope_id = ?
2685
+ `).get(scopeId || "")
2686
+ return row ? {
2687
+ candidate_min_bytes: Number(row.candidate_min_bytes),
2688
+ updated_at: Number(row.updated_at) || 0
2689
+ } : null
2690
+ }
2691
+
2692
+ setScanSetting(scopeId = null, candidateMinBytes) {
2693
+ const minimum = candidateMinBytes
2694
+ if (!Number.isSafeInteger(minimum) || minimum < 0) {
2695
+ throw new Error("Invalid minimum file size setting.")
2696
+ }
2697
+ const updatedAt = Date.now()
2698
+ this.database.prepare(`
2699
+ INSERT INTO minimum_size_settings(
2700
+ scope_id, candidate_min_bytes, updated_at
2701
+ ) VALUES (?, ?, ?)
2702
+ ON CONFLICT(scope_id) DO UPDATE SET
2703
+ candidate_min_bytes = excluded.candidate_min_bytes,
2704
+ updated_at = excluded.updated_at
2705
+ `).run(scopeId || "", minimum, updatedAt)
2706
+ return {
2707
+ candidate_min_bytes: minimum,
2708
+ updated_at: updatedAt
2709
+ }
2710
+ }
2711
+
2673
2712
  automaticAppScanStates() {
2674
2713
  return this.database.prepare(`
2675
2714
  SELECT app, state, signature, updated_at
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.0.78",
3
+ "version": "8.0.80",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -1870,8 +1870,10 @@ class Server {
1870
1870
  }
1871
1871
  return ""
1872
1872
  }
1873
+ const launcherPath = this.kernel.path("api", name)
1874
+ const launcherDisplayPath = this.formatLogsDisplayPath(launcherPath)
1873
1875
  const launcherRemote = this.kernel.api && typeof this.kernel.api.parentGitURI === "function"
1874
- ? (this.kernel.api.parentGitURI(this.kernel.path("api", name)) || "")
1876
+ ? (this.kernel.api.parentGitURI(launcherPath) || "")
1875
1877
  : ""
1876
1878
  const launcherRemoteUrl = buildGitRemoteWebUrl(launcherRemote)
1877
1879
 
@@ -1926,7 +1928,8 @@ class Server {
1926
1928
  dev_link,
1927
1929
  // repos,
1928
1930
  current_urls,
1929
- path: this.kernel.path("api", name),
1931
+ path: launcherPath,
1932
+ launcher_path_display: launcherDisplayPath,
1930
1933
  log_path: this.kernel.path("api", name, "logs"),
1931
1934
  plugin_menu: null,
1932
1935
  portal: this.portal,
@@ -276,17 +276,24 @@
276
276
  return;
277
277
  }
278
278
  const rect = anchor.getBoundingClientRect();
279
- const scrollX = window.scrollX || window.pageXOffset || 0;
280
- const scrollY = window.scrollY || window.pageYOffset || 0;
281
- const top = rect.bottom + scrollY + 6;
282
- let left = rect.left + scrollX;
279
+ const viewportPadding = 12;
280
+ const menuGap = 6;
283
281
  const menuWidth = menu.offsetWidth || 0;
284
- const viewportRight = scrollX + window.innerWidth;
285
- if (left + menuWidth > viewportRight - 12) {
286
- left = Math.max(scrollX + 12, viewportRight - menuWidth - 12);
287
- }
288
- if (left < scrollX + 12) {
289
- left = scrollX + 12;
282
+ const menuHeight = menu.offsetHeight || 0;
283
+ let left = rect.left;
284
+ let top = rect.bottom + menuGap;
285
+ if (left + menuWidth > window.innerWidth - viewportPadding) {
286
+ left = Math.max(viewportPadding, window.innerWidth - menuWidth - viewportPadding);
287
+ }
288
+ if (left < viewportPadding) {
289
+ left = viewportPadding;
290
+ }
291
+ const topAbove = rect.top - menuHeight - menuGap;
292
+ if (top + menuHeight > window.innerHeight - viewportPadding && topAbove >= viewportPadding) {
293
+ top = topAbove;
294
+ } else {
295
+ top = Math.min(top, Math.max(viewportPadding, window.innerHeight - menuHeight - viewportPadding));
296
+ top = Math.max(viewportPadding, top);
290
297
  }
291
298
  menu.style.top = `${Math.round(top)}px`;
292
299
  menu.style.left = `${Math.round(left)}px`;
@@ -857,10 +864,13 @@
857
864
  color: var(--pinokio-focus-color, #4c9afe);
858
865
  }
859
866
  .pinokio-notify-popover {
860
- position: absolute;
867
+ position: fixed;
861
868
  z-index: 2147482000;
862
869
  min-width: 220px;
863
870
  max-width: 280px;
871
+ max-height: calc(100dvh - 24px);
872
+ overflow-y: auto;
873
+ overscroll-behavior: contain;
864
874
  color: #f8fafc;
865
875
  background: rgba(15, 23, 42, 0.97);
866
876
  border-radius: 10px;
@@ -980,6 +990,133 @@ const syncToggleAppearance = (toggle, enabled) => {
980
990
  });
981
991
  };
982
992
 
993
+ const updateNotificationPreferenceForFrame = (frameName, enabled) => {
994
+ const state = getOrCreateState(frameName);
995
+ if (!state) {
996
+ return null;
997
+ }
998
+ const next = Boolean(enabled);
999
+ state.notifyEnabled = next;
1000
+ setPreference(frameName, next);
1001
+ syncInlineToggleStateForFrame(frameName);
1002
+ return next;
1003
+ };
1004
+
1005
+ const populateInlineSoundSelect = (select, options) => {
1006
+ if (!(select instanceof HTMLSelectElement)) {
1007
+ return;
1008
+ }
1009
+ const effectiveOptions = Array.isArray(options) && options.length > 0
1010
+ ? options
1011
+ : baseSoundOptions();
1012
+ const selectedChoice = globalSoundPreference?.choice || SOUND_DEFAULT_CHOICE;
1013
+ select.replaceChildren();
1014
+ effectiveOptions.forEach((option) => {
1015
+ if (!option || !option.value || !option.label) {
1016
+ return;
1017
+ }
1018
+ const node = document.createElement('option');
1019
+ node.value = option.value;
1020
+ node.textContent = option.label;
1021
+ node.selected = option.value === selectedChoice;
1022
+ select.appendChild(node);
1023
+ });
1024
+ if (Array.from(select.options).some((option) => option.value === selectedChoice)) {
1025
+ select.value = selectedChoice;
1026
+ } else {
1027
+ select.value = SOUND_DEFAULT_CHOICE;
1028
+ }
1029
+ };
1030
+
1031
+ const mountNotificationSettingsForLink = (link, container, callbacks = {}) => {
1032
+ if (!(container instanceof HTMLElement)) {
1033
+ return false;
1034
+ }
1035
+ const context = getNotificationMenuStateForLink(link);
1036
+ if (!context.available || !context.frameName) {
1037
+ return false;
1038
+ }
1039
+ const state = getOrCreateState(context.frameName);
1040
+ if (!state) {
1041
+ return false;
1042
+ }
1043
+
1044
+ container.replaceChildren();
1045
+
1046
+ const toggleRow = document.createElement('div');
1047
+ toggleRow.className = 'tab-link-notification-setting-row';
1048
+ const toggleLabel = document.createElement('span');
1049
+ toggleLabel.className = 'tab-link-notification-setting-label';
1050
+ toggleLabel.textContent = 'Notifications for this tab';
1051
+ const toggleControl = document.createElement('div');
1052
+ toggleControl.className = 'tab-link-notification-toggle-control';
1053
+ const toggleStatus = document.createElement('span');
1054
+ toggleStatus.className = 'tab-link-notification-toggle-status';
1055
+ const toggle = document.createElement('button');
1056
+ toggle.type = 'button';
1057
+ toggle.className = 'tab-link-notification-switch';
1058
+ toggle.setAttribute('role', 'switch');
1059
+ toggle.setAttribute('aria-label', 'Notifications for this tab');
1060
+ const toggleThumb = document.createElement('span');
1061
+ toggleThumb.className = 'tab-link-notification-switch-thumb';
1062
+ toggle.setAttribute('aria-describedby', `${container.id || 'tab-link-notification-settings'}-status`);
1063
+ toggleStatus.id = `${container.id || 'tab-link-notification-settings'}-status`;
1064
+ toggle.appendChild(toggleThumb);
1065
+ toggleControl.append(toggleStatus, toggle);
1066
+ toggleRow.append(toggleLabel, toggleControl);
1067
+
1068
+ const soundRow = document.createElement('label');
1069
+ soundRow.className = 'tab-link-notification-setting-row';
1070
+ const soundLabel = document.createElement('span');
1071
+ soundLabel.className = 'tab-link-notification-setting-label';
1072
+ soundLabel.textContent = 'Sound';
1073
+ const soundSelect = document.createElement('select');
1074
+ soundSelect.className = 'tab-link-notification-sound-select';
1075
+ soundSelect.setAttribute('aria-label', 'Notification sound');
1076
+ soundRow.append(soundLabel, soundSelect);
1077
+
1078
+ const syncToggle = (enabled) => {
1079
+ toggle.setAttribute('aria-checked', enabled ? 'true' : 'false');
1080
+ toggle.dataset.enabled = enabled ? 'true' : 'false';
1081
+ toggleStatus.textContent = enabled ? 'On' : 'Off';
1082
+ };
1083
+
1084
+ syncToggle(Boolean(state.notifyEnabled));
1085
+ populateInlineSoundSelect(soundSelect, soundOptionsCache || baseSoundOptions());
1086
+ container.append(toggleRow, soundRow);
1087
+
1088
+ toggle.addEventListener('click', () => {
1089
+ const current = getOrCreateState(context.frameName);
1090
+ if (!current) {
1091
+ return;
1092
+ }
1093
+ const enabled = updateNotificationPreferenceForFrame(context.frameName, !current.notifyEnabled);
1094
+ if (enabled === null) {
1095
+ return;
1096
+ }
1097
+ syncToggle(enabled);
1098
+ if (typeof callbacks.onEnabledChange === 'function') {
1099
+ callbacks.onEnabledChange(enabled);
1100
+ }
1101
+ });
1102
+
1103
+ soundSelect.addEventListener('change', () => {
1104
+ applySoundSelection(soundSelect.value);
1105
+ if (typeof callbacks.onSoundChange === 'function') {
1106
+ callbacks.onSoundChange(globalSoundPreference.choice);
1107
+ }
1108
+ });
1109
+
1110
+ loadSoundOptions().then((options) => {
1111
+ if (!container.isConnected || !container.contains(soundSelect)) {
1112
+ return;
1113
+ }
1114
+ populateInlineSoundSelect(soundSelect, options);
1115
+ }).catch(() => {});
1116
+
1117
+ return true;
1118
+ };
1119
+
983
1120
  const getNotificationMenuStateForLink = (link) => {
984
1121
  if (!(link instanceof HTMLElement)) {
985
1122
  return { available: false, enabled: true, frameName: null };
@@ -1498,5 +1635,8 @@ const ensureTabAccessories = aggregateDebounce(() => {
1498
1635
  openMenuForLink(link, anchor) {
1499
1636
  return openNotificationMenuForLink(link, anchor);
1500
1637
  },
1638
+ mountSettingsForLink(link, container, callbacks) {
1639
+ return mountNotificationSettingsForLink(link, container, callbacks);
1640
+ },
1501
1641
  };
1502
1642
  })();
@@ -220,6 +220,113 @@ body.dark .tab-link-popover .tab-link-popover-item:hover,
220
220
  body.dark .tab-link-popover .tab-link-popover-item:focus-visible {
221
221
  background: rgba(148, 163, 184, 0.12);
222
222
  }
223
+ .tab-link-popover .tab-link-popover-item[aria-expanded="true"] {
224
+ background: rgba(15, 23, 42, 0.04);
225
+ }
226
+ body.dark .tab-link-popover .tab-link-popover-item[aria-expanded="true"] {
227
+ background: rgba(148, 163, 184, 0.08);
228
+ }
229
+ .tab-link-notification-settings {
230
+ margin: 0 14px 10px 40px;
231
+ border: 1px solid rgba(15, 23, 42, 0.1);
232
+ border-radius: 7px;
233
+ background: rgba(15, 23, 42, 0.025);
234
+ overflow: hidden;
235
+ }
236
+ body.dark .tab-link-notification-settings {
237
+ border-color: rgba(148, 163, 184, 0.18);
238
+ background: rgba(148, 163, 184, 0.045);
239
+ }
240
+ .tab-link-notification-setting-row {
241
+ min-height: 44px;
242
+ padding: 7px 10px;
243
+ display: grid;
244
+ grid-template-columns: minmax(0, 1fr) auto;
245
+ align-items: center;
246
+ gap: 12px;
247
+ box-sizing: border-box;
248
+ color: inherit;
249
+ }
250
+ .tab-link-notification-setting-row + .tab-link-notification-setting-row {
251
+ border-top: 1px solid rgba(15, 23, 42, 0.08);
252
+ }
253
+ body.dark .tab-link-notification-setting-row + .tab-link-notification-setting-row {
254
+ border-top-color: rgba(148, 163, 184, 0.14);
255
+ }
256
+ .tab-link-notification-setting-label {
257
+ min-width: 0;
258
+ font-size: 12px;
259
+ font-weight: 500;
260
+ color: rgba(15, 23, 42, 0.76);
261
+ }
262
+ body.dark .tab-link-notification-setting-label {
263
+ color: rgba(226, 232, 240, 0.86);
264
+ }
265
+ .tab-link-notification-toggle-control {
266
+ display: inline-flex;
267
+ align-items: center;
268
+ gap: 7px;
269
+ }
270
+ .tab-link-notification-toggle-status {
271
+ min-width: 18px;
272
+ font-size: 11px;
273
+ text-align: right;
274
+ color: rgba(15, 23, 42, 0.56);
275
+ }
276
+ body.dark .tab-link-notification-toggle-status {
277
+ color: rgba(226, 232, 240, 0.62);
278
+ }
279
+ .tab-link-notification-switch {
280
+ position: relative;
281
+ width: 36px;
282
+ height: 26px;
283
+ padding: 4px;
284
+ appearance: none;
285
+ border: 0;
286
+ border-radius: 999px;
287
+ background: rgba(100, 116, 139, 0.42);
288
+ cursor: pointer;
289
+ transition: background-color 140ms ease;
290
+ }
291
+ .tab-link-notification-switch[data-enabled="true"] {
292
+ background: var(--pinokio-focus-color, #2563eb);
293
+ }
294
+ .tab-link-notification-switch-thumb {
295
+ display: block;
296
+ width: 18px;
297
+ height: 18px;
298
+ border-radius: 50%;
299
+ background: #f8fafc;
300
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.24);
301
+ transform: translateX(0);
302
+ transition: transform 140ms ease;
303
+ }
304
+ .tab-link-notification-switch[data-enabled="true"] .tab-link-notification-switch-thumb {
305
+ transform: translateX(10px);
306
+ }
307
+ .tab-link-notification-switch:focus-visible,
308
+ .tab-link-notification-sound-select:focus-visible {
309
+ outline: 2px solid var(--pinokio-focus-color, #2563eb);
310
+ outline-offset: 2px;
311
+ }
312
+ .tab-link-notification-sound-select {
313
+ width: min(160px, 44vw);
314
+ min-height: 32px;
315
+ padding: 5px 28px 5px 9px;
316
+ border: 1px solid rgba(15, 23, 42, 0.14);
317
+ border-radius: 6px;
318
+ background: var(--pinokio-sidebar-tabbar-bg, #ffffff);
319
+ color: rgba(15, 23, 42, 0.82);
320
+ font: inherit;
321
+ font-size: 12px;
322
+ cursor: pointer;
323
+ }
324
+ body.dark .tab-link-notification-sound-select {
325
+ border-color: rgba(148, 163, 184, 0.22);
326
+ background: rgba(15, 23, 42, 0.42);
327
+ color: rgba(241, 245, 249, 0.9);
328
+ color-scheme: dark;
329
+ }
223
330
  .tab-link-popover .tab-link-popover-item .label {
224
331
  font-size: 11px;
225
332
  font-weight: 600;
@@ -344,6 +451,27 @@ body.dark .tab-link-popover .tab-link-popover-footer:focus-visible {
344
451
  padding-right: 16px;
345
452
  padding-left: 16px;
346
453
  }
454
+ .tab-link-notification-settings {
455
+ margin-right: 16px;
456
+ margin-left: 42px;
457
+ }
458
+ .tab-link-notification-setting-row {
459
+ padding-right: 8px;
460
+ padding-left: 8px;
461
+ }
462
+ }
463
+ @media (pointer: coarse) {
464
+ .tab-link-notification-switch {
465
+ width: 44px;
466
+ height: 30px;
467
+ padding: 4px;
468
+ }
469
+ .tab-link-notification-switch[data-enabled="true"] .tab-link-notification-switch-thumb {
470
+ transform: translateX(14px);
471
+ }
472
+ .tab-link-notification-sound-select {
473
+ min-height: 44px;
474
+ }
347
475
  }
348
476
  .appcanvas > aside .menu-container .tab-link-popover-host {
349
477
  display: flex;
@@ -266,10 +266,39 @@
266
266
  if (action === "notifications") {
267
267
  const activeLink = tabLinkActiveLink
268
268
  const notifier = typeof window !== "undefined" ? window.PinokioIdleNotifier : null
269
- const anchor = activeLink ? (activeLink.querySelector(`.${TAB_LINK_TRIGGER_CLASS}`) || activeLink) : null
270
- hideTabLinkPopover({ immediate: true })
271
- if (notifier && typeof notifier.openMenuForLink === "function") {
272
- notifier.openMenuForLink(activeLink, anchor)
269
+ const existingPanel = item.nextElementSibling
270
+ if (existingPanel && existingPanel.classList.contains("tab-link-notification-settings")) {
271
+ existingPanel.remove()
272
+ item.setAttribute("aria-expanded", "false")
273
+ item.removeAttribute("aria-controls")
274
+ return
275
+ }
276
+ if (notifier && typeof notifier.mountSettingsForLink === "function") {
277
+ const panel = document.createElement("div")
278
+ panel.className = "tab-link-notification-settings"
279
+ panel.id = `${TAB_LINK_POPOVER_ID}-notification-settings`
280
+ panel.setAttribute("role", "group")
281
+ panel.setAttribute("aria-label", "Desktop notification settings")
282
+ item.insertAdjacentElement("afterend", panel)
283
+ const value = item.querySelector(".value")
284
+ const icon = item.querySelector(".tab-link-popover-action-icon i")
285
+ const mounted = notifier.mountSettingsForLink(activeLink, panel, {
286
+ onEnabledChange(enabled) {
287
+ if (value) {
288
+ value.textContent = enabled ? "Enabled for this tab" : "Disabled for this tab"
289
+ }
290
+ if (icon) {
291
+ icon.classList.toggle("fa-bell", enabled)
292
+ icon.classList.toggle("fa-bell-slash", !enabled)
293
+ }
294
+ }
295
+ })
296
+ if (mounted) {
297
+ item.setAttribute("aria-controls", panel.id)
298
+ item.setAttribute("aria-expanded", "true")
299
+ } else {
300
+ panel.remove()
301
+ }
273
302
  }
274
303
  return
275
304
  }
@@ -1898,6 +1927,9 @@
1898
1927
  if (actionItem.action) {
1899
1928
  item.setAttribute("data-action", actionItem.action)
1900
1929
  }
1930
+ if (actionItem.action === "notifications") {
1931
+ item.setAttribute("aria-expanded", "false")
1932
+ }
1901
1933
  if (actionItem.url) {
1902
1934
  item.setAttribute("data-url", actionItem.url)
1903
1935
  }