v0.2.6
Pre-release
Pre-release
·
92 commits
to main
since this release
Axis Browser v0.2.6
Branding — macOS
Axis_logo.png(project root) is the single source for the app image: Dock (app.dock.setIcon), allBrowserWindowiconpaths (main, secondary, incognito), and About Axis (dialog.showMessageBoxwith the samenativeImage— the systemrole: 'about'panel cannot show a custom image in dev because it uses the app bundle icon). Previous code pointed at a non-existentsrc/Axis_logo.png.- Dock squircle:
app.dock.setIcon()does not apply the same mask as a bundled.icns, so the logo is preprocessed with Jimp 1.x:contain512×512 → superellipse n=4 alpha (full squircle silhouette), then resize that whole squircle to ~78% and center on a transparent 512×512 tile (safe-zone margin like system icons). Insetting the logo before masking produced a square blob. Disk cache underuserData/dock-squircle-cache-v3.pngskips Jimp whenAxis_logo.pngis unchanged. Dock icon applies aftercreateWindow()so startup is not blocked.prestart/predevremoved — patch script runspostinstallonly (with a fast no-op when already patched). - About copy: Copyright © 2026 Abdelrahman Berchan.
- electron-builder
build.icon/mac.iconplusAxis_logo.pnginbuild.filesso packaged.appbundles get the correct icon in Finder and the Dock. - Display name “Axis”: root
productNameinpackage.jsonsoapp.getName()(About dialog, app menu) is Axis instead of lowercaseaxis.app.setName('Axis')remains at startup. macOS dev Dock (bottom bar):scripts/patch-electron-mac-display-name.jsruns onpostinstall: renamesnode_modules/electron/dist/Electron.app→Axis.app, patchesInfo.plist(CFBundleDisplayName/CFBundleName), and rewritesnode_modules/electron/path.txtsonpx electron .launchesAxis.app.path.txtmust not end with a newline (Electron’s loader does not trim), orspawnfails with ENOENT. If the tooltip still shows “Electron” once, quit the app and runkillall Dock(or log out) so Launch Services refreshes.
Settings window
- White flash on open/close:
backgroundColorfromnativeTheme.shouldUseDarkColors(light #f5f5f7 / dark #1e1e1e) so the shell matchessettings.htmlin light and dark (previously macOS-only light gray);htmlbackground insettings.htmlmatchesbodybefore first paint;nativeTheme.on('updated')callssetBackgroundColorwhile the window is open. (paintWhenInitiallyHidden: falseis not used — it preventsready-to-showfrom firing in Electron.)
Security — dependencies
file-type(CVE-2026-31808 / GHSA-5v7r-6r5c-r473 — ASF / WMV-WMA detection infinite loop on malformed input):jimpis upgraded from 0.22.x to ^1.6.1 (usesfile-type^21).package.jsonoverridessetfile-type>= 21.3.1 so Dependabot’s vulnerable 16.x transitive range is not resolved.
Development — console noise
- Quit / stderr: Native NSLog (e.g.
representedObject is not a WeakPtrToElectronMenuModelAsNSObject,sysctlbyname…kern.hv_vmm_present) bypasses Node’sprocess.stderr.writehook.npm start/npm run devnow usescripts/run-electron.js, whichspawns Electron with stderr piped and drops lines matchingscripts/suppress-stderr-patterns.js.src/main.jsloads the same list for JS-sidestderr/stdout.writefiltering.
Startup / first interaction
- Renderer
init:loadSettingsandrefreshShortcutCacherun in parallel (Promise.all) so the shell is ready sooner. - Page context menu:
webview.executeJavaScriptforspeechSynthesis.speakingruns only when the menu will show Speech (selection + speech on). When needed, it is **Promise.race**d with ~90ms so a cold guest page cannot block the menu for seconds. - Main
app.whenReady:warmUpMainProcessNativePaths()—clipboard.readText(),getShortcuts(), throwawayMenu.buildFromTemplate— reduces first native context menu cold stall.
Tab groups — open / close animation
- Open: Height is measured with
.tab-group-content.openapplied so padding and layout match the expanded state (no ad-hoc +48px buffer).transitionendonmax-height(plus a short timeout fallback) clears thetogglingclass and resetsmax-heighttononeafter the animation. - Close:
max-height→ 0 no longer fights.openpadding underborder-box(which caused stutter). Inlinepaddingis pinned fromgetComputedStyle, thenmax-heightandpaddinganimate together to0/0 6px;.openis removed only infinishCloseafter the transition so there is no one-frame padding snap. - Timing: ~200ms in
src/renderer.jsandsrc/styles.csswith matching easing; doublerequestAnimationFramewhere needed so the “from” state is committed before the transition runs. - Empty group: When the last tab is moved out of a group that was expanded, the group auto-collapses with the same
runTabGroupCollapseAnimationas a manual close (unlesssettings.autoCollapseEmptyTabGroupisfalse). - Context menu target: Right-click on a tab inside a group no longer opens the tab group menu; the group’s capture-phase
contextmenuhandler skips when the hit target is a.tabinside.tab-group-content, so the tab menu runs as usual. - Move between groups: Tabs removed from a group’s DOM with
remove()were no longer found byquerySelector, sogetOrCreateTabElementbuilt duplicate tab rows (broken webview / selection). Tabs are now reparented into a hidden#tab-element-pooluntil re-mounted.addTabToTabGroupresolves string/number Map keys viafindTabGroupKey, removes the tab from all other groups, sets destinationopen: true, and applies the same empty-group collapse animation for the source group when it becomes empty. - In-group reorder drag: Smooth vertical drag for tabs inside
.tab-group-contentclampstranslateYand drop-slot math to the group content box (setupTabDragDrop/draggedPosCenter) and to the top of the tab list scroll port (max(group.top, #tabs-container top)) so rows cannot be dragged above the visible sidebar tab area.
Sidebar — smooth tab drag (setupTabDragDrop)
- Pinned/unpinned separator (live): When the row directly below
#tabs-separatoris the item being dragged toward pinned tabs, the separator’stranslateYnow follows the last pinned row’s slide (sepShiftForIndex,firstUnpinnedIndex - 1) during the drag. Previously that case was skipped (j === currentDragIdx), so the line only moved after mouseup. - Pinned band (separator + New Tab + incognito): The same smoothed shift is applied to
#sidebar-new-tab-btnand#incognito-indicatorviasetPinnedUnpinnedBandShift/clearPinnedUnpinnedBandTransforms, so the + New Tab row stays aligned with the separator during drag (not only on drop). - Top of tab list (main sidebar): For
#tabs-containerdrags (tabs and tab groups),draggedCenteris clamped so the row center cannot move above#tabs-container’s viewport top (getBoundingClientRect().top) plus half row height — bottom is unconstrained (only the top edge is walled), matching the in-group idea but only for the upper bound.
Native context menus — behavior (src/renderer.js + src/main.js)
- Search selection: “Search for …” opens a tab with
getSearchUrl(trimmedSelection)(native IPC path and legacy DOM handler); previously it calledcreateNewTab()with no URL. - Webview edit actions:
webview.focus()before guestexecCommandfor cut / copy / select all. Copy falls back towindow.getSelection()+navigator.clipboard.writeTextwhenexecCommand('copy')fails. Paste readsnavigator.clipboard.readText()and inserts into the guest focused field (pastePlainIntoWebview) becauseexecCommand('paste')is unreliable in<webview>guests; Paste and Match Style for the page reuses that injector after reading the clipboard once. - URL bar menu: Paste and Match Style inserts plain text into the focused URL field via
insertTextInInput(clipboard read in the handler). It no longer callspasteMatchStyle(), which targeted the wrong document and could lose focus afterawaiton the clipboard. - Position:
Menu.popup({ x, y })was fedclientX/clientYfrom the renderer, which are not the same coordinate space Electron expects formenu.popupon macOS (title bar, scaling, etc.), so menus appeared far from the pointer. All native context menus again usemenu.popup({ window })only so the OS places the menu at the cursor (correct right-click position).
macOS menu bar (application menu)
- Structure: File (new tab, new window/incognito, focus URL, print, Downloads; Quit only on non-macOS — Axis app menu handles Quit on darwin), Edit (standard roles plus Paste and Match Style, Copy Page URL, Copy URL as Markdown (
Cmd/Ctrl+Shift+Alt+C), Find, darwin Delete / Start/Stop Speaking), Tabs (new/close/reopen/duplicate/pin/mute, Show Next/Previous Tab — macOSOption+Cmd+←/→, Windows/LinuxCtrl+Tab/Ctrl+Shift+Tab), View (Back/Forward/Stop/Reload, devtools, zoom, sidebar/chat, full screen), History, Window, Help. - Routing: Menu actions use
sendBrowserShortcut()so the focused browser window (including incognito) receivesbrowser-shortcutIPC, not onlymainWindow. - Renderer:
go-back,go-forward,stop-loadinghandled inexecuteBrowserShortcut;copy-url-markdowncopies[title](url)(settings tab:[Settings](axis://settings));next-tab/previous-tabuseswitchToAdjacentTab. - Defaults (Windows/Linux):
pin-tabdefault isCtrl+Alt+Pso it does not duplicate File → New Incognito (Ctrl+Shift+P). macOS unchanged (Shift+Cmd+P). - Help menu: removed Axis Browser Help (readme link) and Keyboard Shortcuts… (shortcuts remain under the Axis app menu on macOS and Edit → Settings on other platforms).
- Context menus: Consistent ordering in
src/main.js— sidebar: New Tab → Tab Group → Incognito → toggle → move; page: link → image → navigation → edit → selection (search/speech) → page (including Copy URL as Markdown); URL bar: cut/copy/paste/paste & match/select all → paste and go; tab: actions → (if in a tab group: Remove from Tab Group, Move to another tab group with other groups only; else Add to Tab Group when groups exist) → separator → Close; tab group unchanged (rename/duplicate/color/icon → delete).index.htmltemplates andrenderer.jslisteners aligned; webpage DOM menu adds paste & match, markdown, print. - Sidebar tabs: Pinned tabs with an open webview use − on the close control (empty pinned slots use ×); pinned/closed state refresh calls
updateIcon. Pinned/unpinned separator: idle = full line (::before); hover = opacity crossfade to a shorter line (::after,--tabs-separator-short-right) — norightanimation; Clear still fades in viaopacity.
Full Changelog: v0.2.5...v0.2.6