Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"tailwindcss-react-aria-components": "^2.0.1",
"typescript": "catalog:",
"typescript-eslint": "^8.48.0",
"vite": "^7.3.1"
"vite": "^7.3.1",
"yjs": "^13.6.29"
}
}
48 changes: 46 additions & 2 deletions apps/playground/src/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import {
PencilIcon,
PencilOffIcon,
SeparatorHorizontalIcon,
WifiIcon,
WifiOffIcon,
XIcon,
} from 'lucide-react'
import {useContext, useEffect, useState, type JSX} from 'react'
Expand Down Expand Up @@ -75,9 +77,12 @@ import {Tooltip} from './primitives/tooltip'
import {RangeDecorationButton} from './range-decoration-button'
import {SlashCommandPickerPlugin} from './slash-command-picker'
import {PortableTextToolbar} from './toolbar/portable-text-toolbar'
import {useEditorOffline} from './yjs-latency-provider'
import {PlaygroundYjsPlugin} from './yjs-plugin'

export function Editor(props: {
editorRef: EditorActorRef
editorIndex: number
rangeDecorations: RangeDecoration[]
}) {
const value = useSelector(props.editorRef, (s) => s.context.value)
Expand Down Expand Up @@ -108,6 +113,10 @@ export function Editor(props: {
schemaDefinition: playgroundSchemaDefinition,
}}
>
<PlaygroundYjsPlugin
enabled={playgroundFeatureFlags.yjsMode}
editorIndex={props.editorIndex}
/>
<EditorEventListener
editorRef={props.editorRef}
value={value}
Expand Down Expand Up @@ -202,7 +211,11 @@ export function Editor(props: {
</ErrorBoundary>
{loading ? <Spinner /> : null}
</div>
<EditorFooter editorRef={props.editorRef} readOnly={readOnly} />
<EditorFooter
editorRef={props.editorRef}
editorIndex={props.editorIndex}
readOnly={readOnly}
/>
</Container>
</EditorProvider>
</ErrorBoundary>
Expand Down Expand Up @@ -550,8 +563,13 @@ const styleMap: Map<string, (props: BlockStyleRenderProps) => JSX.Element> =
],
])

