Franknoirot/cmd bar (#328)
* Add XState and naive ActionBar
* Add basic dialog and combobox
* Selectable commands in command bar
* Add a few (broken) file actions
* Home commands
* Add subcommand descriptions, cleanup on navigate
* Refactor: move command creation and types to lib
* Refactor to allow any machine to add commands
* Add auth to command bar, add ability to hide cmds
* Refactor: consolidate theme utilities
* Add settings as machine and command set
* Fix: type tweaks
* Fix: only allow auth to navigate from signin
* Remove zustand-powered settings
* Fix: remove zustand settings from App
* Fix: browser infinite redirect
* Feature: allow commands to be hidden per-platform
* Fix: tsc errors
* Fix: hide default project directory from cmd bar
* Polish: transitions, css tweaks
* Feature: label current value in options settings
* Fix broken debug panel UI
* Refactor: move settings toasts to actions
* Tweak: css rounding
* Fix: set default directory recursion and reload 🐞
* Refactor: move machines to their own directory
* Fix formatting
* @Irev-Dev clean-up catches, import cleanup
This commit is contained in:
@ -8,6 +8,7 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@headlessui/react": "^1.7.13",
|
"@headlessui/react": "^1.7.13",
|
||||||
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
"@kittycad/lib": "^0.0.34",
|
"@kittycad/lib": "^0.0.34",
|
||||||
"@react-hook/resize-observer": "^1.2.6",
|
"@react-hook/resize-observer": "^1.2.6",
|
||||||
"@tauri-apps/api": "^1.3.0",
|
"@tauri-apps/api": "^1.3.0",
|
||||||
@ -22,6 +23,7 @@
|
|||||||
"@xstate/react": "^3.2.2",
|
"@xstate/react": "^3.2.2",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"formik": "^2.4.3",
|
"formik": "^2.4.3",
|
||||||
|
"fuse.js": "^6.6.2",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"re-resizable": "^6.9.9",
|
"re-resizable": "^6.9.9",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
21
src-tauri/Cargo.lock
generated
21
src-tauri/Cargo.lock
generated
@ -1628,6 +1628,12 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minisign-verify"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@ -3022,6 +3028,7 @@ checksum = "d42ba3a2e8556722f31336a0750c10dbb6a81396a1c452977f515da83f69f842"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"attohttpc",
|
"attohttpc",
|
||||||
|
"base64 0.21.2",
|
||||||
"cocoa",
|
"cocoa",
|
||||||
"dirs-next",
|
"dirs-next",
|
||||||
"embed_plist",
|
"embed_plist",
|
||||||
@ -3034,6 +3041,7 @@ dependencies = [
|
|||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"http",
|
"http",
|
||||||
"ignore",
|
"ignore",
|
||||||
|
"minisign-verify",
|
||||||
"objc",
|
"objc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"open",
|
"open",
|
||||||
@ -3055,12 +3063,14 @@ dependencies = [
|
|||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows 0.39.0",
|
"windows 0.39.0",
|
||||||
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4228,3 +4238,14 @@ checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "0.6.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"crc32fast",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
@ -19,7 +19,7 @@ anyhow = "1"
|
|||||||
oauth2 = "4.4.1"
|
oauth2 = "4.4.1"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tauri = { version = "1.3.0", features = [ "path-all", "dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] }
|
tauri = { version = "1.3.0", features = [ "updater", "path-all", "dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] }
|
||||||
tokio = { version = "1.29.1", features = ["time"] }
|
tokio = { version = "1.29.1", features = ["time"] }
|
||||||
toml = "0.6.0"
|
toml = "0.6.0"
|
||||||
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||||
|
17
src/App.tsx
17
src/App.tsx
@ -5,6 +5,7 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useCallback,
|
useCallback,
|
||||||
MouseEventHandler,
|
MouseEventHandler,
|
||||||
|
useContext,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { DebugPanel } from './components/DebugPanel'
|
import { DebugPanel } from './components/DebugPanel'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
@ -18,7 +19,7 @@ import {
|
|||||||
lineHighlightField,
|
lineHighlightField,
|
||||||
addLineHighlight,
|
addLineHighlight,
|
||||||
} from './editor/highlightextension'
|
} from './editor/highlightextension'
|
||||||
import { PaneType, Selections, Themes, useStore } from './useStore'
|
import { PaneType, Selections, useStore } from './useStore'
|
||||||
import { Logs, KCLErrors } from './components/Logs'
|
import { Logs, KCLErrors } from './components/Logs'
|
||||||
import { CollapsiblePanel } from './components/CollapsiblePanel'
|
import { CollapsiblePanel } from './components/CollapsiblePanel'
|
||||||
import { MemoryPanel } from './components/MemoryPanel'
|
import { MemoryPanel } from './components/MemoryPanel'
|
||||||
@ -41,7 +42,7 @@ import {
|
|||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { TEST } from './env'
|
import { TEST } from './env'
|
||||||
import { getNormalisedCoordinates } from './lib/utils'
|
import { getNormalisedCoordinates } from './lib/utils'
|
||||||
import { getSystemTheme } from './lib/getSystemTheme'
|
import { Themes, getSystemTheme } from './lib/theme'
|
||||||
import { isTauri } from './lib/isTauri'
|
import { isTauri } from './lib/isTauri'
|
||||||
import { useLoaderData, useParams } from 'react-router-dom'
|
import { useLoaderData, useParams } from 'react-router-dom'
|
||||||
import { writeTextFile } from '@tauri-apps/api/fs'
|
import { writeTextFile } from '@tauri-apps/api/fs'
|
||||||
@ -49,6 +50,7 @@ import { PROJECT_ENTRYPOINT } from './lib/tauriFS'
|
|||||||
import { IndexLoaderData } from './Router'
|
import { IndexLoaderData } from './Router'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { useAuthMachine } from './hooks/useAuthMachine'
|
import { useAuthMachine } from './hooks/useAuthMachine'
|
||||||
|
import { SettingsContext } from 'components/SettingsCommandProvider'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
const { code: loadedCode, project } = useLoaderData() as IndexLoaderData
|
||||||
@ -83,11 +85,8 @@ export function App() {
|
|||||||
cmdId,
|
cmdId,
|
||||||
setCmdId,
|
setCmdId,
|
||||||
formatCode,
|
formatCode,
|
||||||
debugPanel,
|
|
||||||
theme,
|
|
||||||
openPanes,
|
openPanes,
|
||||||
setOpenPanes,
|
setOpenPanes,
|
||||||
onboardingStatus,
|
|
||||||
didDragInStream,
|
didDragInStream,
|
||||||
setDidDragInStream,
|
setDidDragInStream,
|
||||||
setStreamDimensions,
|
setStreamDimensions,
|
||||||
@ -122,17 +121,17 @@ export function App() {
|
|||||||
cmdId: s.cmdId,
|
cmdId: s.cmdId,
|
||||||
setCmdId: s.setCmdId,
|
setCmdId: s.setCmdId,
|
||||||
formatCode: s.formatCode,
|
formatCode: s.formatCode,
|
||||||
debugPanel: s.debugPanel,
|
|
||||||
addKCLError: s.addKCLError,
|
addKCLError: s.addKCLError,
|
||||||
theme: s.theme,
|
|
||||||
openPanes: s.openPanes,
|
openPanes: s.openPanes,
|
||||||
setOpenPanes: s.setOpenPanes,
|
setOpenPanes: s.setOpenPanes,
|
||||||
onboardingStatus: s.onboardingStatus,
|
|
||||||
didDragInStream: s.didDragInStream,
|
didDragInStream: s.didDragInStream,
|
||||||
setDidDragInStream: s.setDidDragInStream,
|
setDidDragInStream: s.setDidDragInStream,
|
||||||
setStreamDimensions: s.setStreamDimensions,
|
setStreamDimensions: s.setStreamDimensions,
|
||||||
streamDimensions: s.streamDimensions,
|
streamDimensions: s.streamDimensions,
|
||||||
}))
|
}))
|
||||||
|
const { showDebugPanel, theme, onboardingStatus } =
|
||||||
|
useContext(SettingsContext)
|
||||||
|
|
||||||
const [token] = useAuthMachine((s) => s?.context?.token)
|
const [token] = useAuthMachine((s) => s?.context?.token)
|
||||||
|
|
||||||
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||||
@ -510,7 +509,7 @@ export function App() {
|
|||||||
</div>
|
</div>
|
||||||
</Resizable>
|
</Resizable>
|
||||||
<Stream className="absolute inset-0 z-0" />
|
<Stream className="absolute inset-0 z-0" />
|
||||||
{debugPanel && (
|
{showDebugPanel && (
|
||||||
<DebugPanel
|
<DebugPanel
|
||||||
title="Debug"
|
title="Debug"
|
||||||
className={
|
className={
|
||||||
|
@ -24,7 +24,16 @@ import {
|
|||||||
} from './lib/tauriFS'
|
} from './lib/tauriFS'
|
||||||
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
|
import { metadata, type Metadata } from 'tauri-plugin-fs-extra-api'
|
||||||
import DownloadAppBanner from './components/DownloadAppBanner'
|
import DownloadAppBanner from './components/DownloadAppBanner'
|
||||||
import { GlobalStateProvider } from './hooks/useAuthMachine'
|
import {
|
||||||
|
AuthMachineCommandProvider,
|
||||||
|
GlobalStateProvider,
|
||||||
|
} from './hooks/useAuthMachine'
|
||||||
|
import SettingsCommandProvider from './components/SettingsCommandProvider'
|
||||||
|
import {
|
||||||
|
SETTINGS_PERSIST_KEY,
|
||||||
|
settingsMachine,
|
||||||
|
} from './machines/settingsMachine'
|
||||||
|
import { ContextFrom } from 'xstate'
|
||||||
|
|
||||||
const prependRoutes =
|
const prependRoutes =
|
||||||
(routesObject: Record<string, string>) => (prepend: string) => {
|
(routesObject: Record<string, string>) => (prepend: string) => {
|
||||||
@ -68,7 +77,15 @@ const addGlobalContextToElements = (
|
|||||||
'element' in route
|
'element' in route
|
||||||
? {
|
? {
|
||||||
...route,
|
...route,
|
||||||
element: <GlobalStateProvider>{route.element}</GlobalStateProvider>,
|
element: (
|
||||||
|
<GlobalStateProvider>
|
||||||
|
<AuthMachineCommandProvider>
|
||||||
|
<SettingsCommandProvider>
|
||||||
|
{route.element}
|
||||||
|
</SettingsCommandProvider>
|
||||||
|
</AuthMachineCommandProvider>
|
||||||
|
</GlobalStateProvider>
|
||||||
|
),
|
||||||
}
|
}
|
||||||
: route
|
: route
|
||||||
)
|
)
|
||||||
@ -95,26 +112,23 @@ const router = createBrowserRouter(
|
|||||||
request,
|
request,
|
||||||
params,
|
params,
|
||||||
}): Promise<IndexLoaderData | Response> => {
|
}): Promise<IndexLoaderData | Response> => {
|
||||||
const store = localStorage.getItem('store')
|
const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY)
|
||||||
if (store === null) {
|
const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial<
|
||||||
return redirect(paths.ONBOARDING.INDEX)
|
ContextFrom<typeof settingsMachine>
|
||||||
} else {
|
>
|
||||||
const status = JSON.parse(store).state.onboardingStatus || ''
|
|
||||||
const notEnRouteToOnboarding =
|
|
||||||
!request.url.includes(paths.ONBOARDING.INDEX) &&
|
|
||||||
request.method === 'GET'
|
|
||||||
// '' is the initial state, 'done' and 'dismissed' are the final states
|
|
||||||
const hasValidOnboardingStatus =
|
|
||||||
(status !== undefined && status.length === 0) ||
|
|
||||||
!(status === 'done' || status === 'dismissed')
|
|
||||||
const shouldRedirectToOnboarding =
|
|
||||||
notEnRouteToOnboarding && hasValidOnboardingStatus
|
|
||||||
|
|
||||||
if (shouldRedirectToOnboarding) {
|
const status = persistedSettings.onboardingStatus || ''
|
||||||
return redirect(
|
const notEnRouteToOnboarding = !request.url.includes(
|
||||||
makeUrlPathRelative(paths.ONBOARDING.INDEX) + status
|
paths.ONBOARDING.INDEX
|
||||||
)
|
)
|
||||||
}
|
// '' is the initial state, 'done' and 'dismissed' are the final states
|
||||||
|
const hasValidOnboardingStatus =
|
||||||
|
status.length === 0 || !(status === 'done' || status === 'dismissed')
|
||||||
|
const shouldRedirectToOnboarding =
|
||||||
|
notEnRouteToOnboarding && hasValidOnboardingStatus
|
||||||
|
|
||||||
|
if (shouldRedirectToOnboarding) {
|
||||||
|
return redirect(makeUrlPathRelative(paths.ONBOARDING.INDEX) + status)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.id && params.id !== 'new') {
|
if (params.id && params.id !== 'new') {
|
||||||
@ -164,9 +178,23 @@ const router = createBrowserRouter(
|
|||||||
if (!isTauri()) {
|
if (!isTauri()) {
|
||||||
return redirect(paths.FILE + '/new')
|
return redirect(paths.FILE + '/new')
|
||||||
}
|
}
|
||||||
|
const fetchedStorage = localStorage?.getItem(SETTINGS_PERSIST_KEY)
|
||||||
const projectDir = await initializeProjectDirectory()
|
const persistedSettings = JSON.parse(fetchedStorage || '{}') as Partial<
|
||||||
const projectsNoMeta = (await readDir(projectDir.dir)).filter(
|
ContextFrom<typeof settingsMachine>
|
||||||
|
>
|
||||||
|
const projectDir = await initializeProjectDirectory(
|
||||||
|
persistedSettings.defaultDirectory || ''
|
||||||
|
)
|
||||||
|
if (projectDir !== persistedSettings.defaultDirectory) {
|
||||||
|
localStorage.setItem(
|
||||||
|
SETTINGS_PERSIST_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
...persistedSettings,
|
||||||
|
defaultDirectory: projectDir,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const projectsNoMeta = (await readDir(projectDir)).filter(
|
||||||
isProjectDirectory
|
isProjectDirectory
|
||||||
)
|
)
|
||||||
const projects = await Promise.all(
|
const projects = await Promise.all(
|
||||||
|
@ -8,11 +8,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|||||||
const iconSizes = {
|
const iconSizes = {
|
||||||
sm: 12,
|
sm: 12,
|
||||||
md: 14.4,
|
md: 14.4,
|
||||||
lg: 18,
|
lg: 20,
|
||||||
|
xl: 28,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionIconProps extends React.PropsWithChildren {
|
export interface ActionIconProps extends React.PropsWithChildren {
|
||||||
icon?: SolidIconDefinition | BrandIconDefinition
|
icon?: SolidIconDefinition | BrandIconDefinition
|
||||||
|
className?: string
|
||||||
bgClassName?: string
|
bgClassName?: string
|
||||||
iconClassName?: string
|
iconClassName?: string
|
||||||
size?: keyof typeof iconSizes
|
size?: keyof typeof iconSizes
|
||||||
@ -20,6 +22,7 @@ export interface ActionIconProps extends React.PropsWithChildren {
|
|||||||
|
|
||||||
export const ActionIcon = ({
|
export const ActionIcon = ({
|
||||||
icon = faCircleExclamation,
|
icon = faCircleExclamation,
|
||||||
|
className,
|
||||||
bgClassName,
|
bgClassName,
|
||||||
iconClassName,
|
iconClassName,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
@ -28,7 +31,9 @@ export const ActionIcon = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'p-1 w-fit inline-grid place-content-center ' +
|
`p-${
|
||||||
|
size === 'xl' ? '2' : '1'
|
||||||
|
} w-fit inline-grid place-content-center ${className} ` +
|
||||||
(bgClassName ||
|
(bgClassName ||
|
||||||
'bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-liquid-20 dark:group-hover:bg-liquid-10 dark:hover:bg-liquid-10')
|
'bg-chalkboard-100 group-hover:bg-chalkboard-90 hover:bg-chalkboard-90 dark:bg-liquid-20 dark:group-hover:bg-liquid-10 dark:hover:bg-liquid-10')
|
||||||
}
|
}
|
||||||
@ -40,7 +45,7 @@ export const ActionIcon = ({
|
|||||||
height={iconSizes[size]}
|
height={iconSizes[size]}
|
||||||
className={
|
className={
|
||||||
iconClassName ||
|
iconClassName ||
|
||||||
'text-liquid-20 group-hover:text-liquid-10 hover:text-liquid-10 dark:text-liquid-100 dark:group-hover:text-liquid-100 dark:hover:text-liquid-100'
|
'text-liquid-20 h-auto group-hover:text-liquid-10 hover:text-liquid-10 dark:text-liquid-100 dark:group-hover:text-liquid-100 dark:hover:text-liquid-100'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
258
src/components/CommandBar.tsx
Normal file
258
src/components/CommandBar.tsx
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
import { Combobox, Dialog, Transition } from '@headlessui/react'
|
||||||
|
import {
|
||||||
|
Dispatch,
|
||||||
|
Fragment,
|
||||||
|
SetStateAction,
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import { ActionIcon } from './ActionIcon'
|
||||||
|
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
import { Command, SubCommand } from '../lib/commands'
|
||||||
|
|
||||||
|
export type SortedCommand = {
|
||||||
|
item: Partial<Command | SubCommand> & { name: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommandsContext = createContext(
|
||||||
|
{} as {
|
||||||
|
commands: Command[]
|
||||||
|
addCommands: (commands: Command[]) => void
|
||||||
|
removeCommands: (commands: Command[]) => void
|
||||||
|
commandBarOpen: boolean
|
||||||
|
setCommandBarOpen: Dispatch<SetStateAction<boolean>>
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const CommandBar = () => {
|
||||||
|
const { commands, commandBarOpen, setCommandBarOpen } =
|
||||||
|
useContext(CommandsContext)
|
||||||
|
useHotkeys('meta+k', () => {
|
||||||
|
if (commands.length === 0) return
|
||||||
|
setCommandBarOpen(!commandBarOpen)
|
||||||
|
})
|
||||||
|
|
||||||
|
const [selectedCommand, setSelectedCommand] = useState<SortedCommand | null>(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
// keep track of the current subcommand index
|
||||||
|
const [subCommandIndex, setSubCommandIndex] = useState<number>()
|
||||||
|
const [subCommandData, setSubCommandData] = useState<{
|
||||||
|
[key: string]: string
|
||||||
|
}>({})
|
||||||
|
|
||||||
|
// if the subcommand index is null, we're not in a subcommand
|
||||||
|
const inSubCommand =
|
||||||
|
selectedCommand &&
|
||||||
|
'meta' in selectedCommand.item &&
|
||||||
|
selectedCommand.item.meta?.args !== undefined &&
|
||||||
|
subCommandIndex !== undefined
|
||||||
|
const currentSubCommand =
|
||||||
|
inSubCommand && 'meta' in selectedCommand.item
|
||||||
|
? selectedCommand.item.meta?.args[subCommandIndex]
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
|
||||||
|
const availableCommands =
|
||||||
|
inSubCommand && currentSubCommand
|
||||||
|
? currentSubCommand.type === 'string'
|
||||||
|
? query
|
||||||
|
? [{ name: query }]
|
||||||
|
: currentSubCommand.options
|
||||||
|
: currentSubCommand.options
|
||||||
|
: commands
|
||||||
|
|
||||||
|
const fuse = new Fuse(availableCommands || [], {
|
||||||
|
keys: ['name', 'description'],
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredCommands = query
|
||||||
|
? fuse.search(query)
|
||||||
|
: availableCommands?.map((c) => ({ item: c } as SortedCommand))
|
||||||
|
|
||||||
|
function clearState() {
|
||||||
|
setQuery('')
|
||||||
|
setCommandBarOpen(false)
|
||||||
|
setSelectedCommand(null)
|
||||||
|
setSubCommandIndex(undefined)
|
||||||
|
setSubCommandData({})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCommandSelection(entry: SortedCommand) {
|
||||||
|
// If we have subcommands and have not yet gathered all the
|
||||||
|
// data required from them, set the selected command to the
|
||||||
|
// current command and increment the subcommand index
|
||||||
|
if (selectedCommand === null && 'meta' in entry.item && entry.item.meta) {
|
||||||
|
setSelectedCommand(entry)
|
||||||
|
setSubCommandIndex(0)
|
||||||
|
setQuery('')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { item } = entry
|
||||||
|
// If we have just selected a command with no subcommands, run it
|
||||||
|
const isCommandWithoutSubcommands =
|
||||||
|
'callback' in item && !('meta' in item && item.meta)
|
||||||
|
if (isCommandWithoutSubcommands) {
|
||||||
|
if (item.callback === undefined) return
|
||||||
|
item.callback()
|
||||||
|
setCommandBarOpen(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have subcommands and have not yet gathered all the
|
||||||
|
// data required from them, set the selected command to the
|
||||||
|
// current command and increment the subcommand index
|
||||||
|
if (
|
||||||
|
selectedCommand &&
|
||||||
|
subCommandIndex !== undefined &&
|
||||||
|
'meta' in selectedCommand.item
|
||||||
|
) {
|
||||||
|
const subCommand = selectedCommand.item.meta?.args[subCommandIndex]
|
||||||
|
|
||||||
|
if (subCommand) {
|
||||||
|
const newSubCommandData = {
|
||||||
|
...subCommandData,
|
||||||
|
[subCommand.name]: item.name,
|
||||||
|
}
|
||||||
|
const newSubCommandIndex = subCommandIndex + 1
|
||||||
|
|
||||||
|
// If we have subcommands and have gathered all the data required
|
||||||
|
// from them, run the command with the gathered data
|
||||||
|
if (
|
||||||
|
selectedCommand.item.callback &&
|
||||||
|
selectedCommand.item.meta?.args.length === newSubCommandIndex
|
||||||
|
) {
|
||||||
|
selectedCommand.item.callback(newSubCommandData)
|
||||||
|
setCommandBarOpen(false)
|
||||||
|
} else {
|
||||||
|
// Otherwise, set the subcommand data and increment the subcommand index
|
||||||
|
setSubCommandData(newSubCommandData)
|
||||||
|
setSubCommandIndex(newSubCommandIndex)
|
||||||
|
setQuery('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayValue(command: Command) {
|
||||||
|
if (command.meta?.displayValue === undefined || !command.meta.args)
|
||||||
|
return command.name
|
||||||
|
return command.meta?.displayValue(
|
||||||
|
command.meta.args.map((c) =>
|
||||||
|
subCommandData[c.name] ? subCommandData[c.name] : `<${c.name}>`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root
|
||||||
|
show={
|
||||||
|
commandBarOpen &&
|
||||||
|
availableCommands?.length !== undefined &&
|
||||||
|
availableCommands.length > 0
|
||||||
|
}
|
||||||
|
as={Fragment}
|
||||||
|
afterLeave={() => clearState()}
|
||||||
|
>
|
||||||
|
<Dialog
|
||||||
|
onClose={() => {
|
||||||
|
setCommandBarOpen(false)
|
||||||
|
clearState()
|
||||||
|
}}
|
||||||
|
className="fixed inset-0 overflow-y-auto p-4 pt-[25vh]"
|
||||||
|
>
|
||||||
|
<Transition.Child
|
||||||
|
enter="duration-100 ease-out"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="duration-75 ease-in"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
as={Fragment}
|
||||||
|
>
|
||||||
|
<Dialog.Overlay className="fixed z-40 inset-0 bg-chalkboard-10/70 dark:bg-chalkboard-110/50" />
|
||||||
|
</Transition.Child>
|
||||||
|
<Transition.Child
|
||||||
|
enter="duration-100 ease-out"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="duration-75 ease-in"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
as={Fragment}
|
||||||
|
>
|
||||||
|
<Combobox
|
||||||
|
value={selectedCommand}
|
||||||
|
onChange={handleCommandSelection}
|
||||||
|
className="rounded relative mx-auto z-40 p-2 bg-chalkboard-10 dark:bg-chalkboard-100 border dark:border-chalkboard-70 max-w-xl w-full shadow-lg"
|
||||||
|
as="div"
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<ActionIcon icon={faSearch} size="xl" className="rounded-sm" />
|
||||||
|
<div>
|
||||||
|
{inSubCommand && (
|
||||||
|
<p className="text-liquid-70 dark:text-liquid-30">
|
||||||
|
{selectedCommand.item &&
|
||||||
|
getDisplayValue(selectedCommand.item as Command)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<Combobox.Input
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
className="bg-transparent focus:outline-none w-full"
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.metaKey && event.key === 'k')
|
||||||
|
setCommandBarOpen(false)
|
||||||
|
if (
|
||||||
|
inSubCommand &&
|
||||||
|
event.key === 'Backspace' &&
|
||||||
|
!event.currentTarget.value
|
||||||
|
) {
|
||||||
|
setSubCommandIndex(subCommandIndex - 1)
|
||||||
|
setSelectedCommand(null)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
displayValue={(command: SortedCommand) =>
|
||||||
|
command !== null ? command.item.name : ''
|
||||||
|
}
|
||||||
|
placeholder={
|
||||||
|
inSubCommand
|
||||||
|
? `Enter <${currentSubCommand?.name}>`
|
||||||
|
: 'Search for a command'
|
||||||
|
}
|
||||||
|
value={query}
|
||||||
|
autoCapitalize="off"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
spellCheck="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Combobox.Options static className="max-h-96 overflow-y-auto">
|
||||||
|
{filteredCommands?.map((commandResult) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={commandResult.item.name}
|
||||||
|
value={commandResult}
|
||||||
|
className="my-2 first:mt-4 last:mb-4 ui-active:bg-liquid-10 dark:ui-active:bg-liquid-90 py-1 px-2"
|
||||||
|
>
|
||||||
|
<p>{commandResult.item.name}</p>
|
||||||
|
{(commandResult.item as SubCommand).description && (
|
||||||
|
<p className="mt-0.5 text-liquid-70 dark:text-liquid-30 text-sm">
|
||||||
|
{(commandResult.item as SubCommand).description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</Combobox.Options>
|
||||||
|
</Combobox>
|
||||||
|
</Transition.Child>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandBar
|
@ -1,7 +1,8 @@
|
|||||||
import ReactJson from 'react-json-view'
|
import ReactJson from 'react-json-view'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { Themes, useStore } from '../useStore'
|
import { useStore } from '../useStore'
|
||||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||||
|
import { Themes } from '../lib/theme'
|
||||||
|
|
||||||
const ReactJsonTypeHack = ReactJson as any
|
const ReactJsonTypeHack = ReactJson as any
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import ReactJson from 'react-json-view'
|
import ReactJson from 'react-json-view'
|
||||||
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
|
||||||
import { Themes, useStore } from '../useStore'
|
import { useStore } from '../useStore'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { ProgramMemory } from '../lang/executor'
|
import { ProgramMemory } from '../lang/executor'
|
||||||
|
import { Themes } from '../lib/theme'
|
||||||
|
|
||||||
interface MemoryPanelProps extends CollapsiblePanelProps {
|
interface MemoryPanelProps extends CollapsiblePanelProps {
|
||||||
theme?: Exclude<Themes, Themes.System>
|
theme?: Exclude<Themes, Themes.System>
|
||||||
|
76
src/components/SettingsCommandProvider.tsx
Normal file
76
src/components/SettingsCommandProvider.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import {
|
||||||
|
SETTINGS_PERSIST_KEY,
|
||||||
|
settingsCommandBarMeta,
|
||||||
|
settingsMachine,
|
||||||
|
} from '../machines/settingsMachine'
|
||||||
|
import { useMachine } from '@xstate/react'
|
||||||
|
import { CommandsContext } from './CommandBar'
|
||||||
|
import { createContext, useContext, useEffect, useRef } from 'react'
|
||||||
|
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||||
|
import { setThemeClass } from '../lib/theme'
|
||||||
|
import { ContextFrom, InterpreterFrom, Prop } from 'xstate'
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
|
||||||
|
export const SettingsContext = createContext(
|
||||||
|
{} as {
|
||||||
|
send: Prop<InterpreterFrom<typeof settingsMachine>, 'send'>
|
||||||
|
} & ContextFrom<typeof settingsMachine>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default function SettingsCommandProvider({
|
||||||
|
children,
|
||||||
|
}: React.PropsWithChildren) {
|
||||||
|
const retrievedSettings = useRef(
|
||||||
|
localStorage?.getItem(SETTINGS_PERSIST_KEY) || '{}'
|
||||||
|
)
|
||||||
|
const persistedSettings = Object.assign(
|
||||||
|
settingsMachine.initialState.context,
|
||||||
|
JSON.parse(retrievedSettings.current) as Partial<
|
||||||
|
(typeof settingsMachine)['context']
|
||||||
|
>
|
||||||
|
)
|
||||||
|
|
||||||
|
const [state, send] = useMachine(settingsMachine, {
|
||||||
|
context: persistedSettings,
|
||||||
|
actions: {
|
||||||
|
toastSuccess: (context, event) => {
|
||||||
|
const truncatedNewValue =
|
||||||
|
'data' in event && event.data instanceof Object
|
||||||
|
? (context[Object.keys(event.data)[0] as keyof typeof context]
|
||||||
|
.toString()
|
||||||
|
.substring(0, 28) as any)
|
||||||
|
: undefined
|
||||||
|
toast.success(
|
||||||
|
event.type +
|
||||||
|
(truncatedNewValue
|
||||||
|
? ` to "${truncatedNewValue}${
|
||||||
|
truncatedNewValue.length === 28 ? '...' : ''
|
||||||
|
}"`
|
||||||
|
: '')
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { commands } = useContext(CommandsContext)
|
||||||
|
|
||||||
|
useStateMachineCommands({
|
||||||
|
state,
|
||||||
|
send,
|
||||||
|
commands,
|
||||||
|
owner: 'settings',
|
||||||
|
commandBarMeta: settingsCommandBarMeta,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => setThemeClass(state.context.theme), [state.context.theme])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsContext.Provider
|
||||||
|
value={{
|
||||||
|
send,
|
||||||
|
...state.context,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SettingsContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
@ -120,7 +120,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
|||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() => send('logout')}
|
onClick={() => send('Log out')}
|
||||||
icon={{
|
icon={{
|
||||||
icon: faSignOutAlt,
|
icon: faSignOutAlt,
|
||||||
bgClassName: 'bg-destroy-80',
|
bgClassName: 'bg-destroy-80',
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
import { createActorContext } from '@xstate/react'
|
import { createActorContext } from '@xstate/react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { paths } from '../Router'
|
import { paths } from '../Router'
|
||||||
import { authMachine, TOKEN_PERSIST_KEY } from '../lib/authMachine'
|
import {
|
||||||
|
authCommandBarMeta,
|
||||||
|
authMachine,
|
||||||
|
TOKEN_PERSIST_KEY,
|
||||||
|
} from '../machines/authMachine'
|
||||||
import withBaseUrl from '../lib/withBaseURL'
|
import withBaseUrl from '../lib/withBaseURL'
|
||||||
|
import React, { useContext, useState } from 'react'
|
||||||
|
import CommandBar, { CommandsContext } from '../components/CommandBar'
|
||||||
|
import { Command } from '../lib/commands'
|
||||||
|
import useStateMachineCommands from './useStateMachineCommands'
|
||||||
|
|
||||||
export const AuthMachineContext = createActorContext(authMachine)
|
export const AuthMachineContext = createActorContext(authMachine)
|
||||||
|
|
||||||
@ -11,7 +19,19 @@ export const GlobalStateProvider = ({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
|
const [commands, internalSetCommands] = useState([] as Command[])
|
||||||
|
const [commandBarOpen, setCommandBarOpen] = useState(false)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const addCommands = (newCommands: Command[]) => {
|
||||||
|
internalSetCommands((prevCommands) => [...newCommands, ...prevCommands])
|
||||||
|
}
|
||||||
|
const removeCommands = (newCommands: Command[]) => {
|
||||||
|
internalSetCommands((prevCommands) =>
|
||||||
|
prevCommands.filter((command) => !newCommands.includes(command))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthMachineContext.Provider
|
<AuthMachineContext.Provider
|
||||||
machine={() =>
|
machine={() =>
|
||||||
@ -21,12 +41,27 @@ export const GlobalStateProvider = ({
|
|||||||
navigate(paths.SIGN_IN)
|
navigate(paths.SIGN_IN)
|
||||||
logout()
|
logout()
|
||||||
},
|
},
|
||||||
goToIndexPage: () => navigate(paths.INDEX),
|
goToIndexPage: () => {
|
||||||
|
if (window.location.pathname.includes(paths.SIGN_IN)) {
|
||||||
|
navigate(paths.INDEX)
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{children}
|
<CommandsContext.Provider
|
||||||
|
value={{
|
||||||
|
commands,
|
||||||
|
addCommands,
|
||||||
|
removeCommands,
|
||||||
|
commandBarOpen,
|
||||||
|
setCommandBarOpen,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<CommandBar />
|
||||||
|
</CommandsContext.Provider>
|
||||||
</AuthMachineContext.Provider>
|
</AuthMachineContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -52,3 +87,18 @@ export function logout() {
|
|||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function AuthMachineCommandProvider(props: React.PropsWithChildren<{}>) {
|
||||||
|
const [state, send] = AuthMachineContext.useActor()
|
||||||
|
const { commands } = useContext(CommandsContext)
|
||||||
|
|
||||||
|
useStateMachineCommands({
|
||||||
|
state,
|
||||||
|
send,
|
||||||
|
commands,
|
||||||
|
commandBarMeta: authCommandBarMeta,
|
||||||
|
owner: 'auth',
|
||||||
|
})
|
||||||
|
|
||||||
|
return <>{props.children}</>
|
||||||
|
}
|
||||||
|
42
src/hooks/useStateMachineCommands.ts
Normal file
42
src/hooks/useStateMachineCommands.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { useContext, useEffect } from 'react'
|
||||||
|
import { AnyStateMachine, StateFrom } from 'xstate'
|
||||||
|
import { Command, CommandBarMeta, createMachineCommand } from '../lib/commands'
|
||||||
|
import { CommandsContext } from '../components/CommandBar'
|
||||||
|
|
||||||
|
interface UseStateMachineCommandsArgs<T extends AnyStateMachine> {
|
||||||
|
state: StateFrom<T>
|
||||||
|
send: Function
|
||||||
|
commandBarMeta?: CommandBarMeta
|
||||||
|
commands: Command[]
|
||||||
|
owner: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useStateMachineCommands<T extends AnyStateMachine>({
|
||||||
|
state,
|
||||||
|
send,
|
||||||
|
commandBarMeta,
|
||||||
|
owner,
|
||||||
|
}: UseStateMachineCommandsArgs<T>) {
|
||||||
|
const { addCommands, removeCommands } = useContext(CommandsContext)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newCommands = state.nextEvents
|
||||||
|
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
|
||||||
|
.map((type) =>
|
||||||
|
createMachineCommand<T>({
|
||||||
|
type,
|
||||||
|
state,
|
||||||
|
send,
|
||||||
|
commandBarMeta,
|
||||||
|
owner,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.filter((c) => c !== null) as Command[]
|
||||||
|
|
||||||
|
addCommands(newCommands)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
removeCommands(newCommands)
|
||||||
|
}
|
||||||
|
}, [state])
|
||||||
|
}
|
@ -1,67 +0,0 @@
|
|||||||
import { useEffect } from 'react'
|
|
||||||
import { useStore } from '../useStore'
|
|
||||||
import { parse } from 'toml'
|
|
||||||
import {
|
|
||||||
createDir,
|
|
||||||
BaseDirectory,
|
|
||||||
readDir,
|
|
||||||
readTextFile,
|
|
||||||
} from '@tauri-apps/api/fs'
|
|
||||||
|
|
||||||
export const useTauriBoot = () => {
|
|
||||||
const { defaultDir, setDefaultDir, setHomeMenuItems } = useStore((s) => ({
|
|
||||||
defaultDir: s.defaultDir,
|
|
||||||
setDefaultDir: s.setDefaultDir,
|
|
||||||
setHomeMenuItems: s.setHomeMenuItems,
|
|
||||||
}))
|
|
||||||
useEffect(() => {
|
|
||||||
const isTauri = (window as any).__TAURI__
|
|
||||||
if (!isTauri) return
|
|
||||||
const run = async () => {
|
|
||||||
if (!defaultDir.base) {
|
|
||||||
createDir('puffin-projects/example', {
|
|
||||||
dir: BaseDirectory.Home,
|
|
||||||
recursive: true,
|
|
||||||
})
|
|
||||||
setDefaultDir({
|
|
||||||
base: BaseDirectory.Home,
|
|
||||||
dir: 'puffin-projects',
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const directoryResult = await readDir(defaultDir.dir, {
|
|
||||||
dir: defaultDir.base,
|
|
||||||
recursive: true,
|
|
||||||
})
|
|
||||||
const puffinProjects = directoryResult.filter(
|
|
||||||
(file) =>
|
|
||||||
!file?.name?.startsWith('.') &&
|
|
||||||
file?.children?.find((child) => child?.name === 'wax.toml')
|
|
||||||
)
|
|
||||||
|
|
||||||
const tomlFiles = await Promise.all(
|
|
||||||
puffinProjects.map(async (file) => {
|
|
||||||
const parsedToml = parse(
|
|
||||||
await readTextFile(`${file.path}/wax.toml`, {
|
|
||||||
dir: defaultDir.base,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
const mainPath = parsedToml?.package?.main
|
|
||||||
const projectName = parsedToml?.package?.name
|
|
||||||
return {
|
|
||||||
file,
|
|
||||||
mainPath,
|
|
||||||
projectName,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
setHomeMenuItems(
|
|
||||||
tomlFiles.map(({ file, mainPath, projectName }) => ({
|
|
||||||
name: projectName,
|
|
||||||
path: mainPath ? `${file.path}/${mainPath}` : file.path,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
run()
|
|
||||||
}, [])
|
|
||||||
}
|
|
@ -2,23 +2,10 @@ import ReactDOM from 'react-dom/client'
|
|||||||
import './index.css'
|
import './index.css'
|
||||||
import reportWebVitals from './reportWebVitals'
|
import reportWebVitals from './reportWebVitals'
|
||||||
import { Toaster } from 'react-hot-toast'
|
import { Toaster } from 'react-hot-toast'
|
||||||
import { Themes, useStore } from './useStore'
|
|
||||||
import { Router } from './Router'
|
import { Router } from './Router'
|
||||||
import { HotkeysProvider } from 'react-hotkeys-hook'
|
import { HotkeysProvider } from 'react-hotkeys-hook'
|
||||||
import { getSystemTheme } from './lib/getSystemTheme'
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
||||||
function setThemeClass(state: Partial<{ theme: Themes }>) {
|
|
||||||
const systemTheme = state.theme === Themes.System && getSystemTheme()
|
|
||||||
if (state.theme === Themes.Dark || systemTheme === Themes.Dark) {
|
|
||||||
document.body.classList.add('dark')
|
|
||||||
} else {
|
|
||||||
document.body.classList.remove('dark')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { theme } = useStore.getState()
|
|
||||||
setThemeClass({ theme })
|
|
||||||
useStore.subscribe(setThemeClass)
|
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<HotkeysProvider>
|
<HotkeysProvider>
|
||||||
|
126
src/lib/commands.ts
Normal file
126
src/lib/commands.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { AnyStateMachine, EventFrom, StateFrom } from 'xstate'
|
||||||
|
import { isTauri } from './isTauri'
|
||||||
|
|
||||||
|
type InitialCommandBarMetaArg = {
|
||||||
|
name: string
|
||||||
|
type: 'string' | 'select'
|
||||||
|
description?: string
|
||||||
|
defaultValue?: string
|
||||||
|
options: string | Array<{ name: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Platform = 'both' | 'web' | 'desktop'
|
||||||
|
|
||||||
|
export type CommandBarMeta = {
|
||||||
|
[key: string]:
|
||||||
|
| {
|
||||||
|
displayValue: (args: string[]) => string
|
||||||
|
args: InitialCommandBarMetaArg[]
|
||||||
|
hide?: Platform
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
hide?: Platform
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Command = {
|
||||||
|
owner: string
|
||||||
|
name: string
|
||||||
|
callback: Function
|
||||||
|
meta?: {
|
||||||
|
displayValue(args: string[]): string | string
|
||||||
|
args: SubCommand[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SubCommand = {
|
||||||
|
name: string
|
||||||
|
type: 'select' | 'string'
|
||||||
|
description?: string
|
||||||
|
options?: Partial<{ name: string }>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommandBarArgs<T extends AnyStateMachine> {
|
||||||
|
type: EventFrom<T>['type']
|
||||||
|
state: StateFrom<T>
|
||||||
|
commandBarMeta?: CommandBarMeta
|
||||||
|
send: Function
|
||||||
|
owner: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMachineCommand<T extends AnyStateMachine>({
|
||||||
|
type,
|
||||||
|
state,
|
||||||
|
commandBarMeta,
|
||||||
|
send,
|
||||||
|
owner,
|
||||||
|
}: CommandBarArgs<T>): Command | null {
|
||||||
|
const lookedUpMeta = commandBarMeta && commandBarMeta[type]
|
||||||
|
if (lookedUpMeta && 'hide' in lookedUpMeta) {
|
||||||
|
const { hide } = lookedUpMeta
|
||||||
|
if (hide === 'both') return null
|
||||||
|
else if (hide === 'desktop' && isTauri()) return null
|
||||||
|
else if (hide === 'web' && !isTauri()) return null
|
||||||
|
}
|
||||||
|
let replacedArgs
|
||||||
|
|
||||||
|
if (lookedUpMeta && 'args' in lookedUpMeta) {
|
||||||
|
replacedArgs = lookedUpMeta.args.map((arg) => {
|
||||||
|
const optionsFromContext = state.context[
|
||||||
|
arg.options as keyof typeof state.context
|
||||||
|
] as { name: string }[] | string | undefined
|
||||||
|
const defaultValueFromContext = state.context[
|
||||||
|
arg.defaultValue as keyof typeof state.context
|
||||||
|
] as string | undefined
|
||||||
|
|
||||||
|
console.log(arg.name, { defaultValueFromContext })
|
||||||
|
|
||||||
|
const options =
|
||||||
|
arg.options instanceof Array
|
||||||
|
? arg.options.map((o) => ({
|
||||||
|
...o,
|
||||||
|
description:
|
||||||
|
defaultValueFromContext === o.name ? '(current)' : '',
|
||||||
|
}))
|
||||||
|
: !optionsFromContext || typeof optionsFromContext === 'string'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: optionsFromContext,
|
||||||
|
description: arg.description || '',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: optionsFromContext.map((o) => ({
|
||||||
|
name: o.name || '',
|
||||||
|
description: arg.description || '',
|
||||||
|
}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
...arg,
|
||||||
|
options,
|
||||||
|
}
|
||||||
|
}) as any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have to recreate this object every time,
|
||||||
|
// otherwise we'll have stale state in the CommandBar
|
||||||
|
// after completing our first action
|
||||||
|
const meta = lookedUpMeta
|
||||||
|
? {
|
||||||
|
...lookedUpMeta,
|
||||||
|
args: replacedArgs,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: type,
|
||||||
|
owner,
|
||||||
|
callback: (data: EventFrom<T, typeof type>) => {
|
||||||
|
if (data !== undefined && data !== null) {
|
||||||
|
send(type, { data })
|
||||||
|
} else {
|
||||||
|
send(type)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
meta: meta as any,
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +0,0 @@
|
|||||||
import { Themes } from '../useStore'
|
|
||||||
|
|
||||||
export function getSystemTheme(): Exclude<Themes, 'system'> {
|
|
||||||
return typeof window !== 'undefined' &&
|
|
||||||
'matchMedia' in window &&
|
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
||||||
? Themes.Dark
|
|
||||||
: Themes.Light
|
|
||||||
}
|
|
64
src/lib/sorting.ts
Normal file
64
src/lib/sorting.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
faArrowDown,
|
||||||
|
faArrowUp,
|
||||||
|
faCircleDot,
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||||
|
|
||||||
|
const DESC = ':desc'
|
||||||
|
|
||||||
|
export function getSortIcon(currentSort: string, newSort: string) {
|
||||||
|
if (currentSort === newSort) {
|
||||||
|
return faArrowUp
|
||||||
|
} else if (currentSort === newSort + DESC) {
|
||||||
|
return faArrowDown
|
||||||
|
}
|
||||||
|
return faCircleDot
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNextSearchParams(currentSort: string, newSort: string) {
|
||||||
|
if (currentSort === null || !currentSort)
|
||||||
|
return { sort_by: newSort + (newSort !== 'modified' ? DESC : '') }
|
||||||
|
if (currentSort.includes(newSort) && !currentSort.includes(DESC))
|
||||||
|
return { sort_by: '' }
|
||||||
|
return {
|
||||||
|
sort_by: newSort + (currentSort.includes(DESC) ? '' : DESC),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSortFunction(sortBy: string) {
|
||||||
|
const sortByName = (
|
||||||
|
a: ProjectWithEntryPointMetadata,
|
||||||
|
b: ProjectWithEntryPointMetadata
|
||||||
|
) => {
|
||||||
|
if (a.name && b.name) {
|
||||||
|
return sortBy.includes('desc')
|
||||||
|
? a.name.localeCompare(b.name)
|
||||||
|
: b.name.localeCompare(a.name)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortByModified = (
|
||||||
|
a: ProjectWithEntryPointMetadata,
|
||||||
|
b: ProjectWithEntryPointMetadata
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
a.entrypoint_metadata?.modifiedAt &&
|
||||||
|
b.entrypoint_metadata?.modifiedAt
|
||||||
|
) {
|
||||||
|
return !sortBy || sortBy.includes('desc')
|
||||||
|
? b.entrypoint_metadata.modifiedAt.getTime() -
|
||||||
|
a.entrypoint_metadata.modifiedAt.getTime()
|
||||||
|
: a.entrypoint_metadata.modifiedAt.getTime() -
|
||||||
|
b.entrypoint_metadata.modifiedAt.getTime()
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortBy?.includes('name')) {
|
||||||
|
return sortByName
|
||||||
|
} else {
|
||||||
|
return sortByModified
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,11 @@
|
|||||||
import { FileEntry, createDir, exists, writeTextFile } from '@tauri-apps/api/fs'
|
import {
|
||||||
|
FileEntry,
|
||||||
|
createDir,
|
||||||
|
exists,
|
||||||
|
readDir,
|
||||||
|
writeTextFile,
|
||||||
|
} from '@tauri-apps/api/fs'
|
||||||
import { documentDir } from '@tauri-apps/api/path'
|
import { documentDir } from '@tauri-apps/api/path'
|
||||||
import { useStore } from '../useStore'
|
|
||||||
import { isTauri } from './isTauri'
|
import { isTauri } from './isTauri'
|
||||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||||
import { metadata } from 'tauri-plugin-fs-extra-api'
|
import { metadata } from 'tauri-plugin-fs-extra-api'
|
||||||
@ -12,35 +17,31 @@ const INDEX_IDENTIFIER = '$n' // $nn.. will pad the number with 0s
|
|||||||
export const MAX_PADDING = 7
|
export const MAX_PADDING = 7
|
||||||
|
|
||||||
// Initializes the project directory and returns the path
|
// Initializes the project directory and returns the path
|
||||||
export async function initializeProjectDirectory() {
|
export async function initializeProjectDirectory(directory: string) {
|
||||||
if (!isTauri()) {
|
if (!isTauri()) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'initializeProjectDirectory() can only be called from a Tauri app'
|
'initializeProjectDirectory() can only be called from a Tauri app'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const { defaultDir: projectDir, setDefaultDir } = useStore.getState()
|
|
||||||
|
|
||||||
if (projectDir && projectDir.dir.length > 0) {
|
if (directory) {
|
||||||
const dirExists = await exists(projectDir.dir)
|
const dirExists = await exists(directory)
|
||||||
if (!dirExists) {
|
if (!dirExists) {
|
||||||
await createDir(projectDir.dir, { recursive: true })
|
await createDir(directory, { recursive: true })
|
||||||
}
|
}
|
||||||
return projectDir
|
return directory
|
||||||
}
|
}
|
||||||
|
|
||||||
const appData = await documentDir()
|
const docDirectory = await documentDir()
|
||||||
|
|
||||||
const INITIAL_DEFAULT_DIR = {
|
const INITIAL_DEFAULT_DIR = docDirectory + PROJECT_FOLDER
|
||||||
dir: appData + PROJECT_FOLDER,
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultDirExists = await exists(INITIAL_DEFAULT_DIR.dir)
|
const defaultDirExists = await exists(INITIAL_DEFAULT_DIR)
|
||||||
|
|
||||||
if (!defaultDirExists) {
|
if (!defaultDirExists) {
|
||||||
await createDir(INITIAL_DEFAULT_DIR.dir, { recursive: true })
|
await createDir(INITIAL_DEFAULT_DIR, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
setDefaultDir(INITIAL_DEFAULT_DIR)
|
|
||||||
return INITIAL_DEFAULT_DIR
|
return INITIAL_DEFAULT_DIR
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,6 +52,25 @@ export function isProjectDirectory(fileOrDir: Partial<FileEntry>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read the contents of a directory
|
||||||
|
// and return the valid projects
|
||||||
|
export async function getProjectsInDir(projectDir: string) {
|
||||||
|
const readProjects = (
|
||||||
|
await readDir(projectDir, {
|
||||||
|
recursive: true,
|
||||||
|
})
|
||||||
|
).filter(isProjectDirectory)
|
||||||
|
|
||||||
|
const projectsWithMetadata = await Promise.all(
|
||||||
|
readProjects.map(async (p) => ({
|
||||||
|
entrypoint_metadata: await metadata(p.path + '/' + PROJECT_ENTRYPOINT),
|
||||||
|
...p,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
return projectsWithMetadata
|
||||||
|
}
|
||||||
|
|
||||||
// Creates a new file in the default directory with the default project name
|
// Creates a new file in the default directory with the default project name
|
||||||
// Returns the path to the new file
|
// Returns the path to the new file
|
||||||
export async function createNewProject(
|
export async function createNewProject(
|
||||||
|
22
src/lib/theme.ts
Normal file
22
src/lib/theme.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export enum Themes {
|
||||||
|
Light = 'light',
|
||||||
|
Dark = 'dark',
|
||||||
|
System = 'system',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSystemTheme(): Exclude<Themes, 'system'> {
|
||||||
|
return typeof window !== 'undefined' &&
|
||||||
|
'matchMedia' in window &&
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
? Themes.Dark
|
||||||
|
: Themes.Light
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setThemeClass(theme: Themes) {
|
||||||
|
const systemTheme = theme === Themes.System && getSystemTheme()
|
||||||
|
if (theme === Themes.Dark || systemTheme === Themes.Dark) {
|
||||||
|
document.body.classList.add('dark')
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('dark')
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { createMachine, assign } from 'xstate'
|
import { createMachine, assign } from 'xstate'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import withBaseURL from '../lib/withBaseURL'
|
import withBaseURL from '../lib/withBaseURL'
|
||||||
|
import { CommandBarMeta } from '../lib/commands'
|
||||||
|
|
||||||
export interface UserContext {
|
export interface UserContext {
|
||||||
user?: Models['User_type']
|
user?: Models['User_type']
|
||||||
@ -9,16 +10,22 @@ export interface UserContext {
|
|||||||
|
|
||||||
export type Events =
|
export type Events =
|
||||||
| {
|
| {
|
||||||
type: 'logout'
|
type: 'Log out'
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'tryLogin'
|
type: 'Log in'
|
||||||
token?: string
|
token?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
|
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
|
||||||
const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
|
const persistedToken = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
|
||||||
|
|
||||||
|
export const authCommandBarMeta: CommandBarMeta = {
|
||||||
|
'Log in': {
|
||||||
|
hide: 'both',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export const authMachine = createMachine<UserContext, Events>(
|
export const authMachine = createMachine<UserContext, Events>(
|
||||||
{
|
{
|
||||||
id: 'Auth',
|
id: 'Auth',
|
||||||
@ -50,7 +57,7 @@ export const authMachine = createMachine<UserContext, Events>(
|
|||||||
loggedIn: {
|
loggedIn: {
|
||||||
entry: ['goToIndexPage'],
|
entry: ['goToIndexPage'],
|
||||||
on: {
|
on: {
|
||||||
logout: {
|
'Log out': {
|
||||||
target: 'loggedOut',
|
target: 'loggedOut',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -58,10 +65,10 @@ export const authMachine = createMachine<UserContext, Events>(
|
|||||||
loggedOut: {
|
loggedOut: {
|
||||||
entry: ['goToSignInPage'],
|
entry: ['goToSignInPage'],
|
||||||
on: {
|
on: {
|
||||||
tryLogin: {
|
'Log in': {
|
||||||
target: 'checkIfLoggedIn',
|
target: 'checkIfLoggedIn',
|
||||||
actions: assign({
|
actions: assign({
|
||||||
token: (context, event) => {
|
token: (_, event) => {
|
||||||
const token = event.token || ''
|
const token = event.token || ''
|
||||||
localStorage.setItem(TOKEN_PERSIST_KEY, token)
|
localStorage.setItem(TOKEN_PERSIST_KEY, token)
|
||||||
return token
|
return token
|
||||||
@ -71,7 +78,7 @@ export const authMachine = createMachine<UserContext, Events>(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
schema: { events: {} as { type: 'logout' } | { type: 'tryLogin' } },
|
schema: { events: {} as { type: 'Log out' } | { type: 'Log in' } },
|
||||||
predictableActionArguments: true,
|
predictableActionArguments: true,
|
||||||
preserveActionOrder: true,
|
preserveActionOrder: true,
|
||||||
context: { token: persistedToken },
|
context: { token: persistedToken },
|
218
src/machines/homeMachine.ts
Normal file
218
src/machines/homeMachine.ts
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
import { assign, createMachine } from 'xstate'
|
||||||
|
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||||
|
import { CommandBarMeta } from '../lib/commands'
|
||||||
|
|
||||||
|
export const homeCommandMeta: CommandBarMeta = {
|
||||||
|
'Create project': {
|
||||||
|
displayValue: (args: string[]) => `Create project "${args[0]}"`,
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
description: '(default)',
|
||||||
|
options: 'defaultProjectName',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Open project': {
|
||||||
|
displayValue: (args: string[]) => `Open project "${args[0]}"`,
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'select',
|
||||||
|
options: 'projects',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Delete project': {
|
||||||
|
displayValue: (args: string[]) => `Delete project "${args[0]}"`,
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'select',
|
||||||
|
options: 'projects',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Rename project': {
|
||||||
|
displayValue: (args: string[]) =>
|
||||||
|
`Rename project "${args[0]}" to "${args[1]}"`,
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'oldName',
|
||||||
|
type: 'select',
|
||||||
|
options: 'projects',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'newName',
|
||||||
|
type: 'string',
|
||||||
|
description: '(default)',
|
||||||
|
options: 'defaultProjectName',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
assign: {
|
||||||
|
hide: 'both',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const homeMachine = createMachine(
|
||||||
|
{
|
||||||
|
/** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAHTK6xampYAOATqgFZj4AusAxAMLMwuLthbtOXANoAGALqJQjVLGJdiqUopAAPRAHYAbPooAWABwBGUwE5zAJgeGArM-MAaEAE9EN0wGYKGX97GX1nGVNDS0MbfwBfeM80TBwCEnIqGiZWDm4+ACUwUlxU8TzpeW1lVXVNbT0EcJNg02d-fzt7fU77Tx8EQ0iKCPtnfUsjGRtLGXtE5IxsPCIySmpacsk+QWFRHIluWQUkEBq1DS1TxqN7ChjzOxtXf0t7a37EcwsRibH-ZzRezA8wLEApZbpNZZTa5ba8AAiYAANmB9lsjlVTuc6ldQDdDOYKP5bm0os5TDJDJ8mlEzPpzIZHA4bO9umCIWlVpkNgcKnwAPKMYp8yTHaoqC71a6IEmBUz6BkWZzWDq2Uw0qzOIJAwz+PXWfSmeZJcFLLkZSi7ERkKCi7i8CCaShkABuqAA1pR8EIRGAALQYyonJSS3ENRDA2wUeyvd6dPVhGw0-RhGOp8IA8xGFkc80rS0Ua3qUh2oO8MDMVjMCiMZEiABmqGY6AoPr2AaD4uxYcuEYQoQpQWNNjsMnMgLGKbT3TC7TcOfsNjzqQL0KKJXQtvtXEdzoobs9lCEm87cMxIbOvel+MQqtMQRmS5ks31sZpAUsZkcIX+cQZJIrpC3KUBupTbuWlbVrW9ZcE2LYUCepRnocwYSrUfYyggbzvBQ+jMq49imLYwTUt4iCft+5i-u0-7UfoQEWtCSKoiWZbnruTqZIeXoUBAKJoihFTdqGGE3rod7UdqsQTI8hiGAqrIauRA7RvYeoqhO1jtAqjFrpkLFohBHEVlWzYwY2zatvxrFCWKWKiVKeISdh4yBJE-jGs4fhhA4zg0kRNgxhplhaW0nn4XpUKZEUuAQMZqF8FxLqkO6vG+hAgYcbAIlXmJzmNERdy0RYNiKgpthxDSEU6q8MSTJYjWGFFIEULF8WljuSX7jxx7CJlQY5ZYl44pht4IP61gyPc8njt0lIuH51UKrVVITEyMy2C1hbtQl-KmdBdaWQhGVZYluWjeJjSTf402shMEyuEyljPAFL0UNmMiuN86lWHMiSmvQ-HwKcnL6WA6FOf2k3mESMRDA4RpUm4U4qf6gSEt0QIvvqfjOCaiyrtF6zZPQXWQ+GWFlUEsbmNMf1TV9NLeXDcqRIySnNaaYPEzC5M9vl-b+IyFCjupryPF9jKWP5Kks-cbMWLERHRNt0LFntkgU2NLk4dqsz43YsTK++Kk2C+MbTOOcxzOMrhqzFxTgZ1Qba1dd6BUE1jGsLMxxK9KlDNqm3tMLUQvqYlgO5QhlsTubsFXesTTUuPTfHExshDS0RftRftGgEnTZtHbX9Zr+QJ-2S4Y3qnmTC+4tMyp1EfeOnmeQqdOhyXQrFOXXCV1hCkmLDOnBJYvRRDSsyRzGjiKj0lKdAkANAA */
|
||||||
|
id: 'Home machine',
|
||||||
|
|
||||||
|
initial: 'Reading projects',
|
||||||
|
|
||||||
|
context: {
|
||||||
|
projects: [] as ProjectWithEntryPointMetadata[],
|
||||||
|
defaultProjectName: '',
|
||||||
|
defaultDirectory: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
on: {
|
||||||
|
assign: {
|
||||||
|
actions: assign((_, event) => ({
|
||||||
|
...event.data,
|
||||||
|
})),
|
||||||
|
target: '.Reading projects',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
states: {
|
||||||
|
'Has no projects': {
|
||||||
|
on: {
|
||||||
|
'Create project': {
|
||||||
|
target: 'Creating project',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Has projects': {
|
||||||
|
on: {
|
||||||
|
'Rename project': {
|
||||||
|
target: 'Renaming project',
|
||||||
|
},
|
||||||
|
|
||||||
|
'Create project': {
|
||||||
|
target: 'Creating project',
|
||||||
|
},
|
||||||
|
|
||||||
|
'Delete project': {
|
||||||
|
target: 'Deleting project',
|
||||||
|
},
|
||||||
|
|
||||||
|
'Open project': {
|
||||||
|
target: 'Opening project',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Creating project': {
|
||||||
|
invoke: {
|
||||||
|
id: 'create-project',
|
||||||
|
src: 'createProject',
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
target: 'Reading projects',
|
||||||
|
actions: ['toastSuccess'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
target: 'Reading projects',
|
||||||
|
actions: ['toastError'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Renaming project': {
|
||||||
|
invoke: {
|
||||||
|
id: 'rename-project',
|
||||||
|
src: 'renameProject',
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
target: '#Home machine.Reading projects',
|
||||||
|
actions: ['toastSuccess'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
target: '#Home machine.Reading projects',
|
||||||
|
actions: ['toastError'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Deleting project': {
|
||||||
|
invoke: {
|
||||||
|
id: 'delete-project',
|
||||||
|
src: 'deleteProject',
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
actions: ['toastSuccess'],
|
||||||
|
target: '#Home machine.Reading projects',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onError: {
|
||||||
|
actions: ['toastError'],
|
||||||
|
target: '#Home machine.Has projects',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Reading projects': {
|
||||||
|
invoke: {
|
||||||
|
id: 'read-projects',
|
||||||
|
src: 'readProjects',
|
||||||
|
onDone: [
|
||||||
|
{
|
||||||
|
cond: 'Has at least 1 project',
|
||||||
|
target: 'Has projects',
|
||||||
|
actions: ['setProjects'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'Has no projects',
|
||||||
|
actions: ['setProjects'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onError: [
|
||||||
|
{
|
||||||
|
target: 'Has no projects',
|
||||||
|
actions: ['toastError'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Opening project': {
|
||||||
|
entry: ['navigateToProject'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
schema: {
|
||||||
|
events: {} as
|
||||||
|
| { type: 'Open project'; data: { name: string } }
|
||||||
|
| { type: 'Rename project'; data: { oldName: string; newName: string } }
|
||||||
|
| { type: 'Create project'; data: { name: string } }
|
||||||
|
| { type: 'Delete project'; data: { name: string } }
|
||||||
|
| { type: 'navigate'; data: { name: string } }
|
||||||
|
| {
|
||||||
|
type: 'done.invoke.read-projects'
|
||||||
|
data: ProjectWithEntryPointMetadata[]
|
||||||
|
}
|
||||||
|
| { type: 'assign'; data: { [key: string]: any } },
|
||||||
|
},
|
||||||
|
|
||||||
|
predictableActionArguments: true,
|
||||||
|
preserveActionOrder: true,
|
||||||
|
tsTypes: {} as import('./homeMachine.typegen').Typegen0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: {
|
||||||
|
setProjects: assign((_, event) => {
|
||||||
|
return { projects: event.data as ProjectWithEntryPointMetadata[] }
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
99
src/machines/homeMachine.typegen.ts
Normal file
99
src/machines/homeMachine.typegen.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
// This file was automatically generated. Edits will be overwritten
|
||||||
|
|
||||||
|
export interface Typegen0 {
|
||||||
|
'@@xstate/typegen': true
|
||||||
|
internalEvents: {
|
||||||
|
'done.invoke.create-project': {
|
||||||
|
type: 'done.invoke.create-project'
|
||||||
|
data: unknown
|
||||||
|
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||||
|
}
|
||||||
|
'done.invoke.delete-project': {
|
||||||
|
type: 'done.invoke.delete-project'
|
||||||
|
data: unknown
|
||||||
|
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||||
|
}
|
||||||
|
'done.invoke.read-projects': {
|
||||||
|
type: 'done.invoke.read-projects'
|
||||||
|
data: unknown
|
||||||
|
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||||
|
}
|
||||||
|
'done.invoke.rename-project': {
|
||||||
|
type: 'done.invoke.rename-project'
|
||||||
|
data: unknown
|
||||||
|
__tip: 'See the XState TS docs to learn how to strongly type this.'
|
||||||
|
}
|
||||||
|
'error.platform.create-project': {
|
||||||
|
type: 'error.platform.create-project'
|
||||||
|
data: unknown
|
||||||
|
}
|
||||||
|
'error.platform.delete-project': {
|
||||||
|
type: 'error.platform.delete-project'
|
||||||
|
data: unknown
|
||||||
|
}
|
||||||
|
'error.platform.read-projects': {
|
||||||
|
type: 'error.platform.read-projects'
|
||||||
|
data: unknown
|
||||||
|
}
|
||||||
|
'error.platform.rename-project': {
|
||||||
|
type: 'error.platform.rename-project'
|
||||||
|
data: unknown
|
||||||
|
}
|
||||||
|
'xstate.init': { type: 'xstate.init' }
|
||||||
|
}
|
||||||
|
invokeSrcNameMap: {
|
||||||
|
createProject: 'done.invoke.create-project'
|
||||||
|
deleteProject: 'done.invoke.delete-project'
|
||||||
|
readProjects: 'done.invoke.read-projects'
|
||||||
|
renameProject: 'done.invoke.rename-project'
|
||||||
|
}
|
||||||
|
missingImplementations: {
|
||||||
|
actions: 'navigateToProject' | 'toastError' | 'toastSuccess'
|
||||||
|
delays: never
|
||||||
|
guards: 'Has at least 1 project'
|
||||||
|
services:
|
||||||
|
| 'createProject'
|
||||||
|
| 'deleteProject'
|
||||||
|
| 'readProjects'
|
||||||
|
| 'renameProject'
|
||||||
|
}
|
||||||
|
eventsCausingActions: {
|
||||||
|
navigateToProject: 'Open project'
|
||||||
|
setProjects: 'done.invoke.read-projects'
|
||||||
|
toastError:
|
||||||
|
| 'error.platform.create-project'
|
||||||
|
| 'error.platform.delete-project'
|
||||||
|
| 'error.platform.read-projects'
|
||||||
|
| 'error.platform.rename-project'
|
||||||
|
toastSuccess:
|
||||||
|
| 'done.invoke.create-project'
|
||||||
|
| 'done.invoke.delete-project'
|
||||||
|
| 'done.invoke.rename-project'
|
||||||
|
}
|
||||||
|
eventsCausingDelays: {}
|
||||||
|
eventsCausingGuards: {
|
||||||
|
'Has at least 1 project': 'done.invoke.read-projects'
|
||||||
|
}
|
||||||
|
eventsCausingServices: {
|
||||||
|
createProject: 'Create project'
|
||||||
|
deleteProject: 'Delete project'
|
||||||
|
readProjects:
|
||||||
|
| 'assign'
|
||||||
|
| 'done.invoke.create-project'
|
||||||
|
| 'done.invoke.delete-project'
|
||||||
|
| 'done.invoke.rename-project'
|
||||||
|
| 'error.platform.create-project'
|
||||||
|
| 'error.platform.rename-project'
|
||||||
|
| 'xstate.init'
|
||||||
|
renameProject: 'Rename project'
|
||||||
|
}
|
||||||
|
matchesStates:
|
||||||
|
| 'Creating project'
|
||||||
|
| 'Deleting project'
|
||||||
|
| 'Has no projects'
|
||||||
|
| 'Has projects'
|
||||||
|
| 'Opening project'
|
||||||
|
| 'Reading projects'
|
||||||
|
| 'Renaming project'
|
||||||
|
tags: never
|
||||||
|
}
|
194
src/machines/settingsMachine.ts
Normal file
194
src/machines/settingsMachine.ts
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
import { assign, createMachine } from 'xstate'
|
||||||
|
import { BaseUnit, baseUnitsUnion } from '../useStore'
|
||||||
|
import { CommandBarMeta } from '../lib/commands'
|
||||||
|
import { Themes } from '../lib/theme'
|
||||||
|
|
||||||
|
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
|
||||||
|
|
||||||
|
export const settingsCommandBarMeta: CommandBarMeta = {
|
||||||
|
'Set Theme': {
|
||||||
|
displayValue: (args: string[]) => 'Change the app theme',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'theme',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'theme',
|
||||||
|
options: Object.values(Themes).map((v) => ({ name: v })) as {
|
||||||
|
name: string
|
||||||
|
}[],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Set Default Project Name': {
|
||||||
|
displayValue: (args: string[]) => 'Set a new default project name',
|
||||||
|
hide: 'web',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'defaultProjectName',
|
||||||
|
type: 'string',
|
||||||
|
description: '(default)',
|
||||||
|
defaultValue: 'defaultProjectName',
|
||||||
|
options: 'defaultProjectName',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Set Default Directory': {
|
||||||
|
hide: 'both',
|
||||||
|
},
|
||||||
|
'Set Unit System': {
|
||||||
|
displayValue: (args: string[]) => 'Set your default unit system',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'unitSystem',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'unitSystem',
|
||||||
|
options: [{ name: 'imperial' }, { name: 'metric' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Set Base Unit': {
|
||||||
|
displayValue: (args: string[]) => 'Set your default base unit',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'baseUnit',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'baseUnit',
|
||||||
|
options: Object.values(baseUnitsUnion).map((v) => ({ name: v })),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Set Onboarding Status': {
|
||||||
|
hide: 'both',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settingsMachine = createMachine(
|
||||||
|
{
|
||||||
|
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlTQAIAVACzAFswBtABgF1FQAHAe1iYsfbNxAAPRAA42+AEwB2KQFYAzGznKAnADZli1QBoQAT2kBGKfm37lOned3nzqgL6vjlLLgJFSFdCoAETAAMwBDAFdiagAFACc+ACswAGNqADlw5nYuJBB+QWFRfMkEABY5fDYa2rra83LjMwQdLWV8BXLyuxlVLU1Ld090bzxCEnJKYLComODMeLS0PniTXLFCoUwRMTK7fC1zNql7NgUjtnKjU0RlBSqpLVUVPVUda60tYZAvHHG-FNAgBVbBCKjIEywNBMDb5LbFPaILqdfRSORsS4qcxXZqIHqyK6qY4XOxsGTKco-P4+Cb+aYAIXCsDAVFBQjhvAE212pWkskUKnUml0+gUNxaqkU+EccnKF1UCnucnMcjcHl+o3+vkmZBofCgUFIMwARpEoFRYuFsGBiJyCtzEXzWrJlGxlKdVFKvfY1XiEBjyvhVOVzBdzu13pYFNStbTAQFqAB5bAmvjheIQf4QtDhNCRWD2hE7EqgfayHTEh7lHQNSxSf1Scz4cpHHFyFVujTKczuDXYPgQOBiGl4TaOktIhAAWg6X3nC4Xp39050sYw2rpYHHRUnztVhPJqmUlIGbEriv9WhrLZ6uibHcqUr7riAA */
|
||||||
|
id: 'Settings',
|
||||||
|
predictableActionArguments: true,
|
||||||
|
context: {
|
||||||
|
theme: Themes.System,
|
||||||
|
defaultProjectName: '',
|
||||||
|
unitSystem: 'imperial' as 'imperial' | 'metric',
|
||||||
|
baseUnit: 'in' as BaseUnit,
|
||||||
|
defaultDirectory: '',
|
||||||
|
showDebugPanel: false,
|
||||||
|
onboardingStatus: '',
|
||||||
|
},
|
||||||
|
initial: 'idle',
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
'Set Theme': {
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
theme: (_, event) => event.data.theme,
|
||||||
|
}),
|
||||||
|
'persistSettings',
|
||||||
|
'toastSuccess',
|
||||||
|
],
|
||||||
|
target: 'idle',
|
||||||
|
internal: true,
|
||||||
|
},
|
||||||
|
'Set Default Project Name': {
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
defaultProjectName: (_, event) => event.data.defaultProjectName,
|
||||||
|
}),
|
||||||
|
'persistSettings',
|
||||||
|
'toastSuccess',
|
||||||
|
],
|
||||||
|
target: 'idle',
|
||||||
|
internal: true,
|
||||||
|
},
|
||||||
|
'Set Default Directory': {
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
defaultDirectory: (_, event) => event.data.defaultDirectory,
|
||||||
|
}),
|
||||||
|
'persistSettings',
|
||||||
|
'toastSuccess',
|
||||||
|
],
|
||||||
|
target: 'idle',
|
||||||
|
internal: true,
|
||||||
|
},
|
||||||
|
'Set Unit System': {
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
unitSystem: (_, event) => event.data.unitSystem,
|
||||||
|
baseUnit: (_, event) =>
|
||||||
|
event.data.unitSystem === 'imperial' ? 'in' : 'mm',
|
||||||
|
}),
|
||||||
|
'persistSettings',
|
||||||
|
'toastSuccess',
|
||||||
|
],
|
||||||
|
target: 'idle',
|
||||||
|
internal: true,
|
||||||
|
},
|
||||||
|
'Set Base Unit': {
|
||||||
|
actions: [
|
||||||
|
assign({ baseUnit: (_, event) => event.data.baseUnit }),
|
||||||
|
'persistSettings',
|
||||||
|
'toastSuccess',
|
||||||
|
],
|
||||||
|
target: 'idle',
|
||||||
|
internal: true,
|
||||||
|
},
|
||||||
|
'Toggle Debug Panel': {
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
showDebugPanel: (context) => {
|
||||||
|
return !context.showDebugPanel
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'persistSettings',
|
||||||
|
'toastSuccess',
|
||||||
|
],
|
||||||
|
target: 'idle',
|
||||||
|
internal: true,
|
||||||
|
},
|
||||||
|
'Set Onboarding Status': {
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
onboardingStatus: (_, event) => event.data.onboardingStatus,
|
||||||
|
}),
|
||||||
|
'persistSettings',
|
||||||
|
'toastSuccess',
|
||||||
|
],
|
||||||
|
target: 'idle',
|
||||||
|
internal: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
|
||||||
|
schema: {
|
||||||
|
events: {} as
|
||||||
|
| { type: 'Set Theme'; data: { theme: Themes } }
|
||||||
|
| {
|
||||||
|
type: 'Set Default Project Name'
|
||||||
|
data: { defaultProjectName: string }
|
||||||
|
}
|
||||||
|
| { type: 'Set Default Directory'; data: { defaultDirectory: string } }
|
||||||
|
| {
|
||||||
|
type: 'Set Unit System'
|
||||||
|
data: { unitSystem: 'imperial' | 'metric' }
|
||||||
|
}
|
||||||
|
| { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
|
||||||
|
| { type: 'Set Onboarding Status'; data: { onboardingStatus: string } }
|
||||||
|
| { type: 'Toggle Debug Panel' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: {
|
||||||
|
persistSettings: (context) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(SETTINGS_PERSIST_KEY, JSON.stringify(context))
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
38
src/machines/settingsMachine.typegen.ts
Normal file
38
src/machines/settingsMachine.typegen.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// This file was automatically generated. Edits will be overwritten
|
||||||
|
|
||||||
|
export interface Typegen0 {
|
||||||
|
'@@xstate/typegen': true
|
||||||
|
internalEvents: {
|
||||||
|
'xstate.init': { type: 'xstate.init' }
|
||||||
|
}
|
||||||
|
invokeSrcNameMap: {}
|
||||||
|
missingImplementations: {
|
||||||
|
actions: 'toastSuccess'
|
||||||
|
delays: never
|
||||||
|
guards: never
|
||||||
|
services: never
|
||||||
|
}
|
||||||
|
eventsCausingActions: {
|
||||||
|
persistSettings:
|
||||||
|
| 'Set Base Unit'
|
||||||
|
| 'Set Default Directory'
|
||||||
|
| 'Set Default Project Name'
|
||||||
|
| 'Set Onboarding Status'
|
||||||
|
| 'Set Theme'
|
||||||
|
| 'Set Unit System'
|
||||||
|
| 'Toggle Debug Panel'
|
||||||
|
toastSuccess:
|
||||||
|
| 'Set Base Unit'
|
||||||
|
| 'Set Default Directory'
|
||||||
|
| 'Set Default Project Name'
|
||||||
|
| 'Set Onboarding Status'
|
||||||
|
| 'Set Theme'
|
||||||
|
| 'Set Unit System'
|
||||||
|
| 'Toggle Debug Panel'
|
||||||
|
}
|
||||||
|
eventsCausingDelays: {}
|
||||||
|
eventsCausingGuards: {}
|
||||||
|
eventsCausingServices: {}
|
||||||
|
matchesStates: 'idle'
|
||||||
|
tags: never
|
||||||
|
}
|
@ -1,93 +1,135 @@
|
|||||||
import { FormEvent, useCallback, useEffect, useState } from 'react'
|
import { FormEvent, useContext, useEffect } from 'react'
|
||||||
import { readDir, removeDir, renameFile } from '@tauri-apps/api/fs'
|
import { removeDir, renameFile } from '@tauri-apps/api/fs'
|
||||||
import {
|
import {
|
||||||
createNewProject,
|
createNewProject,
|
||||||
getNextProjectIndex,
|
getNextProjectIndex,
|
||||||
interpolateProjectNameWithIndex,
|
interpolateProjectNameWithIndex,
|
||||||
doesProjectNameNeedInterpolated,
|
doesProjectNameNeedInterpolated,
|
||||||
isProjectDirectory,
|
getProjectsInDir,
|
||||||
PROJECT_ENTRYPOINT,
|
|
||||||
} from '../lib/tauriFS'
|
} from '../lib/tauriFS'
|
||||||
import { ActionButton } from '../components/ActionButton'
|
import { ActionButton } from '../components/ActionButton'
|
||||||
import {
|
import { faArrowDown, faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||||
faArrowDown,
|
|
||||||
faArrowUp,
|
|
||||||
faCircleDot,
|
|
||||||
faPlus,
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { useStore } from '../useStore'
|
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { AppHeader } from '../components/AppHeader'
|
import { AppHeader } from '../components/AppHeader'
|
||||||
import ProjectCard from '../components/ProjectCard'
|
import ProjectCard from '../components/ProjectCard'
|
||||||
import { useLoaderData, useSearchParams } from 'react-router-dom'
|
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { ProjectWithEntryPointMetadata, HomeLoaderData } from '../Router'
|
import { ProjectWithEntryPointMetadata, HomeLoaderData } from '../Router'
|
||||||
import Loading from '../components/Loading'
|
import Loading from '../components/Loading'
|
||||||
import { metadata } from 'tauri-plugin-fs-extra-api'
|
import { useMachine } from '@xstate/react'
|
||||||
|
import { homeCommandMeta, homeMachine } from '../machines/homeMachine'
|
||||||
const DESC = ':desc'
|
import { ContextFrom, EventFrom } from 'xstate'
|
||||||
|
import { paths } from '../Router'
|
||||||
|
import {
|
||||||
|
getNextSearchParams,
|
||||||
|
getSortFunction,
|
||||||
|
getSortIcon,
|
||||||
|
} from '../lib/sorting'
|
||||||
|
import { CommandsContext } from '../components/CommandBar'
|
||||||
|
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||||
|
import { SettingsContext } from '../components/SettingsCommandProvider'
|
||||||
|
|
||||||
// This route only opens in the Tauri desktop context for now,
|
// This route only opens in the Tauri desktop context for now,
|
||||||
// as defined in Router.tsx, so we can use the Tauri APIs and types.
|
// as defined in Router.tsx, so we can use the Tauri APIs and types.
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const { commands, setCommandBarOpen } = useContext(CommandsContext)
|
||||||
const sort = searchParams.get('sort_by') ?? 'modified:desc'
|
const navigate = useNavigate()
|
||||||
const { projects: loadedProjects } = useLoaderData() as HomeLoaderData
|
const { projects: loadedProjects } = useLoaderData() as HomeLoaderData
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const { defaultDirectory, defaultProjectName } = useContext(SettingsContext)
|
||||||
const [projects, setProjects] = useState(loadedProjects || [])
|
|
||||||
const { defaultDir, defaultProjectName } = useStore((s) => ({
|
|
||||||
defaultDir: s.defaultDir,
|
|
||||||
defaultProjectName: s.defaultProjectName,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const modifiedSelected = sort?.includes('modified') || !sort || sort === null
|
const [state, send] = useMachine(homeMachine, {
|
||||||
|
context: {
|
||||||
|
projects: loadedProjects,
|
||||||
|
defaultProjectName,
|
||||||
|
defaultDirectory,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
navigateToProject: (
|
||||||
|
context: ContextFrom<typeof homeMachine>,
|
||||||
|
event: EventFrom<typeof homeMachine>
|
||||||
|
) => {
|
||||||
|
if (event.data && 'name' in event.data) {
|
||||||
|
setCommandBarOpen(false)
|
||||||
|
navigate(
|
||||||
|
`${paths.FILE}/${encodeURIComponent(
|
||||||
|
context.defaultDirectory + '/' + event.data.name
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toastSuccess: (_, event) => toast.success((event.data || '') + ''),
|
||||||
|
toastError: (_, event) => toast.error((event.data || '') + ''),
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
readProjects: async (context: ContextFrom<typeof homeMachine>) =>
|
||||||
|
getProjectsInDir(context.defaultDirectory),
|
||||||
|
createProject: async (
|
||||||
|
context: ContextFrom<typeof homeMachine>,
|
||||||
|
event: EventFrom<typeof homeMachine, 'Create project'>
|
||||||
|
) => {
|
||||||
|
let name =
|
||||||
|
event.data && 'name' in event.data
|
||||||
|
? event.data.name
|
||||||
|
: defaultProjectName
|
||||||
|
if (doesProjectNameNeedInterpolated(name)) {
|
||||||
|
const nextIndex = await getNextProjectIndex(name, projects)
|
||||||
|
name = interpolateProjectNameWithIndex(name, nextIndex)
|
||||||
|
}
|
||||||
|
|
||||||
const refreshProjects = useCallback(
|
await createNewProject(context.defaultDirectory + '/' + name)
|
||||||
async (projectDir = defaultDir) => {
|
return `Successfully created "${name}"`
|
||||||
const readProjects = (
|
},
|
||||||
await readDir(projectDir.dir, {
|
renameProject: async (
|
||||||
|
context: ContextFrom<typeof homeMachine>,
|
||||||
|
event: EventFrom<typeof homeMachine, 'Rename project'>
|
||||||
|
) => {
|
||||||
|
const { oldName, newName } = event.data
|
||||||
|
let name = newName ? newName : context.defaultProjectName
|
||||||
|
if (doesProjectNameNeedInterpolated(name)) {
|
||||||
|
const nextIndex = await getNextProjectIndex(name, projects)
|
||||||
|
name = interpolateProjectNameWithIndex(name, nextIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
await renameFile(
|
||||||
|
context.defaultDirectory + '/' + oldName,
|
||||||
|
context.defaultDirectory + '/' + name
|
||||||
|
)
|
||||||
|
return `Successfully renamed "${oldName}" to "${name}"`
|
||||||
|
},
|
||||||
|
deleteProject: async (
|
||||||
|
context: ContextFrom<typeof homeMachine>,
|
||||||
|
event: EventFrom<typeof homeMachine, 'Delete project'>
|
||||||
|
) => {
|
||||||
|
await removeDir(context.defaultDirectory + '/' + event.data.name, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
})
|
})
|
||||||
).filter(isProjectDirectory)
|
return `Successfully deleted "${event.data.name}"`
|
||||||
|
},
|
||||||
const projectsWithMetadata = await Promise.all(
|
|
||||||
readProjects.map(async (p) => ({
|
|
||||||
entrypoint_metadata: await metadata(
|
|
||||||
p.path + '/' + PROJECT_ENTRYPOINT
|
|
||||||
),
|
|
||||||
...p,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
setProjects(projectsWithMetadata)
|
|
||||||
},
|
},
|
||||||
[defaultDir, setProjects]
|
guards: {
|
||||||
)
|
'Has at least 1 project': (_, event: EventFrom<typeof homeMachine>) => {
|
||||||
|
if (event.type !== 'done.invoke.read-projects') return false
|
||||||
|
return event?.data?.length ? event.data?.length >= 1 : false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { projects } = state.context
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
const sort = searchParams.get('sort_by') ?? 'modified:desc'
|
||||||
|
|
||||||
|
const isSortByModified = sort?.includes('modified') || !sort || sort === null
|
||||||
|
|
||||||
|
useStateMachineCommands<typeof homeMachine>({
|
||||||
|
commands,
|
||||||
|
send,
|
||||||
|
state,
|
||||||
|
commandBarMeta: homeCommandMeta,
|
||||||
|
owner: 'home',
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshProjects(defaultDir).then(() => {
|
send({ type: 'assign', data: { defaultProjectName, defaultDirectory } })
|
||||||
setIsLoading(false)
|
}, [defaultDirectory, defaultProjectName, send])
|
||||||
})
|
|
||||||
}, [setIsLoading, refreshProjects, defaultDir])
|
|
||||||
|
|
||||||
async function handleNewProject() {
|
|
||||||
let projectName = defaultProjectName
|
|
||||||
if (doesProjectNameNeedInterpolated(projectName)) {
|
|
||||||
const nextIndex = await getNextProjectIndex(defaultProjectName, projects)
|
|
||||||
projectName = interpolateProjectNameWithIndex(
|
|
||||||
defaultProjectName,
|
|
||||||
nextIndex
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
await createNewProject(defaultDir.dir + '/' + projectName).catch((err) => {
|
|
||||||
console.error('Error creating project:', err)
|
|
||||||
toast.error('Error creating project')
|
|
||||||
})
|
|
||||||
|
|
||||||
await refreshProjects()
|
|
||||||
toast.success('Project created')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRenameProject(
|
async function handleRenameProject(
|
||||||
e: FormEvent<HTMLFormElement>,
|
e: FormEvent<HTMLFormElement>,
|
||||||
@ -96,85 +138,14 @@ const Home = () => {
|
|||||||
const { newProjectName } = Object.fromEntries(
|
const { newProjectName } = Object.fromEntries(
|
||||||
new FormData(e.target as HTMLFormElement)
|
new FormData(e.target as HTMLFormElement)
|
||||||
)
|
)
|
||||||
if (newProjectName && project.name && newProjectName !== project.name) {
|
|
||||||
const dir = project.path?.slice(0, project.path?.lastIndexOf('/'))
|
|
||||||
await renameFile(project.path, dir + '/' + newProjectName).catch(
|
|
||||||
(err) => {
|
|
||||||
console.error('Error renaming project:', err)
|
|
||||||
toast.error('Error renaming project')
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
await refreshProjects()
|
send('Rename project', {
|
||||||
toast.success('Project renamed')
|
data: { oldName: project.name, newName: newProjectName },
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteProject(project: ProjectWithEntryPointMetadata) {
|
async function handleDeleteProject(project: ProjectWithEntryPointMetadata) {
|
||||||
if (project.path) {
|
send('Delete project', { data: { name: project.name || '' } })
|
||||||
await removeDir(project.path, { recursive: true }).catch((err) => {
|
|
||||||
console.error('Error deleting project:', err)
|
|
||||||
toast.error('Error deleting project')
|
|
||||||
})
|
|
||||||
|
|
||||||
await refreshProjects()
|
|
||||||
toast.success('Project deleted')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSortIcon(sortBy: string) {
|
|
||||||
if (sort === sortBy) {
|
|
||||||
return faArrowUp
|
|
||||||
} else if (sort === sortBy + DESC) {
|
|
||||||
return faArrowDown
|
|
||||||
}
|
|
||||||
return faCircleDot
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNextSearchParams(sortBy: string) {
|
|
||||||
if (sort === null || !sort)
|
|
||||||
return { sort_by: sortBy + (sortBy !== 'modified' ? DESC : '') }
|
|
||||||
if (sort.includes(sortBy) && !sort.includes(DESC)) return { sort_by: '' }
|
|
||||||
return {
|
|
||||||
sort_by: sortBy + (sort.includes(DESC) ? '' : DESC),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSortFunction(sortBy: string) {
|
|
||||||
const sortByName = (
|
|
||||||
a: ProjectWithEntryPointMetadata,
|
|
||||||
b: ProjectWithEntryPointMetadata
|
|
||||||
) => {
|
|
||||||
if (a.name && b.name) {
|
|
||||||
return sortBy.includes('desc')
|
|
||||||
? a.name.localeCompare(b.name)
|
|
||||||
: b.name.localeCompare(a.name)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortByModified = (
|
|
||||||
a: ProjectWithEntryPointMetadata,
|
|
||||||
b: ProjectWithEntryPointMetadata
|
|
||||||
) => {
|
|
||||||
if (
|
|
||||||
a.entrypoint_metadata?.modifiedAt &&
|
|
||||||
b.entrypoint_metadata?.modifiedAt
|
|
||||||
) {
|
|
||||||
return !sortBy || sortBy.includes('desc')
|
|
||||||
? b.entrypoint_metadata.modifiedAt.getTime() -
|
|
||||||
a.entrypoint_metadata.modifiedAt.getTime()
|
|
||||||
: a.entrypoint_metadata.modifiedAt.getTime() -
|
|
||||||
b.entrypoint_metadata.modifiedAt.getTime()
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sortBy?.includes('name')) {
|
|
||||||
return sortByName
|
|
||||||
} else {
|
|
||||||
return sortByModified
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -191,9 +162,9 @@ const Home = () => {
|
|||||||
? 'text-chalkboard-80 dark:text-chalkboard-40'
|
? 'text-chalkboard-80 dark:text-chalkboard-40'
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
onClick={() => setSearchParams(getNextSearchParams('name'))}
|
onClick={() => setSearchParams(getNextSearchParams(sort, 'name'))}
|
||||||
icon={{
|
icon={{
|
||||||
icon: getSortIcon('name'),
|
icon: getSortIcon(sort, 'name'),
|
||||||
bgClassName: !sort?.includes('name')
|
bgClassName: !sort?.includes('name')
|
||||||
? 'bg-liquid-50 dark:bg-liquid-70'
|
? 'bg-liquid-50 dark:bg-liquid-70'
|
||||||
: '',
|
: '',
|
||||||
@ -207,17 +178,19 @@ const Home = () => {
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
className={
|
className={
|
||||||
!modifiedSelected
|
!isSortByModified
|
||||||
? 'text-chalkboard-80 dark:text-chalkboard-40'
|
? 'text-chalkboard-80 dark:text-chalkboard-40'
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
onClick={() => setSearchParams(getNextSearchParams('modified'))}
|
onClick={() =>
|
||||||
|
setSearchParams(getNextSearchParams(sort, 'modified'))
|
||||||
|
}
|
||||||
icon={{
|
icon={{
|
||||||
icon: sort ? getSortIcon('modified') : faArrowDown,
|
icon: sort ? getSortIcon(sort, 'modified') : faArrowDown,
|
||||||
bgClassName: !modifiedSelected
|
bgClassName: !isSortByModified
|
||||||
? 'bg-liquid-50 dark:bg-liquid-70'
|
? 'bg-liquid-50 dark:bg-liquid-70'
|
||||||
: '',
|
: '',
|
||||||
iconClassName: !modifiedSelected
|
iconClassName: !isSortByModified
|
||||||
? 'text-liquid-80 dark:text-liquid-30'
|
? 'text-liquid-80 dark:text-liquid-30'
|
||||||
: '',
|
: '',
|
||||||
}}
|
}}
|
||||||
@ -230,11 +203,11 @@ const Home = () => {
|
|||||||
<p className="my-4 text-sm text-chalkboard-80 dark:text-chalkboard-30">
|
<p className="my-4 text-sm text-chalkboard-80 dark:text-chalkboard-30">
|
||||||
Are being saved at{' '}
|
Are being saved at{' '}
|
||||||
<code className="text-liquid-80 dark:text-liquid-30">
|
<code className="text-liquid-80 dark:text-liquid-30">
|
||||||
{defaultDir.dir}
|
{defaultDirectory}
|
||||||
</code>
|
</code>
|
||||||
, which you can change in your <Link to="settings">Settings</Link>.
|
, which you can change in your <Link to="settings">Settings</Link>.
|
||||||
</p>
|
</p>
|
||||||
{isLoading ? (
|
{state.matches('Reading projects') ? (
|
||||||
<Loading>Loading your Projects...</Loading>
|
<Loading>Loading your Projects...</Loading>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@ -256,7 +229,7 @@ const Home = () => {
|
|||||||
)}
|
)}
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={handleNewProject}
|
onClick={() => send('Create project')}
|
||||||
icon={{ icon: faPlus }}
|
icon={{ icon: faPlus }}
|
||||||
>
|
>
|
||||||
New file
|
New file
|
||||||
|
@ -1,32 +1,23 @@
|
|||||||
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { baseUnits, useStore } from '../../useStore'
|
import { BaseUnit, baseUnits } from '../../useStore'
|
||||||
import { ActionButton } from '../../components/ActionButton'
|
import { ActionButton } from '../../components/ActionButton'
|
||||||
import { SettingsSection } from '../Settings'
|
import { SettingsSection } from '../Settings'
|
||||||
import { Toggle } from '../../components/Toggle/Toggle'
|
import { Toggle } from '../../components/Toggle/Toggle'
|
||||||
import { useState } from 'react'
|
import { useContext, useState } from 'react'
|
||||||
import { onboardingPaths, useDismiss, useNextClick } from '.'
|
import { onboardingPaths, useDismiss, useNextClick } from '.'
|
||||||
|
import { SettingsContext } from '../../components/SettingsCommandProvider'
|
||||||
|
|
||||||
export default function Units() {
|
export default function Units() {
|
||||||
const dismiss = useDismiss()
|
const dismiss = useDismiss()
|
||||||
const next = useNextClick(onboardingPaths.CAMERA)
|
const next = useNextClick(onboardingPaths.CAMERA)
|
||||||
const {
|
const { send, unitSystem, baseUnit } = useContext(SettingsContext)
|
||||||
defaultUnitSystem: ogDefaultUnitSystem,
|
const [tempUnitSystem, setTempUnitSystem] = useState(unitSystem)
|
||||||
setDefaultUnitSystem: saveDefaultUnitSystem,
|
const [tempBaseUnit, setTempBaseUnit] = useState(baseUnit)
|
||||||
defaultBaseUnit: ogDefaultBaseUnit,
|
|
||||||
setDefaultBaseUnit: saveDefaultBaseUnit,
|
|
||||||
} = useStore((s) => ({
|
|
||||||
defaultUnitSystem: s.defaultUnitSystem,
|
|
||||||
setDefaultUnitSystem: s.setDefaultUnitSystem,
|
|
||||||
defaultBaseUnit: s.defaultBaseUnit,
|
|
||||||
setDefaultBaseUnit: s.setDefaultBaseUnit,
|
|
||||||
}))
|
|
||||||
const [defaultUnitSystem, setDefaultUnitSystem] =
|
|
||||||
useState(ogDefaultUnitSystem)
|
|
||||||
const [defaultBaseUnit, setDefaultBaseUnit] = useState(ogDefaultBaseUnit)
|
|
||||||
|
|
||||||
function handleNextClick() {
|
function handleNextClick() {
|
||||||
saveDefaultUnitSystem(defaultUnitSystem)
|
send({ type: 'Set Unit System', data: { unitSystem: tempUnitSystem } })
|
||||||
saveDefaultBaseUnit(defaultBaseUnit)
|
send({ type: 'Set Base Unit', data: { baseUnit: tempBaseUnit } })
|
||||||
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,9 +33,9 @@ export default function Units() {
|
|||||||
offLabel="Imperial"
|
offLabel="Imperial"
|
||||||
onLabel="Metric"
|
onLabel="Metric"
|
||||||
name="settings-units"
|
name="settings-units"
|
||||||
checked={defaultUnitSystem === 'metric'}
|
checked={tempUnitSystem === 'metric'}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setDefaultUnitSystem(e.target.checked ? 'metric' : 'imperial')
|
setTempUnitSystem(e.target.checked ? 'metric' : 'imperial')
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
@ -55,10 +46,10 @@ export default function Units() {
|
|||||||
<select
|
<select
|
||||||
id="base-unit"
|
id="base-unit"
|
||||||
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
|
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
|
||||||
value={defaultBaseUnit}
|
value={tempBaseUnit}
|
||||||
onChange={(e) => setDefaultBaseUnit(e.target.value)}
|
onChange={(e) => setTempBaseUnit(e.target.value as BaseUnit)}
|
||||||
>
|
>
|
||||||
{baseUnits[defaultUnitSystem].map((unit) => (
|
{baseUnits[unitSystem].map((unit) => (
|
||||||
<option key={unit} value={unit}>
|
<option key={unit} value={unit}>
|
||||||
{unit}
|
{unit}
|
||||||
</option>
|
</option>
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { Outlet, useNavigate } from 'react-router-dom'
|
import { Outlet, useNavigate } from 'react-router-dom'
|
||||||
import { useStore } from '../../useStore'
|
|
||||||
|
|
||||||
import Introduction from './Introduction'
|
import Introduction from './Introduction'
|
||||||
import Units from './Units'
|
import Units from './Units'
|
||||||
import Camera from './Camera'
|
import Camera from './Camera'
|
||||||
import Sketching from './Sketching'
|
import Sketching from './Sketching'
|
||||||
import { useCallback } from 'react'
|
import { useCallback, useContext } from 'react'
|
||||||
import makeUrlPathRelative from '../../lib/makeUrlPathRelative'
|
import makeUrlPathRelative from '../../lib/makeUrlPathRelative'
|
||||||
|
import { SettingsContext } from '../../components/SettingsCommandProvider'
|
||||||
|
|
||||||
export const onboardingPaths = {
|
export const onboardingPaths = {
|
||||||
INDEX: '/',
|
INDEX: '/',
|
||||||
@ -36,29 +35,31 @@ export const onboardingRoutes = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export function useNextClick(newStatus: string) {
|
export function useNextClick(newStatus: string) {
|
||||||
const { setOnboardingStatus } = useStore((s) => ({
|
const { send } = useContext(SettingsContext)
|
||||||
setOnboardingStatus: s.setOnboardingStatus,
|
|
||||||
}))
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
return useCallback(() => {
|
return useCallback(() => {
|
||||||
setOnboardingStatus(newStatus)
|
send({
|
||||||
|
type: 'Set Onboarding Status',
|
||||||
|
data: { onboardingStatus: newStatus },
|
||||||
|
})
|
||||||
navigate((newStatus !== onboardingPaths.UNITS ? '..' : '.') + newStatus)
|
navigate((newStatus !== onboardingPaths.UNITS ? '..' : '.') + newStatus)
|
||||||
}, [newStatus, setOnboardingStatus, navigate])
|
}, [newStatus, send, navigate])
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDismiss() {
|
export function useDismiss() {
|
||||||
const { setOnboardingStatus } = useStore((s) => ({
|
const { send } = useContext(SettingsContext)
|
||||||
setOnboardingStatus: s.setOnboardingStatus,
|
|
||||||
}))
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
(path: string) => {
|
(path: string) => {
|
||||||
setOnboardingStatus('dismissed')
|
send({
|
||||||
|
type: 'Set Onboarding Status',
|
||||||
|
data: { onboardingStatus: 'dismissed' },
|
||||||
|
})
|
||||||
navigate(path)
|
navigate(path)
|
||||||
},
|
},
|
||||||
[setOnboardingStatus, navigate]
|
[send, navigate]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,59 +6,41 @@ import {
|
|||||||
import { ActionButton } from '../components/ActionButton'
|
import { ActionButton } from '../components/ActionButton'
|
||||||
import { AppHeader } from '../components/AppHeader'
|
import { AppHeader } from '../components/AppHeader'
|
||||||
import { open } from '@tauri-apps/api/dialog'
|
import { open } from '@tauri-apps/api/dialog'
|
||||||
import { Themes, baseUnits, useStore } from '../useStore'
|
import { BaseUnit, baseUnits } from '../useStore'
|
||||||
import { useRef } from 'react'
|
import { useContext } from 'react'
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import { Toggle } from '../components/Toggle/Toggle'
|
import { Toggle } from '../components/Toggle/Toggle'
|
||||||
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
|
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { IndexLoaderData, paths } from '../Router'
|
import { IndexLoaderData, paths } from '../Router'
|
||||||
|
import { Themes } from '../lib/theme'
|
||||||
|
import { SettingsContext } from '../components/SettingsCommandProvider'
|
||||||
|
|
||||||
export const Settings = () => {
|
export const Settings = () => {
|
||||||
const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
useHotkeys('esc', () => navigate('../'))
|
useHotkeys('esc', () => navigate('../'))
|
||||||
const {
|
const {
|
||||||
defaultDir,
|
|
||||||
setDefaultDir,
|
|
||||||
defaultProjectName,
|
defaultProjectName,
|
||||||
setDefaultProjectName,
|
showDebugPanel,
|
||||||
defaultUnitSystem,
|
defaultDirectory,
|
||||||
setDefaultUnitSystem,
|
unitSystem,
|
||||||
defaultBaseUnit,
|
baseUnit,
|
||||||
setDefaultBaseUnit,
|
|
||||||
setDebugPanel,
|
|
||||||
debugPanel,
|
|
||||||
setOnboardingStatus,
|
|
||||||
theme,
|
theme,
|
||||||
setTheme,
|
send,
|
||||||
} = useStore((s) => ({
|
} = useContext(SettingsContext)
|
||||||
defaultDir: s.defaultDir,
|
|
||||||
setDefaultDir: s.setDefaultDir,
|
|
||||||
defaultProjectName: s.defaultProjectName,
|
|
||||||
setDefaultProjectName: s.setDefaultProjectName,
|
|
||||||
defaultUnitSystem: s.defaultUnitSystem,
|
|
||||||
setDefaultUnitSystem: s.setDefaultUnitSystem,
|
|
||||||
defaultBaseUnit: s.defaultBaseUnit,
|
|
||||||
setDefaultBaseUnit: s.setDefaultBaseUnit,
|
|
||||||
setDebugPanel: s.setDebugPanel,
|
|
||||||
debugPanel: s.debugPanel,
|
|
||||||
setOnboardingStatus: s.setOnboardingStatus,
|
|
||||||
theme: s.theme,
|
|
||||||
setTheme: s.setTheme,
|
|
||||||
}))
|
|
||||||
const ogDefaultDir = useRef(defaultDir)
|
|
||||||
const ogDefaultProjectName = useRef(defaultProjectName)
|
|
||||||
|
|
||||||
async function handleDirectorySelection() {
|
async function handleDirectorySelection() {
|
||||||
const newDirectory = await open({
|
const newDirectory = await open({
|
||||||
directory: true,
|
directory: true,
|
||||||
defaultPath: (defaultDir.base || '') + (defaultDir.dir || paths.INDEX),
|
defaultPath: defaultDirectory || paths.INDEX,
|
||||||
title: 'Choose a new default directory',
|
title: 'Choose a new default directory',
|
||||||
})
|
})
|
||||||
|
|
||||||
if (newDirectory && newDirectory !== null && !Array.isArray(newDirectory)) {
|
if (newDirectory && newDirectory !== null && !Array.isArray(newDirectory)) {
|
||||||
setDefaultDir({ base: defaultDir.base, dir: newDirectory })
|
send({
|
||||||
|
type: 'Set Default Directory',
|
||||||
|
data: { defaultDirectory: newDirectory },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,18 +84,8 @@ export const Settings = () => {
|
|||||||
<div className="w-full flex gap-4 p-1 rounded border border-chalkboard-30">
|
<div className="w-full flex gap-4 p-1 rounded border border-chalkboard-30">
|
||||||
<input
|
<input
|
||||||
className="flex-1 px-2 bg-transparent"
|
className="flex-1 px-2 bg-transparent"
|
||||||
value={defaultDir.dir}
|
value={defaultDirectory}
|
||||||
onChange={(e) => {
|
disabled
|
||||||
setDefaultDir({
|
|
||||||
base: defaultDir.base,
|
|
||||||
dir: e.target.value,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
ogDefaultDir.current.dir !== defaultDir.dir &&
|
|
||||||
toast.success('Default directory updated')
|
|
||||||
ogDefaultDir.current.dir = defaultDir.dir
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
@ -137,15 +109,15 @@ export const Settings = () => {
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
|
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
|
||||||
value={defaultProjectName}
|
defaultValue={defaultProjectName}
|
||||||
onChange={(e) => {
|
onBlur={(e) => {
|
||||||
setDefaultProjectName(e.target.value)
|
send({
|
||||||
}}
|
type: 'Set Default Project Name',
|
||||||
onBlur={() => {
|
data: { defaultProjectName: e.target.value },
|
||||||
ogDefaultProjectName.current !== defaultProjectName &&
|
})
|
||||||
toast.success('Default project name updated')
|
|
||||||
ogDefaultProjectName.current = defaultProjectName
|
|
||||||
}}
|
}}
|
||||||
|
autoCapitalize="off"
|
||||||
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</>
|
</>
|
||||||
@ -158,12 +130,13 @@ export const Settings = () => {
|
|||||||
offLabel="Imperial"
|
offLabel="Imperial"
|
||||||
onLabel="Metric"
|
onLabel="Metric"
|
||||||
name="settings-units"
|
name="settings-units"
|
||||||
checked={defaultUnitSystem === 'metric'}
|
checked={unitSystem === 'metric'}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newUnitSystem = e.target.checked ? 'metric' : 'imperial'
|
const newUnitSystem = e.target.checked ? 'metric' : 'imperial'
|
||||||
setDefaultUnitSystem(newUnitSystem)
|
send({
|
||||||
setDefaultBaseUnit(baseUnits[newUnitSystem][0])
|
type: 'Set Unit System',
|
||||||
toast.success('Unit system set to ' + newUnitSystem)
|
data: { unitSystem: newUnitSystem },
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
@ -174,13 +147,15 @@ export const Settings = () => {
|
|||||||
<select
|
<select
|
||||||
id="base-unit"
|
id="base-unit"
|
||||||
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
|
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
|
||||||
value={defaultBaseUnit}
|
value={baseUnit}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setDefaultBaseUnit(e.target.value)
|
send({
|
||||||
toast.success('Base unit changed to ' + e.target.value)
|
type: 'Set Base Unit',
|
||||||
|
data: { baseUnit: e.target.value as BaseUnit },
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{baseUnits[defaultUnitSystem].map((unit) => (
|
{baseUnits[unitSystem as keyof typeof baseUnits].map((unit) => (
|
||||||
<option key={unit} value={unit}>
|
<option key={unit} value={unit}>
|
||||||
{unit}
|
{unit}
|
||||||
</option>
|
</option>
|
||||||
@ -193,12 +168,9 @@ export const Settings = () => {
|
|||||||
>
|
>
|
||||||
<Toggle
|
<Toggle
|
||||||
name="settings-debug-panel"
|
name="settings-debug-panel"
|
||||||
checked={debugPanel}
|
checked={showDebugPanel}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setDebugPanel(e.target.checked)
|
send('Toggle Debug Panel')
|
||||||
toast.success(
|
|
||||||
'Debug panel toggled ' + (e.target.checked ? 'on' : 'off')
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
@ -211,12 +183,10 @@ export const Settings = () => {
|
|||||||
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
|
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
|
||||||
value={theme}
|
value={theme}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setTheme(e.target.value as Themes)
|
send({
|
||||||
toast.success(
|
type: 'Set Theme',
|
||||||
'Theme changed to ' +
|
data: { theme: e.target.value as Themes },
|
||||||
e.target.value.slice(0, 1).toLocaleUpperCase() +
|
})
|
||||||
e.target.value.slice(1)
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Object.entries(Themes).map(([label, value]) => (
|
{Object.entries(Themes).map(([label, value]) => (
|
||||||
@ -233,7 +203,10 @@ export const Settings = () => {
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOnboardingStatus('')
|
send({
|
||||||
|
type: 'Set Onboarding Status',
|
||||||
|
data: { onboardingStatus: '' },
|
||||||
|
})
|
||||||
navigate('..' + paths.ONBOARDING.INDEX)
|
navigate('..' + paths.ONBOARDING.INDEX)
|
||||||
}}
|
}}
|
||||||
icon={{ icon: faArrowRotateBack }}
|
icon={{ icon: faArrowRotateBack }}
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons'
|
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { ActionButton } from '../components/ActionButton'
|
import { ActionButton } from '../components/ActionButton'
|
||||||
import { isTauri } from '../lib/isTauri'
|
import { isTauri } from '../lib/isTauri'
|
||||||
import { Themes, useStore } from '../useStore'
|
|
||||||
import { invoke } from '@tauri-apps/api/tauri'
|
import { invoke } from '@tauri-apps/api/tauri'
|
||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import { VITE_KC_SITE_BASE_URL, VITE_KC_API_BASE_URL } from '../env'
|
import { VITE_KC_SITE_BASE_URL, VITE_KC_API_BASE_URL } from '../env'
|
||||||
import { getSystemTheme } from '../lib/getSystemTheme'
|
import { Themes, getSystemTheme } from '../lib/theme'
|
||||||
import { paths } from '../Router'
|
import { paths } from '../Router'
|
||||||
import { useAuthMachine } from '../hooks/useAuthMachine'
|
import { useAuthMachine } from '../hooks/useAuthMachine'
|
||||||
|
import { useContext } from 'react'
|
||||||
|
import { SettingsContext } from 'components/SettingsCommandProvider'
|
||||||
|
|
||||||
const SignIn = () => {
|
const SignIn = () => {
|
||||||
const navigate = useNavigate()
|
const { theme } = useContext(SettingsContext)
|
||||||
const { theme } = useStore((s) => ({
|
|
||||||
theme: s.theme,
|
|
||||||
}))
|
|
||||||
const [_, send] = useAuthMachine()
|
const [_, send] = useAuthMachine()
|
||||||
const appliedTheme = theme === Themes.System ? getSystemTheme() : theme
|
const appliedTheme = theme === Themes.System ? getSystemTheme() : theme
|
||||||
const signInTauri = async () => {
|
const signInTauri = async () => {
|
||||||
@ -22,7 +19,7 @@ const SignIn = () => {
|
|||||||
const token: string = await invoke('login', {
|
const token: string = await invoke('login', {
|
||||||
host: VITE_KC_API_BASE_URL,
|
host: VITE_KC_API_BASE_URL,
|
||||||
})
|
})
|
||||||
send({ type: 'tryLogin', token })
|
send({ type: 'Log in', token })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('login button', error)
|
console.error('login button', error)
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,6 @@ import {
|
|||||||
} from './lang/executor'
|
} from './lang/executor'
|
||||||
import { recast } from './lang/recast'
|
import { recast } from './lang/recast'
|
||||||
import { EditorSelection } from '@codemirror/state'
|
import { EditorSelection } from '@codemirror/state'
|
||||||
import { BaseDirectory } from '@tauri-apps/api/fs'
|
|
||||||
import {
|
import {
|
||||||
ArtifactMap,
|
ArtifactMap,
|
||||||
SourceRangeMap,
|
SourceRangeMap,
|
||||||
@ -95,22 +94,14 @@ export type GuiModes =
|
|||||||
position: Position
|
position: Position
|
||||||
}
|
}
|
||||||
|
|
||||||
type UnitSystem = 'imperial' | 'metric'
|
export const baseUnits = {
|
||||||
export enum Themes {
|
|
||||||
Light = 'light',
|
|
||||||
Dark = 'dark',
|
|
||||||
System = 'system',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const baseUnits: Record<UnitSystem, string[]> = {
|
|
||||||
imperial: ['in', 'ft'],
|
imperial: ['in', 'ft'],
|
||||||
metric: ['mm', 'cm', 'm'],
|
metric: ['mm', 'cm', 'm'],
|
||||||
}
|
} as const
|
||||||
|
|
||||||
interface DefaultDir {
|
export type BaseUnit = 'in' | 'ft' | 'mm' | 'cm' | 'm'
|
||||||
base?: BaseDirectory
|
|
||||||
dir: string
|
export const baseUnitsUnion = Object.values(baseUnits).flatMap((v) => v)
|
||||||
}
|
|
||||||
|
|
||||||
export type PaneType = 'code' | 'variables' | 'debug' | 'kclErrors' | 'logs'
|
export type PaneType = 'code' | 'variables' | 'debug' | 'kclErrors' | 'logs'
|
||||||
|
|
||||||
@ -181,21 +172,8 @@ export interface StoreState {
|
|||||||
streamHeight: number
|
streamHeight: number
|
||||||
}) => void
|
}) => void
|
||||||
|
|
||||||
// tauri specific app settings
|
|
||||||
defaultDir: DefaultDir
|
|
||||||
setDefaultDir: (dir: DefaultDir) => void
|
|
||||||
defaultProjectName: string
|
|
||||||
setDefaultProjectName: (defaultProjectName: string) => void
|
|
||||||
defaultUnitSystem: UnitSystem
|
|
||||||
setDefaultUnitSystem: (defaultUnitSystem: UnitSystem) => void
|
|
||||||
defaultBaseUnit: string
|
|
||||||
setDefaultBaseUnit: (defaultBaseUnit: string) => void
|
|
||||||
showHomeMenu: boolean
|
showHomeMenu: boolean
|
||||||
setHomeShowMenu: (showMenu: boolean) => void
|
setHomeShowMenu: (showMenu: boolean) => void
|
||||||
onboardingStatus: string
|
|
||||||
setOnboardingStatus: (status: string) => void
|
|
||||||
theme: Themes
|
|
||||||
setTheme: (theme: Themes) => void
|
|
||||||
isBannerDismissed: boolean
|
isBannerDismissed: boolean
|
||||||
setBannerDismissed: (isBannerDismissed: boolean) => void
|
setBannerDismissed: (isBannerDismissed: boolean) => void
|
||||||
openPanes: PaneType[]
|
openPanes: PaneType[]
|
||||||
@ -205,8 +183,6 @@ export interface StoreState {
|
|||||||
path: string
|
path: string
|
||||||
}[]
|
}[]
|
||||||
setHomeMenuItems: (items: { name: string; path: string }[]) => void
|
setHomeMenuItems: (items: { name: string; path: string }[]) => void
|
||||||
debugPanel: boolean
|
|
||||||
setDebugPanel: (debugPanel: boolean) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let pendingAstUpdates: number[] = []
|
let pendingAstUpdates: number[] = []
|
||||||
@ -385,18 +361,6 @@ export const useStore = create<StoreState>()(
|
|||||||
defaultDir: {
|
defaultDir: {
|
||||||
dir: '',
|
dir: '',
|
||||||
},
|
},
|
||||||
setDefaultDir: (dir) => set({ defaultDir: dir }),
|
|
||||||
defaultProjectName: 'new-project-$nnn',
|
|
||||||
setDefaultProjectName: (defaultProjectName) =>
|
|
||||||
set({ defaultProjectName }),
|
|
||||||
defaultUnitSystem: 'imperial',
|
|
||||||
setDefaultUnitSystem: (defaultUnitSystem) => set({ defaultUnitSystem }),
|
|
||||||
defaultBaseUnit: 'in',
|
|
||||||
setDefaultBaseUnit: (defaultBaseUnit) => set({ defaultBaseUnit }),
|
|
||||||
onboardingStatus: '',
|
|
||||||
setOnboardingStatus: (onboardingStatus) => set({ onboardingStatus }),
|
|
||||||
theme: Themes.System,
|
|
||||||
setTheme: (theme) => set({ theme }),
|
|
||||||
isBannerDismissed: false,
|
isBannerDismissed: false,
|
||||||
setBannerDismissed: (isBannerDismissed) => set({ isBannerDismissed }),
|
setBannerDismissed: (isBannerDismissed) => set({ isBannerDismissed }),
|
||||||
openPanes: ['code'],
|
openPanes: ['code'],
|
||||||
@ -405,25 +369,13 @@ export const useStore = create<StoreState>()(
|
|||||||
setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
|
setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
|
||||||
homeMenuItems: [],
|
homeMenuItems: [],
|
||||||
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
|
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
|
||||||
debugPanel: false,
|
|
||||||
setDebugPanel: (debugPanel) => set({ debugPanel }),
|
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'store',
|
name: 'store',
|
||||||
partialize: (state) =>
|
partialize: (state) =>
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
Object.entries(state).filter(([key]) =>
|
Object.entries(state).filter(([key]) =>
|
||||||
[
|
['code', 'openPanes'].includes(key)
|
||||||
'code',
|
|
||||||
'defaultDir',
|
|
||||||
'defaultProjectName',
|
|
||||||
'defaultUnitSystem',
|
|
||||||
'defaultBaseUnit',
|
|
||||||
'debugPanel',
|
|
||||||
'onboardingStatus',
|
|
||||||
'theme',
|
|
||||||
'openPanes',
|
|
||||||
].includes(key)
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -39,5 +39,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
plugins: [],
|
plugins: [
|
||||||
|
require('@headlessui/tailwindcss'),
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -1642,6 +1642,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
client-only "^0.0.1"
|
client-only "^0.0.1"
|
||||||
|
|
||||||
|
"@headlessui/tailwindcss@^0.2.0":
|
||||||
|
version "0.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@headlessui/tailwindcss/-/tailwindcss-0.2.0.tgz#2c55c98fd8eee4b4f21ec6eb35a014b840059eec"
|
||||||
|
integrity sha512-fpL830Fln1SykOCboExsWr3JIVeQKieLJ3XytLe/tt1A0XzqUthOftDmjcCYLW62w7mQI7wXcoPXr3tZ9QfGxw==
|
||||||
|
|
||||||
"@humanwhocodes/config-array@^0.11.10":
|
"@humanwhocodes/config-array@^0.11.10":
|
||||||
version "0.11.10"
|
version "0.11.10"
|
||||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2"
|
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2"
|
||||||
@ -3827,6 +3832,11 @@ functions-have-names@^1.2.2, functions-have-names@^1.2.3:
|
|||||||
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
|
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
|
||||||
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
|
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
|
||||||
|
|
||||||
|
fuse.js@^6.6.2:
|
||||||
|
version "6.6.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.6.2.tgz#fe463fed4b98c0226ac3da2856a415576dc9a111"
|
||||||
|
integrity sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==
|
||||||
|
|
||||||
gensync@^1.0.0-beta.2:
|
gensync@^1.0.0-beta.2:
|
||||||
version "1.0.0-beta.2"
|
version "1.0.0-beta.2"
|
||||||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||||
|
Reference in New Issue
Block a user