Add sidebar help menu to footer (#958)
* Add sidebar help menu * Fix help menu test wiring * Fix help menu accessibility * Use native popup for help menu * Use icon button for sidebar help * Add feedback composer and feedback API * Allow preview builds without feedback env * Tighten feedback upload limits * Adjust sidebar footer padding * Tighten sidebar footer spacing * Add link affordances to help menu * Polish sidebar feedback composer * Move feedback icon to trailing edge * Normalize help menu trailing icon sizes * Enlarge help menu trailing icons * Reduce help menu link icon size * Shrink help menu link arrow * Reduce help menu link arrow again * Fix feedback message editor focus * Add send feedback keyboard shortcut * Polish feedback launch and delivery
This commit is contained in:
parent
bb052198e5
commit
29054dc709
17 changed files with 2771 additions and 28 deletions
|
|
@ -73,6 +73,7 @@
|
||||||
A5002000 /* THIRD_PARTY_LICENSES.md in Resources */ = {isa = PBXBuildFile; fileRef = A5002001 /* THIRD_PARTY_LICENSES.md */; };
|
A5002000 /* THIRD_PARTY_LICENSES.md in Resources */ = {isa = PBXBuildFile; fileRef = A5002001 /* THIRD_PARTY_LICENSES.md */; };
|
||||||
B9000012A1B2C3D4E5F60719 /* AutomationSocketUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000011A1B2C3D4E5F60719 /* AutomationSocketUITests.swift */; };
|
B9000012A1B2C3D4E5F60719 /* AutomationSocketUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000011A1B2C3D4E5F60719 /* AutomationSocketUITests.swift */; };
|
||||||
B8F266236A1A3D9A45BD840F /* SidebarResizeUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */; };
|
B8F266236A1A3D9A45BD840F /* SidebarResizeUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */; };
|
||||||
|
B8F266246A1A3D9A45BD840F /* SidebarHelpMenuUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F266256A1A3D9A45BD840F /* SidebarHelpMenuUITests.swift */; };
|
||||||
C0B4D9B0A1B2C3D4E5F60718 /* UpdatePillUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */; };
|
C0B4D9B0A1B2C3D4E5F60718 /* UpdatePillUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */; };
|
||||||
B9000014A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000013A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift */; };
|
B9000014A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000013A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift */; };
|
||||||
B9000015A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000016A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift */; };
|
B9000015A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000016A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift */; };
|
||||||
|
|
@ -203,6 +204,7 @@
|
||||||
A5001241 /* WindowDecorationsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDecorationsController.swift; sourceTree = "<group>"; };
|
A5001241 /* WindowDecorationsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDecorationsController.swift; sourceTree = "<group>"; };
|
||||||
A5001611 /* SessionPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPersistence.swift; sourceTree = "<group>"; };
|
A5001611 /* SessionPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPersistence.swift; sourceTree = "<group>"; };
|
||||||
818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarResizeUITests.swift; sourceTree = "<group>"; };
|
818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarResizeUITests.swift; sourceTree = "<group>"; };
|
||||||
|
B8F266256A1A3D9A45BD840F /* SidebarHelpMenuUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarHelpMenuUITests.swift; sourceTree = "<group>"; };
|
||||||
C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePillUITests.swift; sourceTree = "<group>"; };
|
C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePillUITests.swift; sourceTree = "<group>"; };
|
||||||
A5001101 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
A5001101 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
B2E7294509CC42FE9191870E /* xterm-ghostty */ = {isa = PBXFileReference; lastKnownFileType = file; path = "ghostty/terminfo/78/xterm-ghostty"; sourceTree = "<group>"; };
|
B2E7294509CC42FE9191870E /* xterm-ghostty */ = {isa = PBXFileReference; lastKnownFileType = file; path = "ghostty/terminfo/78/xterm-ghostty"; sourceTree = "<group>"; };
|
||||||
|
|
@ -433,6 +435,7 @@
|
||||||
B9000019A1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift */,
|
B9000019A1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift */,
|
||||||
B9000016A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift */,
|
B9000016A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift */,
|
||||||
818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */,
|
818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */,
|
||||||
|
B8F266256A1A3D9A45BD840F /* SidebarHelpMenuUITests.swift */,
|
||||||
D0E0F0B1A1B2C3D4E5F60718 /* BrowserPaneNavigationKeybindUITests.swift */,
|
D0E0F0B1A1B2C3D4E5F60718 /* BrowserPaneNavigationKeybindUITests.swift */,
|
||||||
D0E0F0B3A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift */,
|
D0E0F0B3A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift */,
|
||||||
C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */,
|
C0B4D9B1A1B2C3D4E5F60718 /* UpdatePillUITests.swift */,
|
||||||
|
|
@ -667,6 +670,7 @@
|
||||||
B9000023A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift in Sources */,
|
B9000023A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift in Sources */,
|
||||||
B9000015A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift in Sources */,
|
B9000015A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift in Sources */,
|
||||||
B8F266236A1A3D9A45BD840F /* SidebarResizeUITests.swift in Sources */,
|
B8F266236A1A3D9A45BD840F /* SidebarResizeUITests.swift in Sources */,
|
||||||
|
B8F266246A1A3D9A45BD840F /* SidebarHelpMenuUITests.swift in Sources */,
|
||||||
D0E0F0B0A1B2C3D4E5F60718 /* BrowserPaneNavigationKeybindUITests.swift in Sources */,
|
D0E0F0B0A1B2C3D4E5F60718 /* BrowserPaneNavigationKeybindUITests.swift in Sources */,
|
||||||
D0E0F0B2A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift in Sources */,
|
D0E0F0B2A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift in Sources */,
|
||||||
C0B4D9B0A1B2C3D4E5F60718 /* UpdatePillUITests.swift in Sources */,
|
C0B4D9B0A1B2C3D4E5F60718 /* UpdatePillUITests.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -567,6 +567,584 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"debug.devBuildBanner.show": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Show Dev Build Banner"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "開発ビルドバナーを表示"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"debug.devBuildBanner.title": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "THIS IS A DEV BUILD"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "これは開発ビルドです"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.button": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Help"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "ヘルプ"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.changelog": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Changelog"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "更新履歴"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.githubIssues": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "GitHub Issues"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "GitHub Issues"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.sendFeedback": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Send Feedback"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "フィードバックを送信"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.attachImages": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Attach Images"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "画像を添付"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.attachImages.prompt": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Attach"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "添付"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.attachImages.title": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Attach Images"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "画像を添付"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.attachmentsHint": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Up to 10 images. Large images will be optimized before sending."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "画像は最大10枚まで添付できます。大きな画像は送信前に最適化されます。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.cancel": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Cancel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "キャンセル"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.connectionError": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Couldn't send feedback. Check your connection and try again."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "フィードバックを送信できませんでした。接続を確認して、もう一度お試しください。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.done": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Done"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "完了"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.email": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Your Email"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "メールアドレス"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.emailPlaceholder": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "you@example.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "you@example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.emptyMessage": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Enter a message before sending."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "送信する前にメッセージを入力してください。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.endpointError": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Feedback is unavailable right now. Email founders@manaflow.com instead."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "現在フィードバックを送信できません。代わりに founders@manaflow.com までメールしてください。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.genericError": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Couldn't send feedback. Please try again."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "フィードバックを送信できませんでした。もう一度お試しください。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.imageTooLarge": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Each image must be 4 MB or smaller."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "各画像は 4 MB 以下にしてください。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.invalidEmail": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Enter a valid email address."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "有効なメールアドレスを入力してください。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.invalidImageSelection": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "One of the selected files could not be attached."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "選択したファイルのうち1つを添付できませんでした。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.message": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Message"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "メッセージ"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.messagePlaceholder": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Share feedback, feature requests, or issues."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "フィードバック、機能要望、不具合をお知らせください。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.messageTooLong": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Your message is too long."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "メッセージが長すぎます。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.note": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "A human will read this! You can also reach us at founders@manaflow.com."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "人間がこれを読みます。founders@manaflow.com 宛てに直接ご連絡いただくこともできます。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.rateLimited": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Too many feedback attempts. Please try again later."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "フィードバックの送信回数が多すぎます。しばらくしてからもう一度お試しください。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.removeAttachment": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Remove"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "削除"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.send": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Send"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "送信"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.successBody": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "A human will read this! You can also reach us at founders@manaflow.com."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "人間がこれを読みます。founders@manaflow.com 宛てに直接ご連絡いただくこともできます。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.successTitle": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Thanks for the feedback."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "フィードバックありがとうございます。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.title": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Send Feedback"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "フィードバックを送信"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.tooManyImages": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "You can attach up to 10 images."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "画像は最大10枚まで添付できます。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.totalImagesTooLarge": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "These images are too large to send together. Remove a few and try again."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "これらの画像はまとめて送信するには大きすぎます。いくつか削除してもう一度お試しください。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar.help.feedback.validationError": {
|
||||||
|
"extractionState": "manual",
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Check your message and attachments, then try again."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "メッセージと添付ファイルを確認して、もう一度お試しください。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"about.github": {
|
"about.github": {
|
||||||
"extractionState": "manual",
|
"extractionState": "manual",
|
||||||
"localizations": {
|
"localizations": {
|
||||||
|
|
|
||||||
|
|
@ -4858,8 +4858,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
static func presentPreferencesWindow(
|
static func presentPreferencesWindow(
|
||||||
showFallbackSettingsWindow: @MainActor () -> Void = {
|
navigationTarget: SettingsNavigationTarget? = nil,
|
||||||
SettingsWindowController.shared.show()
|
showFallbackSettingsWindow: @MainActor (SettingsNavigationTarget?) -> Void = { target in
|
||||||
|
SettingsWindowController.shared.show(navigationTarget: target)
|
||||||
},
|
},
|
||||||
activateApplication: @MainActor () -> Void = {
|
activateApplication: @MainActor () -> Void = {
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
|
@ -4868,7 +4869,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
dlog("settings.open.present path=customWindowDirect")
|
dlog("settings.open.present path=customWindowDirect")
|
||||||
#endif
|
#endif
|
||||||
showFallbackSettingsWindow()
|
showFallbackSettingsWindow(navigationTarget)
|
||||||
activateApplication()
|
activateApplication()
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
dlog("settings.open.present activate=1")
|
dlog("settings.open.present activate=1")
|
||||||
|
|
@ -4876,11 +4877,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func openPreferencesWindow(debugSource: String) {
|
func openPreferencesWindow(debugSource: String, navigationTarget: SettingsNavigationTarget? = nil) {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
dlog("settings.open.request source=\(debugSource)")
|
dlog("settings.open.request source=\(debugSource)")
|
||||||
#endif
|
#endif
|
||||||
Self.presentPreferencesWindow()
|
Self.presentPreferencesWindow(navigationTarget: navigationTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func openPreferencesWindow() {
|
@objc func openPreferencesWindow() {
|
||||||
|
|
@ -6610,6 +6611,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if matchShortcut(event: event, shortcut: KeyboardShortcutSettings.shortcut(for: .sendFeedback)) {
|
||||||
|
guard let targetContext = preferredMainWindowContextForShortcuts(event: event),
|
||||||
|
let targetWindow = targetContext.window ?? windowForMainWindowId(targetContext.windowId) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
setActiveMainWindow(targetWindow)
|
||||||
|
bringToFront(targetWindow)
|
||||||
|
NotificationCenter.default.post(name: .feedbackComposerRequested, object: targetWindow)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Check Jump to Unread shortcut
|
// Check Jump to Unread shortcut
|
||||||
if matchShortcut(event: event, shortcut: KeyboardShortcutSettings.shortcut(for: .jumpToUnread)) {
|
if matchShortcut(event: event, shortcut: KeyboardShortcutSettings.shortcut(for: .jumpToUnread)) {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -10,6 +10,7 @@ enum KeyboardShortcutSettings {
|
||||||
case newWindow
|
case newWindow
|
||||||
case closeWindow
|
case closeWindow
|
||||||
case openFolder
|
case openFolder
|
||||||
|
case sendFeedback
|
||||||
case showNotifications
|
case showNotifications
|
||||||
case jumpToUnread
|
case jumpToUnread
|
||||||
case triggerFlash
|
case triggerFlash
|
||||||
|
|
@ -50,6 +51,7 @@ enum KeyboardShortcutSettings {
|
||||||
case .newWindow: return String(localized: "shortcut.newWindow.label", defaultValue: "New Window")
|
case .newWindow: return String(localized: "shortcut.newWindow.label", defaultValue: "New Window")
|
||||||
case .closeWindow: return String(localized: "shortcut.closeWindow.label", defaultValue: "Close Window")
|
case .closeWindow: return String(localized: "shortcut.closeWindow.label", defaultValue: "Close Window")
|
||||||
case .openFolder: return String(localized: "shortcut.openFolder.label", defaultValue: "Open Folder")
|
case .openFolder: return String(localized: "shortcut.openFolder.label", defaultValue: "Open Folder")
|
||||||
|
case .sendFeedback: return String(localized: "sidebar.help.sendFeedback", defaultValue: "Send Feedback")
|
||||||
case .showNotifications: return String(localized: "shortcut.showNotifications.label", defaultValue: "Show Notifications")
|
case .showNotifications: return String(localized: "shortcut.showNotifications.label", defaultValue: "Show Notifications")
|
||||||
case .jumpToUnread: return String(localized: "shortcut.jumpToUnread.label", defaultValue: "Jump to Latest Unread")
|
case .jumpToUnread: return String(localized: "shortcut.jumpToUnread.label", defaultValue: "Jump to Latest Unread")
|
||||||
case .triggerFlash: return String(localized: "shortcut.flashFocusedPanel.label", defaultValue: "Flash Focused Panel")
|
case .triggerFlash: return String(localized: "shortcut.flashFocusedPanel.label", defaultValue: "Flash Focused Panel")
|
||||||
|
|
@ -84,6 +86,7 @@ enum KeyboardShortcutSettings {
|
||||||
case .newWindow: return "shortcut.newWindow"
|
case .newWindow: return "shortcut.newWindow"
|
||||||
case .closeWindow: return "shortcut.closeWindow"
|
case .closeWindow: return "shortcut.closeWindow"
|
||||||
case .openFolder: return "shortcut.openFolder"
|
case .openFolder: return "shortcut.openFolder"
|
||||||
|
case .sendFeedback: return "shortcut.sendFeedback"
|
||||||
case .showNotifications: return "shortcut.showNotifications"
|
case .showNotifications: return "shortcut.showNotifications"
|
||||||
case .jumpToUnread: return "shortcut.jumpToUnread"
|
case .jumpToUnread: return "shortcut.jumpToUnread"
|
||||||
case .triggerFlash: return "shortcut.triggerFlash"
|
case .triggerFlash: return "shortcut.triggerFlash"
|
||||||
|
|
@ -123,6 +126,8 @@ enum KeyboardShortcutSettings {
|
||||||
return StoredShortcut(key: "w", command: true, shift: false, option: false, control: true)
|
return StoredShortcut(key: "w", command: true, shift: false, option: false, control: true)
|
||||||
case .openFolder:
|
case .openFolder:
|
||||||
return StoredShortcut(key: "o", command: true, shift: false, option: false, control: false)
|
return StoredShortcut(key: "o", command: true, shift: false, option: false, control: false)
|
||||||
|
case .sendFeedback:
|
||||||
|
return StoredShortcut(key: "f", command: true, shift: false, option: true, control: false)
|
||||||
case .showNotifications:
|
case .showNotifications:
|
||||||
return StoredShortcut(key: "i", command: true, shift: false, option: false, control: false)
|
return StoredShortcut(key: "i", command: true, shift: false, option: false, control: false)
|
||||||
case .jumpToUnread:
|
case .jumpToUnread:
|
||||||
|
|
|
||||||
|
|
@ -3676,6 +3676,7 @@ extension Notification.Name {
|
||||||
static let commandPaletteMoveSelection = Notification.Name("cmux.commandPaletteMoveSelection")
|
static let commandPaletteMoveSelection = Notification.Name("cmux.commandPaletteMoveSelection")
|
||||||
static let commandPaletteRenameInputInteractionRequested = Notification.Name("cmux.commandPaletteRenameInputInteractionRequested")
|
static let commandPaletteRenameInputInteractionRequested = Notification.Name("cmux.commandPaletteRenameInputInteractionRequested")
|
||||||
static let commandPaletteRenameInputDeleteBackwardRequested = Notification.Name("cmux.commandPaletteRenameInputDeleteBackwardRequested")
|
static let commandPaletteRenameInputDeleteBackwardRequested = Notification.Name("cmux.commandPaletteRenameInputDeleteBackwardRequested")
|
||||||
|
static let feedbackComposerRequested = Notification.Name("cmux.feedbackComposerRequested")
|
||||||
static let ghosttyDidSetTitle = Notification.Name("ghosttyDidSetTitle")
|
static let ghosttyDidSetTitle = Notification.Name("ghosttyDidSetTitle")
|
||||||
static let ghosttyDidFocusTab = Notification.Name("ghosttyDidFocusTab")
|
static let ghosttyDidFocusTab = Notification.Name("ghosttyDidFocusTab")
|
||||||
static let ghosttyDidFocusSurface = Notification.Name("ghosttyDidFocusSurface")
|
static let ghosttyDidFocusSurface = Notification.Name("ghosttyDidFocusSurface")
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ struct cmuxApp: App {
|
||||||
@AppStorage(AppearanceSettings.appearanceModeKey) private var appearanceMode = AppearanceSettings.defaultMode.rawValue
|
@AppStorage(AppearanceSettings.appearanceModeKey) private var appearanceMode = AppearanceSettings.defaultMode.rawValue
|
||||||
@AppStorage("titlebarControlsStyle") private var titlebarControlsStyle = TitlebarControlsStyle.classic.rawValue
|
@AppStorage("titlebarControlsStyle") private var titlebarControlsStyle = TitlebarControlsStyle.classic.rawValue
|
||||||
@AppStorage(ShortcutHintDebugSettings.alwaysShowHintsKey) private var alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints
|
@AppStorage(ShortcutHintDebugSettings.alwaysShowHintsKey) private var alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints
|
||||||
|
@AppStorage(DevBuildBannerDebugSettings.sidebarBannerVisibleKey)
|
||||||
|
private var showSidebarDevBuildBanner = DevBuildBannerDebugSettings.defaultShowSidebarBanner
|
||||||
@AppStorage(SocketControlSettings.appStorageKey) private var socketControlMode = SocketControlSettings.defaultMode.rawValue
|
@AppStorage(SocketControlSettings.appStorageKey) private var socketControlMode = SocketControlSettings.defaultMode.rawValue
|
||||||
@AppStorage(KeyboardShortcutSettings.Action.toggleSidebar.defaultsKey) private var toggleSidebarShortcutData = Data()
|
@AppStorage(KeyboardShortcutSettings.Action.toggleSidebar.defaultsKey) private var toggleSidebarShortcutData = Data()
|
||||||
@AppStorage(KeyboardShortcutSettings.Action.newTab.defaultsKey) private var newWorkspaceShortcutData = Data()
|
@AppStorage(KeyboardShortcutSettings.Action.newTab.defaultsKey) private var newWorkspaceShortcutData = Data()
|
||||||
|
|
@ -360,6 +362,10 @@ struct cmuxApp: App {
|
||||||
}
|
}
|
||||||
|
|
||||||
Toggle("Always Show Shortcut Hints", isOn: $alwaysShowShortcutHints)
|
Toggle("Always Show Shortcut Hints", isOn: $alwaysShowShortcutHints)
|
||||||
|
Toggle(
|
||||||
|
String(localized: "debug.devBuildBanner.show", defaultValue: "Show Dev Build Banner"),
|
||||||
|
isOn: $showSidebarDevBuildBanner
|
||||||
|
)
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
|
|
@ -1321,6 +1327,7 @@ private enum DebugWindowConfigSnapshot {
|
||||||
sidebarCornerRadius=\(String(format: "%.1f", doubleValue(defaults, key: "sidebarCornerRadius", fallback: 0.0)))
|
sidebarCornerRadius=\(String(format: "%.1f", doubleValue(defaults, key: "sidebarCornerRadius", fallback: 0.0)))
|
||||||
sidebarBranchVerticalLayout=\(boolValue(defaults, key: SidebarBranchLayoutSettings.key, fallback: SidebarBranchLayoutSettings.defaultVerticalLayout))
|
sidebarBranchVerticalLayout=\(boolValue(defaults, key: SidebarBranchLayoutSettings.key, fallback: SidebarBranchLayoutSettings.defaultVerticalLayout))
|
||||||
sidebarActiveTabIndicatorStyle=\(stringValue(defaults, key: SidebarActiveTabIndicatorSettings.styleKey, fallback: SidebarActiveTabIndicatorSettings.defaultStyle.rawValue))
|
sidebarActiveTabIndicatorStyle=\(stringValue(defaults, key: SidebarActiveTabIndicatorSettings.styleKey, fallback: SidebarActiveTabIndicatorSettings.defaultStyle.rawValue))
|
||||||
|
sidebarDevBuildBannerVisible=\(boolValue(defaults, key: DevBuildBannerDebugSettings.sidebarBannerVisibleKey, fallback: DevBuildBannerDebugSettings.defaultShowSidebarBanner))
|
||||||
shortcutHintSidebarXOffset=\(String(format: "%.1f", doubleValue(defaults, key: ShortcutHintDebugSettings.sidebarHintXKey, fallback: ShortcutHintDebugSettings.defaultSidebarHintX)))
|
shortcutHintSidebarXOffset=\(String(format: "%.1f", doubleValue(defaults, key: ShortcutHintDebugSettings.sidebarHintXKey, fallback: ShortcutHintDebugSettings.defaultSidebarHintX)))
|
||||||
shortcutHintSidebarYOffset=\(String(format: "%.1f", doubleValue(defaults, key: ShortcutHintDebugSettings.sidebarHintYKey, fallback: ShortcutHintDebugSettings.defaultSidebarHintY)))
|
shortcutHintSidebarYOffset=\(String(format: "%.1f", doubleValue(defaults, key: ShortcutHintDebugSettings.sidebarHintYKey, fallback: ShortcutHintDebugSettings.defaultSidebarHintY)))
|
||||||
shortcutHintTitlebarXOffset=\(String(format: "%.1f", doubleValue(defaults, key: ShortcutHintDebugSettings.titlebarHintXKey, fallback: ShortcutHintDebugSettings.defaultTitlebarHintX)))
|
shortcutHintTitlebarXOffset=\(String(format: "%.1f", doubleValue(defaults, key: ShortcutHintDebugSettings.titlebarHintXKey, fallback: ShortcutHintDebugSettings.defaultTitlebarHintX)))
|
||||||
|
|
@ -1775,7 +1782,7 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func show() {
|
func show(navigationTarget: SettingsNavigationTarget? = nil) {
|
||||||
guard let window else { return }
|
guard let window else { return }
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
dlog("settings.window.show requested isVisible=\(window.isVisible ? 1 : 0) isKey=\(window.isKeyWindow ? 1 : 0)")
|
dlog("settings.window.show requested isVisible=\(window.isVisible ? 1 : 0) isKey=\(window.isKeyWindow ? 1 : 0)")
|
||||||
|
|
@ -1785,12 +1792,39 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
|
||||||
window.center()
|
window.center()
|
||||||
}
|
}
|
||||||
window.makeKeyAndOrderFront(nil)
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
if let navigationTarget {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||||
|
SettingsNavigationRequest.post(navigationTarget)
|
||||||
|
}
|
||||||
|
}
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
dlog("settings.window.show completed isVisible=\(window.isVisible ? 1 : 0) isKey=\(window.isKeyWindow ? 1 : 0)")
|
dlog("settings.window.show completed isVisible=\(window.isVisible ? 1 : 0) isKey=\(window.isKeyWindow ? 1 : 0)")
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum SettingsNavigationTarget: String {
|
||||||
|
case keyboardShortcuts
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SettingsNavigationRequest {
|
||||||
|
static let notificationName = Notification.Name("cmux.settings.navigate")
|
||||||
|
private static let targetKey = "target"
|
||||||
|
|
||||||
|
static func post(_ target: SettingsNavigationTarget) {
|
||||||
|
NotificationCenter.default.post(
|
||||||
|
name: notificationName,
|
||||||
|
object: nil,
|
||||||
|
userInfo: [targetKey: target.rawValue]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func target(from notification: Notification) -> SettingsNavigationTarget? {
|
||||||
|
guard let rawValue = notification.userInfo?[targetKey] as? String else { return nil }
|
||||||
|
return SettingsNavigationTarget(rawValue: rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final class SidebarDebugWindowController: NSWindowController, NSWindowDelegate {
|
private final class SidebarDebugWindowController: NSWindowController, NSWindowDelegate {
|
||||||
static let shared = SidebarDebugWindowController()
|
static let shared = SidebarDebugWindowController()
|
||||||
|
|
||||||
|
|
@ -1931,6 +1965,8 @@ private struct SidebarDebugView: View {
|
||||||
@AppStorage(ShortcutHintDebugSettings.paneHintXKey) private var paneShortcutHintXOffset = ShortcutHintDebugSettings.defaultPaneHintX
|
@AppStorage(ShortcutHintDebugSettings.paneHintXKey) private var paneShortcutHintXOffset = ShortcutHintDebugSettings.defaultPaneHintX
|
||||||
@AppStorage(ShortcutHintDebugSettings.paneHintYKey) private var paneShortcutHintYOffset = ShortcutHintDebugSettings.defaultPaneHintY
|
@AppStorage(ShortcutHintDebugSettings.paneHintYKey) private var paneShortcutHintYOffset = ShortcutHintDebugSettings.defaultPaneHintY
|
||||||
@AppStorage(ShortcutHintDebugSettings.alwaysShowHintsKey) private var alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints
|
@AppStorage(ShortcutHintDebugSettings.alwaysShowHintsKey) private var alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints
|
||||||
|
@AppStorage(DevBuildBannerDebugSettings.sidebarBannerVisibleKey)
|
||||||
|
private var showSidebarDevBuildBanner = DevBuildBannerDebugSettings.defaultShowSidebarBanner
|
||||||
@AppStorage(SidebarActiveTabIndicatorSettings.styleKey)
|
@AppStorage(SidebarActiveTabIndicatorSettings.styleKey)
|
||||||
private var sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue
|
private var sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue
|
||||||
|
|
||||||
|
|
@ -2154,6 +2190,7 @@ private struct SidebarDebugView: View {
|
||||||
sidebarCornerRadius=\(String(format: "%.1f", sidebarCornerRadius))
|
sidebarCornerRadius=\(String(format: "%.1f", sidebarCornerRadius))
|
||||||
sidebarBranchVerticalLayout=\(sidebarBranchVerticalLayout)
|
sidebarBranchVerticalLayout=\(sidebarBranchVerticalLayout)
|
||||||
sidebarActiveTabIndicatorStyle=\(sidebarActiveTabIndicatorStyle)
|
sidebarActiveTabIndicatorStyle=\(sidebarActiveTabIndicatorStyle)
|
||||||
|
sidebarDevBuildBannerVisible=\(showSidebarDevBuildBanner)
|
||||||
shortcutHintSidebarXOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(sidebarShortcutHintXOffset)))
|
shortcutHintSidebarXOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(sidebarShortcutHintXOffset)))
|
||||||
shortcutHintSidebarYOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(sidebarShortcutHintYOffset)))
|
shortcutHintSidebarYOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(sidebarShortcutHintYOffset)))
|
||||||
shortcutHintTitlebarXOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(titlebarShortcutHintXOffset)))
|
shortcutHintTitlebarXOffset=\(String(format: "%.1f", ShortcutHintDebugSettings.clamped(titlebarShortcutHintXOffset)))
|
||||||
|
|
@ -3168,7 +3205,8 @@ struct SettingsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .top) {
|
ScrollViewReader { proxy in
|
||||||
|
ZStack(alignment: .top) {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading, spacing: 14) {
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
SettingsSectionHeader(title: String(localized: "settings.section.app", defaultValue: "App"))
|
SettingsSectionHeader(title: String(localized: "settings.section.app", defaultValue: "App"))
|
||||||
|
|
@ -3864,6 +3902,8 @@ struct SettingsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsSectionHeader(title: String(localized: "settings.section.keyboardShortcuts", defaultValue: "Keyboard Shortcuts"))
|
SettingsSectionHeader(title: String(localized: "settings.section.keyboardShortcuts", defaultValue: "Keyboard Shortcuts"))
|
||||||
|
.id(SettingsNavigationTarget.keyboardShortcuts)
|
||||||
|
.accessibilityIdentifier("SettingsKeyboardShortcutsSection")
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
SettingsCardRow(
|
SettingsCardRow(
|
||||||
String(localized: "settings.shortcuts.showHints", defaultValue: "Show Cmd/Ctrl-Hold Shortcut Hints"),
|
String(localized: "settings.shortcuts.showHints", defaultValue: "Show Cmd/Ctrl-Hold Shortcut Hints"),
|
||||||
|
|
@ -3894,6 +3934,7 @@ struct SettingsView: View {
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.padding(.leading, 2)
|
.padding(.leading, 2)
|
||||||
|
.accessibilityIdentifier("ShortcutRecordingHint")
|
||||||
|
|
||||||
SettingsSectionHeader(title: String(localized: "settings.section.reset", defaultValue: "Reset"))
|
SettingsSectionHeader(title: String(localized: "settings.section.reset", defaultValue: "Reset"))
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
|
|
@ -4013,6 +4054,14 @@ struct SettingsView: View {
|
||||||
.onReceive(NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)) { _ in
|
||||||
reloadWorkspaceTabColorSettings()
|
reloadWorkspaceTabColorSettings()
|
||||||
}
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: SettingsNavigationRequest.notificationName)) { notification in
|
||||||
|
guard let target = SettingsNavigationRequest.target(from: notification) else { return }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
|
proxy.scrollTo(target, anchor: .top)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.confirmationDialog(
|
.confirmationDialog(
|
||||||
String(localized: "settings.browser.history.clearDialog.title", defaultValue: "Clear browser history?"),
|
String(localized: "settings.browser.history.clearDialog.title", defaultValue: "Clear browser history?"),
|
||||||
isPresented: $showClearBrowserHistoryConfirmation,
|
isPresented: $showClearBrowserHistoryConfirmation,
|
||||||
|
|
@ -4061,6 +4110,7 @@ struct SettingsView: View {
|
||||||
} message: {
|
} message: {
|
||||||
Text(notificationCustomSoundErrorAlertMessage)
|
Text(notificationCustomSoundErrorAlertMessage)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func relaunchApp() {
|
private func relaunchApp() {
|
||||||
|
|
|
||||||
|
|
@ -2071,9 +2071,11 @@ final class AppDelegateShortcutRoutingTests: XCTestCase {
|
||||||
func testPresentPreferencesWindowShowsCustomSettingsWindowAndActivates() {
|
func testPresentPreferencesWindowShowsCustomSettingsWindowAndActivates() {
|
||||||
var showFallbackSettingsWindowCallCount = 0
|
var showFallbackSettingsWindowCallCount = 0
|
||||||
var activateApplicationCallCount = 0
|
var activateApplicationCallCount = 0
|
||||||
|
var receivedNavigationTargets: [SettingsNavigationTarget?] = []
|
||||||
|
|
||||||
AppDelegate.presentPreferencesWindow(
|
AppDelegate.presentPreferencesWindow(
|
||||||
showFallbackSettingsWindow: {
|
showFallbackSettingsWindow: { navigationTarget in
|
||||||
|
receivedNavigationTargets.append(navigationTarget)
|
||||||
showFallbackSettingsWindowCallCount += 1
|
showFallbackSettingsWindowCallCount += 1
|
||||||
},
|
},
|
||||||
activateApplication: {
|
activateApplication: {
|
||||||
|
|
@ -2083,14 +2085,17 @@ final class AppDelegateShortcutRoutingTests: XCTestCase {
|
||||||
|
|
||||||
XCTAssertEqual(showFallbackSettingsWindowCallCount, 1)
|
XCTAssertEqual(showFallbackSettingsWindowCallCount, 1)
|
||||||
XCTAssertEqual(activateApplicationCallCount, 1)
|
XCTAssertEqual(activateApplicationCallCount, 1)
|
||||||
|
XCTAssertEqual(receivedNavigationTargets, [nil])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPresentPreferencesWindowSupportsRepeatedCalls() {
|
func testPresentPreferencesWindowSupportsRepeatedCalls() {
|
||||||
var showFallbackSettingsWindowCallCount = 0
|
var showFallbackSettingsWindowCallCount = 0
|
||||||
var activateApplicationCallCount = 0
|
var activateApplicationCallCount = 0
|
||||||
|
var receivedNavigationTargets: [SettingsNavigationTarget?] = []
|
||||||
|
|
||||||
AppDelegate.presentPreferencesWindow(
|
AppDelegate.presentPreferencesWindow(
|
||||||
showFallbackSettingsWindow: {
|
showFallbackSettingsWindow: { navigationTarget in
|
||||||
|
receivedNavigationTargets.append(navigationTarget)
|
||||||
showFallbackSettingsWindowCallCount += 1
|
showFallbackSettingsWindowCallCount += 1
|
||||||
},
|
},
|
||||||
activateApplication: {
|
activateApplication: {
|
||||||
|
|
@ -2099,7 +2104,8 @@ final class AppDelegateShortcutRoutingTests: XCTestCase {
|
||||||
)
|
)
|
||||||
|
|
||||||
AppDelegate.presentPreferencesWindow(
|
AppDelegate.presentPreferencesWindow(
|
||||||
showFallbackSettingsWindow: {
|
showFallbackSettingsWindow: { navigationTarget in
|
||||||
|
receivedNavigationTargets.append(navigationTarget)
|
||||||
showFallbackSettingsWindowCallCount += 1
|
showFallbackSettingsWindowCallCount += 1
|
||||||
},
|
},
|
||||||
activateApplication: {
|
activateApplication: {
|
||||||
|
|
@ -2109,6 +2115,25 @@ final class AppDelegateShortcutRoutingTests: XCTestCase {
|
||||||
|
|
||||||
XCTAssertEqual(showFallbackSettingsWindowCallCount, 2)
|
XCTAssertEqual(showFallbackSettingsWindowCallCount, 2)
|
||||||
XCTAssertEqual(activateApplicationCallCount, 2)
|
XCTAssertEqual(activateApplicationCallCount, 2)
|
||||||
|
XCTAssertEqual(receivedNavigationTargets, [nil, nil])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPresentPreferencesWindowForwardsNavigationTarget() {
|
||||||
|
var receivedNavigationTarget: SettingsNavigationTarget?
|
||||||
|
var activateApplicationCallCount = 0
|
||||||
|
|
||||||
|
AppDelegate.presentPreferencesWindow(
|
||||||
|
navigationTarget: .keyboardShortcuts,
|
||||||
|
showFallbackSettingsWindow: { navigationTarget in
|
||||||
|
receivedNavigationTarget = navigationTarget
|
||||||
|
},
|
||||||
|
activateApplication: {
|
||||||
|
activateApplicationCallCount += 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(receivedNavigationTarget, .keyboardShortcuts)
|
||||||
|
XCTAssertEqual(activateApplicationCallCount, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeKeyDownEvent(
|
private func makeKeyDownEvent(
|
||||||
|
|
|
||||||
|
|
@ -3590,6 +3590,35 @@ final class ShortcutHintDebugSettingsTests: XCTestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class DevBuildBannerDebugSettingsTests: XCTestCase {
|
||||||
|
func testShowSidebarBannerDefaultsToVisible() {
|
||||||
|
let suiteName = "DevBuildBannerDebugSettingsTests.Default.\(UUID().uuidString)"
|
||||||
|
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
||||||
|
XCTFail("Failed to create isolated UserDefaults suite")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||||
|
|
||||||
|
defaults.removeObject(forKey: DevBuildBannerDebugSettings.sidebarBannerVisibleKey)
|
||||||
|
XCTAssertTrue(DevBuildBannerDebugSettings.showSidebarBanner(defaults: defaults))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testShowSidebarBannerRespectsStoredValue() {
|
||||||
|
let suiteName = "DevBuildBannerDebugSettingsTests.Stored.\(UUID().uuidString)"
|
||||||
|
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
||||||
|
XCTFail("Failed to create isolated UserDefaults suite")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||||
|
|
||||||
|
defaults.set(false, forKey: DevBuildBannerDebugSettings.sidebarBannerVisibleKey)
|
||||||
|
XCTAssertFalse(DevBuildBannerDebugSettings.showSidebarBanner(defaults: defaults))
|
||||||
|
|
||||||
|
defaults.set(true, forKey: DevBuildBannerDebugSettings.sidebarBannerVisibleKey)
|
||||||
|
XCTAssertTrue(DevBuildBannerDebugSettings.showSidebarBanner(defaults: defaults))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class ShortcutHintLanePlannerTests: XCTestCase {
|
final class ShortcutHintLanePlannerTests: XCTestCase {
|
||||||
func testAssignLanesKeepsSeparatedIntervalsOnSingleLane() {
|
func testAssignLanesKeepsSeparatedIntervalsOnSingleLane() {
|
||||||
let intervals: [ClosedRange<CGFloat>] = [0...20, 28...40, 48...64]
|
let intervals: [ClosedRange<CGFloat>] = [0...20, 28...40, 48...64]
|
||||||
|
|
|
||||||
296
cmuxUITests/SidebarHelpMenuUITests.swift
Normal file
296
cmuxUITests/SidebarHelpMenuUITests.swift
Normal file
|
|
@ -0,0 +1,296 @@
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
private func sidebarHelpPollUntil(
|
||||||
|
timeout: TimeInterval,
|
||||||
|
pollInterval: TimeInterval = 0.05,
|
||||||
|
condition: () -> Bool
|
||||||
|
) -> Bool {
|
||||||
|
let start = ProcessInfo.processInfo.systemUptime
|
||||||
|
while true {
|
||||||
|
if condition() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (ProcessInfo.processInfo.systemUptime - start) >= timeout {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
RunLoop.current.run(until: Date().addingTimeInterval(pollInterval))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class SidebarHelpMenuUITests: XCTestCase {
|
||||||
|
override func setUp() {
|
||||||
|
super.setUp()
|
||||||
|
continueAfterFailure = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHelpMenuOpensKeyboardShortcutsSection() {
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
||||||
|
launchAndActivate(app)
|
||||||
|
|
||||||
|
XCTAssertTrue(waitForWindowCount(atLeast: 1, app: app, timeout: 6.0))
|
||||||
|
|
||||||
|
let helpButton = requireElement(
|
||||||
|
candidates: helpButtonCandidates(in: app),
|
||||||
|
timeout: 6.0,
|
||||||
|
description: "sidebar help button"
|
||||||
|
)
|
||||||
|
helpButton.click()
|
||||||
|
|
||||||
|
let keyboardShortcutsItem = requireElement(
|
||||||
|
candidates: helpMenuItemCandidates(in: app, identifier: "SidebarHelpMenuOptionKeyboardShortcuts", title: "Keyboard Shortcuts"),
|
||||||
|
timeout: 3.0,
|
||||||
|
description: "Keyboard Shortcuts help menu item"
|
||||||
|
)
|
||||||
|
keyboardShortcutsItem.click()
|
||||||
|
|
||||||
|
XCTAssertTrue(app.staticTexts["ShortcutRecordingHint"].waitForExistence(timeout: 6.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHelpMenuCheckForUpdatesTriggersSidebarUpdatePill() {
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
||||||
|
app.launchEnvironment["CMUX_UI_TEST_FEED_URL"] = "https://cmux.test/appcast.xml"
|
||||||
|
app.launchEnvironment["CMUX_UI_TEST_FEED_MODE"] = "available"
|
||||||
|
app.launchEnvironment["CMUX_UI_TEST_UPDATE_VERSION"] = "9.9.9"
|
||||||
|
app.launchEnvironment["CMUX_UI_TEST_AUTO_ALLOW_PERMISSION"] = "1"
|
||||||
|
launchAndActivate(app)
|
||||||
|
|
||||||
|
XCTAssertTrue(waitForWindowCount(atLeast: 1, app: app, timeout: 6.0))
|
||||||
|
|
||||||
|
let helpButton = requireElement(
|
||||||
|
candidates: helpButtonCandidates(in: app),
|
||||||
|
timeout: 6.0,
|
||||||
|
description: "sidebar help button"
|
||||||
|
)
|
||||||
|
helpButton.click()
|
||||||
|
|
||||||
|
let checkForUpdatesItem = requireElement(
|
||||||
|
candidates: helpMenuItemCandidates(in: app, identifier: "SidebarHelpMenuOptionCheckForUpdates", title: "Check for Updates"),
|
||||||
|
timeout: 3.0,
|
||||||
|
description: "Check for Updates help menu item"
|
||||||
|
)
|
||||||
|
checkForUpdatesItem.click()
|
||||||
|
|
||||||
|
let updatePill = app.buttons["UpdatePill"]
|
||||||
|
XCTAssertTrue(updatePill.waitForExistence(timeout: 6.0))
|
||||||
|
XCTAssertEqual(updatePill.label, "Update Available: 9.9.9")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHelpMenuSendFeedbackOpensComposerSheet() {
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
||||||
|
launchAndActivate(app)
|
||||||
|
|
||||||
|
XCTAssertTrue(waitForWindowCount(atLeast: 1, app: app, timeout: 6.0))
|
||||||
|
|
||||||
|
let helpButton = requireElement(
|
||||||
|
candidates: helpButtonCandidates(in: app),
|
||||||
|
timeout: 6.0,
|
||||||
|
description: "sidebar help button"
|
||||||
|
)
|
||||||
|
helpButton.click()
|
||||||
|
|
||||||
|
let sendFeedbackItem = requireElement(
|
||||||
|
candidates: helpMenuItemCandidates(in: app, identifier: "SidebarHelpMenuOptionSendFeedback", title: "Send Feedback"),
|
||||||
|
timeout: 3.0,
|
||||||
|
description: "Send Feedback help menu item"
|
||||||
|
)
|
||||||
|
sendFeedbackItem.click()
|
||||||
|
|
||||||
|
XCTAssertTrue(app.staticTexts["Send Feedback"].waitForExistence(timeout: 3.0))
|
||||||
|
XCTAssertTrue(
|
||||||
|
firstExistingElement(
|
||||||
|
candidates: [
|
||||||
|
app.textFields["SidebarFeedbackEmailField"],
|
||||||
|
app.textFields["Your Email"],
|
||||||
|
],
|
||||||
|
timeout: 2.0
|
||||||
|
) != nil
|
||||||
|
)
|
||||||
|
XCTAssertTrue(
|
||||||
|
firstExistingElement(
|
||||||
|
candidates: [
|
||||||
|
app.buttons["SidebarFeedbackAttachButton"],
|
||||||
|
app.buttons["Attach Images"],
|
||||||
|
],
|
||||||
|
timeout: 2.0
|
||||||
|
) != nil
|
||||||
|
)
|
||||||
|
XCTAssertTrue(
|
||||||
|
firstExistingElement(
|
||||||
|
candidates: [
|
||||||
|
app.buttons["SidebarFeedbackSendButton"],
|
||||||
|
app.buttons["Send"],
|
||||||
|
],
|
||||||
|
timeout: 2.0
|
||||||
|
) != nil
|
||||||
|
)
|
||||||
|
XCTAssertTrue(
|
||||||
|
app.staticTexts[
|
||||||
|
"A human will read this! You can also reach us at founders@manaflow.com."
|
||||||
|
].waitForExistence(timeout: 2.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let messageEditor = requireElement(
|
||||||
|
candidates: [
|
||||||
|
app.textViews["SidebarFeedbackMessageEditor"],
|
||||||
|
app.scrollViews["SidebarFeedbackMessageEditor"],
|
||||||
|
app.otherElements["SidebarFeedbackMessageEditor"],
|
||||||
|
app.textViews["Message"],
|
||||||
|
],
|
||||||
|
timeout: 2.0,
|
||||||
|
description: "feedback message editor"
|
||||||
|
)
|
||||||
|
messageEditor.click()
|
||||||
|
app.typeText("hello")
|
||||||
|
XCTAssertTrue(app.staticTexts["5/4000"].waitForExistence(timeout: 2.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func waitForWindowCount(atLeast count: Int, app: XCUIApplication, timeout: TimeInterval) -> Bool {
|
||||||
|
sidebarHelpPollUntil(timeout: timeout) {
|
||||||
|
app.windows.count >= count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func helpButtonCandidates(in app: XCUIApplication) -> [XCUIElement] {
|
||||||
|
let sidebar = app.otherElements["Sidebar"]
|
||||||
|
return [
|
||||||
|
app.buttons["SidebarHelpMenuButton"],
|
||||||
|
app.buttons["Help"],
|
||||||
|
sidebar.buttons["SidebarHelpMenuButton"],
|
||||||
|
sidebar.buttons["Help"],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func helpMenuItemCandidates(
|
||||||
|
in app: XCUIApplication,
|
||||||
|
identifier: String,
|
||||||
|
title: String
|
||||||
|
) -> [XCUIElement] {
|
||||||
|
[
|
||||||
|
app.buttons[identifier],
|
||||||
|
app.buttons[title],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func firstExistingElement(
|
||||||
|
candidates: [XCUIElement],
|
||||||
|
timeout: TimeInterval
|
||||||
|
) -> XCUIElement? {
|
||||||
|
var match: XCUIElement?
|
||||||
|
let found = sidebarHelpPollUntil(timeout: timeout) {
|
||||||
|
for candidate in candidates where candidate.exists {
|
||||||
|
match = candidate
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return found ? match : nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requireElement(
|
||||||
|
candidates: [XCUIElement],
|
||||||
|
timeout: TimeInterval,
|
||||||
|
description: String
|
||||||
|
) -> XCUIElement {
|
||||||
|
guard let element = firstExistingElement(candidates: candidates, timeout: timeout) else {
|
||||||
|
XCTFail("Expected \(description) to exist")
|
||||||
|
return candidates[0]
|
||||||
|
}
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
|
||||||
|
private func launchAndActivate(_ app: XCUIApplication, activateTimeout: TimeInterval = 2.0) {
|
||||||
|
app.launch()
|
||||||
|
let activated = sidebarHelpPollUntil(timeout: activateTimeout) {
|
||||||
|
guard app.state != .runningForeground else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
app.activate()
|
||||||
|
return app.state == .runningForeground
|
||||||
|
}
|
||||||
|
if !activated {
|
||||||
|
app.activate()
|
||||||
|
}
|
||||||
|
XCTAssertTrue(
|
||||||
|
sidebarHelpPollUntil(timeout: 2.0) { app.state == .runningForeground },
|
||||||
|
"App did not reach runningForeground before UI interactions"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class FeedbackComposerShortcutUITests: XCTestCase {
|
||||||
|
override func setUp() {
|
||||||
|
super.setUp()
|
||||||
|
continueAfterFailure = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCmdOptionFOpensFeedbackComposer() {
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
||||||
|
app.launch()
|
||||||
|
app.activate()
|
||||||
|
|
||||||
|
XCTAssertTrue(
|
||||||
|
sidebarHelpPollUntil(timeout: 6.0) {
|
||||||
|
app.windows.count >= 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
app.typeKey("f", modifierFlags: [.command, .option])
|
||||||
|
|
||||||
|
XCTAssertTrue(app.staticTexts["Send Feedback"].waitForExistence(timeout: 3.0))
|
||||||
|
XCTAssertTrue(
|
||||||
|
app.textFields["SidebarFeedbackEmailField"].waitForExistence(timeout: 2.0)
|
||||||
|
|| app.textFields["Your Email"].waitForExistence(timeout: 2.0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCmdOptionFWorksWithHiddenSidebar() {
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
||||||
|
app.launch()
|
||||||
|
app.activate()
|
||||||
|
|
||||||
|
XCTAssertTrue(
|
||||||
|
sidebarHelpPollUntil(timeout: 6.0) {
|
||||||
|
app.windows.count >= 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
app.typeKey("b", modifierFlags: [.command])
|
||||||
|
|
||||||
|
XCTAssertTrue(
|
||||||
|
sidebarHelpPollUntil(timeout: 3.0) {
|
||||||
|
!app.buttons["SidebarHelpMenuButton"].exists && !app.buttons["Help"].exists
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
app.typeKey("f", modifierFlags: [.command, .option])
|
||||||
|
|
||||||
|
XCTAssertTrue(app.staticTexts["Send Feedback"].waitForExistence(timeout: 3.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCmdOptionFWorksFromSettingsWindow() {
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
|
||||||
|
app.launchEnvironment["CMUX_UI_TEST_SHOW_SETTINGS"] = "1"
|
||||||
|
app.launch()
|
||||||
|
app.activate()
|
||||||
|
|
||||||
|
XCTAssertTrue(
|
||||||
|
sidebarHelpPollUntil(timeout: 6.0) {
|
||||||
|
app.windows.count >= 2
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
app.typeKey("f", modifierFlags: [.command, .option])
|
||||||
|
|
||||||
|
XCTAssertTrue(app.staticTexts["Send Feedback"].waitForExistence(timeout: 3.0))
|
||||||
|
XCTAssertTrue(
|
||||||
|
app.textFields["SidebarFeedbackEmailField"].waitForExistence(timeout: 2.0)
|
||||||
|
|| app.textFields["Your Email"].waitForExistence(timeout: 2.0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
3
web/.env.example
Normal file
3
web/.env.example
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
RESEND_API_KEY=
|
||||||
|
CMUX_FEEDBACK_FROM_EMAIL=
|
||||||
|
CMUX_FEEDBACK_RATE_LIMIT_ID=
|
||||||
340
web/app/api/feedback/route.ts
Normal file
340
web/app/api/feedback/route.ts
Normal file
|
|
@ -0,0 +1,340 @@
|
||||||
|
import { checkRateLimit } from "@vercel/firewall";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { Resend } from "resend";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { env } from "@/app/env";
|
||||||
|
|
||||||
|
export const runtime = "nodejs";
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
const feedbackRecipient = "founders@manaflow.com";
|
||||||
|
const maxAttachmentCount = 10;
|
||||||
|
const maxAttachmentBytes = 4 * 1024 * 1024;
|
||||||
|
// Keep multipart requests below Vercel Functions' 4.5 MB request-body limit.
|
||||||
|
const maxTotalAttachmentBytes = 4 * 1024 * 1024;
|
||||||
|
const allowedImageTypes = new Set([
|
||||||
|
"image/gif",
|
||||||
|
"image/heic",
|
||||||
|
"image/heif",
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/tiff",
|
||||||
|
"image/webp",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const feedbackSchema = z.object({
|
||||||
|
email: z.string().trim().email().max(320),
|
||||||
|
message: z.string().trim().min(1).max(4000),
|
||||||
|
appVersion: z.string().trim().max(120).optional().default(""),
|
||||||
|
appBuild: z.string().trim().max(120).optional().default(""),
|
||||||
|
appCommit: z.string().trim().max(120).optional().default(""),
|
||||||
|
bundleIdentifier: z.string().trim().max(200).optional().default(""),
|
||||||
|
osVersion: z.string().trim().max(200).optional().default(""),
|
||||||
|
locale: z.string().trim().max(120).optional().default(""),
|
||||||
|
});
|
||||||
|
|
||||||
|
type PreparedAttachment = {
|
||||||
|
content: Buffer;
|
||||||
|
contentType: string;
|
||||||
|
filename: string;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const feedbackConfig = resolveFeedbackConfig();
|
||||||
|
if (!feedbackConfig) {
|
||||||
|
return jsonError("Feedback endpoint is not configured", 503);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.VERCEL === "1") {
|
||||||
|
const { error, rateLimited } = await checkRateLimit(
|
||||||
|
feedbackConfig.rateLimitId,
|
||||||
|
{ request },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rateLimited || error === "blocked") {
|
||||||
|
return jsonError("Rate limit exceeded", 429);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error === "not-found") {
|
||||||
|
console.error(
|
||||||
|
"feedback.route.rate_limit_not_found",
|
||||||
|
feedbackConfig.rateLimitId,
|
||||||
|
);
|
||||||
|
} else if (error) {
|
||||||
|
console.error("feedback.route.rate_limit_error", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let formData: FormData;
|
||||||
|
try {
|
||||||
|
formData = await request.formData();
|
||||||
|
} catch {
|
||||||
|
return jsonError("Invalid multipart payload", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = feedbackSchema.safeParse({
|
||||||
|
email: getString(formData, "email"),
|
||||||
|
message: getString(formData, "message"),
|
||||||
|
appVersion: getString(formData, "appVersion"),
|
||||||
|
appBuild: getString(formData, "appBuild"),
|
||||||
|
appCommit: getString(formData, "appCommit"),
|
||||||
|
bundleIdentifier: getString(formData, "bundleIdentifier"),
|
||||||
|
osVersion: getString(formData, "osVersion"),
|
||||||
|
locale: getString(formData, "locale"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!parsed.success) {
|
||||||
|
return jsonError("Invalid feedback payload", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachmentsResult = await prepareAttachments(
|
||||||
|
formData.getAll("attachments"),
|
||||||
|
);
|
||||||
|
if ("errorResponse" in attachmentsResult) {
|
||||||
|
return attachmentsResult.errorResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { appBuild, appCommit, appVersion, bundleIdentifier, email, locale, message, osVersion } =
|
||||||
|
parsed.data;
|
||||||
|
const subject = buildSubject(email, message, appVersion);
|
||||||
|
const attachments = attachmentsResult.attachments;
|
||||||
|
const resend = new Resend(feedbackConfig.resendApiKey);
|
||||||
|
|
||||||
|
const { error } = await resend.emails.send({
|
||||||
|
from: `cmux feedback <${feedbackConfig.fromEmail}>`,
|
||||||
|
to: [feedbackRecipient],
|
||||||
|
replyTo: email,
|
||||||
|
subject,
|
||||||
|
text: buildTextBody({
|
||||||
|
email,
|
||||||
|
message,
|
||||||
|
appVersion,
|
||||||
|
appBuild,
|
||||||
|
appCommit,
|
||||||
|
bundleIdentifier,
|
||||||
|
osVersion,
|
||||||
|
locale,
|
||||||
|
attachments,
|
||||||
|
}),
|
||||||
|
html: buildHtmlBody({
|
||||||
|
email,
|
||||||
|
message,
|
||||||
|
appVersion,
|
||||||
|
appBuild,
|
||||||
|
appCommit,
|
||||||
|
bundleIdentifier,
|
||||||
|
osVersion,
|
||||||
|
locale,
|
||||||
|
attachments,
|
||||||
|
}),
|
||||||
|
attachments: attachments.map((attachment) => ({
|
||||||
|
content: attachment.content,
|
||||||
|
contentType: attachment.contentType,
|
||||||
|
filename: attachment.filename,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("feedback.route.resend_failed", error);
|
||||||
|
return jsonError("Failed to send feedback", 502);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ ok: true },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveFeedbackConfig() {
|
||||||
|
const resendApiKey = env.RESEND_API_KEY;
|
||||||
|
const fromEmail = env.CMUX_FEEDBACK_FROM_EMAIL;
|
||||||
|
const rateLimitId = env.CMUX_FEEDBACK_RATE_LIMIT_ID;
|
||||||
|
|
||||||
|
if (!resendApiKey || !fromEmail || !rateLimitId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
resendApiKey,
|
||||||
|
fromEmail,
|
||||||
|
rateLimitId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getString(formData: FormData, key: string) {
|
||||||
|
const value = formData.get(key);
|
||||||
|
return typeof value === "string" ? value.trim() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareAttachments(values: FormDataEntryValue[]) {
|
||||||
|
const files = values.filter(
|
||||||
|
(value): value is File => value instanceof File && value.name.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (files.length > maxAttachmentCount) {
|
||||||
|
return {
|
||||||
|
errorResponse: jsonError("Too many images attached", 400),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalSize = 0;
|
||||||
|
const attachments: PreparedAttachment[] = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (!allowedImageTypes.has(file.type)) {
|
||||||
|
return {
|
||||||
|
errorResponse: jsonError("Unsupported image attachment type", 415),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > maxAttachmentBytes) {
|
||||||
|
return {
|
||||||
|
errorResponse: jsonError("Image attachment is too large", 413),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
totalSize += file.size;
|
||||||
|
if (totalSize > maxTotalAttachmentBytes) {
|
||||||
|
return {
|
||||||
|
errorResponse: jsonError("Total image attachment size is too large", 413),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments.push({
|
||||||
|
content: Buffer.from(await file.arrayBuffer()),
|
||||||
|
contentType: file.type,
|
||||||
|
filename: sanitizeFilename(file.name),
|
||||||
|
size: file.size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { attachments };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSubject(email: string, message: string, appVersion: string) {
|
||||||
|
const firstNonEmptyLine =
|
||||||
|
message
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.find(Boolean) ?? "Feedback";
|
||||||
|
const summary =
|
||||||
|
firstNonEmptyLine.length > 72
|
||||||
|
? `${firstNonEmptyLine.slice(0, 69)}...`
|
||||||
|
: firstNonEmptyLine;
|
||||||
|
const versionSuffix = appVersion ? ` (v${appVersion})` : "";
|
||||||
|
|
||||||
|
return `cmux feedback from ${email}${versionSuffix}: ${summary}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTextBody(input: {
|
||||||
|
email: string;
|
||||||
|
message: string;
|
||||||
|
appVersion: string;
|
||||||
|
appBuild: string;
|
||||||
|
appCommit: string;
|
||||||
|
bundleIdentifier: string;
|
||||||
|
osVersion: string;
|
||||||
|
locale: string;
|
||||||
|
attachments: PreparedAttachment[];
|
||||||
|
}) {
|
||||||
|
const attachmentLines =
|
||||||
|
input.attachments.length === 0
|
||||||
|
? "Attachments: none"
|
||||||
|
: [
|
||||||
|
"Attachments:",
|
||||||
|
...input.attachments.map(
|
||||||
|
(attachment) =>
|
||||||
|
`- ${attachment.filename} (${attachment.contentType}, ${attachment.size} bytes)`,
|
||||||
|
),
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
return [
|
||||||
|
`From: ${input.email}`,
|
||||||
|
`App version: ${input.appVersion || "unknown"}`,
|
||||||
|
`App build: ${input.appBuild || "unknown"}`,
|
||||||
|
`App commit: ${input.appCommit || "unknown"}`,
|
||||||
|
`Bundle identifier: ${input.bundleIdentifier || "unknown"}`,
|
||||||
|
`macOS: ${input.osVersion || "unknown"}`,
|
||||||
|
`Locale: ${input.locale || "unknown"}`,
|
||||||
|
attachmentLines,
|
||||||
|
"",
|
||||||
|
"Message:",
|
||||||
|
input.message,
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHtmlBody(input: {
|
||||||
|
email: string;
|
||||||
|
message: string;
|
||||||
|
appVersion: string;
|
||||||
|
appBuild: string;
|
||||||
|
appCommit: string;
|
||||||
|
bundleIdentifier: string;
|
||||||
|
osVersion: string;
|
||||||
|
locale: string;
|
||||||
|
attachments: PreparedAttachment[];
|
||||||
|
}) {
|
||||||
|
const attachmentMarkup =
|
||||||
|
input.attachments.length === 0
|
||||||
|
? "<p><strong>Attachments:</strong> none</p>"
|
||||||
|
: `<p><strong>Attachments:</strong></p><ul>${input.attachments
|
||||||
|
.map(
|
||||||
|
(attachment) =>
|
||||||
|
`<li>${escapeHtml(attachment.filename)} (${escapeHtml(
|
||||||
|
attachment.contentType,
|
||||||
|
)}, ${attachment.size} bytes)</li>`,
|
||||||
|
)
|
||||||
|
.join("")}</ul>`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#111827;line-height:1.5">
|
||||||
|
<h1 style="font-size:18px;margin:0 0 16px">cmux feedback</h1>
|
||||||
|
<p><strong>From:</strong> ${escapeHtml(input.email)}</p>
|
||||||
|
<p><strong>App version:</strong> ${escapeHtml(input.appVersion || "unknown")}</p>
|
||||||
|
<p><strong>App build:</strong> ${escapeHtml(input.appBuild || "unknown")}</p>
|
||||||
|
<p><strong>App commit:</strong> ${escapeHtml(input.appCommit || "unknown")}</p>
|
||||||
|
<p><strong>Bundle identifier:</strong> ${escapeHtml(
|
||||||
|
input.bundleIdentifier || "unknown",
|
||||||
|
)}</p>
|
||||||
|
<p><strong>macOS:</strong> ${escapeHtml(input.osVersion || "unknown")}</p>
|
||||||
|
<p><strong>Locale:</strong> ${escapeHtml(input.locale || "unknown")}</p>
|
||||||
|
${attachmentMarkup}
|
||||||
|
<h2 style="font-size:15px;margin:24px 0 8px">Message</h2>
|
||||||
|
<pre style="white-space:pre-wrap;font:13px/1.6 SFMono-Regular,Menlo,monospace;background:#f3f4f6;border-radius:10px;padding:12px">${escapeHtml(
|
||||||
|
input.message,
|
||||||
|
)}</pre>
|
||||||
|
</div>
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeFilename(fileName: string) {
|
||||||
|
const cleaned = fileName.replace(/[\r\n"]/g, "").trim();
|
||||||
|
return cleaned.length > 0 ? cleaned : "attachment";
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value: string) {
|
||||||
|
return value
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonError(message: string, status: number) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: message },
|
||||||
|
{
|
||||||
|
status,
|
||||||
|
headers: {
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
18
web/app/env.ts
Normal file
18
web/app/env.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { createEnv } from "@t3-oss/env-nextjs";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const env = createEnv({
|
||||||
|
server: {
|
||||||
|
RESEND_API_KEY: z.string().min(1),
|
||||||
|
CMUX_FEEDBACK_FROM_EMAIL: z.string().email(),
|
||||||
|
CMUX_FEEDBACK_RATE_LIMIT_ID: z.string().min(1),
|
||||||
|
},
|
||||||
|
runtimeEnv: {
|
||||||
|
RESEND_API_KEY: process.env.RESEND_API_KEY,
|
||||||
|
CMUX_FEEDBACK_FROM_EMAIL: process.env.CMUX_FEEDBACK_FROM_EMAIL,
|
||||||
|
CMUX_FEEDBACK_RATE_LIMIT_ID: process.env.CMUX_FEEDBACK_RATE_LIMIT_ID,
|
||||||
|
},
|
||||||
|
skipValidation:
|
||||||
|
process.env.SKIP_ENV_VALIDATION === "1" ||
|
||||||
|
process.env.VERCEL_ENV === "preview",
|
||||||
|
});
|
||||||
24
web/bun.lock
24
web/bun.lock
|
|
@ -5,6 +5,8 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@t3-oss/env-nextjs": "^0.13.10",
|
||||||
|
"@vercel/firewall": "^1.1.2",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"posthog-js": "^1.350.0",
|
"posthog-js": "^1.350.0",
|
||||||
|
|
@ -12,7 +14,9 @@
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"react-tweet": "^3.3.0",
|
"react-tweet": "^3.3.0",
|
||||||
"react-wrap-balancer": "^1.1.1",
|
"react-wrap-balancer": "^1.1.1",
|
||||||
|
"resend": "^6.9.3",
|
||||||
"shiki": "^3.22.0",
|
"shiki": "^3.22.0",
|
||||||
|
"zod": "^4.3.6",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|
@ -249,8 +253,14 @@
|
||||||
|
|
||||||
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||||
|
|
||||||
|
"@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="],
|
||||||
|
|
||||||
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||||
|
|
||||||
|
"@t3-oss/env-core": ["@t3-oss/env-core@0.13.10", "", { "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-NNFfdlJ+HmPHkLi2HKy7nwuat9SIYOxei9K10lO2YlcSObDILY7mHZNSHsieIM3A0/5OOzw/P/b+yLvPdaG52g=="],
|
||||||
|
|
||||||
|
"@t3-oss/env-nextjs": ["@t3-oss/env-nextjs@0.13.10", "", { "dependencies": { "@t3-oss/env-core": "0.13.10" }, "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-JfSA2WXOnvcc/uMdp31paMsfbYhhdvLLRxlwvrnlPE9bwM/n0Z+Qb9xRv48nPpvfMhOrkrTYw1I5Yc06WIKBJQ=="],
|
||||||
|
|
||||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
|
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
|
||||||
|
|
@ -363,6 +373,8 @@
|
||||||
|
|
||||||
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
|
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
|
||||||
|
|
||||||
|
"@vercel/firewall": ["@vercel/firewall@1.1.2", "", {}, "sha512-h0sdBVrloWx8TitvWla/rGj3AnJ5JEYfL5LaGHNNOWkyMuzNqfCcGTvJgnjL2A5eSpAAzoN7Xt609YQ0L7xZdw=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||||
|
|
||||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||||
|
|
@ -543,6 +555,8 @@
|
||||||
|
|
||||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||||
|
|
||||||
|
"fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="],
|
||||||
|
|
||||||
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
||||||
|
|
||||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
@ -819,6 +833,8 @@
|
||||||
|
|
||||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||||
|
|
||||||
|
"postal-mime": ["postal-mime@2.7.3", "", {}, "sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw=="],
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||||
|
|
||||||
"posthog-js": ["posthog-js@1.350.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.208.0", "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", "@posthog/core": "1.23.0", "@posthog/types": "1.350.0", "core-js": "^3.38.1", "dompurify": "^3.3.1", "fflate": "^0.4.8", "preact": "^10.28.2", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.1.0" } }, "sha512-Ab+dyQdlKUTrfUZ12+fvcBo75S4jw/3o2gMleDga21B1v9c15yybiX4S3JrX66uh5L1DYG1H8sxtd4BXIIodjQ=="],
|
"posthog-js": ["posthog-js@1.350.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.208.0", "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", "@posthog/core": "1.23.0", "@posthog/types": "1.350.0", "core-js": "^3.38.1", "dompurify": "^3.3.1", "fflate": "^0.4.8", "preact": "^10.28.2", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.1.0" } }, "sha512-Ab+dyQdlKUTrfUZ12+fvcBo75S4jw/3o2gMleDga21B1v9c15yybiX4S3JrX66uh5L1DYG1H8sxtd4BXIIodjQ=="],
|
||||||
|
|
@ -859,6 +875,8 @@
|
||||||
|
|
||||||
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
||||||
|
|
||||||
|
"resend": ["resend@6.9.3", "", { "dependencies": { "postal-mime": "2.7.3", "svix": "1.84.1" }, "peerDependencies": { "@react-email/render": "*" }, "optionalPeers": ["@react-email/render"] }, "sha512-GRXjH9XZBJA+daH7bBVDuTShr22iWCxXA8P7t495G4dM/RC+d+3gHBK/6bz9K6Vpcq11zRQKmD+B+jECwQlyGQ=="],
|
||||||
|
|
||||||
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
||||||
|
|
||||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||||
|
|
@ -907,6 +925,8 @@
|
||||||
|
|
||||||
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
|
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
|
||||||
|
|
||||||
|
"standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="],
|
||||||
|
|
||||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||||
|
|
||||||
"string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="],
|
"string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="],
|
||||||
|
|
@ -933,6 +953,8 @@
|
||||||
|
|
||||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||||
|
|
||||||
|
"svix": ["svix@1.84.1", "", { "dependencies": { "standardwebhooks": "1.0.0", "uuid": "^10.0.0" } }, "sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ=="],
|
||||||
|
|
||||||
"swr": ["swr@2.4.0", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-sUlC20T8EOt1pHmDiqueUWMmRRX03W7w5YxovWX7VR2KHEPCTMly85x05vpkP5i6Bu4h44ePSMD9Tc+G2MItFw=="],
|
"swr": ["swr@2.4.0", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-sUlC20T8EOt1pHmDiqueUWMmRRX03W7w5YxovWX7VR2KHEPCTMly85x05vpkP5i6Bu4h44ePSMD9Tc+G2MItFw=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
||||||
|
|
@ -987,6 +1009,8 @@
|
||||||
|
|
||||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||||
|
|
||||||
|
"uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
|
||||||
|
|
||||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||||
|
|
||||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import "./app/env";
|
||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
|
|
|
||||||
192
web/package-lock.json
generated
192
web/package-lock.json
generated
|
|
@ -9,13 +9,18 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@t3-oss/env-nextjs": "^0.13.10",
|
||||||
|
"@vercel/firewall": "^1.1.2",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"posthog-js": "^1.350.0",
|
"posthog-js": "^1.350.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
|
"react-tweet": "^3.3.0",
|
||||||
"react-wrap-balancer": "^1.1.1",
|
"react-wrap-balancer": "^1.1.1",
|
||||||
"shiki": "^3.22.0"
|
"resend": "^6.9.3",
|
||||||
|
"shiki": "^3.22.0",
|
||||||
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|
@ -1630,6 +1635,12 @@
|
||||||
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
|
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@stablelib/base64": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@swc/helpers": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.15",
|
"version": "0.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||||
|
|
@ -1639,6 +1650,61 @@
|
||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@t3-oss/env-core": {
|
||||||
|
"version": "0.13.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@t3-oss/env-core/-/env-core-0.13.10.tgz",
|
||||||
|
"integrity": "sha512-NNFfdlJ+HmPHkLi2HKy7nwuat9SIYOxei9K10lO2YlcSObDILY7mHZNSHsieIM3A0/5OOzw/P/b+yLvPdaG52g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"arktype": "^2.1.0",
|
||||||
|
"typescript": ">=5.0.0",
|
||||||
|
"valibot": "^1.0.0-beta.7 || ^1.0.0",
|
||||||
|
"zod": "^3.24.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"arktype": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"valibot": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"zod": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@t3-oss/env-nextjs": {
|
||||||
|
"version": "0.13.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@t3-oss/env-nextjs/-/env-nextjs-0.13.10.tgz",
|
||||||
|
"integrity": "sha512-JfSA2WXOnvcc/uMdp31paMsfbYhhdvLLRxlwvrnlPE9bwM/n0Z+Qb9xRv48nPpvfMhOrkrTYw1I5Yc06WIKBJQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@t3-oss/env-core": "0.13.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"arktype": "^2.1.0",
|
||||||
|
"typescript": ">=5.0.0",
|
||||||
|
"valibot": "^1.0.0-beta.7 || ^1.0.0",
|
||||||
|
"zod": "^3.24.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"arktype": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"valibot": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"zod": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.0.tgz",
|
||||||
|
|
@ -2559,6 +2625,15 @@
|
||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@vercel/firewall": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vercel/firewall/-/firewall-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-h0sdBVrloWx8TitvWla/rGj3AnJ5JEYfL5LaGHNNOWkyMuzNqfCcGTvJgnjL2A5eSpAAzoN7Xt609YQ0L7xZdw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.16.0",
|
"version": "8.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||||
|
|
@ -3055,6 +3130,15 @@
|
||||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/clsx": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
|
@ -4031,6 +4115,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-sha256": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
||||||
|
"license": "Unlicense"
|
||||||
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.20.1",
|
"version": "1.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
||||||
|
|
@ -6044,6 +6134,12 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postal-mime": {
|
||||||
|
"version": "2.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.3.tgz",
|
||||||
|
"integrity": "sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw==",
|
||||||
|
"license": "MIT-0"
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
|
|
@ -6225,6 +6321,21 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-tweet": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-tweet/-/react-tweet-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-gSIG2169ZK7UH6rBzuU+j1xnQbH3IlOTLEkuGrRiJJTMgETik+h+26yHyyVKrLkzwrOaYPk4K3OtEKycqKgNLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/helpers": "^0.5.3",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"swr": "^2.2.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-wrap-balancer": {
|
"node_modules/react-wrap-balancer": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-wrap-balancer/-/react-wrap-balancer-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-wrap-balancer/-/react-wrap-balancer-1.1.1.tgz",
|
||||||
|
|
@ -6302,6 +6413,27 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/resend": {
|
||||||
|
"version": "6.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/resend/-/resend-6.9.3.tgz",
|
||||||
|
"integrity": "sha512-GRXjH9XZBJA+daH7bBVDuTShr22iWCxXA8P7t495G4dM/RC+d+3gHBK/6bz9K6Vpcq11zRQKmD+B+jECwQlyGQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"postal-mime": "2.7.3",
|
||||||
|
"svix": "1.84.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@react-email/render": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@react-email/render": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.11",
|
"version": "1.22.11",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||||
|
|
@ -6695,6 +6827,16 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/standardwebhooks": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@stablelib/base64": "^1.0.0",
|
||||||
|
"fast-sha256": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stop-iteration-iterator": {
|
"node_modules/stop-iteration-iterator": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
||||||
|
|
@ -6908,6 +7050,29 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svix": {
|
||||||
|
"version": "1.84.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/svix/-/svix-1.84.1.tgz",
|
||||||
|
"integrity": "sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"standardwebhooks": "1.0.0",
|
||||||
|
"uuid": "^10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swr": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dequal": "^2.0.3",
|
||||||
|
"use-sync-external-store": "^1.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.0.tgz",
|
||||||
|
|
@ -7140,7 +7305,7 @@
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
|
|
@ -7343,6 +7508,28 @@
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vfile": {
|
"node_modules/vfile": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
||||||
|
|
@ -7515,7 +7702,6 @@
|
||||||
"version": "4.3.6",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@t3-oss/env-nextjs": "^0.13.10",
|
||||||
|
"@vercel/firewall": "^1.1.2",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"posthog-js": "^1.350.0",
|
"posthog-js": "^1.350.0",
|
||||||
|
|
@ -17,7 +19,9 @@
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"react-tweet": "^3.3.0",
|
"react-tweet": "^3.3.0",
|
||||||
"react-wrap-balancer": "^1.1.1",
|
"react-wrap-balancer": "^1.1.1",
|
||||||
"shiki": "^3.22.0"
|
"resend": "^6.9.3",
|
||||||
|
"shiki": "^3.22.0",
|
||||||
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue