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>
@ -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)
|
||||||
|
@ -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')
|
||||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 73 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 47 KiB |
@ -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 () => {
|
||||||
|
127
src/App.tsx
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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) =>
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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])
|
||||||
|
@ -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;
|
|
||||||
}
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
27
src/components/ModelingSidebar/ModelingPane.module.css
Normal 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;
|
||||||
|
}
|
50
src/components/ModelingSidebar/ModelingPane.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
18
src/components/ModelingSidebar/ModelingPanes/DebugPane.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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"
|
@ -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}
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
@ -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)
|
||||||
|
|
57
src/components/ModelingSidebar/ModelingPanes/MemoryPane.tsx
Normal 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
|
||||||
|
}
|
67
src/components/ModelingSidebar/ModelingPanes/index.ts
Normal 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',
|
||||||
|
},
|
||||||
|
]
|
11
src/components/ModelingSidebar/ModelingSidebar.module.css
Normal 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;
|
||||||
|
}
|
213
src/components/ModelingSidebar/ModelingSidebar.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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'
|
||||||
|
@ -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: {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
|
16
src/hooks/useResolvedTheme.ts
Normal 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
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|