```
├── .github/
├── workflows/
├── enforce-pr-base.yml (100 tokens)
├── macos-notarize.yml (1700 tokens)
├── .gitignore (500 tokens)
├── .gitmodules (100 tokens)
├── App/
├── AppDelegate.swift (3.1k tokens)
├── ContentView.swift (900 tokens)
├── NookApp.swift (2.3k tokens)
├── NookCommands.swift (3.1k tokens)
├── Window/
├── WindowView.swift (3.8k tokens)
├── CLAUDE.md (2.1k tokens)
├── CODE_OF_CONDUCT.md (400 tokens)
├── CONTRIBUTING.md (800 tokens)
├── CommandPalette/
├── CommandPalette Accessories/
├── CommandPaletteSuggestionView.swift (900 tokens)
├── GenericSuggestionItem.swift (200 tokens)
├── HistorySuggestionItem.swift (800 tokens)
├── TabSuggestionItem.swift (500 tokens)
├── CommandPalette.swift (300 tokens)
├── CommandPaletteView.swift (5.6k tokens)
├── LICENSE (omitted)
├── Navigation/
├── Sidebar/
├── SidebarBottomBar.swift (500 tokens)
├── SidebarHeader.swift (600 tokens)
├── SpaceContextMenu.swift (800 tokens)
├── SpacesList/
├── SpacesList.swift (1300 tokens)
├── SpacesListItem.swift (1600 tokens)
├── SpacesSideBarView.swift (3.1k tokens)
├── Nook.xcodeproj/
├── project.pbxproj (5.9k tokens)
├── project.xcworkspace/
├── contents.xcworkspacedata
├── xcshareddata/
├── WorkspaceSettings.xcsettings
├── swiftpm/
├── Package.resolved (1600 tokens)
├── xcshareddata/
├── xcschemes/
├── Nook.xcscheme (700 tokens)
├── Nook/
├── Assets.xcassets/
├── AccentColor.colorset/
├── Contents.json
├── Browser Logos/
├── Contents.json
├── arc-logo.imageset/
├── Contents.json (100 tokens)
├── Frame-2.png
├── chrome-logo.imageset/
├── Contents.json (100 tokens)
├── Frame-1.png
├── dia-logo.imageset/
├── Contents.json (100 tokens)
├── Frame 255.png
├── firefox-logo.imageset/
├── Contents.json (100 tokens)
├── Frame.png
├── safari-logo.imageset/
├── Contents.json (100 tokens)
├── safari-logo.png
├── zen-logo.imageset/
├── Contents.json (100 tokens)
├── zen-logo.png
├── Contents.json
├── adblocker-off.imageset/
├── Contents.json (100 tokens)
├── adblocker-off.png
├── adblocker-on.png
├── adblocker-on.imageset/
├── Contents.json (100 tokens)
├── adblocker-off.png
├── adblocker-on.png
├── ai-chat-off.imageset/
├── Contents.json (100 tokens)
├── ai-chat-off.png
├── ai-chat-on.imageset/
├── Contents.json (100 tokens)
├── ai-chat-on.png
├── github.fill.symbolset/
├── Contents.json
├── github.fill.svg (9.8k tokens)
├── noise_texture.imageset/
├── Contents.json (100 tokens)
├── noise_texture.png
├── noise_texture@2x.png
├── noise_texture@3x.png
├── nook-logo-1024.imageset/
├── Contents.json (100 tokens)
├── nook-logo-1024.png
├── opencollective-fill.symbolset/
├── Contents.json
├── opencollective.symbols.svg (900 tokens)
├── plainBackgroundColor.colorset/
├── Contents.json (100 tokens)
├── sidebar.imageset/
├── Contents.json (100 tokens)
├── sidebar.png
├── top-of-window.imageset/
├── Contents.json (100 tokens)
├── topofwindow.png
├── tulips.imageset/
├── Contents.json (100 tokens)
├── michael-loftus-aK4Slh-4uhU-unsplash.jpg
├── url-in-sidebar.imageset/
├── Contents.json (100 tokens)
├── url-in-sidebar.png
├── url-top-of-website.imageset/
├── Contents.json (100 tokens)
├── url-top-of-website.png
├── windowBackgroundColor.colorset/
├── Contents.json (100 tokens)
├── Components/
├── Boosts - deprecated/
├── BoostColorCanvas.swift (1300 tokens)
├── ColorWheelPicker.swift (1400 tokens)
├── Boosts/
├── BoostCodeButton.swift (300 tokens)
├── BoostColorPicker.swift (1700 tokens)
├── BoostFontOptions.swift (1100 tokens)
├── BoostFonts.swift (1100 tokens)
├── BoostHeader.swift (700 tokens)
├── BoostOptions.swift (1600 tokens)
├── BoostUI.swift (800 tokens)
├── BoostZapButton.swift (300 tokens)
├── CodeEditor/
├── BoostCodeView.swift (1300 tokens)
├── CodeEditor.swift (400 tokens)
├── CodeEditorFooter.swift (300 tokens)
├── CodeEditorHeader.swift (600 tokens)
├── Browser/
├── Window/
├── SpaceGradientBackgroundView.swift (200 tokens)
├── SplitCardView.swift (300 tokens)
├── SplitDropCaptureView.swift (1300 tokens)
├── TabCompositorView.swift (2.3k tokens)
├── ColorPicker/
├── AngleDial.swift (400 tokens)
├── ColorPickerView.swift (800 tokens)
├── ColorSwatchRowView.swift (1000 tokens)
├── GradientCanvasEditor.swift (5.7k tokens)
├── GradientEditorView.swift (700 tokens)
├── GradientNodePicker.swift (1200 tokens)
├── GradientPreview.swift (300 tokens)
├── GrainDial.swift (400 tokens)
├── GrainSlider.swift (600 tokens)
├── TransparencySlider.swift (800 tokens)
├── Dialog/
├── DialogView.swift (300 tokens)
├── DragDrop/
├── NookDragItem.swift (400 tokens)
├── NookDragPreviewWindow.swift (1900 tokens)
├── NookDragSessionManager.swift (2.7k tokens)
├── NookDragSourceView.swift (900 tokens)
├── NookDropZoneHostView.swift (900 tokens)
├── EmojiPicker/
├── EmojiPicker.swift (3k tokens)
├── Extensions/
├── ExtensionActionView.swift (1100 tokens)
├── ExtensionLibraryButton.swift (700 tokens)
├── ExtensionLibraryMoreMenu.swift (2.2k tokens)
├── ExtensionLibraryPanel.swift (1500 tokens)
├── ExtensionLibraryView.swift (3.7k tokens)
├── ExtensionPermissionView.swift (700 tokens)
├── PersistentPopover.swift (600 tokens)
├── PopupConsoleWindow.swift (1000 tokens)
├── FindBar/
├── FindBarView.swift (1500 tokens)
├── MiniWindow/
├── MiniWindowButtonStyle.swift (900 tokens)
├── MiniWindowToolbar.swift (2.4k tokens)
├── MiniWindowWebView.swift (4.2k tokens)
├── Navigation/
├── HoldGestureButton.swift (400 tokens)
├── NavigationHistoryContextMenu.swift (1200 tokens)
├── NavigationHistoryMenu.swift (1300 tokens)
├── NavigationHistoryOverlay.swift (500 tokens)
├── Peek/
├── PeekOverlayView.swift (2.6k tokens)
├── PulseTextField/
├── PulseTextField.swift (600 tokens)
├── Settings/
├── CacheDetailsView.swift (1800 tokens)
├── CacheManagementView.swift (2.9k tokens)
├── CookieDetailsView.swift (1400 tokens)
├── CookieManagementView.swift (2.6k tokens)
├── PrivacySettingsView.swift (2.4k tokens)
├── ProfilePickerView.swift (900 tokens)
├── ProfileRowView.swift (800 tokens)
├── SettingsTabBar.swift (100 tokens)
├── SettingsUtils.swift (500 tokens)
├── SettingsView.swift (8.5k tokens)
├── SettingsWindow.swift (700 tokens)
├── ShortcutRecorderView.swift (1200 tokens)
├── Tabs/
├── AI.swift (7.1k tokens)
├── AdBlocker.swift (1000 tokens)
├── AirTrafficControlSettingsView.swift (1800 tokens)
├── Appearance.swift (400 tokens)
├── General.swift (1400 tokens)
├── MemberCard.swift (500 tokens)
├── SponsorBlock.swift (600 tokens)
├── Sidebar/
├── AIChat/
├── AISidebarResizeView.swift (800 tokens)
├── SidebarAIChat.swift (6.2k tokens)
├── CopyURLToast/
├── CopyURLToast.swift (300 tokens)
├── FallbackDropBelowEssentialsModifier.swift (300 tokens)
├── MediaControls/
├── MediaControlsView.swift (2.4k tokens)
├── Menu/
├── DownloadIndicator.swift (400 tokens)
├── SidebarMenu.swift (700 tokens)
├── SidebarMenuDownloadsHover.swift (1800 tokens)
├── SidebarMenuDownloadsTab.swift (1700 tokens)
├── SidebarMenuHistoryTab.swift (5.2k tokens)
├── SidebarMenuTab.swift (300 tokens)
├── NavButtonsView.swift (2.1k tokens)
├── PinnedButtons/
├── EssentialTabsScrollView.swift (600 tokens)
├── EssentialsGridLayout.swift (100 tokens)
├── PinnedGrid.swift (3.2k tokens)
├── PinnedTabView.swift (1000 tokens)
├── PinnedUtils.swift (300 tokens)
├── SidebarResizeView.swift (900 tokens)
├── SpaceSection/
├── SpaceProfileBadge.swift (500 tokens)
├── SpaceProfileDropdown.swift (400 tokens)
├── SpaceSeparator.swift (700 tokens)
├── SpaceTab.swift (2.6k tokens)
├── SpaceTitle.swift (2.2k tokens)
├── SpaceView.swift (6.3k tokens)
├── SplitTabRow.swift (1100 tokens)
├── TabFolderView.swift (2.9k tokens)
├── TabClosureToast/
├── TabClosureToast.swift (300 tokens)
├── TopBar/
├── TopBarView.swift (4.1k tokens)
├── URLBarFramePreferenceKey.swift
├── URLBarView.swift (1800 tokens)
├── UpdateNotification/
├── SidebarUpdateNotification.swift (1000 tokens)
├── SidebarUpdateNotificationPreview.swift (600 tokens)
├── Toast/
├── ShortcutConflictToast.swift (600 tokens)
├── ToastView.swift (700 tokens)
├── WebsitePopup/
├── OAuthAssistBanner.swift (400 tokens)
├── WebsiteView/
├── EmptyWebsiteView.swift (300 tokens)
├── PageLoadingProgressBar.swift (800 tokens)
├── WebView.swift (2k tokens)
├── WebsiteLoadingIndicator.swift (300 tokens)
├── WebsiteView.swift (8.5k tokens)
├── Window/
├── DoubleClickView.swift (300 tokens)
├── MacButtons.swift (1100 tokens)
├── ToolExecutionGlowView.swift (400 tokens)
├── ZoomControls/
├── ZoomPopupView.swift (900 tokens)
├── Extensions/
├── NSUserInterfaceItemIdentifier+WebKit.swift (100 tokens)
├── View+GlassEffect.swift (100 tokens)
├── Info.plist (400 tokens)
├── Managers/
├── AIManager/
├── AIConfigService.swift (3k tokens)
├── AIProvider.swift (800 tokens)
├── AIService.swift (3.1k tokens)
├── MCP/
├── MCPClient.swift (1900 tokens)
├── MCPManager.swift (800 tokens)
├── MCPTransport.swift (2.3k tokens)
├── Providers/
├── GeminiProvider.swift (1600 tokens)
├── OllamaProvider.swift (1000 tokens)
├── OpenAICompatibleProvider.swift (1300 tokens)
├── OpenRouterProvider.swift (1500 tokens)
├── Tools/
├── BrowserToolExecutor.swift (4.6k tokens)
├── BrowserTools.swift (1200 tokens)
├── AuthenticationManager/
├── AuthenticationManager.swift (2000 tokens)
├── BasicAuthCredentialStore.swift (800 tokens)
├── BoostsManager/
├── BoostsManager.swift (4.9k tokens)
├── BoostsWindowManager.swift (4.1k tokens)
├── BrowserManager/
├── BrowserManager.swift (23k tokens)
├── CacheManager/
├── CacheManager.swift (2.9k tokens)
├── ContentBlockerManager/
├── AdvancedBlockingEngine.swift (6.3k tokens)
├── ContentBlockerManager.swift (3.3k tokens)
├── ContentRuleListCompiler.swift (2.6k tokens)
├── FilterListManager.swift (3.8k tokens)
├── Resources/
├── facebook-sponsored-blocker.js (1400 tokens)
├── nook-filters-default.txt (1000 tokens)
├── scriptlets.corelibs.json (93.3k tokens)
├── youtube-ad-blocker.js (3.4k tokens)
├── youtube-sponsorblock.js (3.4k tokens)
├── CookieManager/
├── CookieManager.swift (2.1k tokens)
├── DialogManager/
├── DialogManager.swift (2.8k tokens)
├── Dialogs/
├── BasicAuthDialog.swift (700 tokens)
├── BoostsDialog.swift (200 tokens)
├── BrowserImportDialog.swift (300 tokens)
├── EditPinnedURLDialog.swift (700 tokens)
├── ProfileCreationDialog.swift (1100 tokens)
├── ProfileDeleteConfirmationDialog.swift (500 tokens)
├── ProfileRenameDialog.swift (700 tokens)
├── SettingsDialog.swift (600 tokens)
├── SpaceCreationDialog.swift (1300 tokens)
├── SpaceDeleteConfirmationDialog.swift (600 tokens)
├── SpaceEditDialog.swift (1500 tokens)
├── DownloadManager/
├── DownloadManager.swift (5.1k tokens)
├── DragManager/
├── DragLockManager.swift (300 tokens)
├── TabDragManager.swift (200 tokens)
├── ExtensionManager/
├── BitwardenBiometricHandler.swift (2000 tokens)
├── ExtensionBridge.swift (2.1k tokens)
├── ExtensionManager+Delegate.swift (9.8k tokens)
├── ExtensionManager+Diagnostics.swift (3.4k tokens)
├── ExtensionManager+ExternallyConnectable.swift (10.2k tokens)
├── ExtensionManager+Installation.swift (9k tokens)
├── ExtensionManager+TabNotifications.swift (1000 tokens)
├── ExtensionManager.swift (3.6k tokens)
├── InternalNativePortHandler.swift (200 tokens)
├── NativeMessagingHandler.swift (2.4k tokens)
├── PopupUIDelegate.swift (1000 tokens)
├── README-URLSchemeHandler.md
├── ExternalMiniWindowManager/
├── ExternalMiniWindowManager.swift (1600 tokens)
├── MiniBrowserWindowView.swift (600 tokens)
├── FindManager/
├── FindManager.swift (700 tokens)
├── GradientColorManager/
├── GradientColorManager.swift (500 tokens)
├── HistoryManager/
├── HistoryManager.swift (2.4k tokens)
├── HoverSidebarManager/
├── HoverSidebarManager.swift (1500 tokens)
├── ImportManager/
├── Arc.swift (2.8k tokens)
├── Dia.swift (500 tokens)
├── ImportManager.swift (400 tokens)
├── Safari.swift (1700 tokens)
├── KeyboardShortcutManager/
├── KeyboardShortcutManager.swift (4.3k tokens)
├── WebsiteShortcutDetector.swift (2.3k tokens)
├── MediaControlsManager/
├── MediaControlsManager.swift (2.3k tokens)
├── PeekManager/
├── PeekManager.swift (1000 tokens)
├── PeekSession.swift (300 tokens)
├── PeekWebView.swift (1700 tokens)
├── PiPManager.swift (1100 tokens)
├── PrivacyManager/
├── OAuthDetector.swift (1300 tokens)
├── TrackingProtectionManager.swift (2.3k tokens)
├── ProfileManager/
├── ProfileManager.swift (1100 tokens)
├── SearchManager/
├── SearchManager.swift (2.3k tokens)
├── Utils.swift (900 tokens)
├── SiteRoutingManager/
├── SiteRoutingManager.swift (700 tokens)
├── SiteRoutingRule.swift (200 tokens)
├── SplitViewManager/
├── SplitViewManager.swift (3.3k tokens)
├── SponsorBlockManager/
├── SponsorBlockManager.swift (1600 tokens)
├── SponsorBlockModels.swift (900 tokens)
├── TabManager/
├── TabManager.swift (23.1k tokens)
├── TabOrganizerManager/
├── LocalLLMEngine.swift (1800 tokens)
├── TabOrganizationApplier.swift (1800 tokens)
├── TabOrganizationPlan.swift (1800 tokens)
├── TabOrganizationPrompt.swift (1100 tokens)
├── TabOrganizerManager.swift (1300 tokens)
├── WebViewCoordinator/
├── WebViewCoordinator.swift (3.3k tokens)
├── WindowRegistry/
├── WindowRegistry.swift (500 tokens)
├── ZoomManager/
├── ZoomManager.swift (1200 tokens)
├── Models/
├── AI/
├── AIModels.swift (2.9k tokens)
├── MCPModels.swift (1000 tokens)
├── BrowserConfig/
├── BrowserConfig.swift (1400 tokens)
├── BrowserWindowState.swift (1100 tokens)
├── Cache/
├── CacheModels.swift (2.4k tokens)
├── Cookie/
├── CookieModels.swift (1300 tokens)
├── Extension/
├── ExtensionModels.swift (500 tokens)
├── History/
├── HistoryEntity.swift (200 tokens)
├── KeyboardShortcut/
├── KeyboardShortcut.swift (3.5k tokens)
├── WebsiteShortcutRegistry.swift (4.5k tokens)
├── Profile/
├── Profile.swift (1400 tokens)
├── ProfileEntity.swift (100 tokens)
├── Settings/
├── SiteSearch.swift (1000 tokens)
├── Space/
├── GradientNode.swift (200 tokens)
├── Space.swift (200 tokens)
├── SpaceGradient.swift (2k tokens)
├── SpaceModels.swift (200 tokens)
├── Tab/
├── Tab.swift (32.8k tokens)
├── TabFolder.swift (200 tokens)
├── TabsModel.swift (600 tokens)
├── Nook.entitlements (100 tokens)
├── Supporting Files/
├── Nook-Bridging-Header.h
├── ThirdParty/
├── BigUIPaging/
├── Examples/
├── CardDeckExample.swift (300 tokens)
├── CustomPageViewExample.swift (300 tokens)
├── ExampleGridToPageView.swift (1200 tokens)
├── PageIndicatorExample.swift (800 tokens)
├── PageViewBasicExample.swift (400 tokens)
├── PageViewForEachExample.swift (100 tokens)
├── PageViewNavigationStackExample.swift (900 tokens)
├── Implementations/
├── PageIndicator/
├── PageIndicator+Environment.swift (600 tokens)
├── PageIndicator+iOS.swift (800 tokens)
├── PageIndicator.swift (1100 tokens)
├── PageView/
├── Platform/
├── PlatformPageView+iOS.swift (1900 tokens)
├── PlatformPageView+macOS.swift (2.7k tokens)
├── PlatformPageView.swift (100 tokens)
├── Styles/
├── BookPageViewStyle.swift (300 tokens)
├── BookStackPageViewStyle.swift (200 tokens)
├── CardDeckPageViewStyle.swift (1400 tokens)
├── HistoryStackPageViewStyle.swift (200 tokens)
├── PlainPageViewStyle.swift (200 tokens)
├── PlatformPageViewStyle.swift (100 tokens)
├── ScrollPageViewStyle.swift (200 tokens)
├── Types/
├── PageViewDirection.swift (100 tokens)
├── PageViewNavigateAction.swift (200 tokens)
├── PageViewStyle.swift (600 tokens)
├── PageViewStyleConfiguration.swift (500 tokens)
├── View/
├── PageView+Environment.swift (600 tokens)
├── PageView+Preferences.swift (100 tokens)
├── PageView.swift (2.3k tokens)
├── PageViewEnvironmentModifier.swift (200 tokens)
├── PageViewNavigationButton.swift (500 tokens)
├── Utils/
├── View+Inspect.swift (800 tokens)
├── View+Measure.swift (100 tokens)
├── README.md (2000 tokens)
├── HTSymbolHook/
├── HTSymbolHook.h (200 tokens)
├── HTSymbolHook.m (1200 tokens)
├── README.md (200 tokens)
├── mach_override/
├── .gitignore
├── README.markdown (800 tokens)
├── Rakefile (100 tokens)
├── libudis86/
├── decode.c (5.4k tokens)
├── decode.h (1100 tokens)
├── extern.h (700 tokens)
├── input.c (1100 tokens)
├── input.h (600 tokens)
├── itab.c (63.4k tokens)
├── itab.h (2.1k tokens)
├── syn-att.c (1200 tokens)
├── syn-intel.c (1300 tokens)
├── syn.c (1200 tokens)
├── syn.h (400 tokens)
├── types.h (1500 tokens)
├── udint.h (500 tokens)
├── udis86.c (1900 tokens)
├── mach_override.c (5k tokens)
├── mach_override.h (600 tokens)
├── udis86.h (300 tokens)
├── MuteableWKWebView/
├── Info.plist (200 tokens)
├── MethodSwizzler.h
├── MethodSwizzler.m (200 tokens)
├── MuteableWKWebView.h (100 tokens)
├── MuteableWKWebView.m (400 tokens)
├── MuteableWKWebViewPrivate.h (100 tokens)
├── README.md (200 tokens)
├── SearchSymbol.h (100 tokens)
├── SearchSymbol.m (300 tokens)
├── getPage.c (300 tokens)
├── getPage.h (100 tokens)
├── Utils/
├── AnyShape.swift (100 tokens)
├── BarycentricGradientView.swift (1400 tokens)
├── BlurEffectView.swift (100 tokens)
├── BlurModifier.swift (200 tokens)
├── Colors.swift (1200 tokens)
├── Debug/
├── NavigationRootCauseAnalysis.md (1900 tokens)
├── ExtensionUtils.swift (500 tokens)
├── ForceArrowCursorView.swift (400 tokens)
├── HoverTrackingView.swift (400 tokens)
├── PasteboardTypes.swift
├── Shaders/
├── BarycentricShaders.metal (800 tokens)
├── WebKit/
├── FocusableWKWebView.swift (3k tokens)
├── WebContextMenu.swift (2.4k tokens)
├── WebContextMenuBridge.swift (1100 tokens)
├── WebStoreDownloader.swift (1300 tokens)
├── WebStoreInjector.js (1300 tokens)
├── WebStoreScriptHandler.swift (700 tokens)
├── WebViewThemeColorExtension.swift (1500 tokens)
├── WindowUtils.swift (700 tokens)
├── darkreader.js (51.3k tokens)
├── logo-dev.icon/
├── Assets/
├── Grid.png
├── Light Mode.svg (1100 tokens)
├── icon.json (400 tokens)
├── logo.icon/
├── Assets/
├── Light Mode.svg (1100 tokens)
├── icon.json (300 tokens)
├── Onboarding/
├── Components/
├── OnboardingUtils.swift (200 tokens)
├── RoundedSpinner.swift (100 tokens)
├── StageFooter.swift (500 tokens)
├── StageIndicator.swift (100 tokens)
├── TransitionView.swift (600 tokens)
├── ViewTransition.metal (200 tokens)
├── OnboardingView.swift (900 tokens)
├── Stages/
├── AdBlockerStage.swift (300 tokens)
├── AiChatStage.swift (300 tokens)
├── BackgroundStage.swift (600 tokens)
├── FInalStage.swift (100 tokens)
├── HelloStage.swift (100 tokens)
├── ImportStage.swift (400 tokens)
├── SafariImportFlow.swift (2.9k tokens)
├── TabLayoutStage.swift (300 tokens)
├── URLBarStage.swift (300 tokens)
├── README.md (1400 tokens)
├── Settings/
├── NookSettingsService.swift (5.7k tokens)
├── UI/
├── Buttons/
├── NavButtons/
├── NavButton.swift (1400 tokens)
├── NookButton/
├── NookButtonStyle.swift (1400 tokens)
├── ConditionalModifiers.swift (800 tokens)
├── assets/
├── icon.png
├── telemetry-id
```
## /.github/workflows/enforce-pr-base.yml
```yml path="/.github/workflows/enforce-pr-base.yml"
name: Enforce PR base branch
on:
pull_request:
types: [opened, edited, synchronize]
jobs:
check-branch:
runs-on: ubuntu-latest
steps:
- name: Fail if PR targets main
if: github.event.pull_request.base.ref == 'main'
run: |
echo "PRs must target dev, not main."
exit 1
```
## /.github/workflows/macos-notarize.yml
```yml path="/.github/workflows/macos-notarize.yml"
name: macOS Build, Sign, Notarize & Release
on:
release:
types: [published]
workflow_dispatch:
jobs:
build-sign-notarize:
runs-on: macos-26
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set version info
run: |
VERSION="${{ github.event.release.tag_name }}"
SHORT_VERSION="${VERSION#v}"
BUILD_NUMBER=$(grep 'CURRENT_PROJECT_VERSION' Nook.xcodeproj/project.pbxproj \
| head -n1 | grep -oE '[0-9]+' | tail -n1)
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "SHORT_VERSION=$SHORT_VERSION" >> $GITHUB_ENV
echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_ENV
echo "Building version $SHORT_VERSION (build $BUILD_NUMBER)"
- name: Import Developer ID certificate
env:
APPLE_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
printf "%s" "$APPLE_CERTIFICATE_P12_BASE64" | base64 --decode > signing_certificate.p12
security create-keychain -p "" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "" build.keychain
security import signing_certificate.p12 \
-k build.keychain \
-P "$APPLE_CERTIFICATE_PASSWORD" \
-T /usr/bin/codesign
security list-keychains -d user -s build.keychain login.keychain
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" build.keychain
IDENTITY=$(security find-identity -v -p codesigning build.keychain \
| grep "Developer ID Application" | head -n1 | awk '{print $2}')
echo "SIGNING_IDENTITY=$IDENTITY" >> $GITHUB_ENV
echo "Using signing identity: $IDENTITY"
- name: Build app
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
set -e
mkdir -p build
ENTITLEMENTS="$(pwd)/Nook/Nook.entitlements"
echo "Attempting universal build (arm64 + x86_64)..."
if ! xcodebuild -scheme Nook -configuration Release \
-arch arm64 -arch x86_64 \
-derivedDataPath build \
CODE_SIGN_IDENTITY="$SIGNING_IDENTITY" \
CODE_SIGN_STYLE=Manual \
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \
CODE_SIGN_ENTITLEMENTS="$ENTITLEMENTS" \
PROVISIONING_PROFILE_SPECIFIER=""; then
echo "Universal build failed, retrying Apple Silicon only..."
xcodebuild -scheme Nook -configuration Release \
-arch arm64 \
-derivedDataPath build \
CODE_SIGN_IDENTITY="$SIGNING_IDENTITY" \
CODE_SIGN_STYLE=Manual \
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \
CODE_SIGN_ENTITLEMENTS="$ENTITLEMENTS" \
PROVISIONING_PROFILE_SPECIFIER=""
fi
cp -R "build/Build/Products/Release/Nook.app" ./Nook.app
- name: Re-sign for notarization
run: |
# Notarization requires all binaries to be signed with our Developer ID,
# a secure timestamp, and hardened runtime. Sparkle ships pre-signed with
# its own cert, so we must re-sign the entire bundle from inside out.
SPARKLE="Nook.app/Contents/Frameworks/Sparkle.framework/Versions/B"
# Sign innermost binaries first
codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime \
"$SPARKLE/XPCServices/Downloader.xpc/Contents/MacOS/Downloader"
codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime \
"$SPARKLE/XPCServices/Installer.xpc/Contents/MacOS/Installer"
codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime \
"$SPARKLE/Updater.app/Contents/MacOS/Updater"
codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime \
"$SPARKLE/Autoupdate"
# Sign bundles
codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime \
"$SPARKLE/XPCServices/Downloader.xpc"
codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime \
"$SPARKLE/XPCServices/Installer.xpc"
codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime \
"$SPARKLE/Updater.app"
# Sign the Sparkle dylib itself before signing the framework bundle
codesign --force --sign "$SIGNING_IDENTITY" --timestamp \
"$SPARKLE/Sparkle"
# Sign the framework version (Versions/B) — do NOT also sign the
# top-level Sparkle.framework symlink; it resolves to the same Versions/B
# directory and double-signing would invalidate the signature.
codesign --force --sign "$SIGNING_IDENTITY" --timestamp --options runtime "$SPARKLE"
# Re-sign the main app with entitlements and hardened runtime
codesign --force --sign "$SIGNING_IDENTITY" --timestamp \
--options runtime \
--entitlements "$(pwd)/Nook/Nook.entitlements" \
"Nook.app"
- name: Verify signature before notarizing
run: codesign --verify --deep --strict --verbose=2 "Nook.app"
- name: Notarize app
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
run: |
xcrun notarytool store-credentials "nook-notary" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD"
# ditto preserves macOS symlinks and extended attributes; zip -r does not.
# Broken symlinks in the archive cause "invalid signature" errors because
# the bundle seal references symlink targets, not file copies.
ditto -c -k --keepParent "Nook.app" Nook.zip
SUBMIT_OUTPUT=$(xcrun notarytool submit "Nook.zip" \
--keychain-profile "nook-notary" \
--wait 2>&1)
echo "$SUBMIT_OUTPUT"
SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | grep "^ id:" | head -1 | awk '{print $2}')
STATUS=$(echo "$SUBMIT_OUTPUT" | grep "^ status:" | awk '{print $2}')
if [ "$STATUS" != "Accepted" ]; then
echo "Notarization rejected. Fetching log..."
xcrun notarytool log "$SUBMISSION_ID" --keychain-profile "nook-notary"
exit 1
fi
- name: Staple notarization ticket
run: xcrun stapler staple "Nook.app"
- name: Verify signature
run: |
codesign --verify --deep --strict --verbose=2 "Nook.app"
spctl --assess --type execute --verbose "Nook.app"
- name: Create DMG
run: |
hdiutil create -volname "Nook $SHORT_VERSION" \
-srcfolder "Nook.app" \
-ov -format UDZO "Nook-${VERSION}.dmg"
- name: Upload DMG to release
uses: softprops/action-gh-release@v2
with:
files: Nook-*.dmg
fail_on_unmatched_files: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update appcast on gh-pages
run: |
DMG_URL="https://github.com/${{ github.repository }}/releases/download/${VERSION}/Nook-${VERSION}.dmg"
DATE=$(date -R)
git fetch origin gh-pages
git checkout gh-pages
ENTRY=$(cat << XMLEOF
<item>
<title>Version ${SHORT_VERSION}</title>
<sparkle:releaseNotesLink>https://github.com/${{ github.repository }}/releases/tag/${VERSION}</sparkle:releaseNotesLink>
<pubDate>${DATE}</pubDate>
<enclosure
url="${DMG_URL}"
sparkle:version="${BUILD_NUMBER}"
sparkle:shortVersionString="${SHORT_VERSION}"
type="application/octet-stream"/>
</item>
XMLEOF
)
python3 -c "import sys; content = open('appcast.xml').read(); entry = sys.argv[1]; content = content.replace('</channel>', entry + '\n</channel>', 1); open('appcast.xml', 'w').write(content)" "$ENTRY"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add appcast.xml
git commit -m "Release ${VERSION}"
git push origin gh-pages
```
## /.gitignore
```gitignore path="/.gitignore"
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
sessions-config.json
.claude
Nook/.claude
.crush
/sessions
AGENTS.md
.xcodebuildmcp/
package.json
package-lock.json
.opencode/
## User settings
xcuserdata/
# Xcode nonsense
*.pbxproj
# macOS garbage
.DS_Store
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
## Obj-C/Swift specific
*.hmap
## App packaging
*.ipa
*.dSYM.zip
*.dSYM
## Playgrounds
'timeline.xctimeline'
playground.xcworkspace
*.playground
UIPlayground/
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm
.build/
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build/
# Accio dependency management
Dependencies/
.accio/
# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
# Code Injection
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode
iOSInjectionProject/
# IDE
.vscode/
# Xcode build junk
DerivedData/
*.xcresult
*.log
*.profraw
.superpowers/
```
## /.gitmodules
```gitmodules path="/.gitmodules"
[submodule "Fuzi"]
path = Fuzi
url = https://github.com/cezheng/Fuzi.git
[submodule "Highlightr"]
path = Highlightr
url = https://github.com/raspu/Highlightr.git
[submodule "LRUCache"]
path = LRUCache
url = https://github.com/nicklockwood/LRUCache.git
[submodule "Motion"]
path = Motion
url = https://github.com/b3ll/Motion.git
[submodule "reeeed"]
path = reeeed
url = https://github.com/nate-parrott/reeeed.git
[submodule "swift-atomics"]
path = swift-atomics
url = https://github.com/apple/swift-atomics.git
[submodule "swift-numerics"]
path = swift-numerics
url = https://github.com/apple/swift-numerics.git
```
## /App/AppDelegate.swift
```swift path="/App/AppDelegate.swift"
//
// AppDelegate.swift
// Nook
//
// Application lifecycle delegate handling app termination, URL events, and Sparkle updates
//
import AppKit
import OSLog
import Sparkle
/// Handles application-level lifecycle events and coordinates app termination
///
/// Key responsibilities:
/// - **URL Handling**: Opens external URLs (e.g., from other apps, custom URL schemes)
/// - **Mouse Button Events**: Maps mouse buttons 2/3/4 to command palette, back, and forward
/// - **App Termination**: Coordinates graceful shutdown with data persistence
/// - **Sparkle Updates**: Integrates with Sparkle framework for auto-updates
///
/// The termination flow uses async persistence to avoid MainActor deadlocks:
/// 1. Returns `.terminateLater` immediately
/// 2. Persists tab snapshots atomically
/// 3. Saves SwiftData context
/// 4. Cleans up WKWebView processes
/// 5. Replies with terminate approval
class AppDelegate: NSObject, NSApplicationDelegate, SPUUpdaterDelegate {
private static let log = Logger(
subsystem: Bundle.main.bundleIdentifier ?? "Nook", category: "AppTermination")
// TEMPORARY: Reference to BrowserManager for coordinating browser operations
// TODO: Replace with direct access to independent managers (TabManager, etc.)
weak var browserManager: BrowserManager?
// Window registry for accessing active window state
weak var windowRegistry: WindowRegistry?
// MCP Manager reference for cleanup on termination
var mcpManager: MCPManager?
private let urlEventClass = AEEventClass(kInternetEventClass)
private let urlEventID = AEEventID(kAEGetURL)
private var mouseEventMonitor: Any?
private var wakeObserver: Any?
private let userDefaults = UserDefaults.standard
private var pendingURLs: [URL] = []
// MARK: - Sparkle Updates
/// Sparkle updater controller for automatic app updates
lazy var updaterController: SPUStandardUpdaterController = {
return SPUStandardUpdaterController(
startingUpdater: true, updaterDelegate: self, userDriverDelegate: nil)
}()
// MARK: - Application Lifecycle
func applicationDidFinishLaunching(_ notification: Notification) {
setupURLEventHandling()
setupMouseButtonHandling()
setupSleepWakeHandling()
let didFinishOnboarding = userDefaults.bool(forKey: "settings.didFinishOnboarding")
if let window = NSApplication.shared.windows.first {
// Always hide titlebar text immediately to prevent flash during transitions
window.titlebarAppearsTransparent = true
window.titleVisibility = .hidden
window.toolbar?.isVisible = false
if !didFinishOnboarding {
window.setContentSize(NSSize(width: 1200, height: 720))
window.center()
NSApp.activate(ignoringOtherApps: true)
NSApp.hideOtherApplications(nil)
}
}
}
/// Observes system wake notifications and resets crash counters on all tabs.
///
/// When the system wakes from sleep, launchservicesd and other XPC services need
/// a few seconds to fully restart. During this window, new WebContent processes crash
/// immediately with XPC_ERROR_CONNECTION_INVALID. We reset crash counters on wake so
/// the exponential backoff in webViewWebContentProcessDidTerminate starts fresh and
/// the delayed reload eventually succeeds once XPC services are stable.
private func setupSleepWakeHandling() {
wakeObserver = NSWorkspace.shared.notificationCenter.addObserver(
forName: NSWorkspace.didWakeNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.handleSystemWake()
}
}
private func handleSystemWake() {
AppDelegate.log.info("System woke from sleep — resetting web process crash counters")
// Reset crash counters so tabs get fresh backoff windows after wake.
// Tabs that were mid-crash-loop before sleep will retry with a clean slate.
// Called on the main queue (per NSWorkspace notification delivery), so MainActor access is safe.
MainActor.assumeIsolated {
guard let manager = browserManager else { return }
for tab in manager.tabManager.allTabs() {
tab.webProcessCrashCount = 0
tab.lastWebProcessCrashDate = .distantPast
}
}
}
/// Registers handler for external URL events (e.g., clicking links from other apps)
private func setupURLEventHandling() {
NSAppleEventManager.shared().setEventHandler(
self,
andSelector: #selector(handleGetURLEvent(_:withReplyEvent:)),
forEventClass: urlEventClass,
andEventID: urlEventID
)
}
/// Sets up global mouse button event monitoring for extra physical mouse buttons
///
/// Many mice have extra buttons beyond left/right click. This maps them to browser actions:
/// - **Button 2** (middle click/scroll wheel button): Open command palette
/// - **Button 3** (typically a side button labeled "Back"): Navigate back in history
/// - **Button 4** (typically a side button labeled "Forward"): Navigate forward in history
///
/// This is common in browsers - side buttons on gaming/office mice are often used for navigation.
private func setupMouseButtonHandling() {
mouseEventMonitor = NSEvent.addLocalMonitorForEvents(matching: .otherMouseDown) {
[weak self] event in
guard let self = self,
let manager = self.browserManager,
let registry = self.windowRegistry else { return event }
// Mouse events are delivered on the main thread, so we can safely assume main actor isolation
MainActor.assumeIsolated {
switch event.buttonNumber {
case 2: // Middle mouse button
if let hoveredId = manager.hoveredPinnedTabId,
let tab = manager.tabManager.allTabs().first(where: { $0.id == hoveredId }),
tab.pinnedURL != nil {
tab.resetToPinnedURL()
} else {
registry.activeWindow?.commandPalette?.open()
}
case 3: // Back button
guard
let windowState = registry.activeWindow,
let currentTab = manager.currentTab(for: windowState),
let webView = manager.getWebView(for: currentTab.id, in: windowState.id)
else {
return
}
webView.goBack()
case 4: // Forward button
guard
let windowState = registry.activeWindow,
let currentTab = manager.currentTab(for: windowState),
let webView = manager.getWebView(for: currentTab.id, in: windowState.id)
else {
return
}
webView.goForward()
default:
break
}
}
return event
}
}
/// Handles URLs opened from external sources (e.g., Finder, other apps)
func application(_ application: NSApplication, open urls: [URL]) {
urls.forEach { handleIncoming(url: $0) }
}
// MARK: - Application Termination
/// Initiates async termination process to avoid MainActor deadlocks
///
/// Returns `.terminateLater` immediately, then performs async cleanup:
/// 1. Phase 1: Atomic snapshot persistence (non-blocking)
/// 2. Phase 2: SwiftData context save
/// 3. Phase 3: WKWebView process cleanup
///
/// - Returns: Always returns `.terminateLater` to handle termination asynchronously
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
// When "warn before quitting" is disabled, terminate(nil) is called directly
// from the SwiftUI CommandGroup button action. Returning .terminateLater in that
// context deadlocks because the async Task can't execute during the termination
// run loop mode. Since the user opted out of the warning, just quit immediately.
let askBeforeQuit = userDefaults.bool(forKey: "settings.askBeforeQuit")
if !askBeforeQuit {
return .terminateNow
}
handleTermination(sender: sender, shouldTerminate: true)
return .terminateLater
}
/// Performs async termination tasks on MainActor
///
/// This method executes the three-phase shutdown process:
/// - **Phase 1**: Atomic tab snapshot persistence
/// - **Phase 2**: SwiftData context save
/// - **Phase 3**: WebView cleanup
///
/// Timing is logged for each phase to monitor performance.
private func handleTermination(sender: NSApplication, shouldTerminate: Bool) {
AppDelegate.log.info(
"applicationShouldTerminate: returning terminateLater and starting async persistence")
Task { @MainActor in
guard shouldTerminate else {
sender.reply(toApplicationShouldTerminate: false)
return
}
// Minimal fallback if BrowserManager is unavailable
guard let manager = browserManager else {
// Attempt a best-effort save via shared persistence container
do {
let ctx = Persistence.shared.container.mainContext
try ctx.save()
AppDelegate.log.info("Fallback save without BrowserManager succeeded")
} catch {
AppDelegate.log.error(
"Fallback save without BrowserManager failed: \(String(describing: error))"
)
}
sender.reply(toApplicationShouldTerminate: true)
return
}
let overallStart = CFAbsoluteTimeGetCurrent()
AppDelegate.log.info("Termination task started on MainActor")
// Phase 1: Atomic snapshot persistence (non-throwing Bool)
let persistStart = CFAbsoluteTimeGetCurrent()
let atomic: Bool = await manager.tabManager.persistSnapshotAwaitingResult()
let pdt = CFAbsoluteTimeGetCurrent() - persistStart
AppDelegate.log.info(
"Atomic persistence \(atomic ? "succeeded" : "did not run; fallback used") in \(String(format: "%.3f", pdt))s"
)
// Phase 2: Ensure SwiftData changes are committed
let contextSaveStart = CFAbsoluteTimeGetCurrent()
do {
try manager.modelContext.save()
let sdt = CFAbsoluteTimeGetCurrent() - contextSaveStart
AppDelegate.log.info("Context save completed in \(String(format: "%.3f", sdt))s")
} catch {
let sdt = CFAbsoluteTimeGetCurrent() - contextSaveStart
AppDelegate.log.error(
"Context save failed in \(String(format: "%.3f", sdt))s: \(String(describing: error))"
)
}
// Phase 3: Graceful cleanup
manager.cleanupAllTabs()
AppDelegate.log.info("Cleanup completed; WKWebView processes terminated")
let total = CFAbsoluteTimeGetCurrent() - overallStart
AppDelegate.log.info(
"Termination task finished in \(String(format: "%.3f", total))s; replying to terminate"
)
sender.reply(toApplicationShouldTerminate: true)
}
}
func applicationWillTerminate(_ notification: Notification) {
// Keep minimal to avoid MainActor deadlocks; main work happens in applicationShouldTerminate
AppDelegate.log.info("applicationWillTerminate called")
// Stop MCP child processes synchronously (blocking up to 5 seconds)
mcpManager?.stopAllSync()
}
// MARK: - External URL Handling
/// Handles URL events from AppleScript/AppleEvents
@objc private func handleGetURLEvent(
_ event: NSAppleEventDescriptor, withReplyEvent replyEvent: NSAppleEventDescriptor
) {
guard let stringValue = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue,
let url = URL(string: stringValue)
else {
return
}
// Security: Only allow http/https URLs from external automation
guard let scheme = url.scheme?.lowercased(),
scheme == "http" || scheme == "https" else {
return
}
handleIncoming(url: url)
}
/// Routes incoming external URLs to the browser manager
///
/// If the browser manager isn't ready yet (cold launch via URL click),
/// queues the URL and drains it once `browserManager` is set.
private func handleIncoming(url: URL) {
guard let manager = browserManager else {
AppDelegate.log.info("Queuing URL for deferred open: \(url.absoluteString, privacy: .public)")
pendingURLs.append(url)
return
}
Task { @MainActor in
// Air Traffic Control — route to designated space if a rule matches
if manager.siteRoutingManager.applyRoute(url: url, from: nil) {
return
}
manager.presentExternalURL(url)
}
}
/// Opens any URLs that arrived before browserManager was available
func drainPendingURLs() {
guard !pendingURLs.isEmpty else { return }
let urls = pendingURLs
pendingURLs.removeAll()
urls.forEach { handleIncoming(url: $0) }
}
}
// MARK: - Sparkle Delegate
extension AppDelegate {
/// Called when Sparkle finds a valid update
func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) {
Task { @MainActor in
browserManager?.handleUpdaterFoundValidUpdate(item)
}
}
/// Called when Sparkle finishes downloading an update
func updater(_ updater: SPUUpdater, didFinishDownloadingUpdate item: SUAppcastItem) {
Task { @MainActor in
browserManager?.handleUpdaterFinishedDownloading(item)
}
}
/// Called when no update is found
func updaterDidNotFindUpdate(_ updater: SPUUpdater) {
Task { @MainActor in
browserManager?.handleUpdaterDidNotFindUpdate()
}
}
/// Called when user cancels the update download
func userDidCancelDownload(_ updater: SPUUpdater) {
Task { @MainActor in
browserManager?.handleUpdaterAbortedUpdate()
}
}
/// Called when update process encounters an error
func updater(_ updater: SPUUpdater, didAbortWithError error: any Error) {
Task { @MainActor in
browserManager?.handleUpdaterAbortedUpdate()
}
}
/// Called when update is ready to install on quit
func updater(
_ updater: SPUUpdater, willInstallUpdateOnQuit item: SUAppcastItem,
immediateInstallationInvocation: @escaping () -> Void
) {
Task { @MainActor in
browserManager?.handleUpdaterWillInstallOnQuit(item)
}
}
}
```
## /App/ContentView.swift
```swift path="/App/ContentView.swift"
//
// ContentView.swift
// Nook
//
// Created by Maciek Bagiński on 28/07/2025.
// Updated by Aether Aurelia on 15/11/2025.
//
import SwiftUI
import AppKit
struct ContentView: View {
@EnvironmentObject var browserManager: BrowserManager
@EnvironmentObject var tabManager: TabManager
@Environment(WindowRegistry.self) private var windowRegistry
@State private var defaultWindowState = BrowserWindowState()
@State private var commandPalette = CommandPalette()
private let providedWindowState: BrowserWindowState?
init(windowState: BrowserWindowState? = nil) {
self.providedWindowState = windowState
}
private var windowState: BrowserWindowState {
providedWindowState ?? defaultWindowState
}
var body: some View {
WindowView()
.environment(windowState)
.environment(commandPalette)
.environmentObject(browserManager.gradientColorManager)
.background(WindowFocusBridge(windowState: windowState, windowRegistry: windowRegistry))
.frame(minWidth: 470, minHeight: 382)
.onAppear {
// Set TabManager reference for computed properties
windowState.tabManager = tabManager
// Set CommandPalette reference for global shortcuts
windowState.commandPalette = commandPalette
// Register this window state with the registry
windowRegistry.register(windowState)
}
.onDisappear {
// Unregister this window state when the window closes
windowRegistry.unregister(windowState.id)
}
}
}
private struct WindowFocusBridge: NSViewRepresentable {
let windowState: BrowserWindowState
let windowRegistry: WindowRegistry
func makeCoordinator() -> Coordinator {
Coordinator(windowState: windowState, windowRegistry: windowRegistry)
}
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
context.coordinator.attach(to: view.window)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
// Window is available at update time — call synchronously.
// The attach method guards against re-attachment to the same window.
context.coordinator.attach(to: nsView.window)
}
static func dismantleNSView(_ nsView: NSView, coordinator: Coordinator) {
coordinator.detach()
}
final class Coordinator: NSObject {
let windowState: BrowserWindowState
let windowRegistry: WindowRegistry
private weak var window: NSWindow?
private var keyObserver: Any?
init(windowState: BrowserWindowState, windowRegistry: WindowRegistry) {
self.windowState = windowState
self.windowRegistry = windowRegistry
}
func attach(to window: NSWindow?) {
guard self.window !== window else { return }
detach()
self.window = window
guard let window else { return }
// Store NSWindow reference on the window state so other systems
// (e.g. KeyboardShortcutManager) can identify browser windows
Task { @MainActor in
windowState.window = window
}
keyObserver = NotificationCenter.default.addObserver(
forName: NSWindow.didBecomeKeyNotification,
object: window,
queue: .main
) { [weak self] _ in
guard let self else { return }
Task { @MainActor in
self.windowRegistry.setActive(self.windowState)
}
}
if window.isKeyWindow {
Task { @MainActor in
windowRegistry.setActive(windowState)
}
}
}
func detach() {
if let observer = keyObserver {
NotificationCenter.default.removeObserver(observer)
keyObserver = nil
}
window = nil
}
deinit {
detach()
}
}
}
```
## /App/NookApp.swift
```swift path="/App/NookApp.swift"
//
// NookApp.swift
// Nook
//
// Created by Maciek Bagiński on 28/07/2025.
// Updated by Aether Aurelia on 15/11/2025.
//
import AppKit
import Carbon
import OSLog
import Sparkle
import SwiftUI
@main
struct NookApp: App {
@State private var windowRegistry = WindowRegistry()
@State private var webViewCoordinator = WebViewCoordinator()
@State private var settingsManager = NookSettingsService()
@State private var keyboardShortcutManager = KeyboardShortcutManager()
@State private var aiConfigService: AIConfigService
@State private var mcpManager = MCPManager()
@State private var aiService: AIService
@State private var tabOrganizerManager = TabOrganizerManager()
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
// TEMPORARY: BrowserManager will be phased out as a global singleton.
// Eventually each manager (TabManager, etc.) will be independent and injected via environment.
@StateObject private var browserManager = BrowserManager()
init() {
let config = AIConfigService()
_aiConfigService = State(initialValue: config)
_aiService = State(initialValue: AIService(configService: config))
}
var body: some Scene {
WindowGroup {
TransitionView(showB: $settingsManager.didFinishOnboarding) {
OnboardingView()
.ignoresSafeArea(.all)
.background(BackgroundWindowModifier())
.environment(\.nookSettings, settingsManager)
.environmentObject(browserManager)
} viewB: {
ContentView()
.ignoresSafeArea(.all)
.background(BackgroundWindowModifier())
.environmentObject(browserManager)
.environmentObject(browserManager.tabManager)
.environment(windowRegistry)
.environment(webViewCoordinator)
.environment(\.nookSettings, settingsManager)
.environment(keyboardShortcutManager)
.environment(aiConfigService)
.environment(mcpManager)
.environment(aiService)
.environment(tabOrganizerManager)
.onAppear {
setupApplicationLifecycle()
setupAIServices()
}
}
}
.windowStyle(.hiddenTitleBar)
.commands {
NookCommands(
browserManager: browserManager,
windowRegistry: windowRegistry,
shortcutManager: keyboardShortcutManager,
tabOrganizerManager: tabOrganizerManager
)
}
// macOS 26 style sidebar settings window
Window("Nook Settings", id: "nook-settings") {
SettingsWindow()
.environmentObject(browserManager)
.environmentObject(browserManager.tabManager)
.environmentObject(browserManager.gradientColorManager)
.environment(\.nookSettings, settingsManager)
.environment(keyboardShortcutManager)
.environment(aiConfigService)
.environment(mcpManager)
.environment(tabOrganizerManager)
}
.windowResizability(.contentSize)
.defaultPosition(.center)
}
// MARK: - Application Lifecycle Setup
/// Wires AI services to runtime dependencies (BrowserManager, MCP, etc.)
private func setupAIServices() {
aiService.browserManager = browserManager
aiService.mcpManager = mcpManager
let toolExecutor = BrowserToolExecutor(browserManager: browserManager)
aiService.browserToolExecutor = toolExecutor
// Start enabled MCP servers
mcpManager.startEnabledServers(configs: aiConfigService.mcpServers)
}
/// Configures application-level dependencies and callbacks when the first window appears.
///
/// This function sets up the following connections:
/// - AppDelegate ↔ BrowserManager: For app termination cleanup and Sparkle update integration
/// - WindowRegistry callbacks: Register, close, and activate window state
/// - Keyboard shortcut manager: Enable global keyboard shortcuts
///
/// TEMPORARY CONNECTIONS (to be removed during refactoring):
/// - BrowserManager ← WebViewCoordinator: Currently BrowserManager holds a reference to coordinate web views
/// - BrowserManager ← WindowRegistry: Currently BrowserManager tracks active window state
///
/// These temporary connections exist because BrowserManager is currently a god object.
/// Future refactoring will eliminate these by:
/// 1. Moving WebViewCoordinator ownership to per-window state
/// 2. Moving window management out of BrowserManager entirely
/// 3. Using pure environment-based dependency injection
private func setupApplicationLifecycle() {
// Connect AppDelegate for termination and updates
appDelegate.browserManager = browserManager
appDelegate.windowRegistry = windowRegistry
appDelegate.mcpManager = mcpManager
appDelegate.drainPendingURLs()
browserManager.appDelegate = appDelegate
// TEMPORARY: Wire coordinators to BrowserManager
// TODO: Remove these connections - coordinators should be independent
browserManager.webViewCoordinator = webViewCoordinator
browserManager.windowRegistry = windowRegistry
browserManager.nookSettings = settingsManager
browserManager.tabManager.nookSettings = settingsManager
browserManager.siteRoutingManager.settingsService = settingsManager
browserManager.siteRoutingManager.browserManager = browserManager
browserManager.aiService = aiService
browserManager.aiConfigService = aiConfigService
// Configure managers that depend on settings
browserManager.compositorManager.setMode(
settingsManager.tabManagementMode
)
browserManager.contentBlockerManager.setEnabled(
settingsManager.blockCrossSiteTracking || settingsManager.adBlockerEnabled
)
// Apply appearance mode
applyAppearanceMode(settingsManager.appearanceMode)
NotificationCenter.default.addObserver(
forName: .appearanceModeChanged,
object: nil,
queue: .main
) { [weak settingsManager] _ in
guard let settings = settingsManager else { return }
applyAppearanceMode(settings.appearanceMode)
}
// Initialize keyboard shortcut manager
keyboardShortcutManager.setBrowserManager(browserManager)
browserManager.keyboardShortcutManager = keyboardShortcutManager
browserManager.mcpManager = mcpManager
browserManager.tabOrganizerManager = tabOrganizerManager
// Set up window lifecycle callbacks
windowRegistry.onWindowRegister = { [weak browserManager] windowState in
browserManager?.setupWindowState(windowState)
}
// Retroactively set up any windows that registered before this callback was set
// (child .onAppear fires before parent .onAppear in SwiftUI)
for (_, windowState) in windowRegistry.windows {
browserManager.setupWindowState(windowState)
}
windowRegistry.onWindowClose = {
[webViewCoordinator, weak browserManager] windowId in
// Only cleanup if browserManager still exists (it's captured weakly)
if let browserManager = browserManager {
webViewCoordinator.cleanupWindow(
windowId,
tabManager: browserManager.tabManager
)
browserManager.splitManager.cleanupWindow(windowId)
// Clean up incognito window if applicable
if let windowState = browserManager.windowRegistry?.windows[windowId],
windowState.isIncognito {
Task {
await browserManager.closeIncognitoWindow(windowState)
}
}
} else {
// BrowserManager was deallocated - perform minimal cleanup
// Remove compositor container view to prevent leaks
webViewCoordinator.removeCompositorContainerView(for: windowId)
}
}
windowRegistry.onActiveWindowChange = {
[weak browserManager] windowState in
browserManager?.setActiveWindowState(windowState)
}
}
}
// MARK: - Appearance Mode
private func applyAppearanceMode(_ mode: AppearanceMode) {
switch mode {
case .system:
NSApp.appearance = nil // Follow system
case .light:
NSApp.appearance = NSAppearance(named: .aqua)
case .dark:
NSApp.appearance = NSAppearance(named: .darkAqua)
}
}
// MARK: - Window Configuration
/// Configures the window appearance and behavior for Nook browser windows
///
/// This modifier:
/// - Hides the title bar text while keeping native traffic light buttons visible
/// - Sets transparent background for custom window styling
/// - Configures minimum window size
/// - Enables full-size content view for edge-to-edge content
struct BackgroundWindowModifier: NSViewRepresentable {
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
if let window = view.window {
window.toolbar?.isVisible = false
window.titlebarAppearsTransparent = true
window.backgroundColor = .clear
window.titleVisibility = .hidden
window.isReleasedWhenClosed = false
// window.isMovableByWindowBackground = true // Disabled - use SwiftUI-based window drag system instead
window.isMovable = true
var mask: NSWindow.StyleMask = [
.titled, .closable, .miniaturizable, .resizable,
.fullSizeContentView,
]
// Preserve fullScreen flag — removing it outside a transition crashes on macOS 15.5+
if window.styleMask.contains(.fullScreen) {
mask.insert(.fullScreen)
}
window.styleMask = mask
window.minSize = NSSize(width: 470, height: 382)
window.contentMinSize = NSSize(width: 470, height: 382)
// Persist and restore window frame (position + size) across launches.
// setFrameAutosaveName makes macOS automatically save the frame to
// UserDefaults whenever it changes, so the window size is remembered
// on close — not just on quit.
window.setFrameAutosaveName("NookBrowserWindow")
}
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
guard let window = nsView.window else { return }
// Only re-apply if somehow reset (e.g., view transition flash)
guard !window.titlebarAppearsTransparent else { return }
window.titlebarAppearsTransparent = true
window.titleVisibility = .hidden
}
}
```
## /App/NookCommands.swift
```swift path="/App/NookCommands.swift"
//
// NookCommands.swift
// Nook
//
// Menu bar commands for the Nook browser application
//
import AppKit
import SwiftUI
import WebKit
struct NookCommands: Commands {
let browserManager: BrowserManager
let windowRegistry: WindowRegistry
let shortcutManager: KeyboardShortcutManager
let tabOrganizerManager: TabOrganizerManager
@Environment(\.openWindow) private var openWindow
@Environment(\.nookSettings) var nookSettings
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
init(browserManager: BrowserManager, windowRegistry: WindowRegistry, shortcutManager: KeyboardShortcutManager, tabOrganizerManager: TabOrganizerManager) {
self.browserManager = browserManager
self.windowRegistry = windowRegistry
self.shortcutManager = shortcutManager
self.tabOrganizerManager = tabOrganizerManager
}
// MARK: - Dynamic Keyboard Shortcuts
/// Returns the key equivalent for a given action, or nil if disabled
private func keyEquivalent(for action: ShortcutAction) -> KeyEquivalent? {
guard let shortcut = shortcutManager.shortcut(for: action),
shortcut.isEnabled else { return nil }
// Handle special keys
switch shortcut.keyCombination.key.lowercased() {
case "return", "enter": return .return
case "escape", "esc": return .escape
case "delete", "backspace": return .delete
case "tab": return .tab
case "space": return .space
case "up", "uparrow": return .upArrow
case "down", "downarrow": return .downArrow
case "left", "leftarrow": return .leftArrow
case "right", "rightarrow": return .rightArrow
case "home": return .home
case "end": return .end
case "pageup": return .pageUp
case "pagedown": return .pageDown
case "clear": return .clear
default:
// Handle single character keys
if shortcut.keyCombination.key.count == 1,
let char = shortcut.keyCombination.key.first {
return KeyEquivalent(char)
}
return nil
}
}
/// Returns the event modifiers for a given action
private func eventModifiers(for action: ShortcutAction) -> EventModifiers {
guard let shortcut = shortcutManager.shortcut(for: action),
shortcut.isEnabled else { return [] }
var modifiers: EventModifiers = []
if shortcut.keyCombination.modifiers.contains(.command) { modifiers.insert(.command) }
if shortcut.keyCombination.modifiers.contains(.shift) { modifiers.insert(.shift) }
if shortcut.keyCombination.modifiers.contains(.option) { modifiers.insert(.option) }
if shortcut.keyCombination.modifiers.contains(.control) { modifiers.insert(.control) }
return modifiers
}
/// View extension to apply dynamic keyboard shortcut if enabled
private func dynamicShortcut(_ action: ShortcutAction) -> some ViewModifier {
DynamicShortcutModifier(
keyEquivalent: keyEquivalent(for: action),
modifiers: eventModifiers(for: action)
)
}
var body: some Commands {
CommandGroup(replacing: .newItem) {}
CommandGroup(replacing: .windowList) {}
// Replace the native Settings menu item to open our custom sidebar settings window
CommandGroup(replacing: .appSettings) {
Button("Settings...") {
openWindow(id: "nook-settings")
}
.keyboardShortcut(",", modifiers: .command)
Button("Import from another Browser") {
browserManager.dialogManager.showDialog(
BrowserImportDialog(
onCancel: {
browserManager.dialogManager.closeDialog()
}
)
)
}
}
// Replace the standard Quit menu item to route through showQuitDialog(),
// which respects the "warn before quitting" setting
CommandGroup(replacing: .appTermination) {
Button("Quit Nook") {
browserManager.showQuitDialog()
}
.keyboardShortcut("q", modifiers: .command)
}
// App Menu Section (under Nook)
CommandGroup(after: .appInfo) {
Divider()
Button("Make Nook Default Browser") {
browserManager.setAsDefaultBrowser()
}
Button("Check for Updates...") {
appDelegate.updaterController.checkForUpdates(nil)
}
}
// Edit Section
CommandGroup(replacing: .undoRedo) {
Button("Undo Close Tab") {
browserManager.undoCloseTab()
}
.modifier(dynamicShortcut(.undoCloseTab))
Button("Reopen Closed Tab") {
browserManager.undoCloseTab()
}
.keyboardShortcut("t", modifiers: [.command, .shift])
}
// File Section
CommandGroup(after: .newItem) {
Button("New Tab") {
windowRegistry.activeWindow?.commandPalette?.open()
}
.modifier(dynamicShortcut(.newTab))
Button("New Window") {
browserManager.createNewWindow()
}
.modifier(dynamicShortcut(.newWindow))
Button("New Incognito Window") {
browserManager.createIncognitoWindow()
}
.keyboardShortcut("n", modifiers: [.command, .shift])
Divider()
Button("Open Command Bar") {
let currentURL = browserManager.currentTabForActiveWindow()?.url.absoluteString ?? ""
windowRegistry.activeWindow?.commandPalette?.open(prefill: currentURL, navigateCurrentTab: true)
}
.modifier(dynamicShortcut(.focusAddressBar))
.disabled(browserManager.currentTabForActiveWindow() == nil)
Button("Copy Current URL") {
browserManager.copyCurrentURL()
}
.modifier(dynamicShortcut(.copyCurrentURL))
.disabled(browserManager.currentTabForActiveWindow() == nil)
}
// Sidebar commands
CommandGroup(after: .sidebar) {
Button("Toggle Sidebar") {
browserManager.toggleSidebar()
}
.modifier(dynamicShortcut(.toggleSidebar))
Button("Toggle AI Assistant") {
browserManager.toggleAISidebar()
}
.modifier(dynamicShortcut(.toggleAIAssistant))
.disabled(!nookSettings.showAIAssistant)
Button("Toggle Picture in Picture") {
browserManager.requestPiPForCurrentTabInActiveWindow()
}
.modifier(dynamicShortcut(.togglePictureInPicture))
.disabled(
browserManager.currentTabForActiveWindow() == nil
|| !(browserManager.currentTabHasVideoContent()
|| browserManager.currentTabHasPiPActive())
)
Divider()
Button("Organize Tabs") {
let targetSpace =
windowRegistry.activeWindow?.currentSpaceId.flatMap { id in
browserManager.tabManager.spaces.first(where: { $0.id == id })
} ?? browserManager.tabManager.currentSpace
if let space = targetSpace {
Task {
await tabOrganizerManager.organizeTabs(
in: space,
using: browserManager.tabManager
)
}
}
}
.modifier(dynamicShortcut(.organizeTabs))
.disabled(
tabOrganizerManager.isOrganizing
|| browserManager.tabManager.currentSpace == nil
)
}
// View commands
CommandGroup(after: .windowSize) {
Button("Find in Page") {
browserManager.showFindBar()
}
.modifier(dynamicShortcut(.findInPage))
.disabled(browserManager.currentTabForActiveWindow() == nil)
Button("Reload Page") {
browserManager.refreshCurrentTabInActiveWindow()
}
.modifier(dynamicShortcut(.refresh))
.disabled(browserManager.currentTabForActiveWindow() == nil)
Divider()
Button("Zoom In") {
browserManager.zoomInCurrentTab()
}
.modifier(dynamicShortcut(.zoomIn))
.disabled(browserManager.currentTabForActiveWindow() == nil)
Button("Zoom Out") {
browserManager.zoomOutCurrentTab()
}
.modifier(dynamicShortcut(.zoomOut))
.disabled(browserManager.currentTabForActiveWindow() == nil)
Button("Actual Size") {
browserManager.resetZoomCurrentTab()
}
.modifier(dynamicShortcut(.actualSize))
.disabled(browserManager.currentTabForActiveWindow() == nil)
Divider()
Button("Hard Reload (Ignore Cache)") {
browserManager.hardReloadCurrentPage()
}
.modifier(dynamicShortcut(.hardReload))
.disabled(browserManager.currentTabForActiveWindow() == nil)
Divider()
Button("Web Inspector") {
browserManager.openWebInspector()
}
.modifier(dynamicShortcut(.openDevTools))
.disabled(browserManager.currentTabForActiveWindow() == nil)
Divider()
Button(browserManager.currentTabIsMuted() ? "Unmute Audio" : "Mute Audio") {
browserManager.toggleMuteCurrentTabInActiveWindow()
}
.modifier(dynamicShortcut(.muteUnmuteAudio))
.disabled(
browserManager.currentTabForActiveWindow() == nil
|| !browserManager.currentTabHasAudioContent())
}
Group {
CommandMenu("Privacy") {
Menu("Clear Cookies") {
Button("Clear Cookies for Current Site") {
browserManager.clearCurrentPageCookies()
}
.disabled(browserManager.currentTabForActiveWindow()?.url.host == nil)
Button("Clear Expired Cookies") {
browserManager.clearExpiredCookies()
}
Divider()
Button("Clear All Cookies") {
browserManager.clearAllCookies()
}
Divider()
Button("Clear Third-Party Cookies") {
browserManager.clearThirdPartyCookies()
}
Button("Clear High-Risk Cookies") {
browserManager.clearHighRiskCookies()
}
}
Menu("Clear Cache") {
Button("Clear Cache for Current Site") {
browserManager.clearCurrentPageCache()
}
.disabled(browserManager.currentTabForActiveWindow()?.url.host == nil)
Button("Clear Stale Cache") {
browserManager.clearStaleCache()
}
Button("Clear Disk Cache") {
browserManager.clearDiskCache()
}
Button("Clear Memory Cache") {
browserManager.clearMemoryCache()
}
Divider()
Button("Clear All Cache") {
browserManager.clearAllCache()
}
Divider()
Button("Clear Personal Data Cache") {
browserManager.clearPersonalDataCache()
}
Button("Clear Favicon Cache") {
browserManager.clearFaviconCache()
}
}
Divider()
Button("Privacy Cleanup") {
browserManager.performPrivacyCleanup()
}
Button("Clear Browsing History") {
browserManager.historyManager.clearHistory()
}
Button("Clear All Website Data") {
Task {
let dataStore = WKWebsiteDataStore.default()
let dataTypes = WKWebsiteDataStore.allWebsiteDataTypes()
await dataStore.removeData(ofTypes: dataTypes, modifiedSince: Date.distantPast)
}
}
}
if #available(macOS 15.5, *) {
CommandMenu("Extensions") {
Button("Toggle Extension Library") {
browserManager.toggleExtensionLibrary()
}
.keyboardShortcut("e", modifiers: [.command, .shift])
Divider()
Button("Install Extension...") {
browserManager.showExtensionInstallDialog()
}
.modifier(dynamicShortcut(.installExtension))
Button("Manage Extensions...") {
nookSettings.currentSettingsTab = .extensions
openWindow(id: "nook-settings")
}
Divider()
Button("Chrome Web Store") {
if let tab = browserManager.currentTabForActiveWindow() {
tab.loadURL("https://chromewebstore.google.com")
}
}
#if DEBUG
Divider()
Button("Open Popup Console") {
browserManager.extensionManager?.showPopupConsole()
}
#endif
}
}
CommandMenu("Appearance") {
Button("Customize Space Gradient...") {
browserManager.showGradientEditor()
}
.modifier(dynamicShortcut(.customizeSpaceGradient))
.disabled(browserManager.tabManager.currentSpace == nil)
Divider()
Button("Create Boosts") {
browserManager.showBoostsDialog()
}
.modifier(dynamicShortcut(.createBoost))
.disabled(browserManager.currentTabForActiveWindow() == nil)
}
}
}
}
// MARK: - Dynamic Shortcut Modifier
/// View modifier that conditionally applies a keyboard shortcut based on user preferences
struct DynamicShortcutModifier: ViewModifier {
let keyEquivalent: KeyEquivalent?
let modifiers: EventModifiers
func body(content: Content) -> some View {
if let keyEquivalent = keyEquivalent {
content.keyboardShortcut(keyEquivalent, modifiers: modifiers)
} else {
content
}
}
}
```
## /App/Window/WindowView.swift
```swift path="/App/Window/WindowView.swift"
//
// WindowView.swift
// Nook
//
// Created by Maciek Bagiński on 30/07/2025.
// Updated by Aether Aurelia on 15/11/2025.
//
import SwiftUI
/// Main window view that orchestrates the browser UI layout
struct WindowView: View {
@EnvironmentObject var browserManager: BrowserManager
@EnvironmentObject var tabManager: TabManager
@Environment(BrowserWindowState.self) private var windowState
@Environment(CommandPalette.self) private var commandPalette
@Environment(WindowRegistry.self) private var windowRegistry
@Environment(AIService.self) private var aiService
@Environment(TabOrganizerManager.self) private var tabOrganizerManager
@Environment(\.nookSettings) var nookSettings
@StateObject private var hoverSidebarManager = HoverSidebarManager()
@Environment(\.colorScheme) var colorScheme
var body: some View {
ZStack {
WindowBackground()
.contextMenu {
Button("Customize Space Gradient...") {
browserManager.showGradientEditor()
}
.disabled(tabManager.currentSpace == nil)
}
SidebarWebViewStack()
CommandPaletteView()
DialogView()
// Peek overlay for external link previews
PeekOverlayView()
// Find bar - always rendered (24/7), visibility controlled via opacity
FindBarView(findManager: browserManager.findManager)
.zIndex(10000)
}
// System notification toasts - top trailing corner
.overlay(alignment: .topTrailing) {
VStack(spacing: 8) {
// Profile switch toast
if windowState.isShowingProfileSwitchToast,
let toast = windowState.profileSwitchToast
{
ProfileSwitchToastView(toast: toast)
.environment(windowState)
.environmentObject(browserManager)
}
// Tab closure toast
if browserManager.showTabClosureToast && browserManager.tabClosureToastCount > 0 {
TabClosureToast()
.environmentObject(browserManager)
}
// Copy URL toast
if windowState.isShowingCopyURLToast {
CopyURLToast()
.environment(windowState)
}
// Shortcut conflict toast
if windowState.isShowingShortcutConflictToast,
let conflictInfo = windowState.shortcutConflictInfo
{
ShortcutConflictToast(conflictInfo: conflictInfo)
.environment(windowState)
}
}
.padding(10)
// Animate toast insertions/removals
.animation(.smooth(duration: 0.25), value: windowState.isShowingProfileSwitchToast)
.animation(.smooth(duration: 0.25), value: browserManager.showTabClosureToast)
.animation(.smooth(duration: 0.25), value: windowState.isShowingCopyURLToast)
.animation(.smooth(duration: 0.25), value: windowState.isShowingShortcutConflictToast)
}
// Zoom control popup - separate from system toasts
.overlay(alignment: .topTrailing) {
if browserManager.shouldShowZoomPopup {
ZoomPopupView(
zoomManager: browserManager.zoomManager,
onZoomIn: { browserManager.zoomInCurrentTab() },
onZoomOut: { browserManager.zoomOutCurrentTab() },
onZoomReset: { browserManager.resetZoomCurrentTab() },
onZoomPresetSelected: { zoomLevel in browserManager.applyZoomLevel(zoomLevel) },
onDismiss: { browserManager.shouldShowZoomPopup = false }
)
.transition(.scale(scale: 0.0, anchor: .top))
.animation(.spring(response: 0.5, dampingFraction: 0.8), value: browserManager.shouldShowZoomPopup)
.onTapGesture {
browserManager.shouldShowZoomPopup = false
}
.padding(10)
}
}
// Lifecycle management
.onAppear {
hoverSidebarManager.attach(browserManager: browserManager)
hoverSidebarManager.windowRegistry = windowRegistry
hoverSidebarManager.nookSettings = nookSettings
hoverSidebarManager.start()
windowState.hoverSidebarManager = hoverSidebarManager
}
.onDisappear {
hoverSidebarManager.stop()
}
// Handle shortcut conflict notifications
.onReceive(NotificationCenter.default.publisher(for: .shortcutConflictDetected)) { notification in
if let conflictInfo = notification.userInfo?["conflictInfo"] as? ShortcutConflictInfo,
conflictInfo.windowId == windowState.id {
windowState.shortcutConflictInfo = conflictInfo
windowState.isShowingShortcutConflictToast = true
// Auto-dismiss after 1.5 seconds (slightly longer than the 1s timeout)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
if windowState.shortcutConflictInfo?.timestamp == conflictInfo.timestamp {
windowState.isShowingShortcutConflictToast = false
}
}
}
}
// Handle shortcut conflict dismissal
.onReceive(NotificationCenter.default.publisher(for: .shortcutConflictDismissed)) { notification in
if let windowId = notification.userInfo?["windowId"] as? UUID,
windowId == windowState.id {
windowState.isShowingShortcutConflictToast = false
}
}
// Handle organize tabs notification from keyboard shortcut manager
.onReceive(NotificationCenter.default.publisher(for: .organizeTabsRequested)) { _ in
guard windowRegistry.activeWindow?.id == windowState.id else { return }
let targetSpace =
windowState.currentSpaceId.flatMap { id in
browserManager.tabManager.spaces.first(where: { $0.id == id })
} ?? browserManager.tabManager.currentSpace
if let space = targetSpace {
Task {
await tabOrganizerManager.organizeTabs(
in: space,
using: browserManager.tabManager
)
}
}
}
.environmentObject(browserManager)
.environmentObject(browserManager.gradientColorManager)
.environmentObject(browserManager.splitManager)
.environmentObject(hoverSidebarManager)
.preferredColorScheme(resolvedColorScheme)
}
private var resolvedColorScheme: ColorScheme? {
switch nookSettings.appearanceMode {
case .light: return .light
case .dark: return .dark
case .system: return nil // Follow system appearance
}
}
// MARK: - Layout Components
@ViewBuilder
private func WindowBackground() -> some View {
ZStack {
BlurEffectView(material: nookSettings.currentMaterial, state: .followsWindowActiveState)
.frame(maxWidth: .infinity, maxHeight: .infinity)
SpaceGradientBackgroundView()
}
.backgroundDraggable()
.environment(windowState)
}
@ViewBuilder
private func SidebarWebViewStack() -> some View {
let aiVisible = windowState.isSidebarAIChatVisible
let sidebarVisible = windowState.isSidebarVisible
let sidebarOnLeft = nookSettings.sidebarPosition == .left
// Fixed-order layout: [LeftSpacer] [WebContent] [RightSpacer]
// WebContent always stays in the middle with stable view identity.
// Spacer widths push content based on what's on each side.
let leftWidth: CGFloat = {
if sidebarOnLeft {
return sidebarVisible ? windowState.sidebarWidth : 0
} else {
return aiVisible ? windowState.aiSidebarWidth : 0
}
}()
let rightWidth: CGFloat = {
if sidebarOnLeft {
return aiVisible ? windowState.aiSidebarWidth : 0
} else {
return sidebarVisible ? windowState.sidebarWidth : 0
}
}()
// Determine edge padding: remove padding when sidebar/AI is visible on that side
let hasLeftContent = (sidebarOnLeft && sidebarVisible) || (!sidebarOnLeft && aiVisible)
let hasRightContent = (!sidebarOnLeft && sidebarVisible) || (sidebarOnLeft && aiVisible)
ZStack {
// When pinned: sidebar sits below web content (zIndex 0) so position
// swaps slide it under. When floating: above (zIndex 2) so it hovers.
UnifiedSidebar()
.zIndex(windowState.isSidebarVisible ? 0 : 2)
if aiVisible {
AISidebar()
.frame(maxWidth: .infinity, maxHeight: .infinity,
alignment: sidebarOnLeft ? .trailing : .leading)
.zIndex(0)
}
// Web content column — above pinned sidebars so they slide under it
HStack(spacing: 0) {
Color.clear
.frame(width: leftWidth)
.allowsHitTesting(false)
WebContent()
Color.clear
.frame(width: rightWidth)
.allowsHitTesting(false)
}
.padding(.leading, hasLeftContent ? 0 : 8)
.padding(.trailing, hasRightContent ? 0 : 8)
.zIndex(1)
}
.animation(.smooth(duration: 0.3), value: nookSettings.sidebarPosition)
}
/// Single sidebar instance rendered as an overlay — always the same view identity.
/// When floating, uses offset to slide in/out (preserving view identity without removal).
@ViewBuilder
private func UnifiedSidebar() -> some View {
let isPinned = windowState.isSidebarVisible
let isFloatingVisible = hoverSidebarManager.isOverlayVisible && !isPinned
let shouldShow = isPinned || isFloatingVisible
let onLeft = nookSettings.sidebarPosition == .left
// Slide offset: push sidebar fully off-screen in the appropriate direction
let slideOffset: CGFloat = {
if isPinned || isFloatingVisible { return 0 }
// Slide out to the left or right edge
return onLeft ? -(windowState.sidebarWidth + 14) : (windowState.sidebarWidth + 14)
}()
ZStack(alignment: onLeft ? .leading : .trailing) {
// Edge hover trigger zone — always present when sidebar is unpinned
if !isPinned {
Color.clear
.frame(width: hoverSidebarManager.triggerWidth)
.contentShape(Rectangle())
.onHover { isIn in
if isIn && !windowState.isSidebarVisible {
withAnimation(.easeInOut(duration: 0.12)) {
hoverSidebarManager.isOverlayVisible = true
}
}
NSCursor.arrow.set()
}
.frame(maxWidth: .infinity, maxHeight: .infinity,
alignment: onLeft ? .leading : .trailing)
}
// The single sidebar panel — slides in/out when floating, always visible when pinned
sidebarPanel(isPinned: isPinned)
.offset(x: isPinned ? 0 : slideOffset)
.allowsHitTesting(shouldShow)
}
.frame(maxWidth: .infinity, maxHeight: .infinity,
alignment: onLeft ? .leading : .trailing)
.animation(.easeInOut(duration: 0.15), value: isFloatingVisible)
.animation(.smooth(duration: 0.3), value: nookSettings.sidebarPosition)
// Briefly flash the floating sidebar on its new side after a position swap
.onChange(of: nookSettings.sidebarPosition) { _, _ in
guard !isPinned else { return }
hoverSidebarManager.peekOverlay(for: 2.0)
}
}
/// Wraps `SpacesSideBarView` with mode-dependent styling.
@ViewBuilder
private func sidebarPanel(isPinned: Bool) -> some View {
let cornerRadius: CGFloat = isPinned ? 0 : 12
let inset: CGFloat = isPinned ? 0 : 7
let resizeHandleAlignment: Alignment = nookSettings.sidebarPosition == .left ? .trailing : .leading
SpacesSideBarView()
.frame(width: windowState.sidebarWidth)
.frame(maxHeight: .infinity)
.overlay(alignment: resizeHandleAlignment) {
SidebarResizeView()
.frame(maxHeight: .infinity)
.environmentObject(browserManager)
.environment(windowState)
.zIndex(2000)
.opacity(isPinned ? 1 : 0)
.allowsHitTesting(isPinned)
}
.background {
if !isPinned {
SpaceGradientBackgroundView()
.environmentObject(browserManager)
.environmentObject(browserManager.gradientColorManager)
.environment(windowState)
.clipShape(.rect(cornerRadius: cornerRadius))
Rectangle()
.fill(Color.clear)
.universalGlassEffect(.regular.tint(Color(.windowBackgroundColor).opacity(0.35)), in: .rect(cornerRadius: cornerRadius))
}
}
.padding(nookSettings.sidebarPosition == .left ? .leading : .trailing, inset)
.padding(.vertical, inset)
.environmentObject(browserManager)
.environment(windowState)
.environment(commandPalette)
.environmentObject(browserManager.gradientColorManager)
}
@ViewBuilder
private func WebContent() -> some View {
let cornerRadius: CGFloat = {
if #available(macOS 26.0, *) {
return 8
} else {
return 8
}
}()
let hasTopBar = nookSettings.topBarAddressView
ZStack(alignment: .top) {
VStack(spacing: 0) {
if hasTopBar {
WebsiteLoadingIndicator()
.zIndex(3000)
TopBarView()
.environmentObject(browserManager)
.environment(windowState)
.zIndex(2500)
} else {
WebsiteLoadingIndicator()
}
WebsiteView()
.zIndex(2000)
}
// Shadow shape positioned behind both top bar and webview
// The webview will block the bottom shadow, leaving only top/left/right shadows visible
if hasTopBar {
UnevenRoundedRectangle(
topLeadingRadius: cornerRadius + 1,
bottomLeadingRadius: 0,
bottomTrailingRadius: 0,
topTrailingRadius: cornerRadius + 1,
style: .continuous
)
.frame(height: TopBarMetrics.height)
.frame(maxWidth: .infinity)
.offset(y: 8)
.shadow(color: Color.black.opacity(0.3), radius: 4, x: 0, y: 0)
.allowsHitTesting(false)
.zIndex(-1)
}
}
.overlay {
if aiService.isExecutingTools {
ToolExecutionGlowView()
.transition(.opacity.animation(.easeInOut(duration: 0.3)))
.allowsHitTesting(false)
}
}
.padding(.bottom, 8)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
@ViewBuilder
private func AISidebar() -> some View {
let handleAlignment: Alignment = nookSettings.sidebarPosition == .left ? .leading : .trailing
SidebarAIChat()
.frame(width: windowState.aiSidebarWidth)
.overlay(alignment: handleAlignment) {
AISidebarResizeView()
.frame(maxHeight: .infinity)
.environmentObject(browserManager)
.environment(windowState)
}
.transition(
.move(edge: nookSettings.sidebarPosition == .left ? .trailing : .leading)
.combined(with: .opacity)
)
.environmentObject(browserManager)
.environment(windowState)
.environment(nookSettings)
}
private func websiteColumnClipShape(cornerRadius: CGFloat, hasTopBar: Bool) -> AnyShape {
if hasTopBar {
return AnyShape(UnevenRoundedRectangle(
topLeadingRadius: 0,
bottomLeadingRadius: cornerRadius,
bottomTrailingRadius: cornerRadius,
topTrailingRadius: 0,
style: .continuous
))
} else {
return AnyShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
}
}
}
// MARK: - Profile Switch Toast View
private struct ProfileSwitchToastView: View {
let toast: BrowserManager.ProfileSwitchToast
@Environment(BrowserWindowState.self) private var windowState
@EnvironmentObject var browserManager: BrowserManager
var body: some View {
ToastView {
HStack {
Text("Switched to \(toast.toProfile.name)")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(.white)
Image(systemName: "person.crop.circle")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(.white)
.frame(width: 14, height: 14)
.padding(4)
.background(Color.white.opacity(0.2))
.clipShape(RoundedRectangle(cornerRadius: 6))
.overlay {
RoundedRectangle(cornerRadius: 6)
.stroke(.white.opacity(0.4), lineWidth: 1)
}
}
}
.transition(.toast)
.onAppear {
// Auto-dismiss after 2 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
browserManager.hideProfileSwitchToast(for: windowState)
}
}
.onTapGesture {
browserManager.hideProfileSwitchToast(for: windowState)
}
}
}
```
## /CLAUDE.md
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Nook is a fast, minimal macOS browser with sidebar-first design. Built with Swift 6, SwiftUI, and WKWebView. Requires macOS 15.5+ and Xcode to build.
## Build & Run
```bash
# Open in Xcode (single scheme: "Nook")
open Nook.xcodeproj
# Build from command line
xcodebuild -scheme Nook -configuration Debug -arch arm64 -derivedDataPath build
# Release build (universal)
xcodebuild -scheme Nook -configuration Release -arch arm64 -arch x86_64 -derivedDataPath build
```
You must set your personal Development Team in Xcode Signing settings to build locally.
**Test target**: `NookUITests` (UI tests only, run via Xcode or `xcodebuild test -scheme Nook`).
**No SPM**: All dependencies are embedded locally in `Nook/ThirdParty/` — no `swift package resolve` needed.
## Git Workflow
- **PRs must target `dev`**, not `main` (enforced by CI). Branch from `dev` for all work.
- Releases are tagged `v*` on `main`, triggering notarized DMG builds via GitHub Actions.
- AI assistance in contributions must be disclosed per CONTRIBUTING.md.
## Architecture
### Manager-Based Pattern
The app uses specialized **Managers** for each feature domain, coordinated through environment injection:
- **BrowserManager** (`Nook/Managers/BrowserManager/`) — Current "god object" (~2800 lines) that connects all managers. Being refactored toward independent, environment-injected managers.
- **TabManager** — Tab lifecycle, persistence (atomic snapshots via `PersistenceActor`), spaces, folders, pin management.
- **ProfileManager** — Persistent profiles (each with isolated `WKWebsiteDataStore`) and ephemeral/incognito profiles using `WKWebsiteDataStore.nonPersistent()`.
- **ExtensionManager** — `WKWebExtensionController` integration (macOS 15.4+). Singleton pattern.
- **WindowRegistry** — Multi-window state tracking. Single source of truth for all open windows.
- **WebViewCoordinator** — WebView pool management for multi-window tab display.
### State Management
- **`@Observable`** (Swift Observation): `Profile`, `Space`, `Tab`, `BrowserWindowState`, `WebViewCoordinator`, `WindowRegistry`
- **`@Published` / `ObservableObject`** (Combine): `BrowserManager`, `Tab` (dual — uses both patterns), `ExtensionManager`
- **SwiftData**: Persistence layer for `SpaceEntity`, `ProfileEntity`, `TabEntity`, `FolderEntity`, `HistoryEntity`, `ExtensionEntity`
- **All state is `@MainActor`** confined for thread safety.
### App Entry & Window Hierarchy
```
NookApp.swift — @main entry, creates WindowGroup scene
└─ ContentView.swift — Per-window container, registers with WindowRegistry
└─ WindowView — Main browser: Sidebar + WebsiteView + TopBar + StatusBar
```
**Environment injection flow**: `NookApp` creates `BrowserManager`, `WindowRegistry`, `WebViewCoordinator`, `NookSettingsService` and injects them as `@EnvironmentObject` / `@Environment`. Each window gets its own `BrowserWindowState`.
### Top-Level Modules
| Directory | Purpose |
|-----------|---------|
| `App/` | Entry point, AppDelegate, ContentView, window management, NookCommands |
| `Nook/Managers/` | ~30 feature managers (business logic) |
| `Nook/Models/` | Data models and entities |
| `Nook/Components/` | SwiftUI view components |
| `Nook/Protocols/` | Protocol definitions (e.g., `TabListDataSource`) |
| `Nook/Utils/` | Utilities, WebKit extensions, Metal shaders, debug tools |
| `Nook/ThirdParty/` | Embedded dependencies (BigUIPaging, HTSymbolHook, MuteableWKWebView, swift-atomics, swift-numerics) |
| `Settings/` | Settings module |
| `CommandPalette/` | Command palette UI |
| `UI/` | Shared UI components |
| `Navigation/` | Navigation models |
## Extension System (WKWebExtension, macOS 15.4+)
The extension system is the most complex subsystem. All extension code requires `@available(macOS 15.4, *)` guards. Content script injection specifically requires macOS 15.5+.
### Key Files
| File | Purpose |
|------|---------|
| `Nook/Managers/ExtensionManager/ExtensionManager.swift` | Core manager (~3800 lines), singleton, handles full lifecycle |
| `Nook/Managers/ExtensionManager/ExtensionBridge.swift` | `WKWebExtensionTab` / `WKWebExtensionWindow` protocol adapters |
| `Nook/Models/Extension/ExtensionModels.swift` | `ExtensionEntity` (SwiftData) + `InstalledExtension` runtime model |
| `Nook/Models/BrowserConfig/BrowserConfig.swift` | Shared `WKWebViewConfiguration` factory — extension controller lives here |
| `Nook/Components/Extensions/ExtensionActionView.swift` | Toolbar buttons, popup anchor positioning |
| `Nook/Components/Extensions/ExtensionPermissionView.swift` | Permission grant/deny dialogs |
| `Nook/Components/Extensions/PopupConsoleWindow.swift` | Debug console for extension popups |
| `Nook/Utils/ExtensionUtils.swift` | Manifest validation, version checks |
### Critical: WebView Config Derivation
Tab webview configs **MUST** derive from the same `WKWebViewConfiguration` that the `WKWebExtensionController` was configured with (via `.copy()`). Creating a fresh `WKWebViewConfiguration()` and just setting `webExtensionController` on it is **NOT** enough — WebKit needs the config to share the same process pool / internal state. See `BrowserConfig.swift:webViewConfiguration(for:)`.
The chain: `BrowserConfig.shared.webViewConfiguration` (base) → ExtensionManager sets `.webExtensionController` on it → `webViewConfiguration(for: profile)` calls `.copy()` + sets profile-specific data store → tab gets that derived config.
### Installation Flow
Supported formats: `.zip`, `.appex` (Safari extension bundle), `.app` (scans `Contents/PlugIns/` for `.appex`), bare directories.
1. Extract/resolve source to get `manifest.json`
2. `ExtensionUtils.validateManifest()` — checks required fields
3. MV3 validation — verifies `background.service_worker` exists
4. `patchManifestForWebKit()` — patches world isolation, injects externally_connectable bridge
5. Create temporary `WKWebExtension` to get `uniqueIdentifier`
6. Move to `~/Library/Application Support/Nook/Extensions/{extensionId}/`
7. Grant ALL manifest permissions + host_permissions at install time (Chrome-like model)
8. Load background service worker immediately
9. Extract icon (128/64/48/32/16px from manifest icons), resolve `__MSG_key__` locale strings
### Externally Connectable Bridge
**Problem**: Pages like `account.proton.me` call `browser.runtime.sendMessage(SAFARI_EXT_ID, msg)` but Safari extension IDs don't match WKWebExtension IDs.
**Solution** (`setupExternallyConnectableBridge`): Two-layer bridge injected as content scripts:
- **PAGE world script**: Wraps `browser.runtime.sendMessage()` and `.connect()`, relays via `window.postMessage()` to the isolated world
- **ISOLATED world script** (`nook_bridge.js`): Receives postMessages, calls the real `browser.runtime.sendMessage()`, forwards responses back
`patchManifestForWebKit()` auto-injects the bridge content script entry into `manifest.json` when `externally_connectable` is present.
### Extension Bridge (ExtensionBridge.swift)
- **`ExtensionWindowAdapter`** implements `WKWebExtensionWindow`: exposes active tab, tab list, window state (minimized/maximized/fullscreen), focus/close operations, privacy status.
- **`ExtensionTabAdapter`** implements `WKWebExtensionTab`: exposes url, title, selection state, loading, pinned, muted, audio state. Returns `tab.assignedWebView` (does NOT trigger lazy init). Stable adapters cached in `tabAdapters` dictionary by `Tab.id`.
### Tab ↔ Extension Notification
Tab notifies the extension system after webview creation:
```
Tab.setupWebView()
→ ExtensionManager.shared.notifyTabOpened(tab) // controller.didOpenTab(adapter)
→ If active: notifyTabActivated() // controller.didActivateTab(adapter)
→ tab.didNotifyOpenToExtensions = true
```
### Permission Model
- **Install-time**: ALL manifest `permissions` + `host_permissions` auto-granted (matching Chrome behavior)
- **On load (existing extensions)**: Grants both requested + optional permissions/match patterns, enables Web Inspector
- **Runtime** (`chrome.permissions.request`): Triggers `ExtensionPermissionView` dialog via delegate
### Storage Isolation
- Extensions installed globally (`~/Library/Application Support/Nook/Extensions/{id}/`)
- Runtime storage (`chrome.storage.*`, cookies, indexedDB) isolated per profile via separate `WKWebsiteDataStore`
- On profile switch: `controller.configuration.defaultWebsiteDataStore` updated to profile-specific store
### Native Messaging
Looks up host manifests in order: `~/Library/Application Support/Nook/NativeMessagingHosts/`, then Chrome, Chromium, Edge, Brave, Mozilla paths. Protocol: 4-byte native-endian length prefix + JSON. Supports single-shot (5s timeout) and long-lived `MessagePort` connections.
### Delegate Methods (WKWebExtensionControllerDelegate)
Key delegate implementations in ExtensionManager:
- **Action popup**: Grants permissions, wakes MV3 service worker, positions popover via registered anchor views
- **Open tab/window**: Creates tabs for extension pages, handles OAuth popup flows
- **Options page**: Resolves URL from manifest (`options_ui.page` / `options_page`), opens in separate NSWindow with extension's webViewConfiguration. Includes path traversal protection.
- **Permission prompts**: `promptForPermissions()` and `promptForPermissionToAccess()` for runtime permission requests
### Diagnostics
- `probeBackgroundHealth()` — Runs at +3s and +8s after background load; uses KVC to access `_backgroundWebView` and evaluates capability probe (available APIs, permissions, errors)
- `diagnoseExtensionState()` — Full diagnostic on content scripts + messaging per extension
- Memory debug logging uses `🔍 [MEMDEBUG]` prefix
## Key Patterns
- **Lazy WebView**: `Tab.webView` is lazily initialized on first access. Tabs can exist without a loaded webview to save memory.
- **Multi-window webviews**: Same tab shown in multiple windows gets separate webview instances managed by `WebViewCoordinator`. Primary window owns the "real" webview; others get clones.
- **Profile data isolation**: Each `Profile` owns a unique `WKWebsiteDataStore`. Ephemeral profiles use non-persistent stores that are destroyed on window close.
- **Atomic persistence**: `TabManager` uses a Swift `actor` (`PersistenceActor`) for coalesced, atomic snapshot writes with backup recovery.
## /CODE_OF_CONDUCT.md
# Code of Conduct
## Our Commitment
We are committed to providing a welcoming and inclusive environment for all contributors to Nook, regardless of experience level, background, or identity.
Nook is built by volunteers who contribute their time and expertise out of passion for creating great software. We appreciate everyone who takes time to contribute to making Nook better.
## Expected Behavior
- Be respectful, welcoming, and considerate in all interactions
- Discuss ideas, give/receive constructive criticism gracefully
- Focus on what's best for the community and project
- Show empathy toward other community members
- Help create a positive environment for learning and collaboration
## Unacceptable Behavior
- Harassment, discrimination, or intimidation of any kind
- Offensive, derogatory, or discriminatory comments
- Personal attacks or trolling
- Publishing others' private information without permission
- Spam or off-topic discussions
- Any conduct that would be inappropriate in a professional setting
## Scope
This Code of Conduct applies to all project spaces, including:
- GitHub repository (issues, PRs, discussions)
- Discord community
- Any other official Nook communication channels
## Enforcement
Project maintainers are responsible for clarifying standards and will take appropriate action in response to violations. This may include:
- Warning the individual
- Temporary or permanent ban from project spaces (GitHub, Discord, etc.)
- Reporting to appropriate platforms (GitHub, Discord, etc.)
## Reporting
If you experience or witness unacceptable behavior, please report it by:
- Opening a GitHub issue (for public matters)
- Contacting maintainers directly on Discord
All reports will be handled confidentially and reviewed promptly.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.
---
By participating in the Nook community, you agree to abide by this Code of Conduct.
## /CONTRIBUTING.md
# Contributing to Nook
Thank you for your interest in contributing to Nook! This document provides guidelines and instructions for contributing to the project.
## AI Assistance Notice
If you are using any kind of AI assistance to contribute, this must be disclosed in your pull request, commit, or issue, including the extent to which AI assistance was used (e.g. docs only vs. code generation). If PR responses, commit messages, or comments are being generated by an AI, please disclose that as well.
As a small exception, trivial tab-completion doesn't need to be disclosed, so long as it is limited to single keywords or short phrases.
When using AI assistance, we expect contributors to understand the code that is produced and be able to answer critical questions about it. It isn't a maintainer's job to review a PR so broken that it requires significant rework to be acceptable.
Please be respectful to maintainers and disclose AI assistance.
## 🚀 Development Setup
1. **Clone the repository**
```bash
git clone https://github.com/yourusername/Nook.git
cd Nook
```
2. **Switch to the dev branch**
```bash
git checkout dev
```
3. **Open in Xcode**
```bash
open Nook.xcodeproj
```
4. **Build and run**
- Select your target device/simulator
- Press `Cmd + R` to build and run
## 📝 Code Style & Standards
### Conventions
- Follow existing SwiftUI/AppKit patterns in the codebase
- Use the established file organization structure
- Prefer modern Swift/SwiftUI APIs when possible
### Compatibility & OS Versions
- **Maintain backward compatibility**: The current minimum deployment target is macOS 15.5
- **New features requiring newer OS versions** should use `@available` or `#if available` checks to preserve compatibility
- **Discuss before raising minimum version**: If a feature would significantly benefit from raising the minimum OS version, open an issue to discuss the trade-offs before implementation
## 📋 Pull Request Process
### Before You Start
- **Check for duplicates**: Search existing issues and PRs to ensure no one is already working on the same feature or bug
- **Avoid duplicate work**: Comment on relevant issues to indicate you're working on them
- **For major changes**, consider opening an issue first to discuss the approach
### Creating Pull Requests
**IMPORTANT**: All development work must be done on the `dev` branch:
1. **Create your branch from `dev`**, not `main`:
```bash
git checkout dev
git pull origin dev
git checkout -b feature/your-feature-name
```
2. **Make your changes** and commit them with clear, descriptive messages
3. **Push to your fork**:
```bash
git push origin feature/your-feature-name
```
4. **Open a Pull Request to the `dev` branch**, not `main`
- Provide a clear description of the changes
- Reference any related issues
- Include screenshots or videos for UI changes
- Disclose any AI assistance used (see AI Assistance Notice above)
5. **Respond to review feedback** promptly and constructively
## 🐛 Issue Reporting
### Bug Reports
Be descriptive and thorough. Include:
- **macOS version**
- **Nook version**
- **Clear steps to reproduce the issue**
- **Expected vs actual behavior**
- **Screenshots if applicable**
- **Relevant console/error messages**
### Feature Requests
Provide detailed context. Include:
- **Clear description of the feature and its use case**
- **Explain how it fits with Nook's goals** (fast, secure, beautiful)
- **Consider implementation complexity and user impact**
- **Check that similar functionality doesn't already exist**
## 💬 Community Guidelines
- Be respectful and constructive
- Help maintain a welcoming environment for all contributors
- Focus on the code and ideas, not individuals
## License
By contributing to Nook, you agree that your contributions will be licensed under the GPL-3.0 License.
## /CommandPalette/CommandPalette Accessories/CommandPaletteSuggestionView.swift
```swift path="/CommandPalette/CommandPalette Accessories/CommandPaletteSuggestionView.swift"
//
// CommandPaletteSuggestionView.swift
// Nook
//
// Created by Maciek Bagiński on 31/07/2025.
//
import SwiftUI
import FaviconFinder
struct CommandPaletteSuggestionView: View {
var favicon: SwiftUI.Image
var text: String
var secondaryText: String? = nil
var isTabSuggestion: Bool = false
var isSelected: Bool = false
var historyURL: URL? = nil
@State private var isHovered: Bool = false
@State private var resolvedFavicon: SwiftUI.Image? = nil
var body: some View {
HStack(alignment: .center,spacing: 12) {
(resolvedFavicon ?? favicon)
.resizable()
.scaledToFit()
.frame(width: 14, height: 14)
.foregroundStyle(.white.opacity(0.2))
if let secondary = secondaryText, !secondary.isEmpty {
HStack(spacing: 6) {
Text(text)
.font(.system(size: 14, weight: .medium))
.lineLimit(1)
.truncationMode(.tail)
Text("-")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(.white.opacity(0.35))
Text(secondary)
.font(.system(size: 14, weight: .medium))
.foregroundStyle(.white.opacity(0.5))
.lineLimit(1)
.truncationMode(.tail)
}
} else {
Text(text)
.font(.system(size: 14, weight: .medium))
.lineLimit(1)
.truncationMode(.tail)
}
Spacer()
if isTabSuggestion {
HStack(spacing: 6) {
Text("Switch to Tab")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(.white.opacity(0.5))
Image(systemName: "arrow.right")
.font(.system(size: 8, weight: .medium))
.foregroundStyle(.white.opacity(0.5))
}
}
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.frame(maxWidth: .infinity)
.background(backgroundColor)
.clipShape(RoundedRectangle(cornerRadius: 8))
.onHoverTracking { hovering in
withAnimation(.easeInOut(duration: 0.15)) {
isHovered = hovering
}
}
.onAppear {
guard let url = historyURL else { return }
Task { await fetchFavicon(for: url) }
}
}
private var backgroundColor: Color {
if isSelected {
return Color.white.opacity(0.25)
} else if isHovered {
return Color.white.opacity(0.15)
} else {
return Color.clear
}
}
// MARK: - Favicon Fetching (for history items)
private func fetchFavicon(for url: URL) async {
let defaultFavicon = SwiftUI.Image(systemName: "globe")
// Skip favicon fetching for non-web schemes
guard url.scheme == "http" || url.scheme == "https", url.host != nil else {
await MainActor.run { self.resolvedFavicon = defaultFavicon }
return
}
// Check cache first
let cacheKey = url.host ?? url.absoluteString
if let cachedFavicon = Tab.getCachedFavicon(for: cacheKey) {
await MainActor.run { self.resolvedFavicon = cachedFavicon }
return
}
do {
let favicon = try await FaviconFinder(url: url)
.fetchFaviconURLs()
.download()
.largest()
if let faviconImage = favicon.image {
let nsImage = faviconImage.image
let swiftUIImage = SwiftUI.Image(nsImage: nsImage)
// Cache the favicon
Tab.cacheFavicon(swiftUIImage, for: cacheKey)
await MainActor.run { self.resolvedFavicon = swiftUIImage }
} else {
await MainActor.run { self.resolvedFavicon = defaultFavicon }
}
} catch {
await MainActor.run { self.resolvedFavicon = defaultFavicon }
}
}
}
```
## /CommandPalette/CommandPalette Accessories/GenericSuggestionItem.swift
```swift path="/CommandPalette/CommandPalette Accessories/GenericSuggestionItem.swift"
//
// GenericSuggestionItem.swift
// Nook
//
// Created by Maciek Bagiński on 18/08/2025.
//
import SwiftUI
struct GenericSuggestionItem: View {
let icon: Image
let text: String
var isSelected: Bool = false
@Environment(\.colorScheme) var colorScheme
var body: some View {
let isDark = colorScheme == .dark
HStack(alignment: .center, spacing: 12) {
ZStack {
icon
.resizable()
.scaledToFit()
.frame(width: 14, height: 14)
.foregroundStyle(isSelected ? .white : isDark ? .white.opacity(0.7) : .black.opacity(0.7))
}
.frame(width: 24, height: 24)
.clipShape(RoundedRectangle(cornerRadius: 4))
Text(text)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(isSelected ? .white : isDark ? .white.opacity(0.6) : .black.opacity(0.8))
.lineLimit(1)
.truncationMode(.tail)
Spacer()
}
.frame(maxWidth: .infinity)
}
}
```
## /CommandPalette/CommandPalette Accessories/HistorySuggestionItem.swift
```swift path="/CommandPalette/CommandPalette Accessories/HistorySuggestionItem.swift"
//
// HistorySuggestionItem.swift
// Nook
//
// Created by Maciek Bagiński on 18/08/2025.
//
import SwiftUI
import FaviconFinder
struct HistorySuggestionItem: View {
let entry: HistoryEntry
var isSelected: Bool = false
@State private var isHovered: Bool = false
@State private var resolvedFavicon: SwiftUI.Image? = nil
@Environment(\.colorScheme) var colorScheme
// Color configuration
private var colors: ColorConfig {
ColorConfig(
isDark: colorScheme == .dark,
isSelected: isSelected,
isHovered: isHovered
)
}
var body: some View {
HStack(alignment: .center, spacing: 9) {
ZStack {
(resolvedFavicon ?? Image(systemName: "globe"))
.resizable()
.scaledToFit()
.foregroundStyle(colors.faviconColor)
.frame(width: 14, height: 14)
}
.frame(width: 24, height: 24)
.background(colors.faviconBackground)
.clipShape(RoundedRectangle(cornerRadius: 4))
HStack(spacing: 4) {
Text(entry.displayTitle)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(colors.titleColor)
.lineLimit(1)
.truncationMode(.tail)
Text("-")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(colors.urlColor)
Text(entry.displayURL)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(colors.urlColor)
.lineLimit(1)
.truncationMode(.tail)
}
Spacer()
}
.frame(maxWidth: .infinity)
.onHoverTracking { hovering in
withAnimation(.easeInOut(duration: 0.15)) {
isHovered = hovering
}
}
.onAppear {
Task { await fetchFavicon(for: entry.url) }
}
}
private func fetchFavicon(for url: URL) async {
let defaultFavicon = SwiftUI.Image(systemName: "globe")
guard url.scheme == "http" || url.scheme == "https", url.host != nil else {
await MainActor.run { self.resolvedFavicon = defaultFavicon }
return
}
let cacheKey = url.host ?? url.absoluteString
if let cachedFavicon = Tab.getCachedFavicon(for: cacheKey) {
await MainActor.run { self.resolvedFavicon = cachedFavicon }
return
}
do {
let favicon = try await FaviconFinder(url: url)
.fetchFaviconURLs()
.download()
.largest()
if let faviconImage = favicon.image {
let nsImage = faviconImage.image
let swiftUIImage = SwiftUI.Image(nsImage: nsImage)
Tab.cacheFavicon(swiftUIImage, for: cacheKey)
await MainActor.run { self.resolvedFavicon = swiftUIImage }
} else {
await MainActor.run { self.resolvedFavicon = defaultFavicon }
}
} catch {
await MainActor.run { self.resolvedFavicon = defaultFavicon }
}
}
}
// MARK: - Colors simplified
private struct ColorConfig {
let isDark: Bool
let isSelected: Bool
let isHovered: Bool
var titleColor: Color {
if isSelected {
return .white
}
return isDark ? .white : .black
}
var urlColor: Color {
if isSelected {
return .white.opacity(0.5)
}
return isDark ? .white.opacity(0.3) : .black.opacity(0.3)
}
var faviconColor: Color {
return .white.opacity(0.5)
}
var faviconBackground: Color {
return isSelected ? .white : .clear
}
}
```
## /CommandPalette/CommandPalette Accessories/TabSuggestionItem.swift
```swift path="/CommandPalette/CommandPalette Accessories/TabSuggestionItem.swift"
//
// TabSuggestionItem.swift
// Nook
//
// Created by Maciek Bagiński on 18/08/2025.
//
import SwiftUI
struct TabSuggestionItem: View {
let tab: Tab
var isSelected: Bool = false
@State private var isHovered: Bool = false
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var gradientColorManager: GradientColorManager
var body: some View {
let isDark = colorScheme == .dark
HStack(alignment: .center, spacing: 0) {
HStack(spacing: 9) {
ZStack {
tab.favicon
.resizable()
.scaledToFit()
.foregroundStyle(.white.opacity(0.5))
.frame(width: 14, height: 14)
}
.frame(width: 24, height: 24)
.background(isSelected ? .white : .clear)
.clipShape(
RoundedRectangle(cornerRadius: 4)
)
Text(tab.name)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(isSelected ? .white : isDark ? .white.opacity(0.6) : .black.opacity(0.8))
.lineLimit(1)
.truncationMode(.tail)
}
Spacer()
HStack(spacing: 10) {
Text("Switch to Tab")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(isSelected ? .white : isDark ? .white.opacity(0.3) : .black.opacity(0.3))
ZStack {
Image(systemName: "arrow.right")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(isSelected ? gradientColorManager.primaryColor : isDark ? .white.opacity(0.5) : .black.opacity(0.5))
.frame(width: 16, height: 16)
}
.frame(width: 24, height: 24)
.background(isSelected ? .white : isDark ? .white.opacity(0.05) : .black.opacity(0.05))
.clipShape(RoundedRectangle(cornerRadius: 4))
}
}
.frame(maxWidth: .infinity)
.onHoverTracking { hovering in
withAnimation(.easeInOut(duration: 0.15)) {
isHovered = hovering
}
}
}
}
```
## /CommandPalette/CommandPalette.swift
```swift path="/CommandPalette/CommandPalette.swift"
//
// CommandPalette.swift
// Nook
//
// Per-window command palette state and actions
//
import Foundation
import SwiftUI
@MainActor
@Observable
class CommandPalette {
/// Whether the command palette is visible
var isVisible: Bool = false
/// Text to prefill in the command palette
var prefilledText: String = ""
/// Whether pressing Return should navigate the current tab (vs creating new tab)
var shouldNavigateCurrentTab: Bool = false
// MARK: - Actions
/// Open the command palette with optional prefill text
func open(prefill: String = "", navigateCurrentTab: Bool = false) {
prefilledText = prefill
self.shouldNavigateCurrentTab = navigateCurrentTab
DispatchQueue.main.async {
self.isVisible = true
}
}
/// Open the command palette with the current tab's URL
func openWithCurrentURL(_ url: URL) {
open(prefill: url.absoluteString, navigateCurrentTab: true)
}
/// Close the command palette
func close() {
isVisible = false
shouldNavigateCurrentTab = false
prefilledText = ""
}
/// Toggle the command palette visibility
func toggle() {
if isVisible {
close()
} else {
open()
}
}
}
```
## /CommandPalette/CommandPaletteView.swift
```swift path="/CommandPalette/CommandPaletteView.swift"
//
// CommandPaletteView.swift
// Nook
//
// Created by Maciek Bagiński on 28/07/2025.
//
import AppKit
import SwiftUI
import Garnish
struct CommandPaletteView: View {
@EnvironmentObject var browserManager: BrowserManager
@Environment(BrowserWindowState.self) private var windowState
@Environment(CommandPalette.self) private var commandPalette
@EnvironmentObject var gradientColorManager: GradientColorManager
@State private var searchManager = SearchManager()
@Environment(\.colorScheme) var colorScheme
@Environment(\.nookSettings) var nookSettings
@FocusState private var isSearchFocused: Bool
@State private var text: String = ""
@State private var selectedSuggestionIndex: Int = -1
@State private var hoveredSuggestionIndex: Int? = nil
@State private var userTypedText: String = ""
@State private var isNavigatingSuggestion: Bool = false
@State private var activeSiteSearch: SiteSearchEntry? = nil
private var siteSearchMatch: SiteSearchEntry? {
guard activeSiteSearch == nil else { return nil }
return SiteSearchEntry.match(for: text, in: nookSettings.siteSearchEntries)
}
private var visibleSuggestions: [SearchManager.SearchSuggestion] {
if activeSiteSearch != nil {
return searchManager.suggestions.filter {
if case .search = $0.type { return true }
return false
}
}
return searchManager.suggestions
}
let commandPaletteWidth: CGFloat = 765
let commandPaletteHorizontalPadding: CGFloat = 10
/// Active window width
private var currentWindowWidth: CGFloat {
return NSApplication.shared.keyWindow?.frame.width ?? 0
}
/// Check if the command palette fits in the window
private var isWindowTooNarrow: Bool {
let requiredWidth = commandPaletteWidth + (commandPaletteHorizontalPadding * 2)
return currentWindowWidth <= requiredWidth
}
/// Caclulate the correct command palette width
private var effectiveCommandPaletteWidth: CGFloat {
if isWindowTooNarrow {
return max(200, currentWindowWidth - (commandPaletteHorizontalPadding * 2))
} else {
return commandPaletteWidth
}
}
var body: some View {
let isDark = colorScheme == .dark
let isVisible = commandPalette.isVisible
let textFieldColor: Color = text.isEmpty
? (isDark ? .white.opacity(0.25) : .black.opacity(0.25))
: (isDark ? .white.opacity(0.9) : .black.opacity(0.9))
return ZStack {
Color.clear
.ignoresSafeArea()
.contentShape(Rectangle())
.onTapGesture {
commandPalette.close()
}
.gesture(WindowDragGesture())
VStack {
Spacer()
HStack {
Spacer()
VStack {
VStack(alignment: .center,spacing: 6) {
HStack(spacing: 15) {
Image(
systemName: activeSiteSearch != nil
? "magnifyingglass"
: isLikelyURL(text)
? "globe" : "magnifyingglass"
)
.id(activeSiteSearch != nil ? "magnifyingglass" : isLikelyURL(text) ? "globe" : "magnifyingglass")
.transition(.blur(intensity: 2, scale: 0.6).animation(.smooth(duration: 0.3)))
.font(.system(size: 14, weight: .regular))
.foregroundStyle(isDark ? .white : .black)
.frame(width: 15)
if let site = activeSiteSearch {
Text(site.name)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(Garnish.contrastingShade(of: site.color, targetRatio: 4.5, blendStyle: .strong) ?? .white)
.padding(.horizontal, 10)
.padding(.vertical, 4)
.background(site.color)
.clipShape(Capsule())
.transition(
.blur(intensity: 8, scale: 0.6)
.animation(.spring(response: 0.35, dampingFraction: 0.75))
)
}
ZStack(alignment: .trailing) {
TextField(
activeSiteSearch != nil
? "Search \(activeSiteSearch!.name)..."
: "Search or enter URL...",
text: $text
)
.textFieldStyle(.plain)
.font(.system(size: 18, weight: .medium))
.foregroundColor(textFieldColor)
.tint(gradientColorManager.primaryColor)
.overlay(alignment: .leading) {
if let suffix = inlineCompletionSuffix {
(Text(text).foregroundColor(.clear) + Text(suffix).foregroundColor(isDark ? .white.opacity(0.25) : .black.opacity(0.25)))
.font(.system(size: 18, weight: .medium))
.lineLimit(1)
.allowsHitTesting(false)
}
}
.focused($isSearchFocused)
.onKeyPress(.tab) {
if let match = siteSearchMatch, activeSiteSearch == nil {
withAnimation(.spring(response: 0.35, dampingFraction: 0.75)) {
activeSiteSearch = match
}
text = ""
return .handled
}
// Tab accepts the top suggestion when nothing is selected
if selectedSuggestionIndex < 0 && !visibleSuggestions.isEmpty {
selectedSuggestionIndex = 0
isNavigatingSuggestion = true
text = displayTextForSuggestion(visibleSuggestions[0])
return .handled
}
// Tab with a selected suggestion = accept it (same as Enter)
if selectedSuggestionIndex >= 0 && selectedSuggestionIndex < visibleSuggestions.count {
let suggestion = visibleSuggestions[selectedSuggestionIndex]
selectSuggestion(suggestion)
return .handled
}
return .ignored
}
.onKeyPress(.return) {
handleReturn()
return .handled
}
.onKeyPress(.upArrow) {
navigateSuggestions(direction: -1)
return .handled
}
.onKeyPress(.downArrow) {
navigateSuggestions(direction: 1)
return .handled
}
.onKeyPress(.escape) {
if activeSiteSearch != nil {
withAnimation(.smooth(duration: 0.25)) {
activeSiteSearch = nil
}
return .handled
}
commandPalette.close()
return .handled
}
.onKeyPress(.delete) {
if activeSiteSearch != nil && text.isEmpty {
withAnimation(.smooth(duration: 0.25)) {
activeSiteSearch = nil
}
return .handled
}
return .ignored
}
.onKeyPress(characters: CharacterSet(charactersIn: "\u{7F}")) { _ in
if activeSiteSearch != nil && text.isEmpty {
withAnimation(.smooth(duration: 0.25)) {
activeSiteSearch = nil
}
return .handled
}
return .ignored
}
.onChange(of: text) { _, newValue in
if isNavigatingSuggestion {
isNavigatingSuggestion = false
return
}
userTypedText = newValue
searchManager.searchSuggestions(
for: newValue
)
selectedSuggestionIndex = -1
}
if activeSiteSearch == nil, let match = siteSearchMatch {
HStack(spacing: 6) {
Text("Search \(match.name)")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(isDark ? .white.opacity(0.3) : .black.opacity(0.3))
Text("Tab")
.font(.system(size: 11, weight: .medium))
.foregroundStyle(isDark ? .white.opacity(0.4) : .black.opacity(0.4))
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(
RoundedRectangle(cornerRadius: 4)
.fill(isDark ? .white.opacity(0.1) : .black.opacity(0.08))
)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(isDark ? .white.opacity(0.15) : .black.opacity(0.12), lineWidth: 0.5)
)
}
.allowsHitTesting(false)
.transition(
.blur(intensity: 4, scale: 0.92)
.animation(.smooth(duration: 0.3))
)
}
}
}
.animation(.spring(response: 0.35, dampingFraction: 0.75), value: activeSiteSearch != nil)
.padding(.vertical, 8)
.padding(.horizontal, 8)
if !visibleSuggestions.isEmpty {
RoundedRectangle(cornerRadius: 100)
.fill(
isDark
? Color.white.opacity(0.4)
: Color.black.opacity(0.4)
)
.frame(height: 0.5)
.frame(maxWidth: .infinity)
}
if !visibleSuggestions.isEmpty {
CommandPaletteSuggestionsListView(
suggestions: visibleSuggestions,
selectedIndex: $selectedSuggestionIndex,
hoveredIndex: $hoveredSuggestionIndex,
onSelect: { suggestion in
selectSuggestion(suggestion)
}
)
}
}
.padding(10)
.frame(maxWidth: .infinity)
.frame(width: effectiveCommandPaletteWidth)
.background(Color(.windowBackgroundColor).opacity(0.35))
.clipShape(.rect(cornerRadius: 26))
.nookGlassEffect(in: .rect(cornerRadius: 26))
.animation(
.easeInOut(duration: 0.15),
value: searchManager.suggestions.count
)
Spacer()
}
.frame(
width: effectiveCommandPaletteWidth,
height: 328
)
Spacer()
}
Spacer()
}
}
.allowsHitTesting(isVisible)
.opacity(isVisible ? 1.0 : 0.0)
.onChange(of: commandPalette.isVisible) { _, newVisible in
if newVisible {
searchManager.setTabManager(browserManager.tabManager)
searchManager.setHistoryManager(browserManager.historyManager)
searchManager.updateProfileContext()
text = commandPalette.prefilledText
userTypedText = commandPalette.prefilledText
DispatchQueue.main.async {
isSearchFocused = true
DispatchQueue.main.async {
NSApplication.shared.sendAction(
#selector(NSText.selectAll(_:)),
to: nil,
from: nil
)
}
}
} else {
isSearchFocused = false
searchManager.clearSuggestions()
text = ""
userTypedText = ""
activeSiteSearch = nil
selectedSuggestionIndex = -1
}
}
.onChange(of: browserManager.currentProfile?.id) { _, _ in
if commandPalette.isVisible {
searchManager.updateProfileContext()
searchManager.clearSuggestions()
}
}
.onChange(of: searchManager.suggestions.count) { _, _ in
let count = visibleSuggestions.count
if count == 0 {
selectedSuggestionIndex = -1
} else if selectedSuggestionIndex >= count {
selectedSuggestionIndex = count - 1
}
}
.animation(.easeInOut(duration: 0.15), value: selectedSuggestionIndex)
.onChange(of: commandPalette.prefilledText) { _, newValue in
if isVisible {
text = newValue
userTypedText = newValue
DispatchQueue.main.async {
isSearchFocused = true
}
}
}
}
private func isEmoji(_ string: String) -> Bool {
return string.unicodeScalars.contains { scalar in
(scalar.value >= 0x1F300 && scalar.value <= 0x1F9FF)
|| (scalar.value >= 0x2600 && scalar.value <= 0x26FF)
|| (scalar.value >= 0x2700 && scalar.value <= 0x27BF)
}
}
// MARK: - Suggestions List Subview
private struct CommandPaletteSuggestionsListView: View {
@EnvironmentObject var gradientColorManager: GradientColorManager
let suggestions: [SearchManager.SearchSuggestion]
@Binding var selectedIndex: Int
@Binding var hoveredIndex: Int?
@Environment(\.colorScheme) var colorScheme
let onSelect: (SearchManager.SearchSuggestion) -> Void
var body: some View {
let isDark = colorScheme == .dark
LazyVStack(spacing: 5) {
ForEach(suggestions.indices, id: \.self) { index in
let suggestion = suggestions[index]
let isHovered = hoveredIndex == index
row(for: suggestion, isSelected: selectedIndex == index)
.padding(.horizontal, 10)
.padding(.vertical, 11)
.background(
selectedIndex == index
? gradientColorManager.primaryColor
: isHovered
? isDark
? .white.opacity(0.05)
: .black.opacity(0.05) : .clear
)
.clipShape(
RoundedRectangle(cornerRadius: 6)
)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(.white)
.contentShape(RoundedRectangle(cornerRadius: 6))
.onHoverTracking { hovering in
withAnimation(.easeInOut(duration: 0.12)) {
if hovering {
hoveredIndex = index
} else {
hoveredIndex = nil
}
}
}
.onTapGesture { onSelect(suggestion) }
}
}
}
@ViewBuilder
private func row(
for suggestion: SearchManager.SearchSuggestion,
isSelected: Bool
) -> some View {
switch suggestion.type {
case .tab(let tab):
TabSuggestionItem(tab: tab, isSelected: isSelected)
case .history(let entry):
HistorySuggestionItem(entry: entry, isSelected: isSelected)
case .url:
GenericSuggestionItem(
icon: Image(systemName: "link"),
text: suggestion.text,
isSelected: isSelected
)
case .search:
GenericSuggestionItem(
icon: Image(systemName: "magnifyingglass"),
text: suggestion.text,
isSelected: isSelected
)
}
}
}
private func handleReturn() {
if let site = activeSiteSearch {
let query: String
if selectedSuggestionIndex >= 0 && selectedSuggestionIndex < visibleSuggestions.count {
query = visibleSuggestions[selectedSuggestionIndex].text
} else {
query = text
}
guard !query.isEmpty else { return }
let navigateURL: String
if let url = site.searchURL(for: query) {
navigateURL = url.absoluteString
} else {
// Fallback: search on the site's domain directly
navigateURL = "https://\(site.domain)/search?q=\(query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? query)"
}
if commandPalette.shouldNavigateCurrentTab
&& browserManager.currentTab(for: windowState) != nil
{
browserManager.currentTab(for: windowState)?.loadURL(navigateURL)
} else {
browserManager.createNewTab(in: windowState, url: navigateURL)
}
text = ""
activeSiteSearch = nil
selectedSuggestionIndex = -1
commandPalette.close()
return
}
if selectedSuggestionIndex >= 0
&& selectedSuggestionIndex < visibleSuggestions.count
{
let suggestion = visibleSuggestions[selectedSuggestionIndex]
selectSuggestion(suggestion)
} else {
let newSuggestion = SearchManager.SearchSuggestion(
text: text,
type: isLikelyURL(text) ? .url : .search
)
selectSuggestion(newSuggestion)
}
}
private func selectSuggestion(_ suggestion: SearchManager.SearchSuggestion)
{
switch suggestion.type {
case .tab(let existingTab):
browserManager.selectTab(existingTab, in: windowState)
case .history(let historyEntry):
if commandPalette.shouldNavigateCurrentTab
&& browserManager.currentTab(for: windowState) != nil
{
browserManager.currentTab(for: windowState)?.loadURL(
historyEntry.url.absoluteString
)
} else {
browserManager.createNewTab(in: windowState, url: historyEntry.url.absoluteString)
}
case .url, .search:
if commandPalette.shouldNavigateCurrentTab
&& browserManager.currentTab(for: windowState) != nil
{
browserManager.currentTab(for: windowState)?.navigateToURL(
suggestion.text
)
} else {
// Normalize the URL/search query first, then create the tab with
// the correct URL so the webview loads it directly without a race.
let template = browserManager.nookSettings?.resolvedSearchEngineTemplate ?? SearchProvider.google.queryTemplate
let resolved = normalizeURL(suggestion.text, queryTemplate: template)
browserManager.createNewTab(in: windowState, url: resolved)
}
}
text = ""
activeSiteSearch = nil
selectedSuggestionIndex = -1
commandPalette.close()
}
private func navigateSuggestions(direction: Int) {
let maxIndex = visibleSuggestions.count - 1
if direction > 0 {
selectedSuggestionIndex = min(selectedSuggestionIndex + 1, maxIndex)
} else {
selectedSuggestionIndex = max(selectedSuggestionIndex - 1, -1)
}
// Update text field to show selected suggestion's info
isNavigatingSuggestion = true
if selectedSuggestionIndex >= 0 && selectedSuggestionIndex < visibleSuggestions.count {
text = displayTextForSuggestion(visibleSuggestions[selectedSuggestionIndex])
} else {
text = userTypedText
}
}
private func stripScheme(_ urlString: String) -> String {
for prefix in ["https://", "http://"] {
if urlString.hasPrefix(prefix) {
return String(urlString.dropFirst(prefix.count))
}
}
return urlString
}
private func displayTextForSuggestion(_ suggestion: SearchManager.SearchSuggestion) -> String {
switch suggestion.type {
case .tab(let tab):
return stripScheme(tab.url.absoluteString)
case .history(let entry):
return stripScheme(entry.url.absoluteString)
case .url, .search:
return suggestion.text
}
}
private var inlineCompletionSuffix: String? {
guard text == userTypedText,
!text.isEmpty,
selectedSuggestionIndex >= 0,
selectedSuggestionIndex < visibleSuggestions.count else { return nil }
let suggestion = visibleSuggestions[selectedSuggestionIndex]
let target = suggestion.text
guard target.lowercased().hasPrefix(text.lowercased()),
target.count > text.count else { return nil }
return String(target.dropFirst(text.count))
}
private func iconForSuggestion(_ suggestion: SearchManager.SearchSuggestion)
-> Image
{
switch suggestion.type {
case .tab(let tab):
return tab.favicon
case .history:
return Image(systemName: "globe")
case .url:
return Image(systemName: "link")
case .search:
return Image(systemName: "magnifyingglass")
}
}
@ViewBuilder
private func suggestionRow(
for suggestion: SearchManager.SearchSuggestion,
isSelected: Bool
) -> some View {
switch suggestion.type {
case .tab(let tab):
TabSuggestionItem(tab: tab, isSelected: isSelected)
.foregroundStyle(AppColors.textPrimary)
case .history(let entry):
HistorySuggestionItem(entry: entry, isSelected: isSelected)
.foregroundStyle(AppColors.textPrimary)
case .url:
GenericSuggestionItem(
icon: Image(systemName: "link"),
text: suggestion.text,
isSelected: isSelected
)
.foregroundStyle(AppColors.textPrimary)
case .search:
GenericSuggestionItem(
icon: Image(systemName: "magnifyingglass"),
text: suggestion.text,
isSelected: isSelected
)
.foregroundStyle(AppColors.textPrimary)
}
}
private func urlForSuggestion(_ suggestion: SearchManager.SearchSuggestion)
-> URL?
{
switch suggestion.type {
case .history(let entry):
return entry.url
default:
return nil
}
}
private func isTabSuggestion(_ suggestion: SearchManager.SearchSuggestion)
-> Bool
{
switch suggestion.type {
case .tab:
return true
case .search, .url, .history:
return false
}
}
}
struct BackdropView: NSViewRepresentable {
func makeNSView(context: Context) -> NSVisualEffectView {
let view = NSVisualEffectView()
view.material = .popover
view.blendingMode = .withinWindow
view.state = .active
return view
}
func updateNSView(_ nsView: NSVisualEffectView, context: Context) { }
}
```
## /Navigation/Sidebar/SidebarBottomBar.swift
```swift path="/Navigation/Sidebar/SidebarBottomBar.swift"
//
// SidebarBottomBar.swift
// Nook
//
// Created by Aether on 15/11/2025.
//
import SwiftUI
/// Bottom bar of the sidebar containing menu button, spaces list, and new space button
struct SidebarBottomBar: View {
@EnvironmentObject var browserManager: BrowserManager
@EnvironmentObject var tabManager: TabManager
@Environment(BrowserWindowState.self) private var windowState
@Binding var isMenuButtonHovered: Bool
let onMenuTap: () -> Void
let onNewSpaceTap: () -> Void
let onMenuHover: (Bool) -> Void
var body: some View {
HStack(alignment: .bottom, spacing: 10) {
menuButton
// Hide spaces list in incognito windows (only one ephemeral space)
if !windowState.isIncognito {
SpacesList()
.frame(maxWidth: .infinity)
.environmentObject(browserManager)
.environment(windowState)
}
// Hide new space button in incognito windows
if !windowState.isIncognito {
newSpaceButton
}
}.fixedSize(horizontal: false, vertical: true)
.padding(.horizontal, 8)
}
private var menuButton: some View {
ZStack {
Button("Menu", systemImage: "archivebox") {
onMenuTap()
}
.labelStyle(.iconOnly)
.buttonStyle(NavButtonStyle())
.foregroundStyle(Color.primary)
.onHoverTracking { isHovered in
isMenuButtonHovered = isHovered
onMenuHover(isHovered)
}
DownloadIndicator()
.offset(x: 12, y: -12)
}
}
private var newSpaceButton: some View {
Menu{
Button("New Space", systemImage: "square.grid.2x2") {
onNewSpaceTap()
}
Button("New Folder", systemImage: "folder.badge.plus") {
if let currentSpace = tabManager.currentSpace {
tabManager.createFolder(for: currentSpace.id)
}
}
Divider()
Button("New Profile", systemImage: "person.badge.plus") {
// TODO: Show profile creation dialog
}
} label:{
Label("Actions", systemImage: "plus")
.labelStyle(.iconOnly)
}
.menuStyle(.button)
.buttonStyle(NavButtonStyle())
.foregroundStyle(Color.primary)
}
}
```
## /Navigation/Sidebar/SidebarHeader.swift
```swift path="/Navigation/Sidebar/SidebarHeader.swift"
//
// SidebarHeader.swift
// Nook
//
// Created by Aether on 15/11/2025.
//
import SwiftUI
/// Header section of the sidebar (window controls, navigation buttons, URL bar)
struct SidebarHeader: View {
@EnvironmentObject var browserManager: BrowserManager
@EnvironmentObject var hoverSidebarManager: HoverSidebarManager
@Environment(BrowserWindowState.self) private var windowState
@Environment(\.nookSettings) var nookSettings
let isSidebarHovered: Bool
@State private var sidebarWidth: CGFloat = 0
var body: some View {
VStack(spacing: 8) {
if nookSettings.topBarAddressView {
windowControls
}
if !nookSettings.topBarAddressView {
navigationButtons
urlBar
}
}
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.size.width
} action: { newWidth in
sidebarWidth = newWidth
}
}
private var windowControls: some View {
SidebarWindowControlsView()
.environmentObject(browserManager)
.environment(windowState)
.padding(.horizontal, 8)
}
private var navigationButtons: some View {
HStack(spacing: 2) {
NavButtonsView(effectiveSidebarWidth: sidebarWidth)
}
.padding(.horizontal, 8)
.frame(height: 30)
}
private var urlBar: some View {
URLBarView(isSidebarHovered: isSidebarHovered)
.padding(.horizontal, 8)
}
}
// MARK: - Sidebar Window Controls (Top Bar Mode)
struct SidebarWindowControlsView: View {
@EnvironmentObject var browserManager: BrowserManager
@EnvironmentObject var hoverSidebarManager: HoverSidebarManager
@Environment(BrowserWindowState.self) private var windowState
@Environment(\.nookSettings) var nookSettings
var body: some View {
HStack(spacing: 8) {
MacButtonsView()
.frame(width: 70)
Button("Toggle Sidebar", systemImage: nookSettings.sidebarPosition == .left ? "sidebar.left" : "sidebar.right") {
browserManager.toggleSidebar(for: windowState, floatingVisible: hoverSidebarManager.isOverlayVisible)
}
.labelStyle(.iconOnly)
.buttonStyle(NavButtonStyle())
.foregroundStyle(Color.primary)
if nookSettings.showAIAssistant {
Button("Toggle AI Assistant", systemImage: "sparkle") {
browserManager.toggleAISidebar(for: windowState)
}
.labelStyle(.iconOnly)
.buttonStyle(NavButtonStyle())
.foregroundStyle(Color.primary)
}
Spacer()
}
.frame(height: 28)
}
}
```
## /Navigation/Sidebar/SpaceContextMenu.swift
```swift path="/Navigation/Sidebar/SpaceContextMenu.swift"
//
// SpaceContextMenu.swift
// Nook
//
// Created by Aether on 15/11/2025.
//
import SwiftUI
/// Shared context menu for spaces (used in SpaceTitle and SpacesList)
struct SpaceContextMenu: View {
@EnvironmentObject var browserManager: BrowserManager
@EnvironmentObject var tabManager: TabManager
let space: Space
let canDelete: Bool
let onEditName: (() -> Void)?
let onEditIcon: (() -> Void)?
let onOpenSettings: () -> Void
let onDeleteSpace: () -> Void
var body: some View {
Group {
// Profile picker
Picker(
currentProfileName,
systemImage: currentProfileIcon,
selection: Binding(
get: {
space.profileId ?? browserManager.profileManager.profiles.first?.id ?? UUID()
},
set: { newProfileId in
tabManager.assign(spaceId: space.id, toProfile: newProfileId)
}
)
) {
ForEach(browserManager.profileManager.profiles, id: \.id) { profile in
Label(profile.name, systemImage: profile.icon).tag(profile.id)
}
}
Divider()
// Rename (optional - only available for SpaceTitle)
if let onEditName = onEditName {
Button {
onEditName()
} label: {
Label("Rename", systemImage: "textformat")
}
}
// Change icon (optional - only available for SpaceTitle)
if let onEditIcon = onEditIcon {
Button {
onEditIcon()
} label: {
Label("Change Icon", systemImage: "face.smiling")
}
}
// Customize appearance
Button {
browserManager.showGradientEditor()
} label: {
Label("Customize Appearance", systemImage: "paintpalette")
}
Divider()
// Space settings
Button {
onOpenSettings()
} label: {
Label("Space Settings", systemImage: "gear")
}
Divider()
// Delete space
if canDelete {
Button(role: .destructive) {
showDeleteConfirmation()
} label: {
Label("Delete Space", systemImage: "trash")
}
}
}
}
// MARK: - Helper Methods
private func showDeleteConfirmation() {
// Count both regular and space-pinned tabs
let regularTabsCount = tabManager.tabsBySpace[space.id]?.count ?? 0
let spacePinnedTabsCount = tabManager.spacePinnedTabs(for: space.id).count
let tabsCount = regularTabsCount + spacePinnedTabsCount
browserManager.dialogManager.showDialog(
SpaceDeleteConfirmationDialog(
spaceName: space.name,
spaceIcon: space.icon,
tabsCount: tabsCount,
isLastSpace: tabManager.spaces.count <= 1,
onDelete: {
onDeleteSpace()
browserManager.dialogManager.closeDialog()
},
onCancel: {
browserManager.dialogManager.closeDialog()
}
)
)
}
// MARK: - Helper Properties
private var currentProfileName: String {
guard let profileId = space.profileId,
let profile = browserManager.profileManager.profiles.first(where: { $0.id == profileId })
else {
return browserManager.profileManager.profiles.first?.name ?? "Default"
}
return profile.name
}
private var currentProfileIcon: String {
guard let profileId = space.profileId,
let profile = browserManager.profileManager.profiles.first(where: { $0.id == profileId })
else {
return browserManager.profileManager.profiles.first?.icon ?? "person.circle"
}
return profile.icon
}
}
```
## /Navigation/Sidebar/SpacesList/SpacesList.swift
```swift path="/Navigation/Sidebar/SpacesList/SpacesList.swift"
//
// SpacesList.swift
// Nook
//
// Created by Maciek Bagiński on 04/08/2025.
// Refactored by Aether on 15/11/2025.
//
import SwiftUI
struct SpacesList: View {
@EnvironmentObject var browserManager: BrowserManager
@EnvironmentObject var tabManager: TabManager
@Environment(BrowserWindowState.self) private var windowState
@State private var availableWidth: CGFloat = 0
@State private var hoveredSpaceId: UUID?
@State private var showPreview: Bool = false
@State private var isHoveringList: Bool = false
private var layoutMode: SpacesListLayoutMode {
let spaces = windowState.isIncognito
? windowState.ephemeralSpaces
: tabManager.spaces
return SpacesListLayoutMode.determine(
spacesCount: spaces.count,
availableWidth: availableWidth
)
}
private var visibleSpaces: [Space] {
if windowState.isIncognito {
return windowState.ephemeralSpaces
}
return tabManager.spaces
}
var body: some View {
Color.clear
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.size.width
} action: { newWidth in
availableWidth = newWidth
}
.overlay{
HStack(spacing: 0) {
ForEach(Array(visibleSpaces.enumerated()), id: \.element.id) { index, space in
SpacesListItem(
space: space,
isActive: windowState.currentSpaceId == space.id,
compact: layoutMode == .compact,
isFaded: false,
onHoverChange: { isHovering in
if isHovering {
hoveredSpaceId = space.id
if showPreview {
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
if hoveredSpaceId == space.id && isHoveringList {
withAnimation(.easeInOut(duration: 0.2)) {
showPreview = true
}
}
}
}
} else if hoveredSpaceId == space.id {
hoveredSpaceId = nil
}
}
)
.environmentObject(browserManager)
.environment(windowState)
.id(space.id)
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity),
removal: .scale.combined(with: .opacity)
))
if index != visibleSpaces.count - 1 {
Spacer()
.frame(minWidth: 1, maxWidth: 8)
.layoutPriority(-1)
}
}
}
.onHoverTracking { hovering in
isHoveringList = hovering
if !hovering {
showPreview = false
hoveredSpaceId = nil
}
}
.overlay(alignment: .top) {
if showPreview,
let hoveredId = hoveredSpaceId,
hoveredId != windowState.currentSpaceId,
let hoveredSpace = visibleSpaces.first(where: { $0.id == hoveredId }) {
Text(hoveredSpace.name)
.font(.caption)
.foregroundStyle(previewTextColor)
.opacity(0.7)
.lineLimit(1)
.id(hoveredSpace.id)
.transition(.blur.animation(.smooth(duration: 0.2)))
.offset(y: -20)
}
}
}
.animation(.easeInOut(duration: 0.3), value: visibleSpaces.count)
}
private var previewTextColor: Color {
browserManager.gradientColorManager.isDark
? AppColors.spaceTabTextDark
: AppColors.spaceTabTextLight
}
}
// MARK: - Layout Mode
enum SpacesListLayoutMode {
case normal // Full icons with spacing
case compact // Dots for inactive, icons for active
static func determine(spacesCount: Int, availableWidth: CGFloat) -> Self {
guard spacesCount > 0 else { return .normal }
// Measurements for NavButtonStyle button with default .regular control size
let buttonSize: CGFloat = 32.0 // NavButtonStyle .regular = 32pt
let minSpacing: CGFloat = 4.0
// Normal mode: all icons visible with minimum spacing
let normalMinWidth = (CGFloat(spacesCount) * buttonSize) + (CGFloat(spacesCount - 1) * minSpacing)
// Compact mode: 1 active icon + (n-1) dots with minimum spacing
let dotSize: CGFloat = 6.0
let totalDots = spacesCount - 1
let compactMinWidth = buttonSize + (CGFloat(totalDots) * dotSize) + (CGFloat(totalDots) * minSpacing)
// Choose mode: switch to compact only when normal mode would be too cramped
// Stay in normal as long as we have at least minimum spacing
if availableWidth >= normalMinWidth {
return .normal
} else if availableWidth >= compactMinWidth {
return .compact
} else {
// Even compact doesn't fit perfectly, but use compact anyway
return .compact
}
}
}
```
## /Navigation/Sidebar/SpacesList/SpacesListItem.swift
```swift path="/Navigation/Sidebar/SpacesList/SpacesListItem.swift"
//
// SpacesListItem.swift
// Nook
//
// Created by Maciek Bagiński on 04/08/2025.
// Refactored by Aether on 15/11/2025.
//
import SwiftUI
struct SpacesListItem: View {
@EnvironmentObject var browserManager: BrowserManager
@EnvironmentObject var tabManager: TabManager
@Environment(BrowserWindowState.self) private var windowState
let space: Space
let isActive: Bool
let compact: Bool
let isFaded: Bool
let onHoverChange: ((Bool) -> Void)?
@State private var isHovering: Bool = false
@StateObject private var emojiManager = EmojiPickerManager()
private let dotSize: CGFloat = 6
init(
space: Space,
isActive: Bool,
compact: Bool,
isFaded: Bool,
onHoverChange: ((Bool) -> Void)? = nil
) {
self.space = space
self.isActive = isActive
self.compact = compact
self.isFaded = isFaded
self.onHoverChange = onHoverChange
}
var body: some View {
Button {
NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .now)
withAnimation(.easeInOut(duration: 0.2)) {
browserManager.setActiveSpace(space, in: windowState)
}
} label: {
spaceIcon
.opacity(isActive ? 1.0 : 0.7)
.frame(maxWidth: .infinity)
}
.labelStyle(.iconOnly)
.buttonStyle(SpaceListItemButtonStyle())
.layoutPriority(2)
.foregroundStyle(Color.primary)
.layoutPriority(isActive ? 1 : 0)
.opacity(isFaded ? 0.3 : 1.0)
.onHoverTracking { hovering in
isHovering = hovering
onHoverChange?(hovering)
}
.contextMenu {
spaceContextMenu
}
}
// MARK: - Icon
@ViewBuilder
private var spaceIcon: some View {
if compact && !isActive {
// Compact mode: show dot
Circle()
.fill(iconColor)
.frame(width: dotSize, height: dotSize)
} else {
// Normal mode: show icon or emoji
if isEmoji(space.icon) {
Text(space.icon)
.conditionally(if: !isActive, apply: { view in
view.colorMultiply(.gray).blendMode(.luminosity)
})
.background(EmojiPickerAnchor(manager: emojiManager))
.onChange(of: emojiManager.selectedEmoji) { _, newValue in
space.icon = newValue
tabManager.persistSnapshot()
}
} else {
Image(systemName: space.icon)
.foregroundStyle(iconColor)
.background(EmojiPickerAnchor(manager: emojiManager))
.onChange(of: emojiManager.selectedEmoji) { _, newValue in
space.icon = newValue
tabManager.persistSnapshot()
}
}
}
}
private var iconColor: Color {
browserManager.gradientColorManager.isDark
? AppColors.spaceTabTextDark
: AppColors.spaceTabTextLight
}
// MARK: - Context Menu
@ViewBuilder
private var spaceContextMenu: some View {
Button {
showSpaceEditDialog()
} label: {
Label("Space Settings", systemImage: "gear")
}
if tabManager.spaces.count > 1 {
Button(role: .destructive) {
showDeleteConfirmation()
} label: {
Label("Delete Space", systemImage: "trash")
}
}
}
// MARK: - Helper Methods
private func showDeleteConfirmation() {
// Count both regular and space-pinned tabs
let regularTabsCount = tabManager.tabsBySpace[space.id]?.count ?? 0
let spacePinnedTabsCount = tabManager.spacePinnedTabs(for: space.id).count
let tabsCount = regularTabsCount + spacePinnedTabsCount
browserManager.dialogManager.showDialog(
SpaceDeleteConfirmationDialog(
spaceName: space.name,
spaceIcon: space.icon,
tabsCount: tabsCount,
isLastSpace: tabManager.spaces.count <= 1,
onDelete: {
tabManager.removeSpace(space.id)
browserManager.dialogManager.closeDialog()
},
onCancel: {
browserManager.dialogManager.closeDialog()
}
)
)
}
private func showSpaceEditDialog() {
browserManager.dialogManager.showDialog(
SpaceEditDialog(
space: space,
mode: .icon,
onSave: { newName, newIcon, newProfileId in
do {
if newIcon != space.icon {
try tabManager.updateSpaceIcon(
spaceId: space.id,
icon: newIcon
)
}
if newName != space.name {
try tabManager.renameSpace(
spaceId: space.id,
newName: newName
)
}
// Update profile if changed
if newProfileId != space.profileId, let profileId = newProfileId {
tabManager.assign(spaceId: space.id, toProfile: profileId)
}
browserManager.dialogManager.closeDialog()
} catch {
}
},
onCancel: {
browserManager.dialogManager.closeDialog()
}
)
)
}
private func isEmoji(_ string: String) -> Bool {
string.unicodeScalars.contains { scalar in
(scalar.value >= 0x1F300 && scalar.value <= 0x1F9FF) // Emoticons & pictographs
|| (scalar.value >= 0x2600 && scalar.value <= 0x26FF) // Miscellaneous symbols
|| (scalar.value >= 0x2700 && scalar.value <= 0x27BF) // Dingbats
}
}
}
struct SpaceListItemButtonStyle: ButtonStyle {
@Environment(\.colorScheme) var colorScheme
@Environment(\.isEnabled) var isEnabled
@Environment(\.controlSize) var controlSize
@State private var isHovering: Bool = false
func makeBody(configuration: Configuration) -> some View {
ZStack {
RoundedRectangle(cornerRadius: cornerRadius)
.fill(.primary.opacity(backgroundColorOpacity(isPressed: configuration.isPressed)))
configuration.label
.foregroundStyle(.primary)
}
.frame(height: size)
.frame(maxWidth: size)
.opacity(isEnabled ? 1.0 : 0.3)
.contentTransition(.symbolEffect(.replace.upUp.byLayer, options: .nonRepeating))
.scaleEffect(configuration.isPressed && isEnabled ? 0.95 : 1.0)
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
.animation(.easeInOut(duration: 0.15), value: isHovering)
.onHoverTracking { hovering in
isHovering = hovering
}
}
private var size: CGFloat {
switch controlSize {
case .mini: 24
case .small: 28
case .regular: 32
case .large: 40
case .extraLarge: 48
@unknown default: 32
}
}
private var cornerRadius: CGFloat {
8
}
private func backgroundColorOpacity(isPressed: Bool) -> Double {
if (isHovering || isPressed) && isEnabled {
return colorScheme == .dark ? 0.2 : 0.1
} else {
return 0.0
}
}
}
```
## /Navigation/Sidebar/SpacesSideBarView.swift
```swift path="/Navigation/Sidebar/SpacesSideBarView.swift"
//
// SpacesSideBarView.swift
// Nook
//
// Created by Maciek Bagiński on 30/07/2025.
// Refactored by Aether on 15/11/2025.
//
import AppKit
import SwiftUI
import UniformTypeIdentifiers
import Sparkle
struct SpacesSideBarView: View {
@EnvironmentObject var browserManager: BrowserManager
@EnvironmentObject var tabManager: TabManager
@Environment(BrowserWindowState.self) private var windowState
@Environment(WindowRegistry.self) private var windowRegistry
@Environment(\.nookSettings) var nookSettings
@Environment(CommandPalette.self) var commandPalette
@Environment(TabOrganizerManager.self) var tabOrganizerManager
// Space navigation
@State private var activeSpaceIndex: Int = 0
@State private var activeTabRefreshTrigger: Bool = false
// Hover states
@State private var isSidebarHovered: Bool = false
@State private var isMenuButtonHovered = false
@State private var isDownloadsHovered = false
@State private var showDownloadsMenu = false
@State private var animateDownloadsMenu: Bool = false
var body: some View {
sidebarContent
.contentShape(Rectangle())
.onHoverTracking { state in
isSidebarHovered = state
}
.contextMenu {
sidebarContextMenu
}
}
// MARK: - Main Content
private var sidebarContent: some View {
ZStack {
if windowState.isSidebarMenuVisible {
SidebarMenu()
.transition(menuTransition)
} else {
mainSidebarContent
.transition(.opacity)
}
}
}
@ObservedObject private var dragSession = NookDragSessionManager.shared
private var mainSidebarContent: some View {
return VStack(spacing: 8) {
// Header (window controls, nav buttons, URL bar)
SidebarHeader(isSidebarHovered: isSidebarHovered)
.environmentObject(browserManager)
.environment(windowState)
// Spaces page view with draggable spacer
ZStack {
spacesPageView
.zIndex(1)
// Bottom spacer for window dragging
Color.clear
.contentShape(Rectangle())
.conditionalWindowDrag()
.frame(minHeight: 40)
.zIndex(0)
}
// Downloads menu hover overlay
if showDownloadsMenu {
downloadsMenuOverlay
}
// Update notification
SidebarUpdateNotification(downloadsMenuVisible: showDownloadsMenu)
.environmentObject(browserManager)
.environment(windowState)
.environment(nookSettings)
.padding(.horizontal, 8)
.padding(.bottom, 8)
// Media controls
MediaControlsView()
.environmentObject(browserManager)
.environment(windowState)
// Bottom bar (menu, spaces indicators, new space)
SidebarBottomBar(
isMenuButtonHovered: $isMenuButtonHovered,
onMenuTap: handleMenuTap,
onNewSpaceTap: showSpaceCreationDialog,
onMenuHover: handleMenuHover
)
.environmentObject(browserManager)
.environment(windowState)
}
// Extra top padding when sidebar is on the left to avoid overlapping native traffic light buttons
.padding(.top, nookSettings.sidebarPosition == .left ? 30 : 8)
.padding(.bottom, 8)
.background(
GeometryReader { geo in
Color.clear
.onAppear {
updateSidebarScreenFrame(geo)
}
.onChange(of: geo.frame(in: .global)) { _, _ in
updateSidebarScreenFrame(geo)
}
}
)
}
private func updateSidebarScreenFrame(_ geo: GeometryProxy) {
let frame = geo.frame(in: .global)
guard let window = windowState.window ?? NSApp.windows.first(where: { $0.isVisible }),
let contentView = window.contentView else { return }
let appKitY = contentView.bounds.height - frame.maxY
let bottomLeft = NSPoint(x: frame.origin.x, y: appKitY)
let screenBottomLeft = window.convertPoint(toScreen: bottomLeft)
dragSession.sidebarScreenFrame = CGRect(
x: screenBottomLeft.x,
y: screenBottomLeft.y,
width: frame.width,
height: frame.height
)
}
// MARK: - Spaces Page View
private var spacesPageView: some View {
let spaces = windowState.isIncognito
? windowState.ephemeralSpaces
: tabManager.spaces
return Group {
if spaces.isEmpty {
emptyStateView
} else {
spacesContent(spaces: spaces)
}
}
}
private func spacesContent(spaces: [Space]) -> some View {
PageView(selection: $activeSpaceIndex) {
ForEach(spaces.indices, id: \.self) { index in
if index >= 0 && index < spaces.count {
makeSpaceView(for: spaces[index], index: index)
} else {
EmptyView()
}
}
}
.pageViewStyle(.scroll)
.contentShape(Rectangle())
.id(activeTabRefreshTrigger)
.onAppear {
if let targetIndex = spaces.firstIndex(where: { $0.id == windowState.currentSpaceId }) {
activeSpaceIndex = targetIndex
}
browserManager.setActiveSpace(spaces[0], in: windowState)
}
.onChange(of: activeSpaceIndex) { _, newIndex in
handleSpaceIndexChange(newIndex, spaces: spaces)
}
.onChange(of: windowState.currentSpaceId) { _, _ in
if let targetIndex = spaces.firstIndex(where: { $0.id == windowState.currentSpaceId }) {
activeSpaceIndex = targetIndex
}
activeTabRefreshTrigger.toggle()
}
.onChange(of: windowState.sidebarContentWidth) { _, _ in
activeTabRefreshTrigger.toggle()
}
}
private var emptyStateView: some View {
VStack(spacing: 16) {
Image(systemName: "square.grid.2x2")
.font(.system(size: 48))
.foregroundColor(.secondary)
VStack(spacing: 8) {
Text("No Spaces")
.font(.title2)
.fontWeight(.semibold)
Text("Create a space to start browsing")
.font(.body)
.foregroundColor(.secondary)
}
Button(action: showSpaceCreationDialog) {
Label("Create Space", systemImage: "plus")
}
.buttonStyle(.borderedProminent)
}
.padding()
}
// MARK: - Downloads Menu
private var downloadsMenuOverlay: some View {
SidebarMenuHoverDownloads(isVisible: animateDownloadsMenu)
.onHoverTracking { isHovered in
isDownloadsHovered = isHovered
if isHovered {
showDownloadsMenu = true
animateDownloadsMenu = true
} else {
hideMenuAfterDelay()
}
}
}
// MARK: - Context Menu
private var sidebarContextMenu: some View {
Group {
Button {
commandPalette.open()
} label: {
Label("New Tab", systemImage: "plus")
}
Button {
if let currentSpace = tabManager.currentSpace {
tabManager.createFolder(for: currentSpace.id)
}
} label: {
Label("New Folder", systemImage: "folder.badge.plus")
}
Divider()
Menu {
ForEach(SidebarPosition.allCases) { position in
Toggle(isOn: Binding(
get: { nookSettings.sidebarPosition == position },
set: { _ in
withAnimation(.smooth(duration: 0.3)) {
nookSettings.sidebarPosition = position
}
}
)) {
Label(position.displayName, systemImage: position.icon)
}
}
} label: {
Label("Position", systemImage: nookSettings.sidebarPosition.icon)
}
}
}
// MARK: - Helper Functions
private func handleMenuTap() {
withAnimation(.easeInOut(duration: 0.2)) {
windowState.isSidebarMenuVisible = true
windowState.isSidebarAIChatVisible = false
let previousWidth = windowState.sidebarWidth
windowState.savedSidebarWidth = previousWidth
let newWidth: CGFloat = 400
windowState.sidebarWidth = newWidth
windowState.sidebarContentWidth = max(newWidth - 16, 0)
}
}
private func handleMenuHover(_ isHovered: Bool) {
if isHovered {
showDownloadsMenu = true
animateDownloadsMenu = true
} else {
hideMenuAfterDelay()
}
}
private func hideMenuAfterDelay() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
if !isMenuButtonHovered, !isDownloadsHovered {
animateDownloadsMenu = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
showDownloadsMenu = false
}
}
}
}
private func handleSpaceIndexChange(_ newIndex: Int, spaces: [Space]) {
guard newIndex >= 0 && newIndex < spaces.count else {
return
}
let space = spaces[newIndex]
// Trigger haptic feedback
NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .default)
// Activate the space
browserManager.setActiveSpace(space, in: windowState)
}
@ViewBuilder
private func makeSpaceView(for space: Space, index: Int) -> some View {
VStack(spacing: 0) {
if !windowState.isIncognito {
PinnedGrid(
width: windowState.sidebarContentWidth,
profileId: space.profileId ?? browserManager.currentProfile?.id
)
.environmentObject(browserManager)
.environmentObject(tabManager)
.environment(windowState)
.environment(windowRegistry)
.environment(nookSettings)
.padding(.horizontal, 8)
.padding(.bottom, 8)
.modifier(FallbackDropBelowEssentialsModifier())
}
SpaceView(
space: space,
isActive: windowState.currentSpaceId == space.id,
isSidebarHovered: $isSidebarHovered,
onActivateTab: { browserManager.selectTab($0, in: windowState) },
onCloseTab: { tabManager.removeTab($0.id) },
onPinTab: { tabManager.pinTab($0) },
onMoveTabUp: { tabManager.moveTabUp($0.id) },
onMoveTabDown: { tabManager.moveTabDown($0.id) },
onMuteTab: { $0.toggleMute() }
)
.environmentObject(browserManager)
.environmentObject(tabManager)
.environment(windowState)
.environment(windowRegistry)
.environment(commandPalette)
.environment(tabOrganizerManager)
.environment(nookSettings)
.environmentObject(browserManager.gradientColorManager)
.environmentObject(browserManager.splitManager)
.id(space.id.uuidString + "-w\(Int(windowState.sidebarContentWidth))")
Spacer()
}
.tag(index)
}
// MARK: - Dialogs
private func showSpaceCreationDialog() {
browserManager.dialogManager.showDialog(
SpaceCreationDialog(
onCreate: { name, icon, profileId in
let finalName = name.isEmpty ? "New Space" : name
let finalIcon = icon.isEmpty ? "✨" : icon
let newSpace = tabManager.createSpace(
name: finalName,
icon: finalIcon
)
// Assign profile if one was selected
if let profileId = profileId {
tabManager.assign(spaceId: newSpace.id, toProfile: profileId)
}
if let targetIndex = tabManager.spaces.firstIndex(where: { $0.id == newSpace.id }) {
activeSpaceIndex = targetIndex
}
browserManager.dialogManager.closeDialog()
},
onCancel: {
browserManager.dialogManager.closeDialog()
}
)
)
}
private func showSpaceEditDialog(mode: SpaceEditDialog.Mode) {
guard let targetSpace = resolveCurrentSpace() else { return }
browserManager.dialogManager.showDialog(
SpaceEditDialog(
space: targetSpace,
mode: mode,
onSave: { newName, newIcon, newProfileId in
let spaceId = targetSpace.id
do {
if newIcon != targetSpace.icon {
try tabManager.updateSpaceIcon(
spaceId: spaceId,
icon: newIcon
)
}
if newName != targetSpace.name {
try tabManager.renameSpace(
spaceId: spaceId,
newName: newName
)
}
// Update profile if changed
if newProfileId != targetSpace.profileId, let profileId = newProfileId {
tabManager.assign(spaceId: spaceId, toProfile: profileId)
}
browserManager.dialogManager.closeDialog()
} catch {
}
},
onCancel: {
browserManager.dialogManager.closeDialog()
}
)
)
}
private func resolveCurrentSpace() -> Space? {
// For incognito windows, use ephemeral spaces
if windowState.isIncognito {
if let currentId = windowState.currentSpaceId {
return windowState.ephemeralSpaces.first { $0.id == currentId }
}
return windowState.ephemeralSpaces.first
}
if let current = tabManager.currentSpace {
return current
}
if let currentId = windowState.currentSpaceId {
return tabManager.spaces.first { $0.id == currentId }
}
return tabManager.spaces.first
}
// MARK: - Computed Properties
private var menuTransition: AnyTransition {
.move(edge: nookSettings.sidebarPosition == .left ? .leading : .trailing)
.combined(with: .opacity)
}
}
```
## /Nook.xcodeproj/project.pbxproj
```pbxproj path="/Nook.xcodeproj/project.pbxproj"
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
2C16A0262E87430B0070894B /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 2C16A0252E87430B0070894B /* Sparkle */; };
2D0822D82EC8A61800C302AC /* UniversalGlass in Frameworks */ = {isa = PBXBuildFile; productRef = 2D0822D72EC8A61800C302AC /* UniversalGlass */; };
3600212C2EC6EADB0016A41E /* ComplexModule in Frameworks */ = {isa = PBXBuildFile; productRef = 3600212B2EC6EADB0016A41E /* ComplexModule */; };
3600212E2EC6EADB0016A41E /* Numerics in Frameworks */ = {isa = PBXBuildFile; productRef = 3600212D2EC6EADB0016A41E /* Numerics */; };
360021302EC6EADB0016A41E /* RealModule in Frameworks */ = {isa = PBXBuildFile; productRef = 3600212F2EC6EADB0016A41E /* RealModule */; };
360021332EC6EAEB0016A41E /* Atomics in Frameworks */ = {isa = PBXBuildFile; productRef = 360021322EC6EAEB0016A41E /* Atomics */; };
360021362EC6EC150016A41E /* Highlightr in Frameworks */ = {isa = PBXBuildFile; productRef = 360021352EC6EC150016A41E /* Highlightr */; };
360021392EC6EC2F0016A41E /* Fuzi in Frameworks */ = {isa = PBXBuildFile; productRef = 360021382EC6EC2F0016A41E /* Fuzi */; };
3600213C2EC6EC450016A41E /* Reeeed in Frameworks */ = {isa = PBXBuildFile; productRef = 3600213B2EC6EC450016A41E /* Reeeed */; };
3600213F2EC6EC710016A41E /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 3600213E2EC6EC710016A41E /* LRUCache */; };
360021422EC6EC7F0016A41E /* Graphing in Frameworks */ = {isa = PBXBuildFile; productRef = 360021412EC6EC7F0016A41E /* Graphing */; };
360021442EC6EC7F0016A41E /* Motion in Frameworks */ = {isa = PBXBuildFile; productRef = 360021432EC6EC7F0016A41E /* Motion */; };
564997D82E9B24CC00D89F78 /* Garnish in Frameworks */ = {isa = PBXBuildFile; productRef = 564997D72E9B24CC00D89F78 /* Garnish */; };
56C82BA92E9C2DB500DDD0D6 /* UniversalGlass in Frameworks */ = {isa = PBXBuildFile; productRef = 56C82BA82E9C2DB500DDD0D6 /* UniversalGlass */; };
7FAFC5DA2E3ADDCD009D7DC4 /* FaviconFinder in Frameworks */ = {isa = PBXBuildFile; productRef = 7FAFC5D92E3ADDCD009D7DC4 /* FaviconFinder */; };
7FE9E0EB2EE59D3500584E16 /* ColorfulX in Frameworks */ = {isa = PBXBuildFile; productRef = 7FE9E0EA2EE59D3500584E16 /* ColorfulX */; };
A1B2C3D62F0E6A0100ABCDEF /* MLXLLM in Frameworks */ = {isa = PBXBuildFile; productRef = A1B2C3D52F0E6A0100ABCDEF /* MLXLLM */; };
A7A6FB442F70F4B8007C79C8 /* ContentBlockerConverter in Frameworks */ = {isa = PBXBuildFile; productRef = B1F2E3D52F0F8B0100FACADE /* ContentBlockerConverter */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
7F8340FC2E37F39400674A5D /* Nook.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Nook.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
2CAC4DD12E82457B00870189 /* Exceptions for "Nook" folder in "Nook" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
ThirdParty/BigUIPaging/README.md,
ThirdParty/HTSymbolHook/README.md,
ThirdParty/MuteableWKWebView/README.md,
);
target = 7F8340FB2E37F39400674A5D /* Nook */;
};
2D0834BF2EC8CE1300C302AC /* Exceptions for "CommandPalette" folder in "Nook" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
"CommandPalette Accessories/CommandPaletteSuggestionView.swift",
"CommandPalette Accessories/GenericSuggestionItem.swift",
"CommandPalette Accessories/HistorySuggestionItem.swift",
"CommandPalette Accessories/TabSuggestionItem.swift",
CommandPalette.swift,
CommandPaletteView.swift,
);
target = 7F8340FB2E37F39400674A5D /* Nook */;
};
2D0834D12EC8E0AD00C302AC /* Exceptions for "Navigation" folder in "Nook" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Sidebar/SidebarBottomBar.swift,
Sidebar/SidebarHeader.swift,
Sidebar/SpaceContextMenu.swift,
Sidebar/SpacesList/SpacesList.swift,
Sidebar/SpacesList/SpacesListItem.swift,
Sidebar/SpacesSideBarView.swift,
);
target = 7F8340FB2E37F39400674A5D /* Nook */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
2CAC4D282E82457B00870189 /* Nook */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
2CAC4DD12E82457B00870189 /* Exceptions for "Nook" folder in "Nook" target */,
);
path = Nook;
sourceTree = "<group>";
};
2D0834B32EC8C43B00C302AC /* App */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = App;
sourceTree = "<group>";
};
2D0834BE2EC8CE0C00C302AC /* CommandPalette */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
2D0834BF2EC8CE1300C302AC /* Exceptions for "CommandPalette" folder in "Nook" target */,
);
path = CommandPalette;
sourceTree = "<group>";
};
2D0834C72EC8D90400C302AC /* Settings */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = Settings;
sourceTree = "<group>";
};
2D0834CF2EC8E0AD00C302AC /* Navigation */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
2D0834D12EC8E0AD00C302AC /* Exceptions for "Navigation" folder in "Nook" target */,
);
path = Navigation;
sourceTree = "<group>";
};
564997D02E9B125200D89F78 /* UI */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = UI;
sourceTree = "<group>";
};
7F6409912F4729B600A02697 /* Onboarding */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = Onboarding;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
7F8340F92E37F39400674A5D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
360021442EC6EC7F0016A41E /* Motion in Frameworks */,
3600213C2EC6EC450016A41E /* Reeeed in Frameworks */,
3600213F2EC6EC710016A41E /* LRUCache in Frameworks */,
564997D82E9B24CC00D89F78 /* Garnish in Frameworks */,
56C82BA92E9C2DB500DDD0D6 /* UniversalGlass in Frameworks */,
7FAFC5DA2E3ADDCD009D7DC4 /* FaviconFinder in Frameworks */,
360021302EC6EADB0016A41E /* RealModule in Frameworks */,
7FE9E0EB2EE59D3500584E16 /* ColorfulX in Frameworks */,
360021392EC6EC2F0016A41E /* Fuzi in Frameworks */,
2C16A0262E87430B0070894B /* Sparkle in Frameworks */,
360021362EC6EC150016A41E /* Highlightr in Frameworks */,
360021422EC6EC7F0016A41E /* Graphing in Frameworks */,
3600212C2EC6EADB0016A41E /* ComplexModule in Frameworks */,
2D0822D82EC8A61800C302AC /* UniversalGlass in Frameworks */,
3600212E2EC6EADB0016A41E /* Numerics in Frameworks */,
360021332EC6EAEB0016A41E /* Atomics in Frameworks */,
A1B2C3D62F0E6A0100ABCDEF /* MLXLLM in Frameworks */,
A7A6FB442F70F4B8007C79C8 /* ContentBlockerConverter in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
7F8340F32E37F39400674A5D = {
isa = PBXGroup;
children = (
7F6409912F4729B600A02697 /* Onboarding */,
2D0834C72EC8D90400C302AC /* Settings */,
2D0834CF2EC8E0AD00C302AC /* Navigation */,
2D0834BE2EC8CE0C00C302AC /* CommandPalette */,
7F8340FD2E37F39400674A5D /* Products */,
564997D02E9B125200D89F78 /* UI */,
2CAC4D282E82457B00870189 /* Nook */,
2D0834B32EC8C43B00C302AC /* App */,
);
sourceTree = "<group>";
};
7F8340FD2E37F39400674A5D /* Products */ = {
isa = PBXGroup;
children = (
7F8340FC2E37F39400674A5D /* Nook.app */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
7F8340FB2E37F39400674A5D /* Nook */ = {
isa = PBXNativeTarget;
buildConfigurationList = 7F8341082E37F39500674A5D /* Build configuration list for PBXNativeTarget "Nook" */;
buildPhases = (
7F8340F82E37F39400674A5D /* Sources */,
7F8340F92E37F39400674A5D /* Frameworks */,
7F8340FA2E37F39400674A5D /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
2CAC4D282E82457B00870189 /* Nook */,
2D0834B32EC8C43B00C302AC /* App */,
2D0834C72EC8D90400C302AC /* Settings */,
564997D02E9B125200D89F78 /* UI */,
7F6409912F4729B600A02697 /* Onboarding */,
);
name = Nook;
packageProductDependencies = (
7FAFC5D92E3ADDCD009D7DC4 /* FaviconFinder */,
2C16A0252E87430B0070894B /* Sparkle */,
564997D72E9B24CC00D89F78 /* Garnish */,
3600212B2EC6EADB0016A41E /* ComplexModule */,
3600212D2EC6EADB0016A41E /* Numerics */,
3600212F2EC6EADB0016A41E /* RealModule */,
360021322EC6EAEB0016A41E /* Atomics */,
360021352EC6EC150016A41E /* Highlightr */,
360021382EC6EC2F0016A41E /* Fuzi */,
3600213B2EC6EC450016A41E /* Reeeed */,
3600213E2EC6EC710016A41E /* LRUCache */,
360021412EC6EC7F0016A41E /* Graphing */,
360021432EC6EC7F0016A41E /* Motion */,
2D0822D72EC8A61800C302AC /* UniversalGlass */,
7FE9E0EA2EE59D3500584E16 /* ColorfulX */,
A1B2C3D52F0E6A0100ABCDEF /* MLXLLM */,
B1F2E3D52F0F8B0100FACADE /* ContentBlockerConverter */,
);
productName = Pulse;
productReference = 7F8340FC2E37F39400674A5D /* Nook.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
7F8340F42E37F39400674A5D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 2600;
LastUpgradeCheck = 1640;
TargetAttributes = {
7F8340FB2E37F39400674A5D = {
CreatedOnToolsVersion = 16.4;
};
};
};
buildConfigurationList = 7F8340F72E37F39400674A5D /* Build configuration list for PBXProject "Nook" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 7F8340F32E37F39400674A5D;
minimizedProjectReferenceProxies = 1;
packageReferences = (
7FAFC5D82E3ADDCD009D7DC4 /* XCRemoteSwiftPackageReference "FaviconFinder" */,
2C16A0242E87430B0070894B /* XCRemoteSwiftPackageReference "Sparkle" */,
564997D62E9B24CC00D89F78 /* XCRemoteSwiftPackageReference "Garnish" */,
3600212A2EC6EADB0016A41E /* XCRemoteSwiftPackageReference "swift-numerics" */,
360021312EC6EAEB0016A41E /* XCRemoteSwiftPackageReference "swift-atomics" */,
360021342EC6EC150016A41E /* XCRemoteSwiftPackageReference "Highlightr" */,
360021372EC6EC2F0016A41E /* XCRemoteSwiftPackageReference "Fuzi" */,
3600213A2EC6EC450016A41E /* XCRemoteSwiftPackageReference "reeeed" */,
3600213D2EC6EC710016A41E /* XCRemoteSwiftPackageReference "LRUCache" */,
360021402EC6EC7F0016A41E /* XCRemoteSwiftPackageReference "Motion" */,
2D0822D62EC8A61800C302AC /* XCRemoteSwiftPackageReference "universalglass" */,
7FE9E0E92EE59D3500584E16 /* XCRemoteSwiftPackageReference "ColorfulX" */,
A1B2C3D42F0E6A0100ABCDEF /* XCRemoteSwiftPackageReference "mlx-swift-lm" */,
B1F2E3D42F0F8B0100FACADE /* XCRemoteSwiftPackageReference "SafariConverterLib" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 7F8340FD2E37F39400674A5D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
7F8340FB2E37F39400674A5D /* Nook */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
7F8340FA2E37F39400674A5D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
7F8340F82E37F39400674A5D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
7F8341062E37F39500674A5D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 96M8ZZRJK6;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 15.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
7F8341072E37F39500674A5D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 96M8ZZRJK6;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 15.5;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
};
name = Release;
};
7F8341092E37F39500674A5D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = "logo-dev";
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_ENTITLEMENTS = Nook/Nook.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 107;
DEVELOPMENT_TEAM = 9DLM793N9T;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Nook/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Nook;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0.7;
PRODUCT_BUNDLE_IDENTIFIER = io.browsewithnook.nook;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
REGISTER_APP_GROUPS = YES;
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = YES;
RUNTIME_EXCEPTION_ALLOW_JIT = YES;
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
RUNTIME_EXCEPTION_DEBUGGING_TOOL = YES;
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = YES;
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/Nook/Supporting Files/Nook-Bridging-Header.h";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
7F83410A2E37F39500674A5D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = logo;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_ENTITLEMENTS = Nook/Nook.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 107;
DEVELOPMENT_TEAM = 9DLM793N9T;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Nook/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Nook;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0.7;
PRODUCT_BUNDLE_IDENTIFIER = io.browsewithnook.nook;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
REGISTER_APP_GROUPS = YES;
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = YES;
RUNTIME_EXCEPTION_ALLOW_JIT = YES;
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/Nook/Supporting Files/Nook-Bridging-Header.h";
SWIFT_VERSION = 5.0;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
7F8340F72E37F39400674A5D /* Build configuration list for PBXProject "Nook" */ = {
isa = XCConfigurationList;
buildConfigurations = (
7F8341062E37F39500674A5D /* Debug */,
7F8341072E37F39500674A5D /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
7F8341082E37F39500674A5D /* Build configuration list for PBXNativeTarget "Nook" */ = {
isa = XCConfigurationList;
buildConfigurations = (
7F8341092E37F39500674A5D /* Debug */,
7F83410A2E37F39500674A5D /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
2C16A0242E87430B0070894B /* XCRemoteSwiftPackageReference "Sparkle" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sparkle-project/Sparkle";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.8.0;
};
};
2D0822D62EC8A61800C302AC /* XCRemoteSwiftPackageReference "universalglass" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/aeastr/universalglass.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
};
};
3600212A2EC6EADB0016A41E /* XCRemoteSwiftPackageReference "swift-numerics" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-numerics.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.1.1;
};
};
360021312EC6EAEB0016A41E /* XCRemoteSwiftPackageReference "swift-atomics" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-atomics.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.3.0;
};
};
360021342EC6EC150016A41E /* XCRemoteSwiftPackageReference "Highlightr" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/raspu/Highlightr/";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.3.0;
};
};
360021372EC6EC2F0016A41E /* XCRemoteSwiftPackageReference "Fuzi" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/cezheng/Fuzi";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 3.1.3;
};
};
3600213A2EC6EC450016A41E /* XCRemoteSwiftPackageReference "reeeed" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/nate-parrott/reeeed";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.1;
};
};
3600213D2EC6EC710016A41E /* XCRemoteSwiftPackageReference "LRUCache" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/nicklockwood/LRUCache.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.2.0;
};
};
360021402EC6EC7F0016A41E /* XCRemoteSwiftPackageReference "Motion" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/b3ll/Motion";
requirement = {
branch = main;
kind = branch;
};
};
564997D62E9B24CC00D89F78 /* XCRemoteSwiftPackageReference "Garnish" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Aeastr/Garnish";
requirement = {
branch = main;
kind = branch;
};
};
56C82BA72E9C2DB500DDD0D6 /* XCRemoteSwiftPackageReference "UniversalGlass" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Aeastr/UniversalGlass";
requirement = {
branch = main;
kind = branch;
};
};
7FAFC5D82E3ADDCD009D7DC4 /* XCRemoteSwiftPackageReference "FaviconFinder" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/will-lumley/FaviconFinder";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 5.1.4;
};
};
7FE9E0E92EE59D3500584E16 /* XCRemoteSwiftPackageReference "ColorfulX" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Lakr233/ColorfulX.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 6.0.2;
};
};
A1B2C3D42F0E6A0100ABCDEF /* XCRemoteSwiftPackageReference "mlx-swift-lm" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/ml-explore/mlx-swift-lm";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.30.6;
};
};
B1F2E3D42F0F8B0100FACADE /* XCRemoteSwiftPackageReference "SafariConverterLib" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/AdguardTeam/SafariConverterLib";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 4.2.1;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
2C16A0252E87430B0070894B /* Sparkle */ = {
isa = XCSwiftPackageProductDependency;
package = 2C16A0242E87430B0070894B /* XCRemoteSwiftPackageReference "Sparkle" */;
productName = Sparkle;
};
2D0822D72EC8A61800C302AC /* UniversalGlass */ = {
isa = XCSwiftPackageProductDependency;
package = 2D0822D62EC8A61800C302AC /* XCRemoteSwiftPackageReference "universalglass" */;
productName = UniversalGlass;
};
3600212B2EC6EADB0016A41E /* ComplexModule */ = {
isa = XCSwiftPackageProductDependency;
package = 3600212A2EC6EADB0016A41E /* XCRemoteSwiftPackageReference "swift-numerics" */;
productName = ComplexModule;
};
3600212D2EC6EADB0016A41E /* Numerics */ = {
isa = XCSwiftPackageProductDependency;
package = 3600212A2EC6EADB0016A41E /* XCRemoteSwiftPackageReference "swift-numerics" */;
productName = Numerics;
};
3600212F2EC6EADB0016A41E /* RealModule */ = {
isa = XCSwiftPackageProductDependency;
package = 3600212A2EC6EADB0016A41E /* XCRemoteSwiftPackageReference "swift-numerics" */;
productName = RealModule;
};
360021322EC6EAEB0016A41E /* Atomics */ = {
isa = XCSwiftPackageProductDependency;
package = 360021312EC6EAEB0016A41E /* XCRemoteSwiftPackageReference "swift-atomics" */;
productName = Atomics;
};
360021352EC6EC150016A41E /* Highlightr */ = {
isa = XCSwiftPackageProductDependency;
package = 360021342EC6EC150016A41E /* XCRemoteSwiftPackageReference "Highlightr" */;
productName = Highlightr;
};
360021382EC6EC2F0016A41E /* Fuzi */ = {
isa = XCSwiftPackageProductDependency;
package = 360021372EC6EC2F0016A41E /* XCRemoteSwiftPackageReference "Fuzi" */;
productName = Fuzi;
};
3600213B2EC6EC450016A41E /* Reeeed */ = {
isa = XCSwiftPackageProductDependency;
package = 3600213A2EC6EC450016A41E /* XCRemoteSwiftPackageReference "reeeed" */;
productName = Reeeed;
};
3600213E2EC6EC710016A41E /* LRUCache */ = {
isa = XCSwiftPackageProductDependency;
package = 3600213D2EC6EC710016A41E /* XCRemoteSwiftPackageReference "LRUCache" */;
productName = LRUCache;
};
360021412EC6EC7F0016A41E /* Graphing */ = {
isa = XCSwiftPackageProductDependency;
package = 360021402EC6EC7F0016A41E /* XCRemoteSwiftPackageReference "Motion" */;
productName = Graphing;
};
360021432EC6EC7F0016A41E /* Motion */ = {
isa = XCSwiftPackageProductDependency;
package = 360021402EC6EC7F0016A41E /* XCRemoteSwiftPackageReference "Motion" */;
productName = Motion;
};
564997D72E9B24CC00D89F78 /* Garnish */ = {
isa = XCSwiftPackageProductDependency;
package = 564997D62E9B24CC00D89F78 /* XCRemoteSwiftPackageReference "Garnish" */;
productName = Garnish;
};
56C82BA82E9C2DB500DDD0D6 /* UniversalGlass */ = {
isa = XCSwiftPackageProductDependency;
package = 56C82BA72E9C2DB500DDD0D6 /* XCRemoteSwiftPackageReference "UniversalGlass" */;
productName = UniversalGlass;
};
7FAFC5D92E3ADDCD009D7DC4 /* FaviconFinder */ = {
isa = XCSwiftPackageProductDependency;
package = 7FAFC5D82E3ADDCD009D7DC4 /* XCRemoteSwiftPackageReference "FaviconFinder" */;
productName = FaviconFinder;
};
7FE9E0EA2EE59D3500584E16 /* ColorfulX */ = {
isa = XCSwiftPackageProductDependency;
package = 7FE9E0E92EE59D3500584E16 /* XCRemoteSwiftPackageReference "ColorfulX" */;
productName = ColorfulX;
};
A1B2C3D52F0E6A0100ABCDEF /* MLXLLM */ = {
isa = XCSwiftPackageProductDependency;
package = A1B2C3D42F0E6A0100ABCDEF /* XCRemoteSwiftPackageReference "mlx-swift-lm" */;
productName = MLXLLM;
};
B1F2E3D52F0F8B0100FACADE /* ContentBlockerConverter */ = {
isa = XCSwiftPackageProductDependency;
package = B1F2E3D42F0F8B0100FACADE /* XCRemoteSwiftPackageReference "SafariConverterLib" */;
productName = ContentBlockerConverter;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 7F8340F42E37F39400674A5D /* Project object */;
}
```
## /Nook.xcodeproj/project.xcworkspace/contents.xcworkspacedata
```xcworkspacedata path="/Nook.xcodeproj/project.xcworkspace/contents.xcworkspacedata"
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
```
## /Nook.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
```xcsettings path="/Nook.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
```
## /Nook.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
```resolved path="/Nook.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved"
{
"originHash" : "68d1d32f85ac866faef3e453f3d946881f9578c265728f3069236633b9a20eb9",
"pins" : [
{
"identity" : "chronicle",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Aeastr/Chronicle.git",
"state" : {
"revision" : "78f4d0d634c4834f13f7ab1bcca79c150c4c172a",
"version" : "3.0.2"
}
},
{
"identity" : "colorfulx",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Lakr233/ColorfulX.git",
"state" : {
"revision" : "2f008fcffc7f23811b2ba70eda77c57223815bc3",
"version" : "6.0.2"
}
},
{
"identity" : "colorvector",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Lakr233/ColorVector.git",
"state" : {
"revision" : "6da8726bf38d68eb943d0f2139ac2a1fac70e65b",
"version" : "1.0.4"
}
},
{
"identity" : "faviconfinder",
"kind" : "remoteSourceControl",
"location" : "https://github.com/will-lumley/FaviconFinder",
"state" : {
"revision" : "11c2b27fbdc5fdfe0bf3addac01d27c40299a819",
"version" : "5.1.5"
}
},
{
"identity" : "fuzi",
"kind" : "remoteSourceControl",
"location" : "https://github.com/cezheng/Fuzi",
"state" : {
"revision" : "f08c8323da21e985f3772610753bcfc652c2103f",
"version" : "3.1.3"
}
},
{
"identity" : "garnish",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Aeastr/Garnish",
"state" : {
"branch" : "main",
"revision" : "ffbd0091ed25eb3e250ca47b5ccc8c4f9a6a3420"
}
},
{
"identity" : "highlightr",
"kind" : "remoteSourceControl",
"location" : "https://github.com/raspu/Highlightr/",
"state" : {
"revision" : "05e7fcc63b33925cd0c1faaa205cdd5681e7bbef",
"version" : "2.3.0"
}
},
{
"identity" : "lrucache",
"kind" : "remoteSourceControl",
"location" : "https://github.com/nicklockwood/LRUCache.git",
"state" : {
"revision" : "cb5b2bd0da83ad29c0bec762d39f41c8ad0eaf3e",
"version" : "1.2.1"
}
},
{
"identity" : "mlx-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ml-explore/mlx-swift",
"state" : {
"revision" : "6ba4827fb82c97d012eec9ab4b2de21f85c3b33d",
"version" : "0.30.6"
}
},
{
"identity" : "mlx-swift-lm",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ml-explore/mlx-swift-lm",
"state" : {
"revision" : "7e19e09027923d89ac47dd087d9627f610e5a91a",
"version" : "2.30.6"
}
},
{
"identity" : "motion",
"kind" : "remoteSourceControl",
"location" : "https://github.com/b3ll/Motion",
"state" : {
"branch" : "main",
"revision" : "c9a57f9d2b9d0a1e1b905753175e75a422d381d5"
}
},
{
"identity" : "msdisplaylink",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Lakr233/MSDisplayLink.git",
"state" : {
"revision" : "ebf5823cb5fc1326639d9a05bc06d16bbe82989f",
"version" : "2.0.8"
}
},
{
"identity" : "punycodeswift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gumob/PunycodeSwift.git",
"state" : {
"revision" : "30a462bdb4398ea835a3585472229e0d74b36ba5",
"version" : "3.0.0"
}
},
{
"identity" : "reeeed",
"kind" : "remoteSourceControl",
"location" : "https://github.com/nate-parrott/reeeed",
"state" : {
"revision" : "567e2f50592f8a5fe41e9ff64784b42c77f35433",
"version" : "1.0.1"
}
},
{
"identity" : "safariconverterlib",
"kind" : "remoteSourceControl",
"location" : "https://github.com/AdguardTeam/SafariConverterLib",
"state" : {
"revision" : "d4a4831943c82a838a04658f552b47c1beebecd7",
"version" : "4.2.1"
}
},
{
"identity" : "sparkle",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle",
"state" : {
"revision" : "5581748cef2bae787496fe6d61139aebe0a451f6",
"version" : "2.8.1"
}
},
{
"identity" : "springinterpolation",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Lakr233/SpringInterpolation.git",
"state" : {
"revision" : "cdb556516daa9b43c16aae9436dd39e19ff930fd",
"version" : "1.4.0"
}
},
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
"revision" : "41982a3656a71c768319979febd796c6fd111d5c",
"version" : "1.5.0"
}
},
{
"identity" : "swift-asn1",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-asn1.git",
"state" : {
"revision" : "9f542610331815e29cc3821d3b6f488db8715517",
"version" : "1.6.0"
}
},
{
"identity" : "swift-atomics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-atomics.git",
"state" : {
"revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7",
"version" : "1.3.0"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "6675bc0ff86e61436e615df6fc5174e043e57924",
"version" : "1.4.1"
}
},
{
"identity" : "swift-crypto",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-crypto.git",
"state" : {
"revision" : "fa308c07a6fa04a727212d793e761460e41049c3",
"version" : "4.3.0"
}
},
{
"identity" : "swift-jinja",
"kind" : "remoteSourceControl",
"location" : "https://github.com/huggingface/swift-jinja.git",
"state" : {
"revision" : "f731f03bf746481d4fda07f817c3774390c4d5b9",
"version" : "2.3.2"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"state" : {
"revision" : "bbd81b6725ae874c69e9b8c8804d462356b55523",
"version" : "1.10.1"
}
},
{
"identity" : "swift-numerics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-numerics.git",
"state" : {
"revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2",
"version" : "1.1.1"
}
},
{
"identity" : "swift-psl",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ameshkov/swift-psl",
"state" : {
"revision" : "1d8f7c69bd72abaceefceae90824a41bddf8afc0",
"version" : "1.1.127"
}
},
{
"identity" : "swift-transformers",
"kind" : "remoteSourceControl",
"location" : "https://github.com/huggingface/swift-transformers",
"state" : {
"revision" : "150169bfba0889c229a2ce7494cf8949f18e6906",
"version" : "1.1.9"
}
},
{
"identity" : "swiftsoup",
"kind" : "remoteSourceControl",
"location" : "https://github.com/scinfu/SwiftSoup.git",
"state" : {
"revision" : "d86f244ed497d48012782e2f59c985a55e77b3f5",
"version" : "2.11.3"
}
},
{
"identity" : "universalglass",
"kind" : "remoteSourceControl",
"location" : "https://github.com/aeastr/universalglass.git",
"state" : {
"revision" : "9c084472801ac2c1b4c753668b275094c635b8a2",
"version" : "1.1.0"
}
},
{
"identity" : "yyjson",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ibireme/yyjson.git",
"state" : {
"revision" : "8b4a38dc994a110abaec8a400615567bd996105f",
"version" : "0.12.0"
}
}
],
"version" : 3
}
```
## /Nook.xcodeproj/xcshareddata/xcschemes/Nook.xcscheme
```xcscheme path="/Nook.xcodeproj/xcshareddata/xcschemes/Nook.xcscheme"
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7F8340FB2E37F39400674A5D"
BuildableName = "Nook.app"
BlueprintName = "Nook"
ReferencedContainer = "container:Nook.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2C8925302E88DFF9002236DC"
BuildableName = "NookUITests.xctest"
BlueprintName = "NookUITests"
ReferencedContainer = "container:Nook.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7F8340FB2E37F39400674A5D"
BuildableName = "Nook.app"
BlueprintName = "Nook"
ReferencedContainer = "container:Nook.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7F8340FB2E37F39400674A5D"
BuildableName = "Nook.app"
BlueprintName = "Nook"
ReferencedContainer = "container:Nook.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Release">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
customArchiveName = "Nook Browser"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
```
## /Nook/Assets.xcassets/AccentColor.colorset/Contents.json
```json path="/Nook/Assets.xcassets/AccentColor.colorset/Contents.json"
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/Browser Logos/Contents.json
```json path="/Nook/Assets.xcassets/Browser Logos/Contents.json"
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/Browser Logos/arc-logo.imageset/Contents.json
```json path="/Nook/Assets.xcassets/Browser Logos/arc-logo.imageset/Contents.json"
{
"images" : [
{
"filename" : "Frame-2.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/Browser Logos/arc-logo.imageset/Frame-2.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/Browser Logos/arc-logo.imageset/Frame-2.png
## /Nook/Assets.xcassets/Browser Logos/chrome-logo.imageset/Contents.json
```json path="/Nook/Assets.xcassets/Browser Logos/chrome-logo.imageset/Contents.json"
{
"images" : [
{
"filename" : "Frame-1.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/Browser Logos/chrome-logo.imageset/Frame-1.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/Browser Logos/chrome-logo.imageset/Frame-1.png
## /Nook/Assets.xcassets/Browser Logos/dia-logo.imageset/Contents.json
```json path="/Nook/Assets.xcassets/Browser Logos/dia-logo.imageset/Contents.json"
{
"images" : [
{
"filename" : "Frame 255.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/Browser Logos/dia-logo.imageset/Frame 255.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/Browser Logos/dia-logo.imageset/Frame 255.png
## /Nook/Assets.xcassets/Browser Logos/firefox-logo.imageset/Contents.json
```json path="/Nook/Assets.xcassets/Browser Logos/firefox-logo.imageset/Contents.json"
{
"images" : [
{
"filename" : "Frame.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/Browser Logos/firefox-logo.imageset/Frame.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/Browser Logos/firefox-logo.imageset/Frame.png
## /Nook/Assets.xcassets/Browser Logos/safari-logo.imageset/Contents.json
```json path="/Nook/Assets.xcassets/Browser Logos/safari-logo.imageset/Contents.json"
{
"images" : [
{
"filename" : "safari-logo.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/Browser Logos/safari-logo.imageset/safari-logo.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/Browser Logos/safari-logo.imageset/safari-logo.png
## /Nook/Assets.xcassets/Browser Logos/zen-logo.imageset/Contents.json
```json path="/Nook/Assets.xcassets/Browser Logos/zen-logo.imageset/Contents.json"
{
"images" : [
{
"filename" : "zen-logo.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/Browser Logos/zen-logo.imageset/zen-logo.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/Browser Logos/zen-logo.imageset/zen-logo.png
## /Nook/Assets.xcassets/Contents.json
```json path="/Nook/Assets.xcassets/Contents.json"
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/adblocker-off.imageset/Contents.json
```json path="/Nook/Assets.xcassets/adblocker-off.imageset/Contents.json"
{
"images" : [
{
"filename" : "adblocker-off.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/adblocker-off.imageset/adblocker-off.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/adblocker-off.imageset/adblocker-off.png
## /Nook/Assets.xcassets/adblocker-off.imageset/adblocker-on.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/adblocker-off.imageset/adblocker-on.png
## /Nook/Assets.xcassets/adblocker-on.imageset/Contents.json
```json path="/Nook/Assets.xcassets/adblocker-on.imageset/Contents.json"
{
"images" : [
{
"filename" : "adblocker-on.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/adblocker-on.imageset/adblocker-off.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/adblocker-on.imageset/adblocker-off.png
## /Nook/Assets.xcassets/adblocker-on.imageset/adblocker-on.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/adblocker-on.imageset/adblocker-on.png
## /Nook/Assets.xcassets/ai-chat-off.imageset/Contents.json
```json path="/Nook/Assets.xcassets/ai-chat-off.imageset/Contents.json"
{
"images" : [
{
"filename" : "ai-chat-off.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/ai-chat-off.imageset/ai-chat-off.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/ai-chat-off.imageset/ai-chat-off.png
## /Nook/Assets.xcassets/ai-chat-on.imageset/Contents.json
```json path="/Nook/Assets.xcassets/ai-chat-on.imageset/Contents.json"
{
"images" : [
{
"filename" : "ai-chat-on.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/ai-chat-on.imageset/ai-chat-on.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/ai-chat-on.imageset/ai-chat-on.png
## /Nook/Assets.xcassets/github.fill.symbolset/Contents.json
```json path="/Nook/Assets.xcassets/github.fill.symbolset/Contents.json"
{
"info" : {
"author" : "xcode",
"version" : 1
},
"symbols" : [
{
"filename" : "github.fill.svg",
"idiom" : "universal"
}
]
}
```
## /Nook/Assets.xcassets/noise_texture.imageset/Contents.json
```json path="/Nook/Assets.xcassets/noise_texture.imageset/Contents.json"
{
"images" : [
{
"filename" : "noise_texture.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "noise_texture@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "noise_texture@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/noise_texture.imageset/noise_texture.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/noise_texture.imageset/noise_texture.png
## /Nook/Assets.xcassets/noise_texture.imageset/noise_texture@2x.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/noise_texture.imageset/noise_texture@2x.png
## /Nook/Assets.xcassets/noise_texture.imageset/noise_texture@3x.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/noise_texture.imageset/noise_texture@3x.png
## /Nook/Assets.xcassets/nook-logo-1024.imageset/Contents.json
```json path="/Nook/Assets.xcassets/nook-logo-1024.imageset/Contents.json"
{
"images" : [
{
"filename" : "nook-logo-1024.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/nook-logo-1024.imageset/nook-logo-1024.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/nook-logo-1024.imageset/nook-logo-1024.png
## /Nook/Assets.xcassets/opencollective-fill.symbolset/Contents.json
```json path="/Nook/Assets.xcassets/opencollective-fill.symbolset/Contents.json"
{
"info" : {
"author" : "xcode",
"version" : 1
},
"symbols" : [
{
"filename" : "opencollective.symbols.svg",
"idiom" : "universal"
}
]
}
```
## /Nook/Assets.xcassets/opencollective-fill.symbolset/opencollective.symbols.svg
```svg path="/Nook/Assets.xcassets/opencollective-fill.symbolset/opencollective.symbols.svg"
<?xml version="1.0" encoding="UTF-8"?>
<svg height="600" width="800" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Notes" font-family="'LucidaGrande', 'Lucida Grande', sans-serif" font-size="13">
<rect fill="white" height="600.0" width="800.0" x="0.0" y="0.0" />
<g font-size="13">
<text x="18.0" y="176.0">Small</text>
<text x="18.0" y="376.0">Medium</text>
<text x="18.0" y="576.0">Large</text>
</g>
<g font-size="9">
<text x="250.0" y="30.0">Ultralight</text>
<text x="450.0" y="30.0">Regular</text>
<text x="650.0" y="30.0">Black</text>
<text id="template-version" fill="#505050" text-anchor="end" x="785.0" y="575.0">Template v.3.0</text>
<a href="https://wangchujiang.com/#/app">
<text fill="#505050" text-anchor="end" x="785.0" y="590.0">https://wangchujiang.com/#/app</text>
</a>
</g>
</g>
<g id="Guides" stroke="rgb(39, 170, 225)" stroke-width="0.5">
<path id="Capline-S" d="M18,76 l800,0" />
<path id="H-reference" d="M85,145.755 L87.685,145.755 L113.369,79.287 L114.052,79.287 L114.052,76 L112.148,76 L85,145.755 Z M95.693,121.536 L130.996,121.536 L130.263,119.313 L96.474,119.313 L95.693,121.536 Z M139.15,145.755 L141.787,145.755 L114.638,76 L113.466,76 L113.466,79.287 L139.15,145.755 Z" stroke="none" />
<path id="Baseline-S" d="M18,146 l800,0" />
<path id="left-margin-Ultralight-S" d="M211,56 l0,110" />
<path id="right-margin-Ultralight-S" d="M319,56 l0,110" />
<path id="left-margin-Regular-S" d="M411,56 l0,110" />
<path id="right-margin-Regular-S" d="M519,56 l0,110" />
<path id="left-margin-Black-S" d="M611,56 l0,110" />
<path id="right-margin-Black-S" d="M719,56 l0,110" />
<path id="Capline-M" d="M18,276 l800,0" />
<path id="Baseline-M" d="M18,346 l800,0" />
<path id="Capline-L" d="M18,476 l800,0" />
<path id="Baseline-L" d="M18,546 l800,0" />
</g>
<g id="Symbols">
<g id="Ultralight-S">
<path d="M281.581,110.892 C281.581,114.143 280.606,117.286 278.98,119.887 L285.591,126.606 C288.842,122.271 290.901,116.744 290.901,110.892 C290.901,105.039 288.842,99.512 285.591,95.177 L278.98,101.897 C280.606,104.498 281.581,107.532 281.581,110.892 Z" />
<path d="M265,127.581 C255.897,127.581 248.419,120.103 248.419,110.892 C248.419,101.68 255.897,94.202 265,94.202 C268.36,94.202 271.394,95.177 273.995,96.911 L280.606,90.192 C276.271,86.941 270.852,84.882 265,84.882 C250.695,84.882 238.99,96.478 238.99,111 C238.99,125.522 250.695,136.901 265,136.901 C270.961,136.901 276.379,134.842 280.823,131.591 L274.212,124.872 C271.611,126.498 268.468,127.473 265.108,127.473 L265,127.581 Z" />
</g>
<g id="Regular-S">
<path d="M481.581,110.892 C481.581,114.143 480.606,117.286 478.98,119.887 L485.591,126.606 C488.842,122.271 490.901,116.744 490.901,110.892 C490.901,105.039 488.842,99.512 485.591,95.177 L478.98,101.897 C480.606,104.498 481.581,107.532 481.581,110.892 Z" />
<path d="M465,127.581 C455.897,127.581 448.419,120.103 448.419,110.892 C448.419,101.68 455.897,94.202 465,94.202 C468.36,94.202 471.394,95.177 473.995,96.911 L480.606,90.192 C476.271,86.941 470.852,84.882 465,84.882 C450.695,84.882 438.99,96.478 438.99,111 C438.99,125.522 450.695,136.901 465,136.901 C470.961,136.901 476.379,134.842 480.823,131.591 L474.212,124.872 C471.611,126.498 468.468,127.473 465.108,127.473 L465,127.581 Z" />
</g>
<g id="Black-S">
<path d="M681.581,110.892 C681.581,114.143 680.606,117.286 678.98,119.887 L685.591,126.606 C688.842,122.271 690.901,116.744 690.901,110.892 C690.901,105.039 688.842,99.512 685.591,95.177 L678.98,101.897 C680.606,104.498 681.581,107.532 681.581,110.892 Z" />
<path d="M665,127.581 C655.897,127.581 648.419,120.103 648.419,110.892 C648.419,101.68 655.897,94.202 665,94.202 C668.36,94.202 671.394,95.177 673.995,96.911 L680.606,90.192 C676.271,86.941 670.852,84.882 665,84.882 C650.695,84.882 638.99,96.478 638.99,111 C638.99,125.522 650.695,136.901 665,136.901 C670.961,136.901 676.379,134.842 680.823,131.591 L674.212,124.872 C671.611,126.498 668.468,127.473 665.108,127.473 L665,127.581 Z" />
</g>
</g>
</svg>
```
## /Nook/Assets.xcassets/plainBackgroundColor.colorset/Contents.json
```json path="/Nook/Assets.xcassets/plainBackgroundColor.colorset/Contents.json"
{
"colors" : [
{
"color" : {
"color-space" : "extended-gray",
"components" : {
"alpha" : "1.000",
"white" : "0xFF"
}
},
"idiom" : "mac"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "extended-gray",
"components" : {
"alpha" : "1.000",
"white" : "0x00"
}
},
"idiom" : "mac"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/sidebar.imageset/Contents.json
```json path="/Nook/Assets.xcassets/sidebar.imageset/Contents.json"
{
"images" : [
{
"filename" : "sidebar.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/sidebar.imageset/sidebar.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/sidebar.imageset/sidebar.png
## /Nook/Assets.xcassets/top-of-window.imageset/Contents.json
```json path="/Nook/Assets.xcassets/top-of-window.imageset/Contents.json"
{
"images" : [
{
"filename" : "topofwindow.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/top-of-window.imageset/topofwindow.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/top-of-window.imageset/topofwindow.png
## /Nook/Assets.xcassets/tulips.imageset/Contents.json
```json path="/Nook/Assets.xcassets/tulips.imageset/Contents.json"
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "michael-loftus-aK4Slh-4uhU-unsplash.jpg",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/tulips.imageset/michael-loftus-aK4Slh-4uhU-unsplash.jpg
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/tulips.imageset/michael-loftus-aK4Slh-4uhU-unsplash.jpg
## /Nook/Assets.xcassets/url-in-sidebar.imageset/Contents.json
```json path="/Nook/Assets.xcassets/url-in-sidebar.imageset/Contents.json"
{
"images" : [
{
"filename" : "url-in-sidebar.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/url-in-sidebar.imageset/url-in-sidebar.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/url-in-sidebar.imageset/url-in-sidebar.png
## /Nook/Assets.xcassets/url-top-of-website.imageset/Contents.json
```json path="/Nook/Assets.xcassets/url-top-of-website.imageset/Contents.json"
{
"images" : [
{
"filename" : "url-top-of-website.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Assets.xcassets/url-top-of-website.imageset/url-top-of-website.png
Binary file available at https://raw.githubusercontent.com/nook-browser/Nook/refs/heads/main/Nook/Assets.xcassets/url-top-of-website.imageset/url-top-of-website.png
## /Nook/Assets.xcassets/windowBackgroundColor.colorset/Contents.json
```json path="/Nook/Assets.xcassets/windowBackgroundColor.colorset/Contents.json"
{
"colors" : [
{
"color" : {
"platform" : "osx",
"reference" : "windowBackgroundColor"
},
"idiom" : "mac"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"platform" : "osx",
"reference" : "windowBackgroundColor"
},
"idiom" : "mac"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
```
## /Nook/Components/Boosts - deprecated/BoostColorCanvas.swift
```swift path="/Nook/Components/Boosts - deprecated/BoostColorCanvas.swift"
//
// BoostColorCanvas.swift
// Nook
//
// Created by Jude on 11/11/2025.
//
import SwiftUI
#if canImport(AppKit)
import AppKit
#endif
// MARK: - BoostColorCanvas
// Circular canvas for picking tint color (similar to GradientCanvasEditor but single color)
struct BoostColorCanvas: View {
@Binding var selectedColor: Color
var onColorChange: ((Color) -> Void)?
@State private var handlePosition: CGPoint?
@State private var lightness: Double = 0.6
private let cornerRadius: CGFloat = 16
var body: some View {
GeometryReader { proxy in
let width = proxy.size.width
let height = proxy.size.height
let padding: CGFloat = 24
let center = CGPoint(x: width / 2, y: height / 2)
let radius = min(width, height) / 2 - padding
ZStack {
// Dot grid background
DotGrid()
.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
.allowsHitTesting(false)
// Draggable color handle
if let position = handlePosition {
let clamped = clampToCircle(point: position, center: center, radius: radius)
ColorHandle(color: selectedColor, size: 40)
.position(clamped)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
let clamped = clampToCircle(
point: value.location, center: center, radius: radius)
handlePosition = clamped
updateColorFromPosition(clamped, center: center, radius: radius)
}
)
}
// Border stroke
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.strokeBorder(Color.primary.opacity(0.15), lineWidth: 1)
.allowsHitTesting(false)
}
.onAppear {
if handlePosition == nil {
// Initialize handle position from current color
handlePosition = positionFromColor(
selectedColor, center: center, radius: radius)
}
}
}
.frame(height: 280)
}
// MARK: - Helper Methods
private func clampToCircle(point: CGPoint, center: CGPoint, radius: CGFloat) -> CGPoint {
let dx = point.x - center.x
let dy = point.y - center.y
let distance = sqrt(dx * dx + dy * dy)
if distance <= radius {
return point
} else {
let angle = atan2(dy, dx)
return CGPoint(
x: center.x + cos(angle) * radius,
y: center.y + sin(angle) * radius
)
}
}
private func updateColorFromPosition(_ position: CGPoint, center: CGPoint, radius: CGFloat) {
let color = colorFromCircle(
point: position, center: center, radius: radius, lightness: lightness)
selectedColor = color
onColorChange?(color)
}
private func colorFromCircle(
point: CGPoint, center: CGPoint, radius: CGFloat, lightness: Double
) -> Color {
let dx = point.x - center.x
let dy = point.y - center.y
let distance = sqrt(dx * dx + dy * dy)
// Angle determines hue (0-360 degrees)
var angle = atan2(dy, dx) * 180.0 / .pi
if angle < 0 { angle += 360 }
let hue = angle / 360.0
// Distance from center determines saturation (0-1)
let saturation = min(1.0, distance / radius)
// Use fixed lightness
return Color(hue: hue, saturation: saturation, brightness: lightness)
}
private func positionFromColor(_ color: Color, center: CGPoint, radius: CGFloat) -> CGPoint {
#if canImport(AppKit)
let nsColor = NSColor(color)
var hue: CGFloat = 0
var saturation: CGFloat = 0
var brightness: CGFloat = 0
var alpha: CGFloat = 0
nsColor.usingColorSpace(.deviceRGB)?.getHue(
&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
// Update lightness from color
lightness = brightness
// Convert hue to angle (0-360 degrees)
let angle = hue * 360.0 * .pi / 180.0
// Convert saturation to distance
let distance = saturation * radius
return CGPoint(
x: center.x + cos(angle) * distance,
y: center.y + sin(angle) * distance
)
#else
return center
#endif
}
}
// MARK: - ColorHandle
private struct ColorHandle: View {
let color: Color
let size: CGFloat
var body: some View {
ZStack {
Circle()
.fill(color)
.frame(width: size, height: size)
Circle()
.strokeBorder(Color.white, lineWidth: 3)
.frame(width: size, height: size)
Circle()
.strokeBorder(Color.black.opacity(0.2), lineWidth: 1)
.frame(width: size, height: size)
}
.shadow(color: Color.black.opacity(0.3), radius: 8, y: 4)
}
}
// MARK: - DotGrid (reuse from GradientCanvasEditor)
private struct DotGrid: View {
var body: some View {
Canvas { context, size in
let dotSize: CGFloat = 2
let spacing: CGFloat = 12
let dotColor = Color.primary.opacity(0.08)
let cols = Int(size.width / spacing)
let rows = Int(size.height / spacing)
for row in 0..<rows {
for col in 0..<cols {
let x = CGFloat(col) * spacing
let y = CGFloat(row) * spacing
let rect = CGRect(x: x, y: y, width: dotSize, height: dotSize)
context.fill(Path(ellipseIn: rect), with: .color(dotColor))
}
}
}
}
}
#Preview {
@Previewable @State var color = Color.red
return BoostColorCanvas(selectedColor: $color) { newColor in
print("Color changed: \(newColor)")
}
.padding(40)
}
```
## /Nook/Components/Boosts/BoostCodeButton.swift
```swift path="/Nook/Components/Boosts/BoostCodeButton.swift"
//
// BoostCodeButton.swift
// nook-components
//
// Created by Maciek Bagiński on 12/11/2025.
//
import SwiftUI
struct BoostCodeButton: View {
@State private var isHovered: Bool = false
var isActive: Bool
var onClick: () -> Void
var body: some View {
Button {
onClick()
} label: {
HStack {
Text("Code")
.font(
.system(size: 14, weight: .semibold, design: .rounded)
)
.foregroundStyle(
isActive ? .white.opacity(0.8) : .black.opacity(0.75)
)
Spacer()
Text("{}")
.font(
.system(size: 14, weight: .semibold, design: .rounded)
)
.foregroundStyle(
isActive ? .white.opacity(0.8) : .black.opacity(0.75)
)
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
.background(
isActive
? .black.opacity(0.8)
: isHovered ? .black.opacity(0.1) : .black.opacity(0.07)
)
.clipShape(RoundedRectangle(cornerRadius: 8))
.animation(.linear(duration: 0.1), value: isActive)
}
.buttonStyle(PlainButtonStyle())
.onHover { state in
isHovered = state
}
}
}
#Preview {
BoostCodeButton(isActive: false, onClick: {})
.frame(width: 300, height: 300)
}
```
## /Nook/Components/Boosts/BoostZapButton.swift
```swift path="/Nook/Components/Boosts/BoostZapButton.swift"
//
// BoostZapButton.swift
// nook-components
//
// Created by Maciek Bagiński on 12/11/2025.
//
import SwiftUI
struct BoostZapButton: View {
@State private var isHovered: Bool = false
@Binding var isActive: Bool
var onClick: () -> Void
var body: some View {
Button {
isActive.toggle()
onClick()
} label: {
HStack {
Text("Zap")
.font(
.system(size: 14, weight: .semibold, design: .rounded)
)
.foregroundStyle(
isActive ? .white.opacity(0.8) : .black.opacity(0.75)
)
Spacer()
Image(systemName: "bolt.fill")
.font(
.system(size: 14, weight: .semibold, design: .rounded)
)
.foregroundStyle(
isActive ? .white.opacity(0.8) : .black.opacity(0.75)
)
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
.background(
isActive
? .black.opacity(0.8)
: isHovered ? .black.opacity(0.1) : .black.opacity(0.07)
)
.clipShape(RoundedRectangle(cornerRadius: 8))
.animation(.linear(duration: 0.1), value: isActive)
}
.buttonStyle(PlainButtonStyle())
.onHover { state in
isHovered = state
}
}
}
#Preview {
@Previewable @State var isActive = false
BoostZapButton(isActive: $isActive, onClick: {})
.frame(width: 300, height: 300)
}
```
## /Nook/Components/Sidebar/URLBarFramePreferenceKey.swift
```swift path="/Nook/Components/Sidebar/URLBarFramePreferenceKey.swift"
import SwiftUI
struct URLBarFramePreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
```
The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.