diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ca41cd3 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +# Ignore artifacts: +build +coverage +.gitkeep \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..bffbb1a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,14 @@ +{ + "bracketSpacing": true, + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxSingleQuote": false, + "printWidth": 120, + "proseWrap": "always", + "quoteProps": "as-needed", + "semi": true, + "singleQuote": false, + "tabWidth": 4, + "trailingComma": "all", + "useTabs": false +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7747450..9ec2c31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,6 @@ "react-router": "^7.13.0" }, "devDependencies": { - "@babel/core": "^7.29.0", "@chromatic-com/storybook": "^5.0.1", "@eslint/js": "^9.39.2", "@storybook/addon-a11y": "^10.2.8", @@ -46,6 +45,7 @@ "eslint": "^9.39.2", "eslint-plugin-react-compiler": "^19.1.0-rc.2", "playwright": "^1.58.2", + "prettier": "3.8.4", "storybook": "^10.2.8", "typescript": "~5.8.3", "typescript-eslint": "^8.55.0", @@ -6556,6 +6556,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", diff --git a/package.json b/package.json index 6a511a9..fadeb12 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,9 @@ "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "lint": "eslint src/", + "lint:fix": "eslint src/ --fix", + "prettier:check": "prettier --check src/**/*", + "prettier:write": "prettier --write src/**/*", "check:react-compiler": "node tools/check-react-compiler.mjs" }, "dependencies": { @@ -56,6 +59,7 @@ "eslint": "^9.39.2", "eslint-plugin-react-compiler": "^19.1.0-rc.2", "playwright": "^1.58.2", + "prettier": "3.8.4", "storybook": "^10.2.8", "typescript": "~5.8.3", "typescript-eslint": "^8.55.0", diff --git a/src/App.tsx b/src/App.tsx index 2a08058..99ac3e3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,37 +29,53 @@ function App() { } /> {/* Main launcher window */} - - - - - - - - - - - - - }> - } /> - } /> - } /> - } /> - } /> - - - - - - - - - - - - } /> + + + + + + + + + + + + + }> + } /> + } + /> + } /> + } + /> + } + /> + + + + + + + + + + + + } + /> ); diff --git a/src/components/atoms/news/ChangeEntry.stories.tsx b/src/components/atoms/news/ChangeEntry.stories.tsx index 13a8aae..9f36a1b 100644 --- a/src/components/atoms/news/ChangeEntry.stories.tsx +++ b/src/components/atoms/news/ChangeEntry.stories.tsx @@ -1,9 +1,9 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { Meta, StoryObj } from "@storybook/react-vite"; -import ChangeEntry from './ChangeEntry'; +import ChangeEntry from "./ChangeEntry"; const meta = { - component: ChangeEntry, + component: ChangeEntry, } satisfies Meta; export default meta; @@ -11,32 +11,32 @@ export default meta; type Story = StoryObj; export const New: Story = { - "args": { - "description": "Added something super awesome!", - "author": "John Doe", - "type": "NEW" - } + args: { + description: "Added something super awesome!", + author: "John Doe", + type: "NEW", + }, }; export const Fix: Story = { - "args": { - "description": "Fixed a bug where the application would crash on startup.", - "author": "John Doe", - "type": "FIX" - } + args: { + description: "Fixed a bug where the application would crash on startup.", + author: "John Doe", + type: "FIX", + }, }; export const Improvement: Story = { - "args": { - "description": "Improved performance of the application.", - "author": "John Doe", - "type": "IMPROVEMENT" - } + args: { + description: "Improved performance of the application.", + author: "John Doe", + type: "IMPROVEMENT", + }, }; export const Balance: Story = { - "args": { - "description": "Balanced gameplay mechanics.", - "author": "John Doe", - "type": "BALANCE" - } -}; \ No newline at end of file + args: { + description: "Balanced gameplay mechanics.", + author: "John Doe", + type: "BALANCE", + }, +}; diff --git a/src/components/atoms/news/ChangeEntry.tsx b/src/components/atoms/news/ChangeEntry.tsx index e693fc5..97c9586 100644 --- a/src/components/atoms/news/ChangeEntry.tsx +++ b/src/components/atoms/news/ChangeEntry.tsx @@ -4,7 +4,6 @@ import { AutoAwesome, Balance, Build, QuestionMark, RocketLaunch } from "@mui/ic type ChangeTypeContrained = "FIX" | "NEW" | "IMPROVEMENT" | "BALANCE"; - export default function ChangeEntry(props: Change) { const { description, author, type } = props; const changeType = type as ChangeTypeContrained; @@ -31,10 +30,14 @@ export default function ChangeEntry(props: Change) { {getIconByType()} - {description} - By {author} + + {description} + + + By {author} + - + ); -} \ No newline at end of file +} diff --git a/src/components/atoms/preferences/PreferenceFieldLabel.stories.tsx b/src/components/atoms/preferences/PreferenceFieldLabel.stories.tsx index cac027d..ae91ee4 100644 --- a/src/components/atoms/preferences/PreferenceFieldLabel.stories.tsx +++ b/src/components/atoms/preferences/PreferenceFieldLabel.stories.tsx @@ -33,4 +33,3 @@ export const WithoutTooltip: Story = { label: "Installation path", }, }; - diff --git a/src/components/atoms/servers/ServerFact.tsx b/src/components/atoms/servers/ServerFact.tsx index 6c8e453..64ecc0b 100644 --- a/src/components/atoms/servers/ServerFact.tsx +++ b/src/components/atoms/servers/ServerFact.tsx @@ -10,15 +10,8 @@ export default function ServerFact(props: ServerFactProps) { const { icon, value } = props; return ( - - - {icon} - + + {icon} {value} diff --git a/src/components/atoms/update/VersionBadge.tsx b/src/components/atoms/update/VersionBadge.tsx index d40b869..e2ad610 100644 --- a/src/components/atoms/update/VersionBadge.tsx +++ b/src/components/atoms/update/VersionBadge.tsx @@ -17,10 +17,7 @@ export default function VersionBadge(props: VersionBadgeProps) { Version - + {currentVersion} ; export default meta; type Story = StoryObj; -export const Default: Story = {}; \ No newline at end of file +export const Default: Story = {}; diff --git a/src/components/demo/ThemeDemoLayout.tsx b/src/components/demo/ThemeDemoLayout.tsx index 9f3b692..224acd1 100644 --- a/src/components/demo/ThemeDemoLayout.tsx +++ b/src/components/demo/ThemeDemoLayout.tsx @@ -56,7 +56,6 @@ export default function ThemeDemoLayout() { const [snackbarOpen, setSnackbarOpen] = useState(false); const [tabValue, setTabValue] = useState(0); - return ( @@ -78,7 +77,9 @@ export default function ThemeDemoLayout() { Title Small Body Large Body Medium — The pudu is the world's smallest deer. - Body Small — Native to South American temperate rainforests. + + Body Small — Native to South American temperate rainforests. + Body Extra Small — They stand only 32–44 cm tall. @@ -96,27 +97,57 @@ export default function ThemeDemoLayout() { Soft - Primary - Neutral - Danger - Success - Warning + + Primary + + + Neutral + + + Danger + + + Success + + + Warning + Outlined - Primary - Neutral - Danger - Success - Warning + + Primary + + + Neutral + + + Danger + + + Success + + + Warning + Plain - Primary - Neutral - Danger - Success - Warning + + Primary + + + Neutral + + + Danger + + + Success + + + Warning + Sizes & States @@ -132,11 +163,21 @@ export default function ThemeDemoLayout() { {/* Icon Buttons */} - P - S - W - D - N + + P + + + S + + + W + + + D + + + N + @@ -217,11 +258,7 @@ export default function ThemeDemoLayout() { Cuteness Level: {sliderValue}% - setSliderValue(v as number)} - color="primary" - /> + setSliderValue(v as number)} color="primary" /> @@ -238,10 +275,18 @@ export default function ThemeDemoLayout() { Warning - Soft Primary - Soft Success - Outlined - Outlined + + Soft Primary + + + Soft Success + + + Outlined + + + Outlined + @@ -256,7 +301,9 @@ export default function ThemeDemoLayout() { DU - OK + + OK + A @@ -275,8 +322,12 @@ export default function ThemeDemoLayout() { Success — the pudu population is growing! Warning — habitat fragmentation detected. Danger — poachers reported in the area. - Soft variant — a gentle reminder about pudus. - Outlined variant — conservation efforts are working. + + Soft variant — a gentle reminder about pudus. + + + Outlined variant — conservation efforts are working. + @@ -287,8 +338,8 @@ export default function ThemeDemoLayout() { Southern Pudu - Pudu puda — found in Chile and Argentina. The smallest deer in the world, - standing at just 35–45 cm tall. + Pudu puda — found in Chile and Argentina. The smallest deer in the world, standing at + just 35–45 cm tall. @@ -299,8 +350,8 @@ export default function ThemeDemoLayout() { Northern Pudu - Pudu mephistophiles — found in Colombia, Ecuador, and Peru. - Even smaller than its southern cousin. + Pudu mephistophiles — found in Colombia, Ecuador, and Peru. Even smaller than its + southern cousin. @@ -308,8 +359,8 @@ export default function ThemeDemoLayout() { Conservation - Protected areas and breeding programs help ensure the survival - of these adorable creatures. + Protected areas and breeding programs help ensure the survival of these adorable + creatures. @@ -327,7 +378,12 @@ export default function ThemeDemoLayout() { Soft Sheet A tinted container - + Solid Primary Bold and warm @@ -354,7 +410,8 @@ export default function ThemeDemoLayout() { - Pudus eat leaves, bark, seeds, and fallen fruit. They can stand on hind legs to reach branches. + Pudus eat leaves, bark, seeds, and fallen fruit. They can stand on hind legs to reach + branches. @@ -366,22 +423,22 @@ export default function ThemeDemoLayout() { What is a pudu? - The pudu is a genus of South American deer. Two species are known: the northern pudu - and the southern pudu. They are the world's smallest deer. + The pudu is a genus of South American deer. Two species are known: the northern pudu and the + southern pudu. They are the world's smallest deer. How big do they get? - Southern pudus stand 35–45 cm at the shoulder and weigh 6.4–13.4 kg. - Northern pudus are even smaller. + Southern pudus stand 35–45 cm at the shoulder and weigh 6.4–13.4 kg. Northern pudus are even + smaller. Are they endangered? - The southern pudu is classified as Near Threatened, while the northern pudu - is Vulnerable due to habitat loss and hunting. + The southern pudu is classified as Near Threatened, while the northern pudu is Vulnerable + due to habitat loss and hunting. @@ -404,25 +461,41 @@ export default function ThemeDemoLayout() { Southern Pudu Chile, Argentina 35–45 cm - Near Threatened + + + Near Threatened + + Northern Pudu Colombia, Ecuador, Peru 32–35 cm - Vulnerable + + + Vulnerable + + White-tailed Deer Americas 53–120 cm - Least Concern + + + Least Concern + + Moose North America, Europe 140–210 cm - Least Concern + + + Least Concern + + @@ -436,7 +509,9 @@ export default function ThemeDemoLayout() { - F + + F + Forest Walk @@ -447,7 +522,9 @@ export default function ThemeDemoLayout() { - C + + C + Conservation @@ -458,7 +535,9 @@ export default function ThemeDemoLayout() { - P + + P + Photo Gallery @@ -473,15 +552,27 @@ export default function ThemeDemoLayout() { {/* Breadcrumbs & Links */} - Home - Animals - Cervidae + + Home + + + Animals + + + Cervidae + Pudu - Primary Link - Neutral Link - Danger Link + + Primary Link + + + Neutral Link + + + Danger Link + @@ -522,8 +613,17 @@ export default function ThemeDemoLayout() { - - 16:9 + + + 16:9 + @@ -534,8 +634,17 @@ export default function ThemeDemoLayout() { - - 1:1 + + + 1:1 + @@ -566,7 +675,13 @@ export default function ThemeDemoLayout() { - } sx={{ height: 40 }}> + } + sx={{ height: 40 }} + > Section A Section B Section C @@ -580,7 +695,9 @@ export default function ThemeDemoLayout() { {(["primary", "neutral", "danger", "success", "warning"] as const).map((color) => ( - {color} + + {color} + {[50, 100, 200, 300, 400, 500, 600, 700, 800, 900].map((shade) => ( @@ -616,5 +733,5 @@ export default function ThemeDemoLayout() { - ) -} \ No newline at end of file + ); +} diff --git a/src/components/layouts/InstallationsLayout.tsx b/src/components/layouts/InstallationsLayout.tsx index b212bc9..180340a 100644 --- a/src/components/layouts/InstallationsLayout.tsx +++ b/src/components/layouts/InstallationsLayout.tsx @@ -11,9 +11,7 @@ export default function InstallationsLayout() { - - Installations - + Installations Manage your local game installations @@ -28,16 +26,10 @@ export default function InstallationsLayout() { > Installations Folder - } - onClick={openRegistry} - > + } onClick={openRegistry}> Download Builds - diff --git a/src/components/layouts/NewsLayout.tsx b/src/components/layouts/NewsLayout.tsx index 20f97c7..106b204 100644 --- a/src/components/layouts/NewsLayout.tsx +++ b/src/components/layouts/NewsLayout.tsx @@ -52,9 +52,7 @@ export default function NewsLayout() { {showEmpty && ( - - No news is currently available. - + No news is currently available. )} @@ -108,11 +106,7 @@ export default function NewsLayout() { - + ); } diff --git a/src/components/layouts/PreferencesLayout.tsx b/src/components/layouts/PreferencesLayout.tsx index 979d76b..afa728b 100644 --- a/src/components/layouts/PreferencesLayout.tsx +++ b/src/components/layouts/PreferencesLayout.tsx @@ -1,7 +1,11 @@ import { Box, CircularProgress, Stack, Typography } from "@mui/joy"; import { preferencesSchema } from "../../pudu/generated"; import { usePreferencesContext } from "../../contextProviders/PreferencesContextProvider"; -import { themeIdToPreferenceValue, themePreferenceValueToThemeId, useThemeContext } from "../../contextProviders/ThemeProvider"; +import { + themeIdToPreferenceValue, + themePreferenceValueToThemeId, + useThemeContext, +} from "../../contextProviders/ThemeProvider"; import PreferenceCategory from "../molecules/preferences/PreferenceCategory"; export default function PreferencesLayout() { @@ -30,11 +34,9 @@ export default function PreferencesLayout() { return ( - + - - Preferences - + Preferences {isSaving ? "Saving..." : "Changes are saved automatically"} @@ -49,15 +51,16 @@ export default function PreferencesLayout() { )} - {preferences && preferencesSchema.map((category) => ( - - ))} + {preferences && + preferencesSchema.map((category) => ( + + ))} ); diff --git a/src/components/layouts/ServersLayout.tsx b/src/components/layouts/ServersLayout.tsx index b360127..f1534ca 100644 --- a/src/components/layouts/ServersLayout.tsx +++ b/src/components/layouts/ServersLayout.tsx @@ -8,9 +8,7 @@ export default function ServersLayout() { return ( - - Servers - + Servers {lastUpdatedLabel} @@ -21,26 +19,19 @@ export default function ServersLayout() { - - Refreshing server list... - + Refreshing server list... )} {isEmpty && ( - - No servers are currently available. - + No servers are currently available. )} {cards.map((card) => ( - + ))} diff --git a/src/components/layouts/SideBarLayout.tsx b/src/components/layouts/SideBarLayout.tsx index 3ba369d..cc1fec7 100644 --- a/src/components/layouts/SideBarLayout.tsx +++ b/src/components/layouts/SideBarLayout.tsx @@ -4,21 +4,22 @@ import { Outlet } from "react-router"; import { SideBarContextProvider } from "../../contextProviders/SideBarContextProvider"; export default function SideBarLayout() { - return ( - + - + - ) + ); } diff --git a/src/components/layouts/UpdateLayout.stories.tsx b/src/components/layouts/UpdateLayout.stories.tsx index 1369c71..027033c 100644 --- a/src/components/layouts/UpdateLayout.stories.tsx +++ b/src/components/layouts/UpdateLayout.stories.tsx @@ -2,7 +2,8 @@ import { Box } from "@mui/joy"; import type { Meta, StoryObj } from "@storybook/react-vite"; import UpdateLayout from "./UpdateLayout"; -const sampleReleaseNotes = "It is here! Feature parity with the old stationhub launcher, but with many extra goodies!\r\n\r\n## What's Changed\r\n* feat: first release of pudulauncher, now with 0% bugs by @corp-0 in [7](https://github.com/corp-0/PuduLauncher/pull/7)\r\n* feat: new HonkTTS and HonkTTS installer. Visually install the TTS server that powers immersive voices in Unitystation.\r\n* fix: TTS installation is no longer a side-effect of downloading a game build. You can manage your installation however you want.\r\n* feat: new onboarding system to have a friendly pudu guide you through new features and things that require your attention.\r\n* feat: new Discord rich presence functionality. If you feel like sharing, your friends on Discord can now see if you are playing and on what server.\r\n\r\n\r\n**Full Changelog**: [Here](https://github.com/corp-0/PuduLauncher/compare/v0.1.1...v1.0.0)"; +const sampleReleaseNotes = + "It is here! Feature parity with the old stationhub launcher, but with many extra goodies!\r\n\r\n## What's Changed\r\n* feat: first release of pudulauncher, now with 0% bugs by @corp-0 in [7](https://github.com/corp-0/PuduLauncher/pull/7)\r\n* feat: new HonkTTS and HonkTTS installer. Visually install the TTS server that powers immersive voices in Unitystation.\r\n* fix: TTS installation is no longer a side-effect of downloading a game build. You can manage your installation however you want.\r\n* feat: new onboarding system to have a friendly pudu guide you through new features and things that require your attention.\r\n* feat: new Discord rich presence functionality. If you feel like sharing, your friends on Discord can now see if you are playing and on what server.\r\n\r\n\r\n**Full Changelog**: [Here](https://github.com/corp-0/PuduLauncher/compare/v0.1.1...v1.0.0)"; const meta = { title: "Layouts/UpdateLayout", diff --git a/src/components/layouts/UpdateLayout.tsx b/src/components/layouts/UpdateLayout.tsx index 4400605..d8ceb95 100644 --- a/src/components/layouts/UpdateLayout.tsx +++ b/src/components/layouts/UpdateLayout.tsx @@ -58,42 +58,52 @@ export default function UpdateLayout(props: UpdateLayoutProps) { const isBusy = isDownloading || isInstalling; return ( - - - - - + + inset: 0, + background: + "radial-gradient(ellipse 80% 60% at 30% 80%, rgba(var(--joy-palette-primary-mainChannel) / 0.08) 0%, transparent 70%), radial-gradient(ellipse 60% 50% at 80% 20%, rgba(var(--joy-palette-primary-mainChannel) / 0.05) 0%, transparent 60%)", + pointerEvents: "none", + }} + /> + + + - + @@ -133,7 +145,8 @@ export default function UpdateLayout(props: UpdateLayoutProps) { sx={{ fontWeight: 800, letterSpacing: "-0.01em", - background: "linear-gradient(135deg, var(--joy-palette-primary-300), var(--joy-palette-primary-500))", + background: + "linear-gradient(135deg, var(--joy-palette-primary-300), var(--joy-palette-primary-500))", backgroundClip: "text", WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", @@ -157,25 +170,30 @@ export default function UpdateLayout(props: UpdateLayoutProps) { )} {isInstalling && ( - - + + Installing... the app will restart shortly @@ -183,13 +201,15 @@ export default function UpdateLayout(props: UpdateLayoutProps) { )} {status === "error" && ( - + Update failed. Please download the latest version manually. @@ -224,11 +244,7 @@ export default function UpdateLayout(props: UpdateLayoutProps) { ) : ( - + Open releases page diff --git a/src/components/layouts/onboarding/FirstTimeLaunchLayout.stories.tsx b/src/components/layouts/onboarding/FirstTimeLaunchLayout.stories.tsx index 083e765..decfe83 100644 --- a/src/components/layouts/onboarding/FirstTimeLaunchLayout.stories.tsx +++ b/src/components/layouts/onboarding/FirstTimeLaunchLayout.stories.tsx @@ -4,39 +4,39 @@ import type { OnboardingStep } from "../../../pudu/generated"; import { MockOnboardingProvider } from "../../../storybook/mockProviders"; interface OnboardingPreviewProps { - steps: OnboardingStep[]; + steps: OnboardingStep[]; } function OnboardingPreview(props: OnboardingPreviewProps) { - const { steps } = props; + const { steps } = props; - return ( - - - - ); + return ( + + + + ); } const meta = { - title: "Layouts/Onboarding/FirstTimeLaunch", - component: OnboardingPreview, - parameters: { - layout: "fullscreen", - }, - decorators: [ - (Story) => ( - <> - - - > - ), - ], + title: "Layouts/Onboarding/FirstTimeLaunch", + component: OnboardingPreview, + parameters: { + layout: "fullscreen", + }, + decorators: [ + (Story) => ( + <> + + + > + ), + ], } satisfies Meta; export default meta; @@ -44,51 +44,51 @@ export default meta; type Story = StoryObj; const singleStepMock: OnboardingStep[] = [ - { - id: "welcome", - componentKey: "welcome", - title: "Welcome", - description: "Let's set up your launcher experience.", - isRequired: true, - order: 0, - } + { + id: "welcome", + componentKey: "welcome", + title: "Welcome", + description: "Let's set up your launcher experience.", + isRequired: true, + order: 0, + }, ]; const multiStepMock: OnboardingStep[] = [ - { - id: "welcome", - componentKey: "welcome", - title: "Welcome", - description: "Let's set up your launcher experience.", - isRequired: true, - order: 0, - }, - { - id: "launcher-basics", - componentKey: "launcher-basics", - title: "Launcher basics", - description: "Quick overview of launcher preferences.", - isRequired: false, - order: 1, - }, - { - id: "ready-v1", - componentKey: "all-ready", - title: "You're ready", - description: "Final confirmation step.", - isRequired: true, - order: 2, - }, + { + id: "welcome", + componentKey: "welcome", + title: "Welcome", + description: "Let's set up your launcher experience.", + isRequired: true, + order: 0, + }, + { + id: "launcher-basics", + componentKey: "launcher-basics", + title: "Launcher basics", + description: "Quick overview of launcher preferences.", + isRequired: false, + order: 1, + }, + { + id: "ready-v1", + componentKey: "all-ready", + title: "You're ready", + description: "Final confirmation step.", + isRequired: true, + order: 2, + }, ]; export const SingleStepFlow: Story = { - args: { - steps: singleStepMock, - }, -} + args: { + steps: singleStepMock, + }, +}; export const MultiStepFlow: Story = { - args: { - steps: multiStepMock, - }, + args: { + steps: multiStepMock, + }, }; diff --git a/src/components/layouts/onboarding/OnboardingProgressDots.tsx b/src/components/layouts/onboarding/OnboardingProgressDots.tsx index 2c673ab..1d60bdf 100644 --- a/src/components/layouts/onboarding/OnboardingProgressDots.tsx +++ b/src/components/layouts/onboarding/OnboardingProgressDots.tsx @@ -25,11 +25,7 @@ export default function OnboardingProgressDots(props: OnboardingProgressDotsProp width: isActive ? 24 : 8, height: 8, borderRadius: 99, - backgroundColor: isActive - ? "primary.400" - : isCompleted - ? "primary.700" - : "neutral.700", + backgroundColor: isActive ? "primary.400" : isCompleted ? "primary.700" : "neutral.700", transition: "all 0.4s cubic-bezier(0.4, 0, 0.2, 1)", }} /> diff --git a/src/components/layouts/onboarding/OnboardingStepShell.tsx b/src/components/layouts/onboarding/OnboardingStepShell.tsx index fb5a5a9..c0ffffc 100644 --- a/src/components/layouts/onboarding/OnboardingStepShell.tsx +++ b/src/components/layouts/onboarding/OnboardingStepShell.tsx @@ -20,12 +20,7 @@ interface OnboardingStepShellProps { } export default function OnboardingStepShell(props: OnboardingStepShellProps): JSX.Element { - const { - children, - actions, - illustration, - maxContentWidth = 720, - } = props; + const { children, actions, illustration, maxContentWidth = 720 } = props; return ( ) : ( - - {children} - + {children} )} @@ -116,11 +109,7 @@ export default function OnboardingStepShell(props: OnboardingStepShellProps): JS ); } -function ContentContainer(props: { - children: ReactNode; - maxWidth: number | string; - flex?: string; -}) { +function ContentContainer(props: { children: ReactNode; maxWidth: number | string; flex?: string }) { return ( - You are all set + + You are all set + PuduLauncher is ready to use. diff --git a/src/components/layouts/onboarding/firstTimeLaunchSteps/WelcomeLayout.tsx b/src/components/layouts/onboarding/firstTimeLaunchSteps/WelcomeLayout.tsx index 7995423..3e30d98 100644 --- a/src/components/layouts/onboarding/firstTimeLaunchSteps/WelcomeLayout.tsx +++ b/src/components/layouts/onboarding/firstTimeLaunchSteps/WelcomeLayout.tsx @@ -19,9 +19,7 @@ export default function WelcomeLayout(props: OnboardingStepComponentProps): JSX. return ( Welcome to PuduLauncher! - Thanks for being here! Pudu is the official Unitystation launcher, built to get you into - the game faster and with less hassle. Let's get you set up. + Thanks for being here! Pudu is the official Unitystation launcher, built to get you into the game faster + and with less hassle. Let's get you set up. Need help or have a question? Reach out on{" "} void openDiscord(event)}> Unitystation's Discord - . + + . ); diff --git a/src/components/layouts/onboarding/firstTimeLaunchSteps/launcherBasics/HonkTtsSetupLayout.tsx b/src/components/layouts/onboarding/firstTimeLaunchSteps/launcherBasics/HonkTtsSetupLayout.tsx index d2ffb6c..5f8158b 100644 --- a/src/components/layouts/onboarding/firstTimeLaunchSteps/launcherBasics/HonkTtsSetupLayout.tsx +++ b/src/components/layouts/onboarding/firstTimeLaunchSteps/launcherBasics/HonkTtsSetupLayout.tsx @@ -68,8 +68,8 @@ export default function HonkTtsSetupLayout(props: HonkTtsSetupLayoutProps): JSX. > Set up HonkTTS - Choose where HonkTTS should be installed and whether it should start automatically when - PuduLauncher starts. + Choose where HonkTTS should be installed and whether it should start automatically when PuduLauncher + starts. diff --git a/src/components/layouts/onboarding/firstTimeLaunchSteps/launcherBasics/ImmersiveVoicesIntroLayout.tsx b/src/components/layouts/onboarding/firstTimeLaunchSteps/launcherBasics/ImmersiveVoicesIntroLayout.tsx index 7345fe0..0205708 100644 --- a/src/components/layouts/onboarding/firstTimeLaunchSteps/launcherBasics/ImmersiveVoicesIntroLayout.tsx +++ b/src/components/layouts/onboarding/firstTimeLaunchSteps/launcherBasics/ImmersiveVoicesIntroLayout.tsx @@ -30,8 +30,8 @@ export default function ImmersiveVoicesIntroLayout(props: ImmersiveVoicesIntroLa > Immersive voices with HonkTTS - HonkTTS is the local text-to-speech server used by PuduLauncher immersive voices. - It runs on your machine and adds voices to characters in-game. + HonkTTS is the local text-to-speech server used by PuduLauncher immersive voices. It runs on your + machine and adds voices to characters in-game. You can skip for now and enable it later from Preferences. diff --git a/src/components/layouts/onboarding/firstTimeLaunchSteps/launcherBasics/InstallationsPathLayout.tsx b/src/components/layouts/onboarding/firstTimeLaunchSteps/launcherBasics/InstallationsPathLayout.tsx index f7285ae..879873d 100644 --- a/src/components/layouts/onboarding/firstTimeLaunchSteps/launcherBasics/InstallationsPathLayout.tsx +++ b/src/components/layouts/onboarding/firstTimeLaunchSteps/launcherBasics/InstallationsPathLayout.tsx @@ -50,8 +50,8 @@ export default function InstallationsPathLayout(props: InstallationsPathLayoutPr > Choose your installations folder - This is where PuduLauncher stores downloaded game builds. Whenever you join a server, - the required build is downloaded and stored here. + This is where PuduLauncher stores downloaded game builds. Whenever you join a server, the required build + is downloaded and stored here. You can change this later from Preferences. diff --git a/src/components/layouts/onboarding/firstTimeLaunchSteps/launcherBasics/MainLayout.tsx b/src/components/layouts/onboarding/firstTimeLaunchSteps/launcherBasics/MainLayout.tsx index f4ba953..004f9d2 100644 --- a/src/components/layouts/onboarding/firstTimeLaunchSteps/launcherBasics/MainLayout.tsx +++ b/src/components/layouts/onboarding/firstTimeLaunchSteps/launcherBasics/MainLayout.tsx @@ -111,9 +111,8 @@ export default function MainLayout(props: OnboardingStepComponentProps) { return; } - const resolvedInstallationsPath = installationsPath.trim().length > 0 - ? installationsPath.trim() - : preferences.installations.installationPath; + const resolvedInstallationsPath = + installationsPath.trim().length > 0 ? installationsPath.trim() : preferences.installations.installationPath; const updatedPreferences: Preferences = { ...preferences, @@ -138,13 +137,10 @@ export default function MainLayout(props: OnboardingStepComponentProps) { return true; } - const resolvedInstallationsPath = installationsPath.trim().length > 0 - ? installationsPath.trim() - : preferences.installations.installationPath; + const resolvedInstallationsPath = + installationsPath.trim().length > 0 ? installationsPath.trim() : preferences.installations.installationPath; - const resolvedTtsPath = ttsInstallPath.trim().length > 0 - ? ttsInstallPath.trim() - : preferences.tts.installPath; + const resolvedTtsPath = ttsInstallPath.trim().length > 0 ? ttsInstallPath.trim() : preferences.tts.installPath; const updatedPreferences: Preferences = { ...preferences, @@ -251,8 +247,6 @@ export default function MainLayout(props: OnboardingStepComponentProps) { }; return ( - - {renderStep()} - + {renderStep()} ); } diff --git a/src/components/layouts/tts/TtsInstallerLayout.stories.tsx b/src/components/layouts/tts/TtsInstallerLayout.stories.tsx index cc23aea..aa4028a 100644 --- a/src/components/layouts/tts/TtsInstallerLayout.stories.tsx +++ b/src/components/layouts/tts/TtsInstallerLayout.stories.tsx @@ -10,15 +10,7 @@ const sampleLogs = [ "[4/6] eSpeak NG (espeak)", ]; -const installStepLabels = [ - "Prepare", - "Python", - "Environment", - "Packages", - "eSpeak-ng", - "Model", - "Server", -]; +const installStepLabels = ["Prepare", "Python", "Environment", "Packages", "eSpeak-ng", "Model", "Server"]; const meta = { title: "Layouts/Tts/TtsInstallerLayout", diff --git a/src/components/layouts/tts/TtsInstallerLayout.tsx b/src/components/layouts/tts/TtsInstallerLayout.tsx index b33d784..b1ba34d 100644 --- a/src/components/layouts/tts/TtsInstallerLayout.tsx +++ b/src/components/layouts/tts/TtsInstallerLayout.tsx @@ -1,5 +1,5 @@ -import {Alert, Box, Button, Modal, ModalDialog, Stack, Typography} from "@mui/joy"; -import {useEffect, useRef} from "react"; +import { Alert, Box, Button, Modal, ModalDialog, Stack, Typography } from "@mui/joy"; +import { useEffect, useRef } from "react"; import PuduStepper from "../../organisms/common/PuduStepper"; interface TtsInstallerLayoutProps { @@ -42,8 +42,8 @@ export default function TtsInstallerLayout(props: TtsInstallerLayoutProps) { return ( - - + + HonkTTS Installer @@ -97,12 +97,10 @@ export default function TtsInstallerLayout(props: TtsInstallerLayoutProps) { wordBreak: "break-word", }} > - {installLogs.length > 0 - ? installLogs.join("\n") - : "Waiting for installer output..."} + {installLogs.length > 0 ? installLogs.join("\n") : "Waiting for installer output..."} - + {isBusy ? "Installing..." : "Close"} diff --git a/src/components/layouts/workInProgressLayout/WorkInProgressLayout.stories.tsx b/src/components/layouts/workInProgressLayout/WorkInProgressLayout.stories.tsx index 6c42048..8ce6b33 100644 --- a/src/components/layouts/workInProgressLayout/WorkInProgressLayout.stories.tsx +++ b/src/components/layouts/workInProgressLayout/WorkInProgressLayout.stories.tsx @@ -1,7 +1,6 @@ -import type {Meta, StoryObj} from "@storybook/react-vite"; +import type { Meta, StoryObj } from "@storybook/react-vite"; import WorkInProgressLayout from "./WorkInProgressLayout.tsx"; -import {Box, GlobalStyles} from "@mui/joy"; - +import { Box, GlobalStyles } from "@mui/joy"; const meta = { title: "Layouts/WorkInProgressLayout", @@ -12,23 +11,27 @@ const meta = { decorators: [ (Story) => ( <> - - - + + + > ), - ] + ], } satisfies Meta; export default meta; type Story = StoryObj; -export const Default: Story = {}; \ No newline at end of file +export const Default: Story = {}; diff --git a/src/components/layouts/workInProgressLayout/WorkInProgressLayout.tsx b/src/components/layouts/workInProgressLayout/WorkInProgressLayout.tsx index 72c0c46..7ac7c8b 100644 --- a/src/components/layouts/workInProgressLayout/WorkInProgressLayout.tsx +++ b/src/components/layouts/workInProgressLayout/WorkInProgressLayout.tsx @@ -1,19 +1,21 @@ -import {AspectRatio, Stack, Typography} from "@mui/joy"; +import { AspectRatio, Stack, Typography } from "@mui/joy"; export default function WorkInProgressLayout() { return ( <> - - + + - - Work in progress... - + Work in progress... > - ) + ); } diff --git a/src/components/molecules/PaginationControls.tsx b/src/components/molecules/PaginationControls.tsx index eb0af06..d7b42fa 100644 --- a/src/components/molecules/PaginationControls.tsx +++ b/src/components/molecules/PaginationControls.tsx @@ -14,23 +14,13 @@ export default function PaginationControls(props: PaginationControlsProps) { return ( - + Previous Page {currentPage} of {totalPages} - = totalPages} - onClick={onNext} - > + = totalPages} onClick={onNext}> Next diff --git a/src/components/molecules/discord/DiscordJoinDialog.tsx b/src/components/molecules/discord/DiscordJoinDialog.tsx index 47e7786..e4c91fb 100644 --- a/src/components/molecules/discord/DiscordJoinDialog.tsx +++ b/src/components/molecules/discord/DiscordJoinDialog.tsx @@ -11,14 +11,7 @@ import { Stack, Typography, } from "@mui/joy"; -import { - Download, - Dns, - GamepadRounded, - Map as MapIcon, - People, - SportsEsports, -} from "@mui/icons-material"; +import { Download, Dns, GamepadRounded, Map as MapIcon, People, SportsEsports } from "@mui/icons-material"; import type { ReactNode } from "react"; export interface DiscordJoinDialogProps { @@ -38,9 +31,7 @@ export interface DiscordJoinDialogProps { function InfoRow(props: { icon: ReactNode; label: string; value: string }) { return ( - - {props.icon} - + {props.icon} {props.label} @@ -112,10 +103,7 @@ export default function DiscordJoinDialog(props: DiscordJoinDialogProps) { > - + Game Invite @@ -204,21 +192,13 @@ export default function DiscordJoinDialog(props: DiscordJoinDialogProps) { {/* Install notice */} - + This build is not installed yet. Install it to join the server. {/* Actions */} - + Cancel - - Fatal error - + Fatal error {error?.userMessage} - - Source: {error?.source} - + Source: {error?.source} Time: {error ? new Date(error.timestamp).toLocaleString() : ""} - {error?.code && ( - - Code: {error.code} - - )} + {error?.code && Code: {error.code}} {error?.correlationId && ( - - Correlation: {error.correlationId} - + Correlation: {error.correlationId} )} @@ -82,9 +57,7 @@ export default function FatalErrorModal(props: FatalErrorModalProps) { - void onCopyTrace()}> - Copy trace - + void onCopyTrace()}>Copy trace {onSeeLogs && ( See logs diff --git a/src/components/molecules/errors/FeedbackSnackbar.tsx b/src/components/molecules/errors/FeedbackSnackbar.tsx index e750b76..87b580b 100644 --- a/src/components/molecules/errors/FeedbackSnackbar.tsx +++ b/src/components/molecules/errors/FeedbackSnackbar.tsx @@ -1,11 +1,5 @@ import { useRef } from "react"; -import { - Button, - type ColorPaletteProp, - Snackbar, - Stack, - Typography, -} from "@mui/joy"; +import { Button, type ColorPaletteProp, Snackbar, Stack, Typography } from "@mui/joy"; import type { SnackbarItem, SnackbarSeverity } from "../../../contextProviders/FeedbackContextProvider"; const severityColorMap: Record = { @@ -23,12 +17,7 @@ export interface FeedbackSnackbarProps { } export default function FeedbackSnackbar(props: FeedbackSnackbarProps) { - const { - snackbar, - autoHideDuration = 6_000, - onClose, - onSeeLogs, - } = props; + const { snackbar, autoHideDuration = 6_000, onClose, onSeeLogs } = props; // Keep last displayed item in a ref so color and content stay correct // during the exit animation (when snackbar becomes null but the Snackbar @@ -58,12 +47,7 @@ export default function FeedbackSnackbar(props: FeedbackSnackbarProps) { invertedColors endDecorator={ showSeeLogs && ( - + See logs ) diff --git a/src/components/molecules/installations/InstallationCard.stories.tsx b/src/components/molecules/installations/InstallationCard.stories.tsx index 3dd5fd8..7a67c84 100644 --- a/src/components/molecules/installations/InstallationCard.stories.tsx +++ b/src/components/molecules/installations/InstallationCard.stories.tsx @@ -1,53 +1,50 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; -import type { ComponentProps } from 'react'; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { ComponentProps } from "react"; -import InstallationCard from './InstallationCard'; +import InstallationCard from "./InstallationCard"; type InstallationCardStoryArgs = ComponentProps & { - showLastPlayedChip: boolean; - lastPlayedStatus: "Recently Played" | "A while ago"; + showLastPlayedChip: boolean; + lastPlayedStatus: "Recently Played" | "A while ago"; }; const DAY_MS = 24 * 60 * 60 * 1000; const getLastPlayedDate = (status: InstallationCardStoryArgs["lastPlayedStatus"]) => { - const daysAgo = status === "Recently Played" ? 2 : 45; - return new Date(Date.now() - daysAgo * DAY_MS).toISOString(); + const daysAgo = status === "Recently Played" ? 2 : 45; + return new Date(Date.now() - daysAgo * DAY_MS).toISOString(); }; const meta = { - component: InstallationCard, - args: { - forkName: "UnitystationDevelop", - buildVersion: "26021410", - isNewest: false, - showLastPlayedChip: true, - lastPlayedStatus: "Recently Played", - }, - argTypes: { - isNewest: { - control: "boolean", - description: "Show or hide the Newest chip.", + component: InstallationCard, + args: { + forkName: "UnitystationDevelop", + buildVersion: "26021410", + isNewest: false, + showLastPlayedChip: true, + lastPlayedStatus: "Recently Played", }, - showLastPlayedChip: { - control: "boolean", - description: "Show or hide the Last Played chip.", + argTypes: { + isNewest: { + control: "boolean", + description: "Show or hide the Newest chip.", + }, + showLastPlayedChip: { + control: "boolean", + description: "Show or hide the Last Played chip.", + }, + lastPlayedStatus: { + control: { type: "inline-radio" }, + options: ["Recently Played", "A while ago"], + description: "Controls the label shown when Last Played chip is enabled.", + }, + lastPlayedAt: { + table: { disable: true }, + }, }, - lastPlayedStatus: { - control: { type: "inline-radio" }, - options: ["Recently Played", "A while ago"], - description: "Controls the label shown when Last Played chip is enabled.", - }, - lastPlayedAt: { - table: { disable: true }, - }, - }, - render: ({ showLastPlayedChip, lastPlayedStatus, ...args }) => ( - - ), + render: ({ showLastPlayedChip, lastPlayedStatus, ...args }) => ( + + ), } satisfies Meta; export default meta; @@ -57,27 +54,27 @@ type Story = StoryObj; export const Default: Story = {}; export const AWhileAgo: Story = { - args: { - lastPlayedStatus: "A while ago", - }, + args: { + lastPlayedStatus: "A while ago", + }, }; export const WithoutLastPlayed: Story = { - args: { - showLastPlayedChip: false, - }, + args: { + showLastPlayedChip: false, + }, }; export const NewestBuild: Story = { - args: { - isNewest: true, - }, + args: { + isNewest: true, + }, }; export const LongForkName: Story = { - args: { - forkName: "UnitystationDevelop-CI-Nightly-Windows-x64-Experimental", - lastPlayedStatus: "A while ago", - isNewest: true, - }, + args: { + forkName: "UnitystationDevelop-CI-Nightly-Windows-x64-Experimental", + lastPlayedStatus: "A while ago", + isNewest: true, + }, }; diff --git a/src/components/molecules/installations/InstallationCard.tsx b/src/components/molecules/installations/InstallationCard.tsx index d11f5f4..8257b7b 100644 --- a/src/components/molecules/installations/InstallationCard.tsx +++ b/src/components/molecules/installations/InstallationCard.tsx @@ -42,17 +42,15 @@ export default function InstallationCard(props: InstallationCardProps) { overflow: "hidden", padding: 2, flexShrink: 0, - }}> + }} + > - + alignItems={{ xs: "stretch", sm: "center" }} + > + + }} + > - - + + {forkName} {isNewest && ( @@ -94,30 +86,15 @@ export default function InstallationCard(props: InstallationCardProps) { - - } - onClick={onDelete}> + + } onClick={onDelete}> Delete - } - onClick={onPlay}> + } onClick={onPlay}> Play - ) + ); } diff --git a/src/components/molecules/installations/RegistryBuildRow.tsx b/src/components/molecules/installations/RegistryBuildRow.tsx index 028933d..38ac31f 100644 --- a/src/components/molecules/installations/RegistryBuildRow.tsx +++ b/src/components/molecules/installations/RegistryBuildRow.tsx @@ -1,13 +1,6 @@ -import {Check, CloudDownload, Refresh} from "@mui/icons-material"; -import { - Button, - Chip, - LinearProgress, - Sheet, - Stack, - Typography, -} from "@mui/joy"; -import type {RegistryDownloadSnapshot} from "../../../contextProviders/InstallationsContextProvider"; +import { Check, CloudDownload, Refresh } from "@mui/icons-material"; +import { Button, Chip, LinearProgress, Sheet, Stack, Typography } from "@mui/joy"; +import type { RegistryDownloadSnapshot } from "../../../contextProviders/InstallationsContextProvider"; const STATE_LABELS: Record = { 1: "Downloading", @@ -26,14 +19,14 @@ interface RegistryBuildRowProps { } export default function RegistryBuildRow(props: RegistryBuildRowProps) { - const {versionNumber, dateCreated, download, isInstalled, onDownload} = props; + const { versionNumber, dateCreated, download, isInstalled, onDownload } = props; const formattedDate = dateCreated ? new Date(dateCreated).toLocaleDateString(undefined, { - year: "numeric", - month: "short", - day: "numeric", - }) + year: "numeric", + month: "short", + day: "numeric", + }) : null; const isDownloading = download?.state === 1; @@ -51,22 +44,26 @@ export default function RegistryBuildRow(props: RegistryBuildRowProps) { overflow: "hidden", }} > - + - + Build {versionNumber} {isInstalled && !download && ( - }> + } + > Installed )} {formattedDate && ( - + {formattedDate} )} @@ -77,7 +74,7 @@ export default function RegistryBuildRow(props: RegistryBuildRowProps) { variant="soft" color="primary" size="sm" - startDecorator={} + startDecorator={} onClick={onDownload} > Download @@ -98,7 +95,7 @@ export default function RegistryBuildRow(props: RegistryBuildRowProps) { variant="soft" color="danger" size="sm" - startDecorator={} + startDecorator={} onClick={onDownload} > Retry @@ -113,17 +110,11 @@ export default function RegistryBuildRow(props: RegistryBuildRowProps) { variant="soft" value={download.progress} color="primary" - sx={{borderRadius: 0}} + sx={{ borderRadius: 0 }} /> )} - {isProcessing && ( - - )} + {isProcessing && } ); } diff --git a/src/components/molecules/installations/RegistryBuildsPopup.tsx b/src/components/molecules/installations/RegistryBuildsPopup.tsx index 928131e..19c7ad8 100644 --- a/src/components/molecules/installations/RegistryBuildsPopup.tsx +++ b/src/components/molecules/installations/RegistryBuildsPopup.tsx @@ -1,14 +1,5 @@ import { Close } from "@mui/icons-material"; -import { - Alert, - Box, - CircularProgress, - IconButton, - Modal, - ModalDialog, - Stack, - Typography, -} from "@mui/joy"; +import { Alert, Box, CircularProgress, IconButton, Modal, ModalDialog, Stack, Typography } from "@mui/joy"; import type { RegistryBuild } from "../../../pudu/generated"; import type { RegistryDownloadSnapshot } from "../../../contextProviders/InstallationsContextProvider"; import { usePaginatedCollection } from "../../../hooks/usePaginatedCollection"; @@ -30,14 +21,10 @@ interface RegistryBuildsPopupProps { export default function RegistryBuildsPopup(props: RegistryBuildsPopupProps) { const { open, builds, loading, downloads, installedVersions, onDownload, onClose } = props; - const { - currentPageItems, - currentPage, - totalPages, - nextPage, - previousPage, - totalItems, - } = usePaginatedCollection(builds, PAGE_SIZE); + const { currentPageItems, currentPage, totalPages, nextPage, previousPage, totalItems } = usePaginatedCollection( + builds, + PAGE_SIZE, + ); return ( @@ -59,9 +46,7 @@ export default function RegistryBuildsPopup(props: RegistryBuildsPopupProps) { - - Fetching builds from registry... - + Fetching builds from registry... )} diff --git a/src/components/molecules/news/BuildEntry.stories.tsx b/src/components/molecules/news/BuildEntry.stories.tsx index 083129d..111857e 100644 --- a/src/components/molecules/news/BuildEntry.stories.tsx +++ b/src/components/molecules/news/BuildEntry.stories.tsx @@ -1,9 +1,9 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { Meta, StoryObj } from "@storybook/react-vite"; -import BuildEntry from './BuildEntry'; +import BuildEntry from "./BuildEntry"; const meta = { - component: BuildEntry, + component: BuildEntry, } satisfies Meta; export default meta; @@ -11,30 +11,30 @@ export default meta; type Story = StoryObj; export const Default: Story = { - args: { - "version": "1.0.0", - "dateCreated": "2024-06-01T12:00:00Z", - "changes": [ - { - "description": "Fixed a critical bug that caused crashes on startup.", - "author": "Jane Doe", - "type": "FIX" - }, - { - "description": "Added new feature for customizing user interface.", - "author": "John Smith", - "type": "NEW" - }, - { - "description": "Improved performance of the application.", - "author": "Alice Johnson", - "type": "IMPROVEMENT" - }, - { - "description": "Balanced the game mechanics for better gameplay experience.", - "author": "Bob Brown", - "type": "BALANCE" - } - ] - } -}; \ No newline at end of file + args: { + version: "1.0.0", + dateCreated: "2024-06-01T12:00:00Z", + changes: [ + { + description: "Fixed a critical bug that caused crashes on startup.", + author: "Jane Doe", + type: "FIX", + }, + { + description: "Added new feature for customizing user interface.", + author: "John Smith", + type: "NEW", + }, + { + description: "Improved performance of the application.", + author: "Alice Johnson", + type: "IMPROVEMENT", + }, + { + description: "Balanced the game mechanics for better gameplay experience.", + author: "Bob Brown", + type: "BALANCE", + }, + ], + }, +}; diff --git a/src/components/molecules/news/ChangelogSidebar.stories.tsx b/src/components/molecules/news/ChangelogSidebar.stories.tsx index 60ca069..00c7845 100644 --- a/src/components/molecules/news/ChangelogSidebar.stories.tsx +++ b/src/components/molecules/news/ChangelogSidebar.stories.tsx @@ -1,6 +1,6 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { Meta, StoryObj } from "@storybook/react-vite"; -import ChangelogSidebar from './ChangelogSidebar'; +import ChangelogSidebar from "./ChangelogSidebar"; const meta = { component: ChangelogSidebar, @@ -26,27 +26,67 @@ export const Default: Story = { version: "0.8.3", dateCreated: "2025-06-01T12:00:00Z", changes: [ - { description: "Fixed a critical networking bug that caused disconnections during high player counts.", author: "Jane Doe", type: "FIX" }, - { description: "Added new emote system with over 30 animations.", author: "John Smith", type: "NEW" }, + { + description: + "Fixed a critical networking bug that caused disconnections during high player counts.", + author: "Jane Doe", + type: "FIX", + }, + { + description: "Added new emote system with over 30 animations.", + author: "John Smith", + type: "NEW", + }, ], }, { version: "0.8.2", dateCreated: "2025-05-20T10:00:00Z", changes: [ - { description: "Improved server browser loading speed by caching DNS lookups and reusing HTTP connections.", author: "Alice Johnson", type: "IMPROVEMENT" }, - { description: "Balanced traitor objectives to be more achievable for new players while keeping experienced players challenged.", author: "Bob Brown", type: "BALANCE" }, - { description: "Fixed ghost players being able to interact with physical objects in certain edge cases.", author: "Jane Doe", type: "FIX" }, + { + description: + "Improved server browser loading speed by caching DNS lookups and reusing HTTP connections.", + author: "Alice Johnson", + type: "IMPROVEMENT", + }, + { + description: + "Balanced traitor objectives to be more achievable for new players while keeping experienced players challenged.", + author: "Bob Brown", + type: "BALANCE", + }, + { + description: + "Fixed ghost players being able to interact with physical objects in certain edge cases.", + author: "Jane Doe", + type: "FIX", + }, ], }, { version: "0.8.1", dateCreated: "2025-05-10T08:00:00Z", changes: [ - { description: "Added speech-to-text integration for accessibility.", author: "Charlie Wilson", type: "NEW" }, - { description: "Fixed Linux builds failing to launch on Ubuntu 24.04.", author: "Jane Doe", type: "FIX" }, - { description: "Improved character creation UI responsiveness.", author: "Alice Johnson", type: "IMPROVEMENT" }, - { description: "Fixed inventory duplication exploit when disconnecting during transfers.", author: "Bob Brown", type: "FIX" }, + { + description: "Added speech-to-text integration for accessibility.", + author: "Charlie Wilson", + type: "NEW", + }, + { + description: "Fixed Linux builds failing to launch on Ubuntu 24.04.", + author: "Jane Doe", + type: "FIX", + }, + { + description: "Improved character creation UI responsiveness.", + author: "Alice Johnson", + type: "IMPROVEMENT", + }, + { + description: "Fixed inventory duplication exploit when disconnecting during transfers.", + author: "Bob Brown", + type: "FIX", + }, ], }, ], diff --git a/src/components/molecules/news/FeaturedBlogPostPreview.stories.tsx b/src/components/molecules/news/FeaturedBlogPostPreview.stories.tsx index 5f7cea5..fef90f1 100644 --- a/src/components/molecules/news/FeaturedBlogPostPreview.stories.tsx +++ b/src/components/molecules/news/FeaturedBlogPostPreview.stories.tsx @@ -1,6 +1,6 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { Meta, StoryObj } from "@storybook/react-vite"; -import FeaturedBlogPostPreview from './FeaturedBlogPostPreview'; +import FeaturedBlogPostPreview from "./FeaturedBlogPostPreview"; const meta = { component: FeaturedBlogPostPreview, @@ -17,7 +17,8 @@ export const Default: Story = { author: "Gilles", createDateTime: "2025-05-03T03:07:24.470778Z", imageUrl: "https://unitystationfile.b-cdn.net/WeeklyBlogUpdates/33/sillylights.webp", - summary: "New mobs use the same systems as players, simple bots are back and talky, puddles are real, traps are live, emissive lights got cooler, and Linux builds finally work. We've got a new permissions system, character voices, a flatpacking machine, speech-to-text, and a mountain of bugfixes. It's been a big one.", + summary: + "New mobs use the same systems as players, simple bots are back and talky, puddles are real, traps are live, emissive lights got cooler, and Linux builds finally work. We've got a new permissions system, character voices, a flatpacking machine, speech-to-text, and a mountain of bugfixes. It's been a big one.", }, }; @@ -28,6 +29,7 @@ export const LongSummary: Story = { author: "TeamLead", createDateTime: "2025-04-15T10:30:00Z", imageUrl: "https://unitystationfile.b-cdn.net/WeeklyBlogUpdates/33/sillylights.webp", - summary: "This update brings a complete overhaul of the core game systems including networking, physics, rendering, and AI. We have rewritten the entire networking stack to support better latency and more concurrent players. The physics engine now handles complex interactions between objects more realistically. Our rendering pipeline has been optimized for lower-end hardware while maintaining visual fidelity on high-end systems.", + summary: + "This update brings a complete overhaul of the core game systems including networking, physics, rendering, and AI. We have rewritten the entire networking stack to support better latency and more concurrent players. The physics engine now handles complex interactions between objects more realistically. Our rendering pipeline has been optimized for lower-end hardware while maintaining visual fidelity on high-end systems.", }, }; diff --git a/src/components/molecules/news/HeroBlogPost.stories.tsx b/src/components/molecules/news/HeroBlogPost.stories.tsx index 43a325d..1d27801 100644 --- a/src/components/molecules/news/HeroBlogPost.stories.tsx +++ b/src/components/molecules/news/HeroBlogPost.stories.tsx @@ -1,6 +1,6 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { Meta, StoryObj } from "@storybook/react-vite"; -import HeroBlogPost from './HeroBlogPost'; +import HeroBlogPost from "./HeroBlogPost"; const meta = { component: HeroBlogPost, @@ -17,6 +17,7 @@ export const Default: Story = { author: "Gilles", createDateTime: "2025-05-03T03:07:24.470778Z", imageUrl: "https://unitystationfile.b-cdn.net/WeeklyBlogUpdates/33/sillylights.webp", - summary: "New mobs use the same systems as players, simple bots are back and talky, puddles are real, traps are live, emissive lights got cooler, and Linux builds finally work. We've got a new permissions system, character voices, a flatpacking machine, speech-to-text, and a mountain of bugfixes. It's been a big one.", + summary: + "New mobs use the same systems as players, simple bots are back and talky, puddles are real, traps are live, emissive lights got cooler, and Linux builds finally work. We've got a new permissions system, character voices, a flatpacking machine, speech-to-text, and a mountain of bugfixes. It's been a big one.", }, }; diff --git a/src/components/molecules/news/HeroBlogPost.tsx b/src/components/molecules/news/HeroBlogPost.tsx index 9c32684..5fdcf2c 100644 --- a/src/components/molecules/news/HeroBlogPost.tsx +++ b/src/components/molecules/news/HeroBlogPost.tsx @@ -37,7 +37,8 @@ export default function HeroBlogPost(props: BlogPost) { sx={{ position: "absolute", inset: 0, - background: "linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.4) 50%, rgba(0,0,0,0.1) 100%)", + background: + "linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.4) 50%, rgba(0,0,0,0.1) 100%)", }} /> ; export const Default: Story = { args: { - "title": "Progress #33: It's been a while!", - "slug": "progress-33-its-been-a-while", - "author": "Gilles", - "createDateTime": "2025-05-03T03:07:24.470778Z", - "type": "weekly", - "imageUrl": "https://unitystationfile.b-cdn.net/WeeklyBlogUpdates/33/sillylights.webp", - "summary": "New mobs use the same systems as players, simple bots are back and talky, puddles are real, traps are live, emissive lights got cooler, and Linux builds finally work. We’ve got a new permissions system, character voices, a flatpacking machine, speech-to-text, and a mountain of bugfixes. It's been a big one.", - "state": "published", + title: "Progress #33: It's been a while!", + slug: "progress-33-its-been-a-while", + author: "Gilles", + createDateTime: "2025-05-03T03:07:24.470778Z", + type: "weekly", + imageUrl: "https://unitystationfile.b-cdn.net/WeeklyBlogUpdates/33/sillylights.webp", + summary: + "New mobs use the same systems as players, simple bots are back and talky, puddles are real, traps are live, emissive lights got cooler, and Linux builds finally work. We’ve got a new permissions system, character voices, a flatpacking machine, speech-to-text, and a mountain of bugfixes. It's been a big one.", + state: "published", }, -}; \ No newline at end of file +}; diff --git a/src/components/molecules/news/SmallBlogPostPreview.tsx b/src/components/molecules/news/SmallBlogPostPreview.tsx index e5193cb..11c601f 100644 --- a/src/components/molecules/news/SmallBlogPostPreview.tsx +++ b/src/components/molecules/news/SmallBlogPostPreview.tsx @@ -22,18 +22,12 @@ export default function SmallBlogPostPreview(props: BlogPost) { > - + - - {title} - + {title} {byline} diff --git a/src/components/molecules/preferences/PreferenceCategory.tsx b/src/components/molecules/preferences/PreferenceCategory.tsx index b92316f..cddc6cf 100644 --- a/src/components/molecules/preferences/PreferenceCategory.tsx +++ b/src/components/molecules/preferences/PreferenceCategory.tsx @@ -18,13 +18,7 @@ export default function PreferenceCategory(props: PreferenceCategoryProps) { if (schema.layout) { const CustomLayout = customLayouts[schema.layout]; if (CustomLayout) { - return ( - - ); + return ; } } @@ -35,16 +29,20 @@ export default function PreferenceCategory(props: PreferenceCategoryProps) { return ( {schema.fields.map((field) => { - const value = typeof categoryData === "object" && categoryData !== null - ? (categoryData as unknown as Record)[field.key] - : undefined; + const value = + typeof categoryData === "object" && categoryData !== null + ? (categoryData as unknown as Record)[field.key] + : undefined; return ( updateField(schema.key, field.key, newValue))} + onChange={ + fieldOverrides?.[field.key] ?? + ((newValue) => updateField(schema.key, field.key, newValue)) + } /> ); })} @@ -59,9 +57,7 @@ export default function PreferenceCategory(props: PreferenceCategoryProps) { {schema.label} - - {renderFields()} - + {renderFields()} ); } diff --git a/src/components/molecules/preferences/PreferenceInputFieldRow.stories.tsx b/src/components/molecules/preferences/PreferenceInputFieldRow.stories.tsx index 303e1e6..2462471 100644 --- a/src/components/molecules/preferences/PreferenceInputFieldRow.stories.tsx +++ b/src/components/molecules/preferences/PreferenceInputFieldRow.stories.tsx @@ -41,4 +41,3 @@ export const Number: Story = { value: 60, }, }; - diff --git a/src/components/molecules/preferences/PreferencePathFieldRow.stories.tsx b/src/components/molecules/preferences/PreferencePathFieldRow.stories.tsx index d73854b..3b75377 100644 --- a/src/components/molecules/preferences/PreferencePathFieldRow.stories.tsx +++ b/src/components/molecules/preferences/PreferencePathFieldRow.stories.tsx @@ -28,4 +28,3 @@ export default meta; type Story = StoryObj; export const Default: Story = {}; - diff --git a/src/components/molecules/preferences/PreferencePathFieldRow.tsx b/src/components/molecules/preferences/PreferencePathFieldRow.tsx index 918399b..0fd8b55 100644 --- a/src/components/molecules/preferences/PreferencePathFieldRow.tsx +++ b/src/components/molecules/preferences/PreferencePathFieldRow.tsx @@ -18,11 +18,7 @@ export default function PreferencePathFieldRow(props: PreferencePathFieldRowProp - + ); } - diff --git a/src/components/molecules/preferences/PreferenceSelectFieldRow.stories.tsx b/src/components/molecules/preferences/PreferenceSelectFieldRow.stories.tsx index 885495a..6605de7 100644 --- a/src/components/molecules/preferences/PreferenceSelectFieldRow.stories.tsx +++ b/src/components/molecules/preferences/PreferenceSelectFieldRow.stories.tsx @@ -36,4 +36,3 @@ export const WithoutOptions: Story = { value: "", }, }; - diff --git a/src/components/molecules/preferences/PreferenceToggleFieldRow.stories.tsx b/src/components/molecules/preferences/PreferenceToggleFieldRow.stories.tsx index c369389..dcc7278 100644 --- a/src/components/molecules/preferences/PreferenceToggleFieldRow.stories.tsx +++ b/src/components/molecules/preferences/PreferenceToggleFieldRow.stories.tsx @@ -37,4 +37,3 @@ export const Disabled: Story = { value: false, }, }; - diff --git a/src/components/molecules/preferences/PreferenceToggleFieldRow.tsx b/src/components/molecules/preferences/PreferenceToggleFieldRow.tsx index 141a0c0..2bd56bb 100644 --- a/src/components/molecules/preferences/PreferenceToggleFieldRow.tsx +++ b/src/components/molecules/preferences/PreferenceToggleFieldRow.tsx @@ -13,15 +13,9 @@ export default function PreferenceToggleFieldRow(props: PreferenceToggleFieldRow const { label, tooltip, value, onChange } = props; return ( - + - onChange(e.target.checked)} - /> + onChange(e.target.checked)} /> ); } diff --git a/src/components/molecules/preferences/customLayouts/TtsPreferencesLayout.tsx b/src/components/molecules/preferences/customLayouts/TtsPreferencesLayout.tsx index dafe594..de0ad73 100644 --- a/src/components/molecules/preferences/customLayouts/TtsPreferencesLayout.tsx +++ b/src/components/molecules/preferences/customLayouts/TtsPreferencesLayout.tsx @@ -14,7 +14,11 @@ function getStatusChipColor(status: number | null): "neutral" | "success" | "war return "success"; } - if (status === TTS_STATUS.Downloading || status === TTS_STATUS.Installing || status === TTS_STATUS.CheckingForUpdates) { + if ( + status === TTS_STATUS.Downloading || + status === TTS_STATUS.Installing || + status === TTS_STATUS.CheckingForUpdates + ) { return "primary"; } @@ -46,9 +50,7 @@ export default function TtsPreferencesLayout(props: TtsPreferencesLayoutProps) { runCommand, } = useTtsPreferencesContext(); - const statusLabel = status !== null - ? (TTS_STATUS_LABELS[status] ?? `Status ${status}`) - : "Unknown"; + const statusLabel = status !== null ? (TTS_STATUS_LABELS[status] ?? `Status ${status}`) : "Unknown"; const installButtonLabel = isInstalled ? "Reinstall HonkTTS" : "Install HonkTTS"; @@ -114,16 +116,37 @@ export default function TtsPreferencesLayout(props: TtsPreferencesLayoutProps) { void runCommand("install")}> {installButtonLabel} - void runCommand("checkForUpdates")}> + void runCommand("checkForUpdates")} + > Check updates - void runCommand("startServer")}> + void runCommand("startServer")} + > Start server - void runCommand("stopServer")}> + void runCommand("stopServer")} + > Stop server - void runCommand("uninstall")}> + void runCommand("uninstall")} + > Uninstall diff --git a/src/components/molecules/servers/ServerCardDetails.tsx b/src/components/molecules/servers/ServerCardDetails.tsx index 9c7d289..d6e536a 100644 --- a/src/components/molecules/servers/ServerCardDetails.tsx +++ b/src/components/molecules/servers/ServerCardDetails.tsx @@ -13,14 +13,7 @@ interface ServerCardDetailsProps { } export default function ServerCardDetails(props: ServerCardDetailsProps) { - const { - map, - build, - roundTime, - pingMs, - playersOnline, - playerCapacity, - } = props; + const { map, build, roundTime, pingMs, playersOnline, playerCapacity } = props; return ( diff --git a/src/components/molecules/servers/ServerCardHeader.tsx b/src/components/molecules/servers/ServerCardHeader.tsx index 28fc29e..87f0f9d 100644 --- a/src/components/molecules/servers/ServerCardHeader.tsx +++ b/src/components/molecules/servers/ServerCardHeader.tsx @@ -11,14 +11,9 @@ export default function ServerCardHeader(props: ServerCardHeaderProps) { return ( - - {name} - + {name} }> - + Now playing: {mode} diff --git a/src/components/molecules/servers/serverCardActions.tsx b/src/components/molecules/servers/serverCardActions.tsx index 02c92ac..cad362e 100644 --- a/src/components/molecules/servers/serverCardActions.tsx +++ b/src/components/molecules/servers/serverCardActions.tsx @@ -107,14 +107,21 @@ export function resolveServerAction( const resolvedActionState = actionState ?? inferActionState(actionLabel); const resolvedActionLabel = actionLabel ?? defaultActionLabelByState[resolvedActionState]; const actionVisual = actionVisualByState[resolvedActionState]; - const isBusyAction = resolvedActionState === "downloading" || resolvedActionState === "extracting" || resolvedActionState === "scanning" || resolvedActionState === "playing"; - const isDeterminate = resolvedActionState === "downloading" || resolvedActionState === "downloadFailed" || resolvedActionState === "scanningFailed"; + const isBusyAction = + resolvedActionState === "downloading" || + resolvedActionState === "extracting" || + resolvedActionState === "scanning" || + resolvedActionState === "playing"; + const isDeterminate = + resolvedActionState === "downloading" || + resolvedActionState === "downloadFailed" || + resolvedActionState === "scanningFailed"; return { resolvedActionState, resolvedActionLabel, actionVisual, isBusyAction, - isDeterminate + isDeterminate, }; } diff --git a/src/components/molecules/sideBar/SideBarExternalLink.stories.tsx b/src/components/molecules/sideBar/SideBarExternalLink.stories.tsx index 720155e..e23da31 100644 --- a/src/components/molecules/sideBar/SideBarExternalLink.stories.tsx +++ b/src/components/molecules/sideBar/SideBarExternalLink.stories.tsx @@ -20,8 +20,7 @@ const meta = { ), ], args: { - onClick: () => { - }, + onClick: () => {}, }, } satisfies Meta; diff --git a/src/components/molecules/sideBar/SideBarExternalLink.tsx b/src/components/molecules/sideBar/SideBarExternalLink.tsx index a5dbd50..7fc90d0 100644 --- a/src/components/molecules/sideBar/SideBarExternalLink.tsx +++ b/src/components/molecules/sideBar/SideBarExternalLink.tsx @@ -1,5 +1,5 @@ -import {Avatar, Tooltip} from "@mui/joy"; -import {type JSX} from "react"; +import { Avatar, Tooltip } from "@mui/joy"; +import { type JSX } from "react"; export interface SideBarExternalLinkProps { tooltip: string; @@ -8,7 +8,7 @@ export interface SideBarExternalLinkProps { } export default function SideBarExternalLink(props: SideBarExternalLinkProps) { - const {tooltip, icon, onClick} = props; + const { tooltip, icon, onClick } = props; return ( @@ -28,5 +28,5 @@ export default function SideBarExternalLink(props: SideBarExternalLinkProps) { {icon} - ) + ); } diff --git a/src/components/molecules/sideBar/SideBarMenuItem.stories.tsx b/src/components/molecules/sideBar/SideBarMenuItem.stories.tsx index 7873df4..1ce24d9 100644 --- a/src/components/molecules/sideBar/SideBarMenuItem.stories.tsx +++ b/src/components/molecules/sideBar/SideBarMenuItem.stories.tsx @@ -20,8 +20,7 @@ const meta = { ), ], args: { - onClick: () => { - }, + onClick: () => {}, }, } satisfies Meta; diff --git a/src/components/molecules/sideBar/SideBarMenuItem.tsx b/src/components/molecules/sideBar/SideBarMenuItem.tsx index 6fb7180..ff82386 100644 --- a/src/components/molecules/sideBar/SideBarMenuItem.tsx +++ b/src/components/molecules/sideBar/SideBarMenuItem.tsx @@ -15,10 +15,8 @@ export default function SideBarMenuItem(props: SideBarMenuItemProps) { {icon} - - {text} - + {text} - ) + ); } diff --git a/src/components/molecules/update/ReleaseNotesCard.tsx b/src/components/molecules/update/ReleaseNotesCard.tsx index 30ef69f..4f76855 100644 --- a/src/components/molecules/update/ReleaseNotesCard.tsx +++ b/src/components/molecules/update/ReleaseNotesCard.tsx @@ -27,51 +27,54 @@ export default function ReleaseNotesCard(props: ReleaseNotesCardProps) { const { content } = props; return ( - - - + bgcolor: "background.surface", + maxHeight: 220, + overflow: "auto", + position: "relative", + }} + > + + What's new - + {content} diff --git a/src/components/organisms/common/PuduStepper.tsx b/src/components/organisms/common/PuduStepper.tsx index 0772b09..88de9be 100644 --- a/src/components/organisms/common/PuduStepper.tsx +++ b/src/components/organisms/common/PuduStepper.tsx @@ -20,7 +20,7 @@ export default function PuduStepper(props: PuduStepperProps) { const steps = Array.from({ length: stepCount }, (_, index) => index + 1); return ( - + {steps.map((stepNumber) => { const isCompleted = isComplete || stepNumber < activeStep; const isCurrent = !isComplete && stepNumber === activeStep; @@ -32,10 +32,7 @@ export default function PuduStepper(props: PuduStepperProps) { key={stepNumber} orientation="vertical" indicator={ - + {isCompleted ? : stepNumber - 1 + startNumber} } @@ -45,5 +42,5 @@ export default function PuduStepper(props: PuduStepperProps) { ); })} - ) + ); } diff --git a/src/components/organisms/installations/InstallationCardList.tsx b/src/components/organisms/installations/InstallationCardList.tsx index 9a43792..24ebf65 100644 --- a/src/components/organisms/installations/InstallationCardList.tsx +++ b/src/components/organisms/installations/InstallationCardList.tsx @@ -11,9 +11,7 @@ export default function InstallationCardList() { - - Loading installations... - + Loading installations... )} diff --git a/src/components/organisms/servers/ServerCard.stories.tsx b/src/components/organisms/servers/ServerCard.stories.tsx index 88c71a3..a7b215d 100644 --- a/src/components/organisms/servers/ServerCard.stories.tsx +++ b/src/components/organisms/servers/ServerCard.stories.tsx @@ -1,6 +1,6 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { Meta, StoryObj } from "@storybook/react-vite"; -import ServerCard from './ServerCard'; +import ServerCard from "./ServerCard"; import { Box, GlobalStyles } from "@mui/joy"; const meta = { @@ -24,21 +24,25 @@ const meta = { decorators: [ (Story) => ( <> - - + + > ), - ] + ], } satisfies Meta; export default meta; @@ -63,7 +67,6 @@ export const Downloading: Story = { }, }; - export const Scanning: Story = { args: { actionLabel: "Scanning", diff --git a/src/components/organisms/servers/ServerCard.tsx b/src/components/organisms/servers/ServerCard.tsx index a0a76a5..7f70de6 100644 --- a/src/components/organisms/servers/ServerCard.tsx +++ b/src/components/organisms/servers/ServerCard.tsx @@ -1,20 +1,11 @@ -import { - AspectRatio, - LinearProgress, - Sheet, - Stack, -} from "@mui/joy"; +import { AspectRatio, LinearProgress, Sheet, Stack } from "@mui/joy"; import ServerCardActionButton from "../../molecules/servers/ServerCardActionButton"; import ServerCardDetails from "../../molecules/servers/ServerCardDetails"; import ServerCardHeader from "../../molecules/servers/ServerCardHeader"; -import {resolveServerAction} from "../../molecules/servers/serverCardActions"; -import type {ServerCardProps} from "./serverCard.types"; +import { resolveServerAction } from "../../molecules/servers/serverCardActions"; +import type { ServerCardProps } from "./serverCard.types"; -export type { - ServerActionState, - ServerCardProgress, - ServerCardProps, -} from "./serverCard.types"; +export type { ServerActionState, ServerCardProgress, ServerCardProps } from "./serverCard.types"; export default function ServerCard(props: ServerCardProps) { const { @@ -34,13 +25,10 @@ export default function ServerCard(props: ServerCardProps) { progress = null, } = props; - const { - resolvedActionState, - resolvedActionLabel, - actionVisual, - isBusyAction, - isDeterminate - } = resolveServerAction(actionLabel, actionState); + const { resolvedActionState, resolvedActionLabel, actionVisual, isBusyAction, isDeterminate } = resolveServerAction( + actionLabel, + actionState, + ); const shouldDisableAction = isActionDisabled || isBusyAction; @@ -48,8 +36,8 @@ export default function ServerCard(props: ServerCardProps) { const progressPercent = isScanningFailed ? 100 : progress - ? Math.round(Math.max(0, Math.min(progress.value, 100))) - : 0; + ? Math.round(Math.max(0, Math.min(progress.value, 100))) + : 0; const hasProgress = isScanningFailed || (progress !== null && progress !== undefined); return ( @@ -63,8 +51,12 @@ export default function ServerCard(props: ServerCardProps) { overflow: "hidden", }} > - - + + - + - - + + - + ; export default meta; @@ -17,28 +17,36 @@ export default meta; type Story = StoryObj; const renderAtPath = (path: string) => ( - - - - - - - - + + + + + + + + ); export const ServersActive: Story = { - render: () => renderAtPath("/"), + render: () => renderAtPath("/"), }; export const NewsActive: Story = { - render: () => renderAtPath("/news"), + render: () => renderAtPath("/news"), }; export const InstallationsActive: Story = { - render: () => renderAtPath("/installations"), + render: () => renderAtPath("/installations"), }; export const PreferencesActive: Story = { - render: () => renderAtPath("/preferences"), + render: () => renderAtPath("/preferences"), }; diff --git a/src/components/organisms/sideBar/SideBar.tsx b/src/components/organisms/sideBar/SideBar.tsx index e66d70c..e722126 100644 --- a/src/components/organisms/sideBar/SideBar.tsx +++ b/src/components/organisms/sideBar/SideBar.tsx @@ -1,8 +1,4 @@ -import { - Divider, - List, - Stack, Typography -} from "@mui/joy"; +import { Divider, List, Stack, Typography } from "@mui/joy"; import SideBarMenuItem from "../../molecules/sideBar/SideBarMenuItem.tsx"; import SideBarExternalLink from "../../molecules/sideBar/SideBarExternalLink.tsx"; import { useSideBarContext } from "../../../contextProviders/SideBarContextProvider.tsx"; @@ -11,13 +7,19 @@ export default function SideBar() { const { menuItems, externalLinks } = useSideBarContext(); return ( - + }} + > Pudu Launcher @@ -25,7 +27,7 @@ export default function SideBar() { - + {menuItems.map((item) => ( diff --git a/src/components/pages/IpcPermissionPage.tsx b/src/components/pages/IpcPermissionPage.tsx index 3674393..3143ebc 100644 --- a/src/components/pages/IpcPermissionPage.tsx +++ b/src/components/pages/IpcPermissionPage.tsx @@ -1,15 +1,6 @@ import { useEffect, useRef, useState } from "react"; import { useSearchParams } from "react-router"; -import { - Alert, - Box, - Button, - CssBaseline, - CssVarsProvider, - GlobalStyles, - Stack, - Typography, -} from "@mui/joy"; +import { Alert, Box, Button, CssBaseline, CssVarsProvider, GlobalStyles, Stack, Typography } from "@mui/joy"; import { IpcApi } from "../../pudu/generated"; import { useThemeContext } from "../../contextProviders/ThemeProvider"; import { themeRegistry, themeScrollbarRegistry } from "../../themes"; @@ -19,16 +10,13 @@ const isTauri = "__TAURI_INTERNALS__" in window; const POPUP_WIDTH = 420; const requestTypeDescriptions: Record string> = { - "1": (domain) => - `The game wants to open the following URL in your browser: ${domain}`, - "2": (domain) => - `The game wants to send API requests to the following domain: ${domain}`, + "1": (domain) => `The game wants to open the following URL in your browser: ${domain}`, + "2": (domain) => `The game wants to send API requests to the following domain: ${domain}`, "3": () => "The game is requesting trusted mode. This automatically allows every API and URL " + "action without prompts, and enables the Variable Viewer which can modify game data " + "and could potentially perform unwanted actions on your PC.", - "4": () => - "The game wants to access your microphone while it is running.", + "4": () => "The game wants to access your microphone while it is running.", }; export default function IpcPermissionPage() { @@ -119,29 +107,27 @@ export default function IpcPermissionPage() { userSelect: "none", }} > - + Game Permission Request - - {description} - + {description} {justification && ( Justification from the game: - + {justification} @@ -149,8 +135,7 @@ export default function IpcPermissionPage() { {isDangerous && ( - The text above was provided by the game, not by PuduLauncher. - Treat it with caution. + The text above was provided by the game, not by PuduLauncher. Treat it with caution. )} diff --git a/src/components/pages/ServersPage.tsx b/src/components/pages/ServersPage.tsx index 36c74a6..54ede3d 100644 --- a/src/components/pages/ServersPage.tsx +++ b/src/components/pages/ServersPage.tsx @@ -1,7 +1,5 @@ import ServersLayout from "../layouts/ServersLayout"; export default function ServersPage() { - return ( - - ) + return ; } diff --git a/src/constants/ttsStatus.ts b/src/constants/ttsStatus.ts index 2be458d..cad8e86 100644 --- a/src/constants/ttsStatus.ts +++ b/src/constants/ttsStatus.ts @@ -26,10 +26,7 @@ export const TTS_INSTALLED_STATUSES = new Set([ TTS_STATUS.ServerStarting, ]); -export const TTS_INSTALL_SESSION_START_STATUSES = new Set([ - TTS_STATUS.Downloading, - TTS_STATUS.Installing, -]); +export const TTS_INSTALL_SESSION_START_STATUSES = new Set([TTS_STATUS.Downloading, TTS_STATUS.Installing]); export const TTS_INSTALL_SESSION_BUSY_STATUSES = new Set([ TTS_STATUS.CheckingForUpdates, diff --git a/src/contextProviders/DiscordJoinContextProvider.tsx b/src/contextProviders/DiscordJoinContextProvider.tsx index 898d151..6227af0 100644 --- a/src/contextProviders/DiscordJoinContextProvider.tsx +++ b/src/contextProviders/DiscordJoinContextProvider.tsx @@ -1,9 +1,4 @@ -import { - type PropsWithChildren, - useEffect, - useRef, - useState, -} from "react"; +import { type PropsWithChildren, useEffect, useRef, useState } from "react"; import { EventListener } from "../pudu/events/event-listener"; import type { PuduEventMap, PuduEventType } from "../pudu/generated"; import { DiscordApi, type DiscordJoinRequestEvent } from "../pudu/generated"; diff --git a/src/contextProviders/FeedbackContextProvider.tsx b/src/contextProviders/FeedbackContextProvider.tsx index 1038874..657d981 100644 --- a/src/contextProviders/FeedbackContextProvider.tsx +++ b/src/contextProviders/FeedbackContextProvider.tsx @@ -1,11 +1,4 @@ -import { - createContext, - type PropsWithChildren, - useContext, - useEffect, - useRef, - useState, -} from "react"; +import { createContext, type PropsWithChildren, useContext, useEffect, useRef, useState } from "react"; import { CircularProgress, Modal, ModalDialog, Stack, Typography } from "@mui/joy"; import FatalErrorModal from "../components/molecules/errors/FatalErrorModal"; import FeedbackSnackbar from "../components/molecules/errors/FeedbackSnackbar"; @@ -171,13 +164,7 @@ function SnackbarHost(props: { register: (push: (item: SnackbarItem) => void) => const currentSnackbar = queue[0] ?? null; - return ( - - ); + return ; } export function FeedbackContextProvider(props: PropsWithChildren) { @@ -242,31 +229,37 @@ export function FeedbackContextProvider(props: PropsWithChildren) { }; const showError = (input: ErrorReportInput) => { - pushError({ - id: createId(), - severity: "error", - source: input.source, - userMessage: input.userMessage, - code: input.code, - technicalDetails: input.technicalDetails, - correlationId: input.correlationId, - isTransient: input.isTransient ?? true, - timestamp: new Date().toISOString(), - }, input.dedupe ?? true); + pushError( + { + id: createId(), + severity: "error", + source: input.source, + userMessage: input.userMessage, + code: input.code, + technicalDetails: input.technicalDetails, + correlationId: input.correlationId, + isTransient: input.isTransient ?? true, + timestamp: new Date().toISOString(), + }, + input.dedupe ?? true, + ); }; const showFatal = (input: ErrorReportInput) => { - pushError({ - id: createId(), - severity: "fatal", - source: input.source, - userMessage: input.userMessage, - code: input.code, - technicalDetails: input.technicalDetails, - correlationId: input.correlationId, - isTransient: false, - timestamp: new Date().toISOString(), - }, input.dedupe ?? true); + pushError( + { + id: createId(), + severity: "fatal", + source: input.source, + userMessage: input.userMessage, + code: input.code, + technicalDetails: input.technicalDetails, + correlationId: input.correlationId, + isTransient: false, + timestamp: new Date().toISOString(), + }, + input.dedupe ?? true, + ); }; const showSuccess = (input: FeedbackInput) => pushSnackbar("success", input); @@ -319,9 +312,7 @@ export function FeedbackContextProvider(props: PropsWithChildren) { // Catch unhandled frontend errors globally. useEffect(() => { const onError = (event: ErrorEvent) => { - const details = event.error instanceof Error - ? event.error.stack ?? event.error.message - : event.message; + const details = event.error instanceof Error ? (event.error.stack ?? event.error.message) : event.message; showFatal({ source: "frontend.window-error", @@ -333,9 +324,7 @@ export function FeedbackContextProvider(props: PropsWithChildren) { const onUnhandledRejection = (event: PromiseRejectionEvent) => { const reason = event.reason; - const details = reason instanceof Error - ? reason.stack ?? reason.message - : String(reason); + const details = reason instanceof Error ? (reason.stack ?? reason.message) : String(reason); showFatal({ source: "frontend.unhandled-rejection", @@ -368,13 +357,20 @@ export function FeedbackContextProvider(props: PropsWithChildren) { return ( - {backendReady ? children : ( + {backendReady ? ( + children + ) : ( - - - PuduLauncher - + + PuduLauncher diff --git a/src/contextProviders/InstallationsContextProvider.tsx b/src/contextProviders/InstallationsContextProvider.tsx index 1ee679e..cb40a16 100644 --- a/src/contextProviders/InstallationsContextProvider.tsx +++ b/src/contextProviders/InstallationsContextProvider.tsx @@ -264,11 +264,7 @@ export function InstallationsContextProvider(props: PropsWithChildren) { openInstallationsFolder, }; - return ( - - {children} - - ); + return {children}; } export function useInstallationsContext() { diff --git a/src/contextProviders/IpcContextProvider.tsx b/src/contextProviders/IpcContextProvider.tsx index df65eef..80f3ba2 100644 --- a/src/contextProviders/IpcContextProvider.tsx +++ b/src/contextProviders/IpcContextProvider.tsx @@ -1,8 +1,4 @@ -import { - type PropsWithChildren, - useEffect, - useRef, -} from "react"; +import { type PropsWithChildren, useEffect, useRef } from "react"; import { EventListener } from "../pudu/events/event-listener"; import type { IpcPermissionRequestEvent } from "../pudu/generated"; import { log } from "../pudu/log"; diff --git a/src/contextProviders/NewsContextProvider.tsx b/src/contextProviders/NewsContextProvider.tsx index e863649..6abbaee 100644 --- a/src/contextProviders/NewsContextProvider.tsx +++ b/src/contextProviders/NewsContextProvider.tsx @@ -170,11 +170,7 @@ export function NewsContextProvider(props: PropsWithChildren) { setPaused: setIsPaused, }; - return ( - - {children} - - ); + return {children}; } export function useNewsContext() { diff --git a/src/contextProviders/OnboardingContextProvider.tsx b/src/contextProviders/OnboardingContextProvider.tsx index 94ed48d..5b4da3b 100644 --- a/src/contextProviders/OnboardingContextProvider.tsx +++ b/src/contextProviders/OnboardingContextProvider.tsx @@ -1,10 +1,10 @@ -import {createContext, type PropsWithChildren, useContext, useEffect, useRef, useState} from "react"; -import {Alert, Box, Modal, ModalDialog, Stack, Typography} from "@mui/joy"; -import {OnboardingApi, type OnboardingStep} from "../pudu/generated"; -import {FeedbackContext} from "./FeedbackContextProvider"; -import {onboardingStepRegistry} from "./onboardingStepRegistry"; +import { createContext, type PropsWithChildren, useContext, useEffect, useRef, useState } from "react"; +import { Alert, Box, Modal, ModalDialog, Stack, Typography } from "@mui/joy"; +import { OnboardingApi, type OnboardingStep } from "../pudu/generated"; +import { FeedbackContext } from "./FeedbackContextProvider"; +import { onboardingStepRegistry } from "./onboardingStepRegistry"; import OnboardingProgressDots from "../components/layouts/onboarding/OnboardingProgressDots"; -import {OnboardingAnimatedContent, OnboardingBackground} from "../components/layouts/onboarding/OnboardingStepShell"; +import { OnboardingAnimatedContent, OnboardingBackground } from "../components/layouts/onboarding/OnboardingStepShell"; interface OnboardingContextValue { pendingSteps: OnboardingStep[]; @@ -32,10 +32,9 @@ export interface OnboardingContextProviderProps extends PropsWithChildren { } export function OnboardingContextProvider(props: OnboardingContextProviderProps) { - const {children, createApi, errorReporter} = props; + const { children, createApi, errorReporter } = props; const errorContext = useContext(FeedbackContext); - const showError = errorReporter ?? errorContext?.showError ?? (() => { - }); + const showError = errorReporter ?? errorContext?.showError ?? (() => {}); const buildApi = createApi ?? (() => new OnboardingApi()); const seenStepIdsRef = useRef(new Set()); @@ -161,9 +160,7 @@ export function OnboardingContextProvider(props: OnboardingContextProviderProps) advanceQueue(); }; - const ActiveStepComponent = activeStep - ? onboardingStepRegistry[activeStep.componentKey] - : null; + const ActiveStepComponent = activeStep ? onboardingStepRegistry[activeStep.componentKey] : null; const value: OnboardingContextValue = { pendingSteps, @@ -172,40 +169,30 @@ export function OnboardingContextProvider(props: OnboardingContextProviderProps) }; const missingComponentReferenceWarning = () => ( - - - {activeStep?.title ?? "Onboarding step"} - + + {activeStep?.title ?? "Onboarding step"} No component is registered for onboarding key "{activeStep?.componentKey}". {activeStep?.description && ( <> - - Description was: - - - {activeStep.description} - + Description was: + {activeStep.description} > )} ); const totalSteps = Math.max(allSteps.length, 1); - const activeStepIndex = activeStep - ? allSteps.findIndex((s) => s.id === activeStep.id) - : 0; + const activeStepIndex = activeStep ? allSteps.findIndex((s) => s.id === activeStep.id) : 0; return ( - {isLoading && ( - - )} + {isLoading && } {!isLoading && children} - + - + {ActiveStepComponent ? ( - ) : missingComponentReferenceWarning()} + ) : ( + missingComponentReferenceWarning() + )} {totalSteps > 1 && ( - + = 0 ? activeStepIndex : 0} diff --git a/src/contextProviders/PreferencesContextProvider.tsx b/src/contextProviders/PreferencesContextProvider.tsx index be0b5cc..2cc9e08 100644 --- a/src/contextProviders/PreferencesContextProvider.tsx +++ b/src/contextProviders/PreferencesContextProvider.tsx @@ -165,11 +165,7 @@ export function PreferencesContextProvider(props: PropsWithChildren) { moveInstallationPath, }; - return ( - - {children} - - ); + return {children}; } export function usePreferencesContext() { diff --git a/src/contextProviders/ServersContextProvider.tsx b/src/contextProviders/ServersContextProvider.tsx index c691826..46d5f87 100644 --- a/src/contextProviders/ServersContextProvider.tsx +++ b/src/contextProviders/ServersContextProvider.tsx @@ -72,11 +72,7 @@ export function ServersContextProvider(props: PropsWithChildren) { lastUpdatedLabel, }; - return ( - - {children} - - ); + return {children}; } export function useServersContext() { diff --git a/src/contextProviders/SideBarContextProvider.tsx b/src/contextProviders/SideBarContextProvider.tsx index c4690a0..899f17b 100644 --- a/src/contextProviders/SideBarContextProvider.tsx +++ b/src/contextProviders/SideBarContextProvider.tsx @@ -1,25 +1,13 @@ -import { - Favorite, - Message, - Newspaper, - Public, - Save, - Settings, - VideogameAsset -} from "@mui/icons-material"; +import { Favorite, Message, Newspaper, Public, Save, Settings, VideogameAsset } from "@mui/icons-material"; import { openUrl } from "@tauri-apps/plugin-opener"; -import { - createContext, - type PropsWithChildren, - useContext -} from "react"; +import { createContext, type PropsWithChildren, useContext } from "react"; import { useMatch, useNavigate } from "react-router"; import { SideBarMenuItemProps } from "../components/molecules/sideBar/SideBarMenuItem"; import { SideBarExternalLinkProps } from "../components/molecules/sideBar/SideBarExternalLink"; import { UNITYSTATION_DISCORD_URL, UNITYSTATION_PATREON_URL, - UNITYSTATION_WEBSITE_URL + UNITYSTATION_WEBSITE_URL, } from "../constants/externalLinks"; interface SideBarContextValue { @@ -94,11 +82,7 @@ export function SideBarContextProvider(props: PropsWithChildren) { externalLinks, }; - return ( - - {children} - - ); + return {children}; } export function useSideBarContext() { diff --git a/src/contextProviders/StepContextProvider.tsx b/src/contextProviders/StepContextProvider.tsx index 3cf2168..d2d2a7f 100644 --- a/src/contextProviders/StepContextProvider.tsx +++ b/src/contextProviders/StepContextProvider.tsx @@ -35,19 +35,19 @@ export function StepContextProvider(props: StepContextProps) { }; return ( - + {props.children} ); - } - export function useStepContext() { const context = useContext(StepContext); if (!context) { @@ -55,4 +55,4 @@ export function useStepContext() { } return context; -} \ No newline at end of file +} diff --git a/src/contextProviders/ThemeProvider.tsx b/src/contextProviders/ThemeProvider.tsx index a1bea5b..6e0ff7f 100644 --- a/src/contextProviders/ThemeProvider.tsx +++ b/src/contextProviders/ThemeProvider.tsx @@ -21,9 +21,7 @@ const THEME_ID_TO_PREFERENCE: Record = { vapor: "Vapor", }; -const normalizeThemeToken = (value: string): string => ( - value.toLowerCase().replace(/[^a-z0-9]/g, "") -); +const normalizeThemeToken = (value: string): string => value.toLowerCase().replace(/[^a-z0-9]/g, ""); const THEME_TOKEN_TO_ID: Record = Object.entries(themeRegistry).reduce( (accumulator, [themeId]) => { @@ -38,9 +36,7 @@ Object.entries(THEME_ID_TO_PREFERENCE).forEach(([themeId, preferenceValue]) => { THEME_TOKEN_TO_ID[normalizeThemeToken(preferenceValue)] = themeId as ThemeId; }); -const isThemeId = (value: unknown): value is ThemeId => ( - typeof value === "string" && value in themeRegistry -); +const isThemeId = (value: unknown): value is ThemeId => typeof value === "string" && value in themeRegistry; const parseThemeId = (value: unknown): ThemeId | null => { if (isThemeId(value)) { @@ -102,14 +98,12 @@ export function ThemeProvider(props: PropsWithChildren) { } })(); - return () => { cancelled = true; }; + return () => { + cancelled = true; + }; }, []); - return ( - - {props.children} - - ); + return {props.children}; } export function useThemeContext() { diff --git a/src/contextProviders/TtsInstallerContextProvider.tsx b/src/contextProviders/TtsInstallerContextProvider.tsx index e859972..98a0e70 100644 --- a/src/contextProviders/TtsInstallerContextProvider.tsx +++ b/src/contextProviders/TtsInstallerContextProvider.tsx @@ -1,6 +1,11 @@ import { createContext, type PropsWithChildren, useContext, useEffect, useRef, useState } from "react"; import TtsInstallerLayout from "../components/layouts/tts/TtsInstallerLayout"; -import { TTS_INSTALL_SESSION_BUSY_STATUSES, TTS_INSTALL_SESSION_START_STATUSES, TTS_STATUS, TTS_STATUS_LABELS } from "../constants/ttsStatus"; +import { + TTS_INSTALL_SESSION_BUSY_STATUSES, + TTS_INSTALL_SESSION_START_STATUSES, + TTS_STATUS, + TTS_STATUS_LABELS, +} from "../constants/ttsStatus"; import { useFeedbackContext } from "./FeedbackContextProvider"; import { useTtsState } from "./TtsStateContextProvider"; @@ -109,11 +114,10 @@ export function TtsInstallerContextProvider(props: PropsWithChildren) { setIsInstallerOpen(true); } - if (installSessionRef.current && ( - status === TTS_STATUS.Installed - || status === TTS_STATUS.NotInstalled - || status === TTS_STATUS.Error - )) { + if ( + installSessionRef.current && + (status === TTS_STATUS.Installed || status === TTS_STATUS.NotInstalled || status === TTS_STATUS.Error) + ) { setIsInstallerOpen(true); } @@ -171,9 +175,7 @@ export function TtsInstallerContextProvider(props: PropsWithChildren) { closeInstaller, }; - const statusLabel = status !== null - ? (TTS_STATUS_LABELS[status] ?? `Status ${status}`) - : "Unknown"; + const statusLabel = status !== null ? (TTS_STATUS_LABELS[status] ?? `Status ${status}`) : "Unknown"; // Derive the active step from the log contents on every render instead of // tracking it incrementally. An incremental index cursor desynced from // installLogs whenever the shared log array was cleared for a new session, @@ -184,17 +186,15 @@ export function TtsInstallerContextProvider(props: PropsWithChildren) { const parsed = inferStepFromLogLine(line); return parsed === null ? maxStep : Math.max(maxStep, parsed); }, 1); - const currentStep = Math.min( - Math.max(stepFromLogs, stepFromStatus(status)), - INSTALL_STEP_LABELS.length, - ); + const currentStep = Math.min(Math.max(stepFromLogs, stepFromStatus(status)), INSTALL_STEP_LABELS.length); const isInstallComplete = status === TTS_STATUS.Installed; const stepStatusMessage = isInstallComplete ? INSTALL_SUCCESS_MESSAGE : (INSTALL_STEP_STATUS_MESSAGES[currentStep - 1] ?? statusMessage ?? statusLabel); - const longStepWarning = isBusy && INSTALL_LONG_RUNNING_STEPS.has(currentStep) - ? "This step can take a few minutes. If it looks stuck, please wait. It's still working" - : null; + const longStepWarning = + isBusy && INSTALL_LONG_RUNNING_STEPS.has(currentStep) + ? "This step can take a few minutes. If it looks stuck, please wait. It's still working" + : null; const canRenderModal = isInstallerOpen && installSessionRef.current; diff --git a/src/contextProviders/TtsPreferencesContextProvider.tsx b/src/contextProviders/TtsPreferencesContextProvider.tsx index 9ab441c..2455faa 100644 --- a/src/contextProviders/TtsPreferencesContextProvider.tsx +++ b/src/contextProviders/TtsPreferencesContextProvider.tsx @@ -85,11 +85,7 @@ export function TtsPreferencesContextProvider(props: PropsWithChildren) { runCommand, }; - return ( - - {children} - - ); + return {children}; } export function useTtsPreferencesContext() { @@ -106,9 +102,5 @@ export interface TtsPreferencesTestProviderProps extends PropsWithChildren { } export function TtsPreferencesTestProvider(props: TtsPreferencesTestProviderProps) { - return ( - - {props.children} - - ); + return {props.children}; } diff --git a/src/contextProviders/TtsStateContextProvider.tsx b/src/contextProviders/TtsStateContextProvider.tsx index b8447da..3d94523 100644 --- a/src/contextProviders/TtsStateContextProvider.tsx +++ b/src/contextProviders/TtsStateContextProvider.tsx @@ -106,15 +106,15 @@ export function TtsStateContextProvider(props: PropsWithChildren) { return { ...previous, status: event.status, - errorMessage: event.status === TTS_STATUS.Error - ? event.message ?? previous.errorMessage - : null, + errorMessage: event.status === TTS_STATUS.Error ? (event.message ?? previous.errorMessage) : null, }; }); - if (event.status === TTS_STATUS.Installed - || event.status === TTS_STATUS.NotInstalled - || event.status === TTS_STATUS.Error) { + if ( + event.status === TTS_STATUS.Installed || + event.status === TTS_STATUS.NotInstalled || + event.status === TTS_STATUS.Error + ) { void loadStatus(); } }); @@ -187,11 +187,7 @@ export function TtsStateContextProvider(props: PropsWithChildren) { clearInstallLogs, }; - return ( - - {children} - - ); + return {children}; } export function useTtsState() { diff --git a/src/contextProviders/UpdateContextProvider.tsx b/src/contextProviders/UpdateContextProvider.tsx index 6eae7e7..dc6a0c5 100644 --- a/src/contextProviders/UpdateContextProvider.tsx +++ b/src/contextProviders/UpdateContextProvider.tsx @@ -76,7 +76,9 @@ export function UpdateContextProvider(props: PropsWithChildren) { } })(); - return () => { disposed = true; }; + return () => { + disposed = true; + }; }, []); const startUpdate = () => { @@ -145,7 +147,9 @@ export function UpdateContextProvider(props: PropsWithChildren) { onStartUpdate={startUpdate} onOpenReleasesPage={openReleasesPage} /> - ) : children} + ) : ( + children + )} ); } diff --git a/src/contextProviders/onboardingStepRegistry.tsx b/src/contextProviders/onboardingStepRegistry.tsx index ebb37f6..66702c9 100644 --- a/src/contextProviders/onboardingStepRegistry.tsx +++ b/src/contextProviders/onboardingStepRegistry.tsx @@ -13,7 +13,7 @@ export interface OnboardingStepComponentProps { export type OnboardingStepComponent = ComponentType; export const onboardingStepRegistry: Record = { - "welcome": WelcomeLayout, + welcome: WelcomeLayout, "launcher-basics": LauncherBasicsLayout, "all-ready": AllReadyLayout, }; diff --git a/src/contextProviders/servers.resolvers.ts b/src/contextProviders/servers.resolvers.ts index f19a826..b1223ee 100644 --- a/src/contextProviders/servers.resolvers.ts +++ b/src/contextProviders/servers.resolvers.ts @@ -48,9 +48,7 @@ export function resolveMapName(server: GameServer): string { return "Unknown map"; } - return rawMap - .replace(/^MainStations\//, "") - .replace(/\.json$/i, ""); + return rawMap.replace(/^MainStations\//, "").replace(/\.json$/i, ""); } function downloadStateToActionState(state: number): ServerActionState { diff --git a/src/contextProviders/useServerState.ts b/src/contextProviders/useServerState.ts index 15c4aed..22988c9 100644 --- a/src/contextProviders/useServerState.ts +++ b/src/contextProviders/useServerState.ts @@ -300,9 +300,7 @@ export function useServerState(options: UseServerStateOptions) { dedupe: false, }); } catch (error: unknown) { - const errorMessage = error instanceof Error - ? error.message - : "Failed to start download."; + const errorMessage = error instanceof Error ? error.message : "Failed to start download."; setDownloads((prev) => { const next = new Map(prev); diff --git a/src/devtools/DevToolsPage.tsx b/src/devtools/DevToolsPage.tsx index b9a7e61..077d197 100644 --- a/src/devtools/DevToolsPage.tsx +++ b/src/devtools/DevToolsPage.tsx @@ -39,11 +39,7 @@ function RequestLogRow({ entry }: { entry: RequestLogEntry }) { borderRadius: "sm", cursor: "pointer", borderLeftWidth: 3, - borderLeftColor: hasError - ? "danger.400" - : entry.wasMocked - ? "success.400" - : "neutral.400", + borderLeftColor: hasError ? "danger.400" : entry.wasMocked ? "success.400" : "neutral.400", }} onClick={() => setExpanded(!expanded)} > @@ -89,7 +85,9 @@ function RequestLogRow({ entry }: { entry: RequestLogEntry }) { {entry.requestBody !== null && ( - Request Body + + Request Body + {JSON.stringify(entry.requestBody, null, 2)} @@ -100,7 +98,9 @@ function RequestLogRow({ entry }: { entry: RequestLogEntry }) { {entry.responseBody !== null && ( - Response Body + + Response Body + & Pick) { +function LogTab({ + requestLog, + clearLog, +}: Pick & Pick) { return ( @@ -182,7 +185,9 @@ function MocksTab({ mocks, addMock, removeMock, clearAllMocks }: MocksTabProps) return ( - Add Mock + + Add Mock + Endpoint @@ -207,7 +212,9 @@ function MocksTab({ mocks, addMock, removeMock, clearAllMocks }: MocksTabProps) sx={{ fontFamily: "monospace", fontSize: 12 }} /> {jsonError && ( - {jsonError} + + {jsonError} + )} @@ -243,7 +250,10 @@ function MocksTab({ mocks, addMock, removeMock, clearAllMocks }: MocksTabProps) Remove - + {JSON.stringify(mock.response, null, 2)} @@ -271,14 +281,39 @@ const EVENT_TYPES: string[] = [ ]; const EVENT_TEMPLATES: Record = { - "discord:join-request": { eventType: "discord:join-request", timestamp: "", serverIp: "123.45.67.89", serverPort: 7777, serverName: "Test Server", forkName: "Unitystation", buildVersion: 1234, gameMode: "Secret", currentMap: "BoxStation", playerCount: 12, playerCountMax: 40, status: 0 }, + "discord:join-request": { + eventType: "discord:join-request", + timestamp: "", + serverIp: "123.45.67.89", + serverPort: 7777, + serverName: "Test Server", + forkName: "Unitystation", + buildVersion: 1234, + gameMode: "Secret", + currentMap: "BoxStation", + playerCount: 12, + playerCountMax: 40, + status: 0, + }, "tts:status-changed": { eventType: "tts:status-changed", timestamp: "", status: 0, message: "" }, - "tts:update-available": { eventType: "tts:update-available", timestamp: "", installedVersion: "1.0.0", latestVersion: "1.1.0" }, + "tts:update-available": { + eventType: "tts:update-available", + timestamp: "", + installedVersion: "1.0.0", + latestVersion: "1.1.0", + }, "tts:install-output": { eventType: "tts:install-output", timestamp: "", line: "" }, "servers:updated": { eventType: "servers:updated", timestamp: "" }, "installations:changed": { eventType: "installations:changed", timestamp: "" }, "game:state-changed": { eventType: "game:state-changed", timestamp: "" }, - "download:progress": { eventType: "download:progress", timestamp: "", downloadId: "", bytesDownloaded: 0, totalBytes: 0, speedBps: 0 }, + "download:progress": { + eventType: "download:progress", + timestamp: "", + downloadId: "", + bytesDownloaded: 0, + totalBytes: 0, + speedBps: 0, + }, "download:state-changed": { eventType: "download:state-changed", timestamp: "", downloadId: "", state: 0 }, }; @@ -295,7 +330,13 @@ interface EventsTabProps { addMock: DevToolsChannelActions["addMock"]; } -function PresetRunner({ injectEvent, addMock }: { injectEvent: DevToolsChannelActions["injectEvent"]; addMock: DevToolsChannelActions["addMock"] }) { +function PresetRunner({ + injectEvent, + addMock, +}: { + injectEvent: DevToolsChannelActions["injectEvent"]; + addMock: DevToolsChannelActions["addMock"]; +}) { const [runningLabel, setRunningLabel] = useState(null); const cancelRef = useRef(false); @@ -338,7 +379,9 @@ function PresetRunner({ injectEvent, addMock }: { injectEvent: DevToolsChannelAc return ( - TTS Presets + + TTS Presets + {ttsPresets.map((preset) => ( (EVENT_TYPES[0]); - const [payload, setPayload] = useState(JSON.stringify(EVENT_TEMPLATES[EVENT_TYPES[0]] ?? { eventType: EVENT_TYPES[0], timestamp: "" }, null, 2)); + const [payload, setPayload] = useState( + JSON.stringify(EVENT_TEMPLATES[EVENT_TYPES[0]] ?? { eventType: EVENT_TYPES[0], timestamp: "" }, null, 2), + ); const [jsonError, setJsonError] = useState(null); const [firedEvents, setFiredEvents] = useState([]); @@ -404,17 +449,23 @@ function EventsTab({ injectEvent, addMock }: EventsTabProps) { - Inject Event + + Inject Event + Event Type { if (v) handleTypeChange(v); }} + onChange={(_e, v) => { + if (v) handleTypeChange(v); + }} sx={{ fontFamily: "monospace", fontSize: 13 }} > {EVENT_TYPES.map((t) => ( - {t} + + {t} + ))} @@ -431,7 +482,9 @@ function EventsTab({ injectEvent, addMock }: EventsTabProps) { sx={{ fontFamily: "monospace", fontSize: 12 }} /> {jsonError && ( - {jsonError} + + {jsonError} + )} @@ -499,8 +552,15 @@ function StateTab({ stateSnapshot, requestStateSnapshot, isActive }: StateTabPro {Object.entries(stateSnapshot).map(([name, data]) => ( - - {name} + + + {name} + - setActiveTab(v as string)} sx={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}> + setActiveTab(v as string)} + sx={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }} + > Log Mocks diff --git a/src/devtools/bridge.ts b/src/devtools/bridge.ts index 3a0ade2..cef8ca4 100644 --- a/src/devtools/bridge.ts +++ b/src/devtools/bridge.ts @@ -4,90 +4,89 @@ import { DEVTOOLS_CHANNEL } from "./protocol"; type StateSource = () => unknown; class DevToolsBridgeImpl { - private channel: BroadcastChannel; - private mocks = new Map(); - private stateSources = new Map(); - private eventInjectors = new Set<(eventType: string, data: unknown) => void>(); + private channel: BroadcastChannel; + private mocks = new Map(); + private stateSources = new Map(); + private eventInjectors = new Set<(eventType: string, data: unknown) => void>(); - constructor() { - this.channel = new BroadcastChannel(DEVTOOLS_CHANNEL); - this.channel.onmessage = (event: MessageEvent) => { - this.handleCommand(event.data); - }; - } + constructor() { + this.channel = new BroadcastChannel(DEVTOOLS_CHANNEL); + this.channel.onmessage = (event: MessageEvent) => { + this.handleCommand(event.data); + }; + } - private handleCommand(command: DevToolsCommand): void { - switch (command.type) { - case "mock-endpoint": - this.mocks.set(command.endpoint, command.response); - break; - case "clear-mock": - this.mocks.delete(command.endpoint); - break; - case "clear-all-mocks": - this.mocks.clear(); - break; - case "inject-event": - for (const injector of this.eventInjectors) { - injector(command.eventType, command.data); - } - this.report({ - type: "event-injected", - eventType: command.eventType, - data: command.data, - timestamp: Date.now(), - }); - break; - case "request-state-snapshot": { - const sources: Record = {}; - for (const [name, fn] of this.stateSources) { - try { - sources[name] = fn(); - } catch { - sources[name] = { error: "Failed to collect state" }; - } + private handleCommand(command: DevToolsCommand): void { + switch (command.type) { + case "mock-endpoint": + this.mocks.set(command.endpoint, command.response); + break; + case "clear-mock": + this.mocks.delete(command.endpoint); + break; + case "clear-all-mocks": + this.mocks.clear(); + break; + case "inject-event": + for (const injector of this.eventInjectors) { + injector(command.eventType, command.data); + } + this.report({ + type: "event-injected", + eventType: command.eventType, + data: command.data, + timestamp: Date.now(), + }); + break; + case "request-state-snapshot": { + const sources: Record = {}; + for (const [name, fn] of this.stateSources) { + try { + sources[name] = fn(); + } catch { + sources[name] = { error: "Failed to collect state" }; + } + } + this.report({ type: "state-snapshot", sources }); + break; + } } - this.report({ type: "state-snapshot", sources }); - break; - } } - } - report(message: DevToolsReport): void { - this.channel.postMessage(message); - } + report(message: DevToolsReport): void { + this.channel.postMessage(message); + } - getMock(endpoint: string): unknown | undefined { - return this.mocks.get(endpoint); - } + getMock(endpoint: string): unknown | undefined { + return this.mocks.get(endpoint); + } - hasMock(endpoint: string): boolean { - return this.mocks.has(endpoint); - } + hasMock(endpoint: string): boolean { + return this.mocks.has(endpoint); + } - registerStateSource(name: string, fn: StateSource): () => void { - this.stateSources.set(name, fn); - return () => { - this.stateSources.delete(name); - }; - } + registerStateSource(name: string, fn: StateSource): () => void { + this.stateSources.set(name, fn); + return () => { + this.stateSources.delete(name); + }; + } - registerEventInjector(fn: (eventType: string, data: unknown) => void): () => void { - this.eventInjectors.add(fn); - return () => { - this.eventInjectors.delete(fn); - }; - } + registerEventInjector(fn: (eventType: string, data: unknown) => void): () => void { + this.eventInjectors.add(fn); + return () => { + this.eventInjectors.delete(fn); + }; + } - logRequest(entry: RequestLogEntry): void { - this.report({ type: "request-logged", entry }); - } + logRequest(entry: RequestLogEntry): void { + this.report({ type: "request-logged", entry }); + } - dispose(): void { - this.channel.close(); - } + dispose(): void { + this.channel.close(); + } } // Singleton, only created in dev -export const devBridge: DevToolsBridgeImpl | null = - import.meta.env.DEV ? new DevToolsBridgeImpl() : null; +export const devBridge: DevToolsBridgeImpl | null = import.meta.env.DEV ? new DevToolsBridgeImpl() : null; diff --git a/src/devtools/open-devtools-window.ts b/src/devtools/open-devtools-window.ts index 9a38827..be181eb 100644 --- a/src/devtools/open-devtools-window.ts +++ b/src/devtools/open-devtools-window.ts @@ -3,24 +3,24 @@ import { WebviewWindow } from "@tauri-apps/api/webviewWindow"; let devtoolsWindow: WebviewWindow | null = null; export async function openDevToolsWindow(): Promise { - // Check if already open - if (devtoolsWindow) { - try { - await devtoolsWindow.setFocus(); - return; - } catch { - devtoolsWindow = null; + // Check if already open + if (devtoolsWindow) { + try { + await devtoolsWindow.setFocus(); + return; + } catch { + devtoolsWindow = null; + } } - } - devtoolsWindow = new WebviewWindow("pudu-devtools", { - url: "/devtools", - title: "PuduLauncher DevTools", - width: 900, - height: 600, - }); + devtoolsWindow = new WebviewWindow("pudu-devtools", { + url: "/devtools", + title: "PuduLauncher DevTools", + width: 900, + height: 600, + }); - devtoolsWindow.once("tauri://destroyed", () => { - devtoolsWindow = null; - }); + devtoolsWindow.once("tauri://destroyed", () => { + devtoolsWindow = null; + }); } diff --git a/src/devtools/presets/tts-presets.ts b/src/devtools/presets/tts-presets.ts index 6a51184..2ee10bc 100644 --- a/src/devtools/presets/tts-presets.ts +++ b/src/devtools/presets/tts-presets.ts @@ -21,55 +21,151 @@ export const ttsPresets: EventPreset[] = [ { label: "Status \u2192 NotInstalled", description: "Set TTS status to NotInstalled", - mocks: [{ endpoint: "/api/tts/get-status", response: { success: true, data: { status: 0, updateAvailable: false } } }], + mocks: [ + { + endpoint: "/api/tts/get-status", + response: { success: true, data: { status: 0, updateAvailable: false } }, + }, + ], events: [ - { eventType: "tts:status-changed", data: { eventType: "tts:status-changed", timestamp: now(), status: 0, message: null } }, + { + eventType: "tts:status-changed", + data: { eventType: "tts:status-changed", timestamp: now(), status: 0, message: null }, + }, ], }, { label: "Status \u2192 Installed", description: "Set TTS status to Installed", - mocks: [{ endpoint: "/api/tts/get-status", response: { success: true, data: { status: 4, updateAvailable: false, installedVersion: "1.0.0" } } }], + mocks: [ + { + endpoint: "/api/tts/get-status", + response: { success: true, data: { status: 4, updateAvailable: false, installedVersion: "1.0.0" } }, + }, + ], events: [ - { eventType: "tts:status-changed", data: { eventType: "tts:status-changed", timestamp: now(), status: 4, message: "Installation complete" } }, + { + eventType: "tts:status-changed", + data: { + eventType: "tts:status-changed", + timestamp: now(), + status: 4, + message: "Installation complete", + }, + }, ], }, { label: "Status \u2192 ServerRunning", description: "Set TTS status to ServerRunning", events: [ - { eventType: "tts:status-changed", data: { eventType: "tts:status-changed", timestamp: now(), status: 6, message: "Server started" } }, + { + eventType: "tts:status-changed", + data: { eventType: "tts:status-changed", timestamp: now(), status: 6, message: "Server started" }, + }, ], }, { label: "Status \u2192 Error", description: "Set TTS status to Error with a message", - mocks: [{ endpoint: "/api/tts/get-status", response: { success: true, data: { status: 8, updateAvailable: false, errorMessage: "Something went wrong" } } }], + mocks: [ + { + endpoint: "/api/tts/get-status", + response: { + success: true, + data: { status: 8, updateAvailable: false, errorMessage: "Something went wrong" }, + }, + }, + ], events: [ - { eventType: "tts:status-changed", data: { eventType: "tts:status-changed", timestamp: now(), status: 8, message: "Something went wrong" } }, + { + eventType: "tts:status-changed", + data: { eventType: "tts:status-changed", timestamp: now(), status: 8, message: "Something went wrong" }, + }, ], }, { label: "Update Available", description: "Trigger update available notification", events: [ - { eventType: "tts:update-available", data: { eventType: "tts:update-available", timestamp: now(), installedVersion: "1.0.0", latestVersion: "1.0.2" } }, + { + eventType: "tts:update-available", + data: { + eventType: "tts:update-available", + timestamp: now(), + installedVersion: "1.0.0", + latestVersion: "1.0.2", + }, + }, ], }, { label: "Simulate Install Flow", description: "Full install sequence: Downloading \u2192 Installing \u2192 output lines \u2192 Installed", - mocks: [{ endpoint: "/api/tts/get-status", response: { success: true, data: { status: 4, updateAvailable: false, installedVersion: "1.0.0" } } }], + mocks: [ + { + endpoint: "/api/tts/get-status", + response: { success: true, data: { status: 4, updateAvailable: false, installedVersion: "1.0.0" } }, + }, + ], events: [ - { eventType: "tts:status-changed", data: { eventType: "tts:status-changed", timestamp: now(), status: 2, message: "Downloading..." } }, - { eventType: "tts:status-changed", data: { eventType: "tts:status-changed", timestamp: now(), status: 3, message: "Installing..." }, delayMs: 1500 }, - { eventType: "tts:install-output", data: { eventType: "tts:install-output", timestamp: now(), line: "[1 / 6] Checking Python installation..." }, delayMs: 500 }, - { eventType: "tts:install-output", data: { eventType: "tts:install-output", timestamp: now(), line: "[2 / 6] Creating virtual environment..." }, delayMs: 1000 }, - { eventType: "tts:install-output", data: { eventType: "tts:install-output", timestamp: now(), line: "[3 / 6] Installing requirements..." }, delayMs: 1000 }, - { eventType: "tts:install-output", data: { eventType: "tts:install-output", timestamp: now(), line: "[4 / 6] Installing eSpeak..." }, delayMs: 2000 }, - { eventType: "tts:install-output", data: { eventType: "tts:install-output", timestamp: now(), line: "[5 / 6] Downloading TTS model..." }, delayMs: 1500 }, - { eventType: "tts:install-output", data: { eventType: "tts:install-output", timestamp: now(), line: "[6 / 6] Writing manifest..." }, delayMs: 1000 }, - { eventType: "tts:status-changed", data: { eventType: "tts:status-changed", timestamp: now(), status: 4, message: "Installation complete" }, delayMs: 500 }, + { + eventType: "tts:status-changed", + data: { eventType: "tts:status-changed", timestamp: now(), status: 2, message: "Downloading..." }, + }, + { + eventType: "tts:status-changed", + data: { eventType: "tts:status-changed", timestamp: now(), status: 3, message: "Installing..." }, + delayMs: 1500, + }, + { + eventType: "tts:install-output", + data: { + eventType: "tts:install-output", + timestamp: now(), + line: "[1 / 6] Checking Python installation...", + }, + delayMs: 500, + }, + { + eventType: "tts:install-output", + data: { + eventType: "tts:install-output", + timestamp: now(), + line: "[2 / 6] Creating virtual environment...", + }, + delayMs: 1000, + }, + { + eventType: "tts:install-output", + data: { eventType: "tts:install-output", timestamp: now(), line: "[3 / 6] Installing requirements..." }, + delayMs: 1000, + }, + { + eventType: "tts:install-output", + data: { eventType: "tts:install-output", timestamp: now(), line: "[4 / 6] Installing eSpeak..." }, + delayMs: 2000, + }, + { + eventType: "tts:install-output", + data: { eventType: "tts:install-output", timestamp: now(), line: "[5 / 6] Downloading TTS model..." }, + delayMs: 1500, + }, + { + eventType: "tts:install-output", + data: { eventType: "tts:install-output", timestamp: now(), line: "[6 / 6] Writing manifest..." }, + delayMs: 1000, + }, + { + eventType: "tts:status-changed", + data: { + eventType: "tts:status-changed", + timestamp: now(), + status: 4, + message: "Installation complete", + }, + delayMs: 500, + }, ], }, ]; diff --git a/src/devtools/protocol.ts b/src/devtools/protocol.ts index e39c460..9bb298e 100644 --- a/src/devtools/protocol.ts +++ b/src/devtools/protocol.ts @@ -1,33 +1,33 @@ export interface RequestLogEntry { - id: string; - timestamp: number; - method: string; - endpoint: string; - requestBody: unknown | null; - responseBody: unknown | null; - wasMocked: boolean; - durationMs: number | null; - error: string | null; + id: string; + timestamp: number; + method: string; + endpoint: string; + requestBody: unknown | null; + responseBody: unknown | null; + wasMocked: boolean; + durationMs: number | null; + error: string | null; } export interface MockConfig { - endpoint: string; - response: unknown; + endpoint: string; + response: unknown; } // Commands: dev panel → main window export type DevToolsCommand = - | { type: "mock-endpoint"; endpoint: string; response: unknown } - | { type: "clear-mock"; endpoint: string } - | { type: "clear-all-mocks" } - | { type: "inject-event"; eventType: string; data: unknown } - | { type: "request-state-snapshot" }; + | { type: "mock-endpoint"; endpoint: string; response: unknown } + | { type: "clear-mock"; endpoint: string } + | { type: "clear-all-mocks" } + | { type: "inject-event"; eventType: string; data: unknown } + | { type: "request-state-snapshot" }; // Reports: main window → dev panel export type DevToolsReport = - | { type: "request-logged"; entry: RequestLogEntry } - | { type: "event-injected"; eventType: string; data: unknown; timestamp: number } - | { type: "state-snapshot"; sources: Record }; + | { type: "request-logged"; entry: RequestLogEntry } + | { type: "event-injected"; eventType: string; data: unknown; timestamp: number } + | { type: "state-snapshot"; sources: Record }; export type DevToolsMessage = DevToolsCommand | DevToolsReport; diff --git a/src/hooks/useInstallationState.ts b/src/hooks/useInstallationState.ts index f74325d..79ff729 100644 --- a/src/hooks/useInstallationState.ts +++ b/src/hooks/useInstallationState.ts @@ -51,9 +51,7 @@ export function useInstallationState() { }, []); const deleteInstallation = async (id: string) => { - setInstallations((prev) => - prev ? prev.filter((i) => i.id !== id) : prev, - ); + setInstallations((prev) => (prev ? prev.filter((i) => i.id !== id) : prev)); const api = new InstallationsApi(); try { diff --git a/src/hooks/usePaginatedCollection.ts b/src/hooks/usePaginatedCollection.ts index 3209d80..f39a4db 100644 --- a/src/hooks/usePaginatedCollection.ts +++ b/src/hooks/usePaginatedCollection.ts @@ -19,7 +19,7 @@ interface UsePaginatedCollectionResult { export function usePaginatedCollection( items: T[], pageSize: number, - options?: UsePaginatedCollectionOptions + options?: UsePaginatedCollectionOptions, ): UsePaginatedCollectionResult { const normalizedPageSize = Math.max(1, Math.floor(pageSize)); const totalItems = items.length; diff --git a/src/main.tsx b/src/main.tsx index 08ba942..0a6d03e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,33 +6,33 @@ const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement) // Dev tools window gets its own minimal render tree, no ThemeProvider, no App. // This avoids touching App.tsx which would break React Compiler's JSX memoization. if (import.meta.env.DEV && window.location.pathname === "/devtools") { - import("./devtools/DevToolsPage").then(({ default: DevToolsPage }) => { - root.render( - - - , - ); - }); + import("./devtools/DevToolsPage").then(({ default: DevToolsPage }) => { + root.render( + + + , + ); + }); } else { - // Normal app, static imports keep App.tsx identical to its original form - const { ThemeProvider } = await import("./contextProviders/ThemeProvider"); - const { default: App } = await import("./App"); + // Normal app, static imports keep App.tsx identical to its original form + const { ThemeProvider } = await import("./contextProviders/ThemeProvider"); + const { default: App } = await import("./App"); - root.render( - - - - - , - ); + root.render( + + + + + , + ); - // Ctrl+Shift+D opens devtools window (dev only, outside React tree) - if (import.meta.env.DEV) { - window.addEventListener("keydown", (e) => { - if (e.ctrlKey && e.shiftKey && e.key === "D") { - e.preventDefault(); - void import("./devtools/open-devtools-window").then((m) => m.openDevToolsWindow()); - } - }); - } + // Ctrl+Shift+D opens devtools window (dev only, outside React tree) + if (import.meta.env.DEV) { + window.addEventListener("keydown", (e) => { + if (e.ctrlKey && e.shiftKey && e.key === "D") { + e.preventDefault(); + void import("./devtools/open-devtools-window").then((m) => m.openDevToolsWindow()); + } + }); + } } diff --git a/src/pudu/events/event-listener.ts b/src/pudu/events/event-listener.ts index 9b66b37..e04fb20 100644 --- a/src/pudu/events/event-listener.ts +++ b/src/pudu/events/event-listener.ts @@ -1,5 +1,5 @@ -import type { EventBase, PuduEvent, PuduEventMap, PuduEventType } from '../generated/types'; -import { getSidecarWsUrl } from '../sidecar'; +import type { EventBase, PuduEvent, PuduEventMap, PuduEventType } from "../generated/types"; +import { getSidecarWsUrl } from "../sidecar"; /** * WebSocket-based event listener for receiving real-time events from the sidecar. @@ -7,193 +7,190 @@ import { getSidecarWsUrl } from '../sidecar'; type EventHandler = (event: PuduEvent) => void; export class EventListener { - private ws: WebSocket | null = null; - private handlers = new Map(); - private reconnectInterval = 3000; - private reconnectTimer: number | null = null; - private shouldReconnect = true; - private connectionSession = 0; - private connectPromise: Promise | null = null; - - async connect(): Promise { - this.shouldReconnect = true; - - if (this.ws?.readyState === WebSocket.OPEN || this.ws?.readyState === WebSocket.CONNECTING) { - return; - } - - if (this.connectPromise) { - return this.connectPromise; - } - - this.connectPromise = this.doConnect(); - try { - await this.connectPromise; - } finally { - this.connectPromise = null; - } - } - - private async doConnect(): Promise { - const session = this.connectionSession; - - try { - const wsUrl = await getSidecarWsUrl(); - if (!this.shouldReconnect || session !== this.connectionSession) { - return; - } - - if (this.ws?.readyState === WebSocket.OPEN || this.ws?.readyState === WebSocket.CONNECTING) { - return; - } - - const ws = new WebSocket(`${wsUrl}/events`); - this.ws = ws; + private ws: WebSocket | null = null; + private handlers = new Map(); + private reconnectInterval = 3000; + private reconnectTimer: number | null = null; + private shouldReconnect = true; + private connectionSession = 0; + private connectPromise: Promise | null = null; + + async connect(): Promise { + this.shouldReconnect = true; + + if (this.ws?.readyState === WebSocket.OPEN || this.ws?.readyState === WebSocket.CONNECTING) { + return; + } - ws.onopen = () => { - if (!this.shouldReconnect || session !== this.connectionSession) { - ws.close(); - return; + if (this.connectPromise) { + return this.connectPromise; } - console.log('Connected to event stream'); - if (this.reconnectTimer) { - clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; + this.connectPromise = this.doConnect(); + try { + await this.connectPromise; + } finally { + this.connectPromise = null; } - }; + } + + private async doConnect(): Promise { + const session = this.connectionSession; - ws.onmessage = (msg) => { try { - const event = JSON.parse(msg.data) as EventBase; - const eventType = event.eventType as PuduEventType; - const handlers = this.handlers.get(eventType); - - if (handlers) { - handlers.forEach((handler) => handler(event as PuduEvent)); - } else { - console.log('Unhandled event type:', event.eventType); - } + const wsUrl = await getSidecarWsUrl(); + if (!this.shouldReconnect || session !== this.connectionSession) { + return; + } + + if (this.ws?.readyState === WebSocket.OPEN || this.ws?.readyState === WebSocket.CONNECTING) { + return; + } + + const ws = new WebSocket(`${wsUrl}/events`); + this.ws = ws; + + ws.onopen = () => { + if (!this.shouldReconnect || session !== this.connectionSession) { + ws.close(); + return; + } + + console.log("Connected to event stream"); + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + }; + + ws.onmessage = (msg) => { + try { + const event = JSON.parse(msg.data) as EventBase; + const eventType = event.eventType as PuduEventType; + const handlers = this.handlers.get(eventType); + + if (handlers) { + handlers.forEach((handler) => handler(event as PuduEvent)); + } else { + console.log("Unhandled event type:", event.eventType); + } + } catch (error) { + console.error("Error parsing event:", error); + } + }; + + ws.onerror = (error) => { + console.error("WebSocket error:", error); + }; + + ws.onclose = () => { + if (this.ws === ws) { + this.ws = null; + } + + console.log("Disconnected from event stream"); + if (this.shouldReconnect && session === this.connectionSession) { + this.scheduleReconnect(); + } + }; } catch (error) { - console.error('Error parsing event:', error); + console.error("Failed to connect to event stream:", error); + if (this.shouldReconnect && session === this.connectionSession) { + this.scheduleReconnect(); + } } - }; + } - ws.onerror = (error) => { - console.error('WebSocket error:', error); - }; + on( + eventType: TEventType, + handler: (event: PuduEventMap[TEventType]) => void, + ): void { + if (!this.handlers.has(eventType)) { + this.handlers.set(eventType, []); + } - ws.onclose = () => { - if (this.ws === ws) { - this.ws = null; + this.handlers.get(eventType)!.push(handler as EventHandler); + void this.connect(); + } + + off( + eventType: TEventType, + handler?: (event: PuduEventMap[TEventType]) => void, + ): void { + if (!handler) { + this.handlers.delete(eventType); + if (!this.hasHandlers()) { + this.disconnect(); + } + return; } - console.log('Disconnected from event stream'); - if (this.shouldReconnect && session === this.connectionSession) { - this.scheduleReconnect(); + const handlers = this.handlers.get(eventType); + if (handlers) { + const index = handlers.indexOf(handler as EventHandler); + if (index !== -1) { + handlers.splice(index, 1); + if (handlers.length === 0) { + this.handlers.delete(eventType); + } + } } - }; - } catch (error) { - console.error('Failed to connect to event stream:', error); - if (this.shouldReconnect && session === this.connectionSession) { - this.scheduleReconnect(); - } - } - } - - on( - eventType: TEventType, - handler: (event: PuduEventMap[TEventType]) => void - ): void { - if (!this.handlers.has(eventType)) { - this.handlers.set(eventType, []); - } - this.handlers.get(eventType)!.push(handler as EventHandler); - void this.connect(); - } - - off( - eventType: TEventType, - handler?: (event: PuduEventMap[TEventType]) => void - ): void { - if (!handler) { - this.handlers.delete(eventType); - if (!this.hasHandlers()) { - this.disconnect(); - } - return; + if (!this.hasHandlers()) { + this.disconnect(); + } } - const handlers = this.handlers.get(eventType); - if (handlers) { - const index = handlers.indexOf(handler as EventHandler); - if (index !== -1) { - handlers.splice(index, 1); - if (handlers.length === 0) { - this.handlers.delete(eventType); + injectEvent(eventType: TEventType, data: PuduEventMap[TEventType]): void { + const handlers = this.handlers.get(eventType); + if (handlers) { + handlers.forEach((handler) => handler(data as PuduEvent)); } - } } - if (!this.hasHandlers()) { - this.disconnect(); - } - } - - injectEvent( - eventType: TEventType, - data: PuduEventMap[TEventType] - ): void { - const handlers = this.handlers.get(eventType); - if (handlers) { - handlers.forEach((handler) => handler(data as PuduEvent)); - } - } + disconnect(): void { + this.shouldReconnect = false; + this.connectionSession += 1; + this.connectPromise = null; - disconnect(): void { - this.shouldReconnect = false; - this.connectionSession += 1; - this.connectPromise = null; + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } - if (this.reconnectTimer) { - clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; + if (this.ws) { + this.ws.onclose = null; + this.ws.close(); + this.ws = null; + } } - if (this.ws) { - this.ws.onclose = null; - this.ws.close(); - this.ws = null; - } - } + private scheduleReconnect(): void { + if (this.reconnectTimer) { + return; + } - private scheduleReconnect(): void { - if (this.reconnectTimer) { - return; - } + const session = this.connectionSession; - const session = this.connectionSession; + this.reconnectTimer = window.setTimeout(() => { + this.reconnectTimer = null; - this.reconnectTimer = window.setTimeout(() => { - this.reconnectTimer = null; + if (!this.shouldReconnect || session !== this.connectionSession) { + return; + } - if (!this.shouldReconnect || session !== this.connectionSession) { - return; - } + console.log("Attempting to reconnect..."); + this.connect(); + }, this.reconnectInterval); + } - console.log('Attempting to reconnect...'); - this.connect(); - }, this.reconnectInterval); - } + private hasHandlers(): boolean { + for (const handlers of this.handlers.values()) { + if (handlers.length > 0) { + return true; + } + } - private hasHandlers(): boolean { - for (const handlers of this.handlers.values()) { - if (handlers.length > 0) { - return true; - } + return false; } - - return false; - } } diff --git a/src/pudu/log.ts b/src/pudu/log.ts index 4ced1ee..db381d0 100644 --- a/src/pudu/log.ts +++ b/src/pudu/log.ts @@ -1,24 +1,21 @@ -import { info, warn, error, debug, trace } from '@tauri-apps/plugin-log'; +import { info, warn, error, debug, trace } from "@tauri-apps/plugin-log"; -const isTauri = '__TAURI_INTERNALS__' in window; +const isTauri = "__TAURI_INTERNALS__" in window; -function wrap( - tauriFn: (msg: string) => Promise, - consoleFn: (...args: unknown[]) => void, -): (msg: string) => void { - return (msg: string) => { - if (isTauri) { - tauriFn(`[PuduFrontend] ${msg}`); - } else { - consoleFn(`[PuduFrontend] ${msg}`); - } - }; +function wrap(tauriFn: (msg: string) => Promise, consoleFn: (...args: unknown[]) => void): (msg: string) => void { + return (msg: string) => { + if (isTauri) { + tauriFn(`[PuduFrontend] ${msg}`); + } else { + consoleFn(`[PuduFrontend] ${msg}`); + } + }; } export const log = { - info: wrap(info, console.log), - warn: wrap(warn, console.warn), - error: wrap(error, console.error), - debug: wrap(debug, console.debug), - trace: wrap(trace, console.trace), + info: wrap(info, console.log), + warn: wrap(warn, console.warn), + error: wrap(error, console.error), + debug: wrap(debug, console.debug), + trace: wrap(trace, console.trace), }; diff --git a/src/pudu/pudu-fetch.ts b/src/pudu/pudu-fetch.ts index 1bdaf4d..50178ca 100644 --- a/src/pudu/pudu-fetch.ts +++ b/src/pudu/pudu-fetch.ts @@ -3,99 +3,99 @@ import { getSidecarBaseUrl } from "./sidecar"; let idCounter = 0; -export async function puduFetch( - endpoint: string, - init?: RequestInit, -): Promise { - const baseUrl = await getSidecarBaseUrl(); - const url = `${baseUrl}${endpoint}`; +export async function puduFetch(endpoint: string, init?: RequestInit): Promise { + const baseUrl = await getSidecarBaseUrl(); + const url = `${baseUrl}${endpoint}`; - if (!import.meta.env.DEV || !devBridge) { - return fetch(url, init); - } + if (!import.meta.env.DEV || !devBridge) { + return fetch(url, init); + } - const id = `req-${++idCounter}`; - const timestamp = Date.now(); - let requestBody: unknown = null; - if (init?.body && typeof init.body === "string") { - try { - requestBody = JSON.parse(init.body); - } catch { - requestBody = init.body; + const id = `req-${++idCounter}`; + const timestamp = Date.now(); + let requestBody: unknown = null; + if (init?.body && typeof init.body === "string") { + try { + requestBody = JSON.parse(init.body); + } catch { + requestBody = init.body; + } } - } - // Check for mock - if (devBridge.hasMock(endpoint)) { - const mockResponse = devBridge.getMock(endpoint); + // Check for mock + if (devBridge.hasMock(endpoint)) { + const mockResponse = devBridge.getMock(endpoint); - devBridge.logRequest({ - id, - timestamp, - method: init?.method ?? "GET", - endpoint, - requestBody, - responseBody: mockResponse, - wasMocked: true, - durationMs: 0, - error: null, - }); + devBridge.logRequest({ + id, + timestamp, + method: init?.method ?? "GET", + endpoint, + requestBody, + responseBody: mockResponse, + wasMocked: true, + durationMs: 0, + error: null, + }); + + return new Response(JSON.stringify(mockResponse), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } - return new Response(JSON.stringify(mockResponse), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - } + // Real fetch, log in background to avoid delaying the response + const start = performance.now(); + try { + const response = await fetch(url, init); + const durationMs = Math.round(performance.now() - start); - // Real fetch, log in background to avoid delaying the response - const start = performance.now(); - try { - const response = await fetch(url, init); - const durationMs = Math.round(performance.now() - start); + // Fire-and-forget: clone + log without blocking the caller + response + .clone() + .json() + .then( + (responseBody) => { + devBridge!.logRequest({ + id, + timestamp, + method: init?.method ?? "GET", + endpoint, + requestBody, + responseBody, + wasMocked: false, + durationMs, + error: response.ok ? null : `HTTP ${response.status}`, + }); + }, + () => { + devBridge!.logRequest({ + id, + timestamp, + method: init?.method ?? "GET", + endpoint, + requestBody, + responseBody: null, + wasMocked: false, + durationMs, + error: response.ok ? null : `HTTP ${response.status}`, + }); + }, + ); - // Fire-and-forget: clone + log without blocking the caller - response.clone().json().then( - (responseBody) => { - devBridge!.logRequest({ - id, - timestamp, - method: init?.method ?? "GET", - endpoint, - requestBody, - responseBody, - wasMocked: false, - durationMs, - error: response.ok ? null : `HTTP ${response.status}`, - }); - }, - () => { - devBridge!.logRequest({ - id, - timestamp, - method: init?.method ?? "GET", - endpoint, - requestBody, - responseBody: null, - wasMocked: false, - durationMs, - error: response.ok ? null : `HTTP ${response.status}`, + return response; + } catch (error) { + devBridge.logRequest({ + id, + timestamp, + method: init?.method ?? "GET", + endpoint, + requestBody, + responseBody: null, + wasMocked: false, + durationMs: Math.round(performance.now() - start), + error: error instanceof Error ? error.message : "Network error", }); - }, - ); - - return response; - } catch (error) { - devBridge.logRequest({ - id, - timestamp, - method: init?.method ?? "GET", - endpoint, - requestBody, - responseBody: null, - wasMocked: false, - durationMs: Math.round(performance.now() - start), - error: error instanceof Error ? error.message : "Network error", - }); - throw error; - } + throw error; + } } diff --git a/src/pudu/sidecar.ts b/src/pudu/sidecar.ts index 1f31cf8..b940a6b 100644 --- a/src/pudu/sidecar.ts +++ b/src/pudu/sidecar.ts @@ -1,10 +1,10 @@ -import { invoke } from '@tauri-apps/api/core'; -import { listen } from '@tauri-apps/api/event'; +import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; let cachedPort: number | null = null; let portPromise: Promise | null = null; -const isTauri = '__TAURI_INTERNALS__' in window; +const isTauri = "__TAURI_INTERNALS__" in window; const SIDECAR_TIMEOUT_MS = 60_000; @@ -17,61 +17,61 @@ const SIDECAR_TIMEOUT_MS = 60_000; * In standalone dev mode (no Tauri), reads from VITE_SIDECAR_PORT env var. */ export function getSidecarPort(): Promise { - if (cachedPort) return Promise.resolve(cachedPort); + if (cachedPort) return Promise.resolve(cachedPort); - if (!isTauri) { - const envPort = import.meta.env.VITE_SIDECAR_PORT; - if (!envPort) { - return Promise.reject(new Error('Running outside Tauri: set VITE_SIDECAR_PORT in .env.local')); + if (!isTauri) { + const envPort = import.meta.env.VITE_SIDECAR_PORT; + if (!envPort) { + return Promise.reject(new Error("Running outside Tauri: set VITE_SIDECAR_PORT in .env.local")); + } + cachedPort = Number(envPort); + return Promise.resolve(cachedPort); } - cachedPort = Number(envPort); - return Promise.resolve(cachedPort); - } - // Deduplicate concurrent callers, one shared promise. - if (!portPromise) { - portPromise = discoverPort(); - } + // Deduplicate concurrent callers, one shared promise. + if (!portPromise) { + portPromise = discoverPort(); + } - return portPromise; + return portPromise; } async function discoverPort(): Promise { - // The event may have already fired before we loaded. Try the command first. - try { - const port = await invoke('get_sidecar_port'); - cachedPort = port; - return port; - } catch { - // Not ready yet fall through to event listener. - } + // The event may have already fired before we loaded. Try the command first. + try { + const port = await invoke("get_sidecar_port"); + cachedPort = port; + return port; + } catch { + // Not ready yet fall through to event listener. + } - return new Promise((resolve, reject) => { - let settled = false; + return new Promise((resolve, reject) => { + let settled = false; - const timeout = setTimeout(() => { - if (!settled) { - settled = true; - reject(new Error('Sidecar did not start in time')); - } - }, SIDECAR_TIMEOUT_MS); + const timeout = setTimeout(() => { + if (!settled) { + settled = true; + reject(new Error("Sidecar did not start in time")); + } + }, SIDECAR_TIMEOUT_MS); - void listen('sidecar-ready', (event) => { - if (settled) return; - settled = true; - clearTimeout(timeout); - cachedPort = event.payload; - resolve(event.payload); + void listen("sidecar-ready", (event) => { + if (settled) return; + settled = true; + clearTimeout(timeout); + cachedPort = event.payload; + resolve(event.payload); + }); }); - }); } export async function getSidecarBaseUrl(): Promise { - const port = await getSidecarPort(); - return `http://localhost:${port}`; + const port = await getSidecarPort(); + return `http://localhost:${port}`; } export async function getSidecarWsUrl(): Promise { - const port = await getSidecarPort(); - return `ws://localhost:${port}`; + const port = await getSidecarPort(); + return `ws://localhost:${port}`; } diff --git a/src/storybook/mockProviders.tsx b/src/storybook/mockProviders.tsx index ab85c04..b80b4de 100644 --- a/src/storybook/mockProviders.tsx +++ b/src/storybook/mockProviders.tsx @@ -1,8 +1,5 @@ import type { PropsWithChildren } from "react"; -import { - FeedbackContext, - type FeedbackContextValue, -} from "../contextProviders/FeedbackContextProvider"; +import { FeedbackContext, type FeedbackContextValue } from "../contextProviders/FeedbackContextProvider"; import { OnboardingContextProvider, type OnboardingApiClient, @@ -15,11 +12,9 @@ interface OnboardingCommandResult { error?: string | null; } -const NOOP = () => { }; +const NOOP = () => {}; -export function createMockFeedbackContextValue( - overrides: Partial = {}, -): FeedbackContextValue { +export function createMockFeedbackContextValue(overrides: Partial = {}): FeedbackContextValue { return { showError: NOOP, showFatal: NOOP, @@ -40,9 +35,7 @@ export function MockFeedbackProvider(props: MockFeedbackProviderProps) { const { children, value } = props; return ( - - {children} - + {children} ); } @@ -96,27 +89,19 @@ interface MockOnboardingProviderProps extends PropsWithChildren { } export function MockOnboardingProvider(props: MockOnboardingProviderProps) { - const { - children, - pendingSteps, - createApi, - apiMockOptions, - feedbackContextValue, - errorReporter, - } = props; + const { children, pendingSteps, createApi, apiMockOptions, feedbackContextValue, errorReporter } = props; - const createApiForProvider = createApi - ?? (() => createOnboardingApiMock({ - pendingSteps, - ...apiMockOptions, - })); + const createApiForProvider = + createApi ?? + (() => + createOnboardingApiMock({ + pendingSteps, + ...apiMockOptions, + })); return ( - + {children} diff --git a/src/themes/australNight.ts b/src/themes/australNight.ts index 762b85a..7816aa2 100644 --- a/src/themes/australNight.ts +++ b/src/themes/australNight.ts @@ -1,5 +1,5 @@ -import {extendTheme} from "@mui/joy/styles"; -import {CSSProperties} from "react"; +import { extendTheme } from "@mui/joy/styles"; +import { CSSProperties } from "react"; const australNight = extendTheme({ colorSchemes: { @@ -88,11 +88,11 @@ const australNight = extendTheme({ }); export const australNightScrollbarStyles: Record = { - "*": {scrollbarWidth: "thin", scrollbarColor: "#1fab78 #0e1b14"}, - "*::-webkit-scrollbar": {width: "12px", height: "12px"}, - "*::-webkit-scrollbar-track": {background: "#0e1b14"}, - "*::-webkit-scrollbar-thumb": {backgroundColor: "#1fab78", border: "2px solid #0e1b14", borderRadius: "8px"}, - "*::-webkit-scrollbar-thumb:hover": {backgroundColor: "#3bc792"}, + "*": { scrollbarWidth: "thin", scrollbarColor: "#1fab78 #0e1b14" }, + "*::-webkit-scrollbar": { width: "12px", height: "12px" }, + "*::-webkit-scrollbar-track": { background: "#0e1b14" }, + "*::-webkit-scrollbar-thumb": { backgroundColor: "#1fab78", border: "2px solid #0e1b14", borderRadius: "8px" }, + "*::-webkit-scrollbar-thumb:hover": { backgroundColor: "#3bc792" }, }; export default australNight; diff --git a/src/themes/doors95.ts b/src/themes/doors95.ts index f587e6b..bbce6d4 100644 --- a/src/themes/doors95.ts +++ b/src/themes/doors95.ts @@ -1,13 +1,13 @@ -import {extendTheme} from "@mui/joy/styles"; -import {CSSProperties} from "react"; +import { extendTheme } from "@mui/joy/styles"; +import { CSSProperties } from "react"; // DOORS 95 PALETTE -const WIN95_TEAL = "#008080"; // Desktop Background -const WIN95_GRAY = "#C0C0C0"; // Surface/Window Color -const WIN95_BLUE = "#000080"; // Title Bar / Primary -const WIN95_WHITE = "#FFFFFF"; // Highlights -const WIN95_BLACK = "#000000"; // Text / Deep Shadows -const WIN95_DKGRAY = "#808080"; // Shadow mid-tone +const WIN95_TEAL = "#008080"; // Desktop Background +const WIN95_GRAY = "#C0C0C0"; // Surface/Window Color +const WIN95_BLUE = "#000080"; // Title Bar / Primary +const WIN95_WHITE = "#FFFFFF"; // Highlights +const WIN95_BLACK = "#000000"; // Text / Deep Shadows +const WIN95_DKGRAY = "#808080"; // Shadow mid-tone // VGA 16-color palette - the only colors Win95 actually had const WIN95_MAROON = "#800000"; @@ -239,7 +239,7 @@ const doors95 = extendTheme({ components: { JoyButton: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ boxShadow: bevelUp, border: "none", padding: "4px 12px", @@ -257,9 +257,10 @@ const doors95 = extendTheme({ color: WIN95_WHITE, }, }), - ...(ownerState.variant === "solid" && ownerState.color === "primary" && { - fontWeight: "bold", - }), + ...(ownerState.variant === "solid" && + ownerState.color === "primary" && { + fontWeight: "bold", + }), ...(ownerState.variant === "soft" && { boxShadow: "none", border: `1px solid ${WIN95_DKGRAY}`, @@ -276,7 +277,7 @@ const doors95 = extendTheme({ }, JoyIconButton: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ boxShadow: ownerState.variant === "solid" ? bevelUp : "none", border: "none", "&:active": { @@ -408,7 +409,7 @@ const doors95 = extendTheme({ }, JoySheet: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ color: WIN95_BLACK, ...(ownerState.variant === "outlined" && { backgroundColor: WIN95_GRAY, @@ -451,7 +452,7 @@ const doors95 = extendTheme({ }, JoyAlert: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ backgroundColor: WIN95_GRAY, color: WIN95_BLACK, boxShadow: bevelUp, @@ -464,7 +465,7 @@ const doors95 = extendTheme({ }, JoyChip: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ ...(ownerState.variant !== "solid" && { color: WIN95_BLACK, }), @@ -473,7 +474,7 @@ const doors95 = extendTheme({ }, JoySwitch: { styleOverrides: { - track: ({ownerState}) => ({ + track: ({ ownerState }) => ({ backgroundColor: WIN95_GRAY, boxShadow: sunkenWell, ...(ownerState.checked && { @@ -541,14 +542,17 @@ const doors95 = extendTheme({ }, JoyLinearProgress: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ backgroundColor: WIN95_GRAY, boxShadow: sunkenWell, "--LinearProgress-progressColor": - ownerState.color === "danger" ? WIN95_MAROON - : ownerState.color === "success" ? WIN95_GREEN - : ownerState.color === "warning" ? WIN95_OLIVE - : WIN95_BLUE, + ownerState.color === "danger" + ? WIN95_MAROON + : ownerState.color === "success" + ? WIN95_GREEN + : ownerState.color === "warning" + ? WIN95_OLIVE + : WIN95_BLUE, }), }, }, @@ -695,7 +699,7 @@ const doors95 = extendTheme({ }, JoyAvatar: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ ...(ownerState.variant !== "solid" && { color: WIN95_BLACK, }), @@ -768,8 +772,8 @@ const doors95 = extendTheme({ }); export const doors95ScrollbarStyles: Record = { - "*": {scrollbarWidth: "thin", scrollbarColor: `${WIN95_GRAY} ${WIN95_TEAL}`}, - "*::-webkit-scrollbar": {width: "16px", height: "16px"}, + "*": { scrollbarWidth: "thin", scrollbarColor: `${WIN95_GRAY} ${WIN95_TEAL}` }, + "*::-webkit-scrollbar": { width: "16px", height: "16px" }, "*::-webkit-scrollbar-track": { background: WIN95_TEAL, boxShadow: sunkenWell, @@ -779,7 +783,7 @@ export const doors95ScrollbarStyles: Record = { boxShadow: bevelUp, border: "none", }, - "*::-webkit-scrollbar-thumb:hover": {backgroundColor: WIN95_BLUE}, + "*::-webkit-scrollbar-thumb:hover": { backgroundColor: WIN95_BLUE }, }; export default doors95; diff --git a/src/themes/hotdogStand.ts b/src/themes/hotdogStand.ts index baeace6..a6acec8 100644 --- a/src/themes/hotdogStand.ts +++ b/src/themes/hotdogStand.ts @@ -1,5 +1,5 @@ -import {extendTheme} from "@mui/joy/styles"; -import {CSSProperties} from "react"; +import { extendTheme } from "@mui/joy/styles"; +import { CSSProperties } from "react"; // THE UNHOLY TRINITY const RETRO_RED = "#CC0000"; @@ -209,7 +209,7 @@ const hotdogStandTheme = extendTheme({ components: { JoyButton: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ border: `2px solid ${RETRO_BLACK}`, boxShadow: `2px 2px 0px 0px ${RETRO_BLACK}`, "&:active": { @@ -254,7 +254,7 @@ const hotdogStandTheme = extendTheme({ }, JoyIconButton: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ ...(ownerState.variant === "solid" && { backgroundColor: RETRO_YELLOW, color: RETRO_BLACK, @@ -276,7 +276,7 @@ const hotdogStandTheme = extendTheme({ backgroundColor: RETRO_YELLOW, color: RETRO_BLACK, border: `2px solid ${RETRO_BLACK}`, - "&::before": {display: "none"}, + "&::before": { display: "none" }, "&.Mui-disabled": { backgroundColor: RETRO_DKRED, color: "#CC9900", @@ -290,7 +290,7 @@ const hotdogStandTheme = extendTheme({ backgroundColor: RETRO_YELLOW, color: RETRO_BLACK, border: `2px solid ${RETRO_BLACK}`, - "&::before": {display: "none"}, + "&::before": { display: "none" }, }, }, }, @@ -300,7 +300,7 @@ const hotdogStandTheme = extendTheme({ backgroundColor: RETRO_YELLOW, color: RETRO_BLACK, border: `2px solid ${RETRO_BLACK}`, - "&::before": {display: "none"}, + "&::before": { display: "none" }, }, indicator: { color: RETRO_BLACK, @@ -395,7 +395,7 @@ const hotdogStandTheme = extendTheme({ }, JoySheet: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ color: RETRO_YELLOW, backgroundColor: RETRO_RED, ...(ownerState.variant === "outlined" && { @@ -412,7 +412,7 @@ const hotdogStandTheme = extendTheme({ }, JoyAlert: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ backgroundColor: RETRO_YELLOW, color: RETRO_BLACK, border: `2px solid ${RETRO_BLACK}`, @@ -431,7 +431,7 @@ const hotdogStandTheme = extendTheme({ }, JoyChip: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ ...(ownerState.variant === "solid" && { backgroundColor: RETRO_YELLOW, color: RETRO_BLACK, @@ -449,7 +449,7 @@ const hotdogStandTheme = extendTheme({ }, JoySwitch: { styleOverrides: { - track: ({ownerState}) => ({ + track: ({ ownerState }) => ({ backgroundColor: RETRO_BLACK, border: `2px solid ${RETRO_YELLOW}`, ...(ownerState.checked && { @@ -724,15 +724,15 @@ const hotdogStandTheme = extendTheme({ }); export const hotdogStandScrollbarStyles: Record = { - "*": {scrollbarWidth: "thin", scrollbarColor: `${RETRO_YELLOW} ${RETRO_RED}`}, - "*::-webkit-scrollbar": {width: "12px", height: "12px"}, - "*::-webkit-scrollbar-track": {background: RETRO_RED}, + "*": { scrollbarWidth: "thin", scrollbarColor: `${RETRO_YELLOW} ${RETRO_RED}` }, + "*::-webkit-scrollbar": { width: "12px", height: "12px" }, + "*::-webkit-scrollbar-track": { background: RETRO_RED }, "*::-webkit-scrollbar-thumb": { backgroundColor: RETRO_YELLOW, border: `2px solid ${RETRO_RED}`, borderRadius: "0px", }, - "*::-webkit-scrollbar-thumb:hover": {backgroundColor: RETRO_WHITE}, + "*::-webkit-scrollbar-thumb:hover": { backgroundColor: RETRO_WHITE }, }; export default hotdogStandTheme; diff --git a/src/themes/index.ts b/src/themes/index.ts index 56c8460..87460cf 100644 --- a/src/themes/index.ts +++ b/src/themes/index.ts @@ -1,10 +1,10 @@ import australNightTheme, { australNightScrollbarStyles } from "./australNight"; import doors95Theme, { doors95ScrollbarStyles } from "./doors95"; -import hotdogStandTheme, { hotdogStandScrollbarStyles } from "./hotdogStand" -import puduTheme, { puduScrollbarStyles } from "./pudu" -import riseAndShineTheme, { riseAndShineScrollbarStyles } from "./riseAndShine" -import vaporTheme, { vaporScrollbarStyles } from "./vapor" -import unitystationClassicTheme, { unitystationClassicScrollbarStyles } from "./unitystationClassic" +import hotdogStandTheme, { hotdogStandScrollbarStyles } from "./hotdogStand"; +import puduTheme, { puduScrollbarStyles } from "./pudu"; +import riseAndShineTheme, { riseAndShineScrollbarStyles } from "./riseAndShine"; +import vaporTheme, { vaporScrollbarStyles } from "./vapor"; +import unitystationClassicTheme, { unitystationClassicScrollbarStyles } from "./unitystationClassic"; import { CSSProperties } from "react"; export const themeRegistry = { diff --git a/src/themes/riseAndShine.ts b/src/themes/riseAndShine.ts index ba8a64f..88c9842 100644 --- a/src/themes/riseAndShine.ts +++ b/src/themes/riseAndShine.ts @@ -6,22 +6,22 @@ import { CSSProperties } from "react"; // White text, yellow accents, olive-green panels, uniform buttons // Olive-green background tones (from the VGUI panel backgrounds) -const VGUI_DARKEST = "#1b1e14"; // Deepest: behind everything -const VGUI_DARKER = "#272b1e"; // Body background -const VGUI_DARK = "#333828"; // Surface / panels -const VGUI_MID = "#3e4430"; // Elevated surfaces, popups, cards -const VGUI_BORDER = "#4e5540"; // Panel borders, dividers -const VGUI_RAISED = "#565e48"; // Button face, raised elements +const VGUI_DARKEST = "#1b1e14"; // Deepest: behind everything +const VGUI_DARKER = "#272b1e"; // Body background +const VGUI_DARK = "#333828"; // Surface / panels +const VGUI_MID = "#3e4430"; // Elevated surfaces, popups, cards +const VGUI_BORDER = "#4e5540"; // Panel borders, dividers +const VGUI_RAISED = "#565e48"; // Button face, raised elements // Text: white primary, with yellow as the accent -const VGUI_WHITE = "#e8e8e0"; // Primary text (warm white) -const VGUI_GRAY = "#b0b0a4"; // Secondary text -const VGUI_DIM = "#787870"; // Tertiary / disabled text +const VGUI_WHITE = "#e8e8e0"; // Primary text (warm white) +const VGUI_GRAY = "#b0b0a4"; // Secondary text +const VGUI_DIM = "#787870"; // Tertiary / disabled text // THE accent: VGUI yellow-gold (achievement titles, progress bars, highlights) -const VGUI_YELLOW = "#d4a838"; // Primary accent -const VGUI_YELLOW_HI = "#e8bc48"; // Hover / bright -const VGUI_YELLOW_LO = "#b08828"; // Pressed / muted +const VGUI_YELLOW = "#d4a838"; // Primary accent +const VGUI_YELLOW_HI = "#e8bc48"; // Hover / bright +const VGUI_YELLOW_LO = "#b08828"; // Pressed / muted const VGUI_YELLOW_SOFT = "rgba(212, 168, 56, 0.12)"; const VGUI_YELLOW_SOFT_HI = "rgba(212, 168, 56, 0.22)"; const VGUI_YELLOW_SOFT_ACT = "rgba(212, 168, 56, 0.32)"; @@ -429,9 +429,9 @@ const riseAndShineTheme = extendTheme({ }, }); -const SCROLL_TRACK = "#6b7058"; // Lighter muted olive (the background gutter) -const SCROLL_THUMB = "#4a5038"; // Darker olive (the draggable part) -const SCROLL_THUMB_HI = "#565c46"; // Thumb on hover +const SCROLL_TRACK = "#6b7058"; // Lighter muted olive (the background gutter) +const SCROLL_THUMB = "#4a5038"; // Darker olive (the draggable part) +const SCROLL_THUMB_HI = "#565c46"; // Thumb on hover export const riseAndShineScrollbarStyles: Record = { "*": { scrollbarWidth: "auto", scrollbarColor: `${SCROLL_THUMB} ${SCROLL_TRACK}` }, diff --git a/src/themes/vapor.ts b/src/themes/vapor.ts index 0a41f5a..7692a5f 100644 --- a/src/themes/vapor.ts +++ b/src/themes/vapor.ts @@ -180,14 +180,15 @@ const vaporTheme = extendTheme({ textTransform: "none" as const, letterSpacing: "0.02em", transition: "all 0.15s ease", - ...(ownerState.variant === "solid" && (ownerState.color === "primary" || ownerState.color === "warning") && { - background: `linear-gradient(to left, ${GP_DUSTY_BLUE} 5%, ${GP_CHALKY_BLUE} 95%)`, - color: "#ffffff", - "&:hover": { - background: `linear-gradient(to left, #4b9ac4 5%, #7ecef7 95%)`, + ...(ownerState.variant === "solid" && + (ownerState.color === "primary" || ownerState.color === "warning") && { + background: `linear-gradient(to left, ${GP_DUSTY_BLUE} 5%, ${GP_CHALKY_BLUE} 95%)`, color: "#ffffff", - }, - }), + "&:hover": { + background: `linear-gradient(to left, #4b9ac4 5%, #7ecef7 95%)`, + color: "#ffffff", + }, + }), }), }, },
{JSON.stringify(entry.requestBody, null, 2)} @@ -100,7 +98,9 @@ function RequestLogRow({ entry }: { entry: RequestLogEntry }) { {entry.responseBody !== null && ( - Response Body + + Response Body + & Pick) { +function LogTab({ + requestLog, + clearLog, +}: Pick & Pick) { return ( @@ -182,7 +185,9 @@ function MocksTab({ mocks, addMock, removeMock, clearAllMocks }: MocksTabProps) return ( - Add Mock + + Add Mock + Endpoint @@ -207,7 +212,9 @@ function MocksTab({ mocks, addMock, removeMock, clearAllMocks }: MocksTabProps) sx={{ fontFamily: "monospace", fontSize: 12 }} /> {jsonError && ( - {jsonError} + + {jsonError} + )} @@ -243,7 +250,10 @@ function MocksTab({ mocks, addMock, removeMock, clearAllMocks }: MocksTabProps) Remove - + {JSON.stringify(mock.response, null, 2)} @@ -271,14 +281,39 @@ const EVENT_TYPES: string[] = [ ]; const EVENT_TEMPLATES: Record = { - "discord:join-request": { eventType: "discord:join-request", timestamp: "", serverIp: "123.45.67.89", serverPort: 7777, serverName: "Test Server", forkName: "Unitystation", buildVersion: 1234, gameMode: "Secret", currentMap: "BoxStation", playerCount: 12, playerCountMax: 40, status: 0 }, + "discord:join-request": { + eventType: "discord:join-request", + timestamp: "", + serverIp: "123.45.67.89", + serverPort: 7777, + serverName: "Test Server", + forkName: "Unitystation", + buildVersion: 1234, + gameMode: "Secret", + currentMap: "BoxStation", + playerCount: 12, + playerCountMax: 40, + status: 0, + }, "tts:status-changed": { eventType: "tts:status-changed", timestamp: "", status: 0, message: "" }, - "tts:update-available": { eventType: "tts:update-available", timestamp: "", installedVersion: "1.0.0", latestVersion: "1.1.0" }, + "tts:update-available": { + eventType: "tts:update-available", + timestamp: "", + installedVersion: "1.0.0", + latestVersion: "1.1.0", + }, "tts:install-output": { eventType: "tts:install-output", timestamp: "", line: "" }, "servers:updated": { eventType: "servers:updated", timestamp: "" }, "installations:changed": { eventType: "installations:changed", timestamp: "" }, "game:state-changed": { eventType: "game:state-changed", timestamp: "" }, - "download:progress": { eventType: "download:progress", timestamp: "", downloadId: "", bytesDownloaded: 0, totalBytes: 0, speedBps: 0 }, + "download:progress": { + eventType: "download:progress", + timestamp: "", + downloadId: "", + bytesDownloaded: 0, + totalBytes: 0, + speedBps: 0, + }, "download:state-changed": { eventType: "download:state-changed", timestamp: "", downloadId: "", state: 0 }, }; @@ -295,7 +330,13 @@ interface EventsTabProps { addMock: DevToolsChannelActions["addMock"]; } -function PresetRunner({ injectEvent, addMock }: { injectEvent: DevToolsChannelActions["injectEvent"]; addMock: DevToolsChannelActions["addMock"] }) { +function PresetRunner({ + injectEvent, + addMock, +}: { + injectEvent: DevToolsChannelActions["injectEvent"]; + addMock: DevToolsChannelActions["addMock"]; +}) { const [runningLabel, setRunningLabel] = useState(null); const cancelRef = useRef(false); @@ -338,7 +379,9 @@ function PresetRunner({ injectEvent, addMock }: { injectEvent: DevToolsChannelAc return ( - TTS Presets + + TTS Presets + {ttsPresets.map((preset) => ( (EVENT_TYPES[0]); - const [payload, setPayload] = useState(JSON.stringify(EVENT_TEMPLATES[EVENT_TYPES[0]] ?? { eventType: EVENT_TYPES[0], timestamp: "" }, null, 2)); + const [payload, setPayload] = useState( + JSON.stringify(EVENT_TEMPLATES[EVENT_TYPES[0]] ?? { eventType: EVENT_TYPES[0], timestamp: "" }, null, 2), + ); const [jsonError, setJsonError] = useState(null); const [firedEvents, setFiredEvents] = useState([]); @@ -404,17 +449,23 @@ function EventsTab({ injectEvent, addMock }: EventsTabProps) { - Inject Event + + Inject Event + Event Type { if (v) handleTypeChange(v); }} + onChange={(_e, v) => { + if (v) handleTypeChange(v); + }} sx={{ fontFamily: "monospace", fontSize: 13 }} > {EVENT_TYPES.map((t) => ( - {t} + + {t} + ))} @@ -431,7 +482,9 @@ function EventsTab({ injectEvent, addMock }: EventsTabProps) { sx={{ fontFamily: "monospace", fontSize: 12 }} /> {jsonError && ( - {jsonError} + + {jsonError} + )} @@ -499,8 +552,15 @@ function StateTab({ stateSnapshot, requestStateSnapshot, isActive }: StateTabPro {Object.entries(stateSnapshot).map(([name, data]) => ( - - {name} + + + {name} + - setActiveTab(v as string)} sx={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}> + setActiveTab(v as string)} + sx={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }} + > Log Mocks diff --git a/src/devtools/bridge.ts b/src/devtools/bridge.ts index 3a0ade2..cef8ca4 100644 --- a/src/devtools/bridge.ts +++ b/src/devtools/bridge.ts @@ -4,90 +4,89 @@ import { DEVTOOLS_CHANNEL } from "./protocol"; type StateSource = () => unknown; class DevToolsBridgeImpl { - private channel: BroadcastChannel; - private mocks = new Map(); - private stateSources = new Map(); - private eventInjectors = new Set<(eventType: string, data: unknown) => void>(); + private channel: BroadcastChannel; + private mocks = new Map(); + private stateSources = new Map(); + private eventInjectors = new Set<(eventType: string, data: unknown) => void>(); - constructor() { - this.channel = new BroadcastChannel(DEVTOOLS_CHANNEL); - this.channel.onmessage = (event: MessageEvent) => { - this.handleCommand(event.data); - }; - } + constructor() { + this.channel = new BroadcastChannel(DEVTOOLS_CHANNEL); + this.channel.onmessage = (event: MessageEvent) => { + this.handleCommand(event.data); + }; + } - private handleCommand(command: DevToolsCommand): void { - switch (command.type) { - case "mock-endpoint": - this.mocks.set(command.endpoint, command.response); - break; - case "clear-mock": - this.mocks.delete(command.endpoint); - break; - case "clear-all-mocks": - this.mocks.clear(); - break; - case "inject-event": - for (const injector of this.eventInjectors) { - injector(command.eventType, command.data); - } - this.report({ - type: "event-injected", - eventType: command.eventType, - data: command.data, - timestamp: Date.now(), - }); - break; - case "request-state-snapshot": { - const sources: Record = {}; - for (const [name, fn] of this.stateSources) { - try { - sources[name] = fn(); - } catch { - sources[name] = { error: "Failed to collect state" }; - } + private handleCommand(command: DevToolsCommand): void { + switch (command.type) { + case "mock-endpoint": + this.mocks.set(command.endpoint, command.response); + break; + case "clear-mock": + this.mocks.delete(command.endpoint); + break; + case "clear-all-mocks": + this.mocks.clear(); + break; + case "inject-event": + for (const injector of this.eventInjectors) { + injector(command.eventType, command.data); + } + this.report({ + type: "event-injected", + eventType: command.eventType, + data: command.data, + timestamp: Date.now(), + }); + break; + case "request-state-snapshot": { + const sources: Record = {}; + for (const [name, fn] of this.stateSources) { + try { + sources[name] = fn(); + } catch { + sources[name] = { error: "Failed to collect state" }; + } + } + this.report({ type: "state-snapshot", sources }); + break; + } } - this.report({ type: "state-snapshot", sources }); - break; - } } - } - report(message: DevToolsReport): void { - this.channel.postMessage(message); - } + report(message: DevToolsReport): void { + this.channel.postMessage(message); + } - getMock(endpoint: string): unknown | undefined { - return this.mocks.get(endpoint); - } + getMock(endpoint: string): unknown | undefined { + return this.mocks.get(endpoint); + } - hasMock(endpoint: string): boolean { - return this.mocks.has(endpoint); - } + hasMock(endpoint: string): boolean { + return this.mocks.has(endpoint); + } - registerStateSource(name: string, fn: StateSource): () => void { - this.stateSources.set(name, fn); - return () => { - this.stateSources.delete(name); - }; - } + registerStateSource(name: string, fn: StateSource): () => void { + this.stateSources.set(name, fn); + return () => { + this.stateSources.delete(name); + }; + } - registerEventInjector(fn: (eventType: string, data: unknown) => void): () => void { - this.eventInjectors.add(fn); - return () => { - this.eventInjectors.delete(fn); - }; - } + registerEventInjector(fn: (eventType: string, data: unknown) => void): () => void { + this.eventInjectors.add(fn); + return () => { + this.eventInjectors.delete(fn); + }; + } - logRequest(entry: RequestLogEntry): void { - this.report({ type: "request-logged", entry }); - } + logRequest(entry: RequestLogEntry): void { + this.report({ type: "request-logged", entry }); + } - dispose(): void { - this.channel.close(); - } + dispose(): void { + this.channel.close(); + } } // Singleton, only created in dev -export const devBridge: DevToolsBridgeImpl | null = - import.meta.env.DEV ? new DevToolsBridgeImpl() : null; +export const devBridge: DevToolsBridgeImpl | null = import.meta.env.DEV ? new DevToolsBridgeImpl() : null; diff --git a/src/devtools/open-devtools-window.ts b/src/devtools/open-devtools-window.ts index 9a38827..be181eb 100644 --- a/src/devtools/open-devtools-window.ts +++ b/src/devtools/open-devtools-window.ts @@ -3,24 +3,24 @@ import { WebviewWindow } from "@tauri-apps/api/webviewWindow"; let devtoolsWindow: WebviewWindow | null = null; export async function openDevToolsWindow(): Promise { - // Check if already open - if (devtoolsWindow) { - try { - await devtoolsWindow.setFocus(); - return; - } catch { - devtoolsWindow = null; + // Check if already open + if (devtoolsWindow) { + try { + await devtoolsWindow.setFocus(); + return; + } catch { + devtoolsWindow = null; + } } - } - devtoolsWindow = new WebviewWindow("pudu-devtools", { - url: "/devtools", - title: "PuduLauncher DevTools", - width: 900, - height: 600, - }); + devtoolsWindow = new WebviewWindow("pudu-devtools", { + url: "/devtools", + title: "PuduLauncher DevTools", + width: 900, + height: 600, + }); - devtoolsWindow.once("tauri://destroyed", () => { - devtoolsWindow = null; - }); + devtoolsWindow.once("tauri://destroyed", () => { + devtoolsWindow = null; + }); } diff --git a/src/devtools/presets/tts-presets.ts b/src/devtools/presets/tts-presets.ts index 6a51184..2ee10bc 100644 --- a/src/devtools/presets/tts-presets.ts +++ b/src/devtools/presets/tts-presets.ts @@ -21,55 +21,151 @@ export const ttsPresets: EventPreset[] = [ { label: "Status \u2192 NotInstalled", description: "Set TTS status to NotInstalled", - mocks: [{ endpoint: "/api/tts/get-status", response: { success: true, data: { status: 0, updateAvailable: false } } }], + mocks: [ + { + endpoint: "/api/tts/get-status", + response: { success: true, data: { status: 0, updateAvailable: false } }, + }, + ], events: [ - { eventType: "tts:status-changed", data: { eventType: "tts:status-changed", timestamp: now(), status: 0, message: null } }, + { + eventType: "tts:status-changed", + data: { eventType: "tts:status-changed", timestamp: now(), status: 0, message: null }, + }, ], }, { label: "Status \u2192 Installed", description: "Set TTS status to Installed", - mocks: [{ endpoint: "/api/tts/get-status", response: { success: true, data: { status: 4, updateAvailable: false, installedVersion: "1.0.0" } } }], + mocks: [ + { + endpoint: "/api/tts/get-status", + response: { success: true, data: { status: 4, updateAvailable: false, installedVersion: "1.0.0" } }, + }, + ], events: [ - { eventType: "tts:status-changed", data: { eventType: "tts:status-changed", timestamp: now(), status: 4, message: "Installation complete" } }, + { + eventType: "tts:status-changed", + data: { + eventType: "tts:status-changed", + timestamp: now(), + status: 4, + message: "Installation complete", + }, + }, ], }, { label: "Status \u2192 ServerRunning", description: "Set TTS status to ServerRunning", events: [ - { eventType: "tts:status-changed", data: { eventType: "tts:status-changed", timestamp: now(), status: 6, message: "Server started" } }, + { + eventType: "tts:status-changed", + data: { eventType: "tts:status-changed", timestamp: now(), status: 6, message: "Server started" }, + }, ], }, { label: "Status \u2192 Error", description: "Set TTS status to Error with a message", - mocks: [{ endpoint: "/api/tts/get-status", response: { success: true, data: { status: 8, updateAvailable: false, errorMessage: "Something went wrong" } } }], + mocks: [ + { + endpoint: "/api/tts/get-status", + response: { + success: true, + data: { status: 8, updateAvailable: false, errorMessage: "Something went wrong" }, + }, + }, + ], events: [ - { eventType: "tts:status-changed", data: { eventType: "tts:status-changed", timestamp: now(), status: 8, message: "Something went wrong" } }, + { + eventType: "tts:status-changed", + data: { eventType: "tts:status-changed", timestamp: now(), status: 8, message: "Something went wrong" }, + }, ], }, { label: "Update Available", description: "Trigger update available notification", events: [ - { eventType: "tts:update-available", data: { eventType: "tts:update-available", timestamp: now(), installedVersion: "1.0.0", latestVersion: "1.0.2" } }, + { + eventType: "tts:update-available", + data: { + eventType: "tts:update-available", + timestamp: now(), + installedVersion: "1.0.0", + latestVersion: "1.0.2", + }, + }, ], }, { label: "Simulate Install Flow", description: "Full install sequence: Downloading \u2192 Installing \u2192 output lines \u2192 Installed", - mocks: [{ endpoint: "/api/tts/get-status", response: { success: true, data: { status: 4, updateAvailable: false, installedVersion: "1.0.0" } } }], + mocks: [ + { + endpoint: "/api/tts/get-status", + response: { success: true, data: { status: 4, updateAvailable: false, installedVersion: "1.0.0" } }, + }, + ], events: [ - { eventType: "tts:status-changed", data: { eventType: "tts:status-changed", timestamp: now(), status: 2, message: "Downloading..." } }, - { eventType: "tts:status-changed", data: { eventType: "tts:status-changed", timestamp: now(), status: 3, message: "Installing..." }, delayMs: 1500 }, - { eventType: "tts:install-output", data: { eventType: "tts:install-output", timestamp: now(), line: "[1 / 6] Checking Python installation..." }, delayMs: 500 }, - { eventType: "tts:install-output", data: { eventType: "tts:install-output", timestamp: now(), line: "[2 / 6] Creating virtual environment..." }, delayMs: 1000 }, - { eventType: "tts:install-output", data: { eventType: "tts:install-output", timestamp: now(), line: "[3 / 6] Installing requirements..." }, delayMs: 1000 }, - { eventType: "tts:install-output", data: { eventType: "tts:install-output", timestamp: now(), line: "[4 / 6] Installing eSpeak..." }, delayMs: 2000 }, - { eventType: "tts:install-output", data: { eventType: "tts:install-output", timestamp: now(), line: "[5 / 6] Downloading TTS model..." }, delayMs: 1500 }, - { eventType: "tts:install-output", data: { eventType: "tts:install-output", timestamp: now(), line: "[6 / 6] Writing manifest..." }, delayMs: 1000 }, - { eventType: "tts:status-changed", data: { eventType: "tts:status-changed", timestamp: now(), status: 4, message: "Installation complete" }, delayMs: 500 }, + { + eventType: "tts:status-changed", + data: { eventType: "tts:status-changed", timestamp: now(), status: 2, message: "Downloading..." }, + }, + { + eventType: "tts:status-changed", + data: { eventType: "tts:status-changed", timestamp: now(), status: 3, message: "Installing..." }, + delayMs: 1500, + }, + { + eventType: "tts:install-output", + data: { + eventType: "tts:install-output", + timestamp: now(), + line: "[1 / 6] Checking Python installation...", + }, + delayMs: 500, + }, + { + eventType: "tts:install-output", + data: { + eventType: "tts:install-output", + timestamp: now(), + line: "[2 / 6] Creating virtual environment...", + }, + delayMs: 1000, + }, + { + eventType: "tts:install-output", + data: { eventType: "tts:install-output", timestamp: now(), line: "[3 / 6] Installing requirements..." }, + delayMs: 1000, + }, + { + eventType: "tts:install-output", + data: { eventType: "tts:install-output", timestamp: now(), line: "[4 / 6] Installing eSpeak..." }, + delayMs: 2000, + }, + { + eventType: "tts:install-output", + data: { eventType: "tts:install-output", timestamp: now(), line: "[5 / 6] Downloading TTS model..." }, + delayMs: 1500, + }, + { + eventType: "tts:install-output", + data: { eventType: "tts:install-output", timestamp: now(), line: "[6 / 6] Writing manifest..." }, + delayMs: 1000, + }, + { + eventType: "tts:status-changed", + data: { + eventType: "tts:status-changed", + timestamp: now(), + status: 4, + message: "Installation complete", + }, + delayMs: 500, + }, ], }, ]; diff --git a/src/devtools/protocol.ts b/src/devtools/protocol.ts index e39c460..9bb298e 100644 --- a/src/devtools/protocol.ts +++ b/src/devtools/protocol.ts @@ -1,33 +1,33 @@ export interface RequestLogEntry { - id: string; - timestamp: number; - method: string; - endpoint: string; - requestBody: unknown | null; - responseBody: unknown | null; - wasMocked: boolean; - durationMs: number | null; - error: string | null; + id: string; + timestamp: number; + method: string; + endpoint: string; + requestBody: unknown | null; + responseBody: unknown | null; + wasMocked: boolean; + durationMs: number | null; + error: string | null; } export interface MockConfig { - endpoint: string; - response: unknown; + endpoint: string; + response: unknown; } // Commands: dev panel → main window export type DevToolsCommand = - | { type: "mock-endpoint"; endpoint: string; response: unknown } - | { type: "clear-mock"; endpoint: string } - | { type: "clear-all-mocks" } - | { type: "inject-event"; eventType: string; data: unknown } - | { type: "request-state-snapshot" }; + | { type: "mock-endpoint"; endpoint: string; response: unknown } + | { type: "clear-mock"; endpoint: string } + | { type: "clear-all-mocks" } + | { type: "inject-event"; eventType: string; data: unknown } + | { type: "request-state-snapshot" }; // Reports: main window → dev panel export type DevToolsReport = - | { type: "request-logged"; entry: RequestLogEntry } - | { type: "event-injected"; eventType: string; data: unknown; timestamp: number } - | { type: "state-snapshot"; sources: Record }; + | { type: "request-logged"; entry: RequestLogEntry } + | { type: "event-injected"; eventType: string; data: unknown; timestamp: number } + | { type: "state-snapshot"; sources: Record }; export type DevToolsMessage = DevToolsCommand | DevToolsReport; diff --git a/src/hooks/useInstallationState.ts b/src/hooks/useInstallationState.ts index f74325d..79ff729 100644 --- a/src/hooks/useInstallationState.ts +++ b/src/hooks/useInstallationState.ts @@ -51,9 +51,7 @@ export function useInstallationState() { }, []); const deleteInstallation = async (id: string) => { - setInstallations((prev) => - prev ? prev.filter((i) => i.id !== id) : prev, - ); + setInstallations((prev) => (prev ? prev.filter((i) => i.id !== id) : prev)); const api = new InstallationsApi(); try { diff --git a/src/hooks/usePaginatedCollection.ts b/src/hooks/usePaginatedCollection.ts index 3209d80..f39a4db 100644 --- a/src/hooks/usePaginatedCollection.ts +++ b/src/hooks/usePaginatedCollection.ts @@ -19,7 +19,7 @@ interface UsePaginatedCollectionResult { export function usePaginatedCollection( items: T[], pageSize: number, - options?: UsePaginatedCollectionOptions + options?: UsePaginatedCollectionOptions, ): UsePaginatedCollectionResult { const normalizedPageSize = Math.max(1, Math.floor(pageSize)); const totalItems = items.length; diff --git a/src/main.tsx b/src/main.tsx index 08ba942..0a6d03e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,33 +6,33 @@ const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement) // Dev tools window gets its own minimal render tree, no ThemeProvider, no App. // This avoids touching App.tsx which would break React Compiler's JSX memoization. if (import.meta.env.DEV && window.location.pathname === "/devtools") { - import("./devtools/DevToolsPage").then(({ default: DevToolsPage }) => { - root.render( - - - , - ); - }); + import("./devtools/DevToolsPage").then(({ default: DevToolsPage }) => { + root.render( + + + , + ); + }); } else { - // Normal app, static imports keep App.tsx identical to its original form - const { ThemeProvider } = await import("./contextProviders/ThemeProvider"); - const { default: App } = await import("./App"); + // Normal app, static imports keep App.tsx identical to its original form + const { ThemeProvider } = await import("./contextProviders/ThemeProvider"); + const { default: App } = await import("./App"); - root.render( - - - - - , - ); + root.render( + + + + + , + ); - // Ctrl+Shift+D opens devtools window (dev only, outside React tree) - if (import.meta.env.DEV) { - window.addEventListener("keydown", (e) => { - if (e.ctrlKey && e.shiftKey && e.key === "D") { - e.preventDefault(); - void import("./devtools/open-devtools-window").then((m) => m.openDevToolsWindow()); - } - }); - } + // Ctrl+Shift+D opens devtools window (dev only, outside React tree) + if (import.meta.env.DEV) { + window.addEventListener("keydown", (e) => { + if (e.ctrlKey && e.shiftKey && e.key === "D") { + e.preventDefault(); + void import("./devtools/open-devtools-window").then((m) => m.openDevToolsWindow()); + } + }); + } } diff --git a/src/pudu/events/event-listener.ts b/src/pudu/events/event-listener.ts index 9b66b37..e04fb20 100644 --- a/src/pudu/events/event-listener.ts +++ b/src/pudu/events/event-listener.ts @@ -1,5 +1,5 @@ -import type { EventBase, PuduEvent, PuduEventMap, PuduEventType } from '../generated/types'; -import { getSidecarWsUrl } from '../sidecar'; +import type { EventBase, PuduEvent, PuduEventMap, PuduEventType } from "../generated/types"; +import { getSidecarWsUrl } from "../sidecar"; /** * WebSocket-based event listener for receiving real-time events from the sidecar. @@ -7,193 +7,190 @@ import { getSidecarWsUrl } from '../sidecar'; type EventHandler = (event: PuduEvent) => void; export class EventListener { - private ws: WebSocket | null = null; - private handlers = new Map(); - private reconnectInterval = 3000; - private reconnectTimer: number | null = null; - private shouldReconnect = true; - private connectionSession = 0; - private connectPromise: Promise | null = null; - - async connect(): Promise { - this.shouldReconnect = true; - - if (this.ws?.readyState === WebSocket.OPEN || this.ws?.readyState === WebSocket.CONNECTING) { - return; - } - - if (this.connectPromise) { - return this.connectPromise; - } - - this.connectPromise = this.doConnect(); - try { - await this.connectPromise; - } finally { - this.connectPromise = null; - } - } - - private async doConnect(): Promise { - const session = this.connectionSession; - - try { - const wsUrl = await getSidecarWsUrl(); - if (!this.shouldReconnect || session !== this.connectionSession) { - return; - } - - if (this.ws?.readyState === WebSocket.OPEN || this.ws?.readyState === WebSocket.CONNECTING) { - return; - } - - const ws = new WebSocket(`${wsUrl}/events`); - this.ws = ws; + private ws: WebSocket | null = null; + private handlers = new Map(); + private reconnectInterval = 3000; + private reconnectTimer: number | null = null; + private shouldReconnect = true; + private connectionSession = 0; + private connectPromise: Promise | null = null; + + async connect(): Promise { + this.shouldReconnect = true; + + if (this.ws?.readyState === WebSocket.OPEN || this.ws?.readyState === WebSocket.CONNECTING) { + return; + } - ws.onopen = () => { - if (!this.shouldReconnect || session !== this.connectionSession) { - ws.close(); - return; + if (this.connectPromise) { + return this.connectPromise; } - console.log('Connected to event stream'); - if (this.reconnectTimer) { - clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; + this.connectPromise = this.doConnect(); + try { + await this.connectPromise; + } finally { + this.connectPromise = null; } - }; + } + + private async doConnect(): Promise { + const session = this.connectionSession; - ws.onmessage = (msg) => { try { - const event = JSON.parse(msg.data) as EventBase; - const eventType = event.eventType as PuduEventType; - const handlers = this.handlers.get(eventType); - - if (handlers) { - handlers.forEach((handler) => handler(event as PuduEvent)); - } else { - console.log('Unhandled event type:', event.eventType); - } + const wsUrl = await getSidecarWsUrl(); + if (!this.shouldReconnect || session !== this.connectionSession) { + return; + } + + if (this.ws?.readyState === WebSocket.OPEN || this.ws?.readyState === WebSocket.CONNECTING) { + return; + } + + const ws = new WebSocket(`${wsUrl}/events`); + this.ws = ws; + + ws.onopen = () => { + if (!this.shouldReconnect || session !== this.connectionSession) { + ws.close(); + return; + } + + console.log("Connected to event stream"); + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + }; + + ws.onmessage = (msg) => { + try { + const event = JSON.parse(msg.data) as EventBase; + const eventType = event.eventType as PuduEventType; + const handlers = this.handlers.get(eventType); + + if (handlers) { + handlers.forEach((handler) => handler(event as PuduEvent)); + } else { + console.log("Unhandled event type:", event.eventType); + } + } catch (error) { + console.error("Error parsing event:", error); + } + }; + + ws.onerror = (error) => { + console.error("WebSocket error:", error); + }; + + ws.onclose = () => { + if (this.ws === ws) { + this.ws = null; + } + + console.log("Disconnected from event stream"); + if (this.shouldReconnect && session === this.connectionSession) { + this.scheduleReconnect(); + } + }; } catch (error) { - console.error('Error parsing event:', error); + console.error("Failed to connect to event stream:", error); + if (this.shouldReconnect && session === this.connectionSession) { + this.scheduleReconnect(); + } } - }; + } - ws.onerror = (error) => { - console.error('WebSocket error:', error); - }; + on( + eventType: TEventType, + handler: (event: PuduEventMap[TEventType]) => void, + ): void { + if (!this.handlers.has(eventType)) { + this.handlers.set(eventType, []); + } - ws.onclose = () => { - if (this.ws === ws) { - this.ws = null; + this.handlers.get(eventType)!.push(handler as EventHandler); + void this.connect(); + } + + off( + eventType: TEventType, + handler?: (event: PuduEventMap[TEventType]) => void, + ): void { + if (!handler) { + this.handlers.delete(eventType); + if (!this.hasHandlers()) { + this.disconnect(); + } + return; } - console.log('Disconnected from event stream'); - if (this.shouldReconnect && session === this.connectionSession) { - this.scheduleReconnect(); + const handlers = this.handlers.get(eventType); + if (handlers) { + const index = handlers.indexOf(handler as EventHandler); + if (index !== -1) { + handlers.splice(index, 1); + if (handlers.length === 0) { + this.handlers.delete(eventType); + } + } } - }; - } catch (error) { - console.error('Failed to connect to event stream:', error); - if (this.shouldReconnect && session === this.connectionSession) { - this.scheduleReconnect(); - } - } - } - - on( - eventType: TEventType, - handler: (event: PuduEventMap[TEventType]) => void - ): void { - if (!this.handlers.has(eventType)) { - this.handlers.set(eventType, []); - } - this.handlers.get(eventType)!.push(handler as EventHandler); - void this.connect(); - } - - off( - eventType: TEventType, - handler?: (event: PuduEventMap[TEventType]) => void - ): void { - if (!handler) { - this.handlers.delete(eventType); - if (!this.hasHandlers()) { - this.disconnect(); - } - return; + if (!this.hasHandlers()) { + this.disconnect(); + } } - const handlers = this.handlers.get(eventType); - if (handlers) { - const index = handlers.indexOf(handler as EventHandler); - if (index !== -1) { - handlers.splice(index, 1); - if (handlers.length === 0) { - this.handlers.delete(eventType); + injectEvent(eventType: TEventType, data: PuduEventMap[TEventType]): void { + const handlers = this.handlers.get(eventType); + if (handlers) { + handlers.forEach((handler) => handler(data as PuduEvent)); } - } } - if (!this.hasHandlers()) { - this.disconnect(); - } - } - - injectEvent( - eventType: TEventType, - data: PuduEventMap[TEventType] - ): void { - const handlers = this.handlers.get(eventType); - if (handlers) { - handlers.forEach((handler) => handler(data as PuduEvent)); - } - } + disconnect(): void { + this.shouldReconnect = false; + this.connectionSession += 1; + this.connectPromise = null; - disconnect(): void { - this.shouldReconnect = false; - this.connectionSession += 1; - this.connectPromise = null; + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } - if (this.reconnectTimer) { - clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; + if (this.ws) { + this.ws.onclose = null; + this.ws.close(); + this.ws = null; + } } - if (this.ws) { - this.ws.onclose = null; - this.ws.close(); - this.ws = null; - } - } + private scheduleReconnect(): void { + if (this.reconnectTimer) { + return; + } - private scheduleReconnect(): void { - if (this.reconnectTimer) { - return; - } + const session = this.connectionSession; - const session = this.connectionSession; + this.reconnectTimer = window.setTimeout(() => { + this.reconnectTimer = null; - this.reconnectTimer = window.setTimeout(() => { - this.reconnectTimer = null; + if (!this.shouldReconnect || session !== this.connectionSession) { + return; + } - if (!this.shouldReconnect || session !== this.connectionSession) { - return; - } + console.log("Attempting to reconnect..."); + this.connect(); + }, this.reconnectInterval); + } - console.log('Attempting to reconnect...'); - this.connect(); - }, this.reconnectInterval); - } + private hasHandlers(): boolean { + for (const handlers of this.handlers.values()) { + if (handlers.length > 0) { + return true; + } + } - private hasHandlers(): boolean { - for (const handlers of this.handlers.values()) { - if (handlers.length > 0) { - return true; - } + return false; } - - return false; - } } diff --git a/src/pudu/log.ts b/src/pudu/log.ts index 4ced1ee..db381d0 100644 --- a/src/pudu/log.ts +++ b/src/pudu/log.ts @@ -1,24 +1,21 @@ -import { info, warn, error, debug, trace } from '@tauri-apps/plugin-log'; +import { info, warn, error, debug, trace } from "@tauri-apps/plugin-log"; -const isTauri = '__TAURI_INTERNALS__' in window; +const isTauri = "__TAURI_INTERNALS__" in window; -function wrap( - tauriFn: (msg: string) => Promise, - consoleFn: (...args: unknown[]) => void, -): (msg: string) => void { - return (msg: string) => { - if (isTauri) { - tauriFn(`[PuduFrontend] ${msg}`); - } else { - consoleFn(`[PuduFrontend] ${msg}`); - } - }; +function wrap(tauriFn: (msg: string) => Promise, consoleFn: (...args: unknown[]) => void): (msg: string) => void { + return (msg: string) => { + if (isTauri) { + tauriFn(`[PuduFrontend] ${msg}`); + } else { + consoleFn(`[PuduFrontend] ${msg}`); + } + }; } export const log = { - info: wrap(info, console.log), - warn: wrap(warn, console.warn), - error: wrap(error, console.error), - debug: wrap(debug, console.debug), - trace: wrap(trace, console.trace), + info: wrap(info, console.log), + warn: wrap(warn, console.warn), + error: wrap(error, console.error), + debug: wrap(debug, console.debug), + trace: wrap(trace, console.trace), }; diff --git a/src/pudu/pudu-fetch.ts b/src/pudu/pudu-fetch.ts index 1bdaf4d..50178ca 100644 --- a/src/pudu/pudu-fetch.ts +++ b/src/pudu/pudu-fetch.ts @@ -3,99 +3,99 @@ import { getSidecarBaseUrl } from "./sidecar"; let idCounter = 0; -export async function puduFetch( - endpoint: string, - init?: RequestInit, -): Promise { - const baseUrl = await getSidecarBaseUrl(); - const url = `${baseUrl}${endpoint}`; +export async function puduFetch(endpoint: string, init?: RequestInit): Promise { + const baseUrl = await getSidecarBaseUrl(); + const url = `${baseUrl}${endpoint}`; - if (!import.meta.env.DEV || !devBridge) { - return fetch(url, init); - } + if (!import.meta.env.DEV || !devBridge) { + return fetch(url, init); + } - const id = `req-${++idCounter}`; - const timestamp = Date.now(); - let requestBody: unknown = null; - if (init?.body && typeof init.body === "string") { - try { - requestBody = JSON.parse(init.body); - } catch { - requestBody = init.body; + const id = `req-${++idCounter}`; + const timestamp = Date.now(); + let requestBody: unknown = null; + if (init?.body && typeof init.body === "string") { + try { + requestBody = JSON.parse(init.body); + } catch { + requestBody = init.body; + } } - } - // Check for mock - if (devBridge.hasMock(endpoint)) { - const mockResponse = devBridge.getMock(endpoint); + // Check for mock + if (devBridge.hasMock(endpoint)) { + const mockResponse = devBridge.getMock(endpoint); - devBridge.logRequest({ - id, - timestamp, - method: init?.method ?? "GET", - endpoint, - requestBody, - responseBody: mockResponse, - wasMocked: true, - durationMs: 0, - error: null, - }); + devBridge.logRequest({ + id, + timestamp, + method: init?.method ?? "GET", + endpoint, + requestBody, + responseBody: mockResponse, + wasMocked: true, + durationMs: 0, + error: null, + }); + + return new Response(JSON.stringify(mockResponse), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } - return new Response(JSON.stringify(mockResponse), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - } + // Real fetch, log in background to avoid delaying the response + const start = performance.now(); + try { + const response = await fetch(url, init); + const durationMs = Math.round(performance.now() - start); - // Real fetch, log in background to avoid delaying the response - const start = performance.now(); - try { - const response = await fetch(url, init); - const durationMs = Math.round(performance.now() - start); + // Fire-and-forget: clone + log without blocking the caller + response + .clone() + .json() + .then( + (responseBody) => { + devBridge!.logRequest({ + id, + timestamp, + method: init?.method ?? "GET", + endpoint, + requestBody, + responseBody, + wasMocked: false, + durationMs, + error: response.ok ? null : `HTTP ${response.status}`, + }); + }, + () => { + devBridge!.logRequest({ + id, + timestamp, + method: init?.method ?? "GET", + endpoint, + requestBody, + responseBody: null, + wasMocked: false, + durationMs, + error: response.ok ? null : `HTTP ${response.status}`, + }); + }, + ); - // Fire-and-forget: clone + log without blocking the caller - response.clone().json().then( - (responseBody) => { - devBridge!.logRequest({ - id, - timestamp, - method: init?.method ?? "GET", - endpoint, - requestBody, - responseBody, - wasMocked: false, - durationMs, - error: response.ok ? null : `HTTP ${response.status}`, - }); - }, - () => { - devBridge!.logRequest({ - id, - timestamp, - method: init?.method ?? "GET", - endpoint, - requestBody, - responseBody: null, - wasMocked: false, - durationMs, - error: response.ok ? null : `HTTP ${response.status}`, + return response; + } catch (error) { + devBridge.logRequest({ + id, + timestamp, + method: init?.method ?? "GET", + endpoint, + requestBody, + responseBody: null, + wasMocked: false, + durationMs: Math.round(performance.now() - start), + error: error instanceof Error ? error.message : "Network error", }); - }, - ); - - return response; - } catch (error) { - devBridge.logRequest({ - id, - timestamp, - method: init?.method ?? "GET", - endpoint, - requestBody, - responseBody: null, - wasMocked: false, - durationMs: Math.round(performance.now() - start), - error: error instanceof Error ? error.message : "Network error", - }); - throw error; - } + throw error; + } } diff --git a/src/pudu/sidecar.ts b/src/pudu/sidecar.ts index 1f31cf8..b940a6b 100644 --- a/src/pudu/sidecar.ts +++ b/src/pudu/sidecar.ts @@ -1,10 +1,10 @@ -import { invoke } from '@tauri-apps/api/core'; -import { listen } from '@tauri-apps/api/event'; +import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; let cachedPort: number | null = null; let portPromise: Promise | null = null; -const isTauri = '__TAURI_INTERNALS__' in window; +const isTauri = "__TAURI_INTERNALS__" in window; const SIDECAR_TIMEOUT_MS = 60_000; @@ -17,61 +17,61 @@ const SIDECAR_TIMEOUT_MS = 60_000; * In standalone dev mode (no Tauri), reads from VITE_SIDECAR_PORT env var. */ export function getSidecarPort(): Promise { - if (cachedPort) return Promise.resolve(cachedPort); + if (cachedPort) return Promise.resolve(cachedPort); - if (!isTauri) { - const envPort = import.meta.env.VITE_SIDECAR_PORT; - if (!envPort) { - return Promise.reject(new Error('Running outside Tauri: set VITE_SIDECAR_PORT in .env.local')); + if (!isTauri) { + const envPort = import.meta.env.VITE_SIDECAR_PORT; + if (!envPort) { + return Promise.reject(new Error("Running outside Tauri: set VITE_SIDECAR_PORT in .env.local")); + } + cachedPort = Number(envPort); + return Promise.resolve(cachedPort); } - cachedPort = Number(envPort); - return Promise.resolve(cachedPort); - } - // Deduplicate concurrent callers, one shared promise. - if (!portPromise) { - portPromise = discoverPort(); - } + // Deduplicate concurrent callers, one shared promise. + if (!portPromise) { + portPromise = discoverPort(); + } - return portPromise; + return portPromise; } async function discoverPort(): Promise { - // The event may have already fired before we loaded. Try the command first. - try { - const port = await invoke('get_sidecar_port'); - cachedPort = port; - return port; - } catch { - // Not ready yet fall through to event listener. - } + // The event may have already fired before we loaded. Try the command first. + try { + const port = await invoke("get_sidecar_port"); + cachedPort = port; + return port; + } catch { + // Not ready yet fall through to event listener. + } - return new Promise((resolve, reject) => { - let settled = false; + return new Promise((resolve, reject) => { + let settled = false; - const timeout = setTimeout(() => { - if (!settled) { - settled = true; - reject(new Error('Sidecar did not start in time')); - } - }, SIDECAR_TIMEOUT_MS); + const timeout = setTimeout(() => { + if (!settled) { + settled = true; + reject(new Error("Sidecar did not start in time")); + } + }, SIDECAR_TIMEOUT_MS); - void listen('sidecar-ready', (event) => { - if (settled) return; - settled = true; - clearTimeout(timeout); - cachedPort = event.payload; - resolve(event.payload); + void listen("sidecar-ready", (event) => { + if (settled) return; + settled = true; + clearTimeout(timeout); + cachedPort = event.payload; + resolve(event.payload); + }); }); - }); } export async function getSidecarBaseUrl(): Promise { - const port = await getSidecarPort(); - return `http://localhost:${port}`; + const port = await getSidecarPort(); + return `http://localhost:${port}`; } export async function getSidecarWsUrl(): Promise { - const port = await getSidecarPort(); - return `ws://localhost:${port}`; + const port = await getSidecarPort(); + return `ws://localhost:${port}`; } diff --git a/src/storybook/mockProviders.tsx b/src/storybook/mockProviders.tsx index ab85c04..b80b4de 100644 --- a/src/storybook/mockProviders.tsx +++ b/src/storybook/mockProviders.tsx @@ -1,8 +1,5 @@ import type { PropsWithChildren } from "react"; -import { - FeedbackContext, - type FeedbackContextValue, -} from "../contextProviders/FeedbackContextProvider"; +import { FeedbackContext, type FeedbackContextValue } from "../contextProviders/FeedbackContextProvider"; import { OnboardingContextProvider, type OnboardingApiClient, @@ -15,11 +12,9 @@ interface OnboardingCommandResult { error?: string | null; } -const NOOP = () => { }; +const NOOP = () => {}; -export function createMockFeedbackContextValue( - overrides: Partial = {}, -): FeedbackContextValue { +export function createMockFeedbackContextValue(overrides: Partial = {}): FeedbackContextValue { return { showError: NOOP, showFatal: NOOP, @@ -40,9 +35,7 @@ export function MockFeedbackProvider(props: MockFeedbackProviderProps) { const { children, value } = props; return ( - - {children} - + {children} ); } @@ -96,27 +89,19 @@ interface MockOnboardingProviderProps extends PropsWithChildren { } export function MockOnboardingProvider(props: MockOnboardingProviderProps) { - const { - children, - pendingSteps, - createApi, - apiMockOptions, - feedbackContextValue, - errorReporter, - } = props; + const { children, pendingSteps, createApi, apiMockOptions, feedbackContextValue, errorReporter } = props; - const createApiForProvider = createApi - ?? (() => createOnboardingApiMock({ - pendingSteps, - ...apiMockOptions, - })); + const createApiForProvider = + createApi ?? + (() => + createOnboardingApiMock({ + pendingSteps, + ...apiMockOptions, + })); return ( - + {children} diff --git a/src/themes/australNight.ts b/src/themes/australNight.ts index 762b85a..7816aa2 100644 --- a/src/themes/australNight.ts +++ b/src/themes/australNight.ts @@ -1,5 +1,5 @@ -import {extendTheme} from "@mui/joy/styles"; -import {CSSProperties} from "react"; +import { extendTheme } from "@mui/joy/styles"; +import { CSSProperties } from "react"; const australNight = extendTheme({ colorSchemes: { @@ -88,11 +88,11 @@ const australNight = extendTheme({ }); export const australNightScrollbarStyles: Record = { - "*": {scrollbarWidth: "thin", scrollbarColor: "#1fab78 #0e1b14"}, - "*::-webkit-scrollbar": {width: "12px", height: "12px"}, - "*::-webkit-scrollbar-track": {background: "#0e1b14"}, - "*::-webkit-scrollbar-thumb": {backgroundColor: "#1fab78", border: "2px solid #0e1b14", borderRadius: "8px"}, - "*::-webkit-scrollbar-thumb:hover": {backgroundColor: "#3bc792"}, + "*": { scrollbarWidth: "thin", scrollbarColor: "#1fab78 #0e1b14" }, + "*::-webkit-scrollbar": { width: "12px", height: "12px" }, + "*::-webkit-scrollbar-track": { background: "#0e1b14" }, + "*::-webkit-scrollbar-thumb": { backgroundColor: "#1fab78", border: "2px solid #0e1b14", borderRadius: "8px" }, + "*::-webkit-scrollbar-thumb:hover": { backgroundColor: "#3bc792" }, }; export default australNight; diff --git a/src/themes/doors95.ts b/src/themes/doors95.ts index f587e6b..bbce6d4 100644 --- a/src/themes/doors95.ts +++ b/src/themes/doors95.ts @@ -1,13 +1,13 @@ -import {extendTheme} from "@mui/joy/styles"; -import {CSSProperties} from "react"; +import { extendTheme } from "@mui/joy/styles"; +import { CSSProperties } from "react"; // DOORS 95 PALETTE -const WIN95_TEAL = "#008080"; // Desktop Background -const WIN95_GRAY = "#C0C0C0"; // Surface/Window Color -const WIN95_BLUE = "#000080"; // Title Bar / Primary -const WIN95_WHITE = "#FFFFFF"; // Highlights -const WIN95_BLACK = "#000000"; // Text / Deep Shadows -const WIN95_DKGRAY = "#808080"; // Shadow mid-tone +const WIN95_TEAL = "#008080"; // Desktop Background +const WIN95_GRAY = "#C0C0C0"; // Surface/Window Color +const WIN95_BLUE = "#000080"; // Title Bar / Primary +const WIN95_WHITE = "#FFFFFF"; // Highlights +const WIN95_BLACK = "#000000"; // Text / Deep Shadows +const WIN95_DKGRAY = "#808080"; // Shadow mid-tone // VGA 16-color palette - the only colors Win95 actually had const WIN95_MAROON = "#800000"; @@ -239,7 +239,7 @@ const doors95 = extendTheme({ components: { JoyButton: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ boxShadow: bevelUp, border: "none", padding: "4px 12px", @@ -257,9 +257,10 @@ const doors95 = extendTheme({ color: WIN95_WHITE, }, }), - ...(ownerState.variant === "solid" && ownerState.color === "primary" && { - fontWeight: "bold", - }), + ...(ownerState.variant === "solid" && + ownerState.color === "primary" && { + fontWeight: "bold", + }), ...(ownerState.variant === "soft" && { boxShadow: "none", border: `1px solid ${WIN95_DKGRAY}`, @@ -276,7 +277,7 @@ const doors95 = extendTheme({ }, JoyIconButton: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ boxShadow: ownerState.variant === "solid" ? bevelUp : "none", border: "none", "&:active": { @@ -408,7 +409,7 @@ const doors95 = extendTheme({ }, JoySheet: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ color: WIN95_BLACK, ...(ownerState.variant === "outlined" && { backgroundColor: WIN95_GRAY, @@ -451,7 +452,7 @@ const doors95 = extendTheme({ }, JoyAlert: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ backgroundColor: WIN95_GRAY, color: WIN95_BLACK, boxShadow: bevelUp, @@ -464,7 +465,7 @@ const doors95 = extendTheme({ }, JoyChip: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ ...(ownerState.variant !== "solid" && { color: WIN95_BLACK, }), @@ -473,7 +474,7 @@ const doors95 = extendTheme({ }, JoySwitch: { styleOverrides: { - track: ({ownerState}) => ({ + track: ({ ownerState }) => ({ backgroundColor: WIN95_GRAY, boxShadow: sunkenWell, ...(ownerState.checked && { @@ -541,14 +542,17 @@ const doors95 = extendTheme({ }, JoyLinearProgress: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ backgroundColor: WIN95_GRAY, boxShadow: sunkenWell, "--LinearProgress-progressColor": - ownerState.color === "danger" ? WIN95_MAROON - : ownerState.color === "success" ? WIN95_GREEN - : ownerState.color === "warning" ? WIN95_OLIVE - : WIN95_BLUE, + ownerState.color === "danger" + ? WIN95_MAROON + : ownerState.color === "success" + ? WIN95_GREEN + : ownerState.color === "warning" + ? WIN95_OLIVE + : WIN95_BLUE, }), }, }, @@ -695,7 +699,7 @@ const doors95 = extendTheme({ }, JoyAvatar: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ ...(ownerState.variant !== "solid" && { color: WIN95_BLACK, }), @@ -768,8 +772,8 @@ const doors95 = extendTheme({ }); export const doors95ScrollbarStyles: Record = { - "*": {scrollbarWidth: "thin", scrollbarColor: `${WIN95_GRAY} ${WIN95_TEAL}`}, - "*::-webkit-scrollbar": {width: "16px", height: "16px"}, + "*": { scrollbarWidth: "thin", scrollbarColor: `${WIN95_GRAY} ${WIN95_TEAL}` }, + "*::-webkit-scrollbar": { width: "16px", height: "16px" }, "*::-webkit-scrollbar-track": { background: WIN95_TEAL, boxShadow: sunkenWell, @@ -779,7 +783,7 @@ export const doors95ScrollbarStyles: Record = { boxShadow: bevelUp, border: "none", }, - "*::-webkit-scrollbar-thumb:hover": {backgroundColor: WIN95_BLUE}, + "*::-webkit-scrollbar-thumb:hover": { backgroundColor: WIN95_BLUE }, }; export default doors95; diff --git a/src/themes/hotdogStand.ts b/src/themes/hotdogStand.ts index baeace6..a6acec8 100644 --- a/src/themes/hotdogStand.ts +++ b/src/themes/hotdogStand.ts @@ -1,5 +1,5 @@ -import {extendTheme} from "@mui/joy/styles"; -import {CSSProperties} from "react"; +import { extendTheme } from "@mui/joy/styles"; +import { CSSProperties } from "react"; // THE UNHOLY TRINITY const RETRO_RED = "#CC0000"; @@ -209,7 +209,7 @@ const hotdogStandTheme = extendTheme({ components: { JoyButton: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ border: `2px solid ${RETRO_BLACK}`, boxShadow: `2px 2px 0px 0px ${RETRO_BLACK}`, "&:active": { @@ -254,7 +254,7 @@ const hotdogStandTheme = extendTheme({ }, JoyIconButton: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ ...(ownerState.variant === "solid" && { backgroundColor: RETRO_YELLOW, color: RETRO_BLACK, @@ -276,7 +276,7 @@ const hotdogStandTheme = extendTheme({ backgroundColor: RETRO_YELLOW, color: RETRO_BLACK, border: `2px solid ${RETRO_BLACK}`, - "&::before": {display: "none"}, + "&::before": { display: "none" }, "&.Mui-disabled": { backgroundColor: RETRO_DKRED, color: "#CC9900", @@ -290,7 +290,7 @@ const hotdogStandTheme = extendTheme({ backgroundColor: RETRO_YELLOW, color: RETRO_BLACK, border: `2px solid ${RETRO_BLACK}`, - "&::before": {display: "none"}, + "&::before": { display: "none" }, }, }, }, @@ -300,7 +300,7 @@ const hotdogStandTheme = extendTheme({ backgroundColor: RETRO_YELLOW, color: RETRO_BLACK, border: `2px solid ${RETRO_BLACK}`, - "&::before": {display: "none"}, + "&::before": { display: "none" }, }, indicator: { color: RETRO_BLACK, @@ -395,7 +395,7 @@ const hotdogStandTheme = extendTheme({ }, JoySheet: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ color: RETRO_YELLOW, backgroundColor: RETRO_RED, ...(ownerState.variant === "outlined" && { @@ -412,7 +412,7 @@ const hotdogStandTheme = extendTheme({ }, JoyAlert: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ backgroundColor: RETRO_YELLOW, color: RETRO_BLACK, border: `2px solid ${RETRO_BLACK}`, @@ -431,7 +431,7 @@ const hotdogStandTheme = extendTheme({ }, JoyChip: { styleOverrides: { - root: ({ownerState}) => ({ + root: ({ ownerState }) => ({ ...(ownerState.variant === "solid" && { backgroundColor: RETRO_YELLOW, color: RETRO_BLACK, @@ -449,7 +449,7 @@ const hotdogStandTheme = extendTheme({ }, JoySwitch: { styleOverrides: { - track: ({ownerState}) => ({ + track: ({ ownerState }) => ({ backgroundColor: RETRO_BLACK, border: `2px solid ${RETRO_YELLOW}`, ...(ownerState.checked && { @@ -724,15 +724,15 @@ const hotdogStandTheme = extendTheme({ }); export const hotdogStandScrollbarStyles: Record = { - "*": {scrollbarWidth: "thin", scrollbarColor: `${RETRO_YELLOW} ${RETRO_RED}`}, - "*::-webkit-scrollbar": {width: "12px", height: "12px"}, - "*::-webkit-scrollbar-track": {background: RETRO_RED}, + "*": { scrollbarWidth: "thin", scrollbarColor: `${RETRO_YELLOW} ${RETRO_RED}` }, + "*::-webkit-scrollbar": { width: "12px", height: "12px" }, + "*::-webkit-scrollbar-track": { background: RETRO_RED }, "*::-webkit-scrollbar-thumb": { backgroundColor: RETRO_YELLOW, border: `2px solid ${RETRO_RED}`, borderRadius: "0px", }, - "*::-webkit-scrollbar-thumb:hover": {backgroundColor: RETRO_WHITE}, + "*::-webkit-scrollbar-thumb:hover": { backgroundColor: RETRO_WHITE }, }; export default hotdogStandTheme; diff --git a/src/themes/index.ts b/src/themes/index.ts index 56c8460..87460cf 100644 --- a/src/themes/index.ts +++ b/src/themes/index.ts @@ -1,10 +1,10 @@ import australNightTheme, { australNightScrollbarStyles } from "./australNight"; import doors95Theme, { doors95ScrollbarStyles } from "./doors95"; -import hotdogStandTheme, { hotdogStandScrollbarStyles } from "./hotdogStand" -import puduTheme, { puduScrollbarStyles } from "./pudu" -import riseAndShineTheme, { riseAndShineScrollbarStyles } from "./riseAndShine" -import vaporTheme, { vaporScrollbarStyles } from "./vapor" -import unitystationClassicTheme, { unitystationClassicScrollbarStyles } from "./unitystationClassic" +import hotdogStandTheme, { hotdogStandScrollbarStyles } from "./hotdogStand"; +import puduTheme, { puduScrollbarStyles } from "./pudu"; +import riseAndShineTheme, { riseAndShineScrollbarStyles } from "./riseAndShine"; +import vaporTheme, { vaporScrollbarStyles } from "./vapor"; +import unitystationClassicTheme, { unitystationClassicScrollbarStyles } from "./unitystationClassic"; import { CSSProperties } from "react"; export const themeRegistry = { diff --git a/src/themes/riseAndShine.ts b/src/themes/riseAndShine.ts index ba8a64f..88c9842 100644 --- a/src/themes/riseAndShine.ts +++ b/src/themes/riseAndShine.ts @@ -6,22 +6,22 @@ import { CSSProperties } from "react"; // White text, yellow accents, olive-green panels, uniform buttons // Olive-green background tones (from the VGUI panel backgrounds) -const VGUI_DARKEST = "#1b1e14"; // Deepest: behind everything -const VGUI_DARKER = "#272b1e"; // Body background -const VGUI_DARK = "#333828"; // Surface / panels -const VGUI_MID = "#3e4430"; // Elevated surfaces, popups, cards -const VGUI_BORDER = "#4e5540"; // Panel borders, dividers -const VGUI_RAISED = "#565e48"; // Button face, raised elements +const VGUI_DARKEST = "#1b1e14"; // Deepest: behind everything +const VGUI_DARKER = "#272b1e"; // Body background +const VGUI_DARK = "#333828"; // Surface / panels +const VGUI_MID = "#3e4430"; // Elevated surfaces, popups, cards +const VGUI_BORDER = "#4e5540"; // Panel borders, dividers +const VGUI_RAISED = "#565e48"; // Button face, raised elements // Text: white primary, with yellow as the accent -const VGUI_WHITE = "#e8e8e0"; // Primary text (warm white) -const VGUI_GRAY = "#b0b0a4"; // Secondary text -const VGUI_DIM = "#787870"; // Tertiary / disabled text +const VGUI_WHITE = "#e8e8e0"; // Primary text (warm white) +const VGUI_GRAY = "#b0b0a4"; // Secondary text +const VGUI_DIM = "#787870"; // Tertiary / disabled text // THE accent: VGUI yellow-gold (achievement titles, progress bars, highlights) -const VGUI_YELLOW = "#d4a838"; // Primary accent -const VGUI_YELLOW_HI = "#e8bc48"; // Hover / bright -const VGUI_YELLOW_LO = "#b08828"; // Pressed / muted +const VGUI_YELLOW = "#d4a838"; // Primary accent +const VGUI_YELLOW_HI = "#e8bc48"; // Hover / bright +const VGUI_YELLOW_LO = "#b08828"; // Pressed / muted const VGUI_YELLOW_SOFT = "rgba(212, 168, 56, 0.12)"; const VGUI_YELLOW_SOFT_HI = "rgba(212, 168, 56, 0.22)"; const VGUI_YELLOW_SOFT_ACT = "rgba(212, 168, 56, 0.32)"; @@ -429,9 +429,9 @@ const riseAndShineTheme = extendTheme({ }, }); -const SCROLL_TRACK = "#6b7058"; // Lighter muted olive (the background gutter) -const SCROLL_THUMB = "#4a5038"; // Darker olive (the draggable part) -const SCROLL_THUMB_HI = "#565c46"; // Thumb on hover +const SCROLL_TRACK = "#6b7058"; // Lighter muted olive (the background gutter) +const SCROLL_THUMB = "#4a5038"; // Darker olive (the draggable part) +const SCROLL_THUMB_HI = "#565c46"; // Thumb on hover export const riseAndShineScrollbarStyles: Record = { "*": { scrollbarWidth: "auto", scrollbarColor: `${SCROLL_THUMB} ${SCROLL_TRACK}` }, diff --git a/src/themes/vapor.ts b/src/themes/vapor.ts index 0a41f5a..7692a5f 100644 --- a/src/themes/vapor.ts +++ b/src/themes/vapor.ts @@ -180,14 +180,15 @@ const vaporTheme = extendTheme({ textTransform: "none" as const, letterSpacing: "0.02em", transition: "all 0.15s ease", - ...(ownerState.variant === "solid" && (ownerState.color === "primary" || ownerState.color === "warning") && { - background: `linear-gradient(to left, ${GP_DUSTY_BLUE} 5%, ${GP_CHALKY_BLUE} 95%)`, - color: "#ffffff", - "&:hover": { - background: `linear-gradient(to left, #4b9ac4 5%, #7ecef7 95%)`, + ...(ownerState.variant === "solid" && + (ownerState.color === "primary" || ownerState.color === "warning") && { + background: `linear-gradient(to left, ${GP_DUSTY_BLUE} 5%, ${GP_CHALKY_BLUE} 95%)`, color: "#ffffff", - }, - }), + "&:hover": { + background: `linear-gradient(to left, #4b9ac4 5%, #7ecef7 95%)`, + color: "#ffffff", + }, + }), }), }, },
{JSON.stringify(mock.response, null, 2)}