Merge branch 'main' into pierremtb/issue2805
| @ -7221,6 +7221,7 @@ test.describe('Test network and connection issues', () => { | ||||
|  | ||||
|     // Expect the network to be up | ||||
|     await expect(page.getByText('Network Health (Connected)')).toBeVisible() | ||||
|     await expect(page.getByTestId('loading-stream')).not.toBeAttached() | ||||
|  | ||||
|     // Click off the code pane. | ||||
|     await page.mouse.click(100, 100) | ||||
|  | ||||
| Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB | 
| Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 46 KiB | 
| Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB | 
| Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB | 
| Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB | 
| Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB | 
| Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB | 
| Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB | 
| Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB | 
| Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB | 
| Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB | 
| @ -16,14 +16,14 @@ export const TEST_COLORS = { | ||||
| } as const | ||||
|  | ||||
| async function waitForPageLoad(page: Page) { | ||||
|   // wait for 'Loading stream...' spinner | ||||
|   await page.getByTestId('loading-stream').waitFor() | ||||
|   // wait for all spinners to be gone | ||||
|   await page | ||||
|     .getByTestId('loading') | ||||
|     .waitFor({ state: 'detached', timeout: 20_000 }) | ||||
|   await expect(page.getByTestId('loading')).not.toBeAttached({ | ||||
|     timeout: 20_000, | ||||
|   }) | ||||
|  | ||||
|   await page.getByTestId('start-sketch').waitFor() | ||||
|   await expect(page.getByTestId('start-sketch')).toBeEnabled({ | ||||
|     timeout: 20_000, | ||||
|   }) | ||||
| } | ||||
|  | ||||
| async function removeCurrentCode(page: Page) { | ||||
| @ -471,8 +471,10 @@ export const doExport = async ( | ||||
|   page: Page | ||||
| ): Promise<Paths> => { | ||||
|   await page.getByRole('button', { name: APP_NAME }).click() | ||||
|   await expect(page.getByRole('button', { name: 'Export Part' })).toBeVisible() | ||||
|   await page.getByRole('button', { name: 'Export Part' }).click() | ||||
|   await expect( | ||||
|     page.getByRole('button', { name: 'Export', exact: false }) | ||||
|   ).toBeVisible() | ||||
|   await page.getByRole('button', { name: 'Export', exact: false }).click() | ||||
|   await expect(page.getByTestId('command-bar')).toBeVisible() | ||||
|  | ||||
|   // Go through export via command bar | ||||
|  | ||||
| @ -77,7 +77,7 @@ describe('ZMA authorized user flows', () => { | ||||
|     const menuButton = await $('[data-testid="user-sidebar-toggle"]') | ||||
|     await click(menuButton) | ||||
|  | ||||
|     const settingsButton = await $('[data-testid="settings-button"]') | ||||
|     const settingsButton = await $('[data-testid="user-settings"]') | ||||
|     await click(settingsButton) | ||||
|  | ||||
|     const projectDirInput = await $('[data-testid="project-directory-input"]') | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "zoo-modeling-app", | ||||
|   "version": "0.24.2", | ||||
|   "version": "0.24.3", | ||||
|   "private": true, | ||||
|   "dependencies": { | ||||
|     "@codemirror/autocomplete": "^6.17.0", | ||||
|  | ||||
							
								
								
									
										21
									
								
								src-tauri/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						| @ -3652,9 +3652,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "num" | ||||
| version = "0.4.2" | ||||
| version = "0.4.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3135b08af27d103b0a51f2ae0f8632117b7b185ccf931445affa8df530576a41" | ||||
| checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" | ||||
| dependencies = [ | ||||
|  "num-bigint", | ||||
|  "num-complex", | ||||
| @ -3666,11 +3666,10 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "num-bigint" | ||||
| version = "0.4.4" | ||||
| version = "0.4.6" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" | ||||
| checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" | ||||
| dependencies = [ | ||||
|  "autocfg", | ||||
|  "num-integer", | ||||
|  "num-traits", | ||||
| ] | ||||
| @ -3752,9 +3751,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "num-traits" | ||||
| version = "0.2.18" | ||||
| version = "0.2.19" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" | ||||
| checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" | ||||
| dependencies = [ | ||||
|  "autocfg", | ||||
|  "libm", | ||||
| @ -6762,18 +6761,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" | ||||
|  | ||||
| [[package]] | ||||
| name = "thiserror" | ||||
| version = "1.0.63" | ||||
| version = "1.0.62" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" | ||||
| checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" | ||||
| dependencies = [ | ||||
|  "thiserror-impl", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "thiserror-impl" | ||||
| version = "1.0.63" | ||||
| version = "1.0.62" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" | ||||
| checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  | ||||
| @ -80,5 +80,5 @@ | ||||
|     } | ||||
|   }, | ||||
|   "productName": "Zoo Modeling App", | ||||
|   "version": "0.24.2" | ||||
|   "version": "0.24.3" | ||||
| } | ||||
|  | ||||
| @ -49,9 +49,9 @@ export const AppHeader = ({ | ||||
|           <> | ||||
|             <CommandBarOpenButton /> | ||||
|             <RefreshButton /> | ||||
|             <UserSidebarMenu user={user} /> | ||||
|           </> | ||||
|         )} | ||||
|         <UserSidebarMenu user={user} /> | ||||
|       </div> | ||||
|     </header> | ||||
|   ) | ||||
|  | ||||
| @ -311,6 +311,16 @@ const CustomIconMap = { | ||||
|       /> | ||||
|     </svg> | ||||
|   ), | ||||
|   link: ( | ||||
|     <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||||
|       <path | ||||
|         fillRule="evenodd" | ||||
|         clipRule="evenodd" | ||||
|         d="M10.5864 4.46513C11.9532 3.09829 14.1693 3.09829 15.5361 4.46513C16.903 5.83196 16.903 8.04804 15.5361 9.41488L13.5364 11.4147C13.5839 10.9639 13.5635 10.5074 13.4752 10.0616L14.829 8.70777C15.8053 7.73146 15.8053 6.14855 14.829 5.17224C13.8527 4.19592 12.2698 4.19592 11.2935 5.17224L9.17217 7.29356C8.19586 8.26987 8.19586 9.85278 9.17217 10.8291C9.53458 11.1915 9.98056 11.4194 10.4481 11.5127C10.3749 11.6902 10.2662 11.8565 10.122 12.0007L9.76392 12.3587C9.28973 12.1899 8.84465 11.9158 8.46507 11.5362C7.09823 10.1694 7.09823 7.95328 8.46507 6.58645L10.5864 4.46513ZM4.46507 10.5864L6.46488 8.58663C6.41734 9.03738 6.43772 9.49394 6.52601 9.93972L5.17217 11.2935C4.19586 12.2699 4.19586 13.8528 5.17217 14.8291C6.14849 15.8054 7.7314 15.8054 8.70771 14.8291L10.829 12.7078C11.8053 11.7315 11.8053 10.1485 10.829 9.17223C10.4666 8.80983 10.0207 8.58195 9.55314 8.48859C9.62635 8.31113 9.73506 8.14487 9.87926 8.00066L10.2373 7.64262C10.7115 7.81138 11.1566 8.08555 11.5361 8.46512C12.903 9.83196 12.903 12.048 11.5361 13.4149L9.41481 15.5362C8.04798 16.903 5.8319 16.903 4.46507 15.5362C3.09823 14.1694 3.09823 11.9533 4.46507 10.5864Z" | ||||
|         fill="currentColor" | ||||
|       /> | ||||
|     </svg> | ||||
|   ), | ||||
|   'make-variable': ( | ||||
|     <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||||
|       <path | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| import { Popover, Transition } from '@headlessui/react' | ||||
| import { ActionButton } from './ActionButton' | ||||
| import { ActionButton, ActionButtonProps } from './ActionButton' | ||||
| import { type IndexLoaderData } from 'lib/types' | ||||
| import { paths } from 'lib/paths' | ||||
| import { isTauri } from '../lib/isTauri' | ||||
| import { Link } from 'react-router-dom' | ||||
| import { Fragment } from 'react' | ||||
| import { Link, useLocation, useNavigate } from 'react-router-dom' | ||||
| import { Fragment, useMemo } from 'react' | ||||
| import { sep } from '@tauri-apps/api/path' | ||||
| import { Logo } from './Logo' | ||||
| import { APP_NAME } from 'lib/constants' | ||||
| @ -12,6 +12,9 @@ import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { CustomIcon } from './CustomIcon' | ||||
| import { useLspContext } from './LspProvider' | ||||
| import { engineCommandManager } from 'lib/singletons' | ||||
| import usePlatform from 'hooks/usePlatform' | ||||
| import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' | ||||
| import Tooltip from './Tooltip' | ||||
|  | ||||
| const ProjectSidebarMenu = ({ | ||||
|   project, | ||||
| @ -80,6 +83,10 @@ function ProjectMenuPopover({ | ||||
|   project?: IndexLoaderData['project'] | ||||
|   file?: IndexLoaderData['file'] | ||||
| }) { | ||||
|   const platform = usePlatform() | ||||
|   const location = useLocation() | ||||
|   const navigate = useNavigate() | ||||
|   const filePath = useAbsoluteFilePath() | ||||
|   const { commandBarState, commandBarSend } = useCommandsContext() | ||||
|   const { onProjectClose } = useLspContext() | ||||
|   const exportCommandInfo = { name: 'Export', groupId: 'modeling' } | ||||
| @ -90,13 +97,82 @@ function ProjectMenuPopover({ | ||||
|       ) | ||||
|     ) | ||||
|  | ||||
|   // We filter this memoized list so that no orphan "break" elements are rendered. | ||||
|   const projectMenuItems = useMemo<(ActionButtonProps | 'break')[]>( | ||||
|     () => | ||||
|       [ | ||||
|         { | ||||
|           id: 'settings', | ||||
|           Element: 'button', | ||||
|           children: ( | ||||
|             <> | ||||
|               <span className="flex-1">Project settings</span> | ||||
|               <kbd className="hotkey">{`${platform === 'macos' ? '⌘' : 'Ctrl'}${ | ||||
|                 isTauri() ? '' : '⬆' | ||||
|               },`}</kbd> | ||||
|             </> | ||||
|           ), | ||||
|           onClick: () => { | ||||
|             const targetPath = location.pathname.includes(paths.FILE) | ||||
|               ? filePath + paths.SETTINGS | ||||
|               : paths.HOME + paths.SETTINGS | ||||
|             navigate(targetPath + '?tab=project') | ||||
|           }, | ||||
|         }, | ||||
|         'break', | ||||
|         { | ||||
|           id: 'export', | ||||
|           Element: 'button', | ||||
|           children: ( | ||||
|             <> | ||||
|               <span>Export current part</span> | ||||
|               {!findCommand(exportCommandInfo) && ( | ||||
|                 <Tooltip position="right" className="!max-w-none min-w-fit"> | ||||
|                   Awaiting engine connection | ||||
|                 </Tooltip> | ||||
|               )} | ||||
|             </> | ||||
|           ), | ||||
|           disabled: !findCommand(exportCommandInfo), | ||||
|           onClick: () => | ||||
|             commandBarSend({ | ||||
|               type: 'Find and select command', | ||||
|               data: exportCommandInfo, | ||||
|             }), | ||||
|         }, | ||||
|         'break', | ||||
|         { | ||||
|           id: 'go-home', | ||||
|           Element: 'button', | ||||
|           children: 'Go to Home', | ||||
|           className: !isTauri() ? 'hidden' : '', | ||||
|           onClick: () => { | ||||
|             onProjectClose(file || null, project?.path || null, true) | ||||
|             // Clear the scene and end the session. | ||||
|             engineCommandManager.endSession() | ||||
|           }, | ||||
|         }, | ||||
|       ].filter( | ||||
|         (props) => | ||||
|           props === 'break' || | ||||
|           (typeof props !== 'string' && !props.className?.includes('hidden')) | ||||
|       ) as (ActionButtonProps | 'break')[], | ||||
|     [ | ||||
|       platform, | ||||
|       findCommand, | ||||
|       commandBarSend, | ||||
|       engineCommandManager, | ||||
|       onProjectClose, | ||||
|       isTauri, | ||||
|     ] | ||||
|   ) | ||||
|  | ||||
|   return ( | ||||
|     <Popover className="relative"> | ||||
|       <Popover.Button | ||||
|         className="rounded-sm h-9 mr-auto max-h-min min-w-max border-0 py-1 pl-0 pr-2 flex items-center focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary dark:hover:bg-chalkboard-90" | ||||
|         className="gap-1 rounded-sm h-9 mr-auto max-h-min min-w-max border-0 py-1 px-2 flex items-center focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary dark:hover:bg-chalkboard-90" | ||||
|         data-testid="project-sidebar-toggle" | ||||
|       > | ||||
|         <CustomIcon name="three-dots" className="w-5 h-5 rotate-90" /> | ||||
|         <div className="flex flex-col items-start py-0.5"> | ||||
|           <span className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block"> | ||||
|             {isTauri() && file?.name | ||||
| @ -109,68 +185,53 @@ function ProjectMenuPopover({ | ||||
|             </span> | ||||
|           )} | ||||
|         </div> | ||||
|         <CustomIcon | ||||
|           name="caretDown" | ||||
|           className="w-4 h-4 text-chalkboard-70 dark:text-chalkboard-40 ui-open:rotate-180" | ||||
|         /> | ||||
|       </Popover.Button> | ||||
|       <Transition | ||||
|         enter="duration-200 ease-out" | ||||
|         enterFrom="opacity-0" | ||||
|         enterTo="opacity-100" | ||||
|         leave="duration-100 ease-in" | ||||
|         leaveFrom="opacity-100" | ||||
|         leaveTo="opacity-0" | ||||
|         as={Fragment} | ||||
|       > | ||||
|         <Popover.Overlay className="fixed inset-0 z-20 bg-chalkboard-110/50" /> | ||||
|       </Transition> | ||||
|  | ||||
|       <Transition | ||||
|         enter="duration-100 ease-out" | ||||
|         enterFrom="opacity-0 -translate-x-1/4" | ||||
|         enterTo="opacity-100 translate-x-0" | ||||
|         leave="duration-75 ease-in" | ||||
|         leaveFrom="opacity-100 translate-x-0" | ||||
|         leaveTo="opacity-0 -translate-x-4" | ||||
|         enterFrom="opacity-0 -translate-y-2" | ||||
|         enterTo="opacity-100 translate-y-0" | ||||
|         as={Fragment} | ||||
|       > | ||||
|         <Popover.Panel | ||||
|           className="fixed inset-0 right-auto z-30 grid w-64 h-screen max-h-screen grid-cols-1 border rounded-r-md shadow-md bg-chalkboard-10 dark:bg-chalkboard-100 border-chalkboard-40 dark:border-chalkboard-80" | ||||
|           style={{ gridTemplateRows: 'auto 1fr auto' }} | ||||
|           className={`z-10 absolute top-full left-0 mt-1 pb-1 w-48 bg-chalkboard-10 dark:bg-chalkboard-90 | ||||
|           border border-solid border-chalkboard-20 dark:border-chalkboard-90 rounded | ||||
|           shadow-lg`} | ||||
|         > | ||||
|           {({ close }) => ( | ||||
|             <> | ||||
|               <div className="flex flex-col gap-2 p-4"> | ||||
|                 <ActionButton | ||||
|                   Element="button" | ||||
|                   iconStart={{ icon: 'exportFile', className: 'p-1' }} | ||||
|                   className="border-transparent dark:border-transparent" | ||||
|                   disabled={!findCommand(exportCommandInfo)} | ||||
|                   onClick={() => | ||||
|                     commandBarSend({ | ||||
|                       type: 'Find and select command', | ||||
|                       data: exportCommandInfo, | ||||
|                     }) | ||||
|                   } | ||||
|                 > | ||||
|                   Export Part | ||||
|                 </ActionButton> | ||||
|                 {isTauri() && ( | ||||
|                   <ActionButton | ||||
|                     Element="button" | ||||
|                     onClick={() => { | ||||
|                       onProjectClose(file || null, project?.path || null, true) | ||||
|                       // Clear the scene and end the session. | ||||
|                       engineCommandManager.endSession() | ||||
|                     }} | ||||
|                     iconStart={{ | ||||
|                       icon: 'arrowLeft', | ||||
|                       className: 'p-1', | ||||
|                     }} | ||||
|                     className="border-transparent dark:border-transparent" | ||||
|                   > | ||||
|                     Go to Home | ||||
|                   </ActionButton> | ||||
|                 )} | ||||
|               </div> | ||||
|             </> | ||||
|             <ul className="relative flex flex-col items-stretch content-stretch p-0.5"> | ||||
|               {projectMenuItems.map((props, index) => { | ||||
|                 if (props === 'break') { | ||||
|                   return index !== projectMenuItems.length - 1 ? ( | ||||
|                     <li key={`break-${index}`} className="contents"> | ||||
|                       <hr className="border-chalkboard-20 dark:border-chalkboard-80" /> | ||||
|                     </li> | ||||
|                   ) : null | ||||
|                 } | ||||
|  | ||||
|                 const { id, className, children, ...rest } = props | ||||
|                 return ( | ||||
|                   <li key={id} className="contents"> | ||||
|                     <ActionButton | ||||
|                       {...rest} | ||||
|                       className={ | ||||
|                         'relative !font-sans flex items-center gap-2 rounded-sm py-1.5 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left ' + | ||||
|                         className | ||||
|                       } | ||||
|                       onMouseUp={() => { | ||||
|                         close() | ||||
|                       }} | ||||
|                     > | ||||
|                       {children} | ||||
|                     </ActionButton> | ||||
|                   </li> | ||||
|                 ) | ||||
|               })} | ||||
|             </ul> | ||||
|           )} | ||||
|         </Popover.Panel> | ||||
|       </Transition> | ||||
|  | ||||
| @ -157,6 +157,7 @@ export const Stream = () => { | ||||
|   useEffect(() => { | ||||
|     setIsFirstRender(kclManager.isFirstRender) | ||||
|     if (!kclManager.isFirstRender) videoRef.current?.play() | ||||
|     setIsFreezeFrame(!kclManager.isFirstRender) | ||||
|   }, [kclManager.isFirstRender]) | ||||
|  | ||||
|   useEffect(() => { | ||||
|  | ||||
| @ -8,7 +8,9 @@ | ||||
|   --_delay: 200ms; | ||||
|   --_triangle-width: 8px; | ||||
|   --_triangle-height: 12px; | ||||
|   --_p-inline: calc(50% + calc(var(--isRTL) * var(--_triangle-width) / 2)); | ||||
|   --_p-inline-arrow-alignment: calc( | ||||
|     50% + calc(var(--isRTL) * var(--_triangle-width) / 2) | ||||
|   ); | ||||
|   --_p-block: 4px; | ||||
|   --_bg: var(--chalkboard-10); | ||||
|   --_shadow-alpha: 8%; | ||||
| @ -33,7 +35,7 @@ | ||||
|   font-weight: normal; | ||||
|   line-height: initial; | ||||
|   letter-spacing: 0; | ||||
|   padding: var(--_p-block) var(--_p-inline); | ||||
|   padding: var(--_p-block) calc(2 * var(--_p-block)); | ||||
|   margin: 0; | ||||
|   border-radius: 3px; | ||||
|   background: var(--_bg); | ||||
| @ -119,7 +121,7 @@ | ||||
| } | ||||
|  | ||||
| .tooltip.top-right { | ||||
|   inset-inline-end: var(--_p-inline); | ||||
|   inset-inline-end: var(--_p-inline-arrow-alignment); | ||||
|   inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-height)); | ||||
| } | ||||
|  | ||||
| @ -130,7 +132,7 @@ | ||||
| } | ||||
|  | ||||
| .tooltip.right { | ||||
|   inset-inline-start: calc(100% + var(--_p-inline) + var(--_triangle-height)); | ||||
|   inset-inline-start: calc(100% + var(--_triangle-height)); | ||||
|   inset-block-end: 50%; | ||||
|   --_y: 50%; | ||||
| } | ||||
| @ -142,7 +144,7 @@ | ||||
| } | ||||
|  | ||||
| .tooltip.bottom-right { | ||||
|   inset-inline-end: var(--_p-inline); | ||||
|   inset-inline-end: var(--_p-inline-arrow-alignment); | ||||
|   inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-height)); | ||||
| } | ||||
|  | ||||
| @ -165,7 +167,7 @@ | ||||
| } | ||||
|  | ||||
| .tooltip.bottom-left { | ||||
|   inset-inline-start: var(--_p-inline); | ||||
|   inset-inline-start: var(--_p-inline-arrow-alignment); | ||||
|   inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-height)); | ||||
| } | ||||
|  | ||||
| @ -176,7 +178,9 @@ | ||||
| } | ||||
|  | ||||
| .tooltip.left { | ||||
|   inset-inline-end: calc(100% + var(--_p-inline) + var(--_triangle-height)); | ||||
|   inset-inline-end: calc( | ||||
|     100% + var(--_p-inline-arrow-alignment) + var(--_triangle-height) | ||||
|   ); | ||||
|   inset-block-end: 50%; | ||||
|   --_y: 50%; | ||||
| } | ||||
| @ -188,7 +192,7 @@ | ||||
| } | ||||
|  | ||||
| .tooltip.top-left { | ||||
|   inset-inline-start: var(--_p-inline); | ||||
|   inset-inline-start: var(--_p-inline-arrow-alignment); | ||||
|   inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-height)); | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -25,11 +25,11 @@ export function UnitsMenu() { | ||||
|           border border-solid border-chalkboard-10 dark:border-chalkboard-90 rounded | ||||
|           shadow-lg`} | ||||
|           > | ||||
|             <ul className="relative flex flex-col gap-0.5 items-stretch content-stretch"> | ||||
|             <ul className="relative flex flex-col items-stretch content-stretch p-0.5"> | ||||
|               {baseUnitsUnion.map((unit) => ( | ||||
|                 <li key={unit} className="contents"> | ||||
|                   <button | ||||
|                     className="flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left" | ||||
|                     className="flex items-center gap-2 m-0 py-1.5 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left" | ||||
|                     onClick={() => { | ||||
|                       settings.send({ | ||||
|                         type: 'set.modeling.defaultUnit', | ||||
|  | ||||
| @ -1,18 +1,20 @@ | ||||
| import { Popover, Transition } from '@headlessui/react' | ||||
| import { ActionButton } from './ActionButton' | ||||
| import { faBug, faSignOutAlt } from '@fortawesome/free-solid-svg-icons' | ||||
| import { faGithub } from '@fortawesome/free-brands-svg-icons' | ||||
| import { ActionButton, ActionButtonProps } from './ActionButton' | ||||
| import { useLocation, useNavigate } from 'react-router-dom' | ||||
| import { Fragment, useState } from 'react' | ||||
| import { Fragment, useMemo, useState } from 'react' | ||||
| import { paths } from 'lib/paths' | ||||
| import { Models } from '@kittycad/lib' | ||||
| import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' | ||||
| import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' | ||||
| import Tooltip from './Tooltip' | ||||
| import usePlatform from 'hooks/usePlatform' | ||||
| import { isTauri } from 'lib/isTauri' | ||||
| import { CustomIcon } from './CustomIcon' | ||||
|  | ||||
| type User = Models['User_type'] | ||||
|  | ||||
| const UserSidebarMenu = ({ user }: { user?: User }) => { | ||||
|   const platform = usePlatform() | ||||
|   const location = useLocation() | ||||
|   const filePath = useAbsoluteFilePath() | ||||
|   const displayedName = getDisplayName(user) | ||||
| @ -20,6 +22,128 @@ const UserSidebarMenu = ({ user }: { user?: User }) => { | ||||
|   const navigate = useNavigate() | ||||
|   const send = useSettingsAuthContext()?.auth?.send | ||||
|  | ||||
|   // We filter this memoized list so that no orphan "break" elements are rendered. | ||||
|   const userMenuItems = useMemo<(ActionButtonProps | 'break')[]>( | ||||
|     () => | ||||
|       [ | ||||
|         { | ||||
|           id: 'settings', | ||||
|           Element: 'button', | ||||
|           children: ( | ||||
|             <> | ||||
|               <span className="flex-1">User settings</span> | ||||
|               <kbd className="hotkey">{`${platform === 'macos' ? '⌘' : 'Ctrl'}${ | ||||
|                 isTauri() ? '' : '⬆' | ||||
|               },`}</kbd> | ||||
|             </> | ||||
|           ), | ||||
|           'data-testid': 'user-settings', | ||||
|           onClick: () => { | ||||
|             const targetPath = location.pathname.includes(paths.FILE) | ||||
|               ? filePath + paths.SETTINGS | ||||
|               : paths.HOME + paths.SETTINGS | ||||
|             navigate(targetPath + '?tab=user') | ||||
|           }, | ||||
|         }, | ||||
|         { | ||||
|           id: 'keybindings', | ||||
|           Element: 'button', | ||||
|           children: 'Keyboard shortcuts', | ||||
|           onClick: () => { | ||||
|             const targetPath = location.pathname.includes(paths.FILE) | ||||
|               ? filePath + paths.SETTINGS | ||||
|               : paths.HOME + paths.SETTINGS | ||||
|             navigate(targetPath + '?tab=keybindings') | ||||
|           }, | ||||
|         }, | ||||
|         { | ||||
|           id: 'account', | ||||
|           Element: 'externalLink', | ||||
|           to: 'https://zoo.dev/account', | ||||
|           children: ( | ||||
|             <> | ||||
|               <span className="flex-1">Manage account</span> | ||||
|               <CustomIcon | ||||
|                 name="link" | ||||
|                 className="w-3 h-3 text-chalkboard-70 dark:text-chalkboard-40" | ||||
|               /> | ||||
|             </> | ||||
|           ), | ||||
|         }, | ||||
|         'break', | ||||
|         { | ||||
|           id: 'request-feature', | ||||
|           Element: 'externalLink', | ||||
|           to: 'https://github.com/KittyCAD/modeling-app/discussions', | ||||
|           children: ( | ||||
|             <> | ||||
|               <span className="flex-1">Request a feature</span> | ||||
|               <CustomIcon | ||||
|                 name="link" | ||||
|                 className="w-3 h-3 text-chalkboard-70 dark:text-chalkboard-40" | ||||
|               /> | ||||
|             </> | ||||
|           ), | ||||
|         }, | ||||
|         { | ||||
|           id: 'report-bug', | ||||
|           Element: 'externalLink', | ||||
|           to: 'https://github.com/KittyCAD/modeling-app/issues/new/choose', | ||||
|           children: ( | ||||
|             <> | ||||
|               <span className="flex-1">Report a bug</span> | ||||
|               <CustomIcon | ||||
|                 name="link" | ||||
|                 className="w-3 h-3 text-chalkboard-70 dark:text-chalkboard-40" | ||||
|               /> | ||||
|             </> | ||||
|           ), | ||||
|         }, | ||||
|         { | ||||
|           id: 'community', | ||||
|           Element: 'externalLink', | ||||
|           to: 'https://discord.gg/JQEpHR7Nt2', | ||||
|           children: ( | ||||
|             <> | ||||
|               <span className="flex-1">Ask the community</span> | ||||
|               <CustomIcon | ||||
|                 name="link" | ||||
|                 className="w-3 h-3 text-chalkboard-70 dark:text-chalkboard-40" | ||||
|               /> | ||||
|             </> | ||||
|           ), | ||||
|         }, | ||||
|         { | ||||
|           id: 'release-notes', | ||||
|           Element: 'externalLink', | ||||
|           to: 'https://github.com/KittyCAD/modeling-app/releases', | ||||
|           children: ( | ||||
|             <> | ||||
|               <span className="flex-1">Release notes</span> | ||||
|               <CustomIcon | ||||
|                 name="link" | ||||
|                 className="w-3 h-3 text-chalkboard-70 dark:text-chalkboard-40" | ||||
|               /> | ||||
|             </> | ||||
|           ), | ||||
|         }, | ||||
|         'break', | ||||
|         { | ||||
|           id: 'sign-out', | ||||
|           Element: 'button', | ||||
|           'data-testid': 'user-sidebar-sign-out', | ||||
|           children: 'Sign out', | ||||
|           onClick: () => send('Log out'), | ||||
|           className: '', // Just making TS's filter type coercion happy 😠 | ||||
|         }, | ||||
|       ].filter( | ||||
|         (props) => | ||||
|           props === 'break' || | ||||
|           (typeof props !== 'string' && !props.className?.includes('hidden')) | ||||
|       ) as (ActionButtonProps | 'break')[], | ||||
|     [platform, location, filePath, navigate, send] | ||||
|   ) | ||||
|  | ||||
|   // This image host goes down sometimes. We will instead rewrite the | ||||
|   // resource to be a local one. | ||||
|   if (user?.image === 'https://placekitten.com/200/200') { | ||||
| @ -43,139 +167,90 @@ const UserSidebarMenu = ({ user }: { user?: User }) => { | ||||
|  | ||||
|   return ( | ||||
|     <Popover className="relative"> | ||||
|       {user?.image && !imageLoadFailed ? ( | ||||
|         <Popover.Button | ||||
|           className="relative border-0 rounded-full w-fit min-w-max p-0 group" | ||||
|           data-testid="user-sidebar-toggle" | ||||
|         > | ||||
|           <div className="rounded-full border overflow-hidden"> | ||||
|             <img | ||||
|               src={user?.image || ''} | ||||
|               alt={user?.name || ''} | ||||
|               className="h-8 w-8 rounded-full" | ||||
|               referrerPolicy="no-referrer" | ||||
|               onError={() => setImageLoadFailed(true)} | ||||
|             /> | ||||
|           </div> | ||||
|           <Tooltip position="bottom-right" delay={1000}> | ||||
|             User menu | ||||
|           </Tooltip> | ||||
|         </Popover.Button> | ||||
|       ) : ( | ||||
|         <ActionButton | ||||
|           Element={Popover.Button} | ||||
|           iconStart={{ icon: 'menu' }} | ||||
|           className="border-transparent !px-0" | ||||
|           data-testid="user-sidebar-toggle" | ||||
|         > | ||||
|           <Tooltip position="bottom-right" delay={1000}> | ||||
|             User menu | ||||
|           </Tooltip> | ||||
|         </ActionButton> | ||||
|       )} | ||||
|       <Transition | ||||
|         enter="duration-200 ease-out" | ||||
|         enterFrom="opacity-0" | ||||
|         enterTo="opacity-100" | ||||
|         leave="duration-100 ease-in" | ||||
|         leaveFrom="opacity-100" | ||||
|         leaveTo="opacity-0" | ||||
|         as={Fragment} | ||||
|       <Popover.Button | ||||
|         className="relative group border-0 w-fit min-w-max p-0 rounded-l-full focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary" | ||||
|         data-testid="user-sidebar-toggle" | ||||
|       > | ||||
|         <Popover.Overlay className="fixed z-20 inset-0 bg-chalkboard-110/50" /> | ||||
|       </Transition> | ||||
|  | ||||
|         <div className="flex items-center"> | ||||
|           <div className="rounded-full border overflow-hidden"> | ||||
|             {user?.image && !imageLoadFailed ? ( | ||||
|               <img | ||||
|                 src={user?.image || ''} | ||||
|                 alt={user?.name || ''} | ||||
|                 className="h-7 w-7 rounded-full" | ||||
|                 referrerPolicy="no-referrer" | ||||
|                 onError={() => setImageLoadFailed(true)} | ||||
|               /> | ||||
|             ) : ( | ||||
|               <CustomIcon | ||||
|                 name="person" | ||||
|                 className="w-5 h-5 text-chalkboard-70 dark:text-chalkboard-40 bg-chalkboard-20 dark:bg-chalkboard-80" | ||||
|               /> | ||||
|             )} | ||||
|           </div> | ||||
|           <CustomIcon | ||||
|             name="caretDown" | ||||
|             className="w-4 h-4 text-chalkboard-70 dark:text-chalkboard-40 ui-open:rotate-180" | ||||
|           /> | ||||
|         </div> | ||||
|         <Tooltip position="bottom-right" delay={1000} hoverOnly> | ||||
|           User menu | ||||
|         </Tooltip> | ||||
|       </Popover.Button> | ||||
|       <Transition | ||||
|         enter="duration-100 ease-out" | ||||
|         enterFrom="opacity-0 translate-x-1/4" | ||||
|         enterTo="opacity-100 translate-x-0" | ||||
|         leave="duration-75 ease-in" | ||||
|         leaveFrom="opacity-100 translate-x-0" | ||||
|         leaveTo="opacity-0 translate-x-4" | ||||
|         enterFrom="opacity-0 -translate-y-2" | ||||
|         enterTo="opacity-100 translate-y-0" | ||||
|         as={Fragment} | ||||
|       > | ||||
|         <Popover.Panel className="fixed inset-0 left-auto z-30 w-64 bg-chalkboard-10 dark:bg-chalkboard-90 border border-chalkboard-30 dark:border-chalkboard-80 shadow-md rounded-l-md overflow-hidden"> | ||||
|         <Popover.Panel | ||||
|           className={`z-10 absolute top-full right-0 mt-1 pb-1 w-48 bg-chalkboard-10 dark:bg-chalkboard-90 | ||||
|           border border-solid border-chalkboard-20 dark:border-chalkboard-90 rounded | ||||
|           shadow-lg`} | ||||
|         > | ||||
|           {({ close }) => ( | ||||
|             <> | ||||
|               {user && ( | ||||
|                 <div className="flex items-center gap-4 px-4 py-3 bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80"> | ||||
|                   {user.image && !imageLoadFailed && ( | ||||
|                     <div className="rounded-full shadow-inner overflow-hidden"> | ||||
|                       <img | ||||
|                         src={user.image} | ||||
|                         alt={user.name || ''} | ||||
|                         className="h-8 w-8" | ||||
|                         referrerPolicy="no-referrer" | ||||
|                         onError={() => setImageLoadFailed(true)} | ||||
|                       /> | ||||
|                     </div> | ||||
|                   )} | ||||
|  | ||||
|                   <div> | ||||
|                     <p className="m-0 text-mono" data-testid="username"> | ||||
|                       {displayedName || ''} | ||||
|                 <div className="flex flex-col gap-1 px-2.5 py-3 bg-chalkboard-20 dark:bg-chalkboard-80/50"> | ||||
|                   <p className="m-0 text-mono text-xs" data-testid="username"> | ||||
|                     {displayedName || ''} | ||||
|                   </p> | ||||
|                   {displayedName !== user.email && ( | ||||
|                     <p | ||||
|                       className="m-0 text-chalkboard-70 dark:text-chalkboard-40 text-xs" | ||||
|                       data-testid="email" | ||||
|                     > | ||||
|                       {user.email} | ||||
|                     </p> | ||||
|                     {displayedName !== user.email && ( | ||||
|                       <p | ||||
|                         className="m-0 text-chalkboard-70 dark:text-chalkboard-40 text-xs" | ||||
|                         data-testid="email" | ||||
|                       > | ||||
|                         {user.email} | ||||
|                       </p> | ||||
|                     )} | ||||
|                   </div> | ||||
|                   )} | ||||
|                 </div> | ||||
|               )} | ||||
|               <div className="p-4 flex flex-col gap-2"> | ||||
|                 <ActionButton | ||||
|                   Element="button" | ||||
|                   iconStart={{ icon: 'settings' }} | ||||
|                   className="border-transparent dark:border-transparent hover:bg-transparent" | ||||
|                   onClick={() => { | ||||
|                     // since /settings is a nested route the sidebar doesn't close | ||||
|                     // automatically when navigating to it | ||||
|                     close() | ||||
|                     const targetPath = location.pathname.includes(paths.FILE) | ||||
|                       ? filePath + paths.SETTINGS | ||||
|                       : paths.HOME + paths.SETTINGS | ||||
|                     navigate(targetPath) | ||||
|                   }} | ||||
|                   data-testid="settings-button" | ||||
|                 > | ||||
|                   Settings | ||||
|                 </ActionButton> | ||||
|                 <ActionButton | ||||
|                   Element="externalLink" | ||||
|                   to="https://github.com/KittyCAD/modeling-app/discussions" | ||||
|                   iconStart={{ icon: faGithub, className: 'p-1', size: 'sm' }} | ||||
|                   className="border-transparent dark:border-transparent" | ||||
|                 > | ||||
|                   Request a feature | ||||
|                 </ActionButton> | ||||
|                 <ActionButton | ||||
|                   Element="externalLink" | ||||
|                   to="https://github.com/KittyCAD/modeling-app/issues/new/choose" | ||||
|                   iconStart={{ icon: faBug, className: 'p-1', size: 'sm' }} | ||||
|                   className="border-transparent dark:border-transparent" | ||||
|                 > | ||||
|                   Report a bug | ||||
|                 </ActionButton> | ||||
|                 <ActionButton | ||||
|                   Element="button" | ||||
|                   onClick={() => send('Log out')} | ||||
|                   iconStart={{ | ||||
|                     icon: faSignOutAlt, | ||||
|                     className: 'p-1', | ||||
|                     bgClassName: '!bg-transparent', | ||||
|                     size: 'sm', | ||||
|                     iconClassName: '!text-destroy-70', | ||||
|                   }} | ||||
|                   className="border-transparent dark:border-transparent hover:border-destroy-40 dark:hover:border-destroy-60 hover:bg-destroy-10/20 dark:hover:bg-destroy-80/20" | ||||
|                   data-testid="user-sidebar-sign-out" | ||||
|                 > | ||||
|                   Sign out | ||||
|                 </ActionButton> | ||||
|               </div> | ||||
|               <ul className="relative flex flex-col items-stretch content-stretch p-0.5"> | ||||
|                 {userMenuItems.map((props, index) => { | ||||
|                   if (props === 'break') { | ||||
|                     return index !== userMenuItems.length - 1 ? ( | ||||
|                       <li key={`break-${index}`} className="contents"> | ||||
|                         <hr className="border-chalkboard-20 dark:border-chalkboard-80" /> | ||||
|                       </li> | ||||
|                     ) : null | ||||
|                   } | ||||
|  | ||||
|                   const { id, children, ...rest } = props | ||||
|                   return ( | ||||
|                     <li key={id} className="contents"> | ||||
|                       <ActionButton | ||||
|                         {...rest} | ||||
|                         className="!font-sans flex items-center gap-2 rounded-sm py-1.5 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left" | ||||
|                         onMouseUp={() => { | ||||
|                           close() | ||||
|                         }} | ||||
|                       > | ||||
|                         {children} | ||||
|                       </ActionButton> | ||||
|                     </li> | ||||
|                   ) | ||||
|                 })} | ||||
|               </ul> | ||||
|             </> | ||||
|           )} | ||||
|         </Popover.Panel> | ||||
|  | ||||
| @ -45,9 +45,6 @@ export function useSetupEngineManager( | ||||
|       streamRef?.current?.offsetWidth ?? 0, | ||||
|       streamRef?.current?.offsetHeight ?? 0 | ||||
|     ) | ||||
|     if (restart) { | ||||
|       kclManager.isFirstRender = false | ||||
|     } | ||||
|     engineCommandManager.start({ | ||||
|       restart, | ||||
|       setMediaStream: (mediaStream) => setMediaStream(mediaStream), | ||||
|  | ||||
| @ -260,8 +260,17 @@ code { | ||||
|  | ||||
| @layer components { | ||||
|   kbd.hotkey { | ||||
|     @apply font-mono text-xs inline-block px-1 py-0.5 rounded-sm; | ||||
|     @apply font-mono text-xs inline-block px-0.5 py-[2px] rounded; | ||||
|  | ||||
|     /* This is the only place in our code where layout is impacted by theme. | ||||
|      * We may not want that later, if hotkeys are possibly visible  | ||||
|      * while switching theme, but more padding feels better in dark mode. | ||||
|      */ | ||||
|     @apply dark:px-1; | ||||
|  | ||||
|     @apply text-chalkboard-70 dark:text-chalkboard-40; | ||||
|     @apply bg-chalkboard-20 dark:bg-chalkboard-90; | ||||
|     @apply border border-t-0 border-b-2 border-chalkboard-30 dark:border-chalkboard-80; | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -160,19 +160,13 @@ impl EngineConnection { | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     #[allow(clippy::field_reassign_with_default)] | ||||
|     pub async fn new(ws: reqwest::Upgraded) -> Result<EngineConnection> { | ||||
|         // allowing the field_reassign_with_default lint here because the | ||||
|         // defaults for this object don't match the type defaults. We want | ||||
|         // to inherent the default config | ||||
|         // | ||||
|         // See the `impl Default for WebSocketConfig` in | ||||
|         // `tungstenite/protocol/mod.rs` | ||||
|  | ||||
|         let mut wsconfig = tokio_tungstenite::tungstenite::protocol::WebSocketConfig::default(); | ||||
|         // 4294967296 bytes, which is around 4.2 GB. | ||||
|         wsconfig.max_message_size = Some(0x100000000); | ||||
|         wsconfig.max_frame_size = Some(0x100000000); | ||||
|         let wsconfig = tokio_tungstenite::tungstenite::protocol::WebSocketConfig { | ||||
|             // 4294967296 bytes, which is around 4.2 GB. | ||||
|             max_message_size: Some(0x100000000), | ||||
|             max_frame_size: Some(0x100000000), | ||||
|             ..Default::default() | ||||
|         }; | ||||
|  | ||||
|         let ws_stream = tokio_tungstenite::WebSocketStream::from_raw_socket( | ||||
|             ws, | ||||
|  | ||||
| @ -116,7 +116,7 @@ pub async fn pattern_transform(args: Args) -> Result<MemoryItem, KclError> { | ||||
| /// // Each layer is just a pretty thin cylinder. | ||||
| /// fn layer = () => { | ||||
| ///   return startSketchOn("XY") // or some other plane idk | ||||
| ///     |> circle([0, 0], 1, %, 'tag1') | ||||
| ///     |> circle([0, 0], 1, %, $tag1) | ||||
| ///     |> extrude(h, %) | ||||
| /// } | ||||
| /// // The vase is 100 layers tall. | ||||
|  | ||||