From 0103cd2ab95810fd7ad2f4416bfaf58d987ce221 Mon Sep 17 00:00:00 2001 From: "valentin.roehle" Date: Sat, 23 May 2026 01:01:15 +0200 Subject: [PATCH 1/7] feature: Migrated old styled to modern mui framework --- .prettierignore | 1 + app/.gitignore | 4 +- app/.prettierrc | 16 +- app/.storybook/preview.tsx | 41 +- app/components.json | 21 - app/eslint.config.js | 16 + app/index.html | 62 +- app/package.json | 27 +- app/src-tauri/Cargo.lock | 18 +- app/src-tauri/Cargo.toml | 7 + app/src-tauri/src/config/settings.rs | 76 + app/src-tauri/src/lib.rs | 13 + app/src/App.tsx | 55 +- app/src/Welcome.stories.tsx | 19 - app/src/assets/recrest-icon-dev.svg | 4 +- .../atoms/AheadBehind/AheadBehind.stories.tsx | 15 - .../atoms/AheadBehind/AheadBehind.test.tsx | 32 - .../components/atoms/AheadBehind/index.tsx | 23 - .../components/atoms/Badge/Badge.stories.tsx | 21 - app/src/components/atoms/Badge/Badge.test.tsx | 31 - app/src/components/atoms/Badge/index.tsx | 40 - .../atoms/BranchChip/BranchChip.stories.tsx | 21 - .../atoms/BranchChip/BranchChip.test.tsx | 32 - app/src/components/atoms/BranchChip/index.tsx | 20 - .../atoms/BrandIcon/BrandIcon.stories.tsx | 18 - .../atoms/BrandIcon/BrandIcon.test.tsx | 39 - app/src/components/atoms/BrandIcon/index.tsx | 65 - .../atoms/Button/Button.stories.tsx | 27 - .../components/atoms/Button/Button.test.tsx | 42 - app/src/components/atoms/Button/index.tsx | 66 - .../atoms/Checkbox/Checkbox.stories.tsx | 18 - .../atoms/Checkbox/Checkbox.test.tsx | 35 - app/src/components/atoms/Checkbox/index.tsx | 25 - .../components/atoms/CiDot/CiDot.stories.tsx | 15 - app/src/components/atoms/CiDot/CiDot.test.tsx | 31 - app/src/components/atoms/CiDot/index.tsx | 42 - .../components/atoms/ConfirmDialog/index.tsx | 103 - .../atoms/ConfirmDialog/useConfirm.ts | 37 - .../atoms/DiffStat/DiffStat.stories.tsx | 17 - .../atoms/DiffStat/DiffStat.test.tsx | 29 - app/src/components/atoms/DiffStat/index.tsx | 15 - .../components/atoms/Icon/Icon.stories.tsx | 16 - app/src/components/atoms/Icon/Icon.test.tsx | 33 - app/src/components/atoms/Icon/index.tsx | 298 -- .../atoms/IdeIcon/IdeIcon.stories.tsx | 25 - .../components/atoms/IdeIcon/IdeIcon.test.tsx | 33 - app/src/components/atoms/IdeIcon/index.tsx | 104 - .../components/atoms/Input/Input.stories.tsx | 20 - app/src/components/atoms/Input/Input.test.tsx | 41 - app/src/components/atoms/Input/index.tsx | 31 - app/src/components/atoms/Kbd/Kbd.stories.tsx | 15 - app/src/components/atoms/Kbd/Kbd.test.tsx | 25 - app/src/components/atoms/Kbd/index.tsx | 10 - .../components/atoms/Label/Label.stories.tsx | 20 - app/src/components/atoms/Label/Label.test.tsx | 27 - app/src/components/atoms/Label/index.tsx | 17 - .../atoms/LangDot/LangDot.stories.tsx | 16 - .../components/atoms/LangDot/LangDot.test.tsx | 41 - app/src/components/atoms/LangDot/index.tsx | 25 - .../atoms/Mascot/Mascot.stories.tsx | 66 - .../atoms/Separator/Separator.stories.tsx | 24 - .../atoms/Separator/Separator.test.tsx | 31 - app/src/components/atoms/Separator/index.tsx | 25 - .../atoms/Skeleton/Skeleton.stories.tsx | 18 - .../atoms/Skeleton/Skeleton.test.tsx | 36 - app/src/components/atoms/Skeleton/index.tsx | 19 - .../atoms/Sparkline/Sparkline.stories.tsx | 22 - .../atoms/Sparkline/Sparkline.test.tsx | 34 - app/src/components/atoms/Sparkline/index.tsx | 25 - .../atoms/Spinner/Spinner.stories.tsx | 17 - .../components/atoms/Spinner/Spinner.test.tsx | 31 - app/src/components/atoms/Spinner/index.tsx | 25 - .../atoms/StatusDot/StatusDot.stories.tsx | 14 - .../atoms/StatusDot/StatusDot.test.tsx | 22 - app/src/components/atoms/StatusDot/index.tsx | 10 - .../atoms/Switch/Switch.stories.tsx | 15 - .../components/atoms/Switch/Switch.test.tsx | 35 - app/src/components/atoms/Switch/index.tsx | 39 - .../atoms/badges/GeneralBadge/index.tsx | 58 + .../components/atoms/brand/AppIcon/index.tsx | 60 + app/src/components/atoms/brand/Logo/index.tsx | 78 + .../atoms/{ => brand}/Mascot/index.tsx | 136 +- .../atoms/buttons/GeneralButton/index.tsx | 94 + .../buttons/GeneralButtonGroup/index.tsx | 138 + .../atoms/buttons/GeneralIconButton/index.tsx | 259 ++ .../buttons/ThemeSwitcherButton/index.tsx | 45 + .../atoms/data/GeneralSparkline/index.tsx | 60 + .../atoms/dividers/GeneralDivider/index.tsx | 9 + .../atoms/feedback/GeneralSkeleton/index.tsx | 9 + .../atoms/feedback/GeneralSpinner/index.tsx | 11 + .../atoms/feedback/GeneralTooltip/index.tsx | 48 + .../atoms/icons/BrandIcon/index.tsx | 46 + .../components/atoms/icons/IdeIcon/index.tsx | 84 + .../IdeIcon/logos/intellij-idea.svg | 0 .../{ => icons}/IdeIcon/logos/jetbrains.svg | 0 .../IdeIcon/logos/visual-studio-code.svg | 0 .../{ => icons}/IdeIcon/logos/webstorm.svg | 0 .../atoms/icons/ShellIcon/index.tsx | 90 + .../atoms/icons/ShellIcon/logos/cmd.svg | 5 + .../icons/ShellIcon/logos/powershell-core.svg | 5 + .../ShellIcon/logos/windows-powershell.svg | 5 + .../atoms/icons/ShellIcon/logos/wsl.svg | 6 + .../atoms/icons/TerminalIcon/index.tsx | 111 + .../TerminalIcon/logos/apple-terminal.svg | 5 + .../atoms/icons/TerminalIcon/logos/cmd.svg | 5 + .../atoms/icons/TerminalIcon/logos/kitty.svg | 6 + .../icons/TerminalIcon/logos/konsole.svg | 5 + .../icons/TerminalIcon/logos/powershell.svg | 5 + .../atoms/icons/TerminalIcon/logos/tilix.svg | 6 + .../TerminalIcon/logos/windows-terminal.svg | 5 + .../atoms/icons/TerminalIcon/logos/xterm.svg | 5 + .../inputs/GeneralCheckboxInput/index.tsx | 25 + .../atoms/inputs/GeneralInput/index.tsx | 14 + .../atoms/inputs/GeneralLabel/index.tsx | 13 + .../atoms/inputs/GeneralSwitchInput/index.tsx | 80 + .../ComingSoonPlaceholder/index.tsx | 26 + .../transitions/PageTransition/index.tsx | 84 + .../transitions/StaggeredReveal/index.tsx | 100 + .../AuthorAvatar/AuthorAvatar.stories.tsx | 22 - .../AuthorAvatar/AuthorAvatar.test.tsx | 46 - .../molecules/AuthorAvatar/index.tsx | 187 - .../BranchFilterChip.stories.tsx | 24 - .../BranchFilterChip.test.tsx | 38 - .../molecules/BranchFilterChip/index.tsx | 19 - .../molecules/DetailSection/index.tsx | 34 - app/src/components/molecules/Drawer/index.tsx | 183 - .../EmptyState/EmptyState.stories.tsx | 68 - .../molecules/EmptyState/EmptyState.test.tsx | 31 - .../components/molecules/EmptyState/index.tsx | 51 - .../ExternalLinkButton.stories.tsx | 22 - .../ExternalLinkButton.test.tsx | 34 - .../molecules/ExternalLinkButton/index.tsx | 52 - .../IconButton/IconButton.stories.tsx | 18 - .../molecules/IconButton/IconButton.test.tsx | 56 - .../components/molecules/IconButton/index.tsx | 73 - .../molecules/InfoCard/InfoCard.stories.tsx | 18 - .../molecules/InfoCard/InfoCard.test.tsx | 25 - .../components/molecules/InfoCard/index.tsx | 24 - .../molecules/InfoHint/InfoHint.stories.tsx | 36 - .../molecules/InfoHint/InfoHint.test.tsx | 25 - .../components/molecules/InfoHint/index.tsx | 47 - .../molecules/KpiCard/KpiCard.stories.tsx | 36 - .../molecules/KpiCard/KpiCard.test.tsx | 22 - .../components/molecules/KpiCard/index.tsx | 19 - .../molecules/KpiTile/KpiTile.stories.tsx | 23 - .../molecules/KpiTile/KpiTile.test.tsx | 29 - .../components/molecules/KpiTile/index.tsx | 26 - .../molecules/MrChip/MrChip.stories.tsx | 18 - .../molecules/MrChip/MrChip.test.tsx | 36 - app/src/components/molecules/MrChip/index.tsx | 21 - .../OpenInIdeButton.stories.tsx | 43 - .../OpenInIdeButton/OpenInIdeButton.test.tsx | 61 - .../molecules/OpenInIdeButton/index.tsx | 131 - .../RepoAvatar/RepoAvatar.stories.tsx | 15 - .../molecules/RepoAvatar/RepoAvatar.test.tsx | 40 - .../components/molecules/RepoAvatar/index.tsx | 245 -- .../SettingsField/SettingsField.stories.tsx | 28 - .../SettingsField/SettingsField.test.tsx | 52 - .../molecules/SettingsField/index.tsx | 44 - .../SettingsSectionHeader.stories.tsx | 18 - .../SettingsSectionHeader.test.tsx | 21 - .../molecules/SettingsSectionHeader/index.tsx | 20 - .../molecules/Sonner/Sonner.stories.tsx | 73 - .../molecules/Sonner/Sonner.test.tsx | 24 - app/src/components/molecules/Sonner/index.tsx | 32 - .../avatars/GeneralAuthorAvatar/index.tsx | 80 + .../avatars/GeneralRepoAvatar/index.tsx | 99 + .../molecules/cards/GeneralCard/index.tsx | 237 ++ .../AlertDialog/AlertDialog.stories.tsx | 92 - .../AlertDialog/AlertDialog.test.tsx | 65 - .../molecules/compounds/AlertDialog/index.tsx | 116 - .../ConfirmDialog/ConfirmDialog.stories.tsx | 78 - .../ConfirmDialog/ConfirmDialog.test.tsx | 58 - .../compounds/ConfirmDialog/index.tsx | 177 - .../compounds/Dialog/Dialog.stories.tsx | 80 - .../compounds/Dialog/Dialog.test.tsx | 52 - .../molecules/compounds/Dialog/index.tsx | 116 - .../DropdownMenu/DropdownMenu.stories.tsx | 102 - .../DropdownMenu/DropdownMenu.test.tsx | 58 - .../compounds/DropdownMenu/index.tsx | 193 - .../compounds/Select/Select.stories.tsx | 95 - .../compounds/Select/Select.test.tsx | 41 - .../molecules/compounds/Select/index.tsx | 128 - .../molecules/compounds/Tabs/Tabs.stories.tsx | 78 - .../molecules/compounds/Tabs/Tabs.test.tsx | 51 - .../molecules/compounds/Tabs/index.tsx | 51 - .../compounds/Tooltip/Tooltip.stories.tsx | 64 - .../compounds/Tooltip/Tooltip.test.tsx | 39 - .../molecules/compounds/Tooltip/index.tsx | 41 - .../TruncatedTooltip.test.tsx | 83 - .../compounds/TruncatedTooltip/index.tsx | 71 - .../molecules/dialogs/ConfirmDialog/index.tsx | 56 + .../molecules/drawers/GeneralDrawer/index.tsx | 86 + .../molecules/feedback/EmptyState/index.tsx | 117 + .../feedback/GeneralToaster/index.tsx | 27 + .../EmptyStatePlaceholder/index.tsx | 52 + .../ActivityBarsSkeleton.stories.tsx | 24 - .../ActivityBarsSkeleton.test.tsx | 16 - .../skeletons/ActivityBarsSkeleton/index.tsx | 13 - .../BranchRowSkeleton.stories.tsx | 22 - .../BranchRowSkeleton.test.tsx | 16 - .../skeletons/BranchRowSkeleton/index.tsx | 15 - .../CardBlockSkeleton.stories.tsx | 17 - .../CardBlockSkeleton.test.tsx | 22 - .../skeletons/CardBlockSkeleton/index.tsx | 28 - .../CommitListSkeleton.stories.tsx | 14 - .../CommitListSkeleton.test.tsx | 16 - .../skeletons/CommitListSkeleton/index.tsx | 15 - .../CommitRowSkeleton.stories.tsx | 22 - .../CommitRowSkeleton.test.tsx | 16 - .../skeletons/CommitRowSkeleton/index.tsx | 17 - .../skeletons/DashboardSkeletons/index.tsx | 170 + .../FileChangesSkeleton.stories.tsx | 14 - .../FileChangesSkeleton.test.tsx | 16 - .../skeletons/FileChangesSkeleton/index.tsx | 21 - .../KpiSkeleton/KpiSkeleton.stories.tsx | 23 - .../KpiSkeleton/KpiSkeleton.test.tsx | 16 - .../molecules/skeletons/KpiSkeleton/index.tsx | 11 - .../MrListSkeleton/MrListSkeleton.stories.tsx | 14 - .../MrListSkeleton/MrListSkeleton.test.tsx | 16 - .../skeletons/MrListSkeleton/index.tsx | 15 - .../MrRowSkeleton/MrRowSkeleton.stories.tsx | 22 - .../MrRowSkeleton/MrRowSkeleton.test.tsx | 16 - .../skeletons/MrRowSkeleton/index.tsx | 25 - .../RemoteRepoCardSkeleton.stories.tsx | 22 - .../RemoteRepoCardSkeleton.test.tsx | 16 - .../RemoteRepoCardSkeleton/index.tsx | 20 - .../RemoteRepoListSkeleton.stories.tsx | 14 - .../RemoteRepoListSkeleton.test.tsx | 17 - .../RemoteRepoListSkeleton/index.tsx | 15 - .../RepoListSkeleton.stories.tsx | 14 - .../RepoListSkeleton.test.tsx | 17 - .../skeletons/RepoListSkeleton/index.tsx | 26 - .../RepoRowSkeleton.stories.tsx | 22 - .../RepoRowSkeleton/RepoRowSkeleton.test.tsx | 17 - .../skeletons/RepoRowSkeleton/index.tsx | 40 - .../ReviewerChipsSkeleton.stories.tsx | 21 - .../ReviewerChipsSkeleton.test.tsx | 16 - .../skeletons/ReviewerChipsSkeleton/index.tsx | 11 - .../SearchGroupSkeleton.stories.tsx | 14 - .../SearchGroupSkeleton.test.tsx | 17 - .../skeletons/SearchGroupSkeleton/index.tsx | 19 - .../SearchHitSkeleton.stories.tsx | 22 - .../SearchHitSkeleton.test.tsx | 16 - .../skeletons/SearchHitSkeleton/index.tsx | 10 - .../TimelineEventsSkeleton.stories.tsx | 14 - .../TimelineEventsSkeleton.test.tsx | 16 - .../TimelineEventsSkeleton/index.tsx | 21 - .../molecules/toggles/ScopeToggle/index.tsx | 125 + .../activity/Timeline/Timeline.stories.tsx | 46 - .../activity/Timeline/Timeline.test.tsx | 46 - .../organisms/activity/Timeline/index.tsx | 604 ++- .../AuthorClockCard.stories.tsx | 23 - .../AuthorClockCard/AuthorClockCard.test.tsx | 14 - .../activity/cards/AuthorClockCard/index.tsx | 90 +- .../cards/AuthorsHero/AuthorsHero.stories.tsx | 27 - .../cards/AuthorsHero/AuthorsHero.test.tsx | 21 - .../activity/cards/AuthorsHero/index.tsx | 127 +- .../BusiestPeakCard.stories.tsx | 23 - .../BusiestPeakCard/BusiestPeakCard.test.tsx | 28 - .../activity/cards/BusiestPeakCard/index.tsx | 85 +- .../cards/CardShell/CardShell.stories.tsx | 17 - .../cards/CardShell/CardShell.test.tsx | 17 - .../activity/cards/CardShell/index.tsx | 96 - .../cards/ChurnCard/ChurnCard.stories.tsx | 21 - .../cards/ChurnCard/ChurnCard.test.tsx | 20 - .../activity/cards/ChurnCard/index.tsx | 102 +- .../CiHealthHero/CiHealthHero.stories.tsx | 24 - .../cards/CiHealthHero/CiHealthHero.test.tsx | 18 - .../activity/cards/CiHealthHero/index.tsx | 279 +- .../CiPassRateCard/CiPassRateCard.stories.tsx | 20 - .../CiPassRateCard/CiPassRateCard.test.tsx | 19 - .../activity/cards/CiPassRateCard/index.tsx | 244 +- .../cards/CommitsHero/CommitsHero.stories.tsx | 30 - .../cards/CommitsHero/CommitsHero.test.tsx | 17 - .../activity/cards/CommitsHero/index.tsx | 122 +- .../FlakyReposCard/FlakyReposCard.stories.tsx | 21 - .../FlakyReposCard/FlakyReposCard.test.tsx | 22 - .../activity/cards/FlakyReposCard/index.tsx | 96 +- .../cards/HeatmapCard/HeatmapCard.stories.tsx | 27 - .../cards/HeatmapCard/HeatmapCard.test.tsx | 18 - .../activity/cards/HeatmapCard/index.tsx | 99 +- .../LanguageDonutCard.stories.tsx | 20 - .../LanguageDonutCard.test.tsx | 20 - .../cards/LanguageDonutCard/index.tsx | 124 +- .../LeaderboardCard.stories.tsx | 45 - .../LeaderboardCard/LeaderboardCard.test.tsx | 37 - .../activity/cards/LeaderboardCard/index.tsx | 185 +- .../cards/OpenPrsHero/OpenPrsHero.stories.tsx | 24 - .../cards/OpenPrsHero/OpenPrsHero.test.tsx | 25 - .../activity/cards/OpenPrsHero/index.tsx | 61 +- .../PrVelocityCard/PrVelocityCard.stories.tsx | 25 - .../PrVelocityCard/PrVelocityCard.test.tsx | 17 - .../activity/cards/PrVelocityCard/index.tsx | 100 +- .../QuietestReposCard.stories.tsx | 24 - .../QuietestReposCard.test.tsx | 23 - .../cards/QuietestReposCard/index.tsx | 75 +- .../ReviewQueueCard.stories.tsx | 38 - .../ReviewQueueCard/ReviewQueueCard.test.tsx | 39 - .../activity/cards/ReviewQueueCard/index.tsx | 142 +- .../StackedChartCard.stories.tsx | 33 - .../StackedChartCard.test.tsx | 30 - .../activity/cards/StackedChartCard/index.tsx | 209 +- .../cards/StreakCard/StreakCard.stories.tsx | 13 - .../cards/StreakCard/StreakCard.test.tsx | 17 - .../activity/cards/StreakCard/index.tsx | 91 +- .../TimeToMergeCard.stories.tsx | 20 - .../TimeToMergeCard/TimeToMergeCard.test.tsx | 22 - .../activity/cards/TimeToMergeCard/index.tsx | 85 +- .../organisms/activity/cards/_fixtures.ts | 91 - .../organisms/banners/UpdaterBanner/index.tsx | 57 + .../organisms/brand/AppIcon/index.tsx | 36 - .../brand/BrandMark/BrandMark.stories.tsx | 15 - .../brand/BrandMark/BrandMark.test.tsx | 24 - .../organisms/brand/BrandMark/index.tsx | 29 - .../organisms/brand/Logo/Logo.stories.tsx | 17 - .../organisms/brand/Logo/Logo.test.tsx | 16 - .../components/organisms/brand/Logo/index.tsx | 23 - .../cards/ActivityCardShell/index.tsx | 172 + .../organisms/cards/HeatmapCard/index.tsx | 129 + .../cards/LanguageDonutCard/index.tsx | 198 + .../ErrorBoundary/ErrorBoundary.stories.tsx | 43 - .../ErrorBoundary/ErrorBoundary.test.tsx | 45 - .../feedback/ErrorBoundary/index.tsx | 44 - .../UpdaterBanner/UpdaterBanner.stories.tsx | 63 - .../UpdaterBanner/UpdaterBanner.test.tsx | 103 - .../feedback/UpdaterBanner/index.tsx | 112 - .../layout/AppShell/AppShell.stories.tsx | 34 - .../layout/AppShell/AppShell.test.tsx | 43 - .../organisms/layout/AppShell/index.tsx | 172 - .../layout/DetailPane/DetailPane.stories.tsx | 90 - .../layout/DetailPane/DetailPane.test.tsx | 43 - .../organisms/layout/DetailPane/index.tsx | 407 -- .../layout/Header/Header.stories.tsx | 38 - .../organisms/layout/Header/Header.test.tsx | 46 - .../organisms/layout/Header/index.tsx | 529 ++- .../layout/Sidebar/Sidebar.stories.tsx | 32 - .../organisms/layout/Sidebar/Sidebar.test.tsx | 69 - .../organisms/layout/Sidebar/index.tsx | 472 ++- .../layout/Titlebar/GnomeTitlebar.tsx | 43 - .../layout/Titlebar/MacOverlayTitlebar.tsx | 26 - .../layout/Titlebar/Titlebar.stories.tsx | 22 - .../layout/Titlebar/Titlebar.test.tsx | 53 - .../MergeRequestDetailPanel/index.tsx | 353 -- .../OnboardingWizard.stories.tsx | 23 - .../OnboardingWizard.test.tsx | 37 - .../onboarding/OnboardingWizard/index.tsx | 115 - .../organisms/onboarding/_test-helpers.tsx | 27 - .../steps/BasicsStep/BasicsStep.stories.tsx | 21 - .../steps/BasicsStep/BasicsStep.test.tsx | 29 - .../onboarding/steps/BasicsStep/index.tsx | 180 - .../ConnectProviderStep.stories.tsx | 21 - .../ConnectProviderStep.test.tsx | 16 - .../steps/ConnectProviderStep/index.tsx | 144 - .../steps/DoneStep/DoneStep.stories.tsx | 21 - .../steps/DoneStep/DoneStep.test.tsx | 31 - .../onboarding/steps/DoneStep/index.tsx | 42 - .../InitialScanStep.stories.tsx | 21 - .../InitialScanStep/InitialScanStep.test.tsx | 16 - .../steps/InitialScanStep/index.tsx | 94 - .../PickFolderStep/PickFolderStep.stories.tsx | 21 - .../PickFolderStep/PickFolderStep.test.tsx | 16 - .../onboarding/steps/PickFolderStep/index.tsx | 159 - .../steps/WelcomeStep/WelcomeStep.stories.tsx | 19 - .../steps/WelcomeStep/WelcomeStep.test.tsx | 27 - .../onboarding/steps/WelcomeStep/index.tsx | 39 - .../organisms/prs/PrList/PrList.stories.tsx | 57 - .../organisms/prs/PrList/PrList.test.tsx | 49 - .../components/organisms/prs/PrList/index.tsx | 42 - .../organisms/prs/PrRow/PrRow.stories.tsx | 33 - .../organisms/prs/PrRow/PrRow.test.tsx | 24 - .../components/organisms/prs/PrRow/index.tsx | 88 - .../organisms/repos/AddRepoDialog/index.tsx | 1245 ++++++ .../ChangedFilesList.stories.tsx | 58 - .../ChangedFilesList.test.tsx | 42 - .../repos/ChangedFilesList/index.tsx | 86 - .../CreateBranchDialog.stories.tsx | 27 - .../CreateBranchDialog.test.tsx | 29 - .../repos/CreateBranchDialog/index.tsx | 103 - .../FindAcrossReposDialog.stories.tsx | 34 - .../FindAcrossReposDialog.test.tsx | 37 - .../repos/FindAcrossReposDialog/index.tsx | 146 - .../ImportFromProviderDialog.stories.tsx | 34 - .../ImportFromProviderDialog.test.tsx | 37 - .../repos/ImportFromProviderDialog/index.tsx | 787 ---- .../organisms/repos/RepoCard/index.tsx | 209 - .../repos/RepoDetail/RepoDetail.stories.tsx | 63 - .../repos/RepoDetail/RepoDetail.test.tsx | 33 - .../organisms/repos/RepoDetail/index.tsx | 135 - .../repos/RepoList/RepoList.stories.tsx | 53 - .../repos/RepoList/RepoList.test.tsx | 66 - .../organisms/repos/RepoList/index.tsx | 212 - .../repos/RepoRow/RepoRow.stories.tsx | 69 - .../organisms/repos/RepoRow/RepoRow.test.tsx | 37 - .../organisms/repos/RepoRow/index.tsx | 244 -- .../repos/RepoStats/RepoStats.stories.tsx | 33 - .../repos/RepoStats/RepoStats.test.tsx | 35 - .../organisms/repos/RepoStats/index.tsx | 68 - .../SearchOverlay/SearchOverlay.stories.tsx | 43 - .../SearchOverlay/SearchOverlay.test.tsx | 48 - .../organisms/search/SearchOverlay/index.tsx | 348 +- .../ProviderAuth/ProviderAuth.stories.tsx | 20 - .../ProviderAuth/ProviderAuth.test.tsx | 18 - .../organisms/settings/ProviderAuth/index.tsx | 319 -- .../RepoSources/RepoSources.stories.tsx | 20 - .../settings/RepoSources/RepoSources.test.tsx | 16 - .../organisms/settings/RepoSources/index.tsx | 191 - .../SettingsSection.stories.tsx | 25 - .../SettingsSection/SettingsSection.test.tsx | 26 - .../settings/SettingsSection/index.tsx | 26 - .../SettingsView/SettingsView.stories.tsx | 20 - .../SettingsView/SettingsView.test.tsx | 18 - .../organisms/settings/SettingsView/index.tsx | 240 -- .../organisms/settings/_test-helpers.tsx | 22 - .../tabs/AboutTab/AboutTab.stories.tsx | 20 - .../settings/tabs/AboutTab/AboutTab.test.tsx | 16 - .../settings/tabs/AboutTab/index.tsx | 215 - .../AppearanceSettings.stories.tsx | 20 - .../AppearanceSettings.test.tsx | 17 - .../tabs/AppearanceSettings/index.tsx | 293 -- .../DesktopSettings.stories.tsx | 20 - .../DesktopSettings/DesktopSettings.test.tsx | 16 - .../settings/tabs/DesktopSettings/index.tsx | 115 - .../tabs/DeveloperTab/DeveloperTab.test.tsx | 112 - .../settings/tabs/DeveloperTab/index.tsx | 1231 ------ .../DiagnosticsSettings.stories.tsx | 20 - .../DiagnosticsSettings.test.tsx | 16 - .../tabs/DiagnosticsSettings/index.tsx | 68 - .../NotificationSettings.stories.tsx | 20 - .../NotificationSettings.test.tsx | 16 - .../tabs/NotificationSettings/index.tsx | 128 - .../SystemSettings/SystemSettings.stories.tsx | 20 - .../SystemSettings/SystemSettings.test.tsx | 16 - .../settings/tabs/SystemSettings/index.tsx | 135 - .../UpdatesSettings.stories.tsx | 20 - .../UpdatesSettings/UpdatesSettings.test.tsx | 17 - .../settings/tabs/UpdatesSettings/index.tsx | 124 - .../settings/tabs/useSettingsSaver.ts | 22 - .../titlebars/GnomeTitlebar/index.tsx | 80 + .../titlebars/MacOverlayTitlebar/index.tsx | 40 + .../{layout => titlebars}/Titlebar/index.tsx | 24 +- .../Win11Titlebar/index.tsx} | 131 +- .../Titlebar => titlebars}/runWindow.ts | 6 +- app/src/hooks/useActiveIde.ts | 36 - app/src/hooks/useAppBootstrap.ts | 26 + app/src/hooks/useCheckRuns.ts | 78 +- app/src/hooks/useDeepLinks.ts | 84 - app/src/hooks/useDevFlag.test.ts | 51 - app/src/hooks/useDevFlag.ts | 23 - app/src/hooks/useDrawerSwipe.ts | 22 +- app/src/hooks/useFaviconSync.ts | 50 + app/src/hooks/useFirstRun.ts | 49 - app/src/hooks/useFullbleedScroll.ts | 26 + app/src/hooks/useGitInfo.ts | 54 - app/src/hooks/useGlobalEvents.ts | 98 - app/src/hooks/useGlobalShortcuts.ts | 95 - app/src/hooks/useLastSeenVersion.ts | 34 - app/src/hooks/useLocaleSync.ts | 29 + app/src/hooks/useNotificationTriggers.test.ts | 438 -- app/src/hooks/useNotificationTriggers.ts | 295 -- app/src/hooks/usePageSwipe.ts | 33 +- app/src/hooks/usePlatform.ts | 15 +- app/src/hooks/usePrEvents.ts | 46 +- .../{useProviders.ts => usePrPolling.ts} | 23 +- app/src/hooks/useReducedMotion.ts | 35 + app/src/hooks/useRepoFavicon.ts | 158 - app/src/hooks/useRepoLogo.ts | 84 - app/src/hooks/useRepos.ts | 10 +- app/src/hooks/useResponsiveSidebar.ts | 31 + app/src/hooks/useScrollRestoration.ts | 35 +- app/src/hooks/useSearch.ts | 68 +- app/src/hooks/useTauri.ts | 191 - app/src/hooks/useTheme.ts | 63 - app/src/hooks/useThemeAttribute.ts | 36 + app/src/hooks/useTrayBadgeSync.ts | 21 - app/src/layouts/AppLayout/AppLayout.tsx | 133 + app/src/layouts/AppLayout/index.ts | 1 + app/src/lib/activityAggregates.test.ts | 264 -- app/src/lib/animations/pageAnimations.ts | 96 + app/src/lib/authorNormalize.test.ts | 66 - app/src/lib/charts/smoothLine.test.ts | 111 - app/src/lib/constants/events.ts | 26 + app/src/lib/constants/theme.constants.ts | 298 ++ app/src/lib/dev/seed/remote.ts | 179 + app/src/lib/dev/seed/settings.ts | 16 + app/src/lib/gravatar.ts | 170 - app/src/lib/initials.ts | 12 - app/src/lib/languages.ts | 23 - app/src/lib/repoEnrich.test.ts | 31 - .../{dev/tauriStub.ts => tauri/devStub.ts} | 120 +- app/src/lib/tauri/events.ts | 26 + .../lib/tauri/services/autostartService.ts | 54 - app/src/lib/tauri/services/index.ts | 8 - .../lib/tauri/services/notificationService.ts | 58 - app/src/lib/tauri/services/storageService.ts | 60 - app/src/lib/tauri/services/systemService.ts | 13 - app/src/lib/tauri/services/trayService.ts | 7 - app/src/lib/tauri/services/updaterService.ts | 20 - app/src/lib/tauri/services/windowService.ts | 75 - app/src/lib/toast.ts | 35 - app/src/lib/utils.ts | 6 - app/src/lib/utils/brandFromUrl.ts | 13 + app/src/lib/utils/theme.utils.ts | 16 + app/src/{i18n => }/locales/de/common.json | 5 + app/src/{i18n => }/locales/de/errors.json | 0 app/src/{i18n => }/locales/de/onboarding.json | 0 app/src/{i18n => }/locales/de/prs.json | 0 app/src/{i18n => }/locales/de/repos.json | 0 app/src/{i18n => }/locales/de/settings.json | 13 +- app/src/{i18n => }/locales/en/common.json | 5 + app/src/{i18n => }/locales/en/errors.json | 0 app/src/{i18n => }/locales/en/onboarding.json | 0 app/src/{i18n => }/locales/en/prs.json | 0 app/src/{i18n => }/locales/en/repos.json | 0 app/src/{i18n => }/locales/en/settings.json | 13 +- app/src/{i18n => locales}/index.ts | 30 +- app/src/main.tsx | 94 +- app/src/pages/ActivityPage.tsx | 392 -- app/src/pages/BranchesPage.tsx | 760 ---- app/src/pages/DashboardPage.tsx | 589 --- app/src/pages/MergeRequestsPage.tsx | 295 -- app/src/pages/PullRequestsPage.tsx | 39 - app/src/pages/RepoDetailPage.tsx | 698 ---- app/src/pages/ReposPage.tsx | 322 -- app/src/pages/SettingsPage.tsx | 1 - .../pages/__tests__/RepoDetailPage.test.tsx | 81 - app/src/pages/app/Activity/index.tsx | 468 +++ app/src/pages/app/Branches/index.tsx | 1084 +++++ app/src/pages/app/Changes/index.tsx | 10 + .../pages/app/Dashboard/QuickActionsCard.tsx | 255 ++ app/src/pages/app/Dashboard/index.tsx | 957 +++++ .../MrDetailPanel/MrDetailPanel.tsx | 762 ++++ .../components/MrDetailPanel/index.ts | 4 + .../MergeRequests/components/MrRow/MrRow.tsx | 208 + .../MergeRequests/components/MrRow/index.ts | 1 + app/src/pages/app/MergeRequests/index.tsx | 239 ++ app/src/pages/app/RepoDetail/index.tsx | 829 ++++ .../components/DetailPane/DetailPane.tsx | 599 +++ .../app/Repos/components/DetailPane/index.ts | 4 + .../Repos/components/RepoCard/RepoCard.tsx | 322 ++ .../app/Repos/components/RepoCard/index.ts | 1 + .../Repos/components/RepoList/RepoList.tsx | 306 ++ .../app/Repos/components/RepoList/index.ts | 1 + .../app/Repos/components/RepoRow/RepoRow.tsx | 431 ++ .../app/Repos/components/RepoRow/index.ts | 1 + app/src/pages/app/Repos/index.tsx | 374 ++ .../Settings/components/AboutTab/index.tsx | 307 ++ .../Settings/components/AccountsTab/index.tsx | 503 +++ .../components/DeveloperTab/DeveloperTab.tsx | 1031 +++++ .../Settings/components/DeveloperTab/index.ts | 4 + .../Settings/components/GeneralTab/index.tsx | 1072 +++++ .../components/IntegrationsTab/index.tsx | 214 + .../components/SettingsPrimitives/index.tsx | 97 + .../components/ShortcutsTab/index.tsx | 101 + .../Settings/components/StorageTab/index.tsx | 145 + app/src/pages/app/Settings/index.tsx | 378 ++ app/src/store/actions/providers.actions.ts | 33 + app/src/store/actions/prs.actions.ts | 35 + app/src/store/actions/remoteImport.actions.ts | 57 + app/src/store/actions/repos.actions.ts | 125 + app/src/store/actions/settings.actions.ts | 57 + app/src/store/actions/ui.actions.ts | 15 + app/src/store/backendSync.ts | 326 ++ app/src/store/index.ts | 119 +- app/src/store/persistence.ts | 82 - app/src/store/reducers/providersReducer.ts | 54 + app/src/store/reducers/prsReducer.ts | 97 + app/src/store/reducers/remoteImportReducer.ts | 53 + app/src/store/reducers/reposReducer.ts | 128 + app/src/store/reducers/settingsReducer.ts | 312 ++ app/src/store/reducers/uiReducer.ts | 111 + app/src/store/resetListener.test.ts | 98 - app/src/store/resetListener.ts | 54 - app/src/store/slices/providersSlice.ts | 80 - app/src/store/slices/prsSlice.ts | 121 - app/src/store/slices/remoteImportSlice.ts | 121 - app/src/store/slices/reposSlice.ts | 230 - app/src/store/slices/settingsSlice.ts | 148 - app/src/store/slices/uiDevFlagsSlice.ts | 58 - app/src/store/slices/uiSlice.test.ts | 41 - app/src/store/slices/uiSlice.ts | 209 - app/src/store/types/providers.types.ts | 7 + app/src/store/types/prs.types.ts | 11 + app/src/store/types/remoteImport.types.ts | 27 + app/src/store/types/repos.types.ts | 9 + app/src/store/types/settings.types.ts | 55 + app/src/store/types/ui.types.ts | 31 + app/src/styles/globals.css | 227 +- app/src/styles/layout.scss | 1542 ------- app/src/styles/page-anim.scss | 183 - app/src/styles/tokens.scss | 711 ---- app/src/styles/views.scss | 3693 ----------------- app/src/test-utils/fixtures.ts | 106 - app/src/{test-setup.ts => test/setup.ts} | 0 app/src/test/utils.tsx | 59 + app/src/theme/ThemeWrapper.tsx | 137 + app/src/theme/index.test.ts | 57 + app/src/theme/index.ts | 278 ++ app/src/theme/mui.d.ts | 176 + app/src/theme/overrides/index.ts | 84 + app/tsconfig.app.json | 2 +- app/vite.config.ts | 2 - app/vitest.config.ts | 3 +- docs/plans/PLAN_GAPS.md | 140 + package.json | 2 + shared/src/constants/terminal.ts | 229 + shared/src/constants/ui.ts | 39 +- shared/src/index.ts | 1 + shared/src/types/ide.ts | 5 + shared/src/types/settings.ts | 43 + tests/playwright.config.ts | 3 + .../app/99-activity-capture-scroll.spec.ts | 9 +- tests/src/helpers/seed/settings.ts | 16 + yarn.lock | 816 ++-- 614 files changed, 23355 insertions(+), 34231 deletions(-) delete mode 100644 app/components.json delete mode 100644 app/src/Welcome.stories.tsx delete mode 100644 app/src/components/atoms/AheadBehind/AheadBehind.stories.tsx delete mode 100644 app/src/components/atoms/AheadBehind/AheadBehind.test.tsx delete mode 100644 app/src/components/atoms/AheadBehind/index.tsx delete mode 100644 app/src/components/atoms/Badge/Badge.stories.tsx delete mode 100644 app/src/components/atoms/Badge/Badge.test.tsx delete mode 100644 app/src/components/atoms/Badge/index.tsx delete mode 100644 app/src/components/atoms/BranchChip/BranchChip.stories.tsx delete mode 100644 app/src/components/atoms/BranchChip/BranchChip.test.tsx delete mode 100644 app/src/components/atoms/BranchChip/index.tsx delete mode 100644 app/src/components/atoms/BrandIcon/BrandIcon.stories.tsx delete mode 100644 app/src/components/atoms/BrandIcon/BrandIcon.test.tsx delete mode 100644 app/src/components/atoms/BrandIcon/index.tsx delete mode 100644 app/src/components/atoms/Button/Button.stories.tsx delete mode 100644 app/src/components/atoms/Button/Button.test.tsx delete mode 100644 app/src/components/atoms/Button/index.tsx delete mode 100644 app/src/components/atoms/Checkbox/Checkbox.stories.tsx delete mode 100644 app/src/components/atoms/Checkbox/Checkbox.test.tsx delete mode 100644 app/src/components/atoms/Checkbox/index.tsx delete mode 100644 app/src/components/atoms/CiDot/CiDot.stories.tsx delete mode 100644 app/src/components/atoms/CiDot/CiDot.test.tsx delete mode 100644 app/src/components/atoms/CiDot/index.tsx delete mode 100644 app/src/components/atoms/ConfirmDialog/index.tsx delete mode 100644 app/src/components/atoms/ConfirmDialog/useConfirm.ts delete mode 100644 app/src/components/atoms/DiffStat/DiffStat.stories.tsx delete mode 100644 app/src/components/atoms/DiffStat/DiffStat.test.tsx delete mode 100644 app/src/components/atoms/DiffStat/index.tsx delete mode 100644 app/src/components/atoms/Icon/Icon.stories.tsx delete mode 100644 app/src/components/atoms/Icon/Icon.test.tsx delete mode 100644 app/src/components/atoms/Icon/index.tsx delete mode 100644 app/src/components/atoms/IdeIcon/IdeIcon.stories.tsx delete mode 100644 app/src/components/atoms/IdeIcon/IdeIcon.test.tsx delete mode 100644 app/src/components/atoms/IdeIcon/index.tsx delete mode 100644 app/src/components/atoms/Input/Input.stories.tsx delete mode 100644 app/src/components/atoms/Input/Input.test.tsx delete mode 100644 app/src/components/atoms/Input/index.tsx delete mode 100644 app/src/components/atoms/Kbd/Kbd.stories.tsx delete mode 100644 app/src/components/atoms/Kbd/Kbd.test.tsx delete mode 100644 app/src/components/atoms/Kbd/index.tsx delete mode 100644 app/src/components/atoms/Label/Label.stories.tsx delete mode 100644 app/src/components/atoms/Label/Label.test.tsx delete mode 100644 app/src/components/atoms/Label/index.tsx delete mode 100644 app/src/components/atoms/LangDot/LangDot.stories.tsx delete mode 100644 app/src/components/atoms/LangDot/LangDot.test.tsx delete mode 100644 app/src/components/atoms/LangDot/index.tsx delete mode 100644 app/src/components/atoms/Mascot/Mascot.stories.tsx delete mode 100644 app/src/components/atoms/Separator/Separator.stories.tsx delete mode 100644 app/src/components/atoms/Separator/Separator.test.tsx delete mode 100644 app/src/components/atoms/Separator/index.tsx delete mode 100644 app/src/components/atoms/Skeleton/Skeleton.stories.tsx delete mode 100644 app/src/components/atoms/Skeleton/Skeleton.test.tsx delete mode 100644 app/src/components/atoms/Skeleton/index.tsx delete mode 100644 app/src/components/atoms/Sparkline/Sparkline.stories.tsx delete mode 100644 app/src/components/atoms/Sparkline/Sparkline.test.tsx delete mode 100644 app/src/components/atoms/Sparkline/index.tsx delete mode 100644 app/src/components/atoms/Spinner/Spinner.stories.tsx delete mode 100644 app/src/components/atoms/Spinner/Spinner.test.tsx delete mode 100644 app/src/components/atoms/Spinner/index.tsx delete mode 100644 app/src/components/atoms/StatusDot/StatusDot.stories.tsx delete mode 100644 app/src/components/atoms/StatusDot/StatusDot.test.tsx delete mode 100644 app/src/components/atoms/StatusDot/index.tsx delete mode 100644 app/src/components/atoms/Switch/Switch.stories.tsx delete mode 100644 app/src/components/atoms/Switch/Switch.test.tsx delete mode 100644 app/src/components/atoms/Switch/index.tsx create mode 100644 app/src/components/atoms/badges/GeneralBadge/index.tsx create mode 100644 app/src/components/atoms/brand/AppIcon/index.tsx create mode 100644 app/src/components/atoms/brand/Logo/index.tsx rename app/src/components/atoms/{ => brand}/Mascot/index.tsx (66%) create mode 100644 app/src/components/atoms/buttons/GeneralButton/index.tsx create mode 100644 app/src/components/atoms/buttons/GeneralButtonGroup/index.tsx create mode 100644 app/src/components/atoms/buttons/GeneralIconButton/index.tsx create mode 100644 app/src/components/atoms/buttons/ThemeSwitcherButton/index.tsx create mode 100644 app/src/components/atoms/data/GeneralSparkline/index.tsx create mode 100644 app/src/components/atoms/dividers/GeneralDivider/index.tsx create mode 100644 app/src/components/atoms/feedback/GeneralSkeleton/index.tsx create mode 100644 app/src/components/atoms/feedback/GeneralSpinner/index.tsx create mode 100644 app/src/components/atoms/feedback/GeneralTooltip/index.tsx create mode 100644 app/src/components/atoms/icons/BrandIcon/index.tsx create mode 100644 app/src/components/atoms/icons/IdeIcon/index.tsx rename app/src/components/atoms/{ => icons}/IdeIcon/logos/intellij-idea.svg (100%) rename app/src/components/atoms/{ => icons}/IdeIcon/logos/jetbrains.svg (100%) rename app/src/components/atoms/{ => icons}/IdeIcon/logos/visual-studio-code.svg (100%) rename app/src/components/atoms/{ => icons}/IdeIcon/logos/webstorm.svg (100%) create mode 100644 app/src/components/atoms/icons/ShellIcon/index.tsx create mode 100644 app/src/components/atoms/icons/ShellIcon/logos/cmd.svg create mode 100644 app/src/components/atoms/icons/ShellIcon/logos/powershell-core.svg create mode 100644 app/src/components/atoms/icons/ShellIcon/logos/windows-powershell.svg create mode 100644 app/src/components/atoms/icons/ShellIcon/logos/wsl.svg create mode 100644 app/src/components/atoms/icons/TerminalIcon/index.tsx create mode 100644 app/src/components/atoms/icons/TerminalIcon/logos/apple-terminal.svg create mode 100644 app/src/components/atoms/icons/TerminalIcon/logos/cmd.svg create mode 100644 app/src/components/atoms/icons/TerminalIcon/logos/kitty.svg create mode 100644 app/src/components/atoms/icons/TerminalIcon/logos/konsole.svg create mode 100644 app/src/components/atoms/icons/TerminalIcon/logos/powershell.svg create mode 100644 app/src/components/atoms/icons/TerminalIcon/logos/tilix.svg create mode 100644 app/src/components/atoms/icons/TerminalIcon/logos/windows-terminal.svg create mode 100644 app/src/components/atoms/icons/TerminalIcon/logos/xterm.svg create mode 100644 app/src/components/atoms/inputs/GeneralCheckboxInput/index.tsx create mode 100644 app/src/components/atoms/inputs/GeneralInput/index.tsx create mode 100644 app/src/components/atoms/inputs/GeneralLabel/index.tsx create mode 100644 app/src/components/atoms/inputs/GeneralSwitchInput/index.tsx create mode 100644 app/src/components/atoms/placeholders/ComingSoonPlaceholder/index.tsx create mode 100644 app/src/components/atoms/transitions/PageTransition/index.tsx create mode 100644 app/src/components/atoms/transitions/StaggeredReveal/index.tsx delete mode 100644 app/src/components/molecules/AuthorAvatar/AuthorAvatar.stories.tsx delete mode 100644 app/src/components/molecules/AuthorAvatar/AuthorAvatar.test.tsx delete mode 100644 app/src/components/molecules/AuthorAvatar/index.tsx delete mode 100644 app/src/components/molecules/BranchFilterChip/BranchFilterChip.stories.tsx delete mode 100644 app/src/components/molecules/BranchFilterChip/BranchFilterChip.test.tsx delete mode 100644 app/src/components/molecules/BranchFilterChip/index.tsx delete mode 100644 app/src/components/molecules/DetailSection/index.tsx delete mode 100644 app/src/components/molecules/Drawer/index.tsx delete mode 100644 app/src/components/molecules/EmptyState/EmptyState.stories.tsx delete mode 100644 app/src/components/molecules/EmptyState/EmptyState.test.tsx delete mode 100644 app/src/components/molecules/EmptyState/index.tsx delete mode 100644 app/src/components/molecules/ExternalLinkButton/ExternalLinkButton.stories.tsx delete mode 100644 app/src/components/molecules/ExternalLinkButton/ExternalLinkButton.test.tsx delete mode 100644 app/src/components/molecules/ExternalLinkButton/index.tsx delete mode 100644 app/src/components/molecules/IconButton/IconButton.stories.tsx delete mode 100644 app/src/components/molecules/IconButton/IconButton.test.tsx delete mode 100644 app/src/components/molecules/IconButton/index.tsx delete mode 100644 app/src/components/molecules/InfoCard/InfoCard.stories.tsx delete mode 100644 app/src/components/molecules/InfoCard/InfoCard.test.tsx delete mode 100644 app/src/components/molecules/InfoCard/index.tsx delete mode 100644 app/src/components/molecules/InfoHint/InfoHint.stories.tsx delete mode 100644 app/src/components/molecules/InfoHint/InfoHint.test.tsx delete mode 100644 app/src/components/molecules/InfoHint/index.tsx delete mode 100644 app/src/components/molecules/KpiCard/KpiCard.stories.tsx delete mode 100644 app/src/components/molecules/KpiCard/KpiCard.test.tsx delete mode 100644 app/src/components/molecules/KpiCard/index.tsx delete mode 100644 app/src/components/molecules/KpiTile/KpiTile.stories.tsx delete mode 100644 app/src/components/molecules/KpiTile/KpiTile.test.tsx delete mode 100644 app/src/components/molecules/KpiTile/index.tsx delete mode 100644 app/src/components/molecules/MrChip/MrChip.stories.tsx delete mode 100644 app/src/components/molecules/MrChip/MrChip.test.tsx delete mode 100644 app/src/components/molecules/MrChip/index.tsx delete mode 100644 app/src/components/molecules/OpenInIdeButton/OpenInIdeButton.stories.tsx delete mode 100644 app/src/components/molecules/OpenInIdeButton/OpenInIdeButton.test.tsx delete mode 100644 app/src/components/molecules/OpenInIdeButton/index.tsx delete mode 100644 app/src/components/molecules/RepoAvatar/RepoAvatar.stories.tsx delete mode 100644 app/src/components/molecules/RepoAvatar/RepoAvatar.test.tsx delete mode 100644 app/src/components/molecules/RepoAvatar/index.tsx delete mode 100644 app/src/components/molecules/SettingsField/SettingsField.stories.tsx delete mode 100644 app/src/components/molecules/SettingsField/SettingsField.test.tsx delete mode 100644 app/src/components/molecules/SettingsField/index.tsx delete mode 100644 app/src/components/molecules/SettingsSectionHeader/SettingsSectionHeader.stories.tsx delete mode 100644 app/src/components/molecules/SettingsSectionHeader/SettingsSectionHeader.test.tsx delete mode 100644 app/src/components/molecules/SettingsSectionHeader/index.tsx delete mode 100644 app/src/components/molecules/Sonner/Sonner.stories.tsx delete mode 100644 app/src/components/molecules/Sonner/Sonner.test.tsx delete mode 100644 app/src/components/molecules/Sonner/index.tsx create mode 100644 app/src/components/molecules/avatars/GeneralAuthorAvatar/index.tsx create mode 100644 app/src/components/molecules/avatars/GeneralRepoAvatar/index.tsx create mode 100644 app/src/components/molecules/cards/GeneralCard/index.tsx delete mode 100644 app/src/components/molecules/compounds/AlertDialog/AlertDialog.stories.tsx delete mode 100644 app/src/components/molecules/compounds/AlertDialog/AlertDialog.test.tsx delete mode 100644 app/src/components/molecules/compounds/AlertDialog/index.tsx delete mode 100644 app/src/components/molecules/compounds/ConfirmDialog/ConfirmDialog.stories.tsx delete mode 100644 app/src/components/molecules/compounds/ConfirmDialog/ConfirmDialog.test.tsx delete mode 100644 app/src/components/molecules/compounds/ConfirmDialog/index.tsx delete mode 100644 app/src/components/molecules/compounds/Dialog/Dialog.stories.tsx delete mode 100644 app/src/components/molecules/compounds/Dialog/Dialog.test.tsx delete mode 100644 app/src/components/molecules/compounds/Dialog/index.tsx delete mode 100644 app/src/components/molecules/compounds/DropdownMenu/DropdownMenu.stories.tsx delete mode 100644 app/src/components/molecules/compounds/DropdownMenu/DropdownMenu.test.tsx delete mode 100644 app/src/components/molecules/compounds/DropdownMenu/index.tsx delete mode 100644 app/src/components/molecules/compounds/Select/Select.stories.tsx delete mode 100644 app/src/components/molecules/compounds/Select/Select.test.tsx delete mode 100644 app/src/components/molecules/compounds/Select/index.tsx delete mode 100644 app/src/components/molecules/compounds/Tabs/Tabs.stories.tsx delete mode 100644 app/src/components/molecules/compounds/Tabs/Tabs.test.tsx delete mode 100644 app/src/components/molecules/compounds/Tabs/index.tsx delete mode 100644 app/src/components/molecules/compounds/Tooltip/Tooltip.stories.tsx delete mode 100644 app/src/components/molecules/compounds/Tooltip/Tooltip.test.tsx delete mode 100644 app/src/components/molecules/compounds/Tooltip/index.tsx delete mode 100644 app/src/components/molecules/compounds/TruncatedTooltip/TruncatedTooltip.test.tsx delete mode 100644 app/src/components/molecules/compounds/TruncatedTooltip/index.tsx create mode 100644 app/src/components/molecules/dialogs/ConfirmDialog/index.tsx create mode 100644 app/src/components/molecules/drawers/GeneralDrawer/index.tsx create mode 100644 app/src/components/molecules/feedback/EmptyState/index.tsx create mode 100644 app/src/components/molecules/feedback/GeneralToaster/index.tsx create mode 100644 app/src/components/molecules/placeholders/EmptyStatePlaceholder/index.tsx delete mode 100644 app/src/components/molecules/skeletons/ActivityBarsSkeleton/ActivityBarsSkeleton.stories.tsx delete mode 100644 app/src/components/molecules/skeletons/ActivityBarsSkeleton/ActivityBarsSkeleton.test.tsx delete mode 100644 app/src/components/molecules/skeletons/ActivityBarsSkeleton/index.tsx delete mode 100644 app/src/components/molecules/skeletons/BranchRowSkeleton/BranchRowSkeleton.stories.tsx delete mode 100644 app/src/components/molecules/skeletons/BranchRowSkeleton/BranchRowSkeleton.test.tsx delete mode 100644 app/src/components/molecules/skeletons/BranchRowSkeleton/index.tsx delete mode 100644 app/src/components/molecules/skeletons/CardBlockSkeleton/CardBlockSkeleton.stories.tsx delete mode 100644 app/src/components/molecules/skeletons/CardBlockSkeleton/CardBlockSkeleton.test.tsx delete mode 100644 app/src/components/molecules/skeletons/CardBlockSkeleton/index.tsx delete mode 100644 app/src/components/molecules/skeletons/CommitListSkeleton/CommitListSkeleton.stories.tsx delete mode 100644 app/src/components/molecules/skeletons/CommitListSkeleton/CommitListSkeleton.test.tsx delete mode 100644 app/src/components/molecules/skeletons/CommitListSkeleton/index.tsx delete mode 100644 app/src/components/molecules/skeletons/CommitRowSkeleton/CommitRowSkeleton.stories.tsx delete mode 100644 app/src/components/molecules/skeletons/CommitRowSkeleton/CommitRowSkeleton.test.tsx delete mode 100644 app/src/components/molecules/skeletons/CommitRowSkeleton/index.tsx create mode 100644 app/src/components/molecules/skeletons/DashboardSkeletons/index.tsx delete mode 100644 app/src/components/molecules/skeletons/FileChangesSkeleton/FileChangesSkeleton.stories.tsx delete mode 100644 app/src/components/molecules/skeletons/FileChangesSkeleton/FileChangesSkeleton.test.tsx delete mode 100644 app/src/components/molecules/skeletons/FileChangesSkeleton/index.tsx delete mode 100644 app/src/components/molecules/skeletons/KpiSkeleton/KpiSkeleton.stories.tsx delete mode 100644 app/src/components/molecules/skeletons/KpiSkeleton/KpiSkeleton.test.tsx delete mode 100644 app/src/components/molecules/skeletons/KpiSkeleton/index.tsx delete mode 100644 app/src/components/molecules/skeletons/MrListSkeleton/MrListSkeleton.stories.tsx delete mode 100644 app/src/components/molecules/skeletons/MrListSkeleton/MrListSkeleton.test.tsx delete mode 100644 app/src/components/molecules/skeletons/MrListSkeleton/index.tsx delete mode 100644 app/src/components/molecules/skeletons/MrRowSkeleton/MrRowSkeleton.stories.tsx delete mode 100644 app/src/components/molecules/skeletons/MrRowSkeleton/MrRowSkeleton.test.tsx delete mode 100644 app/src/components/molecules/skeletons/MrRowSkeleton/index.tsx delete mode 100644 app/src/components/molecules/skeletons/RemoteRepoCardSkeleton/RemoteRepoCardSkeleton.stories.tsx delete mode 100644 app/src/components/molecules/skeletons/RemoteRepoCardSkeleton/RemoteRepoCardSkeleton.test.tsx delete mode 100644 app/src/components/molecules/skeletons/RemoteRepoCardSkeleton/index.tsx delete mode 100644 app/src/components/molecules/skeletons/RemoteRepoListSkeleton/RemoteRepoListSkeleton.stories.tsx delete mode 100644 app/src/components/molecules/skeletons/RemoteRepoListSkeleton/RemoteRepoListSkeleton.test.tsx delete mode 100644 app/src/components/molecules/skeletons/RemoteRepoListSkeleton/index.tsx delete mode 100644 app/src/components/molecules/skeletons/RepoListSkeleton/RepoListSkeleton.stories.tsx delete mode 100644 app/src/components/molecules/skeletons/RepoListSkeleton/RepoListSkeleton.test.tsx delete mode 100644 app/src/components/molecules/skeletons/RepoListSkeleton/index.tsx delete mode 100644 app/src/components/molecules/skeletons/RepoRowSkeleton/RepoRowSkeleton.stories.tsx delete mode 100644 app/src/components/molecules/skeletons/RepoRowSkeleton/RepoRowSkeleton.test.tsx delete mode 100644 app/src/components/molecules/skeletons/RepoRowSkeleton/index.tsx delete mode 100644 app/src/components/molecules/skeletons/ReviewerChipsSkeleton/ReviewerChipsSkeleton.stories.tsx delete mode 100644 app/src/components/molecules/skeletons/ReviewerChipsSkeleton/ReviewerChipsSkeleton.test.tsx delete mode 100644 app/src/components/molecules/skeletons/ReviewerChipsSkeleton/index.tsx delete mode 100644 app/src/components/molecules/skeletons/SearchGroupSkeleton/SearchGroupSkeleton.stories.tsx delete mode 100644 app/src/components/molecules/skeletons/SearchGroupSkeleton/SearchGroupSkeleton.test.tsx delete mode 100644 app/src/components/molecules/skeletons/SearchGroupSkeleton/index.tsx delete mode 100644 app/src/components/molecules/skeletons/SearchHitSkeleton/SearchHitSkeleton.stories.tsx delete mode 100644 app/src/components/molecules/skeletons/SearchHitSkeleton/SearchHitSkeleton.test.tsx delete mode 100644 app/src/components/molecules/skeletons/SearchHitSkeleton/index.tsx delete mode 100644 app/src/components/molecules/skeletons/TimelineEventsSkeleton/TimelineEventsSkeleton.stories.tsx delete mode 100644 app/src/components/molecules/skeletons/TimelineEventsSkeleton/TimelineEventsSkeleton.test.tsx delete mode 100644 app/src/components/molecules/skeletons/TimelineEventsSkeleton/index.tsx create mode 100644 app/src/components/molecules/toggles/ScopeToggle/index.tsx delete mode 100644 app/src/components/organisms/activity/Timeline/Timeline.stories.tsx delete mode 100644 app/src/components/organisms/activity/Timeline/Timeline.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/AuthorClockCard/AuthorClockCard.stories.tsx delete mode 100644 app/src/components/organisms/activity/cards/AuthorClockCard/AuthorClockCard.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/AuthorsHero/AuthorsHero.stories.tsx delete mode 100644 app/src/components/organisms/activity/cards/AuthorsHero/AuthorsHero.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/BusiestPeakCard/BusiestPeakCard.stories.tsx delete mode 100644 app/src/components/organisms/activity/cards/BusiestPeakCard/BusiestPeakCard.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/CardShell/CardShell.stories.tsx delete mode 100644 app/src/components/organisms/activity/cards/CardShell/CardShell.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/CardShell/index.tsx delete mode 100644 app/src/components/organisms/activity/cards/ChurnCard/ChurnCard.stories.tsx delete mode 100644 app/src/components/organisms/activity/cards/ChurnCard/ChurnCard.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/CiHealthHero/CiHealthHero.stories.tsx delete mode 100644 app/src/components/organisms/activity/cards/CiHealthHero/CiHealthHero.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/CiPassRateCard/CiPassRateCard.stories.tsx delete mode 100644 app/src/components/organisms/activity/cards/CiPassRateCard/CiPassRateCard.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/CommitsHero/CommitsHero.stories.tsx delete mode 100644 app/src/components/organisms/activity/cards/CommitsHero/CommitsHero.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/FlakyReposCard/FlakyReposCard.stories.tsx delete mode 100644 app/src/components/organisms/activity/cards/FlakyReposCard/FlakyReposCard.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/HeatmapCard/HeatmapCard.stories.tsx delete mode 100644 app/src/components/organisms/activity/cards/HeatmapCard/HeatmapCard.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/LanguageDonutCard/LanguageDonutCard.stories.tsx delete mode 100644 app/src/components/organisms/activity/cards/LanguageDonutCard/LanguageDonutCard.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/LeaderboardCard/LeaderboardCard.stories.tsx delete mode 100644 app/src/components/organisms/activity/cards/LeaderboardCard/LeaderboardCard.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/OpenPrsHero/OpenPrsHero.stories.tsx delete mode 100644 app/src/components/organisms/activity/cards/OpenPrsHero/OpenPrsHero.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/PrVelocityCard/PrVelocityCard.stories.tsx delete mode 100644 app/src/components/organisms/activity/cards/PrVelocityCard/PrVelocityCard.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/QuietestReposCard/QuietestReposCard.stories.tsx delete mode 100644 app/src/components/organisms/activity/cards/QuietestReposCard/QuietestReposCard.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/ReviewQueueCard/ReviewQueueCard.stories.tsx delete mode 100644 app/src/components/organisms/activity/cards/ReviewQueueCard/ReviewQueueCard.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/StackedChartCard/StackedChartCard.stories.tsx delete mode 100644 app/src/components/organisms/activity/cards/StackedChartCard/StackedChartCard.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/StreakCard/StreakCard.stories.tsx delete mode 100644 app/src/components/organisms/activity/cards/StreakCard/StreakCard.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/TimeToMergeCard/TimeToMergeCard.stories.tsx delete mode 100644 app/src/components/organisms/activity/cards/TimeToMergeCard/TimeToMergeCard.test.tsx delete mode 100644 app/src/components/organisms/activity/cards/_fixtures.ts create mode 100644 app/src/components/organisms/banners/UpdaterBanner/index.tsx delete mode 100644 app/src/components/organisms/brand/AppIcon/index.tsx delete mode 100644 app/src/components/organisms/brand/BrandMark/BrandMark.stories.tsx delete mode 100644 app/src/components/organisms/brand/BrandMark/BrandMark.test.tsx delete mode 100644 app/src/components/organisms/brand/BrandMark/index.tsx delete mode 100644 app/src/components/organisms/brand/Logo/Logo.stories.tsx delete mode 100644 app/src/components/organisms/brand/Logo/Logo.test.tsx delete mode 100644 app/src/components/organisms/brand/Logo/index.tsx create mode 100644 app/src/components/organisms/cards/ActivityCardShell/index.tsx create mode 100644 app/src/components/organisms/cards/HeatmapCard/index.tsx create mode 100644 app/src/components/organisms/cards/LanguageDonutCard/index.tsx delete mode 100644 app/src/components/organisms/feedback/ErrorBoundary/ErrorBoundary.stories.tsx delete mode 100644 app/src/components/organisms/feedback/ErrorBoundary/ErrorBoundary.test.tsx delete mode 100644 app/src/components/organisms/feedback/ErrorBoundary/index.tsx delete mode 100644 app/src/components/organisms/feedback/UpdaterBanner/UpdaterBanner.stories.tsx delete mode 100644 app/src/components/organisms/feedback/UpdaterBanner/UpdaterBanner.test.tsx delete mode 100644 app/src/components/organisms/feedback/UpdaterBanner/index.tsx delete mode 100644 app/src/components/organisms/layout/AppShell/AppShell.stories.tsx delete mode 100644 app/src/components/organisms/layout/AppShell/AppShell.test.tsx delete mode 100644 app/src/components/organisms/layout/AppShell/index.tsx delete mode 100644 app/src/components/organisms/layout/DetailPane/DetailPane.stories.tsx delete mode 100644 app/src/components/organisms/layout/DetailPane/DetailPane.test.tsx delete mode 100644 app/src/components/organisms/layout/DetailPane/index.tsx delete mode 100644 app/src/components/organisms/layout/Header/Header.stories.tsx delete mode 100644 app/src/components/organisms/layout/Header/Header.test.tsx delete mode 100644 app/src/components/organisms/layout/Sidebar/Sidebar.stories.tsx delete mode 100644 app/src/components/organisms/layout/Sidebar/Sidebar.test.tsx delete mode 100644 app/src/components/organisms/layout/Titlebar/GnomeTitlebar.tsx delete mode 100644 app/src/components/organisms/layout/Titlebar/MacOverlayTitlebar.tsx delete mode 100644 app/src/components/organisms/layout/Titlebar/Titlebar.stories.tsx delete mode 100644 app/src/components/organisms/layout/Titlebar/Titlebar.test.tsx delete mode 100644 app/src/components/organisms/mergeRequests/MergeRequestDetailPanel/index.tsx delete mode 100644 app/src/components/organisms/onboarding/OnboardingWizard/OnboardingWizard.stories.tsx delete mode 100644 app/src/components/organisms/onboarding/OnboardingWizard/OnboardingWizard.test.tsx delete mode 100644 app/src/components/organisms/onboarding/OnboardingWizard/index.tsx delete mode 100644 app/src/components/organisms/onboarding/_test-helpers.tsx delete mode 100644 app/src/components/organisms/onboarding/steps/BasicsStep/BasicsStep.stories.tsx delete mode 100644 app/src/components/organisms/onboarding/steps/BasicsStep/BasicsStep.test.tsx delete mode 100644 app/src/components/organisms/onboarding/steps/BasicsStep/index.tsx delete mode 100644 app/src/components/organisms/onboarding/steps/ConnectProviderStep/ConnectProviderStep.stories.tsx delete mode 100644 app/src/components/organisms/onboarding/steps/ConnectProviderStep/ConnectProviderStep.test.tsx delete mode 100644 app/src/components/organisms/onboarding/steps/ConnectProviderStep/index.tsx delete mode 100644 app/src/components/organisms/onboarding/steps/DoneStep/DoneStep.stories.tsx delete mode 100644 app/src/components/organisms/onboarding/steps/DoneStep/DoneStep.test.tsx delete mode 100644 app/src/components/organisms/onboarding/steps/DoneStep/index.tsx delete mode 100644 app/src/components/organisms/onboarding/steps/InitialScanStep/InitialScanStep.stories.tsx delete mode 100644 app/src/components/organisms/onboarding/steps/InitialScanStep/InitialScanStep.test.tsx delete mode 100644 app/src/components/organisms/onboarding/steps/InitialScanStep/index.tsx delete mode 100644 app/src/components/organisms/onboarding/steps/PickFolderStep/PickFolderStep.stories.tsx delete mode 100644 app/src/components/organisms/onboarding/steps/PickFolderStep/PickFolderStep.test.tsx delete mode 100644 app/src/components/organisms/onboarding/steps/PickFolderStep/index.tsx delete mode 100644 app/src/components/organisms/onboarding/steps/WelcomeStep/WelcomeStep.stories.tsx delete mode 100644 app/src/components/organisms/onboarding/steps/WelcomeStep/WelcomeStep.test.tsx delete mode 100644 app/src/components/organisms/onboarding/steps/WelcomeStep/index.tsx delete mode 100644 app/src/components/organisms/prs/PrList/PrList.stories.tsx delete mode 100644 app/src/components/organisms/prs/PrList/PrList.test.tsx delete mode 100644 app/src/components/organisms/prs/PrList/index.tsx delete mode 100644 app/src/components/organisms/prs/PrRow/PrRow.stories.tsx delete mode 100644 app/src/components/organisms/prs/PrRow/PrRow.test.tsx delete mode 100644 app/src/components/organisms/prs/PrRow/index.tsx create mode 100644 app/src/components/organisms/repos/AddRepoDialog/index.tsx delete mode 100644 app/src/components/organisms/repos/ChangedFilesList/ChangedFilesList.stories.tsx delete mode 100644 app/src/components/organisms/repos/ChangedFilesList/ChangedFilesList.test.tsx delete mode 100644 app/src/components/organisms/repos/ChangedFilesList/index.tsx delete mode 100644 app/src/components/organisms/repos/CreateBranchDialog/CreateBranchDialog.stories.tsx delete mode 100644 app/src/components/organisms/repos/CreateBranchDialog/CreateBranchDialog.test.tsx delete mode 100644 app/src/components/organisms/repos/CreateBranchDialog/index.tsx delete mode 100644 app/src/components/organisms/repos/FindAcrossReposDialog/FindAcrossReposDialog.stories.tsx delete mode 100644 app/src/components/organisms/repos/FindAcrossReposDialog/FindAcrossReposDialog.test.tsx delete mode 100644 app/src/components/organisms/repos/FindAcrossReposDialog/index.tsx delete mode 100644 app/src/components/organisms/repos/ImportFromProviderDialog/ImportFromProviderDialog.stories.tsx delete mode 100644 app/src/components/organisms/repos/ImportFromProviderDialog/ImportFromProviderDialog.test.tsx delete mode 100644 app/src/components/organisms/repos/ImportFromProviderDialog/index.tsx delete mode 100644 app/src/components/organisms/repos/RepoCard/index.tsx delete mode 100644 app/src/components/organisms/repos/RepoDetail/RepoDetail.stories.tsx delete mode 100644 app/src/components/organisms/repos/RepoDetail/RepoDetail.test.tsx delete mode 100644 app/src/components/organisms/repos/RepoDetail/index.tsx delete mode 100644 app/src/components/organisms/repos/RepoList/RepoList.stories.tsx delete mode 100644 app/src/components/organisms/repos/RepoList/RepoList.test.tsx delete mode 100644 app/src/components/organisms/repos/RepoList/index.tsx delete mode 100644 app/src/components/organisms/repos/RepoRow/RepoRow.stories.tsx delete mode 100644 app/src/components/organisms/repos/RepoRow/RepoRow.test.tsx delete mode 100644 app/src/components/organisms/repos/RepoRow/index.tsx delete mode 100644 app/src/components/organisms/repos/RepoStats/RepoStats.stories.tsx delete mode 100644 app/src/components/organisms/repos/RepoStats/RepoStats.test.tsx delete mode 100644 app/src/components/organisms/repos/RepoStats/index.tsx delete mode 100644 app/src/components/organisms/search/SearchOverlay/SearchOverlay.stories.tsx delete mode 100644 app/src/components/organisms/search/SearchOverlay/SearchOverlay.test.tsx delete mode 100644 app/src/components/organisms/settings/ProviderAuth/ProviderAuth.stories.tsx delete mode 100644 app/src/components/organisms/settings/ProviderAuth/ProviderAuth.test.tsx delete mode 100644 app/src/components/organisms/settings/ProviderAuth/index.tsx delete mode 100644 app/src/components/organisms/settings/RepoSources/RepoSources.stories.tsx delete mode 100644 app/src/components/organisms/settings/RepoSources/RepoSources.test.tsx delete mode 100644 app/src/components/organisms/settings/RepoSources/index.tsx delete mode 100644 app/src/components/organisms/settings/SettingsSection/SettingsSection.stories.tsx delete mode 100644 app/src/components/organisms/settings/SettingsSection/SettingsSection.test.tsx delete mode 100644 app/src/components/organisms/settings/SettingsSection/index.tsx delete mode 100644 app/src/components/organisms/settings/SettingsView/SettingsView.stories.tsx delete mode 100644 app/src/components/organisms/settings/SettingsView/SettingsView.test.tsx delete mode 100644 app/src/components/organisms/settings/SettingsView/index.tsx delete mode 100644 app/src/components/organisms/settings/_test-helpers.tsx delete mode 100644 app/src/components/organisms/settings/tabs/AboutTab/AboutTab.stories.tsx delete mode 100644 app/src/components/organisms/settings/tabs/AboutTab/AboutTab.test.tsx delete mode 100644 app/src/components/organisms/settings/tabs/AboutTab/index.tsx delete mode 100644 app/src/components/organisms/settings/tabs/AppearanceSettings/AppearanceSettings.stories.tsx delete mode 100644 app/src/components/organisms/settings/tabs/AppearanceSettings/AppearanceSettings.test.tsx delete mode 100644 app/src/components/organisms/settings/tabs/AppearanceSettings/index.tsx delete mode 100644 app/src/components/organisms/settings/tabs/DesktopSettings/DesktopSettings.stories.tsx delete mode 100644 app/src/components/organisms/settings/tabs/DesktopSettings/DesktopSettings.test.tsx delete mode 100644 app/src/components/organisms/settings/tabs/DesktopSettings/index.tsx delete mode 100644 app/src/components/organisms/settings/tabs/DeveloperTab/DeveloperTab.test.tsx delete mode 100644 app/src/components/organisms/settings/tabs/DeveloperTab/index.tsx delete mode 100644 app/src/components/organisms/settings/tabs/DiagnosticsSettings/DiagnosticsSettings.stories.tsx delete mode 100644 app/src/components/organisms/settings/tabs/DiagnosticsSettings/DiagnosticsSettings.test.tsx delete mode 100644 app/src/components/organisms/settings/tabs/DiagnosticsSettings/index.tsx delete mode 100644 app/src/components/organisms/settings/tabs/NotificationSettings/NotificationSettings.stories.tsx delete mode 100644 app/src/components/organisms/settings/tabs/NotificationSettings/NotificationSettings.test.tsx delete mode 100644 app/src/components/organisms/settings/tabs/NotificationSettings/index.tsx delete mode 100644 app/src/components/organisms/settings/tabs/SystemSettings/SystemSettings.stories.tsx delete mode 100644 app/src/components/organisms/settings/tabs/SystemSettings/SystemSettings.test.tsx delete mode 100644 app/src/components/organisms/settings/tabs/SystemSettings/index.tsx delete mode 100644 app/src/components/organisms/settings/tabs/UpdatesSettings/UpdatesSettings.stories.tsx delete mode 100644 app/src/components/organisms/settings/tabs/UpdatesSettings/UpdatesSettings.test.tsx delete mode 100644 app/src/components/organisms/settings/tabs/UpdatesSettings/index.tsx delete mode 100644 app/src/components/organisms/settings/tabs/useSettingsSaver.ts create mode 100644 app/src/components/organisms/titlebars/GnomeTitlebar/index.tsx create mode 100644 app/src/components/organisms/titlebars/MacOverlayTitlebar/index.tsx rename app/src/components/organisms/{layout => titlebars}/Titlebar/index.tsx (59%) rename app/src/components/organisms/{layout/Titlebar/Win11Titlebar.tsx => titlebars/Win11Titlebar/index.tsx} (51%) rename app/src/components/organisms/{layout/Titlebar => titlebars}/runWindow.ts (66%) delete mode 100644 app/src/hooks/useActiveIde.ts create mode 100644 app/src/hooks/useAppBootstrap.ts delete mode 100644 app/src/hooks/useDeepLinks.ts delete mode 100644 app/src/hooks/useDevFlag.test.ts delete mode 100644 app/src/hooks/useDevFlag.ts create mode 100644 app/src/hooks/useFaviconSync.ts delete mode 100644 app/src/hooks/useFirstRun.ts create mode 100644 app/src/hooks/useFullbleedScroll.ts delete mode 100644 app/src/hooks/useGitInfo.ts delete mode 100644 app/src/hooks/useGlobalEvents.ts delete mode 100644 app/src/hooks/useGlobalShortcuts.ts delete mode 100644 app/src/hooks/useLastSeenVersion.ts create mode 100644 app/src/hooks/useLocaleSync.ts delete mode 100644 app/src/hooks/useNotificationTriggers.test.ts delete mode 100644 app/src/hooks/useNotificationTriggers.ts rename app/src/hooks/{useProviders.ts => usePrPolling.ts} (51%) create mode 100644 app/src/hooks/useReducedMotion.ts delete mode 100644 app/src/hooks/useRepoFavicon.ts delete mode 100644 app/src/hooks/useRepoLogo.ts create mode 100644 app/src/hooks/useResponsiveSidebar.ts delete mode 100644 app/src/hooks/useTauri.ts delete mode 100644 app/src/hooks/useTheme.ts create mode 100644 app/src/hooks/useThemeAttribute.ts delete mode 100644 app/src/hooks/useTrayBadgeSync.ts create mode 100644 app/src/layouts/AppLayout/AppLayout.tsx create mode 100644 app/src/layouts/AppLayout/index.ts delete mode 100644 app/src/lib/activityAggregates.test.ts create mode 100644 app/src/lib/animations/pageAnimations.ts delete mode 100644 app/src/lib/authorNormalize.test.ts delete mode 100644 app/src/lib/charts/smoothLine.test.ts create mode 100644 app/src/lib/constants/events.ts create mode 100644 app/src/lib/constants/theme.constants.ts create mode 100644 app/src/lib/dev/seed/remote.ts delete mode 100644 app/src/lib/gravatar.ts delete mode 100644 app/src/lib/initials.ts delete mode 100644 app/src/lib/languages.ts delete mode 100644 app/src/lib/repoEnrich.test.ts rename app/src/lib/{dev/tauriStub.ts => tauri/devStub.ts} (83%) create mode 100644 app/src/lib/tauri/events.ts delete mode 100644 app/src/lib/tauri/services/autostartService.ts delete mode 100644 app/src/lib/tauri/services/index.ts delete mode 100644 app/src/lib/tauri/services/notificationService.ts delete mode 100644 app/src/lib/tauri/services/storageService.ts delete mode 100644 app/src/lib/tauri/services/systemService.ts delete mode 100644 app/src/lib/tauri/services/trayService.ts delete mode 100644 app/src/lib/tauri/services/updaterService.ts delete mode 100644 app/src/lib/tauri/services/windowService.ts delete mode 100644 app/src/lib/toast.ts delete mode 100644 app/src/lib/utils.ts create mode 100644 app/src/lib/utils/brandFromUrl.ts create mode 100644 app/src/lib/utils/theme.utils.ts rename app/src/{i18n => }/locales/de/common.json (98%) rename app/src/{i18n => }/locales/de/errors.json (100%) rename app/src/{i18n => }/locales/de/onboarding.json (100%) rename app/src/{i18n => }/locales/de/prs.json (100%) rename app/src/{i18n => }/locales/de/repos.json (100%) rename app/src/{i18n => }/locales/de/settings.json (98%) rename app/src/{i18n => }/locales/en/common.json (98%) rename app/src/{i18n => }/locales/en/errors.json (100%) rename app/src/{i18n => }/locales/en/onboarding.json (100%) rename app/src/{i18n => }/locales/en/prs.json (100%) rename app/src/{i18n => }/locales/en/repos.json (100%) rename app/src/{i18n => }/locales/en/settings.json (98%) rename app/src/{i18n => locales}/index.ts (69%) delete mode 100644 app/src/pages/ActivityPage.tsx delete mode 100644 app/src/pages/BranchesPage.tsx delete mode 100644 app/src/pages/DashboardPage.tsx delete mode 100644 app/src/pages/MergeRequestsPage.tsx delete mode 100644 app/src/pages/PullRequestsPage.tsx delete mode 100644 app/src/pages/RepoDetailPage.tsx delete mode 100644 app/src/pages/ReposPage.tsx delete mode 100644 app/src/pages/SettingsPage.tsx delete mode 100644 app/src/pages/__tests__/RepoDetailPage.test.tsx create mode 100644 app/src/pages/app/Activity/index.tsx create mode 100644 app/src/pages/app/Branches/index.tsx create mode 100644 app/src/pages/app/Changes/index.tsx create mode 100644 app/src/pages/app/Dashboard/QuickActionsCard.tsx create mode 100644 app/src/pages/app/Dashboard/index.tsx create mode 100644 app/src/pages/app/MergeRequests/components/MrDetailPanel/MrDetailPanel.tsx create mode 100644 app/src/pages/app/MergeRequests/components/MrDetailPanel/index.ts create mode 100644 app/src/pages/app/MergeRequests/components/MrRow/MrRow.tsx create mode 100644 app/src/pages/app/MergeRequests/components/MrRow/index.ts create mode 100644 app/src/pages/app/MergeRequests/index.tsx create mode 100644 app/src/pages/app/RepoDetail/index.tsx create mode 100644 app/src/pages/app/Repos/components/DetailPane/DetailPane.tsx create mode 100644 app/src/pages/app/Repos/components/DetailPane/index.ts create mode 100644 app/src/pages/app/Repos/components/RepoCard/RepoCard.tsx create mode 100644 app/src/pages/app/Repos/components/RepoCard/index.ts create mode 100644 app/src/pages/app/Repos/components/RepoList/RepoList.tsx create mode 100644 app/src/pages/app/Repos/components/RepoList/index.ts create mode 100644 app/src/pages/app/Repos/components/RepoRow/RepoRow.tsx create mode 100644 app/src/pages/app/Repos/components/RepoRow/index.ts create mode 100644 app/src/pages/app/Repos/index.tsx create mode 100644 app/src/pages/app/Settings/components/AboutTab/index.tsx create mode 100644 app/src/pages/app/Settings/components/AccountsTab/index.tsx create mode 100644 app/src/pages/app/Settings/components/DeveloperTab/DeveloperTab.tsx create mode 100644 app/src/pages/app/Settings/components/DeveloperTab/index.ts create mode 100644 app/src/pages/app/Settings/components/GeneralTab/index.tsx create mode 100644 app/src/pages/app/Settings/components/IntegrationsTab/index.tsx create mode 100644 app/src/pages/app/Settings/components/SettingsPrimitives/index.tsx create mode 100644 app/src/pages/app/Settings/components/ShortcutsTab/index.tsx create mode 100644 app/src/pages/app/Settings/components/StorageTab/index.tsx create mode 100644 app/src/pages/app/Settings/index.tsx create mode 100644 app/src/store/actions/providers.actions.ts create mode 100644 app/src/store/actions/prs.actions.ts create mode 100644 app/src/store/actions/remoteImport.actions.ts create mode 100644 app/src/store/actions/repos.actions.ts create mode 100644 app/src/store/actions/settings.actions.ts create mode 100644 app/src/store/actions/ui.actions.ts create mode 100644 app/src/store/backendSync.ts delete mode 100644 app/src/store/persistence.ts create mode 100644 app/src/store/reducers/providersReducer.ts create mode 100644 app/src/store/reducers/prsReducer.ts create mode 100644 app/src/store/reducers/remoteImportReducer.ts create mode 100644 app/src/store/reducers/reposReducer.ts create mode 100644 app/src/store/reducers/settingsReducer.ts create mode 100644 app/src/store/reducers/uiReducer.ts delete mode 100644 app/src/store/resetListener.test.ts delete mode 100644 app/src/store/resetListener.ts delete mode 100644 app/src/store/slices/providersSlice.ts delete mode 100644 app/src/store/slices/prsSlice.ts delete mode 100644 app/src/store/slices/remoteImportSlice.ts delete mode 100644 app/src/store/slices/reposSlice.ts delete mode 100644 app/src/store/slices/settingsSlice.ts delete mode 100644 app/src/store/slices/uiDevFlagsSlice.ts delete mode 100644 app/src/store/slices/uiSlice.test.ts delete mode 100644 app/src/store/slices/uiSlice.ts create mode 100644 app/src/store/types/providers.types.ts create mode 100644 app/src/store/types/prs.types.ts create mode 100644 app/src/store/types/remoteImport.types.ts create mode 100644 app/src/store/types/repos.types.ts create mode 100644 app/src/store/types/settings.types.ts create mode 100644 app/src/store/types/ui.types.ts delete mode 100644 app/src/styles/layout.scss delete mode 100644 app/src/styles/page-anim.scss delete mode 100644 app/src/styles/tokens.scss delete mode 100644 app/src/styles/views.scss delete mode 100644 app/src/test-utils/fixtures.ts rename app/src/{test-setup.ts => test/setup.ts} (100%) create mode 100644 app/src/test/utils.tsx create mode 100644 app/src/theme/ThemeWrapper.tsx create mode 100644 app/src/theme/index.test.ts create mode 100644 app/src/theme/index.ts create mode 100644 app/src/theme/mui.d.ts create mode 100644 app/src/theme/overrides/index.ts create mode 100644 docs/plans/PLAN_GAPS.md create mode 100644 shared/src/constants/terminal.ts diff --git a/.prettierignore b/.prettierignore index c1e8b36..7d7c0b1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,6 +8,7 @@ package-lock.json *.tsbuildinfo app/src-tauri/target app/src-tauri/gen +app/src-old/ # SVGs have no Prettier parser and are either hand-crafted art or generated # from upstream icon sets; leave them untouched. diff --git a/app/.gitignore b/app/.gitignore index 5848829..42e2392 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -27,7 +27,7 @@ dist-ssr ._* .Spotlight-V100 .Trashes -.VolumeIcon.icns +.VolumeIcon.icns # build output build/ @@ -36,4 +36,4 @@ build/ storybook-static/ # Superpowers brainstorm mockups -.superpowers/ \ No newline at end of file +.superpowers/ diff --git a/app/.prettierrc b/app/.prettierrc index 8a8f7e1..a385612 100644 --- a/app/.prettierrc +++ b/app/.prettierrc @@ -8,14 +8,22 @@ "arrowParens": "always", "endOfLine": "lf", "plugins": ["@trivago/prettier-plugin-sort-imports"], + "importOrderParserPlugins": ["typescript", "jsx"], "importOrder": [ - "^react(-dom)?$", + "^react$", + "^react-dom", + "^react-redux$", "^react-router", + "^react-", + "^@mui/", + "^@reduxjs/toolkit", + "^redux", "^@tauri-apps/", - "", "^@recrest/(.*)$", - "^@/(.*)$", - "^[./]" + "", + "^@/", + "^\\.", + "^.+\\.css$" ], "importOrderSeparation": true, "importOrderSortSpecifiers": true diff --git a/app/.storybook/preview.tsx b/app/.storybook/preview.tsx index 7843d83..a7e7010 100644 --- a/app/.storybook/preview.tsx +++ b/app/.storybook/preview.tsx @@ -1,29 +1,38 @@ +import CssBaseline from "@mui/material/CssBaseline"; +import { ThemeProvider } from "@mui/material/styles"; import type { Preview } from "@storybook/react-vite"; -import { TooltipProvider } from "@/components/molecules/compounds/Tooltip"; - -import "@/i18n"; -import "@/styles/globals.css"; +import { THEMES, type ThemeId } from "@/lib/constants/theme.constants"; +import { getTheme } from "@/theme"; const preview: Preview = { parameters: { controls: { expanded: true }, layout: "centered", - backgrounds: { - default: "app", - values: [ - { name: "app", value: "var(--surface)" }, - { name: "dark", value: "#0f1115" }, - { name: "light", value: "#ffffff" }, - ], + }, + globalTypes: { + themeId: { + description: "App theme", + defaultValue: "light", + toolbar: { + title: "Theme", + icon: "paintbrush", + items: THEMES.map((t) => ({ value: t.id, title: t.label })), + dynamicTitle: true, + }, }, }, decorators: [ - (Story) => ( - - - - ), + (Story, ctx) => { + const themeId = (ctx.globals.themeId ?? "light") as ThemeId; + const theme = getTheme(themeId); + return ( + + + + + ); + }, ], }; diff --git a/app/components.json b/app/components.json deleted file mode 100644 index 285033d..0000000 --- a/app/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/styles/globals.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "lucide" -} diff --git a/app/eslint.config.js b/app/eslint.config.js index 4c35011..5b6ac8a 100644 --- a/app/eslint.config.js +++ b/app/eslint.config.js @@ -37,6 +37,21 @@ export default tseslint.config( "error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, ], + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: ["@radix-ui/*"], + message: "Radix is removed — use the matching MUI component instead.", + }, + { + group: ["tailwindcss", "tw-animate-css", "class-variance-authority", "tailwind-merge"], + message: "Tailwind/CVA stack is removed — use MUI sx + theme tokens.", + }, + ], + }, + ], }, settings: { react: { version: "detect" }, @@ -47,6 +62,7 @@ export default tseslint.config( "dist/**", "node_modules/**", "src-tauri/**", + "src-old/**", "storybook-static/**", "src/scripts/**", ], diff --git a/app/index.html b/app/index.html index 065f50e..f7b711c 100644 --- a/app/index.html +++ b/app/index.html @@ -2,9 +2,17 @@ - + - @@ -19,6 +27,56 @@ rel="stylesheet" /> Recrest + diff --git a/app/package.json b/app/package.json index 5518ed5..3e83725 100644 --- a/app/package.json +++ b/app/package.json @@ -30,20 +30,18 @@ "dep-graph:dot": "madge --dot --extensions ts,tsx --ts-config tsconfig.app.json src/ > dependency-graph.dot" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@fontsource/fira-code": "^5.2.7", + "@fontsource/geist": "^5.2.9", + "@fontsource/geist-mono": "^5.2.8", + "@fontsource/ibm-plex-mono": "^5.2.7", + "@fontsource/ibm-plex-sans": "^5.2.8", "@fontsource/inter": "^5.2.8", "@fontsource/opendyslexic": "^5.2.5", "@fontsource/space-grotesk": "^5.2.10", - "@radix-ui/react-alert-dialog": "^1.1.15", - "@radix-ui/react-checkbox": "^1.3.3", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.8", - "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-switch": "^1.2.6", - "@radix-ui/react-tabs": "^1.1.13", - "@radix-ui/react-tooltip": "^1.2.8", + "@mui/icons-material": "^9.0.1", + "@mui/material": "^9.0.1", "@recrest/shared": "*", "@reduxjs/toolkit": "^2.2.8", "@tauri-apps/api": "^2.1.1", @@ -58,7 +56,6 @@ "@tauri-apps/plugin-store": "^2.4.2", "@tauri-apps/plugin-updater": "^2.10.1", "@use-gesture/react": "^10.3.1", - "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "device-type-detection": "^2.1.3", @@ -72,14 +69,12 @@ "react-redux": "^9.1.2", "react-router-dom": "^6.27.0", "simple-icons": "^16.17.0", - "sonner": "^2.0.7", - "tailwind-merge": "^2.5.4" + "sonner": "^2.0.7" }, "devDependencies": { "@resvg/resvg-js": "^2.6.2", "@storybook/addon-docs": "^10.3.5", "@storybook/react-vite": "^10.3.5", - "@tailwindcss/vite": "^4.0.0", "@tauri-apps/cli": "^2.1.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.5.0", @@ -100,8 +95,6 @@ "madge": "^8.0.0", "sass": "^1.83.0", "storybook": "^10.3.5", - "tailwindcss": "^4.0.0", - "tw-animate-css": "^1.2.5", "typescript": "^5.6.3", "typescript-eslint": "^8.8.0", "vite": "^5.4.8", diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock index 79c2a68..2af4b96 100644 --- a/app/src-tauri/Cargo.lock +++ b/app/src-tauri/Cargo.lock @@ -4102,6 +4102,7 @@ dependencies = [ "uuid", "walkdir", "which", + "window-vibrancy 0.7.1", "windows 0.58.0", ] @@ -5337,7 +5338,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "window-vibrancy", + "window-vibrancy 0.6.0", "windows 0.61.3", ] @@ -6785,6 +6786,21 @@ dependencies = [ "windows-version", ] +[[package]] +name = "window-vibrancy" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010797bd7c40396fbc59d3105089fed0885fe267a0ef4a0a4646df54e28647f6" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.60.2", + "windows-version", +] + [[package]] name = "windows" version = "0.56.0" diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml index c036f7a..7cca9b1 100644 --- a/app/src-tauri/Cargo.toml +++ b/app/src-tauri/Cargo.toml @@ -26,6 +26,13 @@ tauri-plugin-deep-link = "2.4" tauri-plugin-process = "2.3" tauri-plugin-single-instance = "2.3" tauri-plugin-dialog = "2.3" +# Native vibrancy / blurred backdrop. On macOS we always apply HudWindow +# vibrancy on startup — when the active theme uses an opaque background it's +# simply invisible; the Glassy theme renders translucent surfaces so the +# vibrancy shows through. On Windows we apply Acrylic for the same reason. +# Linux has no compositor blur, so Glassy falls back to a semi-transparent +# surface without blur — documented as a platform limitation. +window-vibrancy = "0.7" sentry = "0.38" os_info = "3" serde = { version = "1.0", features = ["derive"] } diff --git a/app/src-tauri/src/config/settings.rs b/app/src-tauri/src/config/settings.rs index 515fc47..0f579bc 100644 --- a/app/src-tauri/src/config/settings.rs +++ b/app/src-tauri/src/config/settings.rs @@ -25,6 +25,71 @@ impl Default for NotificationSettings { } } +/// Renderer-scoped appearance + accessibility tokens that the React shell +/// owns end-to-end. Phase-2 moves these out of `localStorage` and onto the +/// Tauri backend so every Recrest surface (web preview included) reads them +/// from a single source of truth. +/// +/// All fields are `#[serde(default)]` so existing `settings.json` files +/// migrate cleanly: missing fields fall back to the renderer's defaults. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AppearanceSettings { + /// Renderer-side theme variant — extends the legacy `theme` (light/dark/system) + /// with "oled" and "glassy" without breaking the older field. When the + /// renderer dispatches `setThemeId`, this slot wins over `theme`. + pub theme_id: String, + /// True ⇔ the renderer should track `prefers-color-scheme`. Mirrors the + /// legacy `theme === "system"` semantic but kept explicit so the renderer + /// can persist "user picked Light" vs. "user is on Light because OS says so". + pub follows_system: bool, + /// Accent / brand color (named scheme, not hex). One of: default, blue, + /// green, purple, pink, orange. + pub primary_color: String, + /// Renderer font slot — "inter" | "opendyslexic" | future additions. + pub font: String, + /// Renderer font size token — "sm" | "md" | "lg". + pub font_size: String, +} + +impl Default for AppearanceSettings { + fn default() -> Self { + Self { + theme_id: "light".into(), + follows_system: true, + primary_color: "default".into(), + font: "inter".into(), + font_size: "md".into(), + } + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccessibilitySettings { + /// Legacy boolean kept in sync with `appearance.font === "opendyslexic"`. + /// Tests written against the pre-Phase-2 shape keep working. + #[serde(default)] + pub dyslexia_font: bool, + #[serde(default)] + pub high_contrast: bool, + #[serde(default)] + pub reduced_motion: bool, + #[serde(default)] + pub underline_links: bool, +} + +/// Tiny window-state slice persisted alongside settings (the sidebar lives +/// here because it survives across sessions exactly like an appearance +/// preference). Future window-state bits (panel splits, last-active route) +/// land in the same struct. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WindowStateSettings { + #[serde(default)] + pub sidebar_collapsed: bool, +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PrivacySettings { @@ -139,6 +204,14 @@ pub struct AppSettings { pub commit_message_template: String, #[serde(default)] pub privacy: PrivacySettings, + + // ---- Phase 2: renderer-scoped preferences moved off localStorage ---- + #[serde(default)] + pub appearance: AppearanceSettings, + #[serde(default)] + pub accessibility: AccessibilitySettings, + #[serde(default)] + pub window_state: WindowStateSettings, } fn default_auto_update() -> String { @@ -184,6 +257,9 @@ impl Default for AppSettings { terminal: TerminalSettings::default(), commit_message_template: default_commit_message_template(), privacy: PrivacySettings::default(), + appearance: AppearanceSettings::default(), + accessibility: AccessibilitySettings::default(), + window_state: WindowStateSettings::default(), } } } diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index 15e1f11..e708d9b 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -468,14 +468,27 @@ pub fn run() { #[cfg(target_os = "macos")] { use tauri::TitleBarStyle; + use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial}; if let Some(window) = handle.get_webview_window("main") { let _ = window.set_decorations(true); let _ = window.set_title_bar_style(TitleBarStyle::Overlay); + // Vibrancy is applied unconditionally — the Glassy theme makes + // the React surfaces translucent so it shows through; opaque + // themes simply cover it. + let _ = apply_vibrancy(&window, NSVisualEffectMaterial::HudWindow, None, None); } set_macos_app_icon(); observe_macos_appearance(); } + #[cfg(target_os = "windows")] + { + use window_vibrancy::apply_acrylic; + if let Some(window) = handle.get_webview_window("main") { + let _ = apply_acrylic(&window, None); + } + } + // Initial Windows icon swap runs AFTER the tray is created // further down so `apply_windows_theme_icon` finds both the // webview window and the tray. The tray block below calls it. diff --git a/app/src/App.tsx b/app/src/App.tsx index 3a994e1..6419a78 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,36 +1,39 @@ -import { Navigate, Route, Routes } from "react-router-dom"; +import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; import { AppRoute } from "@recrest/shared"; -import { AppShell } from "@/components/organisms/layout/AppShell"; -import { ActivityPage } from "@/pages/ActivityPage"; -import { BranchesPage } from "@/pages/BranchesPage"; -import { DashboardPage } from "@/pages/DashboardPage"; -import { MergeRequestsPage } from "@/pages/MergeRequestsPage"; -import { RepoDetailPage } from "@/pages/RepoDetailPage"; -import { ReposPage } from "@/pages/ReposPage"; -import { SettingsPage } from "@/pages/SettingsPage"; +import { AppLayout } from "@/layouts/AppLayout"; +import ActivityPage from "@/pages/app/Activity"; +import BranchesPage from "@/pages/app/Branches"; +import ChangesPage from "@/pages/app/Changes"; +import DashboardPage from "@/pages/app/Dashboard"; +import MergeRequestsPage from "@/pages/app/MergeRequests"; +import RepoDetailPage from "@/pages/app/RepoDetail"; +import ReposPage from "@/pages/app/Repos"; +import SettingsPage from "@/pages/app/Settings"; export default function App() { return ( - + - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {/* Legacy path — keep working until deep links settle. */} - } - /> - } /> - } /> - } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } + /> + } /> + } /> + } /> + + } /> - + ); } diff --git a/app/src/Welcome.stories.tsx b/app/src/Welcome.stories.tsx deleted file mode 100644 index d03fc7a..0000000 --- a/app/src/Welcome.stories.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -function Welcome() { - return ( -
-

