From b0f81b224526a6fe5cf9d89cd84dcc0dea710d12 Mon Sep 17 00:00:00 2001 From: filtered <176114999+webfiltered@users.noreply.github.com> Date: Thu, 25 Sep 2025 05:06:58 +1000 Subject: [PATCH] Rework desktop install / startup UX (#5292) ### Summary Complete redesign of Desktop app installer, implementing the new app startup progress system and error reporting. --- public/assets/images/comfy-brand-mark.svg | 3 + public/assets/images/nvidia-logo-square.jpg | Bin 0 -> 40548 bytes .../tabs/terminal/BaseTerminal.vue | 12 +- src/components/common/StartupDisplay.vue | 71 +++ .../install/DesktopSettingsConfiguration.vue | 20 +- src/components/install/GpuPicker.vue | 254 ++++------- .../install/HardwareOption.stories.ts | 73 +++ src/components/install/HardwareOption.vue | 55 +++ src/components/install/InstallFooter.vue | 79 ++++ .../install/InstallLocationPicker.stories.ts | 148 ++++++ .../install/InstallLocationPicker.vue | 291 +++++++++--- .../install/MigrationPicker.stories.ts | 45 ++ src/components/install/MigrationPicker.vue | 11 +- .../install/MirrorsConfiguration.vue | 121 ----- src/components/install/mirror/MirrorItem.vue | 56 ++- .../bottomPanelTabs/useTerminal.ts | 3 +- src/locales/en/main.json | 32 +- src/locales/en/settings.json | 9 +- src/utils/validationUtil.ts | 13 - src/views/DesktopStartView.vue | 4 +- src/views/InstallView.stories.ts | 423 ++++++++++++++++++ src/views/InstallView.vue | 220 ++++----- src/views/ServerStartView.vue | 236 ++++++++-- src/views/WelcomeView.vue | 84 +--- 24 files changed, 1635 insertions(+), 628 deletions(-) create mode 100644 public/assets/images/comfy-brand-mark.svg create mode 100644 public/assets/images/nvidia-logo-square.jpg create mode 100644 src/components/common/StartupDisplay.vue create mode 100644 src/components/install/HardwareOption.stories.ts create mode 100644 src/components/install/HardwareOption.vue create mode 100644 src/components/install/InstallFooter.vue create mode 100644 src/components/install/InstallLocationPicker.stories.ts create mode 100644 src/components/install/MigrationPicker.stories.ts delete mode 100644 src/components/install/MirrorsConfiguration.vue create mode 100644 src/views/InstallView.stories.ts diff --git a/public/assets/images/comfy-brand-mark.svg b/public/assets/images/comfy-brand-mark.svg new file mode 100644 index 000000000..9056ae149 --- /dev/null +++ b/public/assets/images/comfy-brand-mark.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/assets/images/nvidia-logo-square.jpg b/public/assets/images/nvidia-logo-square.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c57f20b5df7f9a18af81767967a2e231d2e326ad GIT binary patch literal 40548 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 -
+
@@ -98,12 +101,13 @@ onUnmounted(() => { diff --git a/src/components/common/StartupDisplay.vue b/src/components/common/StartupDisplay.vue new file mode 100644 index 000000000..bd42c14e4 --- /dev/null +++ b/src/components/common/StartupDisplay.vue @@ -0,0 +1,71 @@ + + + diff --git a/src/components/install/DesktopSettingsConfiguration.vue b/src/components/install/DesktopSettingsConfiguration.vue index 2868d11a7..f1caa62fa 100644 --- a/src/components/install/DesktopSettingsConfiguration.vue +++ b/src/components/install/DesktopSettingsConfiguration.vue @@ -10,14 +10,14 @@

-
+

{{ $t('install.settings.autoUpdate') }}

-

+

{{ $t('install.settings.autoUpdateDescription') }}

@@ -32,14 +32,10 @@

{{ $t('install.settings.allowMetrics') }}

-

+

{{ $t('install.settings.allowMetricsDescription') }}

- + {{ $t('install.settings.learnMoreAboutData') }}
@@ -51,7 +47,9 @@

@@ -110,11 +108,7 @@ diff --git a/src/components/install/GpuPicker.vue b/src/components/install/GpuPicker.vue index 75916b480..e95d3204c 100644 --- a/src/components/install/GpuPicker.vue +++ b/src/components/install/GpuPicker.vue @@ -1,126 +1,66 @@ @@ -128,20 +68,12 @@ - - diff --git a/src/components/install/HardwareOption.stories.ts b/src/components/install/HardwareOption.stories.ts new file mode 100644 index 000000000..d830af49f --- /dev/null +++ b/src/components/install/HardwareOption.stories.ts @@ -0,0 +1,73 @@ +// eslint-disable-next-line storybook/no-renderer-packages +import type { Meta, StoryObj } from '@storybook/vue3' + +import HardwareOption from './HardwareOption.vue' + +const meta: Meta = { + title: 'Desktop/Components/HardwareOption', + component: HardwareOption, + parameters: { + layout: 'centered', + backgrounds: { + default: 'dark', + values: [{ name: 'dark', value: '#1a1a1a' }] + } + }, + argTypes: { + selected: { control: 'boolean' }, + imagePath: { control: 'text' }, + placeholderText: { control: 'text' }, + subtitle: { control: 'text' } + } +} + +export default meta +type Story = StoryObj + +export const AppleMetalSelected: Story = { + args: { + imagePath: '/assets/images/apple-mps-logo.png', + placeholderText: 'Apple Metal', + subtitle: 'Apple Metal', + value: 'mps', + selected: true + } +} + +export const AppleMetalUnselected: Story = { + args: { + imagePath: '/assets/images/apple-mps-logo.png', + placeholderText: 'Apple Metal', + subtitle: 'Apple Metal', + value: 'mps', + selected: false + } +} + +export const CPUOption: Story = { + args: { + placeholderText: 'CPU', + subtitle: 'Subtitle', + value: 'cpu', + selected: false + } +} + +export const ManualInstall: Story = { + args: { + placeholderText: 'Manual Install', + subtitle: 'Subtitle', + value: 'unsupported', + selected: false + } +} + +export const NvidiaSelected: Story = { + args: { + imagePath: '/assets/images/nvidia-logo-square.jpg', + placeholderText: 'NVIDIA', + subtitle: 'NVIDIA', + value: 'nvidia', + selected: true + } +} diff --git a/src/components/install/HardwareOption.vue b/src/components/install/HardwareOption.vue new file mode 100644 index 000000000..ae254fd8f --- /dev/null +++ b/src/components/install/HardwareOption.vue @@ -0,0 +1,55 @@ + + + diff --git a/src/components/install/InstallFooter.vue b/src/components/install/InstallFooter.vue new file mode 100644 index 000000000..ef9ab698c --- /dev/null +++ b/src/components/install/InstallFooter.vue @@ -0,0 +1,79 @@ + + + diff --git a/src/components/install/InstallLocationPicker.stories.ts b/src/components/install/InstallLocationPicker.stories.ts new file mode 100644 index 000000000..e6ef924ae --- /dev/null +++ b/src/components/install/InstallLocationPicker.stories.ts @@ -0,0 +1,148 @@ +// eslint-disable-next-line storybook/no-renderer-packages +import type { Meta, StoryObj } from '@storybook/vue3' +import { ref } from 'vue' + +import InstallLocationPicker from './InstallLocationPicker.vue' + +const meta: Meta = { + title: 'Desktop/Components/InstallLocationPicker', + component: InstallLocationPicker, + parameters: { + layout: 'padded', + backgrounds: { + default: 'dark', + values: [ + { name: 'dark', value: '#0a0a0a' }, + { name: 'neutral-900', value: '#171717' }, + { name: 'neutral-950', value: '#0a0a0a' } + ] + } + }, + decorators: [ + () => { + // Mock electron API + ;(window as any).electronAPI = { + getSystemPaths: () => + Promise.resolve({ + defaultInstallPath: '/Users/username/ComfyUI' + }), + validateInstallPath: () => + Promise.resolve({ + isValid: true, + exists: false, + canWrite: true, + freeSpace: 100000000000, + requiredSpace: 10000000000, + isNonDefaultDrive: false + }), + validateComfyUISource: () => + Promise.resolve({ + isValid: true + }), + showDirectoryPicker: () => Promise.resolve('/Users/username/ComfyUI') + } + return { template: '' } + } + ] +} + +export default meta +type Story = StoryObj + +// Default story with accordion expanded +export const Default: Story = { + render: (args) => ({ + components: { InstallLocationPicker }, + setup() { + const installPath = ref('/Users/username/ComfyUI') + const pathError = ref('') + const migrationSourcePath = ref('/Users/username/ComfyUI-old') + const migrationItemIds = ref(['models', 'custom_nodes']) + + return { + args, + installPath, + pathError, + migrationSourcePath, + migrationItemIds + } + }, + template: ` +
+ +
+ ` + }) +} + +// Story with different background to test transparency +export const OnNeutral900: Story = { + render: (args) => ({ + components: { InstallLocationPicker }, + setup() { + const installPath = ref('/Users/username/ComfyUI') + const pathError = ref('') + const migrationSourcePath = ref('/Users/username/ComfyUI-old') + const migrationItemIds = ref(['models', 'custom_nodes']) + + return { + args, + installPath, + pathError, + migrationSourcePath, + migrationItemIds + } + }, + template: ` +
+ +
+ ` + }) +} + +// Story with debug overlay showing background colors +export const DebugBackgrounds: Story = { + render: (args) => ({ + components: { InstallLocationPicker }, + setup() { + const installPath = ref('/Users/username/ComfyUI') + const pathError = ref('') + const migrationSourcePath = ref('/Users/username/ComfyUI-old') + const migrationItemIds = ref(['models', 'custom_nodes']) + + return { + args, + installPath, + pathError, + migrationSourcePath, + migrationItemIds + } + }, + template: ` +
+
+
Parent bg: neutral-950 (#0a0a0a)
+
Accordion content: bg-transparent
+
Migration options: bg-transparent + p-4 rounded-lg
+
+ +
+ ` + }) +} diff --git a/src/components/install/InstallLocationPicker.vue b/src/components/install/InstallLocationPicker.vue index 33b32a5f9..0e22f34a9 100644 --- a/src/components/install/InstallLocationPicker.vue +++ b/src/components/install/InstallLocationPicker.vue @@ -1,103 +1,215 @@ + + diff --git a/src/components/install/MigrationPicker.stories.ts b/src/components/install/MigrationPicker.stories.ts new file mode 100644 index 000000000..ad09e1871 --- /dev/null +++ b/src/components/install/MigrationPicker.stories.ts @@ -0,0 +1,45 @@ +// eslint-disable-next-line storybook/no-renderer-packages +import type { Meta, StoryObj } from '@storybook/vue3' +import { ref } from 'vue' + +import MigrationPicker from './MigrationPicker.vue' + +const meta: Meta = { + title: 'Desktop/Components/MigrationPicker', + component: MigrationPicker, + parameters: { + backgrounds: { + default: 'dark', + values: [ + { name: 'dark', value: '#0a0a0a' }, + { name: 'neutral-900', value: '#171717' } + ] + } + }, + decorators: [ + () => { + ;(window as any).electronAPI = { + validateComfyUISource: () => Promise.resolve({ isValid: true }), + showDirectoryPicker: () => Promise.resolve('/Users/username/ComfyUI') + } + + return { template: '' } + } + ] +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: () => ({ + components: { MigrationPicker }, + setup() { + const sourcePath = ref('') + const migrationItemIds = ref([]) + return { sourcePath, migrationItemIds } + }, + template: + '' + }) +} diff --git a/src/components/install/MigrationPicker.vue b/src/components/install/MigrationPicker.vue index 934ffc2f3..ba542ca69 100644 --- a/src/components/install/MigrationPicker.vue +++ b/src/components/install/MigrationPicker.vue @@ -2,10 +2,6 @@
-

- {{ $t('install.migrateFromExistingInstallation') }} -

-

{{ $t('install.migrationSourcePathDescription') }}

@@ -13,7 +9,7 @@
-
+

{{ $t('install.selectItemsToMigrate') }}

diff --git a/src/components/install/MirrorsConfiguration.vue b/src/components/install/MirrorsConfiguration.vue deleted file mode 100644 index c4c3565d6..000000000 --- a/src/components/install/MirrorsConfiguration.vue +++ /dev/null @@ -1,121 +0,0 @@ - - - diff --git a/src/components/install/mirror/MirrorItem.vue b/src/components/install/mirror/MirrorItem.vue index a4c5b563c..3665d66c9 100644 --- a/src/components/install/mirror/MirrorItem.vue +++ b/src/components/install/mirror/MirrorItem.vue @@ -1,10 +1,10 @@ diff --git a/src/views/InstallView.stories.ts b/src/views/InstallView.stories.ts new file mode 100644 index 000000000..9a5c65e79 --- /dev/null +++ b/src/views/InstallView.stories.ts @@ -0,0 +1,423 @@ +// eslint-disable-next-line storybook/no-renderer-packages +import type { Meta, StoryObj } from '@storybook/vue3' +import { nextTick, provide } from 'vue' +import { createMemoryHistory, createRouter } from 'vue-router' + +import InstallView from './InstallView.vue' + +// Create a mock router for stories +const createMockRouter = () => + createRouter({ + history: createMemoryHistory(), + routes: [ + { path: '/', component: { template: '
Home
' } }, + { + path: '/server-start', + component: { template: '
Server Start
' } + }, + { + path: '/manual-configuration', + component: { template: '
Manual Configuration
' } + } + ] + }) + +const meta: Meta = { + title: 'Desktop/Views/InstallView', + component: InstallView, + parameters: { + layout: 'fullscreen', + backgrounds: { + default: 'dark', + values: [ + { name: 'dark', value: '#0a0a0a' }, + { name: 'neutral-900', value: '#171717' }, + { name: 'neutral-950', value: '#0a0a0a' } + ] + } + }, + decorators: [ + (story) => { + // Create router for this story + const router = createMockRouter() + + // Mock electron API + ;(window as any).electronAPI = { + getPlatform: () => 'darwin', + Config: { + getDetectedGpu: () => Promise.resolve('mps') + }, + Events: { + trackEvent: (eventName: string, data?: any) => { + console.log('Track event:', eventName, data) + } + }, + installComfyUI: (options: any) => { + console.log('Install ComfyUI with options:', options) + }, + changeTheme: (theme: any) => { + console.log('Change theme:', theme) + }, + getSystemPaths: () => + Promise.resolve({ + defaultInstallPath: '/Users/username/ComfyUI' + }), + validateInstallPath: () => + Promise.resolve({ + isValid: true, + exists: false, + canWrite: true, + freeSpace: 100000000000, + requiredSpace: 10000000000, + isNonDefaultDrive: false + }), + validateComfyUISource: () => + Promise.resolve({ + isValid: true + }), + showDirectoryPicker: () => Promise.resolve('/Users/username/ComfyUI') + } + + return { + setup() { + // Provide router for all child components + provide('router', router) + return { + story + } + }, + template: '
' + } + } + ] +} + +export default meta +type Story = StoryObj + +// Default story - start at GPU selection +export const GpuSelection: Story = { + render: () => ({ + components: { InstallView }, + setup() { + // The component will automatically start at step 1 + return {} + }, + template: '' + }) +} + +// Story showing the install location step +export const InstallLocation: Story = { + render: () => ({ + components: { InstallView }, + setup() { + return {} + }, + async mounted() { + // Wait for component to be fully mounted + await nextTick() + + // Select Apple Metal option to enable navigation + const hardwareOptions = this.$el.querySelectorAll( + '.p-selectbutton-option' + ) + if (hardwareOptions.length > 0) { + hardwareOptions[0].click() // Click Apple Metal (first option) + } + + await nextTick() + + // Click Next to go to step 2 + const buttons = Array.from( + this.$el.querySelectorAll('button') + ) as HTMLButtonElement[] + const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next')) + if (nextBtn) { + nextBtn.click() + } + }, + template: '' + }) +} + +// Story showing the migration step (currently empty) +export const MigrationStep: Story = { + render: () => ({ + components: { InstallView }, + setup() { + return {} + }, + async mounted() { + // Wait for component to be fully mounted + await nextTick() + + // Select Apple Metal option to enable navigation + const hardwareOptions = this.$el.querySelectorAll( + '.p-selectbutton-option' + ) + if (hardwareOptions.length > 0) { + hardwareOptions[0].click() // Click Apple Metal (first option) + } + + await nextTick() + + // Click Next to go to step 2 + const buttons1 = Array.from( + this.$el.querySelectorAll('button') + ) as HTMLButtonElement[] + const nextBtn1 = buttons1.find((btn) => btn.textContent?.includes('Next')) + if (nextBtn1) { + nextBtn1.click() + } + + await nextTick() + + // Click Next again to go to step 3 + const buttons2 = Array.from( + this.$el.querySelectorAll('button') + ) as HTMLButtonElement[] + const nextBtn2 = buttons2.find((btn) => btn.textContent?.includes('Next')) + if (nextBtn2) { + nextBtn2.click() + } + }, + template: '' + }) +} + +// Story showing the desktop settings configuration +export const DesktopSettings: Story = { + render: () => ({ + components: { InstallView }, + setup() { + return {} + }, + async mounted() { + // Wait for component to be fully mounted + await nextTick() + + // Select Apple Metal option to enable navigation + const hardwareOptions = this.$el.querySelectorAll( + '.p-selectbutton-option' + ) + if (hardwareOptions.length > 0) { + hardwareOptions[0].click() // Click Apple Metal (first option) + } + + await nextTick() + + // Click Next to go to step 2 + const buttons1 = Array.from( + this.$el.querySelectorAll('button') + ) as HTMLButtonElement[] + const nextBtn1 = buttons1.find((btn) => btn.textContent?.includes('Next')) + if (nextBtn1) { + nextBtn1.click() + } + + await nextTick() + + // Click Next again to go to step 3 + const buttons2 = Array.from( + this.$el.querySelectorAll('button') + ) as HTMLButtonElement[] + const nextBtn2 = buttons2.find((btn) => btn.textContent?.includes('Next')) + if (nextBtn2) { + nextBtn2.click() + } + + await nextTick() + + // Click Next again to go to step 4 + const buttons3 = Array.from( + this.$el.querySelectorAll('button') + ) as HTMLButtonElement[] + const nextBtn3 = buttons3.find((btn) => btn.textContent?.includes('Next')) + if (nextBtn3) { + nextBtn3.click() + } + }, + template: '' + }) +} + +// Story with Windows platform (no Apple Metal option) +export const WindowsPlatform: Story = { + render: () => { + // Override the platform to Windows + ;(window as any).electronAPI.getPlatform = () => 'win32' + ;(window as any).electronAPI.Config.getDetectedGpu = () => + Promise.resolve('nvidia') + + return { + components: { InstallView }, + setup() { + return {} + }, + template: '' + } + } +} + +// Story with macOS platform (Apple Metal option) +export const MacOSPlatform: Story = { + name: 'macOS Platform', + render: () => { + // Override the platform to macOS + ;(window as any).electronAPI.getPlatform = () => 'darwin' + ;(window as any).electronAPI.Config.getDetectedGpu = () => + Promise.resolve('mps') + + return { + components: { InstallView }, + setup() { + return {} + }, + template: '' + } + } +} + +// Story with CPU selected +export const CpuSelected: Story = { + render: () => ({ + components: { InstallView }, + setup() { + return {} + }, + async mounted() { + // Wait for component to be fully mounted + await nextTick() + + // Find and click the CPU hardware option + const hardwareButtons = this.$el.querySelectorAll('.hardware-option') + // CPU is the button with "CPU" text + for (const button of hardwareButtons) { + if (button.textContent?.includes('CPU')) { + button.click() + break + } + } + }, + template: '' + }) +} + +// Story with manual install selected +export const ManualInstall: Story = { + render: () => ({ + components: { InstallView }, + setup() { + return {} + }, + async mounted() { + // Wait for component to be fully mounted + await nextTick() + + // Find and click the Manual Install hardware option + const hardwareButtons = this.$el.querySelectorAll('.hardware-option') + // Manual Install is the button with "Manual Install" text + for (const button of hardwareButtons) { + if (button.textContent?.includes('Manual Install')) { + button.click() + break + } + } + }, + template: '' + }) +} + +// Story with error state (invalid install path) +export const ErrorState: Story = { + render: () => { + // Override validation to return an error + ;(window as any).electronAPI.validateInstallPath = () => + Promise.resolve({ + isValid: false, + exists: false, + canWrite: false, + freeSpace: 100000000000, + requiredSpace: 10000000000, + isNonDefaultDrive: false, + error: 'Story mock: Example error state' + }) + + return { + components: { InstallView }, + setup() { + return {} + }, + async mounted() { + // Wait for component to be fully mounted + await nextTick() + + // Select Apple Metal option to enable navigation + const hardwareOptions = this.$el.querySelectorAll( + '.p-selectbutton-option' + ) + if (hardwareOptions.length > 0) { + hardwareOptions[0].click() // Click Apple Metal (first option) + } + + await nextTick() + + // Click Next to go to step 2 where error will be shown + const buttons = Array.from( + this.$el.querySelectorAll('button') + ) as HTMLButtonElement[] + const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next')) + if (nextBtn) { + nextBtn.click() + } + }, + template: '' + } + } +} + +// Story with warning state (non-default drive) +export const WarningState: Story = { + render: () => { + // Override validation to return a warning about non-default drive + ;(window as any).electronAPI.validateInstallPath = () => + Promise.resolve({ + isValid: true, + exists: false, + canWrite: true, + freeSpace: 500_000_000_000, + requiredSpace: 10_000_000_000, + isNonDefaultDrive: true + }) + + return { + components: { InstallView }, + setup() { + return {} + }, + async mounted() { + // Wait for component to be fully mounted + await nextTick() + + // Select Apple Metal option to enable navigation + const hardwareOptions = this.$el.querySelectorAll('.hardware-option') + if (hardwareOptions.length > 0) { + hardwareOptions[0].click() // Click Apple Metal (first option) + } + + await nextTick() + + // Click Next to go to step 2 where warning will be shown + const buttons = Array.from( + this.$el.querySelectorAll('button') + ) as HTMLButtonElement[] + const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next')) + if (nextBtn) { + nextBtn.click() + } + }, + template: '' + } + } +} diff --git a/src/views/InstallView.vue b/src/views/InstallView.vue index ed3da0a8a..6b170937c 100644 --- a/src/views/InstallView.vue +++ b/src/views/InstallView.vue @@ -1,111 +1,54 @@ @@ -114,9 +57,6 @@ import type { InstallOptions, TorchDeviceType } from '@comfyorg/comfyui-electron-types' -import Button from 'primevue/button' -import Step from 'primevue/step' -import StepList from 'primevue/steplist' import StepPanel from 'primevue/steppanel' import StepPanels from 'primevue/steppanels' import Stepper from 'primevue/stepper' @@ -125,9 +65,8 @@ import { useRouter } from 'vue-router' import DesktopSettingsConfiguration from '@/components/install/DesktopSettingsConfiguration.vue' import GpuPicker from '@/components/install/GpuPicker.vue' +import InstallFooter from '@/components/install/InstallFooter.vue' import InstallLocationPicker from '@/components/install/InstallLocationPicker.vue' -import MigrationPicker from '@/components/install/MigrationPicker.vue' -import MirrorsConfiguration from '@/components/install/MirrorsConfiguration.vue' import { electronAPI } from '@/utils/envUtil' import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue' @@ -145,6 +84,9 @@ const pythonMirror = ref('') const pypiMirror = ref('') const torchMirror = ref('') +/** Current step in the stepper */ +const currentStep = ref('1') + /** Forces each install step to be visited at least once. */ const highestStep = ref(0) @@ -164,6 +106,40 @@ const setHighestStep = (value: string | number) => { const hasError = computed(() => pathError.value !== '') const noGpu = computed(() => typeof device.value !== 'string') +// Computed property to determine if user can proceed to next step +const regex = /^Insufficient space - minimum free space: \d+ GB$/ + +const canProceed = computed(() => { + switch (currentStep.value) { + case '1': + return typeof device.value === 'string' + case '2': + return pathError.value === '' || regex.test(pathError.value) + case '3': + return !hasError.value + default: + return false + } +}) + +// Navigation methods +const goToNextStep = () => { + const nextStep = (parseInt(currentStep.value) + 1).toString() + currentStep.value = nextStep + setHighestStep(nextStep) + electronAPI().Events.trackEvent('install_stepper_change', { + step: nextStep + }) +} + +const goToPreviousStep = () => { + const prevStep = (parseInt(currentStep.value) - 1).toString() + currentStep.value = prevStep + electronAPI().Events.trackEvent('install_stepper_change', { + step: prevStep + }) +} + const electron = electronAPI() const router = useRouter() const install = async () => { @@ -195,7 +171,7 @@ onMounted(async () => { } electronAPI().Events.trackEvent('install_stepper_change', { - step: '0', + step: currentStep.value, gpu: detectedGpu }) }) @@ -205,6 +181,30 @@ onMounted(async () => { @reference '../assets/css/style.css'; :deep(.p-steppanel) { + @apply mt-8 flex justify-center bg-transparent; +} + +/* Remove default padding/margin from StepPanels to make scrollbar flush */ +:deep(.p-steppanels) { + @apply p-0 m-0; +} + +/* Ensure StepPanel content container has no top/bottom padding */ +:deep(.p-steppanel-content) { + @apply p-0; +} + +/* Custom overlay scrollbar for WebKit browsers (Electron, Chrome) */ +:deep(.p-steppanels::-webkit-scrollbar) { + @apply w-4; +} + +:deep(.p-steppanels::-webkit-scrollbar-track) { @apply bg-transparent; } + +:deep(.p-steppanels::-webkit-scrollbar-thumb) { + @apply bg-white/20 rounded-lg border-[4px] border-transparent; + background-clip: content-box; +} diff --git a/src/views/ServerStartView.vue b/src/views/ServerStartView.vue index c74e3a967..dd6409991 100644 --- a/src/views/ServerStartView.vue +++ b/src/views/ServerStartView.vue @@ -1,76 +1,205 @@ + + diff --git a/src/views/WelcomeView.vue b/src/views/WelcomeView.vue index 597bc5a6b..ad653e6a8 100644 --- a/src/views/WelcomeView.vue +++ b/src/views/WelcomeView.vue @@ -1,21 +1,27 @@ @@ -31,49 +37,3 @@ const navigateTo = async (path: string) => { await router.push(path) } - -