├── .env ├── .env.local.example ├── .eslintrc.json ├── .gitignore ├── .npmrc ├── .prettierignore ├── ColorConfigEditor.tsx ├── LICENSE ├── README.md ├── chapters └── 01 │ └── index.tsx ├── components ├── ColorTintsPreview.tsx ├── ImagePalettePreview.tsx ├── Playground.tsx ├── Sandbox.tsx ├── Singularity.tsx ├── StudioPage.tsx ├── StudioPageLoader.tsx ├── SuperLivePreview.tsx ├── ThemePreview.tsx ├── blog │ ├── Alert.tsx │ ├── Avatar.tsx │ ├── Container.tsx │ ├── CoverImage.tsx │ ├── Date.tsx │ ├── Footer.tsx │ ├── Header.tsx │ ├── HeroPost.tsx │ ├── Intro.tsx │ ├── Layout.tsx │ ├── Meta.tsx │ ├── MoreStories.tsx │ ├── PostBody.tsx │ ├── PostHeader.tsx │ ├── PostPreview.tsx │ ├── PostTitle.tsx │ ├── SectionSeparator.tsx │ └── markdown-styles.module.css └── studios │ ├── blog-pro-max │ └── index.tsx │ ├── blog-pro │ ├── BlogPreviewWrapper.tsx │ └── index.tsx │ ├── blog │ ├── blogConfig.tsx │ ├── blogSchema.ts │ └── index.tsx │ └── themer │ ├── index.tsx │ ├── themerConfig.tsx │ └── themerSchema.ts ├── hooks ├── index.ts ├── useListeningQuery.ts ├── useSanityClient.ts └── useSanityStudio.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── api │ ├── exit-preview.ts │ ├── preview.ts │ └── studio │ │ └── workspaces.tsx ├── index.tsx ├── manage │ └── [[...workspace]].tsx ├── playground.tsx ├── posts │ └── [slug].tsx └── wp-admin │ └── [[...insanity]].tsx ├── postcss.config.js ├── public ├── eh │ ├── black_body.dat │ ├── colors.cc │ ├── colors.h │ ├── colors.inc │ ├── cube_map.cc │ ├── cube_map.h │ ├── deflection.dat │ ├── doppler.dat │ ├── gaia.cc │ ├── gaia.h │ ├── gaia_color.cc │ ├── gaia_color.h │ ├── gaia_sky_map_generator.cc │ ├── index.html │ ├── inverse_radius.dat │ ├── noise_texture.png │ ├── rocket.dat │ ├── rocket_base_color.png │ ├── rocket_normal.png │ ├── rocket_occlusion_roughness_metallic.png │ ├── tycho.cc │ └── tycho.h ├── favicon.ico ├── preview-mode.svg ├── sanity.svg ├── studio.html └── vercel.svg ├── sanity.config.ts ├── styles └── globals.css ├── tailwind.config.js └── tsconfig.json /.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SINGULARITY=true 2 | # Required, every demo uses this project 3 | NEXT_PUBLIC_SANITY_PROJECT_ID="rkndubl4" 4 | NEXT_PUBLIC_SANITY_DATASET="production" 5 | NEXT_PUBLIC_SANITY_THEMER_DATASET="themes" 6 | SANITY_PREVIEW_SECRET="123" 7 | NEXT_PUBLIC_SANITY_PREVIEW_SECRET="123" 8 | SANITY_API_TOKEN="skqOpaJs7Bo6wQrsU3bNulf7D3zf7JWvPDBMxIU89uZA7Tdec0n9CrI2dLdq5yaSGsLpeupsisF1yAyGQs2pFkE4HMt5bdh8W5wK0ZNtxSVqotXamEFm3HjjpDKEyexhUnUasfN5yhIMFaVG8XThLM0o5M2OtYM4xDrGYVzXavmOSPM3r62j" -------------------------------------------------------------------------------- /.env.local.example: -------------------------------------------------------------------------------- 1 | # Create a "Viewer" Token in sanity.io/manage 2 | SANITY_API_TOKEN= 3 | # Any random string 4 | SANITY_PREVIEW_SECRET= 5 | # Required, every demo uses this project 6 | NEXT_PUBLIC_SANITY_PROJECT_ID= 7 | NEXT_PUBLIC_SANITY_DATASET="prodiction" 8 | NEXT_PUBLIC_SANITY_THEMER_DATASET="themes" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | .vercel 3 | package-lock.json -------------------------------------------------------------------------------- /ColorConfigEditor.tsx: -------------------------------------------------------------------------------- 1 | import { black, white } from '@sanity/color' 2 | import Link from 'next/link' 3 | import Sandbox from 'components/Sandbox' 4 | 5 | import { useId } from '@reach/auto-id' 6 | import { useState, useMemo, useLayoutEffect, useEffect } from 'react' 7 | import styled from 'styled-components' 8 | import { 9 | black as _black, 10 | type ColorTints, 11 | type ColorValue, 12 | hues as _hues, 13 | white as _white, 14 | type ColorHueConfig, 15 | COLOR_TINTS, 16 | type ColorHueKey, 17 | type ColorTintKey, 18 | } from '@sanity/color' 19 | import { defaultTheme, StudioProvider, StudioLayout } from 'sanity' 20 | import { 21 | ThemeProvider, 22 | Avatar, 23 | Container, 24 | Card, 25 | Flex, 26 | Box, 27 | rgbToHex, 28 | Spinner, 29 | Text, 30 | Label, 31 | Grid, 32 | Inline, 33 | Switch, 34 | Checkbox, 35 | createColorTheme, 36 | Select, 37 | usePrefersDark, 38 | parseColor, 39 | Heading, 40 | } from '@sanity/ui' 41 | import { unstable_batchedUpdates } from 'react-dom' 42 | import { useColorConfigState, createTintsFromHue } from 'hooks' 43 | import ColorTintsPreview from 'components/ColorTintsPreview' 44 | 45 | interface Props { 46 | state: Omit 47 | title: string 48 | setDarkest: (state: string) => void 49 | setLightest: (state: string) => void 50 | setMid: (state: string) => void 51 | setMidPoint: (state: number) => void 52 | open?: boolean 53 | } 54 | export default function ColorConfigEditor({ 55 | state, 56 | title, 57 | setMid, 58 | setMidPoint, 59 | setDarkest, 60 | setLightest, 61 | open, 62 | }: Props) { 63 | const config = useMemo(() => ({ ...state, title }), [state, title]) 64 | const { 65 | state: previewState, 66 | setMid: setMidPreview, 67 | setDarkest: setDarkestPreview, 68 | setLightest: setLightestPreview, 69 | setMidPoint: setMidPointPreview, 70 | } = useColorConfigState(config) 71 | // @TODO stop spreading on every update 72 | const previewConfig = useMemo( 73 | () => ({ 74 | ...previewState, 75 | midPoint: roundToScale(previewState.midPoint), 76 | title, 77 | }), 78 | [previewState, title] 79 | ) 80 | const tints = createTintsFromHue(previewConfig) 81 | // Debounce updates as updating a ThemeProvider context is slow 82 | const [tick, setTick] = useState(0) 83 | 84 | /* 85 | // Workspaces fails to load in React v18, unclear why 86 | const [working, startTransition] = 87 | 'useTransition' in React 88 | ? // eslint-disable-next-line react-hooks/rules-of-hooks 89 | React.useTransition() 90 | : // eslint-disable-next-line react-hooks/rules-of-hooks 91 | [tick > 0, unstable_batchedUpdates] 92 | // */ 93 | const working = tick > 0 94 | const startTransition = unstable_batchedUpdates 95 | 96 | // Reset syncing to parent if parent have new props 97 | useEffect(() => { 98 | startTransition(() => setTick(0)) 99 | }, [startTransition, state]) 100 | 101 | // Schedule state update to parent after a debounce 102 | useEffect(() => { 103 | if (tick) { 104 | const changed = 105 | previewState.lightest != state.lightest || 106 | previewState.darkest != state.darkest || 107 | previewState.mid != state.mid || 108 | previewState.midPoint != state.midPoint 109 | if (changed) { 110 | const { mid, midPoint, darkest, lightest } = previewState 111 | /* 112 | if ('startTransition' in React) { 113 | startTransition(() => { 114 | setMid(mid) 115 | setMidPoint(roundToScale(midPoint)) 116 | setDarkest(darkest) 117 | setLightest(lightest) 118 | }) 119 | return 120 | } 121 | // */ 122 | if ('requestIdleCallback' in window) { 123 | const idleCB = requestIdleCallback( 124 | () => { 125 | unstable_batchedUpdates(() => { 126 | setMid(mid) 127 | setMidPoint(roundToScale(midPoint)) 128 | setDarkest(darkest) 129 | setLightest(lightest) 130 | }) 131 | }, 132 | { timeout: 1000 } 133 | ) 134 | return () => cancelIdleCallback(idleCB) 135 | } 136 | const timeout = setTimeout(() => { 137 | unstable_batchedUpdates(() => { 138 | setMid(mid) 139 | setMidPoint(roundToScale(midPoint)) 140 | setDarkest(darkest) 141 | setLightest(lightest) 142 | }) 143 | }, 100) 144 | return () => clearTimeout(timeout) 145 | } 146 | startTransition(() => { 147 | setTick(0) 148 | }) 149 | } 150 | }, [ 151 | previewState, 152 | setDarkest, 153 | setLightest, 154 | setMid, 155 | setMidPoint, 156 | startTransition, 157 | state.darkest, 158 | state.lightest, 159 | state.mid, 160 | state.midPoint, 161 | tick, 162 | ]) 163 | 164 | const midRangeId = `mid-range-${useId()}` 165 | 166 | return ( 167 | 168 | 175 | {config.title}{' '} 176 | {working && ( 177 | 178 | 186 | 187 | )} 188 | 189 | 190 | 191 | 192 | 193 | 200 | { 207 | setMidPreview(value) 208 | startTransition(() => setMid(value)) 209 | } 210 | : setMidPreview 211 | // */ 212 | setMidPreview 213 | } 214 | setTick={setTick} 215 | /> 216 | 222 | 228 | 229 | 232 | { 239 | const { value } = event.currentTarget 240 | const midPoint = Number(value) 241 | setMidPointPreview(roundToScale(midPoint)) 242 | }} 243 | title={`${previewState.midPoint}`} 244 | value={previewState.midPoint} 245 | onBlur={(event) => { 246 | const { value } = event.currentTarget 247 | const midPoint = Number(value) 248 | setMidPointPreview(roundToScale(midPoint)) 249 | setTick((tick) => ++tick) 250 | }} 251 | onChange={(event) => { 252 | const { value } = event.target 253 | const midPoint = Number(value) 254 | setMidPointPreview(midPoint) 255 | if (process.env.NODE_ENV === 'production') 256 | setTick((tick) => ++tick) 257 | // setTick((tick) => ++tick) 258 | 259 | // unstable_batchedUpdates(() => setMid(value)) 260 | }} 261 | list={midRangeId} 262 | /> 263 | 264 | {COLOR_TINTS.map((tint) => ( 265 | 268 | 269 | 270 | 271 | 272 | ) 273 | } 274 | 275 | function roundToScale(value: number): number { 276 | if (value < 75) { 277 | return 50 278 | } 279 | if (value > 925) { 280 | return 950 281 | } 282 | 283 | return Math.round(value / 100) * 100 284 | } 285 | 286 | function ColorPicker({ 287 | label, 288 | value, 289 | setValue, 290 | setTick, 291 | }: { 292 | label: string 293 | value: string 294 | setValue: (value: string) => void 295 | setTick: React.Dispatch> 296 | }) { 297 | const parsedColor = useMemo(() => rgbToHex(parseColor(value)), [value]) 298 | return ( 299 | 300 | 303 | setTick((tick) => ++tick)} 307 | onBlur={() => setTick((tick) => ++tick)} 308 | onChange={(event) => { 309 | const { value } = event.target 310 | setValue(value) 311 | if (process.env.NODE_ENV === 'production') setTick((tick) => ++tick) 312 | }} 313 | /> 314 | 320 | {parsedColor} 321 | 322 | 323 | ) 324 | } 325 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sanity 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Note, we have released a [new version of `next-sanity` with support for embedding Studio v3](https://github.com/sanity-io/next-sanity#next-sanitystudio-dev-preview) inside Next.js 2 | 3 | > Please use that instead of using this repo as a guideline of how to do it. :bow: 4 | 5 | # Using the Studio Within an Application in v3 6 | 7 | [Before digging into the code and what's happening here, checkout the introduction to V3.](https://github.com/sanity-io/sanity/discussions/3339) 8 | 9 | [And the demo this project was made for.](https://youtu.be/298mlqa1-Hk) 10 | 11 | ![image](https://user-images.githubusercontent.com/81981/173820595-a07daf71-e5df-4eb3-ba87-948dc7052366.png) 12 | 13 | 14 | And have fun exploring! If you have any questions you can reach us [on Slack](https://slack.sanity.io/), and [our new GitHub Discussions](https://github.com/sanity-io/sanity/discussions) site ✨ 15 | -------------------------------------------------------------------------------- /chapters/01/index.tsx: -------------------------------------------------------------------------------- 1 | import { type SanityDocument, type Image } from '@sanity/types' 2 | import Head from 'next/head' 3 | import Container from 'components/blog/Container' 4 | import MoreStories from 'components/blog/MoreStories' 5 | import HeroPost from 'components/blog/HeroPost' 6 | import Intro from 'components/blog/Intro' 7 | import Layout from 'components/blog/Layout' 8 | import Link from 'next/link' 9 | import { WhoIsEditing } from 'pages/posts/[slug]' 10 | import { StudioProvider, type SanityDocumentLike } from 'sanity' 11 | import config from 'sanity.config' 12 | import { useMagicRouter } from 'hooks' 13 | 14 | export function WrappedWhoIisEditing({ documentId }: { documentId: string }) { 15 | const history = useMagicRouter( 16 | `${config[0].basePath}/desk/post;${documentId}` 17 | ) 18 | return ( 19 | 24 | 25 | 26 | ) 27 | } 28 | 29 | export default function Index({ 30 | allPosts, 31 | preview, 32 | }: { 33 | allPosts: { 34 | _id?: string 35 | title?: string 36 | coverImage?: Image 37 | date?: string 38 | author?: { name: string; picture: Image } 39 | slug?: string 40 | excerpt?: string 41 | }[] 42 | preview: boolean 43 | }) { 44 | const [heroPost, ...morePosts] = allPosts 45 | return ( 46 | <> 47 | 48 | 49 | Next.js Blog Example with Sanity 50 | 51 | 52 | 53 | {heroPost?._id && } 54 | {heroPost && ( 55 | 63 | )} 64 | {morePosts.length > 0 && } 65 | 66 | 67 | 68 | 75 | 79 | 84 | 89 | 90 | Launch Studio (iframe embedded) 91 | 92 | 93 | 94 | 95 | ) 96 | } 97 | -------------------------------------------------------------------------------- /components/ColorTintsPreview.tsx: -------------------------------------------------------------------------------- 1 | import { mix } from 'polished' 2 | import { 3 | black as _black, 4 | type ColorTints, 5 | type ColorValue, 6 | hues as _hues, 7 | white as _white, 8 | type ColorHueConfig, 9 | COLOR_TINTS, 10 | type ColorHueKey, 11 | type ColorTintKey, 12 | } from '@sanity/color' 13 | import { useState, useMemo, useLayoutEffect, useEffect, memo } from 'react' 14 | import { useRouter } from 'next/router' 15 | import { 16 | type Theme, 17 | rgba, 18 | multiply as _multiply, 19 | screen as _screen, 20 | parseColor, 21 | rgbToHex, 22 | ThemeProvider, 23 | Container, 24 | Card, 25 | Flex, 26 | Box, 27 | Text, 28 | Label, 29 | Grid, 30 | Stack, 31 | Inline, 32 | Switch, 33 | Checkbox, 34 | createColorTheme, 35 | Select, 36 | usePrefersDark, 37 | } from '@sanity/ui' 38 | 39 | interface Props { 40 | tints: ColorTints 41 | } 42 | function ColorTintsPreview({ tints }: Props) { 43 | return ( 44 | <> 45 | {Object.entries(tints).map(([tint, color]) => ( 46 | 47 | 56 | 57 | 58 | {tint} 59 | 60 | 61 | 62 | 63 | {color.hex} 64 | 65 | 66 | 67 | ))} 68 | 69 | ) 70 | } 71 | export default memo(ColorTintsPreview) 72 | -------------------------------------------------------------------------------- /components/ImagePalettePreview.tsx: -------------------------------------------------------------------------------- 1 | import { black, white } from '@sanity/color' 2 | import Link from 'next/link' 3 | import Sandbox from 'components/Sandbox' 4 | import getWordpressConfig from 'sanity.config' 5 | import { BottomSheet } from 'react-spring-bottom-sheet' 6 | import React, { 7 | useState, 8 | useMemo, 9 | useLayoutEffect, 10 | useEffect, 11 | Suspense, 12 | } from 'react' 13 | import styled from 'styled-components' 14 | import { 15 | defaultTheme, 16 | StudioProvider, 17 | StudioLayout, 18 | useWorkspace, 19 | type ImagePalette, 20 | type SwatchName, 21 | } from 'sanity' 22 | import { 23 | ThemeProvider, 24 | Container, 25 | Card, 26 | Flex, 27 | Box, 28 | Text, 29 | Label, 30 | Grid, 31 | Inline, 32 | Switch, 33 | Checkbox, 34 | createColorTheme, 35 | Select, 36 | Stack, 37 | usePrefersDark, 38 | } from '@sanity/ui' 39 | import { createMemoryHistory, createHashHistory, type Listener } from 'history' 40 | import * as stable from 'sanity' 41 | import * as unstable from 'sanity/_unstable' 42 | import { unstable_batchedUpdates } from 'react-dom' 43 | import ThemePreview from 'components/ThemePreview' 44 | import { 45 | createStudioTheme, 46 | useColorConfigState, 47 | useTonesFromPreset, 48 | demoImagePalette, 49 | } from 'hooks' 50 | import ColorTintsPreview from 'components/ColorTintsPreview' 51 | 52 | interface Props { 53 | value: ImagePalette 54 | } 55 | 56 | const previewSwatches: SwatchName[] = [ 57 | 'darkMuted', 58 | 'darkVibrant', 59 | 'dominant', 60 | 'lightMuted', 61 | 'lightVibrant', 62 | 'muted', 63 | 'vibrant', 64 | ] 65 | 66 | export default function ImagePalettePreview({ value }: Props) { 67 | return ( 68 | <> 69 | {previewSwatches.map((swatch) => { 70 | const palette = value[swatch] 71 | if (!palette) return null 72 | return ( 73 | 74 | 75 | {swatch} 76 | 77 | 78 | {[ 79 | 'background' as const, 80 | // 'foreground' as const, 81 | // 'title' as const, 82 | ].map((key) => ( 83 | 84 | 93 | 94 | 95 | {key} 96 | 97 | 98 | 99 | 100 | {palette[key]} 101 | 102 | 103 | 104 | ))} 105 | 106 | 107 | ) 108 | })} 109 | 110 | ) 111 | } 112 | -------------------------------------------------------------------------------- /components/Playground.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import Sandbox from 'components/Sandbox' 3 | 4 | import { BottomSheet } from 'react-spring-bottom-sheet' 5 | import React, { useState, useMemo, useEffect } from 'react' 6 | import styled from 'styled-components' 7 | import { config as themerConfig } from 'components/studios/themer' 8 | 9 | import { defaultTheme, StudioProvider, StudioLayout } from 'sanity' 10 | import { 11 | ThemeProvider, 12 | Card, 13 | Flex, 14 | Box, 15 | Text, 16 | Grid, 17 | Inline, 18 | Switch, 19 | Select, 20 | } from '@sanity/ui' 21 | 22 | import { unstable_batchedUpdates } from 'react-dom' 23 | import ThemePreview from 'components/ThemePreview' 24 | import { 25 | createStudioTheme, 26 | useColorConfigState, 27 | useTonesFromPreset, 28 | demoImagePalette, 29 | useMagicRouter, 30 | } from 'hooks' 31 | import ColorConfigEditor from 'ColorConfigEditor' 32 | import ImagePalettePreview from 'components/ImagePalettePreview' 33 | import { useColorScheme } from 'hooks/useSanityStudio' 34 | 35 | //const location = createLocation({pathname: '/desk'}) 36 | //const history = createMemoryHistory({initialEntries: ['/'], initialIndex: 1}) 37 | 38 | const RENDER_INLINE = true 39 | 40 | console.debug({ defaultTheme }) 41 | type StudioTheme = typeof defaultTheme 42 | function IndexPage() { 43 | // const history = useMagicRouter(config[0].basePath || '/') 44 | const history = useMagicRouter('/') 45 | 46 | const [preset, setPreset] = useState('imagepalette') 47 | const colorConfigs = useTonesFromPreset({ preset }) 48 | const defaultColorState = useColorConfigState(colorConfigs.default) 49 | const transparentColorState = useColorConfigState(colorConfigs.transparent) 50 | const primaryColorState = useColorConfigState(colorConfigs.primary) 51 | const positiveColorState = useColorConfigState(colorConfigs.positive) 52 | const cautionColorState = useColorConfigState(colorConfigs.caution) 53 | const criticalColorState = useColorConfigState(colorConfigs.critical) 54 | 55 | const previewTheme = createStudioTheme({ 56 | config: { 57 | default: { 58 | ...defaultColorState.state, 59 | title: colorConfigs.default.title, 60 | }, 61 | transparent: { 62 | ...transparentColorState.state, 63 | title: colorConfigs.transparent.title, 64 | }, 65 | primary: { 66 | ...primaryColorState.state, 67 | title: colorConfigs.primary.title, 68 | }, 69 | positive: { 70 | ...positiveColorState.state, 71 | title: colorConfigs.positive.title, 72 | }, 73 | caution: { 74 | ...cautionColorState.state, 75 | title: colorConfigs.caution.title, 76 | }, 77 | critical: { 78 | ...criticalColorState.state, 79 | title: colorConfigs.critical.title, 80 | }, 81 | }, 82 | }) 83 | const splitScreenTheme = useMemo(() => { 84 | const superTheme: Partial = {} 85 | 86 | // Split screen, emulates what happens if they were wrapped in iframes 87 | superTheme.media = defaultTheme.media.map((media) => media / 2) 88 | 89 | return { ...previewTheme, ...superTheme } 90 | }, [previewTheme]) 91 | const [splitScreen, setSplitScreen] = useState(false) 92 | 93 | // TODO generate tones, there are 6 tones, default, transparent, primary, positive, caution, critical 94 | // each tone have a range. 95 | // A tone is generated in Sanity using darkest, mid, lightest, and midPoint 96 | // const studioConfig = useMemo(() => getWordpressConfig({ basePath: '/' }), []) 97 | const studioConfig = themerConfig 98 | 99 | return ( 100 | <> 101 | <> 102 | 103 | 104 | 105 | 106 | setSplitScreen((prev) => !prev)} 110 | /> 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 132 | 133 | 134 | 135 | {preset === 'imagepalette' && ( 136 | 137 | 138 | 139 | 140 | 141 | )} 142 | 143 | 144 | 153 | 162 | 170 | 178 | 186 | 194 | 195 | 196 | 197 | {/* */} 198 | 203 | 204 | 205 | {splitScreen && ( 206 | 207 | 208 | 209 | )} 210 | {/* */} 211 | 212 | {RENDER_INLINE && ( 213 | 214 | 219 | 224 | 225 | 226 | {splitScreen && ( 227 | 232 | 233 | 234 | )} 235 | 236 | 237 | )} 238 | 239 | {RENDER_INLINE && ( 240 |
241 | 244 | 251 | 255 | 260 | 265 | 266 | Launch Studio (inline) 267 | 268 | } 269 | > 270 | 275 | 276 | 277 | 278 | 281 | 288 | 292 | 297 | 302 | 303 | Launch Studio (styled) 304 | 305 | } 306 | > 307 | 312 | 313 | 314 | 315 | 316 | 317 | 320 | 327 | 331 | 336 | 341 | 342 | Launch Studio (split-screen) 343 | 344 | } 345 | > 346 | 350 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 404 | 437 |
438 | )} 439 | 440 | ) 441 | } 442 | 443 | export default function Playground() { 444 | const scheme = useColorScheme() 445 | return ( 446 | 447 | 448 | 449 | ) 450 | } 451 | 452 | export const StyledBottomSHeet = styled(BottomSheet)` 453 | --rsbs-antigap-scale-y: 0; 454 | --rsbs-backdrop-bg: rgba(0, 0, 0, 0.6); 455 | --rsbs-backdrop-opacity: 1; 456 | --rsbs-bg: #fff; 457 | --rsbs-content-opacity: 1; 458 | --rsbs-handle-bg: hsla(0, 0%, 0%, 0.14); 459 | --rsbs-max-w: 780px; 460 | --rsbs-ml: env(safe-area-inset-left); 461 | --rsbs-mr: env(safe-area-inset-right); 462 | --rsbs-overlay-h: 0px; 463 | --rsbs-overlay-rounded: 16px; 464 | --rsbs-overlay-translate-y: 0px; 465 | --rsbs-ml: auto; 466 | --rsbs-mr: auto; 467 | @media screen and (min-width: 600px) { 468 | --rsbs-ml: calc(var(--rsbs-overlay-rounded) * 2); 469 | --rsbs-mr: calc(var(--rsbs-overlay-rounded) * 2); 470 | --rsbs-ml: calc( 471 | var(--rsbs-overlay-rounded) * 2 + env(safe-area-inset-left) 472 | ); 473 | --rsbs-mr: calc( 474 | var(--rsbs-overlay-rounded) * 2 + env(safe-area-inset-right) 475 | ); 476 | 477 | & [data-rsbs-overlay] { 478 | --rsbs-overlay-rounded: 16px; 479 | } 480 | } 481 | 482 | @media (prefers-color-scheme: dark) { 483 | --rsbs-bg: #101112; 484 | 485 | & [data-rsbs-overlay] { 486 | box-shadow: hsl(0deg 0% 100% / 25%) 0 0px 0px 1px !important; 487 | } 488 | } 489 | 490 | & [data-rsbs-overlay] { 491 | border-top-left-radius: calc(var(--rsbs-overlay-rounded) - 3px); 492 | border-top-right-radius: calc(var(--rsbs-overlay-rounded) - 3px); 493 | display: flex; 494 | background: var(--rsbs-bg); 495 | flex-direction: column; 496 | height: var(--rsbs-overlay-h); 497 | transform: translate3d(0, var(--rsbs-overlay-translate-y), 0); 498 | will-change: height; 499 | } 500 | & [data-rsbs-overlay]:focus { 501 | outline: none; 502 | } 503 | & [data-rsbs-is-blocking='false'] [data-rsbs-overlay] { 504 | box-shadow: 0 -5px 60px 0 rgba(38, 89, 115, 0.11), 505 | 0 -1px 0 rgba(38, 89, 115, 0.05); 506 | } 507 | & [data-rsbs-overlay], 508 | &:after { 509 | max-width: var(--rsbs-max-w); 510 | margin-left: var(--rsbs-ml); 511 | margin-right: var(--rsbs-mr); 512 | } 513 | & [data-rsbs-overlay], 514 | & [data-rsbs-backdrop], 515 | &:after { 516 | z-index: 3; 517 | overscroll-behavior: none; 518 | touch-action: none; 519 | position: fixed; 520 | right: 0; 521 | bottom: 0; 522 | left: 0; 523 | user-select: none; 524 | -webkit-tap-highlight-color: transparent; 525 | -webkit-touch-callout: none; 526 | } 527 | & [data-rsbs-backdrop] { 528 | top: -60px; 529 | bottom: -60px; 530 | background-color: var(--rsbs-backdrop-bg); 531 | will-change: opacity; 532 | cursor: pointer; 533 | opacity: var(--rsbs-content-opacity); 534 | 535 | @media (prefers-color-scheme: dark) { 536 | --rsbs-backdrop-bg: hsla(0, 0%, 0%, 0.333); 537 | } 538 | } 539 | & [data-rsbs-is-dismissable='false'] [data-rsbs-backdrop] { 540 | cursor: ns-resize; 541 | } 542 | 543 | &:after { 544 | content: ''; 545 | pointer-events: none; 546 | background: var(--rsbs-bg); 547 | height: 1px; 548 | transform-origin: bottom; 549 | transform: scale3d(1, var(--rsbs-antigap-scale-y), 1); 550 | will-change: transform; 551 | } 552 | & [data-rsbs-footer], 553 | & [data-rsbs-header] { 554 | flex-shrink: 0; 555 | cursor: ns-resize; 556 | padding: 16px; 557 | } 558 | & [data-rsbs-header] { 559 | text-align: center; 560 | user-select: none; 561 | padding-top: 0; 562 | padding-bottom: 0; 563 | /* box-shadow: 0 1px 0 564 | rgba(46, 59, 66, calc(var(--rsbs-content-opacity) * 0.125)); */ 565 | z-index: 1; 566 | /* padding-top: calc(20px + env(safe-area-inset-top)); */ 567 | /* padding-bottom: 8px; */ 568 | } 569 | & [data-rsbs-header]:before { 570 | opacity: var(--rsbs-content-opacity); 571 | position: absolute; 572 | content: ''; 573 | display: block; 574 | width: 36px; 575 | height: 4px; 576 | top: calc( 577 | env(safe-area-inset-top) + 16px - calc(var(--rsbs-overlay-rounded) * 2) 578 | ); 579 | left: 50%; 580 | transform: translateX(-50%); 581 | border-radius: 2px; 582 | background-color: var(--rsbs-handle-bg); 583 | background-color: #a8a8a8; 584 | } 585 | @media (min-resolution: 2dppx) { 586 | & [data-rsbs-header]:before { 587 | transform: translateX(-50%) scaleY(0.75); 588 | } 589 | } 590 | &[data-rsbs-has-header='false'] [data-rsbs-header] { 591 | box-shadow: none; 592 | /* padding-top: calc(12px + env(safe-area-inset-top)); */ 593 | } 594 | & [data-rsbs-scroll] { 595 | flex-shrink: 1; 596 | flex-grow: 1; 597 | -webkit-tap-highlight-color: revert; 598 | -webkit-touch-callout: revert; 599 | -webkit-user-select: auto; 600 | -ms-user-select: auto; 601 | user-select: auto; 602 | overflow: auto; 603 | overscroll-behavior: contain; 604 | -webkit-overflow-scrolling: touch; 605 | } 606 | & [data-rsbs-scroll]:focus { 607 | outline: none; 608 | } 609 | &[data-rsbs-has-footer='false'] [data-rsbs-content] { 610 | padding-bottom: env(safe-area-inset-bottom); 611 | } 612 | & [data-rsbs-content] { 613 | /* The overflow hidden is to ensure any margin on child nodes are included when the resize observer is measuring the height */ 614 | overflow: hidden; 615 | } 616 | & [data-rsbs-footer] { 617 | box-shadow: 0 -1px 0 rgba(46, 59, 66, calc(var(--rsbs-content-opacity) * 618 | 0.125)), 619 | 0 2px 0 var(--rsbs-bg); 620 | overflow: hidden; 621 | z-index: 1; 622 | padding-bottom: calc(16px + env(safe-area-inset-bottom)); 623 | } 624 | 625 | &[data-rsbs-is-dismissable='true'], 626 | &[data-rsbs-is-dismissable='false']:matches([data-rsbs-state='opening'], [data-rsbs-state='closing']) { 627 | & :matches([data-rsbs-header], [data-rsbs-scroll], [data-rsbs-footer]) > * { 628 | opacity: var(--rsbs-content-opacity); 629 | } 630 | & [data-rsbs-backdrop] { 631 | opacity: var(--rsbs-backdrop-opacity); 632 | } 633 | } 634 | 635 | &[data-rsbs-state='closed'], 636 | &[data-rsbs-state='closing'] { 637 | /* Allows interactions on the rest of the page before the close transition is finished */ 638 | pointer-events: none; 639 | } 640 | ` 641 | export function BottomSheetStudio({ 642 | children, 643 | label, 644 | sandbox, 645 | hidden, 646 | }: { 647 | children: React.ReactNode 648 | label: React.ReactNode 649 | sandbox?: boolean 650 | hidden?: boolean 651 | }) { 652 | const [open, setOpen] = useState(false) 653 | const minMaxHeight = 300 654 | const [maxHeight, setMaxHeight] = useState(() => 655 | typeof window === 'undefined' ? minMaxHeight : window.innerHeight 656 | ) 657 | useEffect(() => { 658 | const handleResize = () => 659 | void unstable_batchedUpdates(() => 660 | setMaxHeight(Math.max(minMaxHeight, window.innerHeight)) 661 | ) 662 | window.addEventListener('resize', handleResize) 663 | return () => window.removeEventListener('resize', handleResize) 664 | }, []) 665 | 666 | if (hidden) return null 667 | 668 | return ( 669 | <> 670 | 671 | { 674 | event.preventDefault() 675 | setOpen(true) 676 | }} 677 | > 678 | {label} 679 | 680 | 681 | 687 | lastSnap ?? Math.max(...snapPoints) 688 | } 689 | onDismiss={() => setOpen(false)} 690 | snapPoints={({ maxHeight }) => [maxHeight / 3, maxHeight - 64]} 691 | > 692 | {sandbox ? ( 693 | {children} 694 | ) : ( 695 |
699 | {children} 700 |
701 | )} 702 |
703 | 704 | ) 705 | } 706 | -------------------------------------------------------------------------------- /components/Sandbox.tsx: -------------------------------------------------------------------------------- 1 | // An iframe sandbox, suitable for embedding Studio in smaller sizes than 100vu 2 | 3 | import { useState } from 'react' 4 | import { createPortal } from 'react-dom' 5 | import { StyleSheetManager } from 'styled-components' 6 | 7 | export default function Sandbox({ 8 | children, 9 | src, 10 | ...props 11 | }: { 12 | children?: React.ReactNode 13 | src?: string 14 | }) { 15 | const [contentRef, setContentRef] = useState(null) 16 | const mountNode = contentRef?.contentWindow?.document?.body 17 | const cssNode = contentRef?.contentDocument?.head 18 | 19 | return ( 20 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /components/Singularity.tsx: -------------------------------------------------------------------------------- 1 | // In case an infinite recursing embeddable Studio loop happens it's important ot protect 2 | // against accidental singularities 3 | import { timeline, stagger } from 'motion' 4 | import { Portal } from '@sanity/ui' 5 | import { useEffect, useState } from 'react' 6 | import { unstable_batchedUpdates } from 'react-dom' 7 | import styled from 'styled-components' 8 | 9 | const StyledIframe = styled.iframe` 10 | position: absolute; 11 | height: 200vmin; 12 | width: 200vmin; 13 | top: -50vmin; 14 | opacity: 0.5; 15 | padding-inline: calc(calc(100vmax - 100vmin) / 2); 16 | mix-blend-mode: difference; 17 | left: -50vmin; 18 | transform: scale(0); 19 | transition: ease-out 30s; 20 | transition-property: opacity, transform; 21 | transform-origin: 75% center; 22 | mask-image: radial-gradient(circle at 50% 50%, #000 50%, #0000 60%); 23 | box-sizing: content-box; 24 | border: 0 #0000 solid; 25 | pointer-events: none; 26 | z-index: 1000; 27 | 28 | &[data-open] { 29 | opacity: 0.7; 30 | transform: scale(1); 31 | transform-origin: 50% center; 32 | } 33 | ` 34 | 35 | // const url = new URL('/eh/index.html?oi=1185&bhm=257&sfr=497', 'http://localhost:3000') 36 | // const url = new URL('/eh/index.html?oi=1268&bhm=271&sfr=497&cb=210&dt=419&ce=412&sfy=976&do=255&dd=442&ct=1', 'http://localhost:3000') 37 | const url = new URL( 38 | '/eh/index.html?oi=1416&bhm=259&sfr=1475&cb=210&dt=419&ce=412&sfy=3600&do=255&dd=442&ct=1&sfe=0&sfp=645&hc=1', 39 | 'http://localhost:3000' 40 | ) 41 | 42 | // ix-blend-mode: hard-light; is fun or color-dodge 43 | export default function Singularity(props: { singularity: boolean }) { 44 | const [singularity, setSingularity] = useState(props.singularity) 45 | const [open, setOpen] = useState(false) 46 | useEffect(() => { 47 | unstable_batchedUpdates(() => { 48 | setSingularity(props.singularity) 49 | }) 50 | }, [props.singularity]) 51 | useEffect(() => { 52 | if (typeof document !== 'undefined') { 53 | // @ts-expect-error 54 | window.forceSingularity = () => setSingularity(true) 55 | } 56 | if ('requestIdleCallback' in window) { 57 | const cb = requestIdleCallback(() => setOpen(singularity)) 58 | return () => cancelIdleCallback(cb) 59 | } 60 | const timeout = setTimeout(() => setOpen(singularity), 2000) 61 | return () => clearTimeout(timeout) 62 | }, [singularity]) 63 | useEffect(() => { 64 | if (open) { 65 | document.body.style.background = '#000' 66 | 67 | timeline( 68 | [ 69 | [ 70 | '#user-menu,#global-presence-menu', 71 | { 72 | transform: [ 73 | 'translate3d(-1px, 0, 0)', 74 | ' translate3d(2px, 0, 0)', 75 | 'translate3d(-4px, 0, 0)', 76 | ' translate3d(4px, 0, 0)', 77 | ' translate3d(-5px, 0, 0)', 78 | ' translate3d(6px, 0, 0)', 79 | ' translate3d(7px, 0, 0)', 80 | ' translate3d(6px, 0, 0)', 81 | ' translate3d(-5px, 0, 0)', 82 | ' translate3d(4px, 0, 0)', 83 | 'translate3d(-4px, 0, 0)', 84 | ' translate3d(2px, 0, 0)', 85 | 'translate3d(-1px, 0, 0)', 86 | ], 87 | }, 88 | { 89 | delay: stagger(0.1), 90 | }, 91 | ], 92 | [ 93 | '#user-menu,#global-presence-menu', 94 | { 95 | transform: 'translate3d(-50vw,50vh, 0) scale(0)', 96 | }, 97 | { delay: stagger(0.1) }, 98 | ], 99 | [ 100 | '[data-studio-preview] [data-studio-preview] [data-studio-preview] [data-studio-preview] [data-studio-preview] [data-studio-preview]', 101 | { 102 | opacity: 0, 103 | transform: 'translate3d(-5vw,5vh, 0) scale(0.5) rotate(15deg)', 104 | }, 105 | ], 106 | [ 107 | '[data-studio-preview] [data-studio-preview] [data-studio-preview] [data-studio-preview] [data-studio-preview]', 108 | { 109 | opacity: 0, 110 | transform: 'translate3d(-5vw,5vh, 0) scale(0.5) rotate(15deg)', 111 | }, 112 | ], 113 | [ 114 | '[data-studio-preview] [data-studio-preview] [data-studio-preview] [data-studio-preview]', 115 | { 116 | opacity: 0, 117 | transform: 'translate3d(-5vw,5vh, 0) scale(0.5) rotate(15deg)', 118 | }, 119 | ], 120 | [ 121 | '[data-studio-preview] [data-studio-preview] [data-studio-preview]', 122 | { 123 | opacity: 0, 124 | transform: 'translate3d(-5vw,5vh, 0) scale(0.5) rotate(15deg)', 125 | }, 126 | ], 127 | [ 128 | '[data-studio-preview] [data-studio-preview]', 129 | { 130 | opacity: 0, 131 | transform: 'translate3d(-5vw,5vh, 0) scale(0.5) rotate(15deg)', 132 | }, 133 | ], 134 | [ 135 | '[data-studio-preview]', 136 | { 137 | opacity: 0, 138 | transform: 'translate3d(-5vw,5vh, 0) scale(0.5) rotate(15deg)', 139 | }, 140 | ], 141 | [ 142 | '[data-studio-canvas]', 143 | { 144 | opacity: 0, 145 | transform: 'translate3d(-5vw,5vh, 0) scale(0.5) rotate(15deg)', 146 | }, 147 | ], 148 | ], 149 | { delay: 10, defaultOptions: { easing: 'ease-in-out' } } 150 | ) 151 | } 152 | }) 153 | 154 | if (!singularity) return null 155 | 156 | return ( 157 | 158 | 164 | 165 | ) 166 | } 167 | -------------------------------------------------------------------------------- /components/StudioPage.tsx: -------------------------------------------------------------------------------- 1 | import favicon from 'public/sanity.svg' 2 | import Head from 'next/head' 3 | 4 | export default function SanityPage({ 5 | children, 6 | }: { 7 | children: React.ReactNode 8 | }) { 9 | return ( 10 | <> 11 | 12 | 13 | 18 | 22 | 23 | Sanity Studio 24 | 25 | 26 |
30 | {children} 31 |
32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /components/StudioPageLoader.tsx: -------------------------------------------------------------------------------- 1 | //import SanityCanvas from 'components/StudioCanvas' 2 | import StudioPage from 'components/StudioPage' 3 | 4 | // Takes care of rendering an entire Sanity Studio page, this method have the least amount of overrides 5 | // such as unstable_history and have the least probability of breaking. 6 | 7 | export default function StudioPageLoader() { 8 | return ( 9 | <> 10 | {/* */} 11 | TODO 12 | {/* */} 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /components/SuperLivePreview.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { StudioProvider, defaultTheme, StudioLayout } from 'sanity' 3 | import config from 'sanity.config' 4 | import { ThemeProvider, Button } from '@sanity/ui' 5 | import { useMagicRouter } from 'hooks' 6 | import { EditIcon } from '@sanity/icons' 7 | import { useState } from 'react' 8 | import { StyledBottomSHeet } from './Playground' 9 | 10 | // If preview is on it also means we're authenticated, thanks to next-sanity checking that before attempting to stream groq 11 | export function EditPost({ preview, _id }: { preview: boolean; _id: string }) { 12 | // Getting the entire Studio context provider is overkill, but is fast 13 | 14 | return ( 15 | 16 | {!preview && } 17 | {preview && } 18 | 19 | ) 20 | } 21 | 22 | function EditPostLink({ _id }: { _id: string }) { 23 | return ( 24 | 25 | 28 | 29 | ) 30 | } 31 | 32 | let lastOpen = false 33 | function EditPostButton({ _id }: { _id: string }) { 34 | const history = useMagicRouter(`/manage/blog/desk/post;${_id}/posts/`) 35 | const [open, setOpen] = useState(false) 36 | 37 | return ( 38 | <> 39 | 51 | { 57 | setOpen(false) 58 | lastOpen = false 59 | }} 60 | defaultSnap={({ snapPoints, lastSnap }) => 61 | lastSnap ?? Math.min(...snapPoints) 62 | } 63 | snapPoints={({ maxHeight }) => [maxHeight / 3, maxHeight - 64]} 64 | > 65 | 71 |
75 | 76 |
77 |
78 |
79 | 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /components/ThemePreview.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Stack, Text, Avatar, Inline, Flex, Button } from '@sanity/ui' 2 | import { AddIcon, PublishIcon, EditIcon } from '@sanity/icons' 3 | 4 | export default function ThemePreview() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | Text in a card with link 12 | 13 | 14 | 15 | 16 | 17 | Text in a card with link 18 | 19 | 20 | 21 | 22 | 23 | Text in a card with link 24 | 25 | 26 | 27 | 28 | 29 | Text in a card with link 30 | 31 | 32 | 33 | 34 | 35 | Text in a card with link 36 | 37 | 38 | 39 | 40 | 41 | 42 |