Add proper menu

related to #360
This commit is contained in:
Kurt Hutten
2021-06-12 19:20:24 +10:00
parent 6ad731d158
commit 3c18a24cb6
11 changed files with 242 additions and 127 deletions

View File

@@ -42,6 +42,7 @@
"react-helmet": "^6.1.0",
"react-image-crop": "^8.6.6",
"react-mosaic-component": "^4.1.1",
"react-tabs": "^3.2.2",
"react-three-fiber": "^5.3.19",
"rich-markdown-editor": "^11.0.2",
"styled-components": "^5.2.0",

View File

@@ -1,6 +1,20 @@
import { useContext } from 'react'
import { Menu } from '@headlessui/react'
import { IdeContext } from 'src/components/IdeToolbarNew/IdeToolbarNew'
import Svg from 'src/components/Svg/Svg'
import { useRender } from 'src/components/IdeToolbarNew/useRender'
import {makeStlDownloadHandler, PullTitleFromFirstLine} from './helpers'
const EditorMenu = () => {
const handleRender = useRender()
const { state, thunkDispatch } = useContext(IdeContext)
const handleStlDownload = makeStlDownloadHandler({
type: state.objectData?.type,
geometry: state.objectData?.data,
fileName: PullTitleFromFirstLine(state.code || ''),
thunkDispatch,
})
return (
<div className="bg-gray-500 flex items-center h-9 w-full cursor-grab">
<div className=" text-gray-500 bg-gray-300 cursor-grab px-2 h-full flex items-center">
@@ -14,9 +28,10 @@ const EditorMenu = () => {
</button>
<div className="w-px h-full bg-gray-300"/>
<div className="flex gap-6 px-6">
<button className="text-gray-100">
File
</button>
<FileDropdown
handleRender={handleRender}
handleStlDownload={handleStlDownload}
/>
<button className="text-gray-100">
Edit
</button>
@@ -29,3 +44,37 @@ const EditorMenu = () => {
}
export default EditorMenu
function FileDropdown({handleRender, handleStlDownload}) {
return (
<Menu>
<Menu.Button className="text-gray-100">File</Menu.Button>
<Menu.Items className="absolute flex flex-col mt-10 bg-gray-500 rounded shadow-md text-gray-100">
<Menu.Item>
{({ active }) => (
<button
className={`${active && 'bg-gray-600'} px-2 py-1`}
onClick={handleRender}
>
Save &amp; Render <span className="text-gray-400 pl-4">{
/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ?
<><Svg name="mac-cmd-key" className="h-3 w-3 inline-block text-left" />S</> :
'Ctrl S'
}</span>
</button>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<button
className={`${active && 'bg-gray-600'} px-2 py-1 text-left`}
onClick={handleStlDownload}
>
Download STL
</button>
)}
</Menu.Item>
</Menu.Items>
</Menu>
)
}

View File

@@ -0,0 +1,59 @@
import { flow, identity } from 'lodash/fp'
import { fileSave } from 'browser-fs-access'
import { MeshBasicMaterial, Mesh, Scene } from 'three'
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter'
import { requestRender } from 'src/helpers/hooks/useIdeState'
export const PullTitleFromFirstLine = (code: string = '') => {
const firstLine = code.split('\n').filter(identity)[0] || ''
if (!(firstLine.startsWith('//') || firstLine.startsWith('#'))) {
return 'object.stl'
}
return (
(firstLine.replace(/^(\/\/|#)\s*(.+)/, (_, __, titleWithSpaces) =>
titleWithSpaces.replaceAll(/\s/g, '-')
) || 'object') + '.stl'
)
}
export const makeStlDownloadHandler = (({ geometry, fileName, type, thunkDispatch}) => () => {
const makeStlBlobFromGeo = flow(
(geo) => new Mesh(geo, new MeshBasicMaterial()),
(mesh) => new Scene().add(mesh),
(scene) => new STLExporter().parse(scene),
(stl) =>
new Blob([stl], {
type: 'text/plain',
})
)
const saveFile = (geometry) => {
const blob = makeStlBlobFromGeo(geometry)
fileSave(blob, {
fileName,
extensions: ['.stl'],
})
}
if (geometry) {
if (type === 'geometry') {
saveFile(geometry)
} else {
thunkDispatch((dispatch, getState) => {
const state = getState()
if (state.ideType === 'openScad') {
thunkDispatch((dispatch, getState) => {
const state = getState()
dispatch({ type: 'setLoading' })
requestRender({
state,
dispatch,
code: state.code,
viewerSize: state.viewerSize,
camera: state.camera,
specialCadProcess: 'stl',
}).then((result) => result && saveFile(result.data))
})
}
})
}
}
})

View File

@@ -1,23 +1,57 @@
import Svg from 'src/components/Svg/Svg'
import { Popover } from '@headlessui/react'
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import { copyTextToClipboard } from 'src/helpers/clipboard'
import { encode } from 'src/helpers/compress'
const TopButton = ({children}) => (
<button className="flex bg-gray-500 h-10 justify-center items-center px-4 rounded">
const TopButton = ({children, onClick}: {onClick?: () => void, children: React.ReactNode}) => (
<button onClick={onClick} className="flex bg-gray-500 h-10 justify-center items-center px-4 rounded">
<div className="rounded-full bg-gray-200 h-6 w-6 mr-4"/>
{children}
</button>
)
const IdeHeader = () => {
const IdeHeader = ({handleRender}: {handleRender: () => void}) => {
return (
<div className="h-16 bg-gray-900 flex justify-between items-center">
<div className="h-16 w-full bg-gray-900 flex justify-between items-center">
<div className="bg-gray-700 pr-48 h-full">
<div className="w-16 h-full flex items-center justify-center bg-gray-900">
<Svg className="w-12" name="favicon" />
</div>
</div>
<div className="text-gray-200 flex gap-4 mr-4">
<TopButton>Render</TopButton>
<TopButton onClick={handleRender}>Render</TopButton>
<Popover className="relative outline-none w-full h-full">
{({open}) => {
const encodedLink = makeEncodedLink('bing bong')
return (
<>
<Popover.Button className="h-full w-full outline-none">
<TopButton>Share</TopButton>
</Popover.Button>
{open && <Popover.Panel className="absolute z-10 mt-4 right-0">
<Tabs
className="bg-gray-300 rounded-md shadow-md overflow-hidden text-gray-700"
selectedTabClassName="bg-gray-200"
>
<TabPanel className="p-4">
<p className="text-sm pb-4 border-b border-gray-700">Encodes your CodeCad script into a URL so that you can share your work</p>
<input value={encodedLink.replace(/^.+:\/\//g, '')} readOnly className="p-1 mt-4 text-xs rounded-t border border-gray-700 w-full" />
<button className="w-full bg-gray-700 py-1 rounded-b text-gray-300" onClick={() => copyTextToClipboard(encodedLink)} >Copy URL</button>
</TabPanel>
<TabPanel>
<h2 className="h-32">Any content 2</h2>
</TabPanel>
<TabList className="flex whitespace-nowrap text-gray-700 border-t border-gray-700">
<Tab className="p-3 px-5">encoded script</Tab>
<Tab className="p-3 px-5">external script</Tab>
</TabList>
</Tabs>
</Popover.Panel>}
</>
)
}}
</Popover>
<TopButton>Fork</TopButton>
</div>
</div>
@@ -25,3 +59,10 @@ const IdeHeader = () => {
}
export default IdeHeader
const scriptKeyV2 = 'encoded_script_v2' // todo don't leave here
function makeEncodedLink(code: string): string {
const encodedScript = encode(code)
return `${location.href}#${scriptKeyV2}=${encodedScript}`
}

View File

@@ -2,7 +2,10 @@ import Svg from 'src/components/Svg/Svg'
const IdeSideBar = () => {
return (
<div className="h-full flex flex-col justify-end">
<div className="h-full flex flex-col justify-between">
<div className="w-16 h-16 flex items-center justify-center bg-gray-900">
<Svg className="w-12" name="favicon" />
</div>
<button className=" text-gray-300 p-2 pb-4 flex justify-center" aria-label="IDE settings">
<Svg name="big-gear" />
</button>

View File

@@ -1,16 +1,13 @@
import { createContext, useEffect } from 'react'
import IdeContainer from 'src/components/IdeContainer'
import { isBrowser } from '@redwoodjs/prerender/browserUtils'
import { useIdeState, makeCodeStoreKey } from 'src/helpers/hooks/useIdeState'
import { copyTextToClipboard } from 'src/helpers/clipboard'
import { requestRender } from 'src/helpers/hooks/useIdeState'
import { encode, decode } from 'src/helpers/compress'
import { flow, identity } from 'lodash/fp'
import { fileSave } from 'browser-fs-access'
import { MeshBasicMaterial, Mesh, Scene } from 'three'
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter'
import { useIdeState } from 'src/helpers/hooks/useIdeState'
import { handleRenderVerbose } from './useRender'
import { decode } from 'src/helpers/compress'
import { flow } from 'lodash/fp'
import OutBound from 'src/components/OutBound'
import IdeSideBar from 'src/components/IdeSideBar'
import IdeHeader from 'src/components/IdeHeader'
export const githubSafe = (url) =>
url.includes('github.com')
@@ -65,83 +62,8 @@ const IdeToolbarNew = ({ cadPackage }) => {
window.location.hash = ''
}, [cadPackage])
function handleRender() {
thunkDispatch((dispatch, getState) => {
const state = getState()
dispatch({ type: 'setLoading' })
requestRender({
state,
dispatch,
code: state.code,
viewerSize: state.viewerSize,
camera: state.camera,
})
})
localStorage.setItem(makeCodeStoreKey(state.ideType), state.code)
return handleRenderVerbose({thunkDispatch, state})
}
function handleMakeLink() {
if (isBrowser) {
const encodedScript = encode(state.code)
window.location.hash = `${scriptKeyV2}=${encodedScript}`
copyTextToClipboard(window.location.href)
}
}
const PullTitleFromFirstLine = (code = '') => {
const firstLine = code.split('\n').filter(identity)[0] || ''
if (!(firstLine.startsWith('//') || firstLine.startsWith('#'))) {
return 'object.stl'
}
return (
(firstLine.replace(/^(\/\/|#)\s*(.+)/, (_, __, titleWithSpaces) =>
titleWithSpaces.replaceAll(/\s/g, '-')
) || 'object') + '.stl'
)
}
const handleStlDownload = (({ geometry, fileName, type }) => () => {
const makeStlBlobFromGeo = flow(
(geo) => new Mesh(geo, new MeshBasicMaterial()),
(mesh) => new Scene().add(mesh),
(scene) => new STLExporter().parse(scene),
(stl) =>
new Blob([stl], {
type: 'text/plain',
})
)
const saveFile = (geometry) => {
const blob = makeStlBlobFromGeo(geometry)
fileSave(blob, {
fileName,
extensions: ['.stl'],
})
}
if (geometry) {
if (type === 'geometry') {
saveFile(geometry)
} else {
thunkDispatch((dispatch, getState) => {
const state = getState()
if (state.ideType === 'openScad') {
thunkDispatch((dispatch, getState) => {
const state = getState()
dispatch({ type: 'setLoading' })
requestRender({
state,
dispatch,
code: state.code,
viewerSize: state.viewerSize,
camera: state.camera,
specialCadProcess: 'stl',
}).then((result) => result && saveFile(result.data))
})
}
})
}
}
})({
type: state.objectData?.type,
geometry: state.objectData?.data,
fileName: PullTitleFromFirstLine(state.code),
})
return (
<IdeContext.Provider value={{ state, thunkDispatch }}>
@@ -150,6 +72,9 @@ const IdeToolbarNew = ({ cadPackage }) => {
<IdeSideBar />
</div>
<div className="h-full flex flex-grow flex-col">
<nav className="flex">
<IdeHeader handleRender={handleRender} />
</nav>
<div className="py-2 bg-pink-200">
<div className="mx-auto max-w-3xl">
We're still working on this. Since you're here, have a look what{' '}
@@ -162,26 +87,6 @@ const IdeToolbarNew = ({ cadPackage }) => {
.
</div>
</div>
<nav className="flex">
<button
onClick={handleRender}
className="border-2 px-2 text-gray-700 text-sm m-1"
>
Render
</button>
<button
onClick={handleMakeLink}
className="border-2 text-gray-700 px-2 text-sm m-1 ml-2"
>
Copy link
</button>
<button
onClick={handleStlDownload}
className="border-2 text-gray-700 px-2 text-sm m-1 ml-2"
>
Download STL
</button>
</nav>
<IdeContainer />
</div>
</div>

View File

@@ -0,0 +1,25 @@
import { makeCodeStoreKey } from 'src/helpers/hooks/useIdeState'
import { requestRender } from 'src/helpers/hooks/useIdeState'
import { useContext } from 'react'
import { IdeContext } from 'src/components/IdeToolbarNew/IdeToolbarNew'
export const handleRenderVerbose = ({thunkDispatch, state}) => {
thunkDispatch((dispatch, getState) => {
const state = getState()
dispatch({ type: 'setLoading' })
requestRender({
state,
dispatch,
code: state.code,
viewerSize: state.viewerSize,
camera: state.camera,
})
})
localStorage.setItem(makeCodeStoreKey(state.ideType), state.code)
}
export const useRender = () => {
const { state, thunkDispatch } = useContext(IdeContext)
return () => handleRenderVerbose({thunkDispatch, state})
}

View File

@@ -14,6 +14,7 @@ type SvgNames = 'arrow-down' |
'gear' |
'lightbulb' |
'logout' |
'mac-cmd-key' |
'pencil' |
'plus' |
'plus-circle' |
@@ -323,6 +324,18 @@ const Svg = ({ name, className: className2 = '', strokeWidth = 2 }: {
/>
</svg>
),
'mac-cmd-key': (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 220 220"
>
<
path
fill="currentColor"
d="M57.254,121.064c-17.735,0-31.45,4.723-40.765,14.035c-19.39,19.391-19.39,50.943,0,70.334 C25.884,214.828,38.375,220,51.66,220c0,0,0,0,0,0c13.284,0,25.774-5.172,35.168-14.566c11.069-11.07,14.037-27.061,14.041-41.016 c0.001-3.531,0.017-10.09,0.017-10.09h18.227c0,0,0.016,6.559,0.018,10.09c0.004,13.955,2.971,29.945,14.041,41.016 C142.565,214.828,155.056,220,168.34,220c13.285,0,25.775-5.172,35.17-14.566c19.391-19.391,19.391-50.943,0-70.334 c-9.314-9.312-23.029-14.035-40.764-14.035c-3.602,0-10.346,0.02-10.346,0.02c0-0.932,0-22.178,0-22.178s6.744,0.029,10.346,0.029 c17.734,0,31.449-4.721,40.762-14.033c19.391-19.392,19.391-50.943,0.002-70.334C194.113,5.174,181.624,0,168.34,0 c-13.285,0-25.773,5.174-35.168,14.566c-10.67,10.672-13.812,25.914-14.029,39.498l-0.193,11.609H101.05l-0.192-11.609 c-0.217-13.584-3.359-28.826-14.03-39.498C77.434,5.174,64.944,0,51.66,0C38.376,0,25.886,5.174,16.49,14.568 C-2.899,33.959-2.899,65.51,16.491,84.902c9.314,9.313,23.028,14.033,40.762,14.033c3.601,0,10.346-0.029,10.346-0.029 s0,21.246,0,22.178C67.6,121.084,60.855,121.064,57.254,121.064z M154.328,35.587c3.727-3.726,8.683-5.779,13.954-5.779 s10.229,2.053,13.957,5.781c7.692,7.693,7.692,20.213-0.002,27.906c-3.384,3.385-10.327,5.248-19.549,5.248 c-4.6,0-14.107,0-14.107,0v-8.688C148.581,60.056,147.566,41.495,154.328,35.587z M148.581,159.945v-8.688c0,0,9.508,0,14.107,0 c9.222,0,16.165,1.863,19.549,5.248c7.694,7.693,7.694,20.213,0.002,27.906c-3.729,3.729-8.686,5.781-13.957,5.781 s-10.228-2.053-13.954-5.779C147.566,178.506,148.581,159.945,148.581,159.945z M93.75,93.75h32.5v32.5h-32.5V93.75z M57.312,68.743 c-9.222,0-16.165-1.863-19.549-5.248c-7.694-7.693-7.694-20.213-0.002-27.906c3.729-3.729,8.686-5.781,13.957-5.781 s10.228,2.053,13.954,5.779c6.762,5.908,5.747,24.469,5.747,24.469v8.688C71.419,68.743,61.911,68.743,57.312,68.743z M71.419,151.258v8.688c0,0,1.015,18.561-5.747,24.469c-3.727,3.727-8.683,5.779-13.954,5.779s-10.229-2.053-13.957-5.781 c-7.692-7.693-7.692-20.213,0.002-27.906c3.384-3.385,10.327-5.248,19.549-5.248C61.911,151.258,71.419,151.258,71.419,151.258z"
/>
</svg>
),
pencil: (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,6 @@
function fallbackCopyTextToClipboard(text) {
import { toast } from '@redwoodjs/web/toast'
function fallbackCopyTextToClipboard(text: string) {
var textArea = document.createElement('textarea')
textArea.value = text
@@ -21,17 +23,28 @@ function fallbackCopyTextToClipboard(text) {
document.body.removeChild(textArea)
}
export function copyTextToClipboard(text) {
const clipboardSuccessToast = (text: string) => toast.success(() => (
<div className="overflow-hidden">
<p>link added to clipboard.</p>
</div>
))
const makeClipboardCopier = (success: Function) => (text: string) => {
if (!navigator.clipboard) {
fallbackCopyTextToClipboard(text)
success(text)
return
}
navigator.clipboard.writeText(text).then(
function () {
console.log('Async: Copying to clipboard was successful!')
success(text)
},
function (err) {
console.error('Async: Could not copy text: ', err)
}
)
}
export const copyTextToClipboard = makeClipboardCopier(clipboardSuccessToast)

View File

@@ -1,6 +1,6 @@
import Seo from 'src/components/Seo/Seo'
import IdeToolbar from 'src/components/IdeToolbarNew'
import IdeHeader from 'src/components/IdeHeader'
import { Toaster } from '@redwoodjs/web/toast'
const DevIdePage = ({ cadPackage }) => {
return (
@@ -10,11 +10,9 @@ const DevIdePage = ({ cadPackage }) => {
description="new ide in development"
lang="en-US"
/>
<IdeHeader />
<div className="flex-auto">
<Toaster timeout={9000} />
<IdeToolbar cadPackage={cadPackage} />
</div>
</div>
)
}

View File

@@ -6827,7 +6827,7 @@ cloudinary@^1.23.0:
lodash "^4.17.11"
q "^1.5.1"
clsx@^1.0.4, clsx@^1.1.1:
clsx@^1.0.4, clsx@^1.1.0, clsx@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
@@ -14898,7 +14898,7 @@ prompts@2.4.1, prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -15610,6 +15610,14 @@ react-syntax-highlighter@^13.5.0, react-syntax-highlighter@^13.5.3:
prismjs "^1.21.0"
refractor "^3.1.0"
react-tabs@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-3.2.2.tgz#07bdc3cdb17bdffedd02627f32a93cd4b3d6e4d0"
integrity sha512-/o52eGKxFHRa+ssuTEgSM8qORnV4+k7ibW+aNQzKe+5gifeVz8nLxCrsI9xdRhfb0wCLdgIambIpb1qCxaMN+A==
dependencies:
clsx "^1.1.0"
prop-types "^15.5.0"
react-textarea-autosize@^8.1.1:
version "8.3.3"
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.3.3.tgz#f70913945369da453fd554c168f6baacd1fa04d8"