Migrate to new split sidebar from accordion-like panes (#2063)

* Split ModelingSidebar out into own component

* Consolidate all ModelingPane components and config

* Make ModelingSidebar a directory of components and config

* Remove unused components

* Proper pane styling

* Make tooltip configurable to visually appear on hover only

* Remove debug panel from App

* Fix current tests

* Rename to more intuitive names

* Fix useEffect loop bug with showDebugPanel

* Fix snapshot tests

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Rerun CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Merge branch 'main' into franknoirot/sidebar

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Rerun CI

* Maybe some flakiness in the validation initScripts?

* Avoid test flakiness by waiting for more signals that loading is completed

* Don't assert, just wait for the element to be enabled

* Don't let users accidentally click the gap between the pane and the side of the window

* Firm up extrude from command bar test

* Get rid of unused imports

* Add setting to disable blinking cursor (#2065)

* Add support for "current" marker in command bar for boolean settings

* Add a cursorBlinking setting

* Rename setting to blinkingCursor, honor it in the UI

* Fix scroll layout bug in settings modal

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Rerun CI

* CSS tweaks

* Allow settings hotkey within KclEditorPane

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* Rerun CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Rerun CI

* Ensure the KCL code panel is closed for camera movement test

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Make sure that the camera position inputs are ready to be read from

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Remove repeat awaits

* Make camera position fields in debug pane update when the pane is initialized

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Undo that CameraControls change because it made other things weird

* retry fixing camera move test

* Fix race condition where cam setting cam position parts were overwriting each other

* Rerun CI

* Rerun CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Frank Noirot
2024-04-15 12:04:17 -04:00
committed by GitHub
parent fdadd059d6
commit 3fdf7bd45e
48 changed files with 927 additions and 706 deletions

View File

@ -145,6 +145,7 @@ test('Can moving camera', async ({ page, context }) => {
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await u.closeKclCodePanel()
const camPos: [number, number, number] = [0, 85, 85] const camPos: [number, number, number] = [0, 85, 85]
const bakeInRetries = async ( const bakeInRetries = async (
@ -178,6 +179,8 @@ test('Can moving camera', async ({ page, context }) => {
}, 300) }, 300)
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await page.getByTestId('cam-x-position').isVisible()
const vals = await Promise.all([ const vals = await Promise.all([
page.getByTestId('cam-x-position').inputValue(), page.getByTestId('cam-x-position').inputValue(),
page.getByTestId('cam-y-position').inputValue(), page.getByTestId('cam-y-position').inputValue(),
@ -342,7 +345,11 @@ test('executes on load', async ({ page }) => {
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// expand variables section // expand variables section
await page.getByText('Variables').click() const variablesTabButton = page.getByRole('tab', {
name: 'Variables',
exact: false,
})
await variablesTabButton.click()
// can find part001 in the variables summary (pretty-json-container, makes sure we're not looking in the code editor) // can find part001 in the variables summary (pretty-json-container, makes sure we're not looking in the code editor)
// part001 only shows up in the variables summary if it's been executed // part001 only shows up in the variables summary if it's been executed
@ -366,7 +373,11 @@ test('re-executes', async ({ page }) => {
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await page.getByText('Variables').click() const variablesTabButton = page.getByRole('tab', {
name: 'Variables',
exact: false,
})
await variablesTabButton.click()
// expect to see "myVar:5" // expect to see "myVar:5"
await expect( await expect(
page.locator('.pretty-json-container >> text=myVar:5') page.locator('.pretty-json-container >> text=myVar:5')
@ -538,10 +549,13 @@ test('Auto complete works', async ({ page }) => {
test('Stored settings are validated and fall back to defaults', async ({ test('Stored settings are validated and fall back to defaults', async ({
page, page,
context,
}) => { }) => {
const u = getUtils(page)
// Override beforeEach test setup // Override beforeEach test setup
// with corrupted settings // with corrupted settings
await page.addInitScript( await context.addInitScript(
async ({ settingsKey, settings }) => { async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings) localStorage.setItem(settingsKey, settings)
}, },
@ -553,6 +567,7 @@ test('Stored settings are validated and fall back to defaults', async ({
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/') await page.goto('/')
await u.waitForAuthSkipAppStart()
// Check the settings were reset // Check the settings were reset
const storedSettings = TOML.parse( const storedSettings = TOML.parse(
@ -876,14 +891,13 @@ test.describe('Command bar tests', () => {
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
` `const distance = sqrt(20)
const distance = sqrt(20)
const part001 = startSketchOn('-XZ') const part001 = startSketchOn('-XZ')
|> startProfileAt([-6.95, 4.98], %) |> startProfileAt([-6.95, 4.98], %)
|> line([25.1, 0.41], %) |> line([25.1, 0.41], %)
|> line([0.73, -14.93], %) |> line([0.73, -14.93], %)
|> line([-23.44, 0.52], %) |> line([-23.44, 0.52], %)
|> close(%) |> close(%)
` `
) )
}) })
@ -896,15 +910,13 @@ test.describe('Command bar tests', () => {
// Make sure the stream is up // Make sure the stream is up
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
await page.getByText('|> startProfileAt([-6.95, 4.98], %)').click() await u.clearCommandLogs()
await expect( await page.getByText('|> line([0.73, -14.93], %)').click()
page.getByRole('button', { name: 'Extrude' }) await page.getByRole('button', { name: 'Extrude' }).isEnabled()
).not.toBeDisabled()
let cmdSearchBar = page.getByPlaceholder('Search commands') let cmdSearchBar = page.getByPlaceholder('Search commands')
await page.keyboard.press('Meta+K') await page.keyboard.press('Meta+K')
@ -922,23 +934,25 @@ test.describe('Command bar tests', () => {
await expect(page.getByPlaceholder('Variable name')).toHaveValue( await expect(page.getByPlaceholder('Variable name')).toHaveValue(
'distance001' 'distance001'
) )
await expect(page.getByRole('button', { name: 'Continue' })).toBeEnabled()
await page.getByRole('button', { name: 'Continue' }).click() const continueButton = page.getByRole('button', { name: 'Continue' })
const submitButton = page.getByRole('button', { name: 'Submit command' })
await continueButton.click()
// Review step and argument hotkeys // Review step and argument hotkeys
await expect( await expect(submitButton).toBeEnabled()
page.getByRole('button', { name: 'Submit command' })
).toBeEnabled()
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
// Assert we're back on the distance step
await expect( await expect(
page.getByRole('button', { name: 'Distance 12', exact: false }) page.getByRole('button', { name: 'Distance 12', exact: false })
).toBeDisabled() ).toBeDisabled()
await page.keyboard.press('Enter')
await expect(page.getByText('Confirm Extrude')).toBeVisible() await continueButton.click()
await submitButton.click()
// Check that the code was updated // Check that the code was updated
await page.keyboard.press('Enter') await u.waitForCmdReceive('extrude')
// Unfortunately this indentation seems to matter for the test // Unfortunately this indentation seems to matter for the test
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`const distance = sqrt(20) `const distance = sqrt(20)

View File

@ -382,11 +382,11 @@ test('extrude on each default plane should be stable', async ({
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel() await u.clearAndCloseDebugPanel()
await page.getByText('Code').click() await u.closeKclCodePanel()
await expect(page).toHaveScreenshot({ await expect(page).toHaveScreenshot({
maxDiffPixels: 100, maxDiffPixels: 100,
}) })
await page.getByText('Code').click() await u.openKclCodePanel()
} }
await runSnapshotsForOtherPlanes('XY') await runSnapshotsForOtherPlanes('XY')
await runSnapshotsForOtherPlanes('-XY') await runSnapshotsForOtherPlanes('-XY')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -44,26 +44,44 @@ async function waitForDefaultPlanesToBeVisible(page: Page) {
) )
} }
async function openDebugPanel(page: Page) { async function openKclCodePanel(page: Page) {
const isOpen = const paneLocator = page.getByRole('tab', { name: 'KCL Code', exact: false })
(await page const isOpen = (await paneLocator?.getAttribute('aria-selected')) === 'true'
.locator('[data-testid="debug-panel"]')
?.getAttribute('open')) === ''
if (!isOpen) { if (!isOpen) {
await page.getByText('Debug').click() await paneLocator.click()
await page.getByTestId('debug-panel').and(page.locator('[open]')).waitFor() await paneLocator.and(page.locator('[aria-selected="true"]')).waitFor()
}
}
async function closeKclCodePanel(page: Page) {
const paneLocator = page.getByRole('tab', { name: 'KCL Code', exact: false })
const isOpen = (await paneLocator?.getAttribute('aria-selected')) === 'true'
if (isOpen) {
await paneLocator.click()
await paneLocator
.and(page.locator(':not([aria-selected="true"])'))
.waitFor()
}
}
async function openDebugPanel(page: Page) {
const debugLocator = page.getByRole('tab', { name: 'Debug', exact: false })
const isOpen = (await debugLocator?.getAttribute('aria-selected')) === 'true'
if (!isOpen) {
await debugLocator.click()
await debugLocator.and(page.locator('[aria-selected="true"]')).waitFor()
} }
} }
async function closeDebugPanel(page: Page) { async function closeDebugPanel(page: Page) {
const isOpen = const debugLocator = page.getByRole('tab', { name: 'Debug', exact: false })
(await page.getByTestId('debug-panel')?.getAttribute('open')) === '' const isOpen = (await debugLocator?.getAttribute('aria-selected')) === 'true'
if (isOpen) { if (isOpen) {
await page.getByText('Debug').click() await debugLocator.click()
await page await debugLocator
.getByTestId('debug-panel') .and(page.locator(':not([aria-selected="true"])'))
.and(page.locator(':not([open])'))
.waitFor() .waitFor()
} }
} }
@ -81,20 +99,19 @@ export function getUtils(page: Page) {
removeCurrentCode: () => removeCurrentCode(page), removeCurrentCode: () => removeCurrentCode(page),
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd), sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
updateCamPosition: async (xyz: [number, number, number]) => { updateCamPosition: async (xyz: [number, number, number]) => {
const fillInput = async () => { const fillInput = async (axis: 'x' | 'y' | 'z', value: number) => {
await page.fill('[data-testid="cam-x-position"]', String(xyz[0])) await page.fill(`[data-testid="cam-${axis}-position"]`, String(value))
await page.fill('[data-testid="cam-y-position"]', String(xyz[1])) await page.waitForTimeout(100)
await page.fill('[data-testid="cam-z-position"]', String(xyz[2]))
} }
await fillInput()
await page.waitForTimeout(100) await fillInput('x', xyz[0])
await fillInput() await fillInput('y', xyz[1])
await page.waitForTimeout(100) await fillInput('z', xyz[2])
await fillInput()
await page.waitForTimeout(100)
}, },
clearCommandLogs: () => clearCommandLogs(page), clearCommandLogs: () => clearCommandLogs(page),
expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr), expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr),
openKclCodePanel: () => openKclCodePanel(page),
closeKclCodePanel: () => closeKclCodePanel(page),
openDebugPanel: () => openDebugPanel(page), openDebugPanel: () => openDebugPanel(page),
closeDebugPanel: () => closeDebugPanel(page), closeDebugPanel: () => closeDebugPanel(page),
openAndClearDebugPanel: async () => { openAndClearDebugPanel: async () => {

View File

@ -1,22 +1,12 @@
import { useCallback, MouseEventHandler, useEffect, useRef } from 'react' import { MouseEventHandler, useEffect, useRef } from 'react'
import { DebugPanel } from './components/DebugPanel'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { PaneType, useStore } from './useStore' import { useStore } from './useStore'
import { Logs, KCLErrors } from './components/Logs'
import { CollapsiblePanel } from './components/CollapsiblePanel'
import { MemoryPanel } from './components/MemoryPanel'
import { useHotKeyListener } from './hooks/useHotKeyListener' import { useHotKeyListener } from './hooks/useHotKeyListener'
import { Stream } from './components/Stream' import { Stream } from './components/Stream'
import ModalContainer from 'react-modal-promise' import ModalContainer from 'react-modal-promise'
import { EngineCommand } from './lang/std/engineConnection' import { EngineCommand } from './lang/std/engineConnection'
import { throttle } from './lib/utils' import { throttle } from './lib/utils'
import { AppHeader } from './components/AppHeader' import { AppHeader } from './components/AppHeader'
import { Resizable } from 're-resizable'
import {
faCode,
faCodeCommit,
faSquareRootVariable,
} from '@fortawesome/free-solid-svg-icons'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { getNormalisedCoordinates } from './lib/utils' import { getNormalisedCoordinates } from './lib/utils'
import { useLoaderData, useNavigate } from 'react-router-dom' import { useLoaderData, useNavigate } from 'react-router-dom'
@ -24,9 +14,6 @@ import { type IndexLoaderData } from 'lib/types'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { CodeMenu } from 'components/CodeMenu'
import { TextEditor } from 'components/TextEditor'
import { Themes, getSystemTheme } from 'lib/theme'
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions' import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
import { engineCommandManager } from 'lib/singletons' import { engineCommandManager } from 'lib/singletons'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
@ -34,6 +21,7 @@ import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { useLspContext } from 'components/LspProvider' import { useLspContext } from 'components/LspProvider'
import { useRefreshSettings } from 'hooks/useRefreshSettings' import { useRefreshSettings } from 'hooks/useRefreshSettings'
import { ModelingSidebar } from 'components/ModelingSidebar/ModelingSidebar'
export function App() { export function App() {
useRefreshSettings(paths.FILE + 'SETTINGS') useRefreshSettings(paths.FILE + 'SETTINGS')
@ -52,21 +40,13 @@ export function App() {
}, [projectName, projectPath]) }, [projectName, projectPath])
useHotKeyListener() useHotKeyListener()
const { const { buttonDownInStream, didDragInStream, streamDimensions, setHtmlRef } =
buttonDownInStream, useStore((s) => ({
openPanes, buttonDownInStream: s.buttonDownInStream,
setOpenPanes, didDragInStream: s.didDragInStream,
didDragInStream, streamDimensions: s.streamDimensions,
streamDimensions, setHtmlRef: s.setHtmlRef,
setHtmlRef, }))
} = useStore((s) => ({
buttonDownInStream: s.buttonDownInStream,
openPanes: s.openPanes,
setOpenPanes: s.setOpenPanes,
didDragInStream: s.didDragInStream,
streamDimensions: s.streamDimensions,
setHtmlRef: s.setHtmlRef,
}))
useEffect(() => { useEffect(() => {
setHtmlRef(ref) setHtmlRef(ref)
@ -74,27 +54,10 @@ export function App() {
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const { const {
modeling: { showDebugPanel }, app: { onboardingStatus },
app: { theme, onboardingStatus },
} = settings.context } = settings.context
const { state, send } = useModelingContext() const { state, send } = useModelingContext()
const editorTheme =
theme.current === Themes.System ? getSystemTheme() : theme.current
// Pane toggling keyboard shortcuts
const togglePane = useCallback(
(newPane: PaneType) =>
openPanes.includes(newPane)
? setOpenPanes(openPanes.filter((p) => p !== newPane))
: setOpenPanes([...openPanes, newPane]),
[openPanes, setOpenPanes]
)
useHotkeys('shift + c', () => togglePane('code'))
useHotkeys('shift + v', () => togglePane('variables'))
useHotkeys('shift + l', () => togglePane('logs'))
useHotkeys('shift + e', () => togglePane('kclErrors'))
useHotkeys('shift + d', () => togglePane('debug'))
useHotkeys('esc', () => send('Cancel')) useHotkeys('esc', () => send('Cancel'))
useHotkeys('backspace', (e) => { useHotkeys('backspace', (e) => {
e.preventDefault() e.preventDefault()
@ -161,74 +124,8 @@ export function App() {
enableMenu={true} enableMenu={true}
/> />
<ModalContainer /> <ModalContainer />
<Resizable <ModelingSidebar paneOpacity={paneOpacity} />
className={
'pointer-events-none h-full flex flex-col flex-1 z-10 my-2 ml-2 pr-1 transition-opacity transition-duration-75 ' +
+paneOpacity
}
defaultSize={{
width: '550px',
height: 'auto',
}}
minWidth={200}
maxWidth={800}
minHeight={'auto'}
maxHeight={'auto'}
handleClasses={{
right:
'hover:bg-chalkboard-10 hover:dark:bg-chalkboard-110 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ' +
(buttonDownInStream || onboardingStatus.current === 'camera'
? 'pointer-events-none '
: 'pointer-events-auto'),
}}
>
<div
id="code-pane"
className="h-full flex flex-col justify-between pointer-events-none"
>
<CollapsiblePanel
title="Code"
icon={faCode}
className="open:!mb-2"
open={openPanes.includes('code')}
menu={<CodeMenu />}
>
<TextEditor theme={editorTheme} />
</CollapsiblePanel>
<section className="flex flex-col">
<MemoryPanel
theme={editorTheme}
open={openPanes.includes('variables')}
title="Variables"
icon={faSquareRootVariable}
/>
<Logs
theme={editorTheme}
open={openPanes.includes('logs')}
title="Logs"
icon={faCodeCommit}
/>
<KCLErrors
theme={editorTheme}
open={openPanes.includes('kclErrors')}
title="KCL Errors"
iconClassNames={{ bg: 'group-open:bg-destroy-70' }}
/>
</section>
</div>
</Resizable>
<Stream className="absolute inset-0 z-0" /> <Stream className="absolute inset-0 z-0" />
{showDebugPanel.current && (
<DebugPanel
title="Debug"
className={
'transition-opacity transition-duration-75 ' +
paneOpacity +
(buttonDownInStream ? ' pointer-events-none' : '')
}
open={openPanes.includes('debug')}
/>
)}
{/* <CamToggle /> */} {/* <CamToggle /> */}
</div> </div>
) )

View File

@ -110,14 +110,6 @@ export class CameraControls {
}, 400) as any as number }, 400) as any as number
} }
// reacts hooks into some of this singleton's properties
reactCameraProperties: ReactCameraProperties = {
type: 'perspective',
fov: 12,
position: [0, 0, 0],
quaternion: [0, 0, 0, 1],
}
setCam = (camProps: ReactCameraProperties) => { setCam = (camProps: ReactCameraProperties) => {
if ( if (
camProps.type === 'perspective' && camProps.type === 'perspective' &&
@ -910,6 +902,26 @@ export class CameraControls {
.start() .start()
}) })
get reactCameraProperties(): ReactCameraProperties {
return {
type: this.isPerspective ? 'perspective' : 'orthographic',
[this.isPerspective ? 'fov' : 'zoom']:
this.camera instanceof PerspectiveCamera
? this.camera.fov
: this.camera.zoom,
position: [
roundOff(this.camera.position.x, 2),
roundOff(this.camera.position.y, 2),
roundOff(this.camera.position.z, 2),
],
quaternion: [
roundOff(this.camera.quaternion.x, 2),
roundOff(this.camera.quaternion.y, 2),
roundOff(this.camera.quaternion.z, 2),
roundOff(this.camera.quaternion.w, 2),
],
}
}
reactCameraPropertiesCallback: (a: ReactCameraProperties) => void = () => {} reactCameraPropertiesCallback: (a: ReactCameraProperties) => void = () => {}
setReactCameraPropertiesCallback = ( setReactCameraPropertiesCallback = (
cb: (a: ReactCameraProperties) => void cb: (a: ReactCameraProperties) => void
@ -937,24 +949,7 @@ export class CameraControls {
isPerspective: this.isPerspective, isPerspective: this.isPerspective,
target: this.target, target: this.target,
}) })
this.deferReactUpdate({ this.deferReactUpdate(this.reactCameraProperties)
type: this.isPerspective ? 'perspective' : 'orthographic',
[this.isPerspective ? 'fov' : 'zoom']:
this.camera instanceof PerspectiveCamera
? this.camera.fov
: this.camera.zoom,
position: [
roundOff(this.camera.position.x, 2),
roundOff(this.camera.position.y, 2),
roundOff(this.camera.position.z, 2),
],
quaternion: [
roundOff(this.camera.quaternion.x, 2),
roundOff(this.camera.quaternion.y, 2),
roundOff(this.camera.quaternion.z, 2),
roundOff(this.camera.quaternion.w, 2),
],
})
Object.values(this._camChangeCallbacks).forEach((cb) => cb()) Object.values(this._camChangeCallbacks).forEach((cb) => cb())
} }
getInteractionType = (event: any) => getInteractionType = (event: any) =>

View File

@ -126,12 +126,9 @@ const throttled = throttle((a: ReactCameraProperties) => {
}, 1000 / 15) }, 1000 / 15)
export const CamDebugSettings = () => { export const CamDebugSettings = () => {
const [camSettings, setCamSettings] = useState<ReactCameraProperties>({ const [camSettings, setCamSettings] = useState<ReactCameraProperties>(
type: 'perspective', sceneInfra.camControls.reactCameraProperties
fov: 12, )
position: [0, 0, 0],
quaternion: [0, 0, 0, 1],
})
const [fov, setFov] = useState(12) const [fov, setFov] = useState(12)
useEffect(() => { useEffect(() => {

View File

@ -16,7 +16,7 @@ export function AstExplorer() {
const [filterKeys, setFilterKeys] = useState<string[]>(['start', 'end']) const [filterKeys, setFilterKeys] = useState<string[]>(['start', 'end'])
return ( return (
<div className="relative" style={{ width: '300px' }}> <div id="ast-explorer" className="relative">
<div className=""> <div className="">
filter out keys:<div className="w-2 inline-block"></div> filter out keys:<div className="w-2 inline-block"></div>
{['start', 'end', 'type'].map((key) => { {['start', 'end', 'type'].map((key) => {
@ -45,7 +45,7 @@ export function AstExplorer() {
setHighlightRange([0, 0]) setHighlightRange([0, 0])
}} }}
> >
<pre className=" text-xs overflow-y-auto" style={{ width: '300px' }}> <pre className="text-xs">
<DisplayObj <DisplayObj
obj={kclManager.ast} obj={kclManager.ast}
filterKeys={filterKeys} filterKeys={filterKeys}
@ -109,7 +109,7 @@ function DisplayObj({
<pre <pre
ref={ref} ref={ref}
className={`ml-2 border-l border-violet-600 pl-1 ${ className={`ml-2 border-l border-violet-600 pl-1 ${
hasCursor ? 'bg-violet-100/25' : '' hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
}`} }`}
onMouseEnter={(e) => { onMouseEnter={(e) => {
setHighlightRange([obj?.start || 0, obj.end]) setHighlightRange([obj?.start || 0, obj.end])

View File

@ -1,57 +0,0 @@
.panel {
@apply relative z-0;
@apply bg-chalkboard-10/70 backdrop-blur-sm;
}
.header::before,
.header::-webkit-details-marker {
display: none;
}
:global(.dark) .panel {
@apply bg-chalkboard-110/50 backdrop-blur-0;
}
.header {
@apply sticky top-0 z-10 cursor-pointer;
@apply flex items-center justify-between gap-2 w-full p-2;
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
@apply bg-chalkboard-10;
}
.header:not(:last-of-type) {
@apply border-b;
}
:global(.dark) .header {
@apply bg-chalkboard-110 border-b-chalkboard-90 text-chalkboard-30;
}
:global(.dark) .header:not(:last-of-type) {
@apply border-b-2;
}
.panel:first-of-type .header {
@apply rounded-t;
}
.panel:last-of-type .header {
@apply rounded-b;
}
.panel[open] .header {
@apply rounded-t rounded-b-none;
}
.panel[open] {
@apply flex-grow max-h-full h-48 my-1 rounded;
}
.panel[open] + .panel[open],
.panel[open]:first-of-type {
@apply mt-0;
}
.panel[open]:last-of-type {
@apply mb-0;
}

View File

@ -1,76 +0,0 @@
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { ActionIcon } from './ActionIcon'
import styles from './CollapsiblePanel.module.css'
export interface CollapsiblePanelProps
extends React.PropsWithChildren,
React.HTMLAttributes<HTMLDetailsElement> {
title: string
icon?: IconDefinition
open?: boolean
menu?: React.ReactNode
detailsTestId?: string
iconClassNames?: {
bg?: string
icon?: string
}
}
export const PanelHeader = ({
title,
icon,
iconClassNames,
menu,
}: CollapsiblePanelProps) => {
return (
<summary className={styles.header}>
<div className="flex gap-2 items-center flex-1">
<ActionIcon
icon={icon}
className="p-1"
size="sm"
bgClassName={
'dark:!bg-transparent group-open:bg-primary dark:group-open:!bg-primary rounded-sm ' +
(iconClassNames?.bg || '')
}
iconClassName={
'group-open:text-chalkboard-10 ' + (iconClassNames?.icon || '')
}
/>
{title}
</div>
<div className="group-open:opacity-100 opacity-0 group-open:pointer-events-auto pointer-events-none">
{menu}
</div>
</summary>
)
}
export const CollapsiblePanel = ({
title,
icon,
children,
className,
iconClassNames,
menu,
detailsTestId,
...props
}: CollapsiblePanelProps) => {
return (
<details
{...props}
data-testid={detailsTestId}
className={
styles.panel + ' pointer-events-auto group ' + (className || '')
}
>
<PanelHeader
title={title}
icon={icon}
iconClassNames={iconClassNames}
menu={menu}
/>
{children}
</details>
)
}

View File

@ -1,26 +0,0 @@
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { AstExplorer } from './AstExplorer'
import { EngineCommands } from './EngineCommands'
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
return (
<CollapsiblePanel
{...props}
className={
'!absolute overflow-auto !h-auto bottom-5 right-5 ' + className
}
// header height, top-5, and bottom-5
style={{ maxHeight: 'calc(100% - 3rem - 1.25rem - 1.25rem)' }}
detailsTestId="debug-panel"
>
<section className="p-4 flex flex-col gap-4">
<EngineCommands />
<CamDebugSettings />
<div style={{ height: '400px' }} className="overflow-y-auto">
<AstExplorer />
</div>
</section>
</CollapsiblePanel>
)
}

View File

@ -1,76 +0,0 @@
import ReactJson from 'react-json-view'
import { useEffect } from 'react'
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { Themes } from '../lib/theme'
import { useKclContext } from 'lang/KclProvider'
const ReactJsonTypeHack = ReactJson as any
interface LogPanelProps extends CollapsiblePanelProps {
theme?: Exclude<Themes, Themes.System>
}
export const Logs = ({ theme = Themes.Light, ...props }: LogPanelProps) => {
const { logs } = useKclContext()
useEffect(() => {
const element = document.querySelector('.console-tile')
if (element) {
element.scrollTop = element.scrollHeight - element.clientHeight
}
}, [logs])
return (
<CollapsiblePanel {...props}>
<div className="relative w-full">
<div className="absolute inset-0 flex flex-col">
<ReactJsonTypeHack
src={logs}
collapsed={1}
collapseStringsAfterLength={60}
enableClipboard={false}
displayArrayKey={false}
displayDataTypes={false}
displayObjectSize={true}
indentWidth={2}
quotesOnKeys={false}
name={false}
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
/>
</div>
</div>
</CollapsiblePanel>
)
}
export const KCLErrors = ({
theme = Themes.Light,
...props
}: LogPanelProps) => {
const { errors } = useKclContext()
useEffect(() => {
const element = document.querySelector('.console-tile')
if (element) {
element.scrollTop = element.scrollHeight - element.clientHeight
}
}, [errors])
return (
<CollapsiblePanel {...props}>
<div className="h-full relative">
<div className="absolute inset-0 flex flex-col">
<ReactJsonTypeHack
src={errors}
collapsed={1}
collapseStringsAfterLength={60}
enableClipboard={false}
displayArrayKey={false}
displayDataTypes={false}
displayObjectSize={true}
indentWidth={2}
quotesOnKeys={false}
name={false}
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
/>
</div>
</div>
</CollapsiblePanel>
)
}

View File

@ -1,70 +0,0 @@
import ReactJson from 'react-json-view'
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { useMemo } from 'react'
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/wasm'
import { Themes } from '../lib/theme'
import { useKclContext } from 'lang/KclProvider'
interface MemoryPanelProps extends CollapsiblePanelProps {
theme?: Exclude<Themes, Themes.System>
}
export const MemoryPanel = ({
theme = Themes.Light,
...props
}: MemoryPanelProps) => {
const { programMemory } = useKclContext()
const ProcessedMemory = useMemo(
() => processMemory(programMemory),
[programMemory]
)
return (
<CollapsiblePanel {...props}>
<div className="h-full relative">
<div className="absolute inset-0 flex flex-col items-start">
<div
className="overflow-y-auto h-full console-tile w-full"
style={{ marginBottom: 36 }}
>
{/* 36px is the height of PanelHeader */}
<ReactJson
src={ProcessedMemory}
collapsed={1}
collapseStringsAfterLength={60}
enableClipboard={false}
displayDataTypes={false}
displayObjectSize={true}
indentWidth={2}
quotesOnKeys={false}
name={false}
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
/>
</div>
</div>
</div>
</CollapsiblePanel>
)
}
export const processMemory = (programMemory: ProgramMemory) => {
const processedMemory: any = {}
Object.keys(programMemory?.root || {}).forEach((key) => {
const val = programMemory.root[key]
if (typeof val.value !== 'function') {
if (val.type === 'SketchGroup') {
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
return rest
})
} else if (val.type === 'ExtrudeGroup') {
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
return rest
})
} else {
processedMemory[key] = val.value
}
} else if (key !== 'log') {
processedMemory[key] = '__function__'
}
})
return processedMemory
}

View File

@ -0,0 +1,27 @@
.panel {
@apply relative z-0 rounded-r max-w-full h-full flex-1;
display: grid;
grid-template-rows: auto 1fr;
@apply bg-chalkboard-10/50 backdrop-blur-sm border border-chalkboard-20;
scroll-margin-block-start: 41px;
}
.header::before,
.header::-webkit-details-marker {
display: none;
}
:global(.dark) .panel {
@apply bg-chalkboard-100/50 backdrop-blur-[3px] border-chalkboard-80;
}
.header {
@apply z-10 relative rounded-tr;
@apply flex h-[41px] items-center justify-between gap-2 px-2;
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
@apply bg-chalkboard-10 border-b border-chalkboard-20;
}
:global(.dark) .header {
@apply bg-chalkboard-90 text-chalkboard-30 border-chalkboard-80;
}

View File

@ -0,0 +1,50 @@
import { useStore } from 'useStore'
import styles from './ModelingPane.module.css'
export interface ModelingPaneProps
extends React.PropsWithChildren,
React.HTMLAttributes<HTMLDivElement> {
title: string
Menu?: React.ReactNode | React.FC
detailsTestId?: string
}
export const ModelingPaneHeader = ({
title,
Menu,
}: Pick<ModelingPaneProps, 'title' | 'Menu'>) => {
return (
<div className={styles.header}>
<div className="flex gap-2 items-center flex-1">{title}</div>
{Menu instanceof Function ? <Menu /> : Menu}
</div>
)
}
export const ModelingPane = ({
title,
children,
className,
Menu,
detailsTestId,
...props
}: ModelingPaneProps) => {
const { buttonDownInStream } = useStore((s) => ({
buttonDownInStream: s.buttonDownInStream,
}))
return (
<section
{...props}
data-testid={detailsTestId}
className={
(buttonDownInStream ? 'pointer-events-none ' : 'pointer-events-auto ') +
styles.panel +
' group ' +
(className || '')
}
>
<ModelingPaneHeader title={title} Menu={Menu} />
<div className="relative w-full">{children}</div>
</section>
)
}

View File

@ -0,0 +1,18 @@
import { AstExplorer } from '../../AstExplorer'
import { EngineCommands } from '../../EngineCommands'
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
export const DebugPane = () => {
return (
<section
data-testid="debug-panel"
className="absolute inset-0 p-2 box-border overflow-auto"
>
<div className="flex flex-col">
<EngineCommands />
<CamDebugSettings />
<AstExplorer />
</div>
</section>
)
}

View File

@ -1,14 +1,14 @@
import { Menu } from '@headlessui/react' import { Menu } from '@headlessui/react'
import { PropsWithChildren } from 'react' import { PropsWithChildren } from 'react'
import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons' import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'
import { ActionIcon } from './ActionIcon' import { ActionIcon } from 'components/ActionIcon'
import styles from './CodeMenu.module.css' import styles from './KclEditorMenu.module.css'
import { useConvertToVariable } from 'hooks/useToolbarGuards' import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { editorShortcutMeta } from './TextEditor' import { editorShortcutMeta } from './KclEditorPane'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { kclManager } from 'lib/singletons' import { kclManager } from 'lib/singletons'
export const CodeMenu = ({ children }: PropsWithChildren) => { export const KclEditorMenu = ({ children }: PropsWithChildren) => {
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } = const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
useConvertToVariable() useConvertToVariable()
@ -30,7 +30,7 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
className="p-1" className="p-1"
size="sm" size="sm"
bgClassName={ bgClassName={
'!bg-transparent hover:!bg-primary/10 hover:dark:!bg-chalkboard-100 ui-active:!bg-primary/10 dark:ui-active:!bg-chalkboard-100 rounded-sm' '!bg-transparent hover:!bg-primary/10 hover:dark:!bg-chalkboard-100 ui-open:!bg-primary/10 dark:ui-open:!bg-chalkboard-100 rounded-sm'
} }
iconClassName={'!text-chalkboard-90 dark:!text-chalkboard-40'} iconClassName={'!text-chalkboard-90 dark:!text-chalkboard-40'}
/> />
@ -65,7 +65,7 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
> >
<span>Read the KCL docs</span> <span>Read the KCL docs</span>
<small> <small>
On GitHub zoo.dev
<FontAwesomeIcon <FontAwesomeIcon
icon={faArrowUpRightFromSquare} icon={faArrowUpRightFromSquare}
className="ml-1 align-text-top" className="ml-1 align-text-top"
@ -83,7 +83,7 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
> >
<span>KCL samples</span> <span>KCL samples</span>
<small> <small>
On GitHub zoo.dev
<FontAwesomeIcon <FontAwesomeIcon
icon={faArrowUpRightFromSquare} icon={faArrowUpRightFromSquare}
className="ml-1 align-text-top" className="ml-1 align-text-top"

View File

@ -3,12 +3,13 @@ import ReactCodeMirror, {
Extension, Extension,
ViewUpdate, ViewUpdate,
SelectionRange, SelectionRange,
drawSelection,
} from '@uiw/react-codemirror' } from '@uiw/react-codemirror'
import { TEST } from 'env' import { TEST } from 'env'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useConvertToVariable } from 'hooks/useToolbarGuards' import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { Themes } from 'lib/theme' import { Themes, getSystemTheme } from 'lib/theme'
import { useEffect, useMemo, useRef } from 'react' import { useEffect, useMemo, useRef } from 'react'
import { useStore } from 'useStore' import { useStore } from 'useStore'
import { processCodeMirrorRanges } from 'lib/selections' import { processCodeMirrorRanges } from 'lib/selections'
@ -37,15 +38,21 @@ import {
bracketMatching, bracketMatching,
indentOnInput, indentOnInput,
} from '@codemirror/language' } from '@codemirror/language'
import { CSSRuleObject } from 'tailwindcss/types/config'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import interact from '@replit/codemirror-interact' import interact from '@replit/codemirror-interact'
import { engineCommandManager, sceneInfra, kclManager } from 'lib/singletons' import { engineCommandManager, sceneInfra, kclManager } from 'lib/singletons'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { ModelingMachineEvent } from 'machines/modelingMachine' import { ModelingMachineEvent } from 'machines/modelingMachine'
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator' import {
NetworkHealthState,
useNetworkStatus,
} from 'components/NetworkHealthIndicator'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { useLspContext } from './LspProvider' import { isTauri } from 'lib/isTauri'
import { useNavigate } from 'react-router-dom'
import { paths } from 'lib/paths'
import makeUrlPathRelative from 'lib/makeUrlPathRelative'
import { useLspContext } from 'components/LspProvider'
import { Prec, EditorState } from '@codemirror/state' import { Prec, EditorState } from '@codemirror/state'
import { import {
closeBrackets, closeBrackets,
@ -65,11 +72,14 @@ export const editorShortcutMeta = {
}, },
} }
export const TextEditor = ({ export const KclEditorPane = () => {
theme, const {
}: { settings: { context },
theme: Themes.Light | Themes.Dark } = useSettingsAuthContext()
}) => { const theme =
context.app.theme.current === Themes.System
? getSystemTheme()
: context.app.theme.current
const { editorView, setEditorView, isShiftDown } = useStore((s) => ({ const { editorView, setEditorView, isShiftDown } = useStore((s) => ({
editorView: s.editorView, editorView: s.editorView,
setEditorView: s.setEditorView, setEditorView: s.setEditorView,
@ -80,6 +90,7 @@ export const TextEditor = ({
const { overallState } = useNetworkStatus() const { overallState } = useNetworkStatus()
const isNetworkOkay = overallState === NetworkHealthState.Ok const isNetworkOkay = overallState === NetworkHealthState.Ok
const { copilotLSP, kclLSP } = useLspContext() const { copilotLSP, kclLSP } = useLspContext()
const navigate = useNavigate()
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') return if (typeof window === 'undefined') return
@ -109,6 +120,7 @@ export const TextEditor = ({
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const textWrapping = settings.context.textEditor.textWrapping const textWrapping = settings.context.textEditor.textWrapping
const cursorBlinking = settings.context.textEditor.blinkingCursor
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const { enable: convertEnabled, handleClick: convertCallback } = const { enable: convertEnabled, handleClick: convertCallback } =
useConvertToVariable() useConvertToVariable()
@ -189,6 +201,9 @@ export const TextEditor = ({
const editorExtensions = useMemo(() => { const editorExtensions = useMemo(() => {
const extensions = [ const extensions = [
drawSelection({
cursorBlinkRate: cursorBlinking.current ? 1200 : 0,
}),
lineHighlightField, lineHighlightField,
history(), history(),
closeBrackets(), closeBrackets(),
@ -208,6 +223,13 @@ export const TextEditor = ({
return false return false
}, },
}, },
{
key: isTauri() ? 'Meta-,' : 'Meta-Shift-,',
run: () => {
navigate(makeUrlPathRelative(paths.SETTINGS))
return false
},
},
{ {
key: editorShortcutMeta.formatCode.codeMirror, key: editorShortcutMeta.formatCode.codeMirror,
run: () => { run: () => {
@ -287,16 +309,14 @@ export const TextEditor = ({
} }
return extensions return extensions
}, [kclLSP, textWrapping.current, convertCallback]) }, [kclLSP, textWrapping.current, cursorBlinking.current, convertCallback])
return ( return (
<div <div
id="code-mirror-override" id="code-mirror-override"
className="full-height-subtract" className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')}
style={{ '--height-subtract': '4.25rem' } as CSSRuleObject}
> >
<ReactCodeMirror <ReactCodeMirror
className="h-full"
value={code} value={code}
extensions={editorExtensions} extensions={editorExtensions}
onChange={onChange} onChange={onChange}

View File

@ -0,0 +1,53 @@
import ReactJson from 'react-json-view'
import { useKclContext } from 'lang/KclProvider'
import { useResolvedTheme } from 'hooks/useResolvedTheme'
const ReactJsonTypeHack = ReactJson as any
export const LogsPane = () => {
const theme = useResolvedTheme()
const { logs } = useKclContext()
return (
<div className="overflow-hidden">
<div className="absolute inset-0 p-2 flex flex-col overflow-auto">
<ReactJsonTypeHack
src={logs}
collapsed={1}
collapseStringsAfterLength={60}
enableClipboard={false}
displayArrayKey={false}
displayDataTypes={false}
displayObjectSize={true}
indentWidth={2}
quotesOnKeys={false}
name={false}
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
/>
</div>
</div>
)
}
export const KclErrorsPane = () => {
const theme = useResolvedTheme()
const { errors } = useKclContext()
return (
<div className="overflow-hidden">
<div className="absolute inset-0 p-2 flex flex-col overflow-auto">
<ReactJsonTypeHack
src={errors}
collapsed={1}
collapseStringsAfterLength={60}
enableClipboard={false}
displayArrayKey={false}
displayDataTypes={false}
displayObjectSize={true}
indentWidth={2}
quotesOnKeys={false}
name={false}
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
/>
</div>
</div>
)
}

View File

@ -1,6 +1,6 @@
import { processMemory } from './MemoryPanel' import { processMemory } from './MemoryPane'
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../../../lib/testHelpers'
import { initPromise, parse } from '../lang/wasm' import { initPromise, parse } from '../../../lang/wasm'
beforeAll(() => initPromise) beforeAll(() => initPromise)

View File

@ -0,0 +1,57 @@
import ReactJson from 'react-json-view'
import { useMemo } from 'react'
import { ProgramMemory, Path, ExtrudeSurface } from 'lang/wasm'
import { useKclContext } from 'lang/KclProvider'
import { useResolvedTheme } from 'hooks/useResolvedTheme'
export const MemoryPane = () => {
const theme = useResolvedTheme()
const { programMemory } = useKclContext()
const ProcessedMemory = useMemo(
() => processMemory(programMemory),
[programMemory]
)
return (
<div className="h-full relative">
<div className="absolute inset-0 p-2 flex flex-col items-start">
<div className="overflow-auto h-full w-full pb-12">
<ReactJson
src={ProcessedMemory}
collapsed={1}
collapseStringsAfterLength={60}
enableClipboard={false}
displayDataTypes={false}
displayObjectSize={true}
indentWidth={2}
quotesOnKeys={false}
name={false}
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
/>
</div>
</div>
</div>
)
}
export const processMemory = (programMemory: ProgramMemory) => {
const processedMemory: any = {}
Object.keys(programMemory?.root || {}).forEach((key) => {
const val = programMemory.root[key]
if (typeof val.value !== 'function') {
if (val.type === 'SketchGroup') {
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
return rest
})
} else if (val.type === 'ExtrudeGroup') {
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
return rest
})
} else {
processedMemory[key] = val.value
}
} else if (key !== 'log') {
processedMemory[key] = '__function__'
}
})
return processedMemory
}

View File

@ -0,0 +1,67 @@
import {
IconDefinition,
faBugSlash,
faCode,
faCodeCommit,
faExclamationCircle,
faSquareRootVariable,
} from '@fortawesome/free-solid-svg-icons'
import { KclEditorMenu } from 'components/ModelingSidebar/ModelingPanes/KclEditorMenu'
import { CustomIconName } from 'components/CustomIcon'
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
import { ReactNode } from 'react'
import type { PaneType } from 'useStore'
import { MemoryPane } from './MemoryPane'
import { KclErrorsPane, LogsPane } from './LoggingPanes'
import { DebugPane } from './DebugPane'
export type Pane = {
id: PaneType
title: string
icon: CustomIconName | IconDefinition
Content: ReactNode | React.FC
Menu?: ReactNode | React.FC
keybinding: string
}
export const topPanes: Pane[] = [
{
id: 'code',
title: 'KCL Code',
icon: faCode,
Content: KclEditorPane,
keybinding: 'shift + c',
Menu: KclEditorMenu,
},
]
export const bottomPanes: Pane[] = [
{
id: 'variables',
title: 'Variables',
icon: faSquareRootVariable,
Content: MemoryPane,
keybinding: 'shift + v',
},
{
id: 'logs',
title: 'Logs',
icon: faCodeCommit,
Content: LogsPane,
keybinding: 'shift + l',
},
{
id: 'kclErrors',
title: 'KCL Errors',
icon: faExclamationCircle,
Content: KclErrorsPane,
keybinding: 'shift + e',
},
{
id: 'debug',
title: 'Debug',
icon: faBugSlash,
Content: DebugPane,
keybinding: 'shift + d',
},
]

View File

@ -0,0 +1,11 @@
.grid {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: 1fr 1fr;
row-gap: 0.25rem;
align-items: stretch;
position: relative;
padding-block: 1px;
max-width: 100%;
flex: 1 1 0;
}

View File

@ -0,0 +1,213 @@
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Resizable } from 're-resizable'
import { useCallback, useEffect, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { PaneType, useStore } from 'useStore'
import { Tab } from '@headlessui/react'
import { Pane, bottomPanes, topPanes } from './ModelingPanes'
import Tooltip from 'components/Tooltip'
import { ActionIcon } from 'components/ActionIcon'
import styles from './ModelingSidebar.module.css'
import { ModelingPane } from './ModelingPane'
interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40'
}
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const { buttonDownInStream, openPanes } = useStore((s) => ({
buttonDownInStream: s.buttonDownInStream,
openPanes: s.openPanes,
}))
const { settings } = useSettingsAuthContext()
const {
app: { onboardingStatus },
} = settings.context
const pointerEventsCssClass =
buttonDownInStream || onboardingStatus.current === 'camera'
? 'pointer-events-none '
: 'pointer-events-auto'
return (
<Resizable
className={`flex-1 flex flex-col z-10 my-2 pr-1 ${paneOpacity} ${pointerEventsCssClass}`}
defaultSize={{
width: '550px',
height: 'auto',
}}
minWidth={200}
maxWidth={800}
handleClasses={{
right:
(openPanes.length === 0 ? 'hidden ' : 'block ') +
'translate-x-1/2 hover:bg-chalkboard-10 hover:dark:bg-chalkboard-110 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ' +
pointerEventsCssClass,
}}
>
<div className={styles.grid + ' flex-1'}>
<ModelingSidebarSection panes={topPanes} />
<ModelingSidebarSection panes={bottomPanes} alignButtons="end" />
</div>
</Resizable>
)
}
interface ModelingSidebarSectionProps {
panes: Pane[]
alignButtons?: 'start' | 'end'
}
function ModelingSidebarSection({
panes,
alignButtons = 'start',
}: ModelingSidebarSectionProps) {
const { settings } = useSettingsAuthContext()
const showDebugPanel = settings.context.modeling.showDebugPanel
const paneIds = panes.map((pane) => pane.id)
const { openPanes, setOpenPanes } = useStore((s) => ({
openPanes: s.openPanes,
setOpenPanes: s.setOpenPanes,
}))
const foundOpenPane = openPanes.find((pane) => paneIds.includes(pane))
const [currentPane, setCurrentPane] = useState(
foundOpenPane || ('none' as PaneType | 'none')
)
const togglePane = useCallback(
(newPane: PaneType | 'none') => {
if (newPane === 'none') {
setOpenPanes(openPanes.filter((p) => p !== currentPane))
setCurrentPane('none')
} else if (newPane === currentPane) {
setCurrentPane('none')
setOpenPanes(openPanes.filter((p) => p !== newPane))
} else {
setOpenPanes([...openPanes.filter((p) => p !== currentPane), newPane])
setCurrentPane(newPane)
}
},
[openPanes, setOpenPanes, currentPane, setCurrentPane]
)
// Filter out the debug panel if it's not supposed to be shown
// TODO: abstract out for allowing user to configure which panes to show
const filteredPanes = showDebugPanel.current
? panes
: panes.filter((pane) => pane.id !== 'debug')
useEffect(() => {
if (
!showDebugPanel.current &&
currentPane === 'debug' &&
openPanes.includes('debug')
) {
togglePane('debug')
}
}, [showDebugPanel.current, togglePane, openPanes])
return (
<Tab.Group
vertical
selectedIndex={
currentPane === 'none' ? 0 : paneIds.indexOf(currentPane) + 1
}
onChange={(index) => {
const newPane = index === 0 ? 'none' : paneIds[index - 1]
togglePane(newPane)
}}
>
<Tab.List
className={
(alignButtons === 'start'
? 'justify-start self-start'
: 'justify-end self-end') +
(currentPane === 'none'
? ' rounded-r focus-within:!border-primary/50'
: ' border-r-0') +
' p-2 col-start-1 col-span-1 h-fit w-fit flex flex-col items-start gap-2 bg-chalkboard-10 border border-solid border-chalkboard-20 dark:bg-chalkboard-90 dark:border-chalkboard-80 ' +
(openPanes.length === 1 && currentPane === 'none' ? 'pr-0.5' : '')
}
>
<Tab key="none" className="sr-only">
No panes open
</Tab>
{filteredPanes.map((pane) => (
<ModelingPaneButton
key={pane.id}
paneConfig={pane}
currentPane={currentPane}
togglePane={() => togglePane(pane.id)}
/>
))}
</Tab.List>
<Tab.Panels
as="article"
className={
'col-start-2 col-span-1 ' +
(openPanes.length === 1
? currentPane !== 'none'
? `row-start-1 row-end-3`
: `hidden`
: ``)
}
>
<Tab.Panel key="none" />
{filteredPanes.map((pane) => (
<Tab.Panel key={pane.id} className="h-full">
<ModelingPane title={pane.title} Menu={pane.Menu}>
{pane.Content instanceof Function ? (
<pane.Content />
) : (
pane.Content
)}
</ModelingPane>
</Tab.Panel>
))}
</Tab.Panels>
</Tab.Group>
)
}
interface ModelingPaneButtonProps {
paneConfig: Pane
currentPane: PaneType | 'none'
togglePane: () => void
}
function ModelingPaneButton({
paneConfig,
currentPane,
togglePane,
}: ModelingPaneButtonProps) {
useHotkeys(paneConfig.keybinding, togglePane, {
scopes: ['modeling'],
})
return (
<Tab
key={paneConfig.id}
className="pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent p-0 m-0 rounded-sm !outline-none"
onClick={togglePane}
>
<ActionIcon
icon={paneConfig.icon}
className="p-1"
size="sm"
iconClassName={
paneConfig.id === currentPane
? ' !text-chalkboard-10'
: '!text-chalkboard-80 dark:!text-chalkboard-30'
}
bgClassName={
'rounded-sm ' +
(paneConfig.id === currentPane ? '!bg-primary' : '!bg-transparent')
}
/>
<Tooltip position="right" hoverOnly delay={800}>
<span>{paneConfig.title}</span>
<br />
<span className="text-xs capitalize">{paneConfig.keybinding}</span>
</Tooltip>
</Tab>
)
}

View File

@ -1,6 +1,5 @@
import { Popover, Transition } from '@headlessui/react' import { Popover, Transition } from '@headlessui/react'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { faHome } from '@fortawesome/free-solid-svg-icons'
import { type IndexLoaderData } from 'lib/types' import { type IndexLoaderData } from 'lib/types'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import { isTauri } from '../lib/isTauri' import { isTauri } from '../lib/isTauri'

View File

@ -116,7 +116,7 @@ export const SettingsAuthProviderBase = ({
}, },
}) })
}, },
toastSuccess: (context, event) => { toastSuccess: (_, event) => {
const eventParts = event.type.replace(/^set./, '').split('.') as [ const eventParts = event.type.replace(/^set./, '').split('.') as [
keyof typeof settings, keyof typeof settings,
string string
@ -211,6 +211,19 @@ export const SettingsAuthProviderBase = ({
) )
}, [settingsState.context.app.themeColor.current]) }, [settingsState.context.app.themeColor.current])
/**
* Update the --cursor-color CSS variable
* based on the setting textEditor.blinkingCursor.current
*/
useEffect(() => {
document.documentElement.style.setProperty(
`--cursor-color`,
settingsState.context.textEditor.blinkingCursor.current
? 'auto'
: 'transparent'
)
}, [settingsState.context.textEditor.blinkingCursor.current])
// Auth machine setup // Auth machine setup
const [authState, authSend, authActor] = useMachine(authMachine, { const [authState, authSend, authActor] = useMachine(authMachine, {
actions: { actions: {

View File

@ -94,11 +94,15 @@
position: relative; position: relative;
} }
:is(:hover, :focus-visible, :active) > .tooltip { :is(:hover, :active) > .tooltip {
opacity: 1; opacity: 1;
transition-delay: var(--_delay); transition-delay: var(--_delay);
} }
:is(:focus-visible) > .tooltip.withFocus {
opacity: 1;
}
:is(:focus, :focus-visible, :focus-within) > .tooltip { :is(:focus, :focus-visible, :focus-within) > .tooltip {
--_delay: 0 !important; --_delay: 0 !important;
} }

View File

@ -15,6 +15,7 @@ interface TooltipProps extends React.PropsWithChildren {
| 'inlineEnd' | 'inlineEnd'
className?: string className?: string
delay?: number delay?: number
hoverOnly?: boolean
} }
export default function Tooltip({ export default function Tooltip({
@ -22,13 +23,16 @@ export default function Tooltip({
position = 'top', position = 'top',
className, className,
delay = 200, delay = 200,
hoverOnly = false,
}: TooltipProps) { }: TooltipProps) {
return ( return (
<div <div
// @ts-ignore while awaiting merge of this PR for support of "inert" https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60822 // @ts-ignore while awaiting merge of this PR for support of "inert" https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60822
inert="true" inert="true"
role="tooltip" role="tooltip"
className={styles.tooltip + ' ' + styles[position] + ' ' + className} className={`${styles.tooltip} ${hoverOnly ? '' : styles.withFocus} ${
styles[position]
} ${className}`}
style={{ '--_delay': delay + 'ms' } as React.CSSProperties} style={{ '--_delay': delay + 'ms' } as React.CSSProperties}
> >
{children} {children}

View File

@ -0,0 +1,16 @@
import { Themes, getSystemTheme } from 'lib/theme'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
/**
* Resolves the current theme based on the theme setting
* and the system theme if needed.
* @returns {Themes.Light | Themes.Dark}
*/
export function useResolvedTheme() {
const {
settings: { context },
} = useSettingsAuthContext()
return context.app.theme.current === Themes.System
? getSystemTheme()
: context.app.theme.current
}

View File

@ -46,6 +46,15 @@ select {
@apply bg-chalkboard-90; @apply bg-chalkboard-90;
} }
/* We hide the cursor if the user has turned off the textEditor.blinkingCursor setting
* any elements that could present a blinking cursor to the user
*/
input,
textarea,
*[contenteditable] {
caret-color: var(--cursor-color, auto);
}
::-webkit-scrollbar { ::-webkit-scrollbar {
@apply w-2 h-2 rounded-sm; @apply w-2 h-2 rounded-sm;
@apply bg-chalkboard-20; @apply bg-chalkboard-20;
@ -113,32 +122,28 @@ code {
monospace; monospace;
} }
.full-height-subtract { /*
--height-subtract: 2.25rem; * The first descendent of the CodeMirror wrapper is the theme,
height: 100%; * but its identifying class can change depending on the theme.
max-height: calc(100% - var(--height-subtract)); */
} #code-mirror-override > div,
#code-mirror-override .cm-editor { #code-mirror-override .cm-editor {
@apply h-full bg-transparent; @apply bg-transparent h-full;
} }
#code-mirror-override .cm-scroller { #code-mirror-override .cm-scroller {
@apply h-full; overflow: auto;
}
#code-mirror-override .cm-scroller::-webkit-scrollbar {
@apply h-0;
} }
#code-mirror-override .cm-activeLine, #code-mirror-override .cm-activeLine,
#code-mirror-override .cm-activeLineGutter { #code-mirror-override .cm-activeLineGutter {
@apply bg-liquid-10/50; @apply bg-primary/20;
} }
.dark #code-mirror-override .cm-activeLine, .dark #code-mirror-override .cm-activeLine,
.dark #code-mirror-override .cm-activeLineGutter { .dark #code-mirror-override .cm-activeLineGutter {
@apply bg-liquid-80/50; @apply bg-primary/20;
mix-blend-mode: lighten;
} }
#code-mirror-override .cm-gutters { #code-mirror-override .cm-gutters {
@ -149,19 +154,29 @@ code {
@apply bg-chalkboard-110/50; @apply bg-chalkboard-110/50;
} }
#code-mirror-override .cm-content {
@apply caret-primary;
}
.dark #code-mirror-override .cm-content {
@apply caret-chalkboard-10;
}
#code-mirror-override .cm-focused .cm-cursor { #code-mirror-override .cm-focused .cm-cursor {
width: 0px; width: 0px;
} }
#code-mirror-override .cm-cursor { #code-mirror-override .cm-cursor {
display: block; display: block;
width: 1ch; width: 1ch;
@apply bg-liquid-40 mix-blend-multiply; @apply mix-blend-multiply;
@apply border-l-primary;
animation: blink 2s ease-out infinite;
} }
.dark #code-mirror-override .cm-cursor { .dark #code-mirror-override .cm-cursor {
@apply bg-liquid-50; @apply border-l-chalkboard-10;
}
#code-mirror-override.blink .cm-cursor {
animation: blink 1200ms ease-out infinite;
} }
@keyframes blink { @keyframes blink {
@ -169,8 +184,8 @@ code {
100% { 100% {
opacity: 0; opacity: 0;
} }
15% { 10% {
opacity: 0.75; opacity: 1;
} }
} }

View File

@ -15,6 +15,7 @@ import { getPropertyByPath } from 'lib/objectPropertyByPath'
import { buildCommandArgument } from 'lib/createMachineCommand' import { buildCommandArgument } from 'lib/createMachineCommand'
import decamelize from 'decamelize' import decamelize from 'decamelize'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { Setting } from 'lib/settings/initialSettings'
// An array of the paths to all of the settings that have commandConfigs // An array of the paths to all of the settings that have commandConfigs
export const settingsWithCommandConfigs = ( export const settingsWithCommandConfigs = (
@ -87,11 +88,34 @@ export function createSettingsCommand({
) )
return null return null
const valueArgConfig = { let valueArgConfig = {
...valueArgPartialConfig, ...valueArgPartialConfig,
required: true, required: true,
} as CommandArgumentConfig<S['default']> } as CommandArgumentConfig<S['default']>
// If the setting is a boolean, we coerce it into an options input type
if (valueArgConfig.inputType === 'boolean') {
valueArgConfig = {
...valueArgConfig,
inputType: 'options',
options: (cmdBarContext, machineContext) => {
const setting = getPropertyByPath(
machineContext,
type
) as Setting<boolean>
const level = cmdBarContext.argumentsToSubmit.level as SettingsLevel
const isCurrent =
setting[level] === undefined
? setting.getFallback(level) === true
: setting[level] === true
return [
{ name: 'On', value: true, isCurrent },
{ name: 'Off', value: false, isCurrent: !isCurrent },
]
},
}
}
// @ts-ignore - TODO figure out this typing for valueArgConfig // @ts-ignore - TODO figure out this typing for valueArgConfig
const valueArg = buildCommandArgument(valueArgConfig, context, actor) const valueArg = buildCommandArgument(valueArgConfig, context, actor)

View File

@ -151,7 +151,8 @@ export type CommandArgumentConfig<
defaultValue?: defaultValue?:
| OutputType | OutputType
| (( | ((
commandBarContext: ContextFrom<typeof commandBarMachine> commandBarContext: ContextFrom<typeof commandBarMachine>,
machineContext?: C
) => OutputType) ) => OutputType)
defaultValueFromContext?: (context: C) => OutputType defaultValueFromContext?: (context: C) => OutputType
} }

View File

@ -362,6 +362,17 @@ export function createSettings() {
inputType: 'boolean', inputType: 'boolean',
}, },
}), }),
/**
* Whether to make the cursor blink in the editor
*/
blinkingCursor: new Setting<boolean>({
defaultValue: true,
description: 'Whether to make the cursor blink in the editor',
validate: (v) => typeof v === 'boolean',
commandConfig: {
inputType: 'boolean',
},
}),
}, },
/** /**
* Settings that affect the behavior of project management. * Settings that affect the behavior of project management.

View File

@ -483,7 +483,8 @@ export const commandBarMachine = createMachine(
} }
if ( if (
(argConfig.inputType !== 'boolean' (argConfig.inputType !== 'boolean' &&
argConfig.inputType !== 'options'
? !argValue ? !argValue
: argValue === undefined) && : argValue === undefined) &&
isRequired isRequired

View File

@ -126,7 +126,7 @@ export const Settings = () => {
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className="rounded relative mx-auto bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-3xl w-full max-h-[66vh] shadow-lg flex flex-col gap-8 overflow-hidden"> <Dialog.Panel className="rounded relative mx-auto bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-3xl w-full max-h-[66vh] shadow-lg flex flex-col gap-8">
<div className="p-5 pb-0 flex justify-between items-center"> <div className="p-5 pb-0 flex justify-between items-center">
<h1 className="text-2xl font-bold">Settings</h1> <h1 className="text-2xl font-bold">Settings</h1>
<button <button
@ -163,8 +163,11 @@ export const Settings = () => {
</RadioGroup.Option> </RadioGroup.Option>
)} )}
</RadioGroup> </RadioGroup>
<div className="flex flex-grow overflow-hidden items-stretch pl-4 pr-5 pb-5 gap-4"> <div
<div className="flex w-64 flex-col gap-3 pr-2 py-1 border-0 border-r border-r-chalkboard-20 dark:border-r-chalkboard-90"> className="flex-1 grid items-stretch pl-4 pr-5 pb-5 gap-4 overflow-hidden"
style={{ gridTemplateColumns: 'auto 1fr' }}
>
<div className="flex w-32 flex-col gap-3 pr-2 py-1 border-0 border-r border-r-chalkboard-20 dark:border-r-chalkboard-90">
{Object.entries(context) {Object.entries(context)
.filter(([_, categorySettings]) => .filter(([_, categorySettings]) =>
// Filter out categories that don't have any non-hidden settings // Filter out categories that don't have any non-hidden settings
@ -216,176 +219,175 @@ export const Settings = () => {
About About
</button> </button>
</div> </div>
<div <div className="relative overflow-y-auto">
ref={scrollRef} <div ref={scrollRef} className="flex flex-col gap-6 px-2">
className="flex flex-col gap-6 px-2 overflow-y-auto" {Object.entries(context)
> .filter(([_, categorySettings]) =>
{Object.entries(context) // Filter out categories that don't have any non-hidden settings
.filter(([_, categorySettings]) => Object.values(categorySettings).some(
// Filter out categories that don't have any non-hidden settings (setting) => !shouldHideSetting(setting, settingsLevel)
Object.values(categorySettings).some( )
(setting) => !shouldHideSetting(setting, settingsLevel)
) )
) .map(([category, categorySettings]) => (
.map(([category, categorySettings]) => ( <Fragment key={category}>
<Fragment key={category}> <h2
<h2 id={`category-${category}`}
id={`category-${category}`} className="text-2xl mt-6 first-of-type:mt-0 capitalize font-bold"
className="text-2xl mt-6 first-of-type:mt-0 capitalize font-bold" >
> {decamelize(category, { separator: ' ' })}
{decamelize(category, { separator: ' ' })} </h2>
</h2> {Object.entries(categorySettings)
{Object.entries(categorySettings) .filter(
.filter( // Filter out settings that don't have a Component or inputType
// Filter out settings that don't have a Component or inputType // or are hidden on the current level or the current platform
// or are hidden on the current level or the current platform (item: [string, Setting<unknown>]) =>
(item: [string, Setting<unknown>]) => shouldShowSettingInput(item[1], settingsLevel)
shouldShowSettingInput(item[1], settingsLevel)
)
.map(([settingName, s]) => {
const setting = s as Setting
const parentValue =
setting[setting.getParentLevel(settingsLevel)]
return (
<SettingsSection
title={decamelize(settingName, {
separator: ' ',
})}
key={`${category}-${settingName}-${settingsLevel}`}
description={setting.description}
settingHasChanged={
setting[settingsLevel] !== undefined &&
setting[settingsLevel] !==
setting.getFallback(settingsLevel)
}
parentLevel={setting.getParentLevel(
settingsLevel
)}
onFallback={() =>
send({
type: `set.${category}.${settingName}`,
data: {
level: settingsLevel,
value:
parentValue !== undefined
? parentValue
: setting.getFallback(settingsLevel),
},
} as SetEventTypes)
}
>
<GeneratedSetting
category={category}
settingName={settingName}
settingsLevel={settingsLevel}
setting={setting}
/>
</SettingsSection>
) )
})} .map(([settingName, s]) => {
</Fragment> const setting = s as Setting
))} const parentValue =
<h2 id="settings-resets" className="text-2xl mt-6 font-bold"> setting[setting.getParentLevel(settingsLevel)]
Resets return (
</h2> <SettingsSection
<SettingsSection title={decamelize(settingName, {
title="Onboarding" separator: ' ',
description="Replay the onboarding process" })}
> key={`${category}-${settingName}-${settingsLevel}`}
<ActionButton description={setting.description}
Element="button" settingHasChanged={
onClick={restartOnboarding} setting[settingsLevel] !== undefined &&
icon={{ setting[settingsLevel] !==
icon: 'refresh', setting.getFallback(settingsLevel)
size: 'sm', }
className: 'p-1', parentLevel={setting.getParentLevel(
}} settingsLevel
)}
onFallback={() =>
send({
type: `set.${category}.${settingName}`,
data: {
level: settingsLevel,
value:
parentValue !== undefined
? parentValue
: setting.getFallback(settingsLevel),
},
} as SetEventTypes)
}
>
<GeneratedSetting
category={category}
settingName={settingName}
settingsLevel={settingsLevel}
setting={setting}
/>
</SettingsSection>
)
})}
</Fragment>
))}
<h2 id="settings-resets" className="text-2xl mt-6 font-bold">
Resets
</h2>
<SettingsSection
title="Onboarding"
description="Replay the onboarding process"
> >
Replay Onboarding <ActionButton
</ActionButton> Element="button"
</SettingsSection> onClick={restartOnboarding}
<SettingsSection icon={{
title="Reset settings" icon: 'refresh',
description={`Restore settings to their default values. Your settings are saved in size: 'sm',
className: 'p-1',
}}
>
Replay Onboarding
</ActionButton>
</SettingsSection>
<SettingsSection
title="Reset settings"
description={`Restore settings to their default values. Your settings are saved in
${ ${
isTauri() isTauri()
? ' a file in the app data folder for your OS.' ? ' a file in the app data folder for your OS.'
: " your browser's local storage." : " your browser's local storage."
} }
`} `}
> >
<div className="flex flex-col items-start gap-4"> <div className="flex flex-col items-start gap-4">
{isTauri() && ( {isTauri() && (
<ActionButton
Element="button"
onClick={async () => {
const paths = await getSettingsFolderPaths(
projectPath
? decodeURIComponent(projectPath)
: undefined
)
void invoke('show_in_folder', {
path: paths[settingsLevel],
})
}}
icon={{
icon: 'folder',
size: 'sm',
className: 'p-1',
}}
>
Show in folder
</ActionButton>
)}
<ActionButton <ActionButton
Element="button" Element="button"
onClick={async () => { onClick={async () => {
const paths = await getSettingsFolderPaths( const defaultDirectory = await getInitialDefaultDir()
projectPath send({
? decodeURIComponent(projectPath) type: 'Reset settings',
: undefined defaultDirectory,
)
void invoke('show_in_folder', {
path: paths[settingsLevel],
}) })
toast.success('Settings restored to default')
}} }}
icon={{ icon={{
icon: 'folder', icon: 'refresh',
size: 'sm', size: 'sm',
className: 'p-1', className: 'p-1 text-chalkboard-10',
bgClassName: 'bg-destroy-70',
}} }}
> >
Show in folder Restore default settings
</ActionButton> </ActionButton>
)} </div>
<ActionButton </SettingsSection>
Element="button" <h2 id="settings-about" className="text-2xl mt-6 font-bold">
onClick={async () => { About Modeling App
const defaultDirectory = await getInitialDefaultDir() </h2>
send({ <div className="text-sm mb-12">
type: 'Reset settings', <p>
defaultDirectory, {/* This uses a Vite plugin, set in vite.config.ts
})
toast.success('Settings restored to default')
}}
icon={{
icon: 'refresh',
size: 'sm',
className: 'p-1 text-chalkboard-10',
bgClassName: 'bg-destroy-70',
}}
>
Restore default settings
</ActionButton>
</div>
</SettingsSection>
<h2 id="settings-about" className="text-2xl mt-6 font-bold">
About Modeling App
</h2>
<div className="text-sm mb-12">
<p>
{/* This uses a Vite plugin, set in vite.config.ts
to inject the version from package.json */} to inject the version from package.json */}
App version {APP_VERSION}.{' '} App version {APP_VERSION}.{' '}
<a <a
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`} href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
View release on GitHub View release on GitHub
</a> </a>
</p> </p>
<p className="max-w-2xl mt-6"> <p className="max-w-2xl mt-6">
Don't see the feature you want? Check to see if it's on{' '} Don't see the feature you want? Check to see if it's on{' '}
<a <a
href="https://github.com/KittyCAD/modeling-app/discussions" href="https://github.com/KittyCAD/modeling-app/discussions"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
our roadmap our roadmap
</a> </a>
, and start a discussion if you don't see it! Your feedback , and start a discussion if you don't see it! Your
will help us prioritize what to build next. feedback will help us prioritize what to build next.
</p> </p>
</div>
</div> </div>
</div> </div>
</div> </div>