function EditorFooter(props: {editorRef: EditorActorRef; readOnly: boolean}) {
function EditorFooter(props: {
editorRef: EditorActorRef
editorIndex: number
readOnly: boolean
}) {
const editor = useEditor()
const playgroundFeatureFlags = useContext(PlaygroundFeatureFlagsContext)
const patchesActive = useSelector(props.editorRef, (s) =>
s.matches({'patch subscription': 'active'}),
)
Expand All @@ -562,6 +580,8 @@ function EditorFooter(props: {editorRef: EditorActorRef; readOnly: boolean}) {
const value = useEditorSelector(editor, (s) => s.context.value)
const [showSelection, setShowSelection] = useState(false)
const [showValue, setShowValue] = useState(false)
const {setOffline} = useEditorOffline()
const [offline, setOfflineState] = useState(false)

const isExpanded = showSelection || showValue

Expand Down Expand Up @@ -652,6 +672,30 @@ function EditorFooter(props: {editorRef: EditorActorRef; readOnly: boolean}) {
: 'Not receiving value updates'}
</Tooltip>
</TooltipTrigger>
{playgroundFeatureFlags.yjsMode ? (
<TooltipTrigger>
<ToggleButton
variant="ghost"
size="sm"
isSelected={offline}
onChange={(selected) => {
setOfflineState(selected)
setOffline(props.editorIndex, selected)
}}
>
{offline ? (
<WifiOffIcon className="size-3" />
) : (
<WifiIcon className="size-3" />
)}
</ToggleButton>
<Tooltip>
{offline
? 'Offline (click to reconnect)'
: 'Online (click to go offline)'}
</Tooltip>
</TooltipTrigger>
) : null}
<TooltipTrigger>
<Button
variant="ghost"
Expand Down
50 changes: 29 additions & 21 deletions apps/playground/src/editors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {Editor} from './editor'
import {PlaygroundFeatureFlagsContext} from './feature-flags'
import {Inspector} from './inspector'
import type {PlaygroundActorRef} from './playground-machine'
import {LatencyYjsProvider} from './yjs-latency-provider'
import {YjsOperationLogProvider} from './yjs-operation-log'

export function Editors(props: {playgroundRef: PlaygroundActorRef}) {
const showInspector = useSelector(props.playgroundRef, (s) =>
Expand All @@ -20,28 +22,34 @@ export function Editors(props: {playgroundRef: PlaygroundActorRef}) {

return (
<div className="p-3 md:p-4 flex-1 min-w-0">
<div
className={`grid gap-4 items-start grid-cols-1 h-full ${
showInspector ? 'md:grid-cols-2' : ''
}`}
>
<div className="flex flex-col gap-4">
<PlaygroundFeatureFlagsContext.Provider
value={playgroundFeatureFlags}
<PlaygroundFeatureFlagsContext.Provider value={playgroundFeatureFlags}>
<YjsOperationLogProvider>
<LatencyYjsProvider
editorCount={editors.length}
latencyMs={playgroundFeatureFlags.yjsLatency}
>
{editors.map((editor) => (
<Editor
key={editor.id}
editorRef={editor}
rangeDecorations={rangeDecorations}
/>
))}
</PlaygroundFeatureFlagsContext.Provider>
</div>
{showInspector ? (
<Inspector playgroundRef={props.playgroundRef} />
) : null}
</div>
<div
className={`grid gap-4 items-start grid-cols-1 h-full ${
showInspector ? 'md:grid-cols-2' : ''
}`}
>
<div className="flex flex-col gap-4">
{editors.map((editor, index) => (
<Editor
key={editor.id}
editorRef={editor}
editorIndex={index}
rangeDecorations={rangeDecorations}
/>
))}
</div>
{showInspector ? (
<Inspector playgroundRef={props.playgroundRef} />
) : null}
</div>
</LatencyYjsProvider>
</YjsOperationLogProvider>
</PlaygroundFeatureFlagsContext.Provider>
</div>
)
}
4 changes: 4 additions & 0 deletions apps/playground/src/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import {createContext} from 'react'

export type PlaygroundFeatureFlags = {
toolbar: boolean
yjsMode: boolean
yjsLatency: number
}

export const defaultPlaygroundFeatureFlags: PlaygroundFeatureFlags = {
toolbar: true,
yjsMode: false,
yjsLatency: 0,
}

export const PlaygroundFeatureFlagsContext =
Expand Down
50 changes: 50 additions & 0 deletions apps/playground/src/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import {
GithubIcon,
MonitorIcon,
MoonIcon,
NetworkIcon,
PanelRightIcon,
PlusIcon,
SunIcon,
TimerIcon,
WrenchIcon,
} from 'lucide-react'
import {TooltipTrigger} from 'react-aria-components'
Expand Down Expand Up @@ -119,6 +121,21 @@ export function Header(props: {playgroundRef: PlaygroundActorRef}) {
<PanelRightIcon className="size-4" />
<span className="hidden sm:inline">Inspector</span>
</Switch>
<Switch
isSelected={playgroundFeatureFlags.yjsMode}
onChange={() => {
props.playgroundRef.send({
type: 'toggle feature flag',
flag: 'yjsMode',
})
}}
>
<NetworkIcon className="size-4" />
<span className="hidden sm:inline">Yjs Sync</span>
</Switch>
{playgroundFeatureFlags.yjsMode ? (
<LatencySlider playgroundRef={props.playgroundRef} />
) : null}
</div>
<Separator orientation="vertical" className="h-5 hidden sm:block" />
<div className="flex items-center gap-1">
Expand Down Expand Up @@ -153,3 +170,36 @@ export function Header(props: {playgroundRef: PlaygroundActorRef}) {
</header>
)
}

function LatencySlider(props: {playgroundRef: PlaygroundActorRef}) {
const latency = useSelector(
props.playgroundRef,
(s) => s.context.featureFlags.yjsLatency,
)

return (
<TooltipTrigger>
<div className="flex items-center gap-1.5 px-1">
<TimerIcon className="size-3.5 text-gray-500 dark:text-gray-400 shrink-0" />
<input
type="range"
min={0}
max={2000}
step={50}
value={latency}
onChange={(event) => {
props.playgroundRef.send({
type: 'set yjs latency',
latency: Number(event.target.value),
})
}}
className="w-16 h-1 accent-blue-500"
/>
<span className="text-xs text-gray-500 dark:text-gray-400 tabular-nums w-12 text-right">
{latency}ms
</span>
</div>
<Tooltip>Sync latency between editors</Tooltip>
</TooltipTrigger>
)
}
74 changes: 71 additions & 3 deletions apps/playground/src/inspector.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import {useActorRef, useSelector} from '@xstate/react'
import {CheckIcon, CopyIcon, HistoryIcon, TrashIcon} from 'lucide-react'
import {useEffect, useState} from 'react'
import {
ActivityIcon,
CheckIcon,
CopyIcon,
GitBranchIcon,
HistoryIcon,
NetworkIcon,
TrashIcon,
} from 'lucide-react'
import {useContext, useEffect, useState} from 'react'
import {TooltipTrigger, type Key} from 'react-aria-components'
import {PlaygroundFeatureFlagsContext} from './feature-flags'
import {highlightMachine} from './highlight-json-machine'
import {MarkdownLogo, PortableTextLogo, ReactLogo} from './logos'
import {PatchesList} from './patches-list'
Expand All @@ -13,11 +22,22 @@ import {Container} from './primitives/container'
import {Spinner} from './primitives/spinner'
import {Tab, TabList, TabPanel, Tabs} from './primitives/tabs'
import {Tooltip} from './primitives/tooltip'
import {YjsCrdtPanel} from './yjs-crdt-panel'
import {YjsOperationLog} from './yjs-operation-log'
import {YjsTreeViewer} from './yjs-tree-viewer'

type TabId = 'output' | 'patches' | 'react-preview' | 'markdown-preview'
type TabId =
| 'output'
| 'patches'
| 'react-preview'
| 'markdown-preview'
| 'yjs-tree'
| 'yjs-ops'
| 'yjs-crdt'

export function Inspector(props: {playgroundRef: PlaygroundActorRef}) {
const [activeTab, setActiveTab] = useState<TabId>('output')
const featureFlags = useContext(PlaygroundFeatureFlagsContext)

const handleTabChange = (key: Key) => {
setActiveTab(key as TabId)
Expand Down Expand Up @@ -55,6 +75,30 @@ export function Inspector(props: {playgroundRef: PlaygroundActorRef}) {
<span className="hidden sm:inline">Markdown</span>
</span>
</Tab>
{featureFlags.yjsMode ? (
<Tab id="yjs-tree">
<span className="flex items-center gap-1.5">
<GitBranchIcon className="size-3" />
<span className="hidden sm:inline">Y.Doc</span>
</span>
</Tab>
) : null}
{featureFlags.yjsMode ? (
<Tab id="yjs-ops">
<span className="flex items-center gap-1.5">
<NetworkIcon className="size-3" />
<span className="hidden sm:inline">Ops</span>
</span>
</Tab>
) : null}
{featureFlags.yjsMode ? (
<Tab id="yjs-crdt">
<span className="flex items-center gap-1.5">
<ActivityIcon className="size-3" />
<span className="hidden sm:inline">CRDT</span>
</span>
</Tab>
) : null}
</TabList>
<TabActions activeTab={activeTab} playgroundRef={props.playgroundRef} />
</div>
Expand Down Expand Up @@ -82,6 +126,30 @@ export function Inspector(props: {playgroundRef: PlaygroundActorRef}) {
<MarkdownPreview playgroundRef={props.playgroundRef} />
</Container>
</TabPanel>

{featureFlags.yjsMode ? (
<TabPanel id="yjs-tree" className="flex-1 min-h-0">
<Container className="h-full overflow-clip">
<YjsTreeViewer />
</Container>
</TabPanel>
) : null}

{featureFlags.yjsMode ? (
<TabPanel id="yjs-ops" className="flex-1 min-h-0">
<Container className="h-full overflow-clip">
<YjsOperationLog />
</Container>
</TabPanel>
) : null}

{featureFlags.yjsMode ? (
<TabPanel id="yjs-crdt" className="flex-1 min-h-0">
<Container className="h-full overflow-clip">
<YjsCrdtPanel />
</Container>
</TabPanel>
) : null}
</Tabs>
)
}
Expand Down
Loading
Loading