@ -12,9 +12,10 @@ import { CmdBarFixture } from './cmdBarFixture'
 | 
			
		||||
import { EditorFixture } from './editorFixture'
 | 
			
		||||
import { ToolbarFixture } from './toolbarFixture'
 | 
			
		||||
import { SceneFixture } from './sceneFixture'
 | 
			
		||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
 | 
			
		||||
import { HomePageFixture } from './homePageFixture'
 | 
			
		||||
import { unsafeTypedKeys } from 'lib/utils'
 | 
			
		||||
import { DeepPartial } from 'lib/types'
 | 
			
		||||
import { Settings } from 'wasm-lib/kcl/bindings/Settings'
 | 
			
		||||
 | 
			
		||||
export class AuthenticatedApp {
 | 
			
		||||
  public readonly page: Page
 | 
			
		||||
@ -78,7 +79,7 @@ export class AuthenticatedTronApp {
 | 
			
		||||
      fixtures: Partial<Fixtures>
 | 
			
		||||
      folderSetupFn?: (projectDirName: string) => Promise<void>
 | 
			
		||||
      cleanProjectDir?: boolean
 | 
			
		||||
      appSettings?: Partial<SaveSettingsPayload>
 | 
			
		||||
      appSettings?: DeepPartial<Settings>
 | 
			
		||||
    } = { fixtures: {} }
 | 
			