Recrest Component Library

-

Browse atoms, molecules, and organisms via the sidebar.

-
- ); -} - -const meta: Meta = { - title: "Welcome", - component: Welcome, -}; - -export default meta; - -export const Default: StoryObj = {}; diff --git a/app/src/assets/recrest-icon-dev.svg b/app/src/assets/recrest-icon-dev.svg index 1183b44..44570ce 100644 --- a/app/src/assets/recrest-icon-dev.svg +++ b/app/src/assets/recrest-icon-dev.svg @@ -1,7 +1,7 @@ Recrest — Dev build - Recrest icon, development variant: white chevrons with an orange </> badge in the lower-right corner. - + Recrest icon, development variant: transparent background, orange chevrons, orange </> badge in the lower-right corner. + diff --git a/app/src/components/atoms/AheadBehind/AheadBehind.stories.tsx b/app/src/components/atoms/AheadBehind/AheadBehind.stories.tsx deleted file mode 100644 index a3eefb2..0000000 --- a/app/src/components/atoms/AheadBehind/AheadBehind.stories.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { AheadBehind } from "@/components/atoms/AheadBehind"; - -const meta: Meta = { - title: "Atoms/AheadBehind", - component: AheadBehind, -}; - -export default meta; - -export const Even: StoryObj = { args: { ahead: 0, behind: 0 } }; -export const Ahead: StoryObj = { args: { ahead: 3, behind: 0 } }; -export const Behind: StoryObj = { args: { ahead: 0, behind: 2 } }; -export const Diverged: StoryObj = { args: { ahead: 3, behind: 2 } }; diff --git a/app/src/components/atoms/AheadBehind/AheadBehind.test.tsx b/app/src/components/atoms/AheadBehind/AheadBehind.test.tsx deleted file mode 100644 index cbaa90f..0000000 --- a/app/src/components/atoms/AheadBehind/AheadBehind.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; - -import { AheadBehind } from "@/components/atoms/AheadBehind"; - -describe("AheadBehind", () => { - it("rendert ohne Crash bei leerem Stand", () => { - const { container } = render(); - expect(container.textContent).toContain("↕ 0"); - }); - - it("gibt im compact Mode bei gleichem Stand null zurück", () => { - const { container } = render(); - expect(container.firstChild).toBeNull(); - }); - - it("zeigt ahead-Zahl mit Aufwärtspfeil", () => { - render(); - expect(screen.getByText("↑3")).toBeInTheDocument(); - }); - - it("zeigt behind-Zahl mit Abwärtspfeil", () => { - render(); - expect(screen.getByText("↓5")).toBeInTheDocument(); - }); - - it("zeigt beide Zahlen wenn divergent", () => { - render(); - expect(screen.getByText("↑2")).toBeInTheDocument(); - expect(screen.getByText("↓4")).toBeInTheDocument(); - }); -}); diff --git a/app/src/components/atoms/AheadBehind/index.tsx b/app/src/components/atoms/AheadBehind/index.tsx deleted file mode 100644 index 45b9041..0000000 --- a/app/src/components/atoms/AheadBehind/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -interface AheadBehindProps { - ahead: number; - behind: number; - compact?: boolean; -} - -/** Branch ahead/behind counter chip. Returns null in compact mode when the - * branch is even with its upstream; otherwise shows a grey "↕ 0" pill. */ -export function AheadBehind({ ahead, behind, compact }: AheadBehindProps) { - if (!ahead && !behind) { - return compact ? null : ( - - ↕ 0 - - ); - } - return ( - - {ahead > 0 && ↑{ahead}} - {behind > 0 && ↓{behind}} - - ); -} diff --git a/app/src/components/atoms/Badge/Badge.stories.tsx b/app/src/components/atoms/Badge/Badge.stories.tsx deleted file mode 100644 index 1746baa..0000000 --- a/app/src/components/atoms/Badge/Badge.stories.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { Badge } from "@/components/atoms/Badge"; - -const meta: Meta = { - title: "Atoms/Badge", - component: Badge, -}; - -export default meta; - -export const Default: StoryObj = { args: { children: "Badge" } }; -export const Secondary: StoryObj = { - args: { children: "Secondary", variant: "secondary" }, -}; -export const Destructive: StoryObj = { - args: { children: "Destructive", variant: "destructive" }, -}; -export const Outline: StoryObj = { - args: { children: "Outline", variant: "outline" }, -}; diff --git a/app/src/components/atoms/Badge/Badge.test.tsx b/app/src/components/atoms/Badge/Badge.test.tsx deleted file mode 100644 index 6793184..0000000 --- a/app/src/components/atoms/Badge/Badge.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; - -import { Badge } from "@/components/atoms/Badge"; - -describe("Badge", () => { - it("rendert Kind-Inhalt", () => { - render(Neu); - expect(screen.getByText("Neu")).toBeInTheDocument(); - }); - - it("nutzt die default-Variante ohne Prop", () => { - render(default); - expect(screen.getByText("default").className).toContain("bg-primary"); - }); - - it("wendet die outline-Variante an", () => { - render(outline); - expect(screen.getByText("outline").className).toContain("border-border"); - }); - - it("wendet die success-Variante an", () => { - render(ok); - expect(screen.getByText("ok").className).toContain("text-status-success"); - }); - - it("merged zusätzliche className", () => { - render(x); - expect(screen.getByText("x").className).toContain("custom-class"); - }); -}); diff --git a/app/src/components/atoms/Badge/index.tsx b/app/src/components/atoms/Badge/index.tsx deleted file mode 100644 index 7ebbe9d..0000000 --- a/app/src/components/atoms/Badge/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { type ComponentProps } from "react"; - -import { type VariantProps, cva } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const badgeVariants = cva( - "inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background", - { - variants: { - variant: { - default: "border-transparent bg-primary text-primary-foreground", - secondary: "border-transparent bg-secondary text-secondary-foreground", - outline: "border-border text-foreground", - muted: "border-transparent bg-muted text-muted-foreground", - success: "border-transparent bg-status-success/15 text-status-success", - warning: "border-transparent bg-status-warning/15 text-status-warning", - destructive: "border-transparent bg-status-error/15 text-status-error", - info: "border-transparent bg-status-info/15 text-status-info", - }, - size: { - sm: "px-1.5 py-0 text-[10px]", - md: "px-2 py-0.5 text-xs", - }, - }, - defaultVariants: { - variant: "default", - size: "md", - }, - }, -); - -export interface BadgeProps extends ComponentProps<"span">, VariantProps {} - -export function Badge({ className, variant, size, ...props }: BadgeProps) { - return ; -} - -// eslint-disable-next-line react-refresh/only-export-components -export { badgeVariants }; diff --git a/app/src/components/atoms/BranchChip/BranchChip.stories.tsx b/app/src/components/atoms/BranchChip/BranchChip.stories.tsx deleted file mode 100644 index 7616e5c..0000000 --- a/app/src/components/atoms/BranchChip/BranchChip.stories.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { BranchChip } from "@/components/atoms/BranchChip"; - -const meta: Meta = { - title: "Atoms/BranchChip", - component: BranchChip, -}; - -export default meta; - -export const Main: StoryObj = { args: { branch: "main" } }; -export const Feature: StoryObj = { - args: { branch: "feature/new-thing" }, -}; -export const Small: StoryObj = { - args: { branch: "main", size: "sm" }, -}; -export const Big: StoryObj = { - args: { branch: "main", size: "big" }, -}; diff --git a/app/src/components/atoms/BranchChip/BranchChip.test.tsx b/app/src/components/atoms/BranchChip/BranchChip.test.tsx deleted file mode 100644 index f3df3d7..0000000 --- a/app/src/components/atoms/BranchChip/BranchChip.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; - -import { BranchChip } from "@/components/atoms/BranchChip"; - -describe("BranchChip", () => { - it("rendert Branch-Namen", () => { - render(); - expect(screen.getByText("feature/login")).toBeInTheDocument(); - }); - - it("hat keine Größenklasse bei md (default)", () => { - const { container } = render(); - const chip = container.querySelector(".a-branch-chip"); - expect(chip?.className).toBe("a-branch-chip"); - }); - - it("fügt sm-Klasse bei size='sm' hinzu", () => { - const { container } = render(); - expect(container.querySelector(".a-branch-chip.sm")).not.toBeNull(); - }); - - it("fügt big-Klasse bei size='big' hinzu", () => { - const { container } = render(); - expect(container.querySelector(".a-branch-chip.big")).not.toBeNull(); - }); - - it("rendert ein SVG-Icon neben dem Namen", () => { - const { container } = render(); - expect(container.querySelector("svg")).not.toBeNull(); - }); -}); diff --git a/app/src/components/atoms/BranchChip/index.tsx b/app/src/components/atoms/BranchChip/index.tsx deleted file mode 100644 index d8dd0ae..0000000 --- a/app/src/components/atoms/BranchChip/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Icon } from "@/components/atoms/Icon"; - -interface BranchChipProps { - branch: string; - size?: "sm" | "md" | "big"; -} - -/** Rounded pill showing a branch name with the branch icon on the left. - * Three visual sizes mirror the usage across dashboard (big), row lists - * (md) and compact inline chips (sm). */ -export function BranchChip({ branch, size = "md" }: BranchChipProps) { - const cls = `a-branch-chip${size === "sm" ? " sm" : size === "big" ? " big" : ""}`; - const iconSize = size === "sm" ? 10 : size === "big" ? 12 : 11; - return ( - - - {branch} - - ); -} diff --git a/app/src/components/atoms/BrandIcon/BrandIcon.stories.tsx b/app/src/components/atoms/BrandIcon/BrandIcon.stories.tsx deleted file mode 100644 index 6006e90..0000000 --- a/app/src/components/atoms/BrandIcon/BrandIcon.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { BrandIcon } from "@/components/atoms/BrandIcon"; - -const meta: Meta = { - title: "Atoms/BrandIcon", - component: BrandIcon, - args: { size: 32, color: "brand" }, -}; - -export default meta; - -export const GitHub: StoryObj = { args: { slug: "github" } }; -export const GitLab: StoryObj = { args: { slug: "gitlab" } }; -export const Bitbucket: StoryObj = { args: { slug: "bitbucket" } }; -export const Monochrome: StoryObj = { - args: { slug: "github", color: "currentColor" }, -}; diff --git a/app/src/components/atoms/BrandIcon/BrandIcon.test.tsx b/app/src/components/atoms/BrandIcon/BrandIcon.test.tsx deleted file mode 100644 index 02b8850..0000000 --- a/app/src/components/atoms/BrandIcon/BrandIcon.test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; - -import { BrandIcon } from "@/components/atoms/BrandIcon"; - -describe("BrandIcon", () => { - it("rendert ohne Crash mit slug=github", () => { - render(); - expect(screen.getByRole("img")).toBeInTheDocument(); - }); - - it("nimmt die Default-Größe 16", () => { - const { container } = render(); - const svg = container.querySelector("svg"); - expect(svg?.getAttribute("width")).toBe("16"); - expect(svg?.getAttribute("height")).toBe("16"); - }); - - it("wendet custom size an", () => { - const { container } = render(); - expect(container.querySelector("svg")?.getAttribute("width")).toBe("32"); - }); - - it("nutzt currentColor per Default", () => { - const { container } = render(); - expect(container.querySelector("svg")?.getAttribute("fill")).toBe("currentColor"); - }); - - it("setzt Brand-Farbe bei color='brand'", () => { - const { container } = render(); - const fill = container.querySelector("svg")?.getAttribute("fill"); - expect(fill).toMatch(/^#[0-9a-fA-F]{3,8}$/); - }); - - it("nutzt title-Prop als aria-label", () => { - render(); - expect(screen.getByLabelText("My GitHub")).toBeInTheDocument(); - }); -}); diff --git a/app/src/components/atoms/BrandIcon/index.tsx b/app/src/components/atoms/BrandIcon/index.tsx deleted file mode 100644 index ecf149a..0000000 --- a/app/src/components/atoms/BrandIcon/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { type SVGProps } from "react"; - -import { type SimpleIcon, siBitbucket, siGithub, siGitlab } from "simple-icons"; - -import { cn } from "@/lib/utils"; - -export type BrandSlug = "github" | "gitlab" | "bitbucket"; - -const BRAND_ICONS: Record = { - github: siGithub, - gitlab: siGitlab, - bitbucket: siBitbucket, -}; - -interface BrandIconProps extends Omit, "name"> { - slug: BrandSlug; - size?: number; - /** `"currentColor"` (default) picks up surrounding text colour; - * `"brand"` uses Simple Icons' official hex. */ - color?: "currentColor" | "brand" | string; - title?: string; -} - -export function BrandIcon({ - slug, - size = 16, - color = "currentColor", - title, - className, - ...rest -}: BrandIconProps) { - const icon = BRAND_ICONS[slug]; - const fill = color === "brand" ? `#${icon.hex}` : color; - return ( - - - - ); -} - -/** Best-effort mapping from a remote URL's host to the matching Simple Icon. - * Self-hosted instances fall back to `null` — caller decides whether to - * render a generic glyph or nothing. */ -// eslint-disable-next-line react-refresh/only-export-components -export function brandFromUrl(url: string | null | undefined): BrandSlug | null { - if (!url) return null; - const rest = url.startsWith("git@") - ? url.slice(4).split(":")[0] - : (url.split("://")[1] ?? url).split("@").pop()?.split(/[/:]/)[0]; - const host = rest?.toLowerCase() ?? ""; - if (host.endsWith("github.com")) return "github"; - if (host.endsWith("gitlab.com")) return "gitlab"; - if (host.endsWith("bitbucket.org")) return "bitbucket"; - return null; -} diff --git a/app/src/components/atoms/Button/Button.stories.tsx b/app/src/components/atoms/Button/Button.stories.tsx deleted file mode 100644 index 00ddb03..0000000 --- a/app/src/components/atoms/Button/Button.stories.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { Button } from "@/components/atoms/Button"; - -const meta: Meta = { - title: "Atoms/Button", - component: Button, -}; - -export default meta; - -export const Default: StoryObj = { args: { children: "Click me" } }; -export const Outline: StoryObj = { - args: { children: "Outline", variant: "outline" }, -}; -export const Secondary: StoryObj = { - args: { children: "Secondary", variant: "secondary" }, -}; -export const Ghost: StoryObj = { - args: { children: "Ghost", variant: "ghost" }, -}; -export const Destructive: StoryObj = { - args: { children: "Destructive", variant: "destructive" }, -}; -export const Link: StoryObj = { - args: { children: "Link", variant: "link" }, -}; diff --git a/app/src/components/atoms/Button/Button.test.tsx b/app/src/components/atoms/Button/Button.test.tsx deleted file mode 100644 index 297fd9f..0000000 --- a/app/src/components/atoms/Button/Button.test.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { describe, expect, it, vi } from "vitest"; - -import { Button } from "@/components/atoms/Button"; - -describe("Button", () => { - it("rendert Label", () => { - render(); - expect(screen.getByRole("button", { name: "Klick mich" })).toBeInTheDocument(); - }); - - it("wendet die outline-Variante an", () => { - render(); - expect(screen.getByRole("button").className).toContain("border-input"); - }); - - it("ruft onClick bei Klick auf", async () => { - const user = userEvent.setup(); - const handler = vi.fn(); - render(); - await user.click(screen.getByRole("button")); - expect(handler).toHaveBeenCalledTimes(1); - }); - - it("ist disabled während loading", () => { - render(); - expect(screen.getByRole("button")).toBeDisabled(); - }); - - it("ruft onClick nicht auf wenn disabled", async () => { - const user = userEvent.setup(); - const handler = vi.fn(); - render( - , - ); - await user.click(screen.getByRole("button")); - expect(handler).not.toHaveBeenCalled(); - }); -}); diff --git a/app/src/components/atoms/Button/index.tsx b/app/src/components/atoms/Button/index.tsx deleted file mode 100644 index b741cd4..0000000 --- a/app/src/components/atoms/Button/index.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { type ComponentProps } from "react"; - -import { Slot } from "@radix-ui/react-slot"; -import { type VariantProps, cva } from "class-variance-authority"; -import { Loader2 } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -const buttonVariants = cva( - "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90", - destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", - outline: - "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", - secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-6", - icon: "h-9 w-9", - "icon-sm": "h-8 w-8", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, -); - -export interface ButtonProps extends ComponentProps<"button">, VariantProps { - asChild?: boolean; - loading?: boolean; -} - -export function Button({ - className, - variant, - size, - asChild = false, - loading = false, - disabled, - children, - ...props -}: ButtonProps) { - const Comp = asChild ? Slot : "button"; - return ( - - {loading ? : null} - {children} - - ); -} - -// eslint-disable-next-line react-refresh/only-export-components -export { buttonVariants }; diff --git a/app/src/components/atoms/Checkbox/Checkbox.stories.tsx b/app/src/components/atoms/Checkbox/Checkbox.stories.tsx deleted file mode 100644 index 69ffeff..0000000 --- a/app/src/components/atoms/Checkbox/Checkbox.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { Checkbox } from "@/components/atoms/Checkbox"; - -const meta: Meta = { - title: "Atoms/Checkbox", - component: Checkbox, -}; - -export default meta; - -export const Unchecked: StoryObj = { args: { checked: false } }; -export const Checked: StoryObj = { args: { checked: true } }; -export const Indeterminate: StoryObj = { args: { checked: "indeterminate" } }; -export const Disabled: StoryObj = { args: { disabled: true } }; -export const DisabledChecked: StoryObj = { - args: { disabled: true, checked: true }, -}; diff --git a/app/src/components/atoms/Checkbox/Checkbox.test.tsx b/app/src/components/atoms/Checkbox/Checkbox.test.tsx deleted file mode 100644 index 339be86..0000000 --- a/app/src/components/atoms/Checkbox/Checkbox.test.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { describe, expect, it, vi } from "vitest"; - -import { Checkbox } from "@/components/atoms/Checkbox"; - -describe("Checkbox", () => { - it("rendert mit role=checkbox", () => { - render(); - expect(screen.getByRole("checkbox")).toBeInTheDocument(); - }); - - it("ist ungecheckt per Default", () => { - render(); - expect(screen.getByRole("checkbox")).toHaveAttribute("data-state", "unchecked"); - }); - - it("respektiert defaultChecked", () => { - render(); - expect(screen.getByRole("checkbox")).toHaveAttribute("data-state", "checked"); - }); - - it("ruft onCheckedChange bei Klick auf", async () => { - const user = userEvent.setup(); - const handler = vi.fn(); - render(); - await user.click(screen.getByRole("checkbox")); - expect(handler).toHaveBeenCalledWith(true); - }); - - it("ist disabled wenn disabled-Prop gesetzt ist", () => { - render(); - expect(screen.getByRole("checkbox")).toBeDisabled(); - }); -}); diff --git a/app/src/components/atoms/Checkbox/index.tsx b/app/src/components/atoms/Checkbox/index.tsx deleted file mode 100644 index 33ee72a..0000000 --- a/app/src/components/atoms/Checkbox/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { type ComponentProps } from "react"; - -import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; -import { Check } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -export function Checkbox({ className, ...props }: ComponentProps) { - return ( - - - - - - ); -} diff --git a/app/src/components/atoms/CiDot/CiDot.stories.tsx b/app/src/components/atoms/CiDot/CiDot.stories.tsx deleted file mode 100644 index 1a6b276..0000000 --- a/app/src/components/atoms/CiDot/CiDot.stories.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { CiDot } from "@/components/atoms/CiDot"; - -const meta: Meta = { - title: "Atoms/CiDot", - component: CiDot, -}; - -export default meta; - -export const Passing: StoryObj = { args: { state: "passing" } }; -export const Failing: StoryObj = { args: { state: "failing" } }; -export const Running: StoryObj = { args: { state: "running" } }; -export const None: StoryObj = { args: { state: null } }; diff --git a/app/src/components/atoms/CiDot/CiDot.test.tsx b/app/src/components/atoms/CiDot/CiDot.test.tsx deleted file mode 100644 index deaca88..0000000 --- a/app/src/components/atoms/CiDot/CiDot.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; - -import { CiDot } from "@/components/atoms/CiDot"; - -describe("CiDot", () => { - it("zeigt em-dash bei null-State", () => { - render(); - expect(screen.getByText("—")).toBeInTheDocument(); - }); - - it("zeigt em-dash bei undefined-State", () => { - render(); - expect(screen.getByText("—")).toBeInTheDocument(); - }); - - it("zeigt 'passing' Label bei passing-State", () => { - render(); - expect(screen.getByText("passing")).toBeInTheDocument(); - }); - - it("zeigt 'failing' Label bei failing-State", () => { - render(); - expect(screen.getByText("failing")).toBeInTheDocument(); - }); - - it("zeigt 'running' Label bei running-State", () => { - render(); - expect(screen.getByText("running")).toBeInTheDocument(); - }); -}); diff --git a/app/src/components/atoms/CiDot/index.tsx b/app/src/components/atoms/CiDot/index.tsx deleted file mode 100644 index 9cee48f..0000000 --- a/app/src/components/atoms/CiDot/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -export type CiState = "passing" | "failing" | "running" | null | undefined; - -interface CiDotProps { - state: CiState; -} - -const COLORS = { - passing: { dot: "var(--green)", label: "passing" }, - failing: { dot: "var(--red)", label: "failing" }, - running: { dot: "var(--amber)", label: "running" }, -} as const; - -/** Small colored CI-status pill. Grey em-dash when there's no status. - * Running state gets a soft pulse + halo. */ -export function CiDot({ state }: CiDotProps) { - if (!state) return ; - const m = COLORS[state]; - return ( - - - {m.label} - - ); -} diff --git a/app/src/components/atoms/ConfirmDialog/index.tsx b/app/src/components/atoms/ConfirmDialog/index.tsx deleted file mode 100644 index 1b2c316..0000000 --- a/app/src/components/atoms/ConfirmDialog/index.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { type ReactNode, useCallback, useRef, useState } from "react"; - -import { - ConfirmContext, - type ConfirmFn, - type ConfirmOptions, -} from "@/components/atoms/ConfirmDialog/useConfirm"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/molecules/compounds/Dialog"; - -/** - * Plan 1 §D.3: shared confirmation-dialog primitive. - * - * Wraps the existing Dialog atoms with a promise-based API. Mount - * `` near the app root, then call `useConfirm()` from any - * component to open a modal and `await` the user's choice. The hook and - * context live in `./useConfirm.ts` so this file stays component-only and - * Fast Refresh keeps working. - * - * The provider keeps a single dialog instance, so concurrent calls are - * serialised — a second `confirm()` resolves the first one as cancelled - * (`false`) and opens the new dialog. The promise never rejects; callers - * only need to check the boolean. - */ - -interface PendingState { - opts: ConfirmOptions; - resolve: (ok: boolean) => void; -} - -export function ConfirmProvider({ children }: { children: ReactNode }) { - const [pending, setPending] = useState(null); - const pendingRef = useRef(null); - - const confirm = useCallback((opts) => { - return new Promise((resolve) => { - // If a previous confirm is still open, resolve it as cancelled. Two - // dialogs at once would visually stack with no way for the user to - // address the older one. - if (pendingRef.current) { - pendingRef.current.resolve(false); - } - const next: PendingState = { opts, resolve }; - pendingRef.current = next; - setPending(next); - }); - }, []); - - const close = useCallback((ok: boolean) => { - const current = pendingRef.current; - pendingRef.current = null; - setPending(null); - current?.resolve(ok); - }, []); - - return ( - - {children} - { - // Pressing ESC / clicking outside resolves as cancelled. - if (!open) close(false); - }} - > - - - {pending?.opts.title} - {pending?.opts.description && ( - {pending.opts.description} - )} - - - - - - - - - ); -} diff --git a/app/src/components/atoms/ConfirmDialog/useConfirm.ts b/app/src/components/atoms/ConfirmDialog/useConfirm.ts deleted file mode 100644 index da1a3c1..0000000 --- a/app/src/components/atoms/ConfirmDialog/useConfirm.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { type ReactNode, createContext, useContext } from "react"; - -/** - * Plan 1 §D.3: shared confirmation-dialog primitive — public API. - * - * The hook + context live here (instead of next to the provider component) - * so `index.tsx` can stay component-only, which keeps Fast Refresh - * predictable. `` remains the entry point; consumers call - * `useConfirm()` to open the modal and `await` the user's choice: - * - * const confirm = useConfirm(); - * if (!(await confirm({ title: "Delete repo?" }))) return; - */ - -export interface ConfirmOptions { - title: ReactNode; - description?: ReactNode; - /** Label for the confirm button. Defaults to "Confirm". */ - confirmLabel?: string; - /** Label for the cancel button. Defaults to "Cancel". */ - cancelLabel?: string; - /** When true, styles the confirm button with the destructive accent. - * Defaults to `false`. */ - destructive?: boolean; -} - -export type ConfirmFn = (opts: ConfirmOptions) => Promise; - -export const ConfirmContext = createContext(null); - -export function useConfirm(): ConfirmFn { - const fn = useContext(ConfirmContext); - if (!fn) { - throw new Error("useConfirm must be used inside "); - } - return fn; -} diff --git a/app/src/components/atoms/DiffStat/DiffStat.stories.tsx b/app/src/components/atoms/DiffStat/DiffStat.stories.tsx deleted file mode 100644 index ae51930..0000000 --- a/app/src/components/atoms/DiffStat/DiffStat.stories.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { DiffStat } from "@/components/atoms/DiffStat"; - -const meta: Meta = { - title: "Atoms/DiffStat", - component: DiffStat, -}; - -export default meta; - -export const Mixed: StoryObj = { args: { added: 42, removed: 17 } }; -export const OnlyAdded: StoryObj = { args: { added: 12, removed: 0 } }; -export const OnlyRemoved: StoryObj = { args: { added: 0, removed: 53 } }; -export const Large: StoryObj = { args: { added: 1248, removed: 932 } }; -/** Returns null, so Storybook shows an empty canvas. */ -export const None: StoryObj = { args: { added: 0, removed: 0 } }; diff --git a/app/src/components/atoms/DiffStat/DiffStat.test.tsx b/app/src/components/atoms/DiffStat/DiffStat.test.tsx deleted file mode 100644 index bed3f33..0000000 --- a/app/src/components/atoms/DiffStat/DiffStat.test.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; - -import { DiffStat } from "@/components/atoms/DiffStat"; - -describe("DiffStat", () => { - it("gibt null zurück ohne Änderungen", () => { - const { container } = render(); - expect(container.firstChild).toBeNull(); - }); - - it("zeigt nur added wenn removed=0", () => { - render(); - expect(screen.getByText("+12")).toBeInTheDocument(); - expect(screen.queryByText(/^−/)).toBeNull(); - }); - - it("zeigt nur removed wenn added=0", () => { - render(); - expect(screen.getByText("−5")).toBeInTheDocument(); - expect(screen.queryByText(/^\+/)).toBeNull(); - }); - - it("zeigt beides wenn added und removed > 0", () => { - render(); - expect(screen.getByText("+12")).toBeInTheDocument(); - expect(screen.getByText("−53")).toBeInTheDocument(); - }); -}); diff --git a/app/src/components/atoms/DiffStat/index.tsx b/app/src/components/atoms/DiffStat/index.tsx deleted file mode 100644 index fe959af..0000000 --- a/app/src/components/atoms/DiffStat/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -interface DiffStatProps { - added: number; - removed: number; -} - -/** Mini "+12 −53" line stat. Returns null when there are no changes. */ -export function DiffStat({ added, removed }: DiffStatProps) { - if (!added && !removed) return null; - return ( - - {added > 0 && +{added}} - {removed > 0 && −{removed}} - - ); -} diff --git a/app/src/components/atoms/Icon/Icon.stories.tsx b/app/src/components/atoms/Icon/Icon.stories.tsx deleted file mode 100644 index a536029..0000000 --- a/app/src/components/atoms/Icon/Icon.stories.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { Icon } from "@/components/atoms/Icon"; - -const meta: Meta = { - title: "Atoms/Icon", - component: Icon, - args: { size: 24 }, -}; - -export default meta; - -export const Search: StoryObj = { args: { name: "search" } }; -export const Branch: StoryObj = { args: { name: "branch" } }; -export const Settings: StoryObj = { args: { name: "settings" } }; -export const Scale: StoryObj = { args: { name: "scale" } }; diff --git a/app/src/components/atoms/Icon/Icon.test.tsx b/app/src/components/atoms/Icon/Icon.test.tsx deleted file mode 100644 index 15f778c..0000000 --- a/app/src/components/atoms/Icon/Icon.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { render } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; - -import { Icon } from "@/components/atoms/Icon"; - -describe("Icon", () => { - it("rendert ein SVG", () => { - const { container } = render(); - expect(container.querySelector("svg")).not.toBeNull(); - }); - - it("hat Default-Größe 16", () => { - const { container } = render(); - const svg = container.querySelector("svg"); - expect(svg?.getAttribute("width")).toBe("16"); - expect(svg?.getAttribute("height")).toBe("16"); - }); - - it("wendet custom size an", () => { - const { container } = render(); - expect(container.querySelector("svg")?.getAttribute("width")).toBe("24"); - }); - - it("nutzt currentColor per Default als stroke", () => { - const { container } = render(); - expect(container.querySelector("svg")?.getAttribute("stroke")).toBe("currentColor"); - }); - - it("wendet custom color als stroke an", () => { - const { container } = render(); - expect(container.querySelector("svg")?.getAttribute("stroke")).toBe("#ff0000"); - }); -}); diff --git a/app/src/components/atoms/Icon/index.tsx b/app/src/components/atoms/Icon/index.tsx deleted file mode 100644 index e493309..0000000 --- a/app/src/components/atoms/Icon/index.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import type { ReactElement, SVGProps } from "react"; - -import { cn } from "@/lib/utils"; - -export type IconName = - | "search" - | "plus" - | "refresh" - | "branch" - | "git" - | "folder" - | "terminal" - | "code" - | "external" - | "copy" - | "more" - | "chev" - | "chevDown" - | "chevLeft" - | "check" - | "x" - | "arrowUp" - | "arrowDown" - | "play" - | "pull" - | "dot" - | "filter" - | "sort" - | "pin" - | "box" - | "pr" - | "ci" - | "sliders" - | "activity" - | "settings" - | "collapse" - | "expand" - | "maximize" - | "camera" - | "inbox" - | "star" - | "trash" - | "home" - | "user" - | "key" - | "license" - | "scale" - | "repo" - | "edit" - | "wrench"; - -interface IconProps extends Omit, "name"> { - name: IconName; - size?: number; - color?: string; -} - -const PATHS: Record = { - search: ( - <> - - - - ), - plus: , - refresh: ( - <> - - - - - - ), - branch: ( - <> - - - - - - ), - git: ( - <> - - - - ), - folder: , - terminal: ( - <> - - - - - ), - code: ( - <> - - - - ), - external: ( - <> - - - - - ), - copy: ( - <> - - - - ), - more: ( - <> - - - - - ), - chev: , - chevDown: , - chevLeft: , - check: , - x: , - arrowUp: , - arrowDown: , - play: , - pull: , - dot: , - filter: , - sort: , - pin: ( - <> - - - - ), - box: ( - <> - - - - - ), - pr: ( - <> - - - - - - ), - ci: ( - <> - - - - ), - sliders: ( - <> - - - - - - - - - - - ), - activity: , - settings: ( - <> - - - - ), - collapse: ( - <> - - - - ), - expand: ( - <> - - - - ), - /** Classic "enter fullscreen" glyph: four corner brackets pointing - * outward. Used by the DetailPane's Open-full-view CTA. */ - maximize: ( - <> - - - - - - ), - camera: ( - <> - - - - ), - inbox: ( - <> - - - - ), - star: ( - - ), - trash: ( - <> - - - - ), - home: ( - - ), - user: ( - <> - - - - ), - key: ( - <> - - - - - - ), - /** Certificate / license: parchment with an official seal + ribbon. */ - license: ( - <> - - - - - - ), - /** GitHub-style repo glyph: hardcover book with a ribbon bookmark. */ - repo: ( - <> - - - - ), - /** Pencil on a page — used for "Changes" / uncommitted work. */ - edit: ( - <> - - - - ), - /** Wrench — developer / tooling affordance. Matches lucide's `wrench`. */ - wrench: ( - - ), - /** Scales of justice — matches Octicon `law`, GitHub's license glyph. */ - scale: ( - <> - - - - - - - ), -}; - -export function Icon({ name, size = 16, color = "currentColor", className, ...rest }: IconProps) { - return ( - - {PATHS[name]} - - ); -} diff --git a/app/src/components/atoms/IdeIcon/IdeIcon.stories.tsx b/app/src/components/atoms/IdeIcon/IdeIcon.stories.tsx deleted file mode 100644 index 5ccc1ba..0000000 --- a/app/src/components/atoms/IdeIcon/IdeIcon.stories.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { IdeIcon } from "@/components/atoms/IdeIcon"; - -const meta: Meta = { - title: "Atoms/IdeIcon", - component: IdeIcon, - args: { size: 32, color: "brand" }, -}; - -export default meta; - -export const VsCode: StoryObj = { args: { id: "vscode" } }; -export const VsCodeInsiders: StoryObj = { - args: { id: "vscode-insiders" }, -}; -export const Cursor: StoryObj = { args: { id: "cursor" } }; -export const WebStorm: StoryObj = { args: { id: "webstorm" } }; -export const IntelliJIDEA: StoryObj = { args: { id: "idea" } }; -export const JetBrainsToolbox: StoryObj = { - args: { id: "jetbrains-toolbox" }, -}; -export const Disabled: StoryObj = { - args: { id: "webstorm", color: "currentColor" }, -}; diff --git a/app/src/components/atoms/IdeIcon/IdeIcon.test.tsx b/app/src/components/atoms/IdeIcon/IdeIcon.test.tsx deleted file mode 100644 index c54ba1e..0000000 --- a/app/src/components/atoms/IdeIcon/IdeIcon.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; - -import { IdeIcon } from "@/components/atoms/IdeIcon"; - -/** - * IDE logos are inlined as SVGs via `vite-plugin-svgr` (`.svg?react`), so - * they render deterministically in jsdom. Tests cover the Cursor path - * (inline SVG from `simple-icons`) plus a smoke check that every official - * IDE id mounts without crashing. - */ -describe("IdeIcon", () => { - it("renders the Cursor logo inline with the expected aria-label", () => { - render(); - expect(screen.getByLabelText("Cursor")).toBeInTheDocument(); - }); - - it("applies the custom size to the Cursor SVG", () => { - const { container } = render(); - const svg = container.querySelector("svg"); - expect(svg?.getAttribute("width")).toBe("40"); - expect(svg?.getAttribute("height")).toBe("40"); - }); - - it("mounts without crashing for every official IDE id", () => { - const ids = ["vscode", "vscode-insiders", "webstorm", "idea", "jetbrains-toolbox"] as const; - for (const id of ids) { - const { container, unmount } = render(); - expect(container.firstChild).not.toBeNull(); - unmount(); - } - }); -}); diff --git a/app/src/components/atoms/IdeIcon/index.tsx b/app/src/components/atoms/IdeIcon/index.tsx deleted file mode 100644 index 632d48c..0000000 --- a/app/src/components/atoms/IdeIcon/index.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { type CSSProperties, type FC, type SVGProps } from "react"; - -import { siCursor } from "simple-icons"; - -import type { IdeId } from "@recrest/shared"; - -import IntellijIdeaLogo from "@/components/atoms/IdeIcon/logos/intellij-idea.svg?react"; -import JetbrainsLogo from "@/components/atoms/IdeIcon/logos/jetbrains.svg?react"; -import VSCodeLogo from "@/components/atoms/IdeIcon/logos/visual-studio-code.svg?react"; -import WebstormLogo from "@/components/atoms/IdeIcon/logos/webstorm.svg?react"; -import { cn } from "@/lib/utils"; - -/** - * Official IDE logos inlined from the Iconify `logos` set (committed as - * static SVGs in `./logos/`). Imported via `vite-plugin-svgr`'s `?react` - * suffix so they become real React components at build time — no runtime - * fetch to any CDN, which is what Tauri's strict CSP requires. - * Cursor stays inline from `simple-icons` (the `logos` set doesn't ship it). - * VS Code Insiders reuses the VS Code mark with a hue-rotate filter. - */ -const LOGO_COMPONENT: Partial>>> = { - vscode: VSCodeLogo, - "vscode-insiders": VSCodeLogo, - webstorm: WebstormLogo, - idea: IntellijIdeaLogo, - "jetbrains-toolbox": JetbrainsLogo, -}; - -interface IdeIconProps { - id: IdeId; - size?: number; - /** `"brand"` (default) = official colours; `"currentColor"` = greyed out - * (used for disabled items in the dropdown). */ - color?: "brand" | "currentColor"; - title?: string; - style?: CSSProperties; - className?: string; -} - -export function IdeIcon({ id, size = 16, color = "brand", title, style, className }: IdeIconProps) { - const mono = color === "currentColor"; - - if (id === "cursor") { - return ( - - ); - } - - const LogoComponent = LOGO_COMPONENT[id]; - if (!LogoComponent) return null; - - const filterParts: string[] = []; - if (mono) filterParts.push("grayscale(1)"); - if (id === "vscode-insiders") filterParts.push("hue-rotate(140deg)", "saturate(0.9)"); - - const iconStyle: CSSProperties = { - flexShrink: 0, - ...(filterParts.length > 0 ? { filter: filterParts.join(" ") } : null), - ...(mono ? { opacity: 0.55 } : null), - ...style, - }; - - return ( - - ); -} - -/** Cursor logo from simple-icons — Iconify's logos set doesn't cover the IDE yet. */ -function CursorGlyph({ - size, - mono, - title, - style, - className, -}: { - size: number; - mono: boolean; - title?: string; - style?: CSSProperties; - className?: string; -}) { - const fill = mono ? "currentColor" : `#${siCursor.hex}`; - return ( - - - - ); -} diff --git a/app/src/components/atoms/Input/Input.stories.tsx b/app/src/components/atoms/Input/Input.stories.tsx deleted file mode 100644 index 06643ff..0000000 --- a/app/src/components/atoms/Input/Input.stories.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { Input } from "@/components/atoms/Input"; - -const meta: Meta = { - title: "Atoms/Input", - component: Input, -}; - -export default meta; - -export const Default: StoryObj = { - args: { placeholder: "Type something…" }, -}; -export const Password: StoryObj = { - args: { type: "password", placeholder: "Secret" }, -}; -export const Disabled: StoryObj = { - args: { disabled: true, value: "Disabled" }, -}; diff --git a/app/src/components/atoms/Input/Input.test.tsx b/app/src/components/atoms/Input/Input.test.tsx deleted file mode 100644 index 0588435..0000000 --- a/app/src/components/atoms/Input/Input.test.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { describe, expect, it, vi } from "vitest"; - -import { Input } from "@/components/atoms/Input"; - -describe("Input", () => { - it("rendert input mit placeholder", () => { - render(); - expect(screen.getByPlaceholderText("Suchen…")).toBeInTheDocument(); - }); - - it("ruft onChange bei Eingabe auf", async () => { - const user = userEvent.setup(); - const handler = vi.fn(); - render(); - await user.type(screen.getByRole("textbox"), "abc"); - expect(handler).toHaveBeenCalled(); - }); - - it("setzt aria-invalid wenn invalid=true", () => { - render(); - expect(screen.getByRole("textbox")).toHaveAttribute("aria-invalid", "true"); - }); - - it("setzt kein aria-invalid wenn invalid=false", () => { - render(); - expect(screen.getByRole("textbox")).not.toHaveAttribute("aria-invalid"); - }); - - it("ist disabled wenn disabled-Prop gesetzt", () => { - render(); - expect(screen.getByRole("textbox")).toBeDisabled(); - }); - - it("leitet ref auf das input-Element weiter", () => { - const ref = { current: null as HTMLInputElement | null }; - render(); - expect(ref.current).toBeInstanceOf(HTMLInputElement); - }); -}); diff --git a/app/src/components/atoms/Input/index.tsx b/app/src/components/atoms/Input/index.tsx deleted file mode 100644 index b227ef5..0000000 --- a/app/src/components/atoms/Input/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { type ComponentProps, forwardRef } from "react"; - -import { cn } from "@/lib/utils"; - -export interface InputProps extends ComponentProps<"input"> { - invalid?: boolean; -} - -export const Input = forwardRef( - ({ className, type, invalid, ...props }, ref) => { - return ( - - ); - }, -); - -Input.displayName = "Input"; diff --git a/app/src/components/atoms/Kbd/Kbd.stories.tsx b/app/src/components/atoms/Kbd/Kbd.stories.tsx deleted file mode 100644 index 507f7f1..0000000 --- a/app/src/components/atoms/Kbd/Kbd.stories.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { Kbd } from "@/components/atoms/Kbd"; - -const meta: Meta = { - title: "Atoms/Kbd", - component: Kbd, -}; - -export default meta; - -export const Single: StoryObj = { args: { children: "K" } }; -export const ModifierKey: StoryObj = { args: { children: "\u2318K" } }; -export const Combo: StoryObj = { args: { children: "Ctrl+Shift+P" } }; -export const Escape: StoryObj = { args: { children: "Esc" } }; diff --git a/app/src/components/atoms/Kbd/Kbd.test.tsx b/app/src/components/atoms/Kbd/Kbd.test.tsx deleted file mode 100644 index f479a77..0000000 --- a/app/src/components/atoms/Kbd/Kbd.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; - -import { Kbd } from "@/components/atoms/Kbd"; - -describe("Kbd", () => { - it("rendert children", () => { - render(Esc); - expect(screen.getByText("Esc")).toBeInTheDocument(); - }); - - it("setzt die kbd-Klasse", () => { - render(Ctrl); - expect(screen.getByText("Ctrl")).toHaveClass("kbd"); - }); - - it("rendert komplexere ReactNode-Kinder", () => { - render( - - K - , - ); - expect(screen.getByTestId("inner")).toBeInTheDocument(); - }); -}); diff --git a/app/src/components/atoms/Kbd/index.tsx b/app/src/components/atoms/Kbd/index.tsx deleted file mode 100644 index 32fa396..0000000 --- a/app/src/components/atoms/Kbd/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import type { ReactNode } from "react"; - -interface KbdProps { - children: ReactNode; -} - -/** Inline keyboard-key rendering (``-like visual). */ -export function Kbd({ children }: KbdProps) { - return {children}; -} diff --git a/app/src/components/atoms/Label/Label.stories.tsx b/app/src/components/atoms/Label/Label.stories.tsx deleted file mode 100644 index d5529a3..0000000 --- a/app/src/components/atoms/Label/Label.stories.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { Label } from "@/components/atoms/Label"; - -const meta: Meta = { - title: "Atoms/Label", - component: Label, -}; - -export default meta; - -export const Default: StoryObj = { args: { children: "Field label" } }; -export const WithHtmlFor: StoryObj = { - args: { children: "Personal access token", htmlFor: "pat-input" }, -}; -export const LongText: StoryObj = { - args: { - children: "Automatically fetch pull requests every few minutes while the app is open", - }, -}; diff --git a/app/src/components/atoms/Label/Label.test.tsx b/app/src/components/atoms/Label/Label.test.tsx deleted file mode 100644 index 21d9377..0000000 --- a/app/src/components/atoms/Label/Label.test.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; - -import { Label } from "@/components/atoms/Label"; - -describe("Label", () => { - it("rendert Text", () => { - render(); - expect(screen.getByText("Benutzername")).toBeInTheDocument(); - }); - - it("wendet Basisklassen an", () => { - render(); - expect(screen.getByText("Email").className).toContain("text-sm"); - expect(screen.getByText("Email").className).toContain("font-medium"); - }); - - it("merged zusätzliche className", () => { - render(); - expect(screen.getByText("X")).toHaveClass("custom-label"); - }); - - it("unterstützt htmlFor-Prop", () => { - render(); - expect(screen.getByText("Email")).toHaveAttribute("for", "email-input"); - }); -}); diff --git a/app/src/components/atoms/Label/index.tsx b/app/src/components/atoms/Label/index.tsx deleted file mode 100644 index 3c7bf55..0000000 --- a/app/src/components/atoms/Label/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { type ComponentProps } from "react"; - -import * as LabelPrimitive from "@radix-ui/react-label"; - -import { cn } from "@/lib/utils"; - -export function Label({ className, ...props }: ComponentProps) { - return ( - - ); -} diff --git a/app/src/components/atoms/LangDot/LangDot.stories.tsx b/app/src/components/atoms/LangDot/LangDot.stories.tsx deleted file mode 100644 index c6b9b84..0000000 --- a/app/src/components/atoms/LangDot/LangDot.stories.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { LangDot } from "@/components/atoms/LangDot"; - -const meta: Meta = { - title: "Atoms/LangDot", - component: LangDot, -}; - -export default meta; - -export const Rust: StoryObj = { args: { lang: "rs" } }; -export const TypeScript: StoryObj = { args: { lang: "ts" } }; -export const Python: StoryObj = { args: { lang: "Python" } }; -export const Go: StoryObj = { args: { lang: "go" } }; -export const Unknown: StoryObj = { args: { lang: null } }; diff --git a/app/src/components/atoms/LangDot/LangDot.test.tsx b/app/src/components/atoms/LangDot/LangDot.test.tsx deleted file mode 100644 index 6385567..0000000 --- a/app/src/components/atoms/LangDot/LangDot.test.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { render } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; - -import { LangDot } from "@/components/atoms/LangDot"; -import { TooltipProvider } from "@/components/molecules/compounds/Tooltip"; - -function renderDot(lang: string | null | undefined) { - return render( - - - , - ); -} - -describe("LangDot", () => { - it("rendert mit lang-dot Klasse", () => { - const { container } = renderDot("rs"); - expect(container.querySelector(".lang-dot")).not.toBeNull(); - }); - - it("rendert bei null-lang (Fallback)", () => { - const { container } = renderDot(null); - expect(container.querySelector(".lang-dot")).not.toBeNull(); - }); - - it("rendert bei undefined-lang (Fallback)", () => { - const { container } = renderDot(undefined); - expect(container.querySelector(".lang-dot")).not.toBeNull(); - }); - - it("hat ein aria-label mit Sprachenbezeichnung", () => { - const { container } = renderDot("Rust"); - expect(container.querySelector(".lang-dot")?.getAttribute("aria-label")).toBeTruthy(); - }); - - it("setzt background-style aus langMeta", () => { - const { container } = renderDot("Rust"); - const dot = container.querySelector(".lang-dot"); - expect(dot?.style.background).not.toBe(""); - }); -}); diff --git a/app/src/components/atoms/LangDot/index.tsx b/app/src/components/atoms/LangDot/index.tsx deleted file mode 100644 index 75a873f..0000000 --- a/app/src/components/atoms/LangDot/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/molecules/compounds/Tooltip"; -import { langMeta } from "@/lib/languages"; - -interface LangDotProps { - lang: string | null | undefined; -} - -/** Colored 8px dot for a language identifier. Accepts either a file - * extension ("rs") or a canonical linguist name ("Rust"). */ -export function LangDot({ lang }: LangDotProps) { - const meta = langMeta(lang); - return ( - - - - - {meta.label} - - ); -} diff --git a/app/src/components/atoms/Mascot/Mascot.stories.tsx b/app/src/components/atoms/Mascot/Mascot.stories.tsx deleted file mode 100644 index 415995f..0000000 --- a/app/src/components/atoms/Mascot/Mascot.stories.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { Mascot, type MascotVariant } from "@/components/atoms/Mascot"; - -const meta: Meta = { - title: "Atoms/Mascot", - component: Mascot, - args: { - variant: "snoozing", - size: 128, - }, - argTypes: { - variant: { - control: { type: "select" }, - options: [ - "snoozing", - "celebrating", - "searching", - "waving", - "shrugging", - ] satisfies MascotVariant[], - }, - size: { control: { type: "number", min: 48, max: 256, step: 8 } }, - }, -}; - -export default meta; - -type Story = StoryObj; - -export const Snoozing: Story = { args: { variant: "snoozing" } }; -export const Celebrating: Story = { args: { variant: "celebrating" } }; -export const Searching: Story = { args: { variant: "searching" } }; -export const Waving: Story = { args: { variant: "waving" } }; -export const Shrugging: Story = { args: { variant: "shrugging" } }; - -export const AllPoses: Story = { - render: () => ( -
- {(["snoozing", "celebrating", "searching", "waving", "shrugging"] as MascotVariant[]).map( - (v) => ( -
- - {v} -
- ), - )} -
- ), -}; diff --git a/app/src/components/atoms/Separator/Separator.stories.tsx b/app/src/components/atoms/Separator/Separator.stories.tsx deleted file mode 100644 index 0fb38a0..0000000 --- a/app/src/components/atoms/Separator/Separator.stories.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { Separator } from "@/components/atoms/Separator"; - -const meta: Meta = { - title: "Atoms/Separator", - component: Separator, -}; - -export default meta; - -export const Horizontal: StoryObj = {}; -export const Vertical: StoryObj = { - args: { orientation: "vertical" }, - decorators: [ - (Story) => ( -
- Left - - Right -
- ), - ], -}; diff --git a/app/src/components/atoms/Separator/Separator.test.tsx b/app/src/components/atoms/Separator/Separator.test.tsx deleted file mode 100644 index b1f228c..0000000 --- a/app/src/components/atoms/Separator/Separator.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { render } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; - -import { Separator } from "@/components/atoms/Separator"; - -describe("Separator", () => { - it("rendert horizontal per Default", () => { - const { container } = render(); - const el = container.firstElementChild as HTMLElement | null; - expect(el?.className).toContain("h-px"); - expect(el?.className).toContain("w-full"); - }); - - it("rendert vertikal bei orientation='vertical'", () => { - const { container } = render(); - const el = container.firstElementChild as HTMLElement | null; - expect(el?.className).toContain("h-full"); - expect(el?.className).toContain("w-px"); - }); - - it("merged custom className", () => { - const { container } = render(); - expect((container.firstElementChild as HTMLElement).className).toContain("my-sep"); - }); - - it("ist per Default decorative (role=none)", () => { - const { container } = render(); - const role = container.firstElementChild?.getAttribute("role"); - expect(role === "none" || role === null).toBe(true); - }); -}); diff --git a/app/src/components/atoms/Separator/index.tsx b/app/src/components/atoms/Separator/index.tsx deleted file mode 100644 index eebebe9..0000000 --- a/app/src/components/atoms/Separator/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { type ComponentProps } from "react"; - -import * as SeparatorPrimitive from "@radix-ui/react-separator"; - -import { cn } from "@/lib/utils"; - -export function Separator({ - className, - orientation = "horizontal", - decorative = true, - ...props -}: ComponentProps) { - return ( - - ); -} diff --git a/app/src/components/atoms/Skeleton/Skeleton.stories.tsx b/app/src/components/atoms/Skeleton/Skeleton.stories.tsx deleted file mode 100644 index 4aaa043..0000000 --- a/app/src/components/atoms/Skeleton/Skeleton.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { Skeleton } from "@/components/atoms/Skeleton"; - -const meta: Meta = { - title: "Atoms/Skeleton", - component: Skeleton, -}; - -export default meta; - -export const Line: StoryObj = { args: { className: "h-3 w-48" } }; -export const LongLine: StoryObj = { args: { className: "h-3 w-full" } }; -export const Block: StoryObj = { args: { className: "h-16 w-64" } }; -export const Circle: StoryObj = { args: { className: "h-8 w-8 rounded-full" } }; -export const Avatar: StoryObj = { - args: { className: "h-10 w-10 rounded-full" }, -}; diff --git a/app/src/components/atoms/Skeleton/Skeleton.test.tsx b/app/src/components/atoms/Skeleton/Skeleton.test.tsx deleted file mode 100644 index 7eaccce..0000000 --- a/app/src/components/atoms/Skeleton/Skeleton.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { render } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; - -import { Skeleton } from "@/components/atoms/Skeleton"; - -describe("Skeleton", () => { - it("rendert einen div", () => { - const { container } = render(); - expect(container.firstElementChild?.tagName).toBe("DIV"); - }); - - it("wendet Pulse- und Muted-Klassen an", () => { - const { container } = render(); - const el = container.firstElementChild as HTMLElement; - expect(el.className).toContain("animate-pulse"); - expect(el.className).toContain("bg-muted"); - }); - - it("merged zusätzliche className", () => { - const { container } = render(); - const el = container.firstElementChild as HTMLElement; - expect(el.className).toContain("h-8"); - expect(el.className).toContain("w-40"); - }); - - it("wendet inline-style an", () => { - const { container } = render(); - const el = container.firstElementChild as HTMLElement; - expect(el.style.width).toBe("120px"); - }); - - it("ist aria-hidden", () => { - const { container } = render(); - expect(container.firstElementChild).toHaveAttribute("aria-hidden"); - }); -}); diff --git a/app/src/components/atoms/Skeleton/index.tsx b/app/src/components/atoms/Skeleton/index.tsx deleted file mode 100644 index c0876a6..0000000 --- a/app/src/components/atoms/Skeleton/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { CSSProperties } from "react"; - -import { cn } from "@/lib/utils"; - -interface SkeletonProps { - className?: string; - /** Inline style is handy when the parent lays out with - * `gridTemplateColumns` inline — we need to sit in that grid and - * still animate. */ - style?: CSSProperties; -} - -/** Base shimmer placeholder. Sized and positioned by the caller. Use the - * variants in `molecules/skeletons/*` for composed row shapes. */ -export function Skeleton({ className, style }: SkeletonProps) { - return ( -
- ); -} diff --git a/app/src/components/atoms/Sparkline/Sparkline.stories.tsx b/app/src/components/atoms/Sparkline/Sparkline.stories.tsx deleted file mode 100644 index f090c4a..0000000 --- a/app/src/components/atoms/Sparkline/Sparkline.stories.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { Sparkline } from "@/components/atoms/Sparkline"; - -const meta: Meta = { - title: "Atoms/Sparkline", - component: Sparkline, -}; - -export default meta; - -export const Default: StoryObj = { - args: { data: [3, 5, 2, 8, 12, 4, 6, 9, 1, 0, 7, 3, 5, 10] }, -}; - -export const Empty: StoryObj = { - args: { data: [0, 0, 0, 0, 0, 0, 0] }, -}; - -export const Active: StoryObj = { - args: { data: [3, 5, 2, 8, 12, 4, 6], active: true }, -}; diff --git a/app/src/components/atoms/Sparkline/Sparkline.test.tsx b/app/src/components/atoms/Sparkline/Sparkline.test.tsx deleted file mode 100644 index 01fb629..0000000 --- a/app/src/components/atoms/Sparkline/Sparkline.test.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { render } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; - -import { Sparkline } from "@/components/atoms/Sparkline"; - -describe("Sparkline", () => { - it("rendert .spark container", () => { - const { container } = render(); - expect(container.querySelector(".spark")).not.toBeNull(); - }); - - it("rendert einen Balken pro Datenpunkt", () => { - const { container } = render(); - const bars = container.querySelectorAll(".spark > span"); - expect(bars.length).toBe(5); - }); - - it("markiert Null-Werte mit .zero Klasse", () => { - const { container } = render(); - expect(container.querySelectorAll(".zero").length).toBe(2); - }); - - it("fügt .active Klasse im active-Mode hinzu", () => { - const { container } = render(); - expect(container.querySelector(".spark.active")).not.toBeNull(); - }); - - it("wendet width/height aus Props an", () => { - const { container } = render(); - const el = container.querySelector(".spark"); - expect(el?.style.width).toBe("100px"); - expect(el?.style.height).toBe("24px"); - }); -}); diff --git a/app/src/components/atoms/Sparkline/index.tsx b/app/src/components/atoms/Sparkline/index.tsx deleted file mode 100644 index 531f13c..0000000 --- a/app/src/components/atoms/Sparkline/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -interface SparklineProps { - data: number[]; - active?: boolean; - width?: number; - height?: number; -} - -/** Tiny commit-activity bar chart. `data` is an arbitrary-length numeric - * series; bars auto-scale to the series max. Used in repo row sparklines - * and the dashboard 14-day activity strip. */ -export function Sparkline({ data, active, width = 64, height = 18 }: SparklineProps) { - const max = Math.max(...data, 1); - const barW = Math.floor((width - (data.length - 1) * 2) / data.length); - return ( -
- {data.map((v, i) => ( - - ))} -
- ); -} diff --git a/app/src/components/atoms/Spinner/Spinner.stories.tsx b/app/src/components/atoms/Spinner/Spinner.stories.tsx deleted file mode 100644 index ee0f7c6..0000000 --- a/app/src/components/atoms/Spinner/Spinner.stories.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { Spinner } from "@/components/atoms/Spinner"; - -const meta: Meta = { - title: "Atoms/Spinner", - component: Spinner, -}; - -export default meta; - -export const Default: StoryObj = {}; -export const Small: StoryObj = { args: { size: "sm" } }; -export const Large: StoryObj = { args: { size: "lg" } }; -export const WithLabel: StoryObj = { - args: { size: "md", label: "Loading repositories" }, -}; diff --git a/app/src/components/atoms/Spinner/Spinner.test.tsx b/app/src/components/atoms/Spinner/Spinner.test.tsx deleted file mode 100644 index e779d7c..0000000 --- a/app/src/components/atoms/Spinner/Spinner.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; - -import { Spinner } from "@/components/atoms/Spinner"; - -describe("Spinner", () => { - it("rendert mit role=status", () => { - render(); - expect(screen.getByRole("status")).toBeInTheDocument(); - }); - - it("nutzt die md-Größe per Default", () => { - render(); - expect(screen.getByRole("status").getAttribute("class")).toContain("h-4"); - }); - - it("wendet sm-Größe an", () => { - render(); - expect(screen.getByRole("status").getAttribute("class")).toContain("h-3.5"); - }); - - it("wendet lg-Größe an", () => { - render(); - expect(screen.getByRole("status").getAttribute("class")).toContain("h-6"); - }); - - it("setzt aria-label aus label-Prop", () => { - render(); - expect(screen.getByRole("status")).toHaveAttribute("aria-label", "Lade Daten"); - }); -}); diff --git a/app/src/components/atoms/Spinner/index.tsx b/app/src/components/atoms/Spinner/index.tsx deleted file mode 100644 index 490eee7..0000000 --- a/app/src/components/atoms/Spinner/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Loader2 } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -interface SpinnerProps { - className?: string; - size?: "sm" | "md" | "lg"; - label?: string; -} - -const SIZES = { - sm: "h-3.5 w-3.5", - md: "h-4 w-4", - lg: "h-6 w-6", -} as const; - -export function Spinner({ className, size = "md", label }: SpinnerProps) { - return ( - - ); -} diff --git a/app/src/components/atoms/StatusDot/StatusDot.stories.tsx b/app/src/components/atoms/StatusDot/StatusDot.stories.tsx deleted file mode 100644 index eedbcaf..0000000 --- a/app/src/components/atoms/StatusDot/StatusDot.stories.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { StatusDot } from "@/components/atoms/StatusDot"; - -const meta: Meta = { - title: "Atoms/StatusDot", - component: StatusDot, -}; - -export default meta; - -export const Clean: StoryObj = { args: { kind: "clean" } }; -export const Dirty: StoryObj = { args: { kind: "dirty" } }; -export const Behind: StoryObj = { args: { kind: "behind" } }; diff --git a/app/src/components/atoms/StatusDot/StatusDot.test.tsx b/app/src/components/atoms/StatusDot/StatusDot.test.tsx deleted file mode 100644 index f9fe35c..0000000 --- a/app/src/components/atoms/StatusDot/StatusDot.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { render } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; - -import { StatusDot } from "@/components/atoms/StatusDot"; - -describe("StatusDot", () => { - it("rendert span mit status-dot und clean-Klasse", () => { - const { container } = render(); - const el = container.firstElementChild; - expect(el?.className).toBe("status-dot clean"); - }); - - it("wendet dirty-Kind an", () => { - const { container } = render(); - expect(container.firstElementChild?.className).toBe("status-dot dirty"); - }); - - it("wendet behind-Kind an", () => { - const { container } = render(); - expect(container.firstElementChild?.className).toBe("status-dot behind"); - }); -}); diff --git a/app/src/components/atoms/StatusDot/index.tsx b/app/src/components/atoms/StatusDot/index.tsx deleted file mode 100644 index ef8d404..0000000 --- a/app/src/components/atoms/StatusDot/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export type StatusKind = "clean" | "dirty" | "behind"; - -interface StatusDotProps { - kind: StatusKind; -} - -/** Coloured status dot used as a prefix in repo rows and detail summaries. */ -export function StatusDot({ kind }: StatusDotProps) { - return ; -} diff --git a/app/src/components/atoms/Switch/Switch.stories.tsx b/app/src/components/atoms/Switch/Switch.stories.tsx deleted file mode 100644 index d9ded3d..0000000 --- a/app/src/components/atoms/Switch/Switch.stories.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; - -import { Switch } from "@/components/atoms/Switch"; - -const meta: Meta = { - title: "Atoms/Switch", - component: Switch, -}; - -export default meta; - -export const Off: StoryObj = { args: { checked: false } }; -export const On: StoryObj = { args: { checked: true } }; -export const Disabled: StoryObj = { args: { disabled: true, checked: false } }; -export const DisabledOn: StoryObj = { args: { disabled: true, checked: true } }; diff --git a/app/src/components/atoms/Switch/Switch.test.tsx b/app/src/components/atoms/Switch/Switch.test.tsx deleted file mode 100644 index 5e8dc48..0000000 --- a/app/src/components/atoms/Switch/Switch.test.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { describe, expect, it, vi } from "vitest"; - -import { Switch } from "@/components/atoms/Switch"; - -describe("Switch", () => { - it("rendert mit role=switch", () => { - render(); - expect(screen.getByRole("switch")).toBeInTheDocument(); - }); - - it("ist per Default unchecked", () => { - render(); - expect(screen.getByRole("switch")).toHaveAttribute("data-state", "unchecked"); - }); - - it("respektiert defaultChecked", () => { - render(); - expect(screen.getByRole("switch")).toHaveAttribute("data-state", "checked"); - }); - - it("ruft onCheckedChange bei Klick auf", async () => { - const user = userEvent.setup(); - const handler = vi.fn(); - render(); - await user.click(screen.getByRole("switch")); - expect(handler).toHaveBeenCalledWith(true); - }); - - it("ist disabled wenn disabled-Prop gesetzt", () => { - render(); - expect(screen.getByRole("switch")).toBeDisabled(); - }); -}); diff --git a/app/src/components/atoms/Switch/index.tsx b/app/src/components/atoms/Switch/index.tsx deleted file mode 100644 index 4f6efc7..0000000 --- a/app/src/components/atoms/Switch/index.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { type ComponentProps } from "react"; - -import * as SwitchPrimitive from "@radix-ui/react-switch"; - -import { cn } from "@/lib/utils"; - -/** - * Brand-tinted Radix switch. The checked state uses the Recrest accent - * (coral by default, picks up `data-accent` overrides), so the active - * pill stands out in both light and dark mode without falling back to - * the raw `--ink-0` swap that previously rendered as either solid black - * (light) or solid white (dark) — both of which read as "broken" against - * the surrounding card. - * - * The thumb stays opaque white in both states so the iOS-style contrast - * holds up over the accent fill. - */ -export function Switch({ className, ...props }: ComponentProps) { - return ( - - - - ); -} diff --git a/app/src/components/atoms/badges/GeneralBadge/index.tsx b/app/src/components/atoms/badges/GeneralBadge/index.tsx new file mode 100644 index 0000000..59ddddd --- /dev/null +++ b/app/src/components/atoms/badges/GeneralBadge/index.tsx @@ -0,0 +1,58 @@ +import { Chip, type ChipProps } from "@mui/material"; +import { styled } from "@mui/material/styles"; + +export type GeneralBadgeTone = "default" | "green" | "amber" | "red" | "blue" | "purple"; + +export interface GeneralBadgeProps extends Omit { + tone?: GeneralBadgeTone; +} + +interface ToneProp { + tone?: GeneralBadgeTone; +} + +const StyledChip = styled(Chip, { shouldForwardProp: (p) => p !== "tone" })(({ + theme, + tone = "default", +}) => { + const palette = theme.palette; + let bg: string; + let color: string; + switch (tone) { + case "green": + bg = palette.success.light; + color = palette.success.dark; + break; + case "amber": + bg = palette.warning.light; + color = palette.warning.dark; + break; + case "red": + bg = palette.error.light; + color = palette.error.dark; + break; + case "blue": + bg = palette.info.light; + color = palette.info.dark; + break; + case "purple": + bg = palette.secondary.light; + color = palette.secondary.dark; + break; + case "default": + default: + bg = palette.surface.interface.backElevation; + color = palette.text.primary; + } + return { + fontWeight: 500, + backgroundColor: bg, + color, + }; +}); + +function GeneralBadge({ tone = "default", size = "small", ...rest }: GeneralBadgeProps) { + return ; +} + +export default GeneralBadge; diff --git a/app/src/components/atoms/brand/AppIcon/index.tsx b/app/src/components/atoms/brand/AppIcon/index.tsx new file mode 100644 index 0000000..7a05cca --- /dev/null +++ b/app/src/components/atoms/brand/AppIcon/index.tsx @@ -0,0 +1,60 @@ +import { styled } from "@mui/material/styles"; + +import IconDark from "@/assets/recrest-icon-dark.svg?react"; +import IconDevDark from "@/assets/recrest-icon-dev-dark.svg?react"; +import IconDevLight from "@/assets/recrest-icon-dev-light.svg?react"; +import IconLight from "@/assets/recrest-icon-light.svg?react"; + +export interface AppIconProps { + className?: string; + /** Visual label read by screen readers (defaults to "Recrest"). */ + title?: string; +} + +const Root = styled("span")({ + position: "relative", + display: "inline-block", + lineHeight: 0, +}); + +const Light = import.meta.env.DEV ? IconDevLight : IconLight; +const Dark = import.meta.env.DEV ? IconDevDark : IconDark; + +const LightVariant = styled(Light)({ + display: "block", + width: "100%", + height: "100%", + 'html[data-theme="dark"] &': { + display: "none", + }, +}); + +const DarkVariant = styled(Dark)({ + display: "none", + width: "100%", + height: "100%", + 'html[data-theme="dark"] &': { + display: "block", + }, +}); + +/** + * Full app-icon tile (white/dark rounded square + chevrons), the same artwork + * used for the macOS `.icns` / Windows `.ico` bundle. Unlike `Logo` (which + * strips the tile and renders chevrons in `currentColor`), this component is + * the literal icon a user sees in their dock, taskbar or window proxy — + * required by the OS-native window-chrome conventions. + * + * Picks the dev-tagged variant automatically in dev builds; Vite drops the + * unused imports under tree-shaking at production build time. + */ +function AppIcon({ className, title = "Recrest" }: AppIconProps) { + return ( + + + + + ); +} + +export default AppIcon; diff --git a/app/src/components/atoms/brand/Logo/index.tsx b/app/src/components/atoms/brand/Logo/index.tsx new file mode 100644 index 0000000..5f8918c --- /dev/null +++ b/app/src/components/atoms/brand/Logo/index.tsx @@ -0,0 +1,78 @@ +import { styled } from "@mui/material/styles"; + +import IconDev from "@/assets/recrest-icon-dev.svg?react"; +import IconTransparentDark from "@/assets/recrest-icon-transparent-dark.svg?react"; +import IconTransparentWhite from "@/assets/recrest-icon-transparent-white.svg?react"; + +export interface LogoProps { + className?: string; + /** Visual label read by screen readers (defaults to "Recrest"). */ + title?: string; +} + +const Root = styled("span")({ + position: "relative", + display: "inline-block", + lineHeight: 0, +}); + +const DevMark = styled(IconDev)({ + display: "block", + width: "100%", + height: "100%", +}); + +const LightVariant = styled(IconTransparentDark)({ + display: "block", + width: "100%", + height: "100%", + 'html[data-theme="dark"] &': { + display: "none", + }, +}); + +const DarkVariant = styled(IconTransparentWhite)({ + display: "none", + width: "100%", + height: "100%", + 'html[data-theme="dark"] &': { + display: "block", + }, +}); + +const IS_VITE_DEV = import.meta.env.DEV; + +/** + * Recrest mark used in the left sidebar's brand row. + * + * Variant matrix: + * prod, any theme → transparent wordmark, chevron colour follows theme + * (dark chevrons in light mode, white chevrons in dark) + * dev, any theme → `recrest-icon-dev.svg` (orange chevrons + `` badge). + * The dev mark is already theme-neutral (orange reads on + * both surfaces), so we don't need a per-theme split. + * + * "Dev" means **Vite dev build** (`import.meta.env.DEV`) — covers both + * `yarn tauri:dev` and `yarn dev:web`, because both run the dev bundle. + * The previous gating (real-Tauri only) was overly defensive and meant the + * dev badge silently disappeared in `dev:web`, where most UI iteration + * actually happens. The favicon (`useFaviconSync`) uses the same `DEV` + * gate, so tab + sidebar stay in lockstep. + */ +function Logo({ className, title = "Recrest" }: LogoProps) { + if (IS_VITE_DEV) { + return ( + + + + ); + } + return ( + + + + + ); +} + +export default Logo; diff --git a/app/src/components/atoms/Mascot/index.tsx b/app/src/components/atoms/brand/Mascot/index.tsx similarity index 66% rename from app/src/components/atoms/Mascot/index.tsx rename to app/src/components/atoms/brand/Mascot/index.tsx index 5b89f3f..cc9bd64 100644 --- a/app/src/components/atoms/Mascot/index.tsx +++ b/app/src/components/atoms/brand/Mascot/index.tsx @@ -1,4 +1,4 @@ -import { cn } from "@/lib/utils"; +import { useTheme } from "@mui/material/styles"; /** * Recrest's empty-state mascot. A rounded-square character echoing the app @@ -8,7 +8,8 @@ import { cn } from "@/lib/utils"; * shrugging = generic empty). * * Stroke inherits `currentColor` so callers control the ink via CSS `color`. - * The crest uses `--accent` directly for a stable brand touch across themes. + * Accent colours are pulled from the MUI theme so dark/light/oled/glassy all + * tint the crest + cheeks consistently with the rest of the UI. */ export type MascotVariant = "snoozing" | "celebrating" | "searching" | "waving" | "shrugging"; @@ -19,7 +20,22 @@ interface MascotProps { title?: string; } -export function Mascot({ variant = "shrugging", size = 112, className, title }: MascotProps) { +interface MascotPalette { + accent: string; + accentWeak: string; + accentInk: string; + surface: string; +} + +function Mascot({ variant = "shrugging", size = 112, className, title }: MascotProps) { + const theme = useTheme(); + const palette: MascotPalette = { + accent: theme.palette.primary.main, + accentWeak: `color-mix(in srgb, ${theme.palette.primary.main} 16%, transparent)`, + accentInk: theme.palette.primary.dark, + surface: theme.palette.surface.interface.base, + }; + return ( - - - - - + + + + + ); } -/* ───────── Body: rounded-square torso that also houses the head ───────── */ - -function MascotBody() { +function MascotBody({ palette }: { palette: MascotPalette }) { return ( <> - {/* Soft shadow puddle */} - {/* Torso/head combo */} - {/* Subtle belly hairline to give depth */} - + ); case "searching": @@ -124,11 +129,9 @@ function MascotFace({ variant }: { variant: MascotVariant }) { - {/* open, cheerful mouth */} - {/* blush */} - - + + ); case "shrugging": @@ -143,26 +146,30 @@ function MascotFace({ variant }: { variant: MascotVariant }) { } } -/* ───────── Arms: different gestures per mood ───────── */ - -function MascotArms({ variant }: { variant: MascotVariant }) { - const base = { - stroke: "currentColor", - strokeWidth: 4, - fill: "var(--accent-weak)", - } as const; +function MascotArms({ variant, palette }: { variant: MascotVariant; palette: MascotPalette }) { + const armFill = palette.accentWeak; switch (variant) { case "snoozing": - // Arms crossed, resting on the belly return ( - - + + ); case "celebrating": - // Arms up in the air, little "hand" circles at the tips return ( - - + + ); case "searching": - // One arm holds a magnifying glass out to the side return ( - {/* Left arm resting */} - {/* Right arm holds the magnifier */} - {/* Glass shine */} ); case "waving": - // One arm waving overhead, other at the side return ( - + ); case "shrugging": default: - // Arms out to the sides, palms up return ( - - + + ); } } -/* ───────── Decor: the little floaty extras (z's, sparks, dots) ───────── */ - -function MascotDecor({ variant }: { variant: MascotVariant }) { +function MascotDecor({ variant, palette }: { variant: MascotVariant; palette: MascotPalette }) { switch (variant) { case "snoozing": return ( ); case "celebrating": - // Spark bursts either side return ( - + - - + + ); case "searching": - // Tiny question mark / dotted trail above return ( @@ -307,9 +304,14 @@ function MascotDecor({ variant }: { variant: MascotVariant }) { ); case "waving": - // Little motion lines near the raised hand return ( - + @@ -319,3 +321,5 @@ function MascotDecor({ variant }: { variant: MascotVariant }) { return null; } } + +export default Mascot; diff --git a/app/src/components/atoms/buttons/GeneralButton/index.tsx b/app/src/components/atoms/buttons/GeneralButton/index.tsx new file mode 100644 index 0000000..bf4b86a --- /dev/null +++ b/app/src/components/atoms/buttons/GeneralButton/index.tsx @@ -0,0 +1,94 @@ +import { forwardRef } from "react"; + +import { Button, CircularProgress, type ButtonProps as MuiButtonProps } from "@mui/material"; +import { styled } from "@mui/material/styles"; + +export type GeneralButtonVariant = + | "default" + | "destructive" + | "outline" + | "secondary" + | "ghost" + | "link"; + +export type GeneralButtonSize = "default" | "sm" | "lg"; + +export interface GeneralButtonProps extends Omit { + variant?: GeneralButtonVariant; + size?: GeneralButtonSize; + loading?: boolean; +} + +interface StyledButtonProps { + variantKind?: GeneralButtonVariant; +} + +function mapVariant(variant: GeneralButtonVariant): { + muiVariant: MuiButtonProps["variant"]; + muiColor: MuiButtonProps["color"]; +} { + switch (variant) { + case "destructive": + return { muiVariant: "contained", muiColor: "error" }; + case "outline": + return { muiVariant: "outlined", muiColor: "primary" }; + case "secondary": + return { muiVariant: "contained", muiColor: "secondary" }; + case "ghost": + return { muiVariant: "text", muiColor: "inherit" }; + case "link": + return { muiVariant: "text", muiColor: "primary" }; + case "default": + default: + return { muiVariant: "contained", muiColor: "primary" }; + } +} + +function mapSize(size: GeneralButtonSize): MuiButtonProps["size"] { + if (size === "sm") return "small"; + if (size === "lg") return "large"; + return "medium"; +} + +const StyledButton = styled(Button, { + shouldForwardProp: (p) => p !== "variantKind", +})(({ variantKind }) => ({ + textTransform: "none", + ...(variantKind === "link" + ? { + textDecoration: "underline", + "&:hover": { textDecoration: "underline" }, + } + : {}), +})); + +const GeneralButton = forwardRef(function GeneralButton( + { + variant = "default", + size = "default", + loading = false, + disabled, + children, + startIcon, + ...rest + }, + ref, +) { + const { muiVariant, muiColor } = mapVariant(variant); + return ( + : startIcon} + {...rest} + > + {children} + + ); +}); + +export default GeneralButton; diff --git a/app/src/components/atoms/buttons/GeneralButtonGroup/index.tsx b/app/src/components/atoms/buttons/GeneralButtonGroup/index.tsx new file mode 100644 index 0000000..b285d02 --- /dev/null +++ b/app/src/components/atoms/buttons/GeneralButtonGroup/index.tsx @@ -0,0 +1,138 @@ +import { Children, cloneElement, isValidElement } from "react"; + +import { ToggleButton, ToggleButtonGroup, type ToggleButtonGroupProps } from "@mui/material"; +import { styled } from "@mui/material/styles"; + +export type GeneralButtonGroupShape = "pill" | "square"; +export type GeneralButtonGroupSize = "md" | "sm"; + +export interface GeneralButtonGroupProps extends ToggleButtonGroupProps { + shape?: GeneralButtonGroupShape; + /** + * `md` (default): 38px buttons — header / page toolbars. + * `sm`: 32px buttons — tighter toolbars, popovers. + */ + density?: GeneralButtonGroupSize; +} + +interface StyledProps { + shape?: GeneralButtonGroupShape; + density?: GeneralButtonGroupSize; +} + +const SHOULD_FORWARD = (prop: PropertyKey) => prop !== "shape" && prop !== "density"; + +/** + * Segmented button group with **one continuous border** around the whole + * tile and 1px vertical dividers between adjacent segments. The active + * segment is signalled only by a subtle background fill — the outer border + * stays the same colour regardless of selection so the tile reads as one + * coherent control, not "buttons next to each other". + * + * Works with any number of segments (2, 5, 6, …). Adjacent segments share + * a single 1px divider via a `border-left` on every segment except the + * first. The outer border is owned by the group element so segment + * borders never appear at the outer edge. + */ +const StyledGroup = styled(ToggleButtonGroup, { shouldForwardProp: SHOULD_FORWARD })( + ({ theme, shape = "square", density = "md" }) => ({ + display: "inline-flex", + alignItems: "stretch", + gap: 0, + height: density === "sm" ? 32 : 38, + backgroundColor: theme.palette.background.paper, + border: `1px solid ${theme.palette.divider}`, + borderRadius: shape === "square" ? 8 : 999, + padding: 0, + fontFamily: "inherit", + flexWrap: "nowrap", + // Clip children so their square corners can't poke through the group's + // rounded outer corners. + overflow: "hidden", + transition: "border-color 150ms ease", + "&:hover": { + borderColor: theme.palette.border.hover, + }, + "&.MuiToggleButtonGroup-vertical": { + flexDirection: "column", + height: "auto", + width: density === "sm" ? 32 : 38, + }, + }), +); + +const StyledToggle = styled(ToggleButton, { shouldForwardProp: SHOULD_FORWARD })( + ({ theme, density = "md" }) => ({ + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + gap: 6, + height: "100%", + padding: density === "sm" ? "0 12px" : "0 14px", + // No outer borders — those belong to the group. Only a 1px left + // divider between adjacent siblings to mark segment boundaries. + border: 0, + borderRadius: 0, + backgroundColor: "transparent", + color: theme.palette.text.secondary, + fontFamily: "inherit", + fontSize: 12, + fontWeight: 500, + lineHeight: 1, + cursor: "pointer", + whiteSpace: "nowrap", + textTransform: "none", + transition: "color 150ms ease, background-color 150ms ease", + + // Horizontal group: 1px left divider on every segment except the first. + ".MuiToggleButtonGroup-horizontal &:not(:first-of-type)": { + borderLeft: `1px solid ${theme.palette.divider}`, + }, + // Vertical group: 1px top divider on every segment except the first. + ".MuiToggleButtonGroup-vertical &:not(:first-of-type)": { + borderTop: `1px solid ${theme.palette.divider}`, + }, + + "&:hover": { + color: theme.palette.text.primary, + backgroundColor: theme.palette.surface.interface.active, + }, + "&.Mui-selected": { + // Active = subtle fill only. No border-colour change, no font-weight + // jump — the tile still reads as one continuous control. + color: theme.palette.text.primary, + backgroundColor: theme.palette.surface.interface.active, + }, + "&.Mui-selected:hover": { + backgroundColor: theme.palette.surface.interface.active, + }, + "&.Mui-disabled": { + opacity: 0.45, + cursor: "default", + }, + }), +); + +function GeneralButtonGroup({ + shape = "square", + density = "md", + children, + ...rest +}: GeneralButtonGroupProps) { + const decorated = Children.map(children, (child) => { + if (!isValidElement(child)) return child; + return cloneElement(child as React.ReactElement, { + shape: (child.props as StyledProps).shape ?? shape, + density: (child.props as StyledProps).density ?? density, + }); + }); + + return ( + + {decorated} + + ); +} + +export { StyledToggle as GeneralButtonGroupItem }; +export default GeneralButtonGroup; diff --git a/app/src/components/atoms/buttons/GeneralIconButton/index.tsx b/app/src/components/atoms/buttons/GeneralIconButton/index.tsx new file mode 100644 index 0000000..6165148 --- /dev/null +++ b/app/src/components/atoms/buttons/GeneralIconButton/index.tsx @@ -0,0 +1,259 @@ +import React from "react"; + +import { Box, styled } from "@mui/material"; + +import GeneralTooltip from "@/components/atoms/feedback/GeneralTooltip"; +import { useDevice } from "@/hooks/useDevice"; + +interface GeneralIconButtonProps { + id?: string; + icon: React.ReactNode; + onAction?: (_e: React.MouseEvent | React.TouchEvent) => void; + noMargin?: boolean; + noPadding?: boolean; + bigIcon?: boolean; + hugeIcon?: boolean; + customIconSize?: number; + forChat?: boolean; + title?: string; + placement?: "top" | "bottom" | "left" | "right"; + noBackground?: boolean; + fixedBackground?: boolean; + customBackgroundColor?: string; + customColor?: string; + hoverAllowed?: boolean; + toolTipMaxWidth?: string; + tooltipArrow?: boolean; + rounded?: boolean; + disabled?: boolean; + isActive?: boolean; +} + +const IconButton = styled(Box, { + shouldForwardProp: (prop) => + prop !== "noMargin" && + prop !== "noPadding" && + prop !== "bigIcon" && + prop !== "hugeIcon" && + prop !== "customIconSize" && + prop !== "forChat" && + prop !== "noBackground" && + prop !== "fixedBackground" && + prop !== "customBackgroundColor" && + prop !== "customColor" && + prop !== "hoverAllowed" && + prop !== "rounded" && + prop !== "disabled" && + prop !== "isActive", +})>( + ({ + theme, + noMargin, + noPadding, + bigIcon, + hugeIcon, + customIconSize, + forChat, + noBackground, + fixedBackground, + customBackgroundColor, + customColor, + hoverAllowed, + rounded, + disabled, + isActive, + }) => ({ + display: "flex", + alignItems: "center", + borderRadius: rounded ? "50%" : theme.spacing(1), + padding: noPadding ? 0 : "4px", + marginRight: noMargin ? 0 : 8, + backgroundColor: disabled + ? "transparent" + : fixedBackground + ? theme.palette.surface.interface.background + : forChat + ? theme.palette.surface.interface.base + : customBackgroundColor + ? customBackgroundColor + : `transparent`, + backgroundImage: "unset", + color: disabled + ? theme.palette.icon.disabled + : isActive + ? theme.palette.primary.main + : forChat + ? theme.palette.icon.secondary + : customColor + ? customColor + : theme.palette.text.default, + border: forChat ? `1px solid ${theme.palette.border.separator}` : undefined, + ...(hoverAllowed && { + "&:hover": { + backgroundColor: + disabled || noBackground + ? "transparent" + : customBackgroundColor + ? customBackgroundColor + : theme.palette.surface.button.hoverLight, + "& path": { + fill: disabled + ? theme.palette.icon.disabled + : customColor + ? customColor + : theme.palette.icon.primary, + }, + "& span": { + color: disabled + ? theme.palette.icon.disabled + : customColor + ? customColor + : theme.palette.icon.primary, + }, + "& svg": { + color: disabled + ? theme.palette.icon.disabled + : customColor + ? customColor + : theme.palette.icon.primary, + }, + }, + "&:active": { + backgroundColor: + disabled || noBackground ? "transparent" : theme.palette.surface.button.secondary, + "& path": { + fill: disabled + ? theme.palette.icon.disabled + : customColor + ? customColor + : theme.palette.icon.primary, + }, + "& span": { + color: disabled + ? theme.palette.icon.disabled + : customColor + ? customColor + : theme.palette.icon.primary, + }, + "& svg": { + color: disabled + ? theme.palette.icon.disabled + : customColor + ? customColor + : theme.palette.icon.primary, + }, + }, + }), + "& span": { + width: customIconSize ? customIconSize : hugeIcon ? 24 : bigIcon ? 20 : 16, + height: customIconSize ? customIconSize : hugeIcon ? 24 : bigIcon ? 20 : 16, + fontSize: customIconSize ? customIconSize : hugeIcon ? "24px" : bigIcon ? "20px" : "16px", + cursor: !disabled && "pointer", + color: disabled + ? theme.palette.icon.disabled + : isActive + ? theme.palette.primary.main + : forChat + ? theme.palette.icon.secondary + : customColor + ? customColor + : theme.palette.text.default, + }, + "& svg": { + width: customIconSize ? customIconSize : hugeIcon ? 24 : bigIcon ? 20 : 16, + height: customIconSize ? customIconSize : hugeIcon ? 24 : bigIcon ? 20 : 16, + fontSize: customIconSize ? customIconSize : hugeIcon ? "24px" : bigIcon ? "20px" : "16px", + cursor: !disabled && "pointer", + color: disabled + ? theme.palette.icon.disabled + : isActive + ? theme.palette.primary.main + : forChat + ? theme.palette.icon.secondary + : customColor + ? customColor + : theme.palette.text.default, + }, + }), +); + +const GeneralIconButton: React.FC = ({ + id, + icon, + onAction, + noMargin = false, + noPadding = false, + bigIcon = false, + hugeIcon = false, + customIconSize, + forChat = false, + title, + placement, + tooltipArrow = true, + noBackground = false, + fixedBackground = false, + customBackgroundColor, + customColor, + hoverAllowed = true, + rounded = false, + disabled = false, + isActive = false, +}) => { + const { isMobile, isTablet } = useDevice(); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if ((e.key === "Enter" || e.key === " ") && !disabled) { + e.preventDefault(); + onAction?.(e as unknown as React.MouseEvent); + } + }; + + const eventProps = + isMobile || isTablet + ? { + onTouchEnd: (e: React.TouchEvent) => onAction?.(e), + onClick: (e: React.MouseEvent) => onAction?.(e), + onKeyDown: handleKeyDown, + } + : { + onClick: (e: React.MouseEvent) => onAction?.(e), + onKeyDown: handleKeyDown, + }; + + const content = ( + + {icon} + + ); + + if (!disabled && title && placement) { + return ( + + {content} + + ); + } + + return content; +}; + +export default GeneralIconButton; diff --git a/app/src/components/atoms/buttons/ThemeSwitcherButton/index.tsx b/app/src/components/atoms/buttons/ThemeSwitcherButton/index.tsx new file mode 100644 index 0000000..8fd526c --- /dev/null +++ b/app/src/components/atoms/buttons/ThemeSwitcherButton/index.tsx @@ -0,0 +1,45 @@ +import { useTranslation } from "react-i18next"; + +import { FormControl, InputLabel, MenuItem, Select, type SelectChangeEvent } from "@mui/material"; + +import { THEMES, type ThemeId } from "@/lib/constants/theme.constants"; +import { setThemeId } from "@/store/actions/settings.actions"; +import { useAppDispatch, useAppSelector } from "@/store/hooks"; + +export interface ThemeSwitcherButtonProps { + /** Optional label override. Defaults to i18n `settings:theme.themeSelect`. */ + label?: string; +} + +function ThemeSwitcherButton({ label }: ThemeSwitcherButtonProps) { + const { t } = useTranslation("settings"); + const themeId = useAppSelector((s) => s.settings.themeId); + const dispatch = useAppDispatch(); + + const handleChange = (e: SelectChangeEvent) => { + dispatch(setThemeId(e.target.value as ThemeId)); + }; + + const labelText = label ?? t("theme.themeSelect"); + + return ( + + {labelText} + + + ); +} + +export default ThemeSwitcherButton; diff --git a/app/src/components/atoms/data/GeneralSparkline/index.tsx b/app/src/components/atoms/data/GeneralSparkline/index.tsx new file mode 100644 index 0000000..d437c91 --- /dev/null +++ b/app/src/components/atoms/data/GeneralSparkline/index.tsx @@ -0,0 +1,60 @@ +import { Box } from "@mui/material"; +import { styled } from "@mui/material/styles"; + +interface Props { + data: readonly number[]; + active?: boolean; + width?: number; + height?: number; +} + +/** + * Compact bar sparkline used in the Repos table row. Each value renders as a + * thin vertical bar. Active (dirty) rows use the accent (primary) colour; + * inactive rows render in the muted info-light grey. Zero-value buckets + * collapse to a 2px flat line, mirroring the src-old `.spark .zero` rule. + */ +function GeneralSparkline({ data, active = false, width = 88, height = 18 }: Props) { + const peak = Math.max(1, ...data); + return ( + + {data.map((v, i) => ( + + ))} + + ); +} + +export default GeneralSparkline; + +const Bars = styled(Box)(({ theme }) => ({ + display: "flex", + alignItems: "flex-end", + gap: 2, + "& > span": { + backgroundColor: theme.palette.text.informationLight ?? theme.palette.text.secondary, + }, + "& > span[data-zero='true']": { + backgroundColor: theme.palette.border.default, + }, + "&[data-active='true'] > span": { + backgroundColor: theme.palette.primary.main, + }, + "&[data-active='true'] > span[data-zero='true']": { + backgroundColor: theme.palette.border.default, + }, +})); + +const Bar = styled("span")({ + display: "block", + flex: 1, + minWidth: 0, + borderRadius: 1, + minHeight: 2, +}); diff --git a/app/src/components/atoms/dividers/GeneralDivider/index.tsx b/app/src/components/atoms/dividers/GeneralDivider/index.tsx new file mode 100644 index 0000000..5df3340 --- /dev/null +++ b/app/src/components/atoms/dividers/GeneralDivider/index.tsx @@ -0,0 +1,9 @@ +import { Divider, type DividerProps } from "@mui/material"; + +export type GeneralDividerProps = DividerProps; + +function GeneralDivider(props: GeneralDividerProps) { + return ; +} + +export default GeneralDivider; diff --git a/app/src/components/atoms/feedback/GeneralSkeleton/index.tsx b/app/src/components/atoms/feedback/GeneralSkeleton/index.tsx new file mode 100644 index 0000000..1fe98d9 --- /dev/null +++ b/app/src/components/atoms/feedback/GeneralSkeleton/index.tsx @@ -0,0 +1,9 @@ +import { type SkeletonProps as MuiSkeletonProps, Skeleton } from "@mui/material"; + +export type GeneralSkeletonProps = MuiSkeletonProps; + +function GeneralSkeleton(props: GeneralSkeletonProps) { + return ; +} + +export default GeneralSkeleton; diff --git a/app/src/components/atoms/feedback/GeneralSpinner/index.tsx b/app/src/components/atoms/feedback/GeneralSpinner/index.tsx new file mode 100644 index 0000000..ccf7d45 --- /dev/null +++ b/app/src/components/atoms/feedback/GeneralSpinner/index.tsx @@ -0,0 +1,11 @@ +import { CircularProgress, type CircularProgressProps } from "@mui/material"; + +export interface GeneralSpinnerProps extends Omit { + size?: number; +} + +function GeneralSpinner({ size = 14, ...rest }: GeneralSpinnerProps) { + return ; +} + +export default GeneralSpinner; diff --git a/app/src/components/atoms/feedback/GeneralTooltip/index.tsx b/app/src/components/atoms/feedback/GeneralTooltip/index.tsx new file mode 100644 index 0000000..10d69e9 --- /dev/null +++ b/app/src/components/atoms/feedback/GeneralTooltip/index.tsx @@ -0,0 +1,48 @@ +import { type ComponentProps } from "react"; + +import { Tooltip, tooltipClasses } from "@mui/material"; +import { styled } from "@mui/material/styles"; + +/** + * App-wide tooltip styled to match the `bg-popover` surface from `src-old`: + * theme-bound background (light / dark / oled / glassy), border + small + * shadow, rounded-md radius, no MUI default dark-grey block. The slide+fade + * comes from MUI's built-in `Fade` transition (which honours user-side + * reduced-motion via the same CSS toggle we use elsewhere). + * + * Use this anywhere we previously reached for raw `@mui/material/Tooltip`. + * Same prop surface as the underlying component so it's a drop-in swap. + */ +type Props = ComponentProps; + +const Styled = styled(({ className, ...rest }: Props) => ( + +))(({ theme }) => ({ + [`& .${tooltipClasses.tooltip}`]: { + backgroundColor: theme.palette.surface.interface.base, + color: theme.palette.text.primary, + border: `1px solid ${theme.palette.divider}`, + borderRadius: 8, + padding: "6px 10px", + fontSize: 11.5, + fontWeight: 500, + lineHeight: 1.4, + boxShadow: + theme.palette.mode === "dark" + ? "0 8px 24px -12px rgba(0,0,0,0.7), 0 2px 6px -2px rgba(0,0,0,0.55)" + : "0 8px 24px -12px rgba(20,22,28,0.22), 0 2px 6px -2px rgba(20,22,28,0.10)", + // Tabular nums for any numerals that fall inside (commit counts, dates). + fontVariantNumeric: "tabular-nums", + maxWidth: 280, + }, +})); + +// No `enterDelay` — tooltips should appear immediately on hover, anywhere +// on the trigger. Arrow is intentionally absent (cleaner read at the small +// type sizes the dashboard uses; the placement+offset already communicates +// which element the tooltip belongs to). +function GeneralTooltip(props: Props) { + return ; +} + +export default GeneralTooltip; diff --git a/app/src/components/atoms/icons/BrandIcon/index.tsx b/app/src/components/atoms/icons/BrandIcon/index.tsx new file mode 100644 index 0000000..9534cea --- /dev/null +++ b/app/src/components/atoms/icons/BrandIcon/index.tsx @@ -0,0 +1,46 @@ +import { type SVGProps } from "react"; + +import { type SimpleIcon, siBitbucket, siGithub, siGitlab } from "simple-icons"; + +export type BrandSlug = "github" | "gitlab" | "bitbucket"; + +const BRAND_ICONS: Record = { + github: siGithub, + gitlab: siGitlab, + bitbucket: siBitbucket, +}; + +interface BrandIconProps extends Omit, "name"> { + slug: BrandSlug; + size?: number; + color?: "currentColor" | "brand" | string; + title?: string; +} + +function GeneralBrandIcon({ + slug, + size = 16, + color = "currentColor", + title, + ...rest +}: BrandIconProps) { + const icon = BRAND_ICONS[slug]; + const fill = color === "brand" ? `#${icon.hex}` : color; + return ( + + + + ); +} + +export default GeneralBrandIcon; diff --git a/app/src/components/atoms/icons/IdeIcon/index.tsx b/app/src/components/atoms/icons/IdeIcon/index.tsx new file mode 100644 index 0000000..575c698 --- /dev/null +++ b/app/src/components/atoms/icons/IdeIcon/index.tsx @@ -0,0 +1,84 @@ +import { type CSSProperties, type FC, type SVGProps } from "react"; + +import type { IdeId } from "@recrest/shared"; + +import { siCursor } from "simple-icons"; + +import IntellijIdeaLogo from "@/components/atoms/icons/IdeIcon/logos/intellij-idea.svg?react"; +import JetbrainsLogo from "@/components/atoms/icons/IdeIcon/logos/jetbrains.svg?react"; +import VSCodeLogo from "@/components/atoms/icons/IdeIcon/logos/visual-studio-code.svg?react"; +import WebstormLogo from "@/components/atoms/icons/IdeIcon/logos/webstorm.svg?react"; + +/** + * Official IDE logos inlined as React SVG components (via vite-plugin-svgr). + * No runtime CDN fetch — required for Tauri's strict CSP. Cursor stays inline + * from `simple-icons`. VS Code Insiders reuses the VS Code mark with a + * hue-rotate filter (their visual differentiation in marketing material). + */ +const LOGO_COMPONENT: Partial>>> = { + vscode: VSCodeLogo, + "vscode-insiders": VSCodeLogo, + webstorm: WebstormLogo, + idea: IntellijIdeaLogo, + "jetbrains-toolbox": JetbrainsLogo, +}; + +interface IdeIconProps { + id: IdeId; + size?: number; + /** `"brand"` keeps official colours, `"currentColor"` greys out for disabled rows. */ + color?: "brand" | "currentColor"; + title?: string; + style?: CSSProperties; +} + +function GeneralIdeIcon({ id, size = 16, color = "brand", title, style }: IdeIconProps) { + const mono = color === "currentColor"; + + if (id === "cursor") { + const fill = mono ? "currentColor" : `#${siCursor.hex}`; + return ( + + + + ); + } + + const LogoComponent = LOGO_COMPONENT[id]; + if (!LogoComponent) return null; + + const filterParts: string[] = []; + if (mono) filterParts.push("grayscale(1)"); + if (id === "vscode-insiders") filterParts.push("hue-rotate(140deg)", "saturate(0.9)"); + + const iconStyle: CSSProperties = { + flexShrink: 0, + ...(filterParts.length > 0 ? { filter: filterParts.join(" ") } : null), + ...(mono ? { opacity: 0.55 } : null), + ...style, + }; + + return ( + + ); +} + +export default GeneralIdeIcon; diff --git a/app/src/components/atoms/IdeIcon/logos/intellij-idea.svg b/app/src/components/atoms/icons/IdeIcon/logos/intellij-idea.svg similarity index 100% rename from app/src/components/atoms/IdeIcon/logos/intellij-idea.svg rename to app/src/components/atoms/icons/IdeIcon/logos/intellij-idea.svg diff --git a/app/src/components/atoms/IdeIcon/logos/jetbrains.svg b/app/src/components/atoms/icons/IdeIcon/logos/jetbrains.svg similarity index 100% rename from app/src/components/atoms/IdeIcon/logos/jetbrains.svg rename to app/src/components/atoms/icons/IdeIcon/logos/jetbrains.svg diff --git a/app/src/components/atoms/IdeIcon/logos/visual-studio-code.svg b/app/src/components/atoms/icons/IdeIcon/logos/visual-studio-code.svg similarity index 100% rename from app/src/components/atoms/IdeIcon/logos/visual-studio-code.svg rename to app/src/components/atoms/icons/IdeIcon/logos/visual-studio-code.svg diff --git a/app/src/components/atoms/IdeIcon/logos/webstorm.svg b/app/src/components/atoms/icons/IdeIcon/logos/webstorm.svg similarity index 100% rename from app/src/components/atoms/IdeIcon/logos/webstorm.svg rename to app/src/components/atoms/icons/IdeIcon/logos/webstorm.svg diff --git a/app/src/components/atoms/icons/ShellIcon/index.tsx b/app/src/components/atoms/icons/ShellIcon/index.tsx new file mode 100644 index 0000000..63649ea --- /dev/null +++ b/app/src/components/atoms/icons/ShellIcon/index.tsx @@ -0,0 +1,90 @@ +import { type CSSProperties, type FC, type SVGProps } from "react"; + +import type { ShellId } from "@recrest/shared"; + +import { Terminal as LucideTerminal } from "lucide-react"; +import { siFishshell, siGitforwindows, siGnubash, siNushell, siZsh } from "simple-icons"; + +import CmdLogo from "@/components/atoms/icons/ShellIcon/logos/cmd.svg?react"; +import PowershellCoreLogo from "@/components/atoms/icons/ShellIcon/logos/powershell-core.svg?react"; +import WindowsPowershellLogo from "@/components/atoms/icons/ShellIcon/logos/windows-powershell.svg?react"; +import WslLogo from "@/components/atoms/icons/ShellIcon/logos/wsl.svg?react"; + +/** + * Brand marks for every shell Recrest knows about. Same shape as + * `GeneralTerminalIcon`: vendored `.svg?react` for Microsoft / WSL marks + * (no simple-icons entry), `simple-icons` for everything else, Lucide + * terminal glyph as last-resort fallback for obscure shells. + */ + +type SimpleIcon = { hex: string; path: string; title: string }; + +const SI_MARK: Partial> = { + zsh: siZsh as SimpleIcon, + bash: siGnubash as SimpleIcon, + fish: siFishshell as SimpleIcon, + nu: siNushell as SimpleIcon, + "git-bash": siGitforwindows as SimpleIcon, +}; + +const VENDOR_LOGO: Partial>>> = { + "powershell-core": PowershellCoreLogo, + "windows-powershell": WindowsPowershellLogo, + cmd: CmdLogo, + wsl: WslLogo, +}; + +interface ShellIconProps { + id: ShellId; + size?: number; + color?: "brand" | "currentColor"; + title?: string; + style?: CSSProperties; +} + +function GeneralShellIcon({ id, size = 16, color = "brand", title, style }: ShellIconProps) { + const mono = color === "currentColor"; + const baseStyle: CSSProperties = { + flexShrink: 0, + ...(mono ? { opacity: 0.55 } : null), + ...style, + }; + + const VendorLogo = VENDOR_LOGO[id]; + if (VendorLogo) { + return ( + + ); + } + + const si = SI_MARK[id]; + if (si) { + const fill = mono ? "currentColor" : `#${si.hex}`; + return ( + + + + ); + } + + return ; +} + +export default GeneralShellIcon; diff --git a/app/src/components/atoms/icons/ShellIcon/logos/cmd.svg b/app/src/components/atoms/icons/ShellIcon/logos/cmd.svg new file mode 100644 index 0000000..40c3c93 --- /dev/null +++ b/app/src/components/atoms/icons/ShellIcon/logos/cmd.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/components/atoms/icons/ShellIcon/logos/powershell-core.svg b/app/src/components/atoms/icons/ShellIcon/logos/powershell-core.svg new file mode 100644 index 0000000..6df77c5 --- /dev/null +++ b/app/src/components/atoms/icons/ShellIcon/logos/powershell-core.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/components/atoms/icons/ShellIcon/logos/windows-powershell.svg b/app/src/components/atoms/icons/ShellIcon/logos/windows-powershell.svg new file mode 100644 index 0000000..37718dd --- /dev/null +++ b/app/src/components/atoms/icons/ShellIcon/logos/windows-powershell.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/components/atoms/icons/ShellIcon/logos/wsl.svg b/app/src/components/atoms/icons/ShellIcon/logos/wsl.svg new file mode 100644 index 0000000..9fce8e3 --- /dev/null +++ b/app/src/components/atoms/icons/ShellIcon/logos/wsl.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/components/atoms/icons/TerminalIcon/index.tsx b/app/src/components/atoms/icons/TerminalIcon/index.tsx new file mode 100644 index 0000000..3af7a64 --- /dev/null +++ b/app/src/components/atoms/icons/TerminalIcon/index.tsx @@ -0,0 +1,111 @@ +import { type CSSProperties, type FC, type SVGProps } from "react"; + +import type { TerminalId } from "@recrest/shared"; + +import { Terminal as LucideTerminal } from "lucide-react"; +import { + siAlacritty, + siGhostty, + siGnometerminal, + siHyper, + siIterm2, + siWarp, + siWezterm, +} from "simple-icons"; + +import AppleTerminalLogo from "@/components/atoms/icons/TerminalIcon/logos/apple-terminal.svg?react"; +import CmdLogo from "@/components/atoms/icons/TerminalIcon/logos/cmd.svg?react"; +import KittyLogo from "@/components/atoms/icons/TerminalIcon/logos/kitty.svg?react"; +import KonsoleLogo from "@/components/atoms/icons/TerminalIcon/logos/konsole.svg?react"; +import PowershellLogo from "@/components/atoms/icons/TerminalIcon/logos/powershell.svg?react"; +import TilixLogo from "@/components/atoms/icons/TerminalIcon/logos/tilix.svg?react"; +import WindowsTerminalLogo from "@/components/atoms/icons/TerminalIcon/logos/windows-terminal.svg?react"; +import XtermLogo from "@/components/atoms/icons/TerminalIcon/logos/xterm.svg?react"; + +/** + * Brand marks for every terminal emulator Recrest knows about. Mirrors the + * `GeneralIdeIcon` pattern: vendored `.svg?react` assets for marks not in + * simple-icons (Microsoft products, kitty, KDE, etc.), `simple-icons` for the + * rest. Falls back to a generic Lucide terminal glyph for anything obscure + * we haven't authored an asset for yet — this should never actually fire + * given the current `TERMINAL_IDS` set, but it keeps the component honest. + */ + +type SimpleIcon = { hex: string; path: string; title: string }; + +const SI_MARK: Partial> = { + iterm2: siIterm2 as SimpleIcon, + warp: siWarp as SimpleIcon, + wezterm: siWezterm as SimpleIcon, + alacritty: siAlacritty as SimpleIcon, + hyper: siHyper as SimpleIcon, + ghostty: siGhostty as SimpleIcon, + "gnome-terminal": siGnometerminal as SimpleIcon, +}; + +const VENDOR_LOGO: Partial>>> = { + "apple-terminal": AppleTerminalLogo, + "windows-terminal": WindowsTerminalLogo, + powershell: PowershellLogo, + cmd: CmdLogo, + kitty: KittyLogo, + konsole: KonsoleLogo, + xterm: XtermLogo, + tilix: TilixLogo, +}; + +interface TerminalIconProps { + id: TerminalId; + size?: number; + /** `"brand"` keeps official colours, `"currentColor"` greys for disabled rows. */ + color?: "brand" | "currentColor"; + title?: string; + style?: CSSProperties; +} + +function GeneralTerminalIcon({ id, size = 16, color = "brand", title, style }: TerminalIconProps) { + const mono = color === "currentColor"; + const baseStyle: CSSProperties = { + flexShrink: 0, + ...(mono ? { opacity: 0.55 } : null), + ...style, + }; + + const VendorLogo = VENDOR_LOGO[id]; + if (VendorLogo) { + return ( + + ); + } + + const si = SI_MARK[id]; + if (si) { + const fill = mono ? "currentColor" : `#${si.hex}`; + return ( + + + + ); + } + + return ; +} + +export default GeneralTerminalIcon; diff --git a/app/src/components/atoms/icons/TerminalIcon/logos/apple-terminal.svg b/app/src/components/atoms/icons/TerminalIcon/logos/apple-terminal.svg new file mode 100644 index 0000000..6095048 --- /dev/null +++ b/app/src/components/atoms/icons/TerminalIcon/logos/apple-terminal.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/components/atoms/icons/TerminalIcon/logos/cmd.svg b/app/src/components/atoms/icons/TerminalIcon/logos/cmd.svg new file mode 100644 index 0000000..40c3c93 --- /dev/null +++ b/app/src/components/atoms/icons/TerminalIcon/logos/cmd.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/components/atoms/icons/TerminalIcon/logos/kitty.svg b/app/src/components/atoms/icons/TerminalIcon/logos/kitty.svg new file mode 100644 index 0000000..cebcd7c --- /dev/null +++ b/app/src/components/atoms/icons/TerminalIcon/logos/kitty.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/components/atoms/icons/TerminalIcon/logos/konsole.svg b/app/src/components/atoms/icons/TerminalIcon/logos/konsole.svg new file mode 100644 index 0000000..e43f21c --- /dev/null +++ b/app/src/components/atoms/icons/TerminalIcon/logos/konsole.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/components/atoms/icons/TerminalIcon/logos/powershell.svg b/app/src/components/atoms/icons/TerminalIcon/logos/powershell.svg new file mode 100644 index 0000000..6b34c32 --- /dev/null +++ b/app/src/components/atoms/icons/TerminalIcon/logos/powershell.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/components/atoms/icons/TerminalIcon/logos/tilix.svg b/app/src/components/atoms/icons/TerminalIcon/logos/tilix.svg new file mode 100644 index 0000000..dad7bc7 --- /dev/null +++ b/app/src/components/atoms/icons/TerminalIcon/logos/tilix.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/components/atoms/icons/TerminalIcon/logos/windows-terminal.svg b/app/src/components/atoms/icons/TerminalIcon/logos/windows-terminal.svg new file mode 100644 index 0000000..b3e0ab5 --- /dev/null +++ b/app/src/components/atoms/icons/TerminalIcon/logos/windows-terminal.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/components/atoms/icons/TerminalIcon/logos/xterm.svg b/app/src/components/atoms/icons/TerminalIcon/logos/xterm.svg new file mode 100644 index 0000000..33c0322 --- /dev/null +++ b/app/src/components/atoms/icons/TerminalIcon/logos/xterm.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/components/atoms/inputs/GeneralCheckboxInput/index.tsx b/app/src/components/atoms/inputs/GeneralCheckboxInput/index.tsx new file mode 100644 index 0000000..da7ae2b --- /dev/null +++ b/app/src/components/atoms/inputs/GeneralCheckboxInput/index.tsx @@ -0,0 +1,25 @@ +import { forwardRef } from "react"; + +import { Checkbox, type CheckboxProps as MuiCheckboxProps } from "@mui/material"; + +export interface GeneralCheckboxInputProps extends Omit { + onCheckedChange?: (checked: boolean) => void; + onChange?: MuiCheckboxProps["onChange"]; +} + +const GeneralCheckboxInput = forwardRef( + function GeneralCheckboxInput({ onCheckedChange, onChange, ...rest }, ref) { + return ( + { + onChange?.(e, checked); + onCheckedChange?.(checked); + }} + {...rest} + /> + ); + }, +); + +export default GeneralCheckboxInput; diff --git a/app/src/components/atoms/inputs/GeneralInput/index.tsx b/app/src/components/atoms/inputs/GeneralInput/index.tsx new file mode 100644 index 0000000..c1a7657 --- /dev/null +++ b/app/src/components/atoms/inputs/GeneralInput/index.tsx @@ -0,0 +1,14 @@ +import { forwardRef } from "react"; + +import { TextField, type TextFieldProps } from "@mui/material"; + +export type GeneralInputProps = Omit; + +const GeneralInput = forwardRef(function GeneralInput( + { size = "small", fullWidth = true, ...rest }, + ref, +) { + return ; +}); + +export default GeneralInput; diff --git a/app/src/components/atoms/inputs/GeneralLabel/index.tsx b/app/src/components/atoms/inputs/GeneralLabel/index.tsx new file mode 100644 index 0000000..40ae2df --- /dev/null +++ b/app/src/components/atoms/inputs/GeneralLabel/index.tsx @@ -0,0 +1,13 @@ +import { forwardRef } from "react"; + +import { FormLabel, type FormLabelProps } from "@mui/material"; + +export type GeneralLabelProps = FormLabelProps; + +const GeneralLabel = forwardRef( + function GeneralLabel(props, ref) { + return ; + }, +); + +export default GeneralLabel; diff --git a/app/src/components/atoms/inputs/GeneralSwitchInput/index.tsx b/app/src/components/atoms/inputs/GeneralSwitchInput/index.tsx new file mode 100644 index 0000000..d34f607 --- /dev/null +++ b/app/src/components/atoms/inputs/GeneralSwitchInput/index.tsx @@ -0,0 +1,80 @@ +import { forwardRef } from "react"; + +import { type SwitchProps as MuiSwitchProps, Switch } from "@mui/material"; +import { styled } from "@mui/material/styles"; + +export interface GeneralSwitchInputProps extends Omit { + onCheckedChange?: (checked: boolean) => void; + onChange?: MuiSwitchProps["onChange"]; +} + +/** + * Apple-style switch in monochrome — the Apple shape (wide rounded track, + * white thumb with a soft drop shadow, springy thumb travel) but recoloured + * to a black/white palette per Recrest's design language. Light theme: off = + * pale-gray track, on = near-black track. Dark theme inverts to white/charcoal. + * Pre-MUI src-old used the same monochrome treatment — restoring that here. + */ +const StyledSwitch = styled(Switch)(({ theme }) => ({ + width: 42, + height: 26, + padding: 0, + + "& .MuiSwitch-switchBase": { + padding: 0, + margin: 2, + transitionDuration: "260ms", + "&.Mui-checked": { + transform: "translateX(16px)", + color: theme.palette.mode === "dark" ? "#0f1115" : "#ffffff", + "& + .MuiSwitch-track": { + backgroundColor: theme.palette.mode === "dark" ? "#ffffff" : "#0f1115", + opacity: 1, + border: 0, + }, + "&.Mui-disabled + .MuiSwitch-track": { + opacity: 0.5, + }, + }, + "&.Mui-focusVisible .MuiSwitch-thumb": { + color: theme.palette.mode === "dark" ? "#ffffff" : "#0f1115", + border: `6px solid ${theme.palette.background.paper}`, + }, + "&.Mui-disabled .MuiSwitch-thumb": { + color: theme.palette.grey[100], + }, + "&.Mui-disabled + .MuiSwitch-track": { + opacity: 0.7, + }, + }, + "& .MuiSwitch-thumb": { + boxSizing: "border-box", + width: 22, + height: 22, + boxShadow: "0 3px 1px rgba(0,0,0,0.06), 0 3px 8px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.04)", + }, + "& .MuiSwitch-track": { + borderRadius: 26 / 2, + backgroundColor: theme.palette.mode === "dark" ? "#39393D" : "#E9E9EA", + opacity: 1, + transition: theme.transitions.create(["background-color"], { duration: 260 }), + }, +})); + +const GeneralSwitchInput = forwardRef( + function GeneralSwitchInput({ onCheckedChange, onChange, ...rest }, ref) { + return ( + { + onChange?.(e, checked); + onCheckedChange?.(checked); + }} + {...rest} + /> + ); + }, +); + +export default GeneralSwitchInput; diff --git a/app/src/components/atoms/placeholders/ComingSoonPlaceholder/index.tsx b/app/src/components/atoms/placeholders/ComingSoonPlaceholder/index.tsx new file mode 100644 index 0000000..7c8dd79 --- /dev/null +++ b/app/src/components/atoms/placeholders/ComingSoonPlaceholder/index.tsx @@ -0,0 +1,26 @@ +import { Box, Typography } from "@mui/material"; +import { styled } from "@mui/material/styles"; + +export interface ComingSoonPlaceholderProps { + title: string; + phase?: string; +} + +const Root = styled(Box)(({ theme }) => ({ + padding: theme.spacing(4), +})); + +function ComingSoonPlaceholder({ title, phase = "Phase 4" }: ComingSoonPlaceholderProps) { + return ( + + + {title} + + + Coming soon — {phase} + + + ); +} + +export default ComingSoonPlaceholder; diff --git a/app/src/components/atoms/transitions/PageTransition/index.tsx b/app/src/components/atoms/transitions/PageTransition/index.tsx new file mode 100644 index 0000000..4c1247f --- /dev/null +++ b/app/src/components/atoms/transitions/PageTransition/index.tsx @@ -0,0 +1,84 @@ +import { type ReactNode, useLayoutEffect, useState } from "react"; + +import { Box } from "@mui/material"; +import { styled } from "@mui/material/styles"; + +import { useReducedMotion } from "@/hooks/useReducedMotion"; + +/** + * Page-level enter animation. Fades + translates up by 6px over 200ms; honours + * `useReducedMotion` (= explicit Settings toggle ∪ OS media query) by skipping + * the animation entirely and showing the page in its final state. Single + * tween, no library — emotion `styled()` + a one-shot `data-entered` flag. + * + * The animation fires on every mount, which is the natural unit for + * react-router page swaps. Children that animate further internally (skeleton + * → real content) handle that separately; PageTransition only owns the + * page-shell enter. + */ +interface PageTransitionProps { + children: ReactNode; + className?: string; + /** Delay before the enter animation starts. Tiny defaults (0–60ms) help + * pages that mount inside an outer transition feel smoother. */ + delay?: number; +} + +const Root = styled(Box, { + shouldForwardProp: (p) => p !== "entered" && p !== "skipAnimation" && p !== "delay", +})<{ entered: boolean; skipAnimation: boolean; delay: number }>( + ({ entered, skipAnimation, delay }) => ({ + // `height: 100%` (not `minHeight`) so PageTransition exactly fills its + // scroll parent — Settings' 2-pane layout reads `height: 100%` off this + // element and needs a deterministic anchor. Pages that need to scroll + // (Activity, Branches, Repos) own their own internal scroller and let + // the outer ContentScroll stay quiet — see each page's Root styling. + height: "100%", + minHeight: 0, + width: "100%", + minWidth: 0, + display: "flex", + flexDirection: "column", + opacity: skipAnimation ? 1 : entered ? 1 : 0, + transform: skipAnimation + ? "none" + : entered + ? "translate3d(0, 0, 0)" + : "translate3d(0, 10px, 0)", + // Slightly longer + softer than the previous 200/220ms — the old timing + // was so short the animation read as a hard pop. 320ms with an + // ease-out-back-ish curve gives the page time to settle visibly. + transition: skipAnimation + ? "none" + : `opacity 320ms cubic-bezier(0.2, 0.8, 0.2, 1) ${delay}ms, transform 360ms cubic-bezier(0.2, 0.8, 0.2, 1) ${delay}ms`, + willChange: skipAnimation ? "auto" : "opacity, transform", + }), +); + +function PageTransition({ children, className, delay = 0 }: PageTransitionProps) { + const reduced = useReducedMotion(); + const [entered, setEntered] = useState(false); + + // useLayoutEffect so the initial paint always happens with opacity:0 → + // first commit + transition kicks in on the next frame. useEffect would + // race the browser and occasionally let the destination state slip through. + useLayoutEffect(() => { + if (reduced) return; + const id = requestAnimationFrame(() => setEntered(true)); + return () => cancelAnimationFrame(id); + }, [reduced]); + + return ( + + {children} + + ); +} + +export default PageTransition; diff --git a/app/src/components/atoms/transitions/StaggeredReveal/index.tsx b/app/src/components/atoms/transitions/StaggeredReveal/index.tsx new file mode 100644 index 0000000..0c77c23 --- /dev/null +++ b/app/src/components/atoms/transitions/StaggeredReveal/index.tsx @@ -0,0 +1,100 @@ +import { Children, type ReactNode, isValidElement, useLayoutEffect, useState } from "react"; + +import { Box } from "@mui/material"; +import { styled } from "@mui/material/styles"; + +import { useReducedMotion } from "@/hooks/useReducedMotion"; + +/** + * Wrap a list of sibling elements and animate them in with a per-child + * stagger. Each child gets a `transition-delay` of `step * index` ms. Same + * 6px translate + fade as `PageTransition`, so a page that uses both reads + * as one continuous reveal. + * + * For very long lists (>= 12 items) we cap the stagger budget so the last + * card doesn't take a full second to appear — sub-frame staggers feel + * cheap, but you don't want the user staring at a half-loaded grid. + */ +interface StaggeredRevealProps { + children: ReactNode; + /** ms between consecutive children. Defaults to 40ms. */ + step?: number; + /** Hard cap on the cumulative delay. Defaults to 240ms (≈6 children). */ + maxDelay?: number; + className?: string; + /** Element used as the flex/grid container. Defaults to a plain Box. */ + component?: React.ElementType; +} + +interface ItemProps { + entered: boolean; + skipAnimation: boolean; + delay: number; +} + +// Important: `display: contents` would skip the wrapper from layout entirely +// (cleanest for grid parents that want their grandchildren to be tracks), but +// then `opacity`/`transform` no longer apply — block-level containers must +// own a generated box for transforms to take effect. So we keep the wrapper +// as a real flex/grid child and explicitly stretch it so children like +// ` - ); -} diff --git a/app/src/components/molecules/DetailSection/index.tsx b/app/src/components/molecules/DetailSection/index.tsx deleted file mode 100644 index 6b257f8..0000000 --- a/app/src/components/molecules/DetailSection/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { type ReactNode, useState } from "react"; - -import { Icon } from "@/components/atoms/Icon"; - -interface DetailSectionProps { - title: string; - meta?: ReactNode; - children: ReactNode; - defaultOpen?: boolean; -} - -export function DetailSection({ title, meta, children, defaultOpen = true }: DetailSectionProps) { - const [open, setOpen] = useState(defaultOpen); - return ( -
-
- - {meta != null && {meta}} -
- {open &&
{children}
} -
- ); -} diff --git a/app/src/components/molecules/Drawer/index.tsx b/app/src/components/molecules/Drawer/index.tsx deleted file mode 100644 index 4ba6e0d..0000000 --- a/app/src/components/molecules/Drawer/index.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { type ReactNode, useEffect, useRef, useState } from "react"; - -import { useDrawerSwipe } from "@/hooks/useDrawerSwipe"; - -/** - * Shared right-side panel envelope used by `MergeRequestsPage`'s MR drawer - * and (going forward) the repo detail PR drawer. The Phase 0.2 prop surface - * is a discriminated union — callers pass either `children` for free-form - * content or `tabs` for a tabbed shell. `tabs` is reserved for Plan 3 §C.5 - * ("Files Changed" tab) and not exercised yet; the type is locked in now so - * Plan 3 can add it without breaking callers. - */ -export type DrawerTab = { - id: string; - label: string; - content: ReactNode; -}; - -type DrawerCommonProps = { - open: boolean; - side?: "right" | "left"; - size?: "sm" | "md" | "lg" | string; - onClose: () => void; - header?: ReactNode; - footer?: ReactNode; - className?: string; - /** Optional `data-testid` so callers can target the outer aside in tests. */ - testId?: string; - /** - * When true, a transparent backdrop is rendered behind the drawer; clicking - * it dismisses the drawer (Plan 1 §A.1). Defaults to `true` for the - * floating overlay variant and to `false` for the inline variant - * (className contains `a-drawer-inline`) — inline drawers share their - * column with surrounding chrome and a click-outside dismissal would - * conflict with the host page. Pass an explicit value to override. - */ - dismissOnBackdrop?: boolean; -}; - -type DrawerChildrenProps = DrawerCommonProps & { - children: ReactNode; - tabs?: never; - defaultTabId?: never; - onTabChange?: never; -}; - -type DrawerTabsProps = DrawerCommonProps & { - tabs: DrawerTab[]; - defaultTabId?: string; - onTabChange?: (id: string) => void; - children?: never; -}; - -export type DrawerProps = DrawerChildrenProps | DrawerTabsProps; - -function widthFor(size: DrawerProps["size"]): string | undefined { - if (!size || size === "md") return undefined; - if (size === "sm") return "300px"; - if (size === "lg") return "440px"; - return size; -} - -export function Drawer(props: DrawerProps) { - const { - open, - side = "right", - size, - onClose, - header, - footer, - className, - testId, - dismissOnBackdrop, - } = props; - const asideRef = useRef(null); - - // Inline drawers ride inside the host grid (`a-drawer-inline` removes the - // fixed-overlay positioning), so a backdrop spanning the viewport would - // dismiss the drawer on completely unrelated clicks. Default the inline - // variant to opt-out and the floating variant to opt-in; callers can - // still override via the explicit prop. - const isInline = (className ?? "").includes("a-drawer-inline"); - const backdropEnabled = dismissOnBackdrop ?? !isInline; - - // ESC closes the drawer. Bound only while the drawer is open so we don't - // intercept Escape elsewhere. - useEffect(() => { - if (!open) return; - const onKey = (e: KeyboardEvent) => { - if (e.key === "Escape") onClose(); - }; - window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); - }, [open, onClose]); - - // D.5: swipe-to-dismiss on touch devices. Right-side drawers close on - // a rightward swipe (toward the screen edge); left-side mirrors that. - useDrawerSwipe({ - ref: asideRef, - onClose, - enabled: open, - direction: side === "left" ? "left" : "right", - }); - - if (!open) return null; - - const widthOverride = widthFor(size); - - const sideClass = side === "left" ? "a-drawer-side-left" : "a-drawer-side-right"; - - return ( - <> - {backdropEnabled && ( -