innerRef.current!.focus()}
161 | onDoubleClick={() => setEditable(true)}
162 | className='relative h-[400px] w-[400px] shrink-0 cursor-default break-all rounded bg-white/90 py-4 pr-4 font-medium outline-2 outline-offset-2 outline-focus focus:outline'
163 | onKeyDown={event => {
164 | if (event.key === 'Enter' && !event.shiftKey) {
165 | event.preventDefault()
166 | generateNextSibling()
167 | } else if (event.key === 'Tab') {
168 | event.preventDefault()
169 | generateChild()
170 | } else if (!editable && (event.key === 'Delete' || event.key === 'Backspace')) {
171 | deleteCurrent()
172 | }
173 | }}
174 | />
175 | )
176 | } else
177 | return (
178 |
setEditable(true)}
183 | onBlur={() => {
184 | setEditable(false)
185 | }}
186 | onKeyDown={event => {
187 | if (event.key === 'Enter') {
188 | if (editable) {
189 | if (!event.shiftKey) {
190 | setEditable(false)
191 | }
192 | } else {
193 | event.preventDefault()
194 | generateNextSibling()
195 | }
196 | } else if (event.key === 'Tab') {
197 | if (editable) {
198 | event.preventDefault()
199 | setEditable(false)
200 | } else {
201 | event.preventDefault()
202 | generateChild()
203 | }
204 | } else if (event.key === 'Escape') {
205 | setEditable(false)
206 | } else if (
207 | (!editable || innerRef.current!.innerHTML === '') &&
208 | (event.key === 'Delete' || event.key === 'Backspace')
209 | ) {
210 | deleteCurrent()
211 | } else if (editable && event.key === 'e' && innerRef.current?.innerText === '/cod') {
212 | event.preventDefault()
213 | setValue('')
214 | setType('code')
215 | } else if (
216 | !editable &&
217 | !/^.{2,}/.test(event.key) &&
218 | !event.shiftKey &&
219 | !event.altKey &&
220 | !event.metaKey &&
221 | !event.ctrlKey
222 | ) {
223 | setEditable(true)
224 | }
225 | }}
226 | contentEditable={editable}
227 | className={clsx('mind-node', current && current === innerRef.current && 'selected')}
228 | id='mind-node'
229 | style={{ maxWidth, minWidth, ...style }}
230 | />
231 | )
232 | })
233 |
234 | const EditableNode = memo(_EditableNode)
235 |
236 | export default EditableNode
237 |
238 | const styleReducer: React.Reducer<
239 | React.CSSProperties,
240 | { type: 'setWidth' | 'setMinWidth' | 'setMaxWidth' | 'setHeight' | 'setBackgroundColor'; payload: string | number }
241 | > = (style, action) => {
242 | switch (action.type) {
243 | case 'setWidth':
244 | return { ...style, width: action.payload }
245 | case 'setMinWidth':
246 | return { ...style, minWidth: action.payload }
247 | case 'setMaxWidth':
248 | return { ...style, maxWidth: action.payload }
249 | case 'setHeight':
250 | return { ...style, height: action.payload }
251 | case 'setBackgroundColor':
252 | return { ...style, backgroundColor: String(action.payload) }
253 |
254 | default: {
255 | throw Error('Unknown action: ' + action.type)
256 | }
257 | }
258 | }
259 |
--------------------------------------------------------------------------------
/src/components/mind-container.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 | import { useSpring, animated } from '@react-spring/web'
3 | import { containerState, controls } from '@/share'
4 | import { createUseGesture, dragAction, pinchAction, wheelAction } from '@use-gesture/react'
5 | import Scene from '@/themes/sunset/scene'
6 | import { containerListener } from '@/hooks/useSelectState'
7 | import { MAX_SCALE, MIN_SCALE } from '@/consts'
8 |
9 | const useGesture = createUseGesture([dragAction, pinchAction, wheelAction])
10 |
11 | export default function MindContainer({ children }: PropsWithChildren) {
12 | const [springs, api] = useSpring(() => ({
13 | from: { x: containerState.x(), y: containerState.y(), scale: containerState.scale },
14 | config: { tension: 100, friction: 5, mass: 0.1 }
15 | }))
16 |
17 | // Initialize controls in share file
18 | useEffect(() => {
19 | controls.setScale = (s: number) => {
20 | containerState.scale = s
21 | api.start({ scale: s })
22 | }
23 | controls.clearOffset = () => {
24 | containerState.offsetX = 0
25 | containerState.offsetY = 0
26 | containerState.initialX = 0
27 | containerState.initialY = 0
28 | containerState.wheelX = 0
29 | containerState.wheelY = 0
30 | api.start({ x: 0, y: 0 })
31 | }
32 | }, [api])
33 |
34 | const containerRef = useRef
(null)
35 |
36 | // Prevent default gesture event
37 | useEffect(() => {
38 | const handler = (e: Event) => e.preventDefault()
39 | document.addEventListener('gesturestart', handler)
40 | document.addEventListener('gesturechange', handler)
41 | document.addEventListener('gestureend', handler)
42 |
43 | return () => {
44 | document.removeEventListener('gesturestart', handler)
45 | document.removeEventListener('gesturechange', handler)
46 | document.removeEventListener('gestureend', handler)
47 | }
48 | }, [])
49 |
50 | useGesture(
51 | {
52 | onPinch: ({ offset: [s] }) => {
53 | api.start({ scale: s })
54 | containerState.scale = s
55 | },
56 | onDrag: ({ offset, target, cancel, pinching }) => {
57 | if (pinching) return cancel()
58 |
59 | containerState.offsetX = offset[0]
60 | containerState.offsetY = offset[1]
61 | api.set({ x: containerState.x(), y: containerState.y() })
62 | },
63 | onWheel: ({ offset, pinching, event }) => {
64 | if (!pinching && !event.ctrlKey) {
65 | containerState.wheelX = offset[0]
66 | containerState.wheelY = offset[1]
67 | api.set({ x: containerState.x(), y: containerState.y() })
68 | } else {
69 | offset[0] = containerState.wheelX
70 | offset[1] = containerState.wheelY
71 | }
72 | }
73 | },
74 | {
75 | target: containerRef,
76 | pinch: { scaleBounds: { min: MIN_SCALE, max: MAX_SCALE }, rubberband: true },
77 | drag: { pointer: { keys: false, buttons: 'ontouchstart' in window ? 1 : 4 } }
78 | }
79 | )
80 |
81 | // Grabbing cursor
82 | useEffect(() => {
83 | const mousedownHandler = (event: MouseEvent) => {
84 | if ((event.button === 1 || event.buttons === 4) && containerRef.current) {
85 | containerRef.current.style.cursor = 'grabbing'
86 | }
87 | }
88 | const mouseupHandler = (event: MouseEvent) => {
89 | if ((event.button === 1 || event.buttons === 4) && containerRef.current) {
90 | containerRef.current.style.cursor = 'auto'
91 | }
92 | }
93 |
94 | containerRef.current?.addEventListener('mousedown', mousedownHandler)
95 | containerRef.current?.addEventListener('mouseup', mouseupHandler)
96 |
97 | return () => {
98 | containerRef.current?.removeEventListener('mousedown', mousedownHandler)
99 | containerRef.current?.removeEventListener('mouseup', mouseupHandler)
100 | }
101 | }, [])
102 |
103 | return (
104 |
109 |
110 |
111 |
112 |
113 |
114 | {children}
115 |
116 |
117 |
118 | )
119 | }
120 |
--------------------------------------------------------------------------------
/src/components/mind-control.tsx:
--------------------------------------------------------------------------------
1 | import { ReactComponent as BrushSVG } from '@/svgs/brush.svg'
2 | import { ReactComponent as ZoomInSVG } from '@/svgs/zoom-in.svg'
3 | import { ReactComponent as ZoomOutSVG } from '@/svgs/zoom-out.svg'
4 | import { ReactComponent as CenterSVG } from '@/svgs/center.svg'
5 | import { ReactComponent as BoldSVG } from '@/svgs/bold.svg'
6 | import { ReactComponent as ItalicSVG } from '@/svgs/italic.svg'
7 | import { ReactComponent as StrikeThroughSVG } from '@/svgs/strike-through.svg'
8 | import { containerState, controls } from '@/share'
9 | import { useContext, useEffect, useRef, useState } from 'react'
10 | import { useSelectState } from '@/hooks/useSelectState'
11 | import { MindContext } from './code-mind'
12 | import { MAX_SCALE, MIN_SCALE } from '@/consts'
13 | import ColorPicker from './color-picker'
14 |
15 | export default function MindControl() {
16 | return (
17 |
48 | )
49 | }
50 |
51 | function Brush() {
52 | const { maxWidth, minWidth } = useContext(MindContext)
53 |
54 | const [show, setShow] = useState(false)
55 | const [_, updateState] = useState(0)
56 | const ref = useRef(null)
57 |
58 | const { current } = useSelectState()
59 |
60 | useEffect(() => {
61 | if (current && show) {
62 | if (ref.current && current.reactStyle) {
63 | const minWidthElement = ref.current.querySelector('input[name="min-width"]') as HTMLInputElement
64 | if (minWidthElement)
65 | if ('minWidth' in current.reactStyle) minWidthElement.value = String(current.reactStyle.minWidth)
66 | else minWidthElement.value = ''
67 |
68 | const maxWidthElement = ref.current.querySelector('input[name="max-width"]') as HTMLInputElement
69 | if (maxWidthElement)
70 | if ('maxWidth' in current.reactStyle) maxWidthElement.value = String(current.reactStyle.maxWidth)
71 | else maxWidthElement.value = ''
72 |
73 | const widthElement = ref.current.querySelector('input[name="width"]') as HTMLInputElement
74 | if (widthElement)
75 | if ('width' in current.reactStyle) widthElement.value = String(current.reactStyle.width)
76 | else widthElement.value = ''
77 |
78 | const heightElement = ref.current.querySelector('input[name="height"]') as HTMLInputElement
79 | if (heightElement)
80 | if ('height' in current.reactStyle) heightElement.value = String(current.reactStyle.height)
81 | else heightElement.value = ''
82 | }
83 |
84 | const observer = new ResizeObserver(() => updateState(state => ++state))
85 | observer.observe(current, { box: 'border-box' })
86 |
87 | return () => observer.disconnect()
88 | }
89 | }, [current, show])
90 |
91 | const normalChangeHanlder =
92 | (action: 'setMinWidth' | 'setMaxWidth' | 'setWidth' | 'setHeight') =>
93 | (event: React.ChangeEvent) => {
94 | if (current?.dispatchStyle) {
95 | const target = event.target as HTMLInputElement
96 |
97 | if (!target.value) {
98 | current.dispatchStyle({ type: action, payload: '' })
99 | } else {
100 | const value = +target.value
101 |
102 | if (!Object.is(value, NaN)) current.dispatchStyle({ type: action, payload: value })
103 | }
104 | }
105 | }
106 |
107 | return (
108 | <>
109 |
110 |
117 |
118 | {show &&
119 | (current ? (
120 |
124 |
125 |
145 |
165 |
185 |
186 |
206 |
207 |
208 |
209 |
210 |
211 | Fill
212 |
213 |
216 |
217 |
218 |
219 |
220 |
221 |
222 | Text
223 |
224 |
227 |
228 |
243 |
244 |
245 |
248 |
251 |
254 |
255 |
256 |
257 |
258 | ) : (
259 |
262 |
Null
263 |
Please select the node
264 |
265 | ))}
266 |
267 | >
268 | )
269 | }
270 |
--------------------------------------------------------------------------------
/src/components/mind-edge.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useState } from 'react'
2 | import clsx from 'clsx'
3 | import { MindContext } from './code-mind'
4 | import { containerState } from '@/share'
5 | import { amendDistance } from '@/utils'
6 |
7 | interface Props {
8 | parentNode?: NodeRef
9 | childNode: NodeRef
10 | siblings?: MindNode[]
11 | type?: LineType
12 | }
13 |
14 | export default function MindEdge({ parentNode, childNode, siblings, type = 'bezier' }: Props) {
15 | const { distance, layoutFlag } = useContext(MindContext)
16 |
17 | const [height, setHeight] = useState(0)
18 |
19 | useEffect(() => {
20 | if (parentNode?.current && childNode.current) {
21 | const { top: pTop, height: pheight } = parentNode.current.getBoundingClientRect()
22 | const { top: cTop, height: cHeight } = childNode.current.getBoundingClientRect()
23 |
24 | let height = pTop - cTop + (pheight - cHeight) / 2
25 | height /= containerState.scale
26 |
27 | setHeight(height)
28 | }
29 | }, [siblings, layoutFlag])
30 |
31 | const h = Math.abs(height)
32 |
33 | const distance_amend = amendDistance(distance, siblings)
34 |
35 | if (parentNode?.current && childNode.current)
36 | if (h > 2) {
37 | switch (type) {
38 | case 'bezier':
39 | return (
40 |
52 | )
53 | case 'right-angle':
54 | return (
55 |
66 | )
67 | case 'straight-with-handle':
68 | return (
69 |
82 | )
83 | case 'straight':
84 | default:
85 | return (
86 |
97 | )
98 | }
99 | } else
100 | return (
101 |
105 | )
106 |
107 | return null
108 | }
109 |
--------------------------------------------------------------------------------
/src/components/mind-node.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
2 | import MindEdge from './mind-edge'
3 | import { MindContext } from './code-mind'
4 | import EditableNode from './editable-node'
5 | import { stateStore } from '../lib/save'
6 | import { amendDistance } from '@/utils'
7 |
8 | interface Props {
9 | index?: number
10 | parentID?: string
11 | node: MindNode
12 | parentRef?: NodeRef
13 | siblings?: MindNode[]
14 | setParentChildren?: Dispatch
15 | }
16 |
17 | export default function MindNode({ node, parentRef, siblings, setParentChildren, index, parentID }: Props) {
18 | const { distance, gap, updateLayout, saveFlag } = useContext(MindContext)
19 |
20 | const nodeRef = useRef(null)
21 | const contentRef = useRef<{
22 | getContent: () => string
23 | getType: () => NodeType
24 | getCode: () => string
25 | getStyle: () => React.CSSProperties
26 | }>(null)
27 |
28 | const [children, setChildren] = useState(node.children)
29 |
30 | const generateNextSibling = useCallback(() => {
31 | if (siblings && setParentChildren) {
32 | const nextIndex = (index || 0) + 1
33 | const nextNode: MindNode = {
34 | id: String(Date.now()),
35 | value: 'Example ' + (siblings.length + 1),
36 | isNew: true,
37 | isFirstEdit: true
38 | }
39 | siblings.splice(nextIndex, 0, nextNode)
40 | setParentChildren(siblings.slice())
41 | updateLayout()
42 | }
43 | }, [siblings, index])
44 |
45 | const generateChild = useCallback(() => {
46 | if (!Array.isArray(children)) {
47 | setChildren([{ id: String(Date.now()), value: 'Example 1', isNew: true, isFirstEdit: true }])
48 | } else {
49 | setChildren([
50 | ...children,
51 | { id: String(Date.now()), value: 'Example ' + (children.length + 1), isNew: true, isFirstEdit: true }
52 | ])
53 | }
54 | updateLayout()
55 | }, [children])
56 |
57 | const deleteCurrent = useCallback(() => {
58 | if (siblings && setParentChildren) {
59 | siblings?.splice(index!, 1)
60 | setParentChildren(siblings?.slice())
61 | updateLayout()
62 | }
63 | }, [siblings, index])
64 |
65 | const SingleNode = useMemo(
66 | () => (
67 |
68 |
75 |
76 |
77 |
78 | ),
79 | [generateNextSibling, generateNextSibling, generateChild]
80 | )
81 |
82 | // Save feature: Push current state object to the stateStore.
83 | useEffect(() => {
84 | const currentNode: MindNode = {
85 | id: node.id,
86 | value: contentRef.current?.getContent() || '',
87 | parentID,
88 | type: contentRef.current?.getType() || 'text',
89 | style: contentRef.current?.getStyle()
90 | }
91 | if (currentNode.type === 'code') currentNode.code = contentRef.current?.getCode() || ''
92 |
93 | stateStore.current.push(currentNode)
94 | }, [saveFlag])
95 |
96 | const distance_amend = amendDistance(distance, children)
97 |
98 | if (Array.isArray(children) && children.length > 0) {
99 | return (
100 |
101 | {SingleNode}
102 |
103 | {children.map((item, index) => (
104 |
113 | ))}
114 |
115 |
116 | )
117 | }
118 |
119 | return SingleNode
120 | }
121 |
--------------------------------------------------------------------------------
/src/components/monacoWorker.ts:
--------------------------------------------------------------------------------
1 | import * as monaco from 'monaco-editor'
2 | import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
3 | import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
4 | import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
5 |
6 | self.MonacoEnvironment = {
7 | getWorker(_: any, label: string) {
8 | if (label === 'json') {
9 | return new jsonWorker()
10 | }
11 |
12 | if (label === 'typescript' || label === 'javascript') {
13 | return new tsWorker()
14 | }
15 | return new editorWorker()
16 | }
17 | }
18 |
19 | monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true)
20 |
21 | // Import the relevant CSS files directly.
22 | monaco.editor
23 | .create(document.createElement('div'), {
24 | value: '',
25 | language: 'typescript',
26 | minimap: { enabled: false },
27 | scrollbar: {
28 | vertical: 'hidden'
29 | },
30 | tabSize: 2,
31 | scrollBeyondLastLine: false
32 | })
33 | .dispose()
34 |
--------------------------------------------------------------------------------
/src/consts.ts:
--------------------------------------------------------------------------------
1 | export const initialNode: MindNode = {
2 | id: '0',
3 | value: 'CodeMind',
4 | children: [
5 | { id: '1-0', value: 'Example 1', isFirstEdit: true },
6 | { id: '1-1', value: 'Example 2', isFirstEdit: true }
7 | ]
8 | }
9 |
10 | export const MAX_SCALE = 2.5
11 | export const MIN_SCALE = 0.5
12 |
--------------------------------------------------------------------------------
/src/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg' {
2 | const src: string
3 | export default src
4 | export const ReactComponent: React.FC>
5 | }
6 |
7 | declare type PropsWithChildren = React.PropsWithChildren
8 |
9 | declare type NodeElement = HTMLDivElement & {
10 | reactStyle?: React.CSSProperties
11 | dispatchStyle?: React.Dispatch<{
12 | type: 'setWidth' | 'setMinWidth' | 'setMaxWidth' | 'setHeight' | 'setBackgroundColor'
13 | payload: string | number
14 | }>
15 | }
16 | declare type NodeRef = React.RefObject
17 |
18 | declare type LineType = 'straight' | 'straight-with-handle' | 'right-angle' | 'bezier'
19 | declare type NodeType = 'text' | 'code'
20 |
21 | declare type MindNode = {
22 | id: string
23 | parentID?: string
24 | value: string
25 | children?: MindNode[]
26 | isNew?: boolean
27 | isFirstEdit?: boolean
28 | type?: NodeType
29 | code?: string
30 | style?: React.CSSProperties
31 | }
32 |
--------------------------------------------------------------------------------
/src/hooks/useFocusState.ts:
--------------------------------------------------------------------------------
1 | import { createStore, useStore } from 'zustand'
2 |
3 | export const focusStateStore = createStore(() => ({
4 | current: null as NodeElement | null
5 | }))
6 |
7 | export function useFocusState() {
8 | const store = useStore(focusStateStore)
9 |
10 | return store
11 | }
12 |
13 | window.addEventListener(
14 | 'focus',
15 | event => {
16 | const target = event.target
17 | if (target && target instanceof HTMLDivElement && target.id === 'mind-node') {
18 | focusStateStore.setState({ current: target })
19 | }
20 | },
21 | { capture: true }
22 | )
23 | window.addEventListener(
24 | 'blur',
25 | event => {
26 | const target = event.target
27 | if (target && target instanceof HTMLDivElement && target.id === 'mind-node') {
28 | if (target === focusStateStore.getState().current) focusStateStore.setState({ current: null })
29 | }
30 | },
31 | { capture: true }
32 | )
33 |
--------------------------------------------------------------------------------
/src/hooks/useSelectState.ts:
--------------------------------------------------------------------------------
1 | import { createStore, useStore } from 'zustand'
2 |
3 | export const selectStateStore = createStore(() => ({
4 | current: null as NodeElement | null
5 | }))
6 |
7 | export function useSelectState() {
8 | const store = useStore(selectStateStore)
9 |
10 | return store
11 | }
12 |
13 | export const containerListener = (event: React.MouseEvent) => {
14 | const target = event.target
15 |
16 | if (target && target instanceof HTMLDivElement && target.id === 'mind-node') {
17 | // Nothing now
18 | } else {
19 | selectStateStore.setState({ current: null })
20 | }
21 | }
22 |
23 | window.addEventListener(
24 | 'focus',
25 | event => {
26 | const target = event.target
27 | if (target && target instanceof HTMLDivElement && target.id === 'mind-node') {
28 | if (target !== selectStateStore.getState().current) selectStateStore.setState({ current: target })
29 | }
30 | },
31 | { capture: true }
32 | )
33 |
--------------------------------------------------------------------------------
/src/lib/save.ts:
--------------------------------------------------------------------------------
1 | import { isObject } from '@/utils'
2 | import { containerState } from '../share'
3 | import { getStorage, setStorage } from './storage'
4 | import { toast } from 'sonner'
5 | import { initialNode } from '@/consts'
6 |
7 | export const stateStore = {
8 | current: [] as MindNode[],
9 | saveHandle: () => {},
10 | local: getLocalNodeTree()
11 | }
12 |
13 | export function getLocalNodeTree() {
14 | const local = getStorage('state')
15 |
16 | if (local) {
17 | const state: MindNode[] = JSON.parse(local)
18 |
19 | if (Array.isArray(state)) {
20 | const rootNode = state.find(item => !item.parentID) as MindNode
21 |
22 | const nodeMap = new Map()
23 | state.forEach(item => {
24 | nodeMap.set(item.id, item)
25 | })
26 | state.forEach(item => {
27 | if (nodeMap.has(item.parentID)) {
28 | const node = nodeMap.get(item.parentID)
29 |
30 | if (Array.isArray(node!.children)) {
31 | node!.children.push(item)
32 | } else {
33 | node!.children = [item]
34 | }
35 | }
36 | })
37 |
38 | return rootNode
39 | }
40 | }
41 |
42 | return initialNode
43 | }
44 |
45 | export function getLocalContainerState() {
46 | const local = getStorage('container')
47 |
48 | if (local) {
49 | const state = JSON.parse(local)
50 |
51 | if (isObject(state)) {
52 | return state
53 | }
54 | }
55 |
56 | return null
57 | }
58 |
59 | async function save() {
60 | return new Promise(resolve => {
61 | stateStore.current = []
62 |
63 | stateStore.saveHandle()
64 |
65 | setTimeout(() => {
66 | const state = JSON.stringify(stateStore.current)
67 |
68 | setStorage('state', state)
69 | saveContainerState()
70 |
71 | resolve(true)
72 | }, 0)
73 | })
74 | }
75 |
76 | function saveContainerState() {
77 | const containerState_amend = {
78 | initialX: containerState.x(),
79 | initialY: containerState.y(),
80 | scale: containerState.scale
81 | }
82 |
83 | setStorage('container', JSON.stringify(containerState_amend))
84 | }
85 |
86 | window.addEventListener('keydown', async event => {
87 | if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
88 | event.preventDefault()
89 | await save()
90 | toast.success('Saved to local storage!')
91 | }
92 | })
93 |
--------------------------------------------------------------------------------
/src/lib/storage.ts:
--------------------------------------------------------------------------------
1 | const state = 'state'
2 | const container = 'container'
3 |
4 | type Key = typeof state | typeof container
5 |
6 | export const setStorage = (key: Key, value: string) => {
7 | window.localStorage.setItem(key, value)
8 | }
9 |
10 | export const getStorage = (key: Key) => {
11 | if (typeof window !== 'undefined') return window.localStorage.getItem(key)
12 | }
13 |
14 | export const removeStorage = (key: Key) => {
15 | if (typeof window !== 'undefined') return window.localStorage.removeItem(key)
16 | }
17 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client'
2 | import App from './App.tsx'
3 | import './styles'
4 |
5 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render()
6 |
--------------------------------------------------------------------------------
/src/share.ts:
--------------------------------------------------------------------------------
1 | import { getLocalContainerState } from './lib/save'
2 |
3 | export const containerState = {
4 | initialX: 0,
5 | initialY: 0,
6 | offsetX: 0,
7 | offsetY: 0,
8 | wheelX: 0,
9 | wheelY: 0,
10 | x() {
11 | return this.offsetX - this.wheelX + this.initialX
12 | },
13 | y() {
14 | return this.offsetY - this.wheelY + this.initialY
15 | },
16 |
17 | scale: 1,
18 |
19 | ...getLocalContainerState()
20 | } as {
21 | initialX: number
22 | initialY: number
23 | offsetX: number
24 | offsetY: number
25 | wheelX: number
26 | wheelY: number
27 | x: () => number
28 | y: () => number
29 | scale: number
30 | }
31 |
32 | export const controls = {
33 | setScale(s: number) {},
34 | clearOffset() {}
35 | }
36 |
--------------------------------------------------------------------------------
/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @import './monaco';
6 | @import './mind-node';
7 |
8 | html,
9 | body,
10 | #root {
11 | height: 100%;
12 | }
13 |
14 | *:focus-visible {
15 | @apply outline-focus;
16 | }
17 |
18 | body {
19 | @apply bg-bg text-text;
20 | }
21 |
22 | * {
23 | scrollbar-width: none;
24 | &::-webkit-scrollbar {
25 | display: none;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/styles/mind-node.scss:
--------------------------------------------------------------------------------
1 | .mind-node {
2 | @apply relative w-max shrink-0 break-all rounded-md bg-white/80 px-6 py-3 text-sm font-medium backdrop-blur focus:outline-none;
3 |
4 | &.selected {
5 | @apply outline outline-2 outline-offset-2 outline-focus;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/styles/monaco.scss:
--------------------------------------------------------------------------------
1 | .code-mind {
2 | .monaco-editor {
3 | --vscode-editor-background: transparent;
4 | --vscode-editorGutter-background: transparent;
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/svgs/bold.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/svgs/brush.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/svgs/center.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/svgs/italic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/svgs/strike-through.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/svgs/zoom-in.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/svgs/zoom-out.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/themes/sunset/goose-1.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/themes/sunset/goose-2.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/themes/sunset/islet.svg:
--------------------------------------------------------------------------------
1 |
31 |
--------------------------------------------------------------------------------
/src/themes/sunset/scene.tsx:
--------------------------------------------------------------------------------
1 | import { ReactComponent as SunSVG } from './sun.svg'
2 | import { ReactComponent as Goose1SVG } from './goose-1.svg'
3 | import { ReactComponent as Goose2SVG } from './goose-2.svg'
4 | import src_shallowWave from './wave-shallow.svg'
5 | import src_deepWave from './wave-deep.svg'
6 | import { ReactComponent as IsletSVG } from './islet.svg'
7 |
8 | export default function Scene() {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
27 |
39 |
40 | {/*
*/}
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/src/themes/sunset/sun.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/themes/sunset/wave-deep.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/themes/sunset/wave-shallow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | let tCanvas: HTMLCanvasElement | null = null
2 | export function getTextWidth(text: string, font = 'normal 16px Poppins') {
3 | const canvas = tCanvas || (tCanvas = document.createElement('canvas'))
4 | const context = canvas.getContext('2d')
5 | context!.font = font
6 | return context!.measureText(text).width
7 | }
8 |
9 | export function amendDistance(distance: number, siblings?: unknown[]) {
10 | if (siblings && siblings.length > 3) {
11 | return ((siblings.length - 3 + 5) / 5) * distance
12 | }
13 | return distance
14 | }
15 |
16 | export function getMonacoContent(monacoElement?: HTMLDivElement | null) {
17 | if (!monacoElement) return ''
18 |
19 | const marginElement = monacoElement.querySelector('.monaco-editor .margin-view-overlays')
20 |
21 | const contentElement = monacoElement.querySelector('.monaco-editor .view-lines')
22 |
23 | if (marginElement && contentElement) {
24 | return `${marginElement.outerHTML}
${contentElement.outerHTML}
`
25 | }
26 |
27 | return ''
28 | }
29 |
30 | export function isObject(o: unknown) {
31 | return Object.prototype.toString.call(o) === '[object Object]'
32 | }
33 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ['./src/**/*.{html,tsx,jsx,css}', './index.html'],
4 | theme: {
5 | extend: {
6 | fontFamily: {
7 | sans: ['Poppins', 'sans-serif']
8 | },
9 | colors: {
10 | bg: '#FEF3E1',
11 | text: 'black',
12 | focus: '#51A8B9',
13 | edge: '#FED2CB',
14 | '#1': '#FD9886',
15 | '#2': '#51A8B9'
16 | },
17 | screens: {
18 | 'max-xl': { max: '1280px' },
19 | 'max-lg': { max: '1024px' },
20 | 'max-md': { max: '768px' },
21 | 'max-sm': { max: '640px' },
22 | 'max-xs': { max: '360px' }
23 | }
24 | }
25 | },
26 | plugins: []
27 | }
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noFallthroughCasesInSwitch": true,
20 |
21 | "baseUrl": ".",
22 | "paths": {
23 | "@/*": ["./src/*"]
24 | }
25 | },
26 | "include": ["src", ".eslintrc.cjs", "vite.config.ts"],
27 | "references": [{ "path": "./tsconfig.node.json" }]
28 | }
29 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react-swc'
3 | import tsconfigPaths from 'vite-tsconfig-paths'
4 | import svgr from 'vite-plugin-svgr'
5 |
6 | export default defineConfig({
7 | plugins: [react(), svgr(), tsconfigPaths()],
8 | server: {
9 | port: 2222,
10 | host: '0.0.0.0'
11 | },
12 | build: {
13 | sourcemap: true
14 | },
15 | resolve: {
16 | extensions: ['.tsx', '.ts', '.jsx', '.js', '.json', '.scss', '.mjs', '.mts']
17 | }
18 | })
19 |
--------------------------------------------------------------------------------