		||||
  ) {
 | 
			
		||||
    const { electronApp, page, context, dir } = await setupElectron({
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,12 @@
 | 
			
		||||
import { test, expect } from './zoo-test'
 | 
			
		||||
import { join } from 'path'
 | 
			
		||||
import fsp from 'fs/promises'
 | 
			
		||||
import { getUtils, executorInputPath, createProject } from './test-utils'
 | 
			
		||||
import {
 | 
			
		||||
  getUtils,
 | 
			
		||||
  executorInputPath,
 | 
			
		||||
  createProject,
 | 
			
		||||
  settingsToToml,
 | 
			
		||||
} from './test-utils'
 | 
			
		||||
import { bracket } from 'lib/exampleKcl'
 | 
			
		||||
import { onboardingPaths } from 'routes/Onboarding/paths'
 | 
			
		||||
import {
 | 
			
		||||
@ -10,7 +15,6 @@ import {
 | 
			
		||||
  TEST_SETTINGS_ONBOARDING_EXPORT,
 | 
			
		||||
  TEST_SETTINGS_ONBOARDING_USER_MENU,
 | 
			
		||||
} from './storageStates'
 | 
			
		||||
import * as TOML from '@iarna/toml'
 | 
			
		||||
import { expectPixelColor } from './fixtures/sceneFixture'
 | 
			
		||||
 | 
			
		||||
// Because onboarding relies on an app setting we need to set it as incompletel
 | 
			
		||||
@ -22,7 +26,7 @@ test.describe('Onboarding tests', () => {
 | 
			
		||||
    {
 | 
			
		||||
      appSettings: {
 | 
			
		||||
        app: {
 | 
			
		||||
          onboardingStatus: 'incomplete',
 | 
			
		||||
          onboarding_status: 'incomplete',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      cleanProjectDir: true,
 | 
			
		||||
@ -63,7 +67,7 @@ test.describe('Onboarding tests', () => {
 | 
			
		||||
      tag: '@electron',
 | 
			
		||||
      appSettings: {
 | 
			
		||||
        app: {
 | 
			
		||||
          onboardingStatus: 'incomplete',
 | 
			
		||||
          onboarding_status: 'incomplete',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      cleanProjectDir: true,
 | 
			
		||||
@ -108,7 +112,7 @@ test.describe('Onboarding tests', () => {
 | 
			
		||||
    {
 | 
			
		||||
      appSettings: {
 | 
			
		||||
        app: {
 | 
			
		||||
          onboardingStatus: 'incomplete',
 | 
			
		||||
          onboarding_status: 'incomplete',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      cleanProjectDir: true,
 | 
			
		||||
@ -158,7 +162,7 @@ test.describe('Onboarding tests', () => {
 | 
			
		||||
    {
 | 
			
		||||
      appSettings: {
 | 
			
		||||
        app: {
 | 
			
		||||
          onboardingStatus: 'incomplete',
 | 
			
		||||
          onboarding_status: 'incomplete',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
@ -172,7 +176,7 @@ test.describe('Onboarding tests', () => {
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          settingsKey: TEST_SETTINGS_KEY,
 | 
			
		||||
          settings: TOML.stringify({
 | 
			
		||||
          settings: settingsToToml({
 | 
			
		||||
            settings: TEST_SETTINGS_ONBOARDING_START,
 | 
			
		||||
          }),
 | 
			
		||||
        }
 | 
			
		||||
@ -208,7 +212,7 @@ test.describe('Onboarding tests', () => {
 | 
			
		||||
    {
 | 
			
		||||
      appSettings: {
 | 
			
		||||
        app: {
 | 
			
		||||
          onboardingStatus: '/export',
 | 
			
		||||
          onboarding_status: '/export',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      cleanProjectDir: true,
 | 
			
		||||
@ -225,7 +229,7 @@ test.describe('Onboarding tests', () => {
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          settingsKey: TEST_SETTINGS_KEY,
 | 
			
		||||
          settings: TOML.stringify({
 | 
			
		||||
          settings: settingsToToml({
 | 
			
		||||
            settings: TEST_SETTINGS_ONBOARDING_EXPORT,
 | 
			
		||||
          }),
 | 
			
		||||
        }
 | 
			
		||||
@ -263,7 +267,7 @@ test.describe('Onboarding tests', () => {
 | 
			
		||||
    {
 | 
			
		||||
      appSettings: {
 | 
			
		||||
        app: {
 | 
			
		||||
          onboardingStatus: '/parametric-modeling',
 | 
			
		||||
          onboarding_status: '/parametric-modeling',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      cleanProjectDir: true,
 | 
			
		||||
@ -313,7 +317,7 @@ test.describe('Onboarding tests', () => {
 | 
			
		||||
    {
 | 
			
		||||
      appSettings: {
 | 
			
		||||
        app: {
 | 
			
		||||
          onboardingStatus: 'incomplete',
 | 
			
		||||
          onboarding_status: 'incomplete',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      cleanProjectDir: true,
 | 
			
		||||
@ -326,7 +330,7 @@ test.describe('Onboarding tests', () => {
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          settingsKey: TEST_SETTINGS_KEY,
 | 
			
		||||
          settings: TOML.stringify({
 | 
			
		||||
          settings: settingsToToml({
 | 
			
		||||
            settings: TEST_SETTINGS_ONBOARDING_USER_MENU,
 | 
			
		||||
          }),
 | 
			
		||||
        }
 | 
			
		||||
@ -386,7 +390,7 @@ test.describe('Onboarding tests', () => {
 | 
			
		||||
    {
 | 
			
		||||
      appSettings: {
 | 
			
		||||
        app: {
 | 
			
		||||
          onboardingStatus: 'incomplete',
 | 
			
		||||
          onboarding_status: 'incomplete',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      cleanProjectDir: true,
 | 
			
		||||
@ -400,7 +404,7 @@ test.describe('Onboarding tests', () => {
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          settingsKey: TEST_SETTINGS_KEY,
 | 
			
		||||
          settings: TOML.stringify({
 | 
			
		||||
          settings: settingsToToml({
 | 
			
		||||
            settings: TEST_SETTINGS_ONBOARDING_USER_MENU,
 | 
			
		||||
          }),
 | 
			
		||||
        }
 | 
			
		||||
@ -442,7 +446,7 @@ test.fixme(
 | 
			
		||||
  {
 | 
			
		||||
    appSettings: {
 | 
			
		||||
      app: {
 | 
			
		||||
        onboardingStatus: 'dismissed',
 | 
			
		||||
        onboarding_status: 'dismissed',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    cleanProjectDir: true,
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { test, expect } from '@playwright/test'
 | 
			
		||||
import { secrets } from './secrets'
 | 
			
		||||
import { Paths, doExport, getUtils } from './test-utils'
 | 
			
		||||
import { Paths, doExport, getUtils, settingsToToml } from './test-utils'
 | 
			
		||||
import { Models } from '@kittycad/lib'
 | 
			
		||||
import fsp from 'fs/promises'
 | 
			
		||||
import { spawn } from 'child_process'
 | 
			
		||||
@ -12,7 +12,6 @@ import {
 | 
			
		||||
  TEST_SETTINGS,
 | 
			
		||||
  TEST_SETTINGS_KEY,
 | 
			
		||||
} from './storageStates'
 | 
			
		||||
import * as TOML from '@iarna/toml'
 | 
			
		||||
 | 
			
		||||
test.beforeEach(async ({ page }) => {
 | 
			
		||||
  // reducedMotion kills animations, which speeds up tests and reduces flakiness
 | 
			
		||||
@ -29,7 +28,7 @@ test.beforeEach(async ({ page }) => {
 | 
			
		||||
    {
 | 
			
		||||
      token: secrets.token,
 | 
			
		||||
      settingsKey: TEST_SETTINGS_KEY,
 | 
			
		||||
      settings: TOML.stringify({ settings: TEST_SETTINGS }),
 | 
			
		||||
      settings: settingsToToml({ settings: TEST_SETTINGS }),
 | 
			
		||||
      IS_PLAYWRIGHT_KEY: IS_PLAYWRIGHT_KEY,
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
@ -704,12 +703,12 @@ test.describe(
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          settingsKey: TEST_SETTINGS_KEY,
 | 
			
		||||
          settings: TOML.stringify({
 | 
			
		||||
          settings: settingsToToml({
 | 
			
		||||
            settings: {
 | 
			
		||||
              ...TEST_SETTINGS,
 | 
			
		||||
              modeling: {
 | 
			
		||||
                ...TEST_SETTINGS.modeling,
 | 
			
		||||
                defaultUnit: 'mm',
 | 
			
		||||
                base_unit: 'mm',
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          }),
 | 
			
		||||
@ -1062,12 +1061,12 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        settingsKey: TEST_SETTINGS_KEY,
 | 
			
		||||
        settings: TOML.stringify({
 | 
			
		||||
        settings: settingsToToml({
 | 
			
		||||
          settings: {
 | 
			
		||||
            ...TEST_SETTINGS,
 | 
			
		||||
            modeling: {
 | 
			
		||||
              ...TEST_SETTINGS.modeling,
 | 
			
		||||
              showScaleGrid: true,
 | 
			
		||||
              show_scale_grid: true,
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        }),
 | 
			
		||||
 | 
			
		||||
@ -1,78 +1,83 @@
 | 
			
		||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
 | 
			
		||||
import { Themes } from 'lib/theme'
 | 
			
		||||
import { DeepPartial } from 'lib/types'
 | 
			
		||||
import { onboardingPaths } from 'routes/Onboarding/paths'
 | 
			
		||||
import { Settings } from 'wasm-lib/kcl/bindings/Settings'
 | 
			
		||||
 | 
			
		||||
export const IS_PLAYWRIGHT_KEY = 'playwright'
 | 
			
		||||
 | 
			
		||||
export const TEST_SETTINGS_KEY = '/settings.toml'
 | 
			
		||||
export const TEST_SETTINGS = {
 | 
			
		||||
  app: {
 | 
			
		||||
    theme: Themes.Dark,
 | 
			
		||||
    onboardingStatus: 'dismissed',
 | 
			
		||||
    projectDirectory: '',
 | 
			
		||||
    enableSSAO: false,
 | 
			
		||||
    onboarding_status: 'dismissed',
 | 
			
		||||
    appearance: {
 | 
			
		||||
      theme: Themes.Dark,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  modeling: {
 | 
			
		||||
    defaultUnit: 'in',
 | 
			
		||||
    mouseControls: 'Zoo',
 | 
			
		||||
    cameraProjection: 'perspective',
 | 
			
		||||
    showDebugPanel: true,
 | 
			
		||||
    base_unit: 'in',
 | 
			
		||||
    mouse_controls: 'zoo',
 | 
			
		||||
    camera_projection: 'perspective',
 | 
			
		||||
    show_debug_panel: true,
 | 
			
		||||
    enable_ssao: false,
 | 
			
		||||
  },
 | 
			
		||||
  projects: {
 | 
			
		||||
    defaultProjectName: 'project-$nnn',
 | 
			
		||||
  project: {
 | 
			
		||||
    default_project_name: 'project-$nnn',
 | 
			
		||||
    directory: '',
 | 
			
		||||
  },
 | 
			
		||||
  textEditor: {
 | 
			
		||||
    textWrapping: true,
 | 
			
		||||
  text_editor: {
 | 
			
		||||
    text_wrapping: true,
 | 
			
		||||
  },
 | 
			
		||||
} satisfies Partial<SaveSettingsPayload>
 | 
			
		||||
} satisfies DeepPartial<Settings>
 | 
			
		||||
 | 
			
		||||
export const TEST_SETTINGS_ONBOARDING_USER_MENU = {
 | 
			
		||||
  ...TEST_SETTINGS,
 | 
			
		||||
  app: { ...TEST_SETTINGS.app, onboardingStatus: onboardingPaths.USER_MENU },
 | 
			
		||||
} satisfies Partial<SaveSettingsPayload>
 | 
			
		||||
  app: { ...TEST_SETTINGS.app, onboarding_status: onboardingPaths.USER_MENU },
 | 
			
		||||
} satisfies DeepPartial<Settings>
 | 
			
		||||
 | 
			
		||||
export const TEST_SETTINGS_ONBOARDING_EXPORT = {
 | 
			
		||||
  ...TEST_SETTINGS,
 | 
			
		||||
  app: { ...TEST_SETTINGS.app, onboardingStatus: onboardingPaths.EXPORT },
 | 
			
		||||
} satisfies Partial<SaveSettingsPayload>
 | 
			
		||||
  app: { ...TEST_SETTINGS.app, onboarding_status: onboardingPaths.EXPORT },
 | 
			
		||||
} satisfies DeepPartial<Settings>
 | 
			
		||||
 | 
			
		||||
export const TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING = {
 | 
			
		||||
  ...TEST_SETTINGS,
 | 
			
		||||
  app: {
 | 
			
		||||
    ...TEST_SETTINGS.app,
 | 
			
		||||
    onboardingStatus: onboardingPaths.PARAMETRIC_MODELING,
 | 
			
		||||
    onboarding_status: onboardingPaths.PARAMETRIC_MODELING,
 | 
			
		||||
  },
 | 
			
		||||
} satisfies Partial<SaveSettingsPayload>
 | 
			
		||||
} satisfies DeepPartial<Settings>
 | 
			
		||||
 | 
			
		||||
export const TEST_SETTINGS_ONBOARDING_START = {
 | 
			
		||||
  ...TEST_SETTINGS,
 | 
			
		||||
  app: { ...TEST_SETTINGS.app, onboardingStatus: '' },
 | 
			
		||||
} satisfies Partial<SaveSettingsPayload>
 | 
			
		||||
  app: { ...TEST_SETTINGS.app, onboarding_status: '' },
 | 
			
		||||
} satisfies DeepPartial<Settings>
 | 
			
		||||
 | 
			
		||||
export const TEST_SETTINGS_DEFAULT_THEME = {
 | 
			
		||||
  ...TEST_SETTINGS,
 | 
			
		||||
  app: { ...TEST_SETTINGS.app, theme: Themes.System },
 | 
			
		||||
} satisfies Partial<SaveSettingsPayload>
 | 
			
		||||
  app: { ...TEST_SETTINGS.app, appearance: { theme: Themes.System } },
 | 
			
		||||
} satisfies DeepPartial<Settings>
 | 
			
		||||
 | 
			
		||||
export const TEST_SETTINGS_CORRUPTED = {
 | 
			
		||||
  app: {
 | 
			
		||||
    theme: Themes.Dark,
 | 
			
		||||
    onboardingStatus: 'dismissed',
 | 
			
		||||
    projectDirectory: 123 as any,
 | 
			
		||||
    onboarding_status: 'dismissed',
 | 
			
		||||
    appearance: {
 | 
			
		||||
      theme: Themes.Dark,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  modeling: {
 | 
			
		||||
    defaultUnit: 'invalid' as any,
 | 
			
		||||
    mouseControls: `() => alert('hack the planet')` as any,
 | 
			
		||||
    cameraProjection: 'perspective',
 | 
			
		||||
    showDebugPanel: true,
 | 
			
		||||
    base_unit: 'invalid' as any,
 | 
			
		||||
    mouse_controls: `() => alert('hack the planet')` as any,
 | 
			
		||||
    camera_projection: 'perspective',
 | 
			
		||||
    show_debug_panel: true,
 | 
			
		||||
  },
 | 
			
		||||
  projects: {
 | 
			
		||||
    defaultProjectName: false as any,
 | 
			
		||||
  project: {
 | 
			
		||||
    default_project_name: false as any,
 | 
			
		||||
    directory: 123 as any,
 | 
			
		||||
  },
 | 
			
		||||
  textEditor: {
 | 
			
		||||
    textWrapping: true,
 | 
			
		||||
  text_editor: {
 | 
			
		||||
    text_wrapping: true,
 | 
			
		||||
  },
 | 
			
		||||
} satisfies Partial<SaveSettingsPayload>
 | 
			
		||||
} satisfies DeepPartial<Settings>
 | 
			
		||||
 | 
			
		||||
export const TEST_CODE_GIZMO = `part001 = startSketchOn('XZ')
 | 
			
		||||
|> startProfileAt([20, 0], %)
 | 
			
		||||
 | 
			
		||||
@ -23,11 +23,13 @@ import {
 | 
			
		||||
  IS_PLAYWRIGHT_KEY,
 | 
			
		||||
} from './storageStates'
 | 
			
		||||
import * as TOML from '@iarna/toml'
 | 
			
		||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
 | 
			
		||||
import { SETTINGS_FILE_NAME } from 'lib/constants'
 | 
			
		||||
import { isErrorWhitelisted } from './lib/console-error-whitelist'
 | 
			
		||||
import { isArray } from 'lib/utils'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
import { Configuration } from 'lang/wasm'
 | 
			
		||||
import { DeepPartial } from 'lib/types'
 | 
			
		||||
import { Settings } from 'wasm-lib/kcl/bindings/Settings'
 | 
			
		||||
 | 
			
		||||
const toNormalizedCode = (text: string) => {
 | 
			
		||||
  return text.replace(/\s+/g, '')
 | 
			
		||||
@ -884,19 +886,23 @@ export async function setup(
 | 
			
		||||
    {
 | 
			
		||||
      token: secrets.token,
 | 
			
		||||
      settingsKey: TEST_SETTINGS_KEY,
 | 
			
		||||
      settings: TOML.stringify({
 | 
			
		||||
      settings: settingsToToml({
 | 
			
		||||
        settings: {
 | 
			
		||||
          ...TEST_SETTINGS,
 | 
			
		||||
          app: {
 | 
			
		||||
            ...TEST_SETTINGS.projects,
 | 
			
		||||
            projectDirectory: TEST_SETTINGS.app.projectDirectory,
 | 
			
		||||
            onboardingStatus: 'dismissed',
 | 
			
		||||
            theme: 'dark',
 | 
			
		||||
            ...TEST_SETTINGS.app,
 | 
			
		||||
            onboarding_status: 'dismissed',
 | 
			
		||||
            appearance: {
 | 
			
		||||
              theme: 'dark',
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        } as Partial<SaveSettingsPayload>,
 | 
			
		||||
          project: {
 | 
			
		||||
            directory: TEST_SETTINGS.project.directory,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
      IS_PLAYWRIGHT_KEY,
 | 
			
		||||
      PLAYWRIGHT_TEST_DIR: TEST_SETTINGS.app.projectDirectory,
 | 
			
		||||
      PLAYWRIGHT_TEST_DIR: TEST_SETTINGS.project.directory,
 | 
			
		||||
      PERSIST_MODELING_CONTEXT,
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
@ -931,7 +937,7 @@ export async function setupElectron({
 | 
			
		||||
  testInfo: TestInfo
 | 
			
		||||
  folderSetupFn?: (projectDirName: string) => Promise<void>
 | 
			
		||||
  cleanProjectDir?: boolean
 | 
			
		||||
  appSettings?: Partial<SaveSettingsPayload>
 | 
			
		||||
  appSettings?: DeepPartial<Settings>
 | 
			
		||||
}): Promise<{
 | 
			
		||||
  electronApp: ElectronApplication
 | 
			
		||||
  context: BrowserContext
 | 
			
		||||
@ -978,7 +984,7 @@ export async function setupElectron({
 | 
			
		||||
 | 
			
		||||
  if (cleanProjectDir) {
 | 
			
		||||
    const tempSettingsFilePath = path.join(projectDirName, SETTINGS_FILE_NAME)
 | 
			
		||||
    const settingsOverrides = TOML.stringify(
 | 
			
		||||
    const settingsOverrides = settingsToToml(
 | 
			
		||||
      appSettings
 | 
			
		||||
        ? {
 | 
			
		||||
            settings: {
 | 
			
		||||
@ -986,9 +992,11 @@ export async function setupElectron({
 | 
			
		||||
              ...appSettings,
 | 
			
		||||
              app: {
 | 
			
		||||
                ...TEST_SETTINGS.app,
 | 
			
		||||
                projectDirectory: projectDirName,
 | 
			
		||||
                ...appSettings.app,
 | 
			
		||||
              },
 | 
			
		||||
              project: {
 | 
			
		||||
                directory: projectDirName,
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          }
 | 
			
		||||
        : {
 | 
			
		||||
@ -996,7 +1004,9 @@ export async function setupElectron({
 | 
			
		||||
              ...TEST_SETTINGS,
 | 
			
		||||
              app: {
 | 
			
		||||
                ...TEST_SETTINGS.app,
 | 
			
		||||
                projectDirectory: projectDirName,
 | 
			
		||||
              },
 | 
			
		||||
              project: {
 | 
			
		||||
                directory: projectDirName,
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          }
 | 
			
		||||
@ -1190,3 +1200,7 @@ export async function pollEditorLinesSelectedLength(page: Page, lines: number) {
 | 
			
		||||
    })
 | 
			
		||||
    .toBe(lines)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function settingsToToml(settings: DeepPartial<Configuration>) {
 | 
			
		||||
  return TOML.stringify(settings)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -31,13 +31,13 @@ test.describe('Testing settings', () => {
 | 
			
		||||
        )
 | 
			
		||||
      ) as { settings: SaveSettingsPayload }
 | 
			
		||||
 | 
			
		||||
      expect(storedSettings.settings?.app?.theme).toBe('dark')
 | 
			
		||||
      expect(storedSettings.settings?.app?.appearance?.theme).toBe('dark')
 | 
			
		||||
 | 
			
		||||
      // Check that the invalid settings were changed to good defaults
 | 
			
		||||
      expect(storedSettings.settings?.modeling?.defaultUnit).toBe('in')
 | 
			
		||||
      expect(storedSettings.settings?.modeling?.mouseControls).toBe('Zoo')
 | 
			
		||||
      expect(storedSettings.settings?.app?.projectDirectory).toBe('')
 | 
			
		||||
      expect(storedSettings.settings?.projects?.defaultProjectName).toBe(
 | 
			
		||||
      expect(storedSettings.settings?.modeling?.base_unit).toBe('in')
 | 
			
		||||
      expect(storedSettings.settings?.modeling?.mouse_controls).toBe('Zoo')
 | 
			
		||||
      expect(storedSettings.settings?.project?.directory).toBe('')
 | 
			
		||||
      expect(storedSettings.settings?.project?.default_project_name).toBe(
 | 
			
		||||
        'project-$nnn'
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
@ -374,7 +374,9 @@ test.describe('Testing settings', () => {
 | 
			
		||||
      tag: '@electron',
 | 
			
		||||
      appSettings: {
 | 
			
		||||
        app: {
 | 
			
		||||
          themeColor: '259',
 | 
			
		||||
          appearance: {
 | 
			
		||||
            color: 259,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
@ -400,9 +402,11 @@ test.describe('Testing settings', () => {
 | 
			
		||||
      tag: '@electron',
 | 
			
		||||
      appSettings: {
 | 
			
		||||
        app: {
 | 
			
		||||
          // Doesn't matter what you set it to. It will
 | 
			
		||||
          // default to 264.5
 | 
			
		||||
          themeColor: '0',
 | 
			
		||||
          appearance: {
 | 
			
		||||
            // Doesn't matter what you set it to. It will
 | 
			
		||||
            // default to 264.5
 | 
			
		||||
            color: 0,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
@ -832,7 +836,7 @@ test.describe('Testing settings', () => {
 | 
			
		||||
      // but "show debug panel" set to false
 | 
			
		||||
      appSettings: {
 | 
			
		||||
        ...TEST_SETTINGS,
 | 
			
		||||
        modeling: { ...TEST_SETTINGS.modeling, showDebugPanel: false },
 | 
			
		||||
        modeling: { ...TEST_SETTINGS.modeling, show_debug_panel: false },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    async ({ context, page, homePage }) => {
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,8 @@ import {
 | 
			
		||||
  AuthenticatedApp,
 | 
			
		||||
} from './fixtures/fixtureSetup'
 | 
			
		||||
 | 
			
		||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
 | 
			
		||||
import { Settings } from 'wasm-lib/kcl/bindings/Settings'
 | 
			
		||||
import { DeepPartial } from 'lib/types'
 | 
			
		||||
export { expect } from '@playwright/test'
 | 
			
		||||
 | 
			
		||||
declare module '@playwright/test' {
 | 
			
		||||
@ -45,7 +46,7 @@ export type BrowserContext = BrowserContextPlaywright
 | 
			
		||||
export type Page = PagePlaywright
 | 
			
		||||
export type TestDetails = TestDetailsPlaywright & {
 | 
			
		||||
  cleanProjectDir?: boolean
 | 
			
		||||
  appSettings?: Partial<SaveSettingsPayload>
 | 
			
		||||
  appSettings?: DeepPartial<Settings>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Our custom decorated Zoo test object. Makes it easier to add fixtures, and
 | 
			
		||||
 | 
			
		||||
@ -54,7 +54,7 @@ export function App() {
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    app: { onboardingStatus },
 | 
			
		||||
    app: { onboarding_status },
 | 
			
		||||
  } = settings.context
 | 
			
		||||
 | 
			
		||||
  useHotkeys('backspace', (e) => {
 | 
			
		||||
@ -69,7 +69,7 @@ export function App() {
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
 | 
			
		||||
    (p) => p === onboardingStatus.current
 | 
			
		||||
    (p) => p === onboarding_status.current
 | 
			
		||||
  )
 | 
			
		||||
    ? 'opacity-20'
 | 
			
		||||
    : ''
 | 
			
		||||
 | 
			
		||||
@ -77,7 +77,7 @@ export const ClientSideScene = ({
 | 
			
		||||
}: {
 | 
			
		||||
  cameraControls: ReturnType<
 | 
			
		||||
    typeof useSettingsAuthContext
 | 
			
		||||
  >['settings']['context']['modeling']['mouseControls']['current']
 | 
			
		||||
  >['settings']['context']['modeling']['mouse_controls']['current']
 | 
			
		||||
}) => {
 | 
			
		||||
  const canvasRef = useRef<HTMLDivElement>(null)
 | 
			
		||||
  const { state, send, context } = useModelingContext()
 | 
			
		||||
 | 
			
		||||
@ -5,28 +5,28 @@ import { useEffect, useState } from 'react'
 | 
			
		||||
export function CameraProjectionToggle() {
 | 
			
		||||
  const { settings } = useSettingsAuthContext()
 | 
			
		||||
  const isCameraProjectionPerspective =
 | 
			
		||||
    settings.context.modeling.cameraProjection.current === 'perspective'
 | 
			
		||||
    settings.context.modeling.camera_projection.current === 'perspective'
 | 
			
		||||
  const [checked, setChecked] = useState(isCameraProjectionPerspective)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setChecked(
 | 
			
		||||
      settings.context.modeling.cameraProjection.current === 'perspective'
 | 
			
		||||
      settings.context.modeling.camera_projection.current === 'perspective'
 | 
			
		||||
    )
 | 
			
		||||
  }, [settings.context.modeling.cameraProjection.current])
 | 
			
		||||
  }, [settings.context.modeling.camera_projection.current])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Switch
 | 
			
		||||
      checked={checked}
 | 
			
		||||
      onChange={(newValue) => {
 | 
			
		||||
        settings.send({
 | 
			
		||||
          type: 'set.modeling.cameraProjection',
 | 
			
		||||
          type: 'set.modeling.camera_projection',
 | 
			
		||||
          data: {
 | 
			
		||||
            level: 'user',
 | 
			
		||||
            value: newValue ? 'perspective' : 'orthographic',
 | 
			
		||||
          },
 | 
			
		||||
        })
 | 
			
		||||
      }}
 | 
			
		||||
      className={`pointer-events-auto p-0 text-xs text-chalkboard-60 dark:text-chalkboard-40 bg-chalkboard-10/70 hover:bg-chalkboard-10 dark:bg-chalkboard-100/80 dark:hover:bg-chalkboard-100 backdrop-blur-sm 
 | 
			
		||||
      className={`pointer-events-auto p-0 text-xs text-chalkboard-60 dark:text-chalkboard-40 bg-chalkboard-10/70 hover:bg-chalkboard-10 dark:bg-chalkboard-100/80 dark:hover:bg-chalkboard-100 backdrop-blur-sm
 | 
			
		||||
        border border-primary/10 hover:border-primary/50 focus-visible:border-primary/50 rounded-full`}
 | 
			
		||||
    >
 | 
			
		||||
      <span className="sr-only">Camera projection: </span>
 | 
			
		||||
 | 
			
		||||
@ -115,9 +115,9 @@ function CommandBarKclInput({
 | 
			
		||||
          : defaultValue.length,
 | 
			
		||||
    },
 | 
			
		||||
    theme:
 | 
			
		||||
      settings.context.app.theme.current === 'system'
 | 
			
		||||
      settings.context.app.appearance.theme.current === 'system'
 | 
			
		||||
        ? getSystemTheme()
 | 
			
		||||
        : settings.context.app.theme.current,
 | 
			
		||||
        : settings.context.app.appearance.theme.current,
 | 
			
		||||
    extensions: [
 | 
			
		||||
      varMentionsExtension,
 | 
			
		||||
      EditorView.updateListener.of((vu: ViewUpdate) => {
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import { useState } from 'react'
 | 
			
		||||
const DownloadAppBanner = () => {
 | 
			
		||||
  const { settings } = useSettingsAuthContext()
 | 
			
		||||
  const [isBannerDismissed, setIsBannerDismissed] = useState(
 | 
			
		||||
    settings.context.app.dismissWebBanner.current
 | 
			
		||||
    settings.context.app.dismiss_web_banner.current
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
 | 
			
		||||
@ -315,7 +315,7 @@ export const FileMachineProvider = ({
 | 
			
		||||
          // with the sample's setting.
 | 
			
		||||
          if (data.sampleUnits) {
 | 
			
		||||
            settings.send({
 | 
			
		||||
              type: 'set.modeling.defaultUnit',
 | 
			
		||||
              type: 'set.modeling.base_unit',
 | 
			
		||||
              data: {
 | 
			
		||||
                level: 'project',
 | 
			
		||||
                value: data.sampleUnits,
 | 
			
		||||
 | 
			
		||||
@ -107,7 +107,7 @@ export function HelpMenu(props: React.PropsWithChildren) {
 | 
			
		||||
          as="button"
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            settings.send({
 | 
			
		||||
              type: 'set.app.onboardingStatus',
 | 
			
		||||
              type: 'set.app.onboarding_status',
 | 
			
		||||
              data: {
 | 
			
		||||
                value: '',
 | 
			
		||||
                level: 'user',
 | 
			
		||||
 | 
			
		||||
@ -111,12 +111,15 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
    auth,
 | 
			
		||||
    settings: {
 | 
			
		||||
      context: {
 | 
			
		||||
        app: { theme, enableSSAO },
 | 
			
		||||
        app: {
 | 
			
		||||
          appearance: { theme },
 | 
			
		||||
        },
 | 
			
		||||
        modeling: {
 | 
			
		||||
          defaultUnit,
 | 
			
		||||
          cameraProjection,
 | 
			
		||||
          highlightEdges,
 | 
			
		||||
          showScaleGrid,
 | 
			
		||||
          base_unit,
 | 
			
		||||
          camera_projection,
 | 
			
		||||
          highlight_edges,
 | 
			
		||||
          show_scale_grid,
 | 
			
		||||
          enable_ssao,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
@ -172,7 +175,7 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
 | 
			
		||||
          sceneInfra.camControls.syncDirection = 'clientToEngine'
 | 
			
		||||
 | 
			
		||||
          if (cameraProjection.current === 'perspective') {
 | 
			
		||||
          if (camera_projection.current === 'perspective') {
 | 
			
		||||
            await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
@ -508,7 +511,7 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            format.type === 'stl' ||
 | 
			
		||||
            format.type === 'ply'
 | 
			
		||||
          ) {
 | 
			
		||||
            format.units = defaultUnit.current
 | 
			
		||||
            format.units = base_unit.current
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (format.type === 'ply' || format.type === 'stl') {
 | 
			
		||||
@ -533,7 +536,7 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            token,
 | 
			
		||||
            settings: {
 | 
			
		||||
              theme: theme.current,
 | 
			
		||||
              highlightEdges: highlightEdges.current,
 | 
			
		||||
              highlightEdges: highlight_edges.current,
 | 
			
		||||
            },
 | 
			
		||||
          }).catch(reportRejection)
 | 
			
		||||
        },
 | 
			
		||||
@ -1134,10 +1137,10 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
    {
 | 
			
		||||
      pool: pool,
 | 
			
		||||
      theme: theme.current,
 | 
			
		||||
      highlightEdges: highlightEdges.current,
 | 
			
		||||
      enableSSAO: enableSSAO.current,
 | 
			
		||||
      showScaleGrid: showScaleGrid.current,
 | 
			
		||||
      cameraProjection: cameraProjection.current,
 | 
			
		||||
      highlightEdges: highlight_edges.current,
 | 
			
		||||
      enableSSAO: enable_ssao.current,
 | 
			
		||||
      showScaleGrid: show_scale_grid.current,
 | 
			
		||||
      cameraProjection: camera_projection.current,
 | 
			
		||||
    },
 | 
			
		||||
    token
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
@ -69,7 +69,7 @@ export const ModelingPane = ({
 | 
			
		||||
  ...props
 | 
			
		||||
}: ModelingPaneProps) => {
 | 
			
		||||
  const { settings } = useSettingsAuthContext()
 | 
			
		||||
  const onboardingStatus = settings.context.app.onboardingStatus
 | 
			
		||||
  const onboardingStatus = settings.context.app.onboarding_status
 | 
			
		||||
  const pointerEventsCssClass =
 | 
			
		||||
    onboardingStatus.current === onboardingPaths.CAMERA
 | 
			
		||||
      ? 'pointer-events-none '
 | 
			
		||||
 | 
			
		||||
@ -69,9 +69,9 @@ export const KclEditorPane = () => {
 | 
			
		||||
  const lastSelectionEvent = useSelector(kclEditorActor, selectionEventSelector)
 | 
			
		||||
  const editorIsMounted = useSelector(kclEditorActor, editorIsMountedSelector)
 | 
			
		||||
  const theme =
 | 
			
		||||
    context.app.theme.current === Themes.System
 | 
			
		||||
    context.app.appearance.theme.current === Themes.System
 | 
			
		||||
      ? getSystemTheme()
 | 
			
		||||
      : context.app.theme.current
 | 
			
		||||
      : context.app.appearance.theme.current
 | 
			
		||||
  const { copilotLSP, kclLSP } = useLspContext()
 | 
			
		||||
 | 
			
		||||
  // Since these already exist in the editor, we don't need to define them
 | 
			
		||||
@ -104,8 +104,8 @@ export const KclEditorPane = () => {
 | 
			
		||||
    })
 | 
			
		||||
  }, [editorIsMounted, lastSelectionEvent])
 | 
			
		||||
 | 
			
		||||
  const textWrapping = context.textEditor.textWrapping
 | 
			
		||||
  const cursorBlinking = context.textEditor.blinkingCursor
 | 
			
		||||
  const textWrapping = context.text_editor.text_wrapping
 | 
			
		||||
  const cursorBlinking = context.text_editor.blinking_cursor
 | 
			
		||||
  // DO NOT ADD THE CODEMIRROR HOTKEYS HERE TO THE DEPENDENCY ARRAY
 | 
			
		||||
  // It reloads the editor every time we do _anything_ in the editor
 | 
			
		||||
  // I have no idea why.
 | 
			
		||||
 | 
			
		||||
@ -210,6 +210,6 @@ export const sidebarPanes: SidebarPane[] = [
 | 
			
		||||
      )
 | 
			
		||||
    },
 | 
			
		||||
    keybinding: 'Shift + D',
 | 
			
		||||
    hide: ({ settings }) => !settings.modeling.showDebugPanel.current,
 | 
			
		||||
    hide: ({ settings }) => !settings.modeling.show_debug_panel.current,
 | 
			
		||||
  },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@ -40,14 +40,14 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
 | 
			
		||||
  const { commandBarSend } = useCommandsContext()
 | 
			
		||||
  const kclContext = useKclContext()
 | 
			
		||||
  const { settings } = useSettingsAuthContext()
 | 
			
		||||
  const onboardingStatus = settings.context.app.onboardingStatus
 | 
			
		||||
  const onboardingStatus = settings.context.app.onboarding_status
 | 
			
		||||
  const { send, context } = useModelingContext()
 | 
			
		||||
  const pointerEventsCssClass =
 | 
			
		||||
    onboardingStatus.current === onboardingPaths.CAMERA ||
 | 
			
		||||
    context.store?.openPanes.length === 0
 | 
			
		||||
      ? 'pointer-events-none '
 | 
			
		||||
      : 'pointer-events-auto '
 | 
			
		||||
  const showDebugPanel = settings.context.modeling.showDebugPanel
 | 
			
		||||
  const showDebugPanel = settings.context.modeling.show_debug_panel
 | 
			
		||||
 | 
			
		||||
  const paneCallbackProps = useMemo(
 | 
			
		||||
    () => ({
 | 
			
		||||
 | 
			
		||||
@ -79,11 +79,8 @@ const ProjectsContextDesktop = ({
 | 
			
		||||
  } = useSettingsAuthContext()
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    console.log(
 | 
			
		||||
      'project directory changed',
 | 
			
		||||
      settings.app.projectDirectory.current
 | 
			
		||||
    )
 | 
			
		||||
  }, [settings.app.projectDirectory.current])
 | 
			
		||||
    console.log('project directory changed', settings.project.directory.current)
 | 
			
		||||
  }, [settings.project.directory.current])
 | 
			
		||||
 | 
			
		||||
  const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
 | 
			
		||||
  const { projectPaths, projectsDir } = useProjectsLoader([
 | 
			
		||||
@ -192,7 +189,7 @@ const ProjectsContextDesktop = ({
 | 
			
		||||
          let name = (
 | 
			
		||||
            input && 'name' in input && input.name
 | 
			
		||||
              ? input.name
 | 
			
		||||
              : settings.projects.defaultProjectName.current
 | 
			
		||||
              : settings.project.default_project_name.current
 | 
			
		||||
          ).trim()
 | 
			
		||||
 | 
			
		||||
          if (doesProjectNameNeedInterpolated(name)) {
 | 
			
		||||
@ -257,8 +254,8 @@ const ProjectsContextDesktop = ({
 | 
			
		||||
    {
 | 
			
		||||
      input: {
 | 
			
		||||
        projects: projectPaths,
 | 
			
		||||
        defaultProjectName: settings.projects.defaultProjectName.current,
 | 
			
		||||
        defaultDirectory: settings.app.projectDirectory.current,
 | 
			
		||||
        defaultProjectName: settings.project.default_project_name.current,
 | 
			
		||||
        defaultDirectory: settings.project.directory.current,
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
@ -61,7 +61,7 @@ export const AllSettingsFields = forwardRef(
 | 
			
		||||
 | 
			
		||||
    function restartOnboarding() {
 | 
			
		||||
      send({
 | 
			
		||||
        type: `set.app.onboardingStatus`,
 | 
			
		||||
        type: `set.app.onboarding_status`,
 | 
			
		||||
        data: { level: 'user', value: '' },
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
@ -73,7 +73,7 @@ export const AllSettingsFields = forwardRef(
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
      async function navigateToOnboardingStart() {
 | 
			
		||||
        if (
 | 
			
		||||
          state.context.app.onboardingStatus.user === '' &&
 | 
			
		||||
          state.context.app.onboarding_status.user === '' &&
 | 
			
		||||
          state.matches('idle')
 | 
			
		||||
        ) {
 | 
			
		||||
          if (isFileSettings) {
 | 
			
		||||
 | 
			
		||||
@ -122,18 +122,20 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
 | 
			
		||||
        setClientSideSceneUnits: ({ context, event }) => {
 | 
			
		||||
          const newBaseUnit =
 | 
			
		||||
            event.type === 'set.modeling.defaultUnit'
 | 
			
		||||
            event.type === 'set.modeling.base_unit'
 | 
			
		||||
              ? (event.data.value as BaseUnit)
 | 
			
		||||
              : context.modeling.defaultUnit.current
 | 
			
		||||
              : context.modeling.base_unit.current
 | 
			
		||||
          sceneInfra.baseUnit = newBaseUnit
 | 
			
		||||
        },
 | 
			
		||||
        setEngineTheme: ({ context }) => {
 | 
			
		||||
          engineCommandManager
 | 
			
		||||
            .setTheme(context.app.theme.current)
 | 
			
		||||
            .setTheme(context.app.appearance.theme.current)
 | 
			
		||||
            .catch(reportRejection)
 | 
			
		||||
        },
 | 
			
		||||
        setClientTheme: ({ context }) => {
 | 
			
		||||
          const opposingTheme = getOppositeTheme(context.app.theme.current)
 | 
			
		||||
          const opposingTheme = getOppositeTheme(
 | 
			
		||||
            context.app.appearance.theme.current
 | 
			
		||||
          )
 | 
			
		||||
          sceneInfra.theme = opposingTheme
 | 
			
		||||
          sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
 | 
			
		||||
        },
 | 
			
		||||
@ -164,12 +166,12 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
          try {
 | 
			
		||||
            const relevantSetting = (s: typeof settings) => {
 | 
			
		||||
              return (
 | 
			
		||||
                s.modeling?.defaultUnit?.current !==
 | 
			
		||||
                  context.modeling.defaultUnit.current ||
 | 
			
		||||
                s.modeling.showScaleGrid.current !==
 | 
			
		||||
                  context.modeling.showScaleGrid.current ||
 | 
			
		||||
                s.modeling?.highlightEdges.current !==
 | 
			
		||||
                  context.modeling.highlightEdges.current
 | 
			
		||||
                s.modeling?.base_unit.current !==
 | 
			
		||||
                  context.modeling.base_unit.current ||
 | 
			
		||||
                s.modeling.show_scale_grid.current !==
 | 
			
		||||
                  context.modeling.show_scale_grid.current ||
 | 
			
		||||
                s.modeling?.highlight_edges.current !==
 | 
			
		||||
                  context.modeling.highlight_edges.current
 | 
			
		||||
              )
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -180,9 +182,9 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
              event.type === 'Reset settings' && relevantSetting(settings)
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
              event.type === 'set.modeling.defaultUnit' ||
 | 
			
		||||
              event.type === 'set.modeling.showScaleGrid' ||
 | 
			
		||||
              event.type === 'set.modeling.highlightEdges' ||
 | 
			
		||||
              event.type === 'set.modeling.base_unit' ||
 | 
			
		||||
              event.type === 'set.modeling.show_scale_grid' ||
 | 
			
		||||
              event.type === 'set.modeling.highlight_edges' ||
 | 
			
		||||
              allSettingsIncludesUnitChange ||
 | 
			
		||||
              resetSettingsIncludesUnitChange
 | 
			
		||||
            ) {
 | 
			
		||||
@ -258,7 +260,7 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // If the user wants to hide the settings commands
 | 
			
		||||
    //from the command bar don't add them.
 | 
			
		||||
    if (settingsState.context.commandBar.includeSettings.current === false)
 | 
			
		||||
    if (settingsState.context.command_bar.include_settings.current === false)
 | 
			
		||||
      return
 | 
			
		||||
 | 
			
		||||
    const commands = settingsWithCommandConfigs(settingsState.context)
 | 
			
		||||
@ -334,7 +336,8 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
  // events outside of the machine that also depend on the machine's context
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const listener = (e: MediaQueryListEvent) => {
 | 
			
		||||
      if (settingsState.context.app.theme.current !== 'system') return
 | 
			
		||||
      if (settingsState.context.app.appearance.theme.current !== 'system')
 | 
			
		||||
        return
 | 
			
		||||
      setThemeClass(e.matches ? Themes.Dark : Themes.Light)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -349,9 +352,9 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    document.documentElement.style.setProperty(
 | 
			
		||||
      `--primary-hue`,
 | 
			
		||||
      settingsState.context.app.themeColor.current
 | 
			
		||||
      settingsState.context.app.appearance.color.current
 | 
			
		||||
    )
 | 
			
		||||
  }, [settingsState.context.app.themeColor.current])
 | 
			
		||||
  }, [settingsState.context.app.appearance.color.current])
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Update the --cursor-color CSS variable
 | 
			
		||||
@ -360,11 +363,11 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    document.documentElement.style.setProperty(
 | 
			
		||||
      `--cursor-color`,
 | 
			
		||||
      settingsState.context.textEditor.blinkingCursor.current
 | 
			
		||||
      settingsState.context.text_editor.blinking_cursor.current
 | 
			
		||||
        ? 'auto'
 | 
			
		||||
        : 'transparent'
 | 
			
		||||
    )
 | 
			
		||||
  }, [settingsState.context.textEditor.blinkingCursor.current])
 | 
			
		||||
  }, [settingsState.context.text_editor.blinking_cursor.current])
 | 
			
		||||
 | 
			
		||||
  // Auth machine setup
 | 
			
		||||
  const [authState, authSend, authActor] = useMachine(
 | 
			
		||||
 | 
			
		||||
@ -41,7 +41,7 @@ export const Stream = () => {
 | 
			
		||||
  const [streamState, setStreamState] = useState(StreamState.Unset)
 | 
			
		||||
  const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
 | 
			
		||||
 | 
			
		||||
  const IDLE = settings.context.app.streamIdleMode.current
 | 
			
		||||
  const IDLE = settings.context.app.stream_idle_mode.current
 | 
			
		||||
 | 
			
		||||
  const isNetworkOkay =
 | 
			
		||||
    overallState === NetworkHealthState.Ok ||
 | 
			
		||||
@ -342,7 +342,7 @@ export const Stream = () => {
 | 
			
		||||
        id="video-stream"
 | 
			
		||||
      />
 | 
			
		||||
      <ClientSideScene
 | 
			
		||||
        cameraControls={settings.context.modeling.mouseControls.current}
 | 
			
		||||
        cameraControls={settings.context.modeling.mouse_controls.current}
 | 
			
		||||
      />
 | 
			
		||||
      {(streamState === StreamState.Paused ||
 | 
			
		||||
        streamState === StreamState.Resuming) && (
 | 
			
		||||
 | 
			
		||||
@ -9,8 +9,8 @@ export function UnitsMenu() {
 | 
			
		||||
      {({ close }) => (
 | 
			
		||||
        <>
 | 
			
		||||
          <Popover.Button
 | 
			
		||||
            className={`flex items-center gap-2 px-3 py-1 
 | 
			
		||||
        text-xs text-primary bg-chalkboard-10/70 dark:bg-chalkboard-100/80 backdrop-blur-sm 
 | 
			
		||||
            className={`flex items-center gap-2 px-3 py-1
 | 
			
		||||
        text-xs text-primary bg-chalkboard-10/70 dark:bg-chalkboard-100/80 backdrop-blur-sm
 | 
			
		||||
        border !border-primary/50 rounded-full`}
 | 
			
		||||
          >
 | 
			
		||||
            <div className="w-4 h-[1px] bg-primary relative">
 | 
			
		||||
@ -18,7 +18,7 @@ export function UnitsMenu() {
 | 
			
		||||
              <div className="absolute w-[1px] h-[1em] bg-primary right-0 top-1/2 -translate-y-1/2"></div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <span className="sr-only">Current units are: </span>
 | 
			
		||||
            {settings.context.modeling.defaultUnit.current}
 | 
			
		||||
            {settings.context.modeling.base_unit.current}
 | 
			
		||||
          </Popover.Button>
 | 
			
		||||
          <Popover.Panel
 | 
			
		||||
            className={`absolute bottom-full right-0 mb-2 w-48 bg-chalkboard-10 dark:bg-chalkboard-90
 | 
			
		||||
@ -32,7 +32,7 @@ export function UnitsMenu() {
 | 
			
		||||
                    className="flex items-center gap-2 m-0 py-1.5 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left"
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      settings.send({
 | 
			
		||||
                        type: 'set.modeling.defaultUnit',
 | 
			
		||||
                        type: 'set.modeling.base_unit',
 | 
			
		||||
                        data: {
 | 
			
		||||
                          level: 'project',
 | 
			
		||||
                          value: unit,
 | 
			
		||||
@ -42,7 +42,7 @@ export function UnitsMenu() {
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <span className="flex-1">{baseUnitLabels[unit]}</span>
 | 
			
		||||
                    {unit === settings.context.modeling.defaultUnit.current && (
 | 
			
		||||
                    {unit === settings.context.modeling.base_unit.current && (
 | 
			
		||||
                      <span className="text-chalkboard-60">current</span>
 | 
			
		||||
                    )}
 | 
			
		||||
                  </button>
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ export function useResolvedTheme() {
 | 
			
		||||
  const {
 | 
			
		||||
    settings: { context },
 | 
			
		||||
  } = useSettingsAuthContext()
 | 
			
		||||
  return context.app.theme.current === Themes.System
 | 
			
		||||
  return context.app.appearance.theme.current === Themes.System
 | 
			
		||||
    ? getSystemTheme()
 | 
			
		||||
    : context.app.theme.current
 | 
			
		||||
    : context.app.appearance.theme.current
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -63,9 +63,7 @@ export async function renameProjectDirectory(
 | 
			
		||||
export async function ensureProjectDirectoryExists(
 | 
			
		||||
  config: DeepPartial<Configuration>
 | 
			
		||||
): Promise<string | undefined> {
 | 
			
		||||
  const projectDir =
 | 
			
		||||
    config.settings?.app?.project_directory ||
 | 
			
		||||
    config.settings?.project?.directory
 | 
			
		||||
  const projectDir = config.settings?.project?.directory
 | 
			
		||||
  if (!projectDir) {
 | 
			
		||||
    console.error('projectDir is falsey', config)
 | 
			
		||||
    return Promise.reject(new Error('projectDir is falsey'))
 | 
			
		||||
@ -520,8 +518,7 @@ export const readAppSettingsFile = async () => {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const hasProjectDirectorySetting =
 | 
			
		||||
      parsedAppConfig.settings?.project?.directory ||
 | 
			
		||||
      parsedAppConfig.settings?.app?.project_directory
 | 
			
		||||
      parsedAppConfig.settings?.project?.directory
 | 
			
		||||
 | 
			
		||||
    if (hasProjectDirectorySetting) {
 | 
			
		||||
      return parsedAppConfig
 | 
			
		||||
 | 
			
		||||
@ -86,8 +86,7 @@ export function kclCommands(
 | 
			
		||||
                sampleName: data.sample,
 | 
			
		||||
                code,
 | 
			
		||||
                method: data.method,
 | 
			
		||||
                sampleUnits:
 | 
			
		||||
                  projectSettingsPayload.modeling?.defaultUnit || 'mm',
 | 
			
		||||
                sampleUnits: projectSettingsPayload.modeling?.base_unit || 'mm',
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          )
 | 
			
		||||
 | 
			
		||||
@ -55,7 +55,7 @@ export const telemetryLoader: LoaderFunction = async ({
 | 
			
		||||
export const onboardingRedirectLoader: ActionFunction = async (args) => {
 | 
			
		||||
  const { settings } = await loadAndValidateSettings()
 | 
			
		||||
  const onboardingStatus: OnboardingStatus =
 | 
			
		||||
    settings.app.onboardingStatus.current || ''
 | 
			
		||||
    settings.app.onboarding_status.current || ''
 | 
			
		||||
  const notEnRouteToOnboarding = !args.request.url.includes(
 | 
			
		||||
    PATHS.ONBOARDING.INDEX
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
@ -128,61 +128,56 @@ export function createSettings() {
 | 
			
		||||
      /**
 | 
			
		||||
       * The overall appearance of the app: light, dark, or system
 | 
			
		||||
       */
 | 
			
		||||
      theme: new Setting<Themes>({
 | 
			
		||||
        hideOnLevel: 'project',
 | 
			
		||||
        defaultValue: Themes.System,
 | 
			
		||||
        description: 'The overall appearance of the app',
 | 
			
		||||
        validate: (v) => isEnumMember(v, Themes),
 | 
			
		||||
        commandConfig: {
 | 
			
		||||
          inputType: 'options',
 | 
			
		||||
          defaultValueFromContext: (context) => context.app.theme.current,
 | 
			
		||||
          options: (cmdContext, settingsContext) =>
 | 
			
		||||
            Object.values(Themes).map((v) => ({
 | 
			
		||||
              name: v,
 | 
			
		||||
              value: v,
 | 
			
		||||
              isCurrent:
 | 
			
		||||
                v ===
 | 
			
		||||
                settingsContext.app.theme[
 | 
			
		||||
                  cmdContext.argumentsToSubmit.level as SettingsLevel
 | 
			
		||||
                ],
 | 
			
		||||
            })),
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
      themeColor: new Setting<string>({
 | 
			
		||||
        defaultValue: '264.5',
 | 
			
		||||
        description: 'The hue of the primary theme color for the app',
 | 
			
		||||
        validate: (v) => Number(v) >= 0 && Number(v) < 360,
 | 
			
		||||
        Component: ({ value, updateValue }) => (
 | 
			
		||||
          <div className="flex item-center gap-4 px-2 m-0 py-0">
 | 
			
		||||
            <div
 | 
			
		||||
              className="w-4 h-4 rounded-full bg-primary border border-solid border-chalkboard-100 dark:border-chalkboard-30"
 | 
			
		||||
              style={{
 | 
			
		||||
                backgroundColor: `oklch(var(--primary-lightness) var(--primary-chroma) ${value})`,
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
            <input
 | 
			
		||||
              type="range"
 | 
			
		||||
              onChange={(e) => updateValue(e.currentTarget.value)}
 | 
			
		||||
              value={value}
 | 
			
		||||
              min={0}
 | 
			
		||||
              max={259}
 | 
			
		||||
              step={1}
 | 
			
		||||
              className="block flex-1"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
        ),
 | 
			
		||||
      }),
 | 
			
		||||
      enableSSAO: new Setting<boolean>({
 | 
			
		||||
        defaultValue: true,
 | 
			
		||||
        description:
 | 
			
		||||
          'Whether or not Screen Space Ambient Occlusion (SSAO) is enabled',
 | 
			
		||||
        validate: (v) => typeof v === 'boolean',
 | 
			
		||||
        hideOnPlatform: 'both', //for now
 | 
			
		||||
      }),
 | 
			
		||||
      appearance: {
 | 
			
		||||
        theme: new Setting<Themes>({
 | 
			
		||||
          hideOnLevel: 'project',
 | 
			
		||||
          defaultValue: Themes.System,
 | 
			
		||||
          description: 'The overall appearance of the app',
 | 
			
		||||
          validate: (v) => isEnumMember(v, Themes),
 | 
			
		||||
          commandConfig: {
 | 
			
		||||
            inputType: 'options',
 | 
			
		||||
            defaultValueFromContext: (context) => context.app.theme.current,
 | 
			
		||||
            options: (cmdContext, settingsContext) =>
 | 
			
		||||
              Object.values(Themes).map((v) => ({
 | 
			
		||||
                name: v,
 | 
			
		||||
                value: v,
 | 
			
		||||
                isCurrent:
 | 
			
		||||
                  v ===
 | 
			
		||||
                  settingsContext.app.theme[
 | 
			
		||||
                    cmdContext.argumentsToSubmit.level as SettingsLevel
 | 
			
		||||
                  ],
 | 
			
		||||
              })),
 | 
			
		||||
          },
 | 
			
		||||
        }),
 | 
			
		||||
        color: new Setting<string>({
 | 
			
		||||
          defaultValue: '264.5',
 | 
			
		||||
          description: 'The hue of the primary theme color for the app',
 | 
			
		||||
          validate: (v) => Number(v) >= 0 && Number(v) < 360,
 | 
			
		||||
          Component: ({ value, updateValue }) => (
 | 
			
		||||
            <div className="flex item-center gap-4 px-2 m-0 py-0">
 | 
			
		||||
              <div
 | 
			
		||||
                className="w-4 h-4 rounded-full bg-primary border border-solid border-chalkboard-100 dark:border-chalkboard-30"
 | 
			
		||||
                style={{
 | 
			
		||||
                  backgroundColor: `oklch(var(--primary-lightness) var(--primary-chroma) ${value})`,
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
              <input
 | 
			
		||||
                type="range"
 | 
			
		||||
                onChange={(e) => updateValue(e.currentTarget.value)}
 | 
			
		||||
                value={value}
 | 
			
		||||
                min={0}
 | 
			
		||||
                max={259}
 | 
			
		||||
                step={1}
 | 
			
		||||
                className="block flex-1"
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          ),
 | 
			
		||||
        }),
 | 
			
		||||
      },
 | 
			
		||||
      /**
 | 
			
		||||
       * Stream resource saving behavior toggle
 | 
			
		||||
       */
 | 
			
		||||
      streamIdleMode: new Setting<boolean>({
 | 
			
		||||
      stream_idle_mode: new Setting<boolean>({
 | 
			
		||||
        defaultValue: false,
 | 
			
		||||
        description: 'Toggle stream idling, saving bandwidth and battery',
 | 
			
		||||
        validate: (v) => typeof v === 'boolean',
 | 
			
		||||
@ -190,7 +185,7 @@ export function createSettings() {
 | 
			
		||||
          inputType: 'boolean',
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
      onboardingStatus: new Setting<OnboardingStatus>({
 | 
			
		||||
      onboarding_status: new Setting<OnboardingStatus>({
 | 
			
		||||
        defaultValue: '',
 | 
			
		||||
        // TODO: this could be better but we don't have a TS side real enum
 | 
			
		||||
        // for this yet
 | 
			
		||||
@ -198,62 +193,13 @@ export function createSettings() {
 | 
			
		||||
        hideOnPlatform: 'both',
 | 
			
		||||
      }),
 | 
			
		||||
      /** Permanently dismiss the banner warning to download the desktop app. */
 | 
			
		||||
      dismissWebBanner: new Setting<boolean>({
 | 
			
		||||
      dismiss_web_banner: new Setting<boolean>({
 | 
			
		||||
        defaultValue: false,
 | 
			
		||||
        description:
 | 
			
		||||
          'Permanently dismiss the banner warning to download the desktop app.',
 | 
			
		||||
        validate: (v) => typeof v === 'boolean',
 | 
			
		||||
        hideOnPlatform: 'desktop',
 | 
			
		||||
      }),
 | 
			
		||||
      projectDirectory: new Setting<string>({
 | 
			
		||||
        defaultValue: '',
 | 
			
		||||
        description: 'The directory to save and load projects from',
 | 
			
		||||
        hideOnLevel: 'project',
 | 
			
		||||
        hideOnPlatform: 'web',
 | 
			
		||||
        validate: (v) =>
 | 
			
		||||
          typeof v === 'string' && (v.length > 0 || !isDesktop()),
 | 
			
		||||
        Component: ({ value, updateValue }) => {
 | 
			
		||||
          const inputRef = useRef<HTMLInputElement>(null)
 | 
			
		||||
          return (
 | 
			
		||||
            <div className="flex gap-4 p-1 border rounded-sm border-chalkboard-30">
 | 
			
		||||
              <input
 | 
			
		||||
                className="flex-grow text-xs px-2 bg-transparent"
 | 
			
		||||
                value={value}
 | 
			
		||||
                disabled
 | 
			
		||||
                data-testid="project-directory-input"
 | 
			
		||||
                ref={inputRef}
 | 
			
		||||
              />
 | 
			
		||||
              <button
 | 
			
		||||
                onClick={toSync(async () => {
 | 
			
		||||
                  // In desktop end-to-end tests we can't control the file picker,
 | 
			
		||||
                  // so we seed the new directory value in the element's dataset
 | 
			
		||||
                  const inputRefVal = inputRef.current?.dataset.testValue
 | 
			
		||||
                  if (
 | 
			
		||||
                    inputRef.current &&
 | 
			
		||||
                    inputRefVal &&
 | 
			
		||||
                    !Array.isArray(inputRefVal)
 | 
			
		||||
                  ) {
 | 
			
		||||
                    updateValue(inputRefVal)
 | 
			
		||||
                  } else {
 | 
			
		||||
                    const newPath = await window.electron.open({
 | 
			
		||||
                      properties: ['openDirectory', 'createDirectory'],
 | 
			
		||||
                      defaultPath: value,
 | 
			
		||||
                      title: 'Choose a new project directory',
 | 
			
		||||
                    })
 | 
			
		||||
                    if (newPath.canceled) return
 | 
			
		||||
                    updateValue(newPath.filePaths[0])
 | 
			
		||||
                  }
 | 
			
		||||
                }, reportRejection)}
 | 
			
		||||
                className="p-0 m-0 border-none hover:bg-primary/10 focus:bg-primary/10 dark:hover:bg-primary/20 dark:focus::bg-primary/20"
 | 
			
		||||
                data-testid="project-directory-button"
 | 
			
		||||
              >
 | 
			
		||||
                <CustomIcon name="folder" className="w-5 h-5" />
 | 
			
		||||
                <Tooltip position="top-right">Choose a folder</Tooltip>
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
          )
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
    },
 | 
			
		||||
    /**
 | 
			
		||||
     * Settings that affect the behavior while modeling.
 | 
			
		||||
@ -262,7 +208,7 @@ export function createSettings() {
 | 
			
		||||
      /**
 | 
			
		||||
       * The default unit to use in modeling dimensions
 | 
			
		||||
       */
 | 
			
		||||
      defaultUnit: new Setting<BaseUnit>({
 | 
			
		||||
      base_unit: new Setting<BaseUnit>({
 | 
			
		||||
        defaultValue: 'mm',
 | 
			
		||||
        description: 'The default unit to use in modeling dimensions',
 | 
			
		||||
        validate: (v) => baseUnitsUnion.includes(v as BaseUnit),
 | 
			
		||||
@ -285,7 +231,7 @@ export function createSettings() {
 | 
			
		||||
      /**
 | 
			
		||||
       * The controls for how to navigate the 3D view
 | 
			
		||||
       */
 | 
			
		||||
      mouseControls: new Setting<CameraSystem>({
 | 
			
		||||
      mouse_controls: new Setting<CameraSystem>({
 | 
			
		||||
        defaultValue: 'Zoo',
 | 
			
		||||
        description: 'The controls for how to navigate the 3D view',
 | 
			
		||||
        validate: (v) => cameraSystems.includes(v as CameraSystem),
 | 
			
		||||
@ -345,7 +291,7 @@ export function createSettings() {
 | 
			
		||||
      /**
 | 
			
		||||
       * Projection method applied to the 3D view, perspective or orthographic
 | 
			
		||||
       */
 | 
			
		||||
      cameraProjection: new Setting<CameraProjectionType>({
 | 
			
		||||
      camera_projection: new Setting<CameraProjectionType>({
 | 
			
		||||
        defaultValue: 'orthographic',
 | 
			
		||||
        hideOnLevel: 'project',
 | 
			
		||||
        description:
 | 
			
		||||
@ -372,10 +318,17 @@ export function createSettings() {
 | 
			
		||||
            })),
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
      enable_ssao: new Setting<boolean>({
 | 
			
		||||
        defaultValue: true,
 | 
			
		||||
        description:
 | 
			
		||||
          'Whether or not Screen Space Ambient Occlusion (SSAO) is enabled',
 | 
			
		||||
        validate: (v) => typeof v === 'boolean',
 | 
			
		||||
        hideOnPlatform: 'both', //for now
 | 
			
		||||
      }),
 | 
			
		||||
      /**
 | 
			
		||||
       * Whether to highlight edges of 3D objects
 | 
			
		||||
       */
 | 
			
		||||
      highlightEdges: new Setting<boolean>({
 | 
			
		||||
      highlight_edges: new Setting<boolean>({
 | 
			
		||||
        defaultValue: true,
 | 
			
		||||
        description: 'Whether to highlight edges of 3D objects',
 | 
			
		||||
        validate: (v) => typeof v === 'boolean',
 | 
			
		||||
@ -387,7 +340,7 @@ export function createSettings() {
 | 
			
		||||
      /**
 | 
			
		||||
       * Whether to show a scale grid in the 3D modeling view
 | 
			
		||||
       */
 | 
			
		||||
      showScaleGrid: new Setting<boolean>({
 | 
			
		||||
      show_scale_grid: new Setting<boolean>({
 | 
			
		||||
        defaultValue: false,
 | 
			
		||||
        description: 'Whether to show a scale grid in the 3D modeling view',
 | 
			
		||||
        validate: (v) => typeof v === 'boolean',
 | 
			
		||||
@ -400,7 +353,7 @@ export function createSettings() {
 | 
			
		||||
       * Whether to show the debug panel, which lets you see
 | 
			
		||||
       * various states of the app to aid in development
 | 
			
		||||
       */
 | 
			
		||||
      showDebugPanel: new Setting<boolean>({
 | 
			
		||||
      show_debug_panel: new Setting<boolean>({
 | 
			
		||||
        defaultValue: false,
 | 
			
		||||
        description: 'Whether to show the debug panel, a development tool',
 | 
			
		||||
        validate: (v) => typeof v === 'boolean',
 | 
			
		||||
@ -438,11 +391,11 @@ export function createSettings() {
 | 
			
		||||
    /**
 | 
			
		||||
     * Settings that affect the behavior of the KCL text editor.
 | 
			
		||||
     */
 | 
			
		||||
    textEditor: {
 | 
			
		||||
    text_editor: {
 | 
			
		||||
      /**
 | 
			
		||||
       * Whether to wrap text in the editor or overflow with scroll
 | 
			
		||||
       */
 | 
			
		||||
      textWrapping: new Setting<boolean>({
 | 
			
		||||
      text_wrapping: new Setting<boolean>({
 | 
			
		||||
        defaultValue: true,
 | 
			
		||||
        description:
 | 
			
		||||
          'Whether to wrap text in the editor or overflow with scroll',
 | 
			
		||||
@ -454,7 +407,7 @@ export function createSettings() {
 | 
			
		||||
      /**
 | 
			
		||||
       * Whether to make the cursor blink in the editor
 | 
			
		||||
       */
 | 
			
		||||
      blinkingCursor: new Setting<boolean>({
 | 
			
		||||
      blinking_cursor: new Setting<boolean>({
 | 
			
		||||
        defaultValue: true,
 | 
			
		||||
        description: 'Whether to make the cursor blink in the editor',
 | 
			
		||||
        validate: (v) => typeof v === 'boolean',
 | 
			
		||||
@ -466,11 +419,60 @@ export function createSettings() {
 | 
			
		||||
    /**
 | 
			
		||||
     * Settings that affect the behavior of project management.
 | 
			
		||||
     */
 | 
			
		||||
    projects: {
 | 
			
		||||
    project: {
 | 
			
		||||
      directory: new Setting<string>({
 | 
			
		||||
        defaultValue: '',
 | 
			
		||||
        description: 'The directory to save and load projects from',
 | 
			
		||||
        hideOnLevel: 'project',
 | 
			
		||||
        hideOnPlatform: 'web',
 | 
			
		||||
        validate: (v) =>
 | 
			
		||||
          typeof v === 'string' && (v.length > 0 || !isDesktop()),
 | 
			
		||||
        Component: ({ value, updateValue }) => {
 | 
			
		||||
          const inputRef = useRef<HTMLInputElement>(null)
 | 
			
		||||
          return (
 | 
			
		||||
            <div className="flex gap-4 p-1 border rounded-sm border-chalkboard-30">
 | 
			
		||||
              <input
 | 
			
		||||
                className="flex-grow text-xs px-2 bg-transparent"
 | 
			
		||||
                value={value}
 | 
			
		||||
                disabled
 | 
			
		||||
                data-testid="project-directory-input"
 | 
			
		||||
                ref={inputRef}
 | 
			
		||||
              />
 | 
			
		||||
              <button
 | 
			
		||||
                onClick={toSync(async () => {
 | 
			
		||||
                  // In desktop end-to-end tests we can't control the file picker,
 | 
			
		||||
                  // so we seed the new directory value in the element's dataset
 | 
			
		||||
                  const inputRefVal = inputRef.current?.dataset.testValue
 | 
			
		||||
                  if (
 | 
			
		||||
                    inputRef.current &&
 | 
			
		||||
                    inputRefVal &&
 | 
			
		||||
                    !Array.isArray(inputRefVal)
 | 
			
		||||
                  ) {
 | 
			
		||||
                    updateValue(inputRefVal)
 | 
			
		||||
                  } else {
 | 
			
		||||
                    const newPath = await window.electron.open({
 | 
			
		||||
                      properties: ['openDirectory', 'createDirectory'],
 | 
			
		||||
                      defaultPath: value,
 | 
			
		||||
                      title: 'Choose a new project directory',
 | 
			
		||||
                    })
 | 
			
		||||
                    if (newPath.canceled) return
 | 
			
		||||
                    updateValue(newPath.filePaths[0])
 | 
			
		||||
                  }
 | 
			
		||||
                }, reportRejection)}
 | 
			
		||||
                className="p-0 m-0 border-none hover:bg-primary/10 focus:bg-primary/10 dark:hover:bg-primary/20 dark:focus::bg-primary/20"
 | 
			
		||||
                data-testid="project-directory-button"
 | 
			
		||||
              >
 | 
			
		||||
                <CustomIcon name="folder" className="w-5 h-5" />
 | 
			
		||||
                <Tooltip position="top-right">Choose a folder</Tooltip>
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
          )
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
      /**
 | 
			
		||||
       * The default project name to use when creating a new project
 | 
			
		||||
       */
 | 
			
		||||
      defaultProjectName: new Setting<string>({
 | 
			
		||||
      default_project_name: new Setting<string>({
 | 
			
		||||
        defaultValue: DEFAULT_PROJECT_NAME,
 | 
			
		||||
        description:
 | 
			
		||||
          'The default project name to use when creating a new project',
 | 
			
		||||
@ -504,11 +506,11 @@ export function createSettings() {
 | 
			
		||||
    /**
 | 
			
		||||
     * Settings that affect the behavior of the command bar.
 | 
			
		||||
     */
 | 
			
		||||
    commandBar: {
 | 
			
		||||
    command_bar: {
 | 
			
		||||
      /**
 | 
			
		||||
       * Whether to include settings in the command bar
 | 
			
		||||
       */
 | 
			
		||||
      includeSettings: new Setting<boolean>({
 | 
			
		||||
      include_settings: new Setting<boolean>({
 | 
			
		||||
        defaultValue: true,
 | 
			
		||||
        description: 'Whether to include settings in the command bar',
 | 
			
		||||
        validate: (v) => typeof v === 'boolean',
 | 
			
		||||
 | 
			
		||||
@ -26,8 +26,8 @@ describe(`testing settings initialization`, () => {
 | 
			
		||||
 | 
			
		||||
    setSettingsAtLevel(settings, 'user', appSettingsPayload)
 | 
			
		||||
 | 
			
		||||
    expect(settings.app.theme.current).toBe('dark')
 | 
			
		||||
    expect(settings.app.themeColor.current).toBe('190')
 | 
			
		||||
    expect(settings.app.appearance.theme.current).toBe('dark')
 | 
			
		||||
    expect(settings.app.appearance.color.current).toBe('190')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it(`doesn't read theme from project settings`, () => {
 | 
			
		||||
@ -61,9 +61,9 @@ describe(`testing settings initialization`, () => {
 | 
			
		||||
    setSettingsAtLevel(settings, 'project', projectSettingsPayload)
 | 
			
		||||
 | 
			
		||||
    // The 'project'-level for `theme` setting should be ignored completely
 | 
			
		||||
    expect(settings.app.theme.current).toBe('dark')
 | 
			
		||||
    expect(settings.app.appearance.theme.current).toBe('dark')
 | 
			
		||||
    // But the 'project'-level for `themeColor` setting should be applied
 | 
			
		||||
    expect(settings.app.themeColor.current).toBe('200')
 | 
			
		||||
    expect(settings.app.appearance.color.current).toBe('200')
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -106,8 +106,8 @@ describe(`testing getAllCurrentSettings`, () => {
 | 
			
		||||
    const allCurrentSettings = getAllCurrentSettings(settings)
 | 
			
		||||
    // This one gets the 'user'-level theme because it's ignored at the project level
 | 
			
		||||
    // (see the test "doesn't read theme from project settings")
 | 
			
		||||
    expect(allCurrentSettings.app.theme).toBe('dark')
 | 
			
		||||
    expect(allCurrentSettings.app.themeColor).toBe('200')
 | 
			
		||||
    expect(allCurrentSettings.modeling.defaultUnit).toBe('ft')
 | 
			
		||||
    expect(allCurrentSettings.app.appearance?.theme).toBe('dark')
 | 
			
		||||
    expect(allCurrentSettings.app.appearance?.color).toBe('200')
 | 
			
		||||
    expect(allCurrentSettings.modeling.base_unit).toBe('ft')
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -34,36 +34,38 @@ export function configurationToSettingsPayload(
 | 
			
		||||
): DeepPartial<SaveSettingsPayload> {
 | 
			
		||||
  return {
 | 
			
		||||
    app: {
 | 
			
		||||
      theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme),
 | 
			
		||||
      themeColor: configuration?.settings?.app?.appearance?.color
 | 
			
		||||
        ? configuration?.settings?.app?.appearance?.color.toString()
 | 
			
		||||
        : undefined,
 | 
			
		||||
      onboardingStatus: configuration?.settings?.app?.onboarding_status,
 | 
			
		||||
      dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
 | 
			
		||||
      streamIdleMode: configuration?.settings?.app?.stream_idle_mode,
 | 
			
		||||
      projectDirectory: configuration?.settings?.project?.directory,
 | 
			
		||||
      enableSSAO: configuration?.settings?.modeling?.enable_ssao,
 | 
			
		||||
      appearance: {
 | 
			
		||||
        theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme),
 | 
			
		||||
        color: configuration?.settings?.app?.appearance?.color
 | 
			
		||||
          ? configuration?.settings?.app?.appearance?.color.toString()
 | 
			
		||||
          : undefined,
 | 
			
		||||
      },
 | 
			
		||||
      onboarding_status: configuration?.settings?.app?.onboarding_status,
 | 
			
		||||
      dismiss_web_banner: configuration?.settings?.app?.dismiss_web_banner,
 | 
			
		||||
      stream_idle_mode: configuration?.settings?.app?.stream_idle_mode,
 | 
			
		||||
    },
 | 
			
		||||
    modeling: {
 | 
			
		||||
      defaultUnit: configuration?.settings?.modeling?.base_unit,
 | 
			
		||||
      cameraProjection: configuration?.settings?.modeling?.camera_projection,
 | 
			
		||||
      mouseControls: mouseControlsToCameraSystem(
 | 
			
		||||
      base_unit: configuration?.settings?.modeling?.base_unit,
 | 
			
		||||
      camera_projection: configuration?.settings?.modeling?.camera_projection,
 | 
			
		||||
      mouse_controls: mouseControlsToCameraSystem(
 | 
			
		||||
        configuration?.settings?.modeling?.mouse_controls
 | 
			
		||||
      ),
 | 
			
		||||
      highlightEdges: configuration?.settings?.modeling?.highlight_edges,
 | 
			
		||||
      showDebugPanel: configuration?.settings?.modeling?.show_debug_panel,
 | 
			
		||||
      showScaleGrid: configuration?.settings?.modeling?.show_scale_grid,
 | 
			
		||||
      highlight_edges: configuration?.settings?.modeling?.highlight_edges,
 | 
			
		||||
      show_debug_panel: configuration?.settings?.modeling?.show_debug_panel,
 | 
			
		||||
      show_scale_grid: configuration?.settings?.modeling?.show_scale_grid,
 | 
			
		||||
      enable_ssao: configuration?.settings?.modeling?.enable_ssao,
 | 
			
		||||
    },
 | 
			
		||||
    textEditor: {
 | 
			
		||||
      textWrapping: configuration?.settings?.text_editor?.text_wrapping,
 | 
			
		||||
      blinkingCursor: configuration?.settings?.text_editor?.blinking_cursor,
 | 
			
		||||
    text_editor: {
 | 
			
		||||
      text_wrapping: configuration?.settings?.text_editor?.text_wrapping,
 | 
			
		||||
      blinking_cursor: configuration?.settings?.text_editor?.blinking_cursor,
 | 
			
		||||
    },
 | 
			
		||||
    projects: {
 | 
			
		||||
      defaultProjectName:
 | 
			
		||||
    project: {
 | 
			
		||||
      default_project_name:
 | 
			
		||||
        configuration?.settings?.project?.default_project_name,
 | 
			
		||||
      directory: configuration?.settings?.project?.directory,
 | 
			
		||||
    },
 | 
			
		||||
    commandBar: {
 | 
			
		||||
      includeSettings: configuration?.settings?.command_bar?.include_settings,
 | 
			
		||||
    command_bar: {
 | 
			
		||||
      include_settings: configuration?.settings?.command_bar?.include_settings,
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -73,29 +75,31 @@ export function projectConfigurationToSettingsPayload(
 | 
			
		||||
): DeepPartial<SaveSettingsPayload> {
 | 
			
		||||
  return {
 | 
			
		||||
    app: {
 | 
			
		||||
      // do not read in `theme`, because it is blocked on the project level
 | 
			
		||||
      themeColor: configuration?.settings?.app?.appearance?.color
 | 
			
		||||
        ? configuration?.settings?.app?.appearance?.color.toString()
 | 
			
		||||
        : undefined,
 | 
			
		||||
      onboardingStatus: configuration?.settings?.app?.onboarding_status,
 | 
			
		||||
      dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
 | 
			
		||||
      streamIdleMode: configuration?.settings?.app?.stream_idle_mode,
 | 
			
		||||
      enableSSAO: configuration?.settings?.modeling?.enable_ssao,
 | 
			
		||||
      appearance: {
 | 
			
		||||
        // do not read in `theme`, because it is blocked on the project level
 | 
			
		||||
        color: configuration?.settings?.app?.appearance?.color
 | 
			
		||||
          ? configuration?.settings?.app?.appearance?.color.toString()
 | 
			
		||||
          : undefined,
 | 
			
		||||
      },
 | 
			
		||||
      onboarding_status: configuration?.settings?.app?.onboarding_status,
 | 
			
		||||
      dismiss_web_banner: configuration?.settings?.app?.dismiss_web_banner,
 | 
			
		||||
      stream_idle_mode: configuration?.settings?.app?.stream_idle_mode,
 | 
			
		||||
    },
 | 
			
		||||
    modeling: {
 | 
			
		||||
      defaultUnit: configuration?.settings?.modeling?.base_unit,
 | 
			
		||||
      mouseControls: mouseControlsToCameraSystem(
 | 
			
		||||
      base_unit: configuration?.settings?.modeling?.base_unit,
 | 
			
		||||
      mouse_controls: mouseControlsToCameraSystem(
 | 
			
		||||
        configuration?.settings?.modeling?.mouse_controls
 | 
			
		||||
      ),
 | 
			
		||||
      highlightEdges: configuration?.settings?.modeling?.highlight_edges,
 | 
			
		||||
      showDebugPanel: configuration?.settings?.modeling?.show_debug_panel,
 | 
			
		||||
      highlight_edges: configuration?.settings?.modeling?.highlight_edges,
 | 
			
		||||
      show_debug_panel: configuration?.settings?.modeling?.show_debug_panel,
 | 
			
		||||
      enable_ssao: configuration?.settings?.modeling?.enable_ssao,
 | 
			
		||||
    },
 | 
			
		||||
    textEditor: {
 | 
			
		||||
      textWrapping: configuration?.settings?.text_editor?.text_wrapping,
 | 
			
		||||
      blinkingCursor: configuration?.settings?.text_editor?.blinking_cursor,
 | 
			
		||||
    text_editor: {
 | 
			
		||||
      text_wrapping: configuration?.settings?.text_editor?.text_wrapping,
 | 
			
		||||
      blinking_cursor: configuration?.settings?.text_editor?.blinking_cursor,
 | 
			
		||||
    },
 | 
			
		||||
    commandBar: {
 | 
			
		||||
      includeSettings: configuration?.settings?.command_bar?.include_settings,
 | 
			
		||||
    command_bar: {
 | 
			
		||||
      include_settings: configuration?.settings?.command_bar?.include_settings,
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -181,7 +185,7 @@ export async function loadAndValidateSettings(
 | 
			
		||||
 | 
			
		||||
  // Because getting the default directory is async, we need to set it after
 | 
			
		||||
  if (onDesktop) {
 | 
			
		||||
    settings.app.projectDirectory.default = await getInitialDefaultDir()
 | 
			
		||||
    settings.project.directory.default = await getInitialDefaultDir()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  settingsNext = setSettingsAtLevel(
 | 
			
		||||
 | 
			
		||||
@ -84,13 +84,13 @@ export const settingsMachine = setup({
 | 
			
		||||
      return newContext
 | 
			
		||||
    }),
 | 
			
		||||
    setThemeClass: ({ context }) => {
 | 
			
		||||
      const currentTheme = context.app.theme.current ?? Themes.System
 | 
			
		||||
      const currentTheme = context.app.appearance.theme.current ?? Themes.System
 | 
			
		||||
      setThemeClass(
 | 
			
		||||
        currentTheme === Themes.System ? getSystemTheme() : currentTheme
 | 
			
		||||
      )
 | 
			
		||||
    },
 | 
			
		||||
    setEngineCameraProjection: ({ context }) => {
 | 
			
		||||
      const newCurrentProjection = context.modeling.cameraProjection.current
 | 
			
		||||
      const newCurrentProjection = context.modeling.camera_projection.current
 | 
			
		||||
      sceneInfra.camControls.setEngineCameraProjection(newCurrentProjection)
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
@ -79,13 +79,13 @@ const Home = () => {
 | 
			
		||||
    send({
 | 
			
		||||
      type: 'assign',
 | 
			
		||||
      data: {
 | 
			
		||||
        defaultProjectName: settings.projects.defaultProjectName.current,
 | 
			
		||||
        defaultDirectory: settings.app.projectDirectory.current,
 | 
			
		||||
        defaultProjectName: settings.project.default_project_name.current,
 | 
			
		||||
        defaultDirectory: settings.project.directory.current,
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  }, [
 | 
			
		||||
    settings.app.projectDirectory.current,
 | 
			
		||||
    settings.projects.defaultProjectName.current,
 | 
			
		||||
    settings.project.directory.current,
 | 
			
		||||
    settings.project.default_project_name.current,
 | 
			
		||||
    send,
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
@ -134,7 +134,7 @@ const Home = () => {
 | 
			
		||||
                      groupId: 'projects',
 | 
			
		||||
                      name: 'Create project',
 | 
			
		||||
                      argDefaultValues: {
 | 
			
		||||
                        name: settings.projects.defaultProjectName.current,
 | 
			
		||||
                        name: settings.project.default_project_name.current,
 | 
			
		||||
                      },
 | 
			
		||||
                    },
 | 
			
		||||
                  })
 | 
			
		||||
@ -207,7 +207,7 @@ const Home = () => {
 | 
			
		||||
              to={`${PATHS.HOME + PATHS.SETTINGS_USER}#projectDirectory`}
 | 
			
		||||
              className="text-chalkboard-90 dark:text-chalkboard-20 underline underline-offset-2"
 | 
			
		||||
            >
 | 
			
		||||
              {settings.app.projectDirectory.current}
 | 
			
		||||
              {settings.project.directory.current}
 | 
			
		||||
            </Link>
 | 
			
		||||
            .
 | 
			
		||||
          </p>
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ export default function Units() {
 | 
			
		||||
      send,
 | 
			
		||||
      state: {
 | 
			
		||||
        context: {
 | 
			
		||||
          modeling: { mouseControls },
 | 
			
		||||
          modeling: { mouse_controls },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
@ -38,10 +38,10 @@ export default function Units() {
 | 
			
		||||
          <select
 | 
			
		||||
            id="camera-controls"
 | 
			
		||||
            className="block w-full px-3 py-1 bg-transparent border border-chalkboard-30"
 | 
			
		||||
            value={mouseControls.current}
 | 
			
		||||
            value={mouse_controls.current}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              send({
 | 
			
		||||
                type: 'set.modeling.mouseControls',
 | 
			
		||||
                type: 'set.modeling.mouse_controls',
 | 
			
		||||
                data: {
 | 
			
		||||
                  level: 'user',
 | 
			
		||||
                  value: e.target.value as CameraSystem,
 | 
			
		||||
@ -58,15 +58,15 @@ export default function Units() {
 | 
			
		||||
          <ul className="mx-4 my-2 text-sm leading-relaxed">
 | 
			
		||||
            <li>
 | 
			
		||||
              <strong>Pan:</strong>{' '}
 | 
			
		||||
              {cameraMouseDragGuards[mouseControls.current].pan.description}
 | 
			
		||||
              {cameraMouseDragGuards[mouse_controls.current].pan.description}
 | 
			
		||||
            </li>
 | 
			
		||||
            <li>
 | 
			
		||||
              <strong>Zoom:</strong>{' '}
 | 
			
		||||
              {cameraMouseDragGuards[mouseControls.current].zoom.description}
 | 
			
		||||
              {cameraMouseDragGuards[mouse_controls.current].zoom.description}
 | 
			
		||||
            </li>
 | 
			
		||||
            <li>
 | 
			
		||||
              <strong>Rotate:</strong>{' '}
 | 
			
		||||
              {cameraMouseDragGuards[mouseControls.current].rotate.description}
 | 
			
		||||
              {cameraMouseDragGuards[mouse_controls.current].rotate.description}
 | 
			
		||||
            </li>
 | 
			
		||||
          </ul>
 | 
			
		||||
        </SettingsSection>
 | 
			
		||||
 | 
			
		||||
@ -126,7 +126,9 @@ function OnboardingIntroductionInner() {
 | 
			
		||||
    settings: {
 | 
			
		||||
      state: {
 | 
			
		||||
        context: {
 | 
			
		||||
          app: { theme },
 | 
			
		||||
          app: {
 | 
			
		||||
            appearance: { theme },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,9 @@ export default function OnboardingParametricModeling() {
 | 
			
		||||
    settings: {
 | 
			
		||||
      context: {
 | 
			
		||||
        app: {
 | 
			
		||||
          theme: { current: theme },
 | 
			
		||||
          appearance: {
 | 
			
		||||
            theme: { current: theme },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@ export default function Units() {
 | 
			
		||||
    settings: {
 | 
			
		||||
      send,
 | 
			
		||||
      context: {
 | 
			
		||||
        modeling: { defaultUnit },
 | 
			
		||||
        modeling: { base_unit },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  } = useSettingsAuthContext()
 | 
			
		||||
@ -29,10 +29,10 @@ export default function Units() {
 | 
			
		||||
          <select
 | 
			
		||||
            id="base-unit"
 | 
			
		||||
            className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
 | 
			
		||||
            value={defaultUnit.user}
 | 
			
		||||
            value={base_unit.user}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              send({
 | 
			
		||||
                type: 'set.modeling.defaultUnit',
 | 
			
		||||
                type: 'set.modeling.base_unit',
 | 
			
		||||
                data: {
 | 
			
		||||
                  level: 'user',
 | 
			
		||||
                  value: e.target.value as BaseUnit,
 | 
			
		||||
 | 
			
		||||
@ -116,7 +116,7 @@ export function useNextClick(newStatus: string) {
 | 
			
		||||
 | 
			
		||||
  return useCallback(() => {
 | 
			
		||||
    send({
 | 
			
		||||
      type: 'set.app.onboardingStatus',
 | 
			
		||||
      type: 'set.app.onboarding_status',
 | 
			
		||||
      data: { level: 'user', value: newStatus },
 | 
			
		||||
    })
 | 
			
		||||
    navigate(filePath + PATHS.ONBOARDING.INDEX.slice(0, -1) + newStatus)
 | 
			
		||||
@ -132,7 +132,7 @@ export function useDismiss() {
 | 
			
		||||
 | 
			
		||||
  const settingsCallback = useCallback(() => {
 | 
			
		||||
    send({
 | 
			
		||||
      type: 'set.app.onboardingStatus',
 | 
			
		||||
      type: 'set.app.onboarding_status',
 | 
			
		||||
      data: { level: 'user', value: 'dismissed' },
 | 
			
		||||
    })
 | 
			
		||||
  }, [send])
 | 
			
		||||
@ -143,7 +143,7 @@ export function useDismiss() {
 | 
			
		||||
   */
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (
 | 
			
		||||
      state.context.app.onboardingStatus.user === 'dismissed' &&
 | 
			
		||||
      state.context.app.onboarding_status.user === 'dismissed' &&
 | 
			
		||||
      state.matches('idle')
 | 
			
		||||
    ) {
 | 
			
		||||
      navigate(filePath)
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,9 @@ const SignIn = () => {
 | 
			
		||||
    settings: {
 | 
			
		||||
      state: {
 | 
			
		||||
        context: {
 | 
			
		||||
          app: { theme },
 | 
			
		||||
          app: {
 | 
			
		||||
            appearance: { theme },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user