Skip to content

Commit e1995d8

Browse files
authored
Merge branch 'main' into changeset-release/main
2 parents 9b48535 + a64fa9e commit e1995d8

File tree

10 files changed

+198
-96
lines changed

10 files changed

+198
-96
lines changed

.husky/_/.gitignore

Lines changed: 0 additions & 1 deletion
This file was deleted.

.husky/_/husky.sh

Whitespace-only changes.

playground/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@
3131
"@textea/json-viewer": "4.0.1",
3232
"lightningcss-wasm": "1.30.2",
3333
"merge-anything": "5.1.7",
34-
"monaco-editor": "0.55.1",
34+
"monaco-editor": "0.46.0",
3535
"monaco-editor-auto-typings": "0.4.6",
36-
"monaco-jsx-syntax-highlight": "1.2.2",
36+
"monaco-jsx-syntax-highlight": "1.2.0",
3737
"nanoid": "5.1.6",
3838
"next": "15.5.6",
3939
"next-themes": "0.4.6",

playground/src/components/Editor.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const Editor = memo(function Editor(props: PandaEditorProps) {
2929
onCodeEditorFormat,
3030
wordWrap,
3131
onToggleWrap,
32+
immediateValue,
3233
} = useEditor(props)
3334

3435
return (
@@ -105,7 +106,9 @@ export const Editor = memo(function Editor(props: PandaEditorProps) {
105106
/>
106107
) : (
107108
<MonacoEditor
108-
value={props.value[activeTab]}
109+
// Use defaultValue for uncontrolled mode - prevents cursor jumping
110+
// Monaco manages its own state, we only sync via onChange and editor.setValue()
111+
defaultValue={immediateValue[activeTab]}
109112
language={activeTab === 'css' ? 'css' : 'typescript'}
110113
path={editorPaths[activeTab]}
111114
options={{ ...defaultEditorOptions, wordWrap }}

playground/src/components/PlaygroundContent.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const PlaygroundContent = (props: Props) => {
3131
switchLayout,
3232
isPristine,
3333
state,
34+
deferredState,
3435
setState,
3536
onShare,
3637
onShareDiff,
@@ -40,7 +41,8 @@ export const PlaygroundContent = (props: Props) => {
4041
setExample,
4142
} = playground
4243

43-
const _state = diffState ?? state
44+
// Use deferred state for expensive panda processing
45+
const _state = diffState ?? deferredState
4446

4547
const { config, isLoading, error } = _config
4648
const panda = usePanda(_state, config)

playground/src/hooks/useEditor.ts

Lines changed: 101 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
1-
import { OnMount, OnChange, BeforeMount, EditorProps, Monaco as MonacoType } from '@monaco-editor/react'
1+
import { UsePanda } from '@/src/hooks/usePanda'
2+
import { TypingsSourceResolver } from '@/src/lib/typings-source-resolver'
3+
import { BeforeMount, EditorProps, Monaco as MonacoType, OnChange, OnMount } from '@monaco-editor/react'
24
import * as Monaco from 'monaco-editor'
35
import { AutoTypings, LocalStorageCache } from 'monaco-editor-auto-typings/custom-editor'
4-
6+
import { MonacoJsxSyntaxHighlight, getWorker } from 'monaco-jsx-syntax-highlight'
7+
import { useTheme } from 'next-themes'
8+
import { useSearchParams } from 'next/navigation'
59
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
610
import { useLocalStorage, useUpdateEffect } from 'usehooks-ts'
7-
8-
import { State } from './usePlayground'
9-
11+
import { configureAutoImports } from '../lib/auto-import'
1012
import { pandaTheme } from '../lib/gruvbox-theme'
11-
import { useTheme } from 'next-themes'
12-
import { MonacoJsxSyntaxHighlight, getWorker } from 'monaco-jsx-syntax-highlight'
13+
import { State } from './usePlayground'
1314

1415
// @ts-ignore
1516
import pandaDevDts from '../dts/@pandacss_dev.d.ts?raw'
1617
// @ts-ignore
1718
import pandaTypesDts from '../dts/@pandacss_types.d.ts?raw'
1819
// @ts-ignore
1920
import reactDts from '../dts/react.d.ts?raw'
20-
import { useSearchParams } from 'next/navigation'
21-
import { configureAutoImports } from '../lib/auto-import'
22-
import { UsePanda } from '@/src/hooks/usePanda'
23-
import { TypingsSourceResolver } from '@/src/lib/typings-source-resolver'
2421

2522
export interface PandaEditorProps {
2623
value: State
@@ -49,27 +46,17 @@ export const defaultEditorOptions: EditorProps['options'] = {
4946
}
5047

5148
const activateAutoTypings = async (monacoEditor: Monaco.editor.IStandaloneCodeEditor, monaco: MonacoType) => {
52-
const activate = async () => {
53-
const { dispose } = await AutoTypings.create(monacoEditor, {
54-
monaco,
55-
sourceCache: new LocalStorageCache(),
56-
fileRootPath: 'file:///',
57-
debounceDuration: 500,
58-
sourceResolver: new TypingsSourceResolver(),
59-
})
60-
61-
return dispose
62-
}
63-
64-
activate()
65-
66-
monacoEditor.onDidChangeModel(() => {
67-
activate()
49+
// AutoTypings internally watches for content changes via debounceDuration
50+
// No need for additional event handlers - they cause duplicate instances
51+
const { dispose } = await AutoTypings.create(monacoEditor, {
52+
monaco,
53+
sourceCache: new LocalStorageCache(),
54+
fileRootPath: 'file:///',
55+
debounceDuration: 500,
56+
sourceResolver: new TypingsSourceResolver(),
6857
})
6958

70-
monacoEditor.onDidChangeModelContent(() => {
71-
activate()
72-
})
59+
return dispose
7360
}
7461

7562
const activateMonacoJSXHighlighter = async (monacoEditor: Monaco.editor.IStandaloneCodeEditor, monaco: MonacoType) => {
@@ -108,6 +95,11 @@ export function useEditor(props: PandaEditorProps) {
10895
const monacoEditorRef = useRef<Parameters<OnMount>[0] | undefined>(undefined)
10996
const monacoRef = useRef<Parameters<OnMount>[1] | undefined>(undefined)
11097

98+
// Track the last known value for external change detection
99+
// We use refs to avoid re-renders - Monaco manages its own state (uncontrolled)
100+
const lastValueRef = useRef(value)
101+
const isUserEditRef = useRef(false)
102+
111103
const [wordWrap, setWordwrap] = useLocalStorage<'on' | 'off'>('editor_wordWrap', 'off')
112104

113105
const onToggleWrap = useCallback(() => {
@@ -118,12 +110,15 @@ export function useEditor(props: PandaEditorProps) {
118110
monacoEditorRef.current?.updateOptions({ wordWrap })
119111
}, [wordWrap])
120112

113+
// Memoize based on actual data, not context reference, to prevent unnecessary re-renders
114+
const patterns = context.patterns.details
115+
const recipeKeys = context.recipes.keys
121116
const autoImportCtx = useMemo(() => {
122117
return {
123-
patterns: context.patterns.details,
124-
recipes: Array.from(context.recipes.keys),
118+
patterns,
119+
recipes: Array.from(recipeKeys),
125120
}
126-
}, [context])
121+
}, [patterns, recipeKeys])
127122

128123
const configureEditor: OnMount = useCallback(
129124
(editor, monaco) => {
@@ -255,22 +250,49 @@ export function jsxs(type: React.ElementType, props: unknown, key?: React.Key):
255250
[configureEditor, setupLibs, getPandaTypes],
256251
)
257252

258-
const onCodeEditorChange = (content: Parameters<OnChange>[0]) => {
259-
onChange({
260-
...value,
261-
[activeTab]: content,
262-
})
263-
}
253+
// Use ref to track current value for onChange - keeps callback stable
254+
const stateRef = useRef({ value: lastValueRef.current, activeTab })
255+
stateRef.current = { value: lastValueRef.current, activeTab }
256+
257+
const onCodeEditorChange = useCallback(
258+
(content: Parameters<OnChange>[0]) => {
259+
const { value: currentValue, activeTab: currentTab } = stateRef.current
260+
const newValue = { ...currentValue, [currentTab]: content }
261+
// Mark as user edit to skip sync effect
262+
isUserEditRef.current = true
263+
lastValueRef.current = newValue
264+
onChange(newValue)
265+
},
266+
[onChange],
267+
)
264268

265269
const onCodeEditorFormat = () => {
266270
monacoEditorRef.current?.getAction('editor.action.formatDocument')?.run()
267271
}
268272

273+
// Track previous artifacts to avoid unnecessary lib updates that cause cursor jumps
274+
const prevArtifactsRef = useRef<string>('')
275+
const libsDisposablesRef = useRef<ReturnType<typeof setupLibs>>([])
276+
269277
useUpdateEffect(() => {
270-
const libsSetup = setupLibs(monacoRef.current!)
278+
// Create a signature of the artifact type definitions to detect actual changes
279+
const dtsSignature = artifacts
280+
.flatMap((a) => a?.files.filter((f) => f.file.endsWith('.d.ts')).map((f) => f.code) ?? [])
281+
.join('')
282+
283+
// Skip if the type definitions haven't actually changed
284+
if (prevArtifactsRef.current === dtsSignature) return
285+
prevArtifactsRef.current = dtsSignature
286+
287+
// Dispose previous libs before setting up new ones
288+
for (const lib of libsDisposablesRef.current) {
289+
lib?.dispose()
290+
}
291+
292+
libsDisposablesRef.current = setupLibs(monacoRef.current!)
271293

272294
return () => {
273-
for (const lib of libsSetup) {
295+
for (const lib of libsDisposablesRef.current) {
274296
lib?.dispose()
275297
}
276298
}
@@ -286,7 +308,42 @@ export function jsxs(type: React.ElementType, props: unknown, key?: React.Key):
286308
return () => {
287309
autoImports?.dispose()
288310
}
289-
}, [context])
311+
}, [autoImportCtx])
312+
313+
// Sync external changes (example selection) via editor.setValue()
314+
// This is the key to uncontrolled mode - we don't use value prop, we call setValue directly
315+
useEffect(() => {
316+
// Skip if this change came from user editing (our own onChange)
317+
if (isUserEditRef.current) {
318+
isUserEditRef.current = false
319+
return
320+
}
321+
322+
// Only update if content actually changed
323+
const editor = monacoEditorRef.current
324+
if (!editor) return
325+
326+
const currentContent = value[activeTab as keyof Pick<State, 'code' | 'config' | 'css'>]
327+
const editorContent = editor.getValue()
328+
329+
if (currentContent !== editorContent) {
330+
// Save cursor position
331+
const position = editor.getPosition()
332+
const scrollTop = editor.getScrollTop()
333+
334+
// Update editor content directly
335+
editor.setValue(currentContent ?? '')
336+
337+
// Restore cursor position
338+
if (position) {
339+
editor.setPosition(position)
340+
}
341+
editor.setScrollTop(scrollTop)
342+
}
343+
344+
// Update ref
345+
lastValueRef.current = value
346+
}, [value.code, value.config, value.css, activeTab])
290347

291348
return {
292349
activeTab,
@@ -297,5 +354,7 @@ export function jsxs(type: React.ElementType, props: unknown, key?: React.Key):
297354
onCodeEditorFormat,
298355
onToggleWrap,
299356
wordWrap,
357+
// Return initial value for defaultValue prop
358+
immediateValue: value,
300359
}
301360
}

playground/src/hooks/usePandaContext.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const defaultConfig = resolveConfig({
1414
})!
1515

1616
export const usePandaContext = (userConfig: Config | null): Generator & { error?: unknown } => {
17-
const previousContext = useRef<Generator | null>(null)
17+
const previousContext = useRef<(Generator & { error?: unknown }) | null>(null)
1818

1919
const getDefaultContext = () =>
2020
new Generator({
@@ -47,7 +47,12 @@ export const usePandaContext = (userConfig: Config | null): Generator & { error?
4747
error = e
4848
}
4949

50-
if (error) return Object.assign(previousContext.current ?? getDefaultContext(), { error })
50+
if (error) {
51+
// Return stable reference when there's an error to prevent cursor jumps
52+
const ctx = (previousContext.current ?? getDefaultContext()) as Generator & { error?: unknown }
53+
ctx.error = error
54+
return ctx
55+
}
5156

5257
try {
5358
// in event of error (invalid token format), use previous generator

playground/src/hooks/usePlayground.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { EXAMPLES, Example } from '@/src/components/Examples/data'
22
import { parseState } from '@/src/lib/parse-state'
33
import { SplitterRootProps } from '@ark-ui/react/splitter'
4-
import { startTransition, useDeferredValue, useMemo, useRef, useState } from 'react'
4+
import { useCallback, useDeferredValue, useMemo, useRef, useState } from 'react'
55
import { Layout } from '../components/LayoutControl'
66
import { toaster } from '../components/ToastProvider'
77
import { flushSync } from 'react-dom'
@@ -142,6 +142,12 @@ export const usePlayground = (props: UsePlayGroundProps) => {
142142
)
143143
}
144144

145+
// Stable reference for onChange handler - prevents unnecessary re-renders
146+
const handleStateChange = useCallback((newState: State) => {
147+
setIsPristine(false)
148+
setState(newState)
149+
}, [])
150+
145151
return {
146152
isPristine,
147153
layout,
@@ -152,13 +158,11 @@ export const usePlayground = (props: UsePlayGroundProps) => {
152158
setPanelSize,
153159
onResizePanels,
154160
switchLayout,
155-
state: deferredState,
156-
setState: (newState: State) => {
157-
setIsPristine(false)
158-
startTransition(() => {
159-
setState(newState)
160-
})
161-
},
161+
// Immediate state for Editor (prevents cursor jumping)
162+
state,
163+
// Deferred state for expensive operations (panda processing)
164+
deferredState,
165+
setState: handleStateChange,
162166
setExample,
163167
onShare,
164168
onShareDiff,

0 commit comments

Comments
 (0)