UNPKG

@sanity/ui-workshop

Version:

An environment for designing, reviewing, and quality-testing React components.

1 lines 165 kB
{"version":3,"file":"index.cjs","sources":["../src/core/cli/defineConfig.ts","../src/core/cli/defineRuntime.ts","../src/core/config/defineScope.ts","../src/core/constants.ts","../src/core/lib/pubsub.ts","../src/core/lib/qs.ts","../src/core/helpers.ts","../src/core/plugins/props/PropsContext.ts","../src/core/plugins/props/useProps.ts","../src/core/plugins/props/components/booleanProp.tsx","../src/core/plugins/props/components/numberProp.tsx","../src/core/plugins/props/components/selectProp.tsx","../src/core/plugins/props/components/stringProp.tsx","../src/core/plugins/props/components/textProp.tsx","../src/core/plugins/props/components/prop.tsx","../src/core/plugins/props/PropsInspector.tsx","../src/core/lib/isEqual.ts","../src/core/WorkshopContext.ts","../src/core/useWorkshop.ts","../src/core/plugins/props/lib/zlib/zlib.ts","../src/core/plugins/props/helpers.ts","../src/core/plugins/props/propsReducer.ts","../src/core/plugins/props/PropsProvider.tsx","../src/core/plugins/props/hooks/useBoolean.ts","../src/core/plugins/props/hooks/useNumber.ts","../src/core/plugins/props/hooks/useSelect.ts","../src/core/plugins/props/hooks/useString.ts","../src/core/plugins/props/hooks/useText.ts","../src/core/plugins/props/index.ts","../src/core/WorkshopProvider.tsx","../src/core/workshopReducer.ts","../src/core/frame/formatStack.ts","../src/core/frame/WorkshopCanvas.tsx","../src/core/lib/isArray.ts","../src/core/lib/isRecord.ts","../src/core/frame/WorkshopMainController.ts","../src/core/frame/WorkshopFrame.tsx","../src/core/location/LocationStore.ts","../src/core/GlobalStyle.ts","../src/core/inspector/InspectorHeader.tsx","../src/core/inspector/WorkshopInspector.tsx","../src/core/lib/debounce.ts","../src/core/navbar/NavbarBreadcrumbs.tsx","../src/core/navbar/OpenCanvasButton.tsx","../src/core/navbar/SchemeMenu.tsx","../src/core/navbar/ViewportMenu.tsx","../src/core/navbar/ZoomMenu.tsx","../src/core/navbar/WorkshopNavbar.tsx","../src/core/navigator/helpers.ts","../src/core/navigator/SearchResults.tsx","../src/core/navigator/StoryTree.tsx","../src/core/navigator/WorkshopNavigator.tsx","../src/core/WorkshopCanvas.tsx","../src/core/WorkshopFrameController.ts","../src/core/Workshop.tsx","../src/core/mount.tsx","../src/core/mountFrame.tsx","../src/core/useAction.ts"],"sourcesContent":["import {WorkshopConfig} from '../config'\n\n/** @public */\nexport interface WorkshopConfigOptions extends Omit<WorkshopConfig, 'scopes'> {}\n\n/** @public */\nexport function defineConfig(config: WorkshopConfigOptions): WorkshopConfigOptions {\n return config\n}\n","import {WorkshopRuntime} from '../runtime'\n\n/** @public */\nexport interface WorkshopRuntimeOptions extends WorkshopRuntime {}\n\n/** @public */\nexport function defineRuntime(config: WorkshopRuntimeOptions): WorkshopRuntimeOptions {\n return config\n}\n","import {WorkshopScope} from './types'\n\n/** @public */\nexport function defineScope(scope: WorkshopScope): WorkshopScope {\n return scope\n}\n","/** @internal */\nexport const EMPTY_ARRAY: never[] = []\n\n/** @internal */\nexport const EMPTY_RECORD: Record<string, unknown> = {}\n\n/** @internal */\nexport const DEFAULT_VIEWPORT_VALUE = 'auto'\n\n/** @internal */\nexport const DEFAULT_ZOOM_VALUE = 1\n\n/** @internal */\nexport const VIEWPORT_OPTIONS: {\n name: string\n title: string\n rect: {width: number | 'auto'; height?: number}\n}[] = [\n {name: 'auto', title: 'Full', rect: {width: 'auto'}},\n {name: '768', title: '768px', rect: {width: 768}},\n {name: '375', title: '375px', rect: {width: 375, height: 667}},\n {name: '320', title: '320px', rect: {width: 320, height: 568}},\n]\n\n/** @internal */\nexport const ZOOM_OPTIONS: {value: number; title: string}[] = [\n {value: 0.5, title: '50%'},\n {value: 0.75, title: '75%'},\n {value: 1, title: '100%'},\n {value: 1.5, title: '150%'},\n {value: 2, title: '200%'},\n {value: 3, title: '300%'},\n]\n","/** @public */\nexport interface Pubsub<Msg = unknown> {\n publish: (msg: Msg) => void\n subscribe: (subscriber: (msg: Msg) => void) => () => void\n}\n\n/** @internal */\nexport function createPubsub<Msg = unknown>(): Pubsub<Msg> {\n const subscribers = new Set<(msg: Msg) => void>()\n\n return {\n publish(msg: Msg) {\n for (const subscriber of subscribers) {\n subscriber(msg)\n }\n },\n\n subscribe(subscriber: (msg: Msg) => void) {\n subscribers.add(subscriber)\n\n return () => {\n subscribers.delete(subscriber)\n }\n },\n }\n}\n","/** @internal */\nexport const qs = {\n parse(str: string): Record<string, string> {\n const params = new URLSearchParams('?' + str)\n const q: Record<string, string> = {}\n\n params.forEach((value, key) => {\n q[key] = value\n })\n\n return q\n },\n\n stringify(q: {[key: string]: unknown}): string {\n return Object.entries(q)\n .map(([key, value]) => `${key}=${value}`)\n .join('&')\n },\n}\n","import {ThemeColorSchemeKey} from '@sanity/ui'\n\nimport {WorkshopScope, WorkshopStory} from './config'\n\n/** @internal */\nexport function resolveLocation(\n scopes: WorkshopScope[],\n path: string,\n): {scope: WorkshopScope | null; story: WorkshopStory | null} {\n const segments = path.split('/').slice(1).filter(Boolean)\n\n const p = segments.join('/')\n\n if (segments.length === 0) {\n return {\n scope: null,\n story: null,\n }\n }\n\n for (const scope of scopes) {\n for (const story of scope.stories) {\n const storyPath = [scope.name, story.name].filter(Boolean).join('/')\n\n if (p === storyPath) {\n return {scope, story}\n }\n }\n }\n\n return {scope: null, story: null}\n}\n\n/** @internal */\nexport function buildFrameUrl(params: {\n baseUrl?: string\n path: string\n payload: Record<string, unknown>\n scheme: ThemeColorSchemeKey\n viewport: string\n zoom: number\n}): string {\n const {baseUrl = '/frame/', path, payload, scheme, viewport, zoom} = params\n\n return [\n baseUrl,\n `?path=${encodeURIComponent(path)}`,\n `&scheme=${scheme}`,\n `&viewport=${viewport}`,\n `&zoom=${zoom}`,\n ...Object.entries(payload).map(([key, value]) => {\n return `&${key}=${value}`\n }),\n ].join('')\n}\n","import {createContext} from 'react'\n\nimport {PropSchema} from './types'\n\n/** @internal */\nexport interface PropsContextValue {\n registerProp: (propSchema: PropSchema) => void\n schemas: PropSchema[]\n setPropValue: (propName: string, value: unknown) => void\n unregisterProp: (propName: string) => void\n value: Record<string, unknown>\n}\n\n/** @internal */\nexport const PropsContext = createContext<PropsContextValue | null>(null)\n","import {useContext} from 'react'\n\nimport {PropsContext, PropsContextValue} from './PropsContext'\n\n/** @internal */\nexport function useProps(): PropsContextValue {\n const props = useContext(PropsContext)\n\n if (!props) {\n throw new Error('Props: missing context value')\n }\n\n return props\n}\n","import {Box, Checkbox, Flex, Text} from '@sanity/ui'\nimport {memo} from 'react'\n\nimport {BooleanPropSchema} from '../types'\nimport {useProps} from '../useProps'\n\n/** @internal */\nexport const BooleanProp = memo(function BooleanProp(props: {\n schema: BooleanPropSchema\n value?: boolean\n}): React.ReactNode {\n const {schema, value} = props\n const {setPropValue} = useProps()\n\n return (\n <Flex as=\"label\" padding={3}>\n <Box marginRight={2} style={{lineHeight: 0}}>\n <Checkbox\n checked={value || false}\n onChange={(event) => setPropValue(schema.name, event.currentTarget.checked)}\n />\n </Box>\n <Box paddingY={1}>\n <Text size={1} weight=\"semibold\">\n {schema.name}\n </Text>\n </Box>\n </Flex>\n )\n})\n","import {Box, Text, TextInput} from '@sanity/ui'\nimport {memo} from 'react'\n\nimport {NumberPropSchema} from '../types'\nimport {useProps} from '../useProps'\n\n/** @internal */\nexport const NumberProp = memo(function NumberProp(props: {\n schema: NumberPropSchema\n value?: string\n}): React.ReactNode {\n const {schema, value = ''} = props\n const {setPropValue} = useProps()\n\n return (\n <Box padding={3}>\n <Text size={1} weight=\"semibold\">\n {schema.name}\n </Text>\n <Box marginTop={2}>\n <TextInput\n fontSize={[2, 2, 1]}\n onChange={(event) => setPropValue(schema.name, Number(event.currentTarget.value))}\n padding={2}\n value={value}\n />\n </Box>\n </Box>\n )\n})\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {Box, Select, Text} from '@sanity/ui'\nimport {memo, useMemo} from 'react'\n\nimport {SelectPropSchema} from '../types'\nimport {useProps} from '../useProps'\n\n/** @internal */\nexport const SelectProp = memo(function SelectProp(props: {\n schema: SelectPropSchema\n value: any\n}): React.ReactNode {\n const {schema, value: valueProp} = props\n const {setPropValue} = useProps()\n\n const value = useMemo(() => {\n const entries = Object.entries(schema.options)\n\n for (const [k, v] of entries) {\n if (v === valueProp) {\n return k\n }\n }\n\n return ''\n }, [schema, valueProp])\n\n return (\n <Box padding={3}>\n <Text size={1} weight=\"semibold\">\n {schema.name}\n </Text>\n <Box marginTop={2}>\n <Select\n fontSize={[2, 2, 1]}\n onChange={(event) => {\n const optionKey = event.currentTarget.value\n const optionValue = schema.options[optionKey as any]\n\n setPropValue(schema.name, optionValue)\n }}\n padding={2}\n radius={2}\n value={String(value || '')}\n >\n {Object.entries(schema.options).map(([key]) => (\n <option key={key} value={key}>\n {key}\n </option>\n ))}\n </Select>\n </Box>\n </Box>\n )\n})\n","import {Box, Text, TextInput} from '@sanity/ui'\nimport {memo} from 'react'\n\nimport {StringPropSchema} from '../types'\nimport {useProps} from '../useProps'\n\n/** @internal */\nexport const StringProp = memo(function StringProp(props: {\n schema: StringPropSchema\n value?: string\n}): React.ReactNode {\n const {schema, value} = props\n const {setPropValue} = useProps()\n\n return (\n <Box padding={3}>\n <Text size={1} weight=\"semibold\">\n {schema.name}\n </Text>\n <Box marginTop={2}>\n <TextInput\n fontSize={[2, 2, 1]}\n onChange={(event) => setPropValue(schema.name, event.currentTarget.value)}\n padding={2}\n value={value || ''}\n />\n </Box>\n </Box>\n )\n})\n","import {Box, Text, TextArea} from '@sanity/ui'\nimport {memo} from 'react'\n\nimport {TextPropSchema} from '../types'\nimport {useProps} from '../useProps'\n\n/** @internal */\nexport const TextProp = memo(function TextProp(props: {\n schema: TextPropSchema\n value?: string\n}): React.ReactNode {\n const {schema, value} = props\n const {setPropValue} = useProps()\n\n return (\n <Box padding={3}>\n <Text size={1} weight=\"semibold\">\n {schema.name}\n </Text>\n <Box marginTop={2}>\n <TextArea\n fontSize={[2, 2, 1]}\n onChange={(event) => setPropValue(schema.name, event.currentTarget.value)}\n rows={4}\n value={value || ''}\n />\n </Box>\n </Box>\n )\n})\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {Box, Text} from '@sanity/ui'\n\nimport {PropSchema} from '../types'\nimport {BooleanProp} from './booleanProp'\nimport {NumberProp} from './numberProp'\nimport {SelectProp} from './selectProp'\nimport {StringProp} from './stringProp'\nimport {TextProp} from './textProp'\n\n/** @internal */\nexport function Prop(props: {schema: PropSchema; value: any}): React.ReactNode {\n const {schema, value} = props\n\n if (schema.type === 'boolean') {\n return <BooleanProp schema={schema} value={value} />\n }\n\n if (schema.type === 'number') {\n return <NumberProp schema={schema} value={value} />\n }\n\n if (schema.type === 'select') {\n return <SelectProp schema={schema} value={value} />\n }\n\n if (schema.type === 'string') {\n return <StringProp schema={schema} value={value} />\n }\n\n if (schema.type === 'text') {\n return <TextProp schema={schema} value={value} />\n }\n\n return (\n <Box padding={2}>\n <Text size={1} weight=\"semibold\">\n Unknown Prop type:{' '}\n <code>\n {(schema as any).name}: {(schema as any).type}\n </code>\n </Text>\n </Box>\n )\n}\n","import {Box, Text} from '@sanity/ui'\nimport {memo} from 'react'\n\nimport {Prop} from './components/prop'\nimport {useProps} from './useProps'\n\n/** @internal */\nexport const PropsInspector = memo(function PropsInspector(): React.ReactNode {\n const {schemas, value} = useProps()\n\n return (\n <Box padding={2}>\n {schemas.length === 0 && (\n <Box padding={2}>\n <Text muted size={[2, 2, 1]}>\n No properties\n </Text>\n </Box>\n )}\n\n {schemas.length > 0 &&\n schemas.map((schema, schemaIndex) => (\n <Prop\n key={schemaIndex}\n schema={schema}\n value={value[schema.name] === undefined ? schema.defaultValue : value[schema.name]}\n />\n ))}\n </Box>\n )\n})\n","import lodashIsEqual from 'lodash/isEqual'\n\n/** @internal */\nexport const isEqual = lodashIsEqual\n","import {ThemeColorSchemeKey} from '@sanity/ui'\nimport {createContext} from 'react'\n\nimport {WorkshopCollection, WorkshopPlugin, WorkshopScope, WorkshopStory} from './config'\nimport {Pubsub} from './lib/pubsub'\nimport {WorkshopMsg} from './types'\n\n/** @public */\nexport interface WorkshopContextValue<CustomMsg = never> {\n plugins: WorkshopPlugin[]\n broadcast: (msg: WorkshopMsg | CustomMsg) => void\n channel: Pubsub<WorkshopMsg | CustomMsg>\n collections: WorkshopCollection[]\n frameReady: boolean\n frameUrl: string\n origin: 'frame' | 'main'\n path: string\n payload: Record<string, unknown>\n scheme: ThemeColorSchemeKey\n scope: WorkshopScope | null\n scopes: WorkshopScope[]\n story: WorkshopStory | null\n title: string\n viewport: string\n zoom: number\n}\n\n/** @internal */\nexport const WorkshopContext = createContext<WorkshopContextValue | null>(null)\n","import {useContext} from 'react'\n\nimport {WorkshopContext, WorkshopContextValue} from './WorkshopContext'\n\n/** @public */\nexport function useWorkshop<CustomMsg = never>(): WorkshopContextValue<CustomMsg> {\n const workshop = useContext(WorkshopContext)\n\n if (!workshop) {\n throw new Error('Workshop: missing context value')\n }\n\n return workshop as unknown as WorkshopContextValue<CustomMsg>\n}\n","import pako from 'pako'\n\nconst btoa =\n typeof window === 'undefined'\n ? (str: string) => Buffer.from(str, 'binary').toString('base64')\n : window.btoa\n\nconst atob =\n typeof window === 'undefined'\n ? (str: string) => Buffer.from(str, 'base64').toString('binary')\n : window.atob\n\nfunction uint8ArrayToBase64(uint8array: Uint8Array): string {\n let str = ''\n\n for (let i = 0, {length} = uint8array; i < length; i++) {\n str += String.fromCharCode(uint8array[i])\n }\n\n return btoa(str)\n}\n\nfunction base64ToUint8Array(base64: string) {\n const binStr = atob(base64)\n const len = binStr.length\n const bytes = new Uint8Array(len)\n\n for (let i = 0; i < len; i++) {\n bytes[i] = binStr.charCodeAt(i)\n }\n\n return bytes\n}\n\n/** @internal */\nexport function decode(input: string): string {\n if (input.length === 0) return ''\n\n const arr = base64ToUint8Array(input)\n\n return pako.inflate(arr, {to: 'string'})\n}\n\n/** @internal */\nexport function encode(input: string): string {\n if (input.length === 0) return ''\n\n const arr = pako.deflate(input)\n\n return uint8ArrayToBase64(arr)\n}\n","import {EMPTY_RECORD} from '../../constants'\nimport {decode, encode} from './lib/zlib'\n\n/** @internal */\nexport function encodeValue(val: Record<string, unknown>): string {\n return encode(JSON.stringify(val))\n}\n\n/** @internal */\nexport function decodeValue(val: string): Record<string, unknown> {\n try {\n return JSON.parse(decode(val))\n } catch (_) {\n return EMPTY_RECORD\n }\n}\n","import {WorkshopMsg} from '../../types'\nimport {PropsMsg} from './msg'\nimport {PropsState} from './types'\n\n/** @internal */\nexport function propsReducer(state: PropsState, msg: WorkshopMsg | PropsMsg): PropsState {\n if (msg.type === 'workshop/props/setValue') {\n if (state.value === msg.value) {\n return state\n }\n\n return {\n ...state,\n value: msg.value,\n }\n }\n\n if (msg.type === 'workshop/props/registerProp') {\n const schemaIsRegistered = state.schemas.some((s) => s.name === msg.schema.name)\n\n if (schemaIsRegistered) {\n return state\n }\n\n return {\n ...state,\n schemas: state.schemas.concat([msg.schema]),\n }\n }\n\n if (msg.type === 'workshop/props/setPropValue') {\n if (state.value[msg.name] === msg.value) {\n return state\n }\n\n return {\n ...state,\n value: {...state.value, [msg.name]: msg.value},\n }\n }\n\n return state\n}\n","import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'\n\nimport {EMPTY_ARRAY, EMPTY_RECORD} from '../../constants'\nimport {isEqual} from '../../lib/isEqual'\nimport {useWorkshop} from '../../useWorkshop'\nimport {decodeValue, encodeValue} from './helpers'\nimport {PropsMsg} from './msg'\nimport {PropsContext, PropsContextValue} from './PropsContext'\nimport {propsReducer} from './propsReducer'\nimport {PropSchema, PropsState} from './types'\n\n/** @internal */\nexport const PropsProvider = memo(function PropsProvider(props: {\n children?: React.ReactNode\n}): React.ReactNode {\n const {children} = props\n const {channel, broadcast, payload} = useWorkshop<PropsMsg>()\n const encodedValue = payload.value\n const encodedValueRef = useRef(encodedValue)\n\n const [{schemas, value}, setState] = useState<PropsState>(() => ({\n schemas: EMPTY_ARRAY,\n value: decodeValue(String(encodedValue)),\n }))\n\n const registerProp = useCallback(\n (schema: PropSchema) => {\n broadcast({type: 'workshop/props/registerProp', schema})\n },\n [broadcast],\n )\n\n const unregisterProp = useCallback(\n (name: string) => {\n broadcast({type: 'workshop/props/unregisterProp', name})\n },\n [broadcast],\n )\n\n const setPropValue = useCallback(\n (name: string, _value: unknown) => {\n broadcast({type: 'workshop/props/setPropValue', name, value: _value})\n },\n [broadcast],\n )\n\n const ctx: PropsContextValue = useMemo(\n () => ({registerProp, schemas, setPropValue, unregisterProp, value}),\n [registerProp, schemas, setPropValue, unregisterProp, value],\n )\n\n // Subscribe to global messages\n useEffect(\n () =>\n channel.subscribe((msg) => {\n setState((prevState) => {\n const nextState =\n msg.type === 'workshop/setPath'\n ? {schemas: EMPTY_ARRAY, value: EMPTY_RECORD}\n : propsReducer(prevState, msg)\n\n if (isEqual(prevState, nextState)) {\n return prevState\n }\n\n return nextState\n })\n }),\n [channel],\n )\n\n useEffect(() => {\n const nextEncodedValue = encodeValue(value)\n\n if (encodedValueRef.current !== nextEncodedValue) {\n encodedValueRef.current = nextEncodedValue\n\n broadcast({\n type: 'workshop/setPayloadValue',\n key: 'value',\n value: nextEncodedValue,\n })\n }\n }, [broadcast, value])\n\n useEffect(() => {\n if (encodedValueRef.current === encodedValue) {\n return\n }\n\n encodedValueRef.current = encodedValue\n\n setState((prevState) => {\n const nextValue = decodeValue(String(encodedValue)) || {}\n if (isEqual(prevState.value, nextValue)) {\n return prevState\n }\n\n return {\n ...prevState,\n value: nextValue,\n }\n })\n }, [encodedValue])\n\n return <PropsContext.Provider value={ctx}>{children}</PropsContext.Provider>\n})\n","import {useEffect} from 'react'\n\nimport {useProps} from '../useProps'\n\n/** @internal */\nexport function useBoolean(\n name: string,\n defaultValue?: boolean,\n groupName = 'Props',\n): boolean | undefined {\n const {registerProp, unregisterProp, value} = useProps()\n\n useEffect(() => {\n registerProp({\n type: 'boolean',\n groupName,\n name,\n defaultValue,\n })\n\n return () => unregisterProp(name)\n }, [defaultValue, groupName, name, registerProp, unregisterProp])\n\n return value[name] === undefined ? defaultValue : (value[name] as boolean)\n}\n","import {useEffect} from 'react'\n\nimport {useProps} from '../useProps'\n\n/** @internal */\nexport function useNumber(\n name: string,\n defaultValue?: number,\n groupName = 'Props',\n): number | undefined {\n const {registerProp, unregisterProp, value} = useProps()\n\n useEffect(() => {\n registerProp({\n type: 'number',\n groupName,\n name,\n defaultValue,\n })\n\n return () => unregisterProp(name)\n }, [defaultValue, groupName, name, registerProp, unregisterProp])\n\n return value[name] === undefined ? defaultValue : (value[name] as number)\n}\n","import {useEffect} from 'react'\n\nimport {SelectPropOptionsProp, SelectPropValue} from '../types'\nimport {useProps} from '../useProps'\n\n/** @internal */\nexport function useSelect<T extends SelectPropValue>(\n name: string,\n options: SelectPropOptionsProp<T>,\n defaultValue?: T,\n groupName = 'Props',\n): T | undefined {\n const {registerProp, unregisterProp, value} = useProps()\n\n useEffect(() => {\n registerProp({\n type: 'select',\n groupName,\n name,\n options: options as SelectPropOptionsProp,\n defaultValue,\n })\n\n return () => unregisterProp(name)\n }, [defaultValue, groupName, name, options, registerProp, unregisterProp])\n\n return value[name] === undefined ? defaultValue : (value[name] as T)\n}\n","import {useEffect} from 'react'\n\nimport {useProps} from '../useProps'\n\n/** @internal */\nexport function useString(\n name: string,\n defaultValue?: string,\n groupName = 'Props',\n): string | undefined {\n const {registerProp, unregisterProp, value} = useProps()\n\n useEffect(() => {\n registerProp({\n type: 'string',\n groupName,\n name,\n defaultValue,\n })\n\n return () => unregisterProp(name)\n }, [defaultValue, groupName, name, registerProp, unregisterProp])\n\n return value[name] === undefined ? defaultValue : (value[name] as string)\n}\n","import {useEffect} from 'react'\n\nimport {useProps} from '../useProps'\n\n/** @internal */\nexport function useText(\n name: string,\n defaultValue?: string,\n groupName = 'Props',\n): string | undefined {\n const {registerProp, unregisterProp, value} = useProps()\n\n useEffect(() => {\n registerProp({\n type: 'text',\n groupName,\n name,\n defaultValue,\n })\n\n return () => unregisterProp(name)\n }, [defaultValue, groupName, name, registerProp, unregisterProp])\n\n return value[name] === undefined ? defaultValue : (value[name] as string)\n}\n","export type {PropsContextValue} from './PropsContext'\n\nimport {WorkshopPlugin} from '../../config'\nimport {PropsInspector} from './PropsInspector'\nimport {PropsProvider} from './PropsProvider'\n\nexport * from './types'\n\n// export hooks\nexport * from './hooks'\nexport * from './useProps'\n\n/** @internal */\nexport function propsPlugin(): WorkshopPlugin {\n return {\n name: 'props',\n title: 'Properties',\n inspector: PropsInspector,\n provider: PropsProvider,\n }\n}\n","import {ThemeColorSchemeKey} from '@sanity/ui'\nimport {memo, useMemo} from 'react'\n\nimport {WorkshopConfig, WorkshopPlugin} from './config'\nimport {EMPTY_ARRAY, EMPTY_RECORD} from './constants'\nimport {resolveLocation} from './helpers'\nimport {Pubsub} from './lib/pubsub'\nimport {propsPlugin} from './plugins/props'\nimport {WorkshopMsg} from './types'\nimport {WorkshopContext} from './WorkshopContext'\n\n/** @internal */\nexport interface WorkshopProviderProps {\n broadcast: (msg: WorkshopMsg) => void\n children?: React.ReactNode\n channel: Pubsub<WorkshopMsg>\n config: WorkshopConfig\n frameReady: boolean\n origin: 'frame' | 'main'\n path: string\n payload: Record<string, unknown>\n scheme: ThemeColorSchemeKey\n viewport?: string\n zoom?: number\n}\n\n/** @internal */\nexport const WorkshopProvider = memo(function WorkshopProvider(\n props: WorkshopProviderProps,\n): React.ReactNode {\n const {\n broadcast,\n children,\n channel,\n config,\n frameReady,\n origin,\n path,\n payload,\n scheme,\n viewport = 'auto',\n zoom = 1,\n } = props\n\n const {\n plugins: pluginsProp = EMPTY_ARRAY,\n collections = EMPTY_ARRAY,\n frameUrl = '/frame/',\n scopes,\n title = 'Workshop',\n } = config\n\n if (!payload) {\n throw new Error('missing `payload` property')\n }\n\n const plugins: WorkshopPlugin[] = useMemo(() => [propsPlugin(), ...pluginsProp], [pluginsProp])\n const {scope, story} = useMemo(() => resolveLocation(scopes, path), [path, scopes])\n\n let wrappedChildren = children\n for (const plugin of plugins) {\n if (plugin.provider) {\n const Provider = plugin.provider\n wrappedChildren = (\n <Provider options={plugin.options || EMPTY_RECORD}>{wrappedChildren}</Provider>\n )\n }\n }\n\n return (\n <WorkshopContext.Provider\n value={{\n plugins,\n broadcast,\n channel,\n collections,\n frameReady,\n frameUrl,\n origin,\n path,\n payload,\n scheme,\n scope,\n scopes,\n story,\n title,\n viewport,\n zoom,\n }}\n >\n {wrappedChildren}\n </WorkshopContext.Provider>\n )\n})\n\nWorkshopProvider.displayName = 'Memo(WorkshopProvider)'\n","import {isEqual} from './lib/isEqual'\nimport {WorkshopMsg, WorkshopState} from './types'\n\n/** @internal */\nexport function workshopReducer(state: WorkshopState, msg: WorkshopMsg): WorkshopState {\n if (msg.type === 'workshop/frameReady') {\n return {...state, frameReady: true}\n }\n\n if (msg.type === 'workshop/setState') {\n if (isEqual(state, msg.value)) {\n return state\n }\n\n return msg.value\n }\n\n if (msg.type === 'workshop/setZoom') {\n if (state.zoom === msg.value) return state\n\n return {...state, zoom: msg.value}\n }\n\n if (msg.type === 'workshop/setViewport') {\n if (state.viewport === msg.value) return state\n\n return {...state, viewport: msg.value}\n }\n\n if (msg.type === 'workshop/toggleScheme') {\n return {...state, scheme: state.scheme === 'light' ? 'dark' : 'light'}\n }\n\n if (msg.type === 'workshop/setScheme') {\n if (state.scheme === msg.value) return state\n\n return {...state, scheme: msg.value}\n }\n\n if (msg.type === 'workshop/setPath') {\n if (state.path === msg.value) return state\n\n return {...state, path: msg.value}\n }\n\n if (msg.type === 'workshop/setPayload') {\n if (isEqual(state.payload, msg.value)) {\n return state\n }\n\n return {...state, payload: msg.value}\n }\n\n if (msg.type === 'workshop/setPayloadValue') {\n const payload = {...state.payload, [msg.key]: msg.value}\n\n if (isEqual(state.payload, payload)) {\n return state\n }\n\n return {...state, payload}\n }\n\n return state\n}\n","const ROOT_PATH = (() => {\n // Wrap in try/catch to avoid throwing exception in environments that don’t have `process.env`.\n try {\n return process.env.ROOT_PATH\n } catch (_) {\n return undefined\n }\n})()\n\nconst RE_URL = /http:\\/\\/([^:/\\s]+)(:[0-9]+)?/g\nconst RE_VITE_FS_PREFIX = /\\/@fs\\//g\nconst RE_VITE_FS_SUFFIX = /\\?([a-z]{1})=([0-9]+)/g\n\n/** @internal */\nexport function formatStack(stack: string): string {\n let ret = decodeURIComponent(stack)\n\n ret = stack.replace(RE_URL, '').replace(RE_VITE_FS_PREFIX, '/').replace(RE_VITE_FS_SUFFIX, '')\n\n if (ROOT_PATH) return replaceRootPath(ret, ROOT_PATH + '/')\n\n return ret\n}\n\nfunction replaceRootPath(str: string, rootPath: string) {\n const re = new RegExp(rootPath.replace(/\\//g, '\\\\/'), 'g')\n\n return str.replace(re, '')\n}\n","import {Box, Button, Card, Code, ErrorBoundary, Flex, Heading, Spinner, Stack} from '@sanity/ui'\nimport {createElement, memo, Suspense, useCallback, useState} from 'react'\n\nimport {WorkshopStory} from '../config'\nimport {useWorkshop} from '../useWorkshop'\nimport {formatStack} from './formatStack'\n\n/** @internal */\nexport const WorkshopCanvas = memo(function WorkshopCanvas(): React.ReactNode {\n const {story} = useWorkshop()\n const [state, setState] = useState<{error: Error | null; errorInfo: React.ErrorInfo | null}>({\n error: null,\n errorInfo: null,\n })\n\n const catchError = useCallback(\n ({error, info: errorInfo}: {error: Error; info: React.ErrorInfo}) => {\n setState({error, errorInfo})\n },\n [],\n )\n\n const handleRetry = useCallback(() => {\n setState({error: null, errorInfo: null})\n }, [])\n\n if (!story) {\n return <></>\n }\n\n if (state.error) {\n return (\n <Card as=\"main\" height=\"fill\" overflow=\"auto\" tone=\"critical\">\n <ErrorScreen error={state.error} errorInfo={state.errorInfo} onRetry={handleRetry} />\n </Card>\n )\n }\n\n return (\n <>\n <h1 hidden>{story.title}</h1>\n\n <Suspense fallback={<LoadingScreen story={story} />}>\n <Card as=\"main\" height=\"fill\">\n <ErrorBoundary onCatch={catchError}>{createElement(story.component)}</ErrorBoundary>\n </Card>\n </Suspense>\n </>\n )\n})\n\nconst LoadingScreen = memo(function LoadingScreen(props: {story: WorkshopStory}) {\n const {story} = props\n\n return (\n <>\n <h1 hidden>\n Loading <em>{story.title}</em>…\n </h1>\n\n <Flex align=\"center\" as=\"main\" height=\"fill\" justify=\"center\">\n <Spinner muted />\n </Flex>\n </>\n )\n})\n\nconst ErrorScreen = memo(function ErrorScreen(props: {\n error: Error\n errorInfo: React.ErrorInfo | null\n onRetry: () => void\n}) {\n const {error, errorInfo, onRetry} = props\n\n return (\n <Box padding={4}>\n <Stack space={4}>\n <Heading as=\"h1\" size={[1, 1, 2]}>\n {error.message}\n </Heading>\n <Box>\n <Button onClick={onRetry} text=\"Retry\" />\n </Box>\n {error.stack && <Code size={1}>{formatStack(error.stack)}</Code>}\n {errorInfo?.componentStack && (\n <Code size={1}>{'Component stack:' + formatStack(errorInfo.componentStack)}</Code>\n )}\n </Stack>\n </Box>\n )\n})\n","/** @internal */\nexport function isArray(value: unknown): value is unknown[] {\n return Array.isArray(value)\n}\n","/** @internal */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n","import {isArray} from '../lib/isArray'\nimport {isRecord} from '../lib/isRecord'\nimport {Pubsub} from '../lib/pubsub'\nimport {WorkshopMsg} from '../types'\n\nexport interface WorkshopMainController {\n message: Pubsub<WorkshopMsg>\n}\n\n/** @internal */\nexport function createMainController(): WorkshopMainController {\n const _subscribers = new Set<(msg: WorkshopMsg) => void>()\n\n let _msgQueue: WorkshopMsg[] = []\n let _flushTimeout: NodeJS.Timeout | null = null\n\n function _flush() {\n if (_flushTimeout) {\n clearInterval(_flushTimeout)\n }\n\n _flushTimeout = setTimeout(() => {\n window.parent.postMessage(_msgQueue)\n _msgQueue = []\n _flushTimeout = null\n }, 0)\n }\n\n function _handleMessage(event: MessageEvent<unknown>) {\n const msgs = event.data\n\n if (isArray(msgs)) {\n for (const msg of msgs) {\n if (isRecord(msg) && typeof msg.type === 'string' && msg.type.startsWith('workshop/')) {\n for (const subscriber of _subscribers) {\n subscriber(msg as unknown as WorkshopMsg)\n }\n }\n }\n }\n }\n\n function _mount() {\n window.addEventListener('message', _handleMessage, false)\n }\n\n function _unmount() {\n window.removeEventListener('message', _handleMessage, false)\n }\n\n return {\n message: {\n publish(msg: WorkshopMsg) {\n _msgQueue.push(msg)\n _flush()\n },\n subscribe(subscriber) {\n _subscribers.add(subscriber)\n\n if (_subscribers.size === 1) {\n _mount()\n }\n\n return () => {\n _subscribers.delete(subscriber)\n\n if (_subscribers.size === 0) {\n _unmount()\n }\n }\n },\n },\n }\n}\n","import {\n BoundaryElementProvider,\n Card,\n PortalProvider,\n ThemeColorSchemeKey,\n ToastProvider,\n} from '@sanity/ui'\nimport {memo, useCallback, useEffect, useMemo, useState} from 'react'\n\nimport {WorkshopConfig} from '../config'\nimport {createPubsub} from '../lib/pubsub'\nimport {qs} from '../lib/qs'\nimport {WorkshopMsg, WorkshopState} from '../types'\nimport {WorkshopProvider} from '../WorkshopProvider'\nimport {workshopReducer} from '../workshopReducer'\nimport {WorkshopCanvas} from './WorkshopCanvas'\nimport {createMainController} from './WorkshopMainController'\n\n/** @internal */\nexport interface WorkshopFrameProps {\n config: WorkshopConfig\n setScheme: (nextScheme: ThemeColorSchemeKey) => void\n}\n\nfunction getStateFromLocation(): WorkshopState {\n const query = typeof window === 'undefined' ? {} : qs.parse(window.location.search.slice(1))\n const {path = '/', scheme, viewport, zoom, ...payload} = query\n\n return {\n frameReady: false,\n path,\n payload,\n scheme: typeof scheme === 'string' ? (scheme as ThemeColorSchemeKey) : 'light',\n viewport: typeof viewport === 'string' ? viewport : 'auto',\n zoom: typeof zoom === 'string' ? Number(zoom) : 1,\n }\n}\n\n/** @internal */\nexport const WorkshopFrame = memo(function WorkshopFrame(\n props: WorkshopFrameProps,\n): React.ReactNode {\n const {config, setScheme} = props\n const main = useMemo(() => createMainController(), [])\n const channel = useMemo(() => createPubsub<WorkshopMsg>(), [])\n const [boundaryElement, setBoundaryElement] = useState<HTMLDivElement | null>(null)\n const [portalElement, setPortalElement] = useState<HTMLDivElement | null>(null)\n\n // Publish messages to both frame+main\n const broadcast = useCallback(\n (msg: WorkshopMsg) => {\n // Handle msg\n channel.publish(msg)\n\n // Pass message to main\n main.message.publish(msg)\n },\n [channel, main],\n )\n\n const [{frameReady, path, payload, scheme, viewport, zoom}, setState] = useState<WorkshopState>(\n () => getStateFromLocation(),\n )\n\n // Subscribe to global messages\n useEffect(() => channel.subscribe((msg) => setState((s) => workshopReducer(s, msg))), [channel])\n\n // Pipe messages from main to channel\n useEffect(() => main.message.subscribe(channel.publish), [channel, main])\n\n // Update scheme\n useEffect(() => setScheme(scheme), [setScheme, scheme])\n\n // Inform `main` that the frame is ready\n useEffect(() => broadcast({type: 'workshop/frameReady'}), [broadcast])\n\n return (\n <ToastProvider>\n <BoundaryElementProvider element={boundaryElement}>\n <PortalProvider element={portalElement}>\n <WorkshopProvider\n broadcast={broadcast}\n config={config}\n channel={channel}\n frameReady={frameReady}\n origin=\"frame\"\n path={path}\n payload={payload}\n scheme={scheme}\n viewport={viewport}\n zoom={zoom}\n >\n <Card height=\"fill\" ref={setBoundaryElement}>\n <WorkshopCanvas />\n <div data-portal=\"\" ref={setPortalElement} />\n </Card>\n </WorkshopProvider>\n </PortalProvider>\n </BoundaryElementProvider>\n </ToastProvider>\n )\n})\n","import {qs} from '../lib/qs'\nimport {WorkshopLocation} from '../types'\n\n/** @public */\nexport interface WorkshopLocationStore {\n get: () => Omit<WorkshopLocation, 'type'>\n push: (nextLocation: Omit<WorkshopLocation, 'type'>) => void\n replace: (nextLocation: Omit<WorkshopLocation, 'type'>) => void\n subscribe: (subscriber: (nextLocation: WorkshopLocation) => void) => () => void\n}\n\nfunction _buildLocationUrl(loc: Omit<WorkshopLocation, 'type'>): string {\n const search = qs.stringify(loc.query || {})\n\n return `${loc.path}${search ? `?${search}` : ''}`\n}\n\nfunction _getStateFromWindow(): Omit<WorkshopLocation, 'type'> {\n return {\n path: location.pathname,\n query: qs.parse(location.search.substr(1)),\n }\n}\n\n/** @internal */\nexport function createLocationStore(): WorkshopLocationStore {\n const _subscribers = new Set<(nextLocation: WorkshopLocation) => void>()\n\n function _handlePopState() {\n _notifySubscribers({type: 'pop', ..._getStateFromWindow()})\n }\n\n function _notifySubscribers(loc: WorkshopLocation) {\n for (const subscriber of _subscribers) {\n subscriber(loc)\n }\n }\n\n function _mount() {\n window.addEventListener('popstate', _handlePopState)\n }\n\n function _unmount() {\n window.removeEventListener('popstate', _handlePopState)\n }\n\n return {\n get() {\n return _getStateFromWindow()\n },\n push(nextLocation) {\n window.history.pushState(null, document.title, _buildLocationUrl(nextLocation))\n _notifySubscribers({type: 'push', ...nextLocation})\n },\n replace(nextLocation) {\n window.history.replaceState(null, document.title, _buildLocationUrl(nextLocation))\n _notifySubscribers({type: 'replace', ...nextLocation})\n },\n subscribe(subscribe: (nextLocation: WorkshopLocation) => void) {\n _subscribers.add(subscribe)\n\n if (_subscribers.size === 1) _mount()\n\n return () => {\n _subscribers.delete(subscribe)\n\n if (_subscribers.size === 0) _unmount()\n }\n },\n }\n}\n","import {createGlobalStyle} from 'styled-components'\n\nexport const GlobalStyle = createGlobalStyle`\n @font-face {\n font-family: 'Inter';\n font-style: normal;\n font-weight: 100 900;\n font-display: swap;\n src: url('https://rsms.me/inter/font-files/Inter-roman.var.woff2?v=3.19') format('woff2');\n font-named-instance: 'Regular';\n }\n\n @font-face {\n font-family: 'Inter';\n font-style: italic;\n font-weight: 100 900;\n font-display: swap;\n src: url('https://rsms.me/inter/font-files/Inter-italic.var.woff2?v=3.19') format('woff2');\n font-named-instance: 'Italic';\n }\n\n body {\n background-color: ${({theme}) => theme.sanity.color.base.bg};\n }\n`\n","import {Card, Layer, Tab, TabList} from '@sanity/ui'\nimport {CSSProperties, memo, useCallback, useMemo} from 'react'\nimport styled from 'styled-components'\n\nimport {InspectorTab} from './types'\n\nconst MemoTab = memo(Tab)\n\nconst Root = styled(Card)`\n line-height: 0;\n\n @media screen and (max-width: ${({theme}) => theme.sanity.media[1] - 1}px) {\n text-align: center;\n }\n`\n\nexport const InspectorHeader = memo(function InspectorHeader(props: {\n currentTabId: string | null\n onTabChange: (id: string) => void\n tabs: InspectorTab[]\n}) {\n const {currentTabId, onTabChange, tabs} = props\n\n const layerStyle: CSSProperties = useMemo(() => ({flex: 'none', position: 'sticky', top: 0}), [])\n\n const children = useMemo(\n () =>\n tabs.map((tab) => (\n <InspectorTabView\n key={tab.id}\n onTabChange={onTabChange}\n selected={tab.id === currentTabId}\n tab={tab}\n />\n )),\n [currentTabId, onTabChange, tabs],\n )\n\n return (\n <Layer style={layerStyle}>\n <Root padding={2} shadow={1}>\n <TabList space={1}>{children}</TabList>\n </Root>\n </Layer>\n )\n})\n\nfunction InspectorTabView(props: {\n onTabChange: (id: string) => void\n selected: boolean\n tab: InspectorTab\n}) {\n const {onTabChange, selected, tab} = props\n\n const handleClick = useCallback(() => {\n onTabChange(tab.id)\n }, [onTabChange, tab])\n\n return (\n <MemoTab\n aria-controls={`${tab.id}-panel`}\n fontSize={[2, 2, 1]}\n id={tab.id}\n label={tab.label}\n onClick={handleClick}\n selected={selected}\n tone={tab.tone}\n />\n )\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {Box, BoxDisplay, Card, Flex, TabPanel} from '@sanity/ui'\nimport {ElementType, memo, useState} from 'react'\nimport styled from 'styled-components'\n\nimport {EMPTY_RECORD} from '../constants'\nimport {useWorkshop} from '../useWorkshop'\nimport {InspectorHeader} from './InspectorHeader'\nimport {InspectorTab} from './types'\n\nconst Root = styled(Card)`\n overflow: hidden;\n\n @media screen and (min-width: ${({theme}) => theme.sanity.media[1]}px) {\n border-left: 1px solid var(--card-border-color);\n min-width: 180px;\n max-width: 300px;\n overflow: auto;\n }\n`\n\nconst MemoRender = memo(function MemoRender(props: {component: ElementType; options: any}) {\n const {component: Component, options} = props\n return <Component options={options} />\n})\n\n/** @internal */\nexport const WorkshopInspector = memo(function WorkshopInspector(props: {\n expanded: boolean\n}): React.ReactNode {\n const {expanded} = props\n const {plugins} = useWorkshop()\n\n const tabs: InspectorTab[] = plugins\n .filter((plugin) => plugin.inspector)\n .map((plugin) => {\n return {\n id: plugin.name,\n label: plugin.title,\n tone: undefined,\n plugin,\n }\n })\n\n const [tabId, setTabId] = useState<string | null>(tabs.length > 0 ? tabs[0].id : null)\n const currentTab = tabs.find((tab) => tab.id === tabId)\n const showTabs = tabs.length > 1\n\n const display: BoxDisplay[] = expanded ? ['block'] : ['none', 'none', 'block']\n\n return (\n <Root display={display} flex={1}>\n <Flex direction=\"column\" height=\"fill\">\n {showTabs && <InspectorHeader currentTabId={tabId} onTabChange={setTabId} tabs={tabs} />}\n\n {showTabs &&\n tabs.map((tab) => (\n <TabPanel\n aria-labelledby={`${tab.id}-tab`}\n flex={1}\n hidden={tab.id !== tabId}\n id={`${tab.id}-panel`}\n key={tab.id}\n overflow=\"auto\"\n >\n {tab.plugin.inspector && (\n <MemoRender\n component={tab.plugin.inspector}\n options={tab.plugin.options || EMPTY_RECORD}\n />\n )}\n </TabPanel>\n ))}\n\n {!showTabs && currentTab?.plugin.inspector && (\n <Box flex={1} overflow=\"auto\">\n <MemoRender\n component={currentTab.plugin.inspector}\n options={currentTab.plugin.options || EMPTY_RECORD}\n />\n </Box>\n )}\n </Flex>\n </Root>\n )\n})\n","import lodashDebounce from 'lodash/debounce'\n\n/** @internal */\nexport const debounce = lodashDebounce\n","import {Breadcrumbs, Text} from '@sanity/ui'\nimport {memo, useCallback} from 'react'\n\nimport {useWorkshop} from '../useWorkshop'\n\n/** @internal */\nexport function NavbarBreadcrumbs(): React.ReactNode {\n const {broadcast, scope, story, title} = useWorkshop()\n\n const handleHomeClick = useCallback(\n (event: React.MouseEvent) => {\n event.preventDefault()\n broadcast({type: 'workshop/setPath', value: '/'})\n },\n [broadcast],\n )\n\n return (\n <NavbarBreadcrumbsView\n onHomeClick={handleHomeClick}\n scopeTitle={scope?.title}\n storyTitle={story?.title}\n title={title}\n />\n )\n}\n\nconst NavbarBreadcrumbsView = memo(function NavbarBreadcrumbsView(props: {\n onHomeClick: (event: React.MouseEvent) => void\n scopeTitle?: string\n storyTitle?: string\n title: string\n}) {\n const {onHomeClick, scopeTitle, storyTitle, title} = props\n\n return (\n <Breadcrumbs\n separator={\n <Text muted size={[2, 2, 1]}>\n /\n </Text>\n }\n space={2}\n >\n <Text size={[2, 2, 1]} weight=\"bold\">\n <a href=\"/\" onClick={onHomeClick} style={{color: 'inherit'}}>\n {title}\n </a>\n </Text>\n\n {scopeTitle && (\n <Text align=\"center\" size={[2, 2, 1]}>\n {scopeTitle}\n </Text>\n )}\n\n {storyTitle && <Text size={[2, 2, 1]}>{storyTitle}</Text>}\n </Breadcrumbs>\n )\n})\n","import {LaunchIcon} from '@sanity/icons'\nimport {Button} from '@sanity/ui'\nimport {memo, useMemo} from 'react'\n\nimport {buildFrameUrl} from '../helpers'\nimport {useWorkshop} from '../useWorkshop'\n\n/** @internal */\nexport const OpenCanvasButton = memo(function OpenCanvasButton() {\n const {frameUrl, path, payload, scheme, zoom, viewport} = useWorkshop()\n\n const canvasUrl = useMemo(\n () =>\n path === '/'\n ? undefined\n : buildFrameUrl({baseUrl: frameUrl, path, payload, scheme, zoom, viewport}),\n [frameUrl, path, payload, scheme, zoom, viewport],\n )\n\n return (\n <Button\n as={canvasUrl ? 'a' : 'button'}\n disabled={!canvasUrl}\n fontSize={1}\n href={canvasUrl}\n iconRight={LaunchIcon}\n mode=\"ghost\"\n padding={2}\n rel=\"noopener noreferrer\"\n target=\"_blank\"\n text=\"Open story\"\n />\n )\n})\n","import {MoonIcon, SunIcon} from '@sanity/icons'\nimport {Button} from '@sanity/ui'\nimport {memo, useCallback} from 'react'\n\nimport {useWorkshop} from '../useWorkshop'\n\n/** @internal */\nexport function SchemeMenu(): React.ReactNode {\n const {broadcast, scheme} = useWorkshop()\n\n const handleToggleScheme = useCallback(() => {\n broadcast({type: 'workshop/toggleScheme'})\n }, [broadcast])\n\n return <SchemeMenuView dark={scheme === 'dark'} onToggleScheme={handleToggleScheme} />\n}\n\nconst SchemeMenuView = memo(function SchemeMenuView(props: {\n dark: boolean\n onToggleScheme: () => void\n}) {\n const {dark, onToggleScheme} = props\n\n return (\n <Button\n fontSize={1}\n icon={dark ? MoonIcon : SunIcon}\n mode=\"bleed\"\n onClick={onToggleScheme}\n padding={2}\n />\n )\n})\n","import {SelectIcon} from '@sanity/icons'\nimport {Button, Menu, MenuButton, MenuButtonProps, MenuItem} from '@sanity/ui'\nimport {memo, useCallback} from 'react'\n\nimport {VIEWPORT_OPTIONS} from '../constants'\nimport {useWorkshop} from '../useWorkshop'\n\n/** @internal */\nexport const ViewportMenu = memo(function ViewportMenu() {\n const {broadcast, story, viewport} = useWorkshop()\n\n const setViewport = useCallback(\n (value: string) => {\n broadcast({type: 'workshop/setViewport', value})\n },\n [broadcast],\n )\n\n return <ViewportMenuView disabled={!story} setViewport={setViewport} viewport={viewport} />\n})\n\nconst POPOVER_PROPS: MenuButtonProps['popover'] = {\n constrainSize: true,\n placement: 'bottom',\n portal: true,\n}\n\nconst ViewportMenuView = memo(function ViewportMenuView(props: {\n disabled: boolean\n setViewport: (v: string) => void\n viewport: string\n}) {\n const {disabled, setViewport, viewport} = props\n\n return (\n <MenuButton\n button={\n <Button\n disabled={disabled}\n fontSize={1}\n iconRight={SelectIcon}\n mode=\"ghost\"\n padding={2}\n text={VIEWPORT_OPTIONS.find((o) => o.name === viewport)?.title}\n />\n }\n id=\"viewport-menu\"\n menu={\n <Menu>\n {VIEWPORT_OPTIONS.map((option) => (\n <MenuItem\n fontSize={1}\n key={option.name}\n onClick={() => setViewport(option.name)}\n padding={2}\n selected={option.name === viewport}\n text={option.title}\n />\n ))}\n </Menu>\n }\n popover={POPOVER_PROPS}\n />\n )\n})\n","import {SelectIcon} from '@sanity/icons'\nimport {Button, Menu, MenuButton, MenuButtonProps, MenuItem} from '@sanity/ui'\nimport {memo, useCallback} from 'react'\n\nimport {ZOOM_OPTIONS} from '../constants'\nimport {useWorkshop} from '../useWorkshop'\n\n/** @internal */\nexport function ZoomMenu(): React.ReactNode {\n const {broadcast, story, zoom} = useWorkshop()\n\n const setZoom = useCallback(\n (value: number) => broadcast({type: 'workshop/setZoom', value}),\n [broadcast],\n )\n\n return <ZoomMenuView disabled={!story} setZoom={setZoom} zoom={zoom} />\n}\n\nconst POPOVER_PROPS: MenuButtonProps['popover'] = {\n constrainSize: true,\n placement: 'bottom',\n portal: true,\n}\n\nconst ZoomMenuView = memo(function ZoomMenuView(props: {\n disabled: boolean\n setZoom: (z: number) => void\n zoom: number\n}) {\n const {disabled, setZoom, zoom} = props\n\n return (\n <MenuButton\n button={\n <Button\n disabled={disabled}\n fontSize={1}\n iconRight={SelectIcon}\n mode=\"ghost\"\n padding={2}\n text={ZOOM_OPTIONS.find((o) => o.value === zoom)?.title}\n