-
-
+
+
+
+
+
+ {post.author && (
+
+ )}
+
+
-
- {post.content?.length && post.content.length > 0 && (
-
- )}
+ {post.content?.length && post.content.length > 0 && (
+
+ )}
+
)
}}
@@ -121,9 +127,7 @@ export default async function PostPage({params}: {params: Promise<{slug: string}
Recent Stories
-
-
-
+
>
) : (
diff --git a/apps/live-next/package.json b/apps/live-next/package.json
index 5343e3a56..edc32d41c 100644
--- a/apps/live-next/package.json
+++ b/apps/live-next/package.json
@@ -31,6 +31,7 @@
"date-fns": "^4.1.0",
"framer-motion": "12.0.0-alpha.1",
"next": "15.0.0-canary.202",
+ "next-live-transitions": "workspace:*",
"next-sanity": "9.7.0-canary.23",
"postcss": "^8.4.47",
"react": "19.0.0-rc-fb9a90fa48-20240614",
diff --git a/apps/remix/app/PortableText.tsx b/apps/remix/app/PortableText.tsx
new file mode 100644
index 000000000..72d492adc
--- /dev/null
+++ b/apps/remix/app/PortableText.tsx
@@ -0,0 +1,83 @@
+/**
+ * This component uses Portable Text to render a post body.
+ *
+ * You can learn more about Portable Text on:
+ * https://www.sanity.io/docs/block-content
+ * https://github.com/portabletext/react-portabletext
+ * https://portabletext.org/
+ *
+ */
+import {
+ PortableText,
+ type PortableTextBlock,
+ type PortableTextComponents,
+} from '@portabletext/react'
+import type {CSSProperties} from 'react'
+
+function getViewTransitionName(value: string | undefined) {
+ return value ? `pt-${value}` : undefined
+}
+
+function style(value: string | undefined): CSSProperties {
+ return {
+ viewTransitionName: getViewTransitionName(value),
+ }
+}
+
+export default function CustomPortableText({
+ className,
+ value,
+}: {
+ className?: string
+ value: PortableTextBlock[]
+}) {
+ const components: PortableTextComponents = {
+ block: {
+ normal: ({children, value}) =>
{children}
,
+ blockquote: ({children, value}) => (
+
{children}
+ ),
+ h1: ({children, value}) =>
{children}
,
+ h2: ({children, value}) =>
{children}
,
+ h3: ({children, value}) =>
{children}
,
+ h4: ({children, value}) =>
{children}
,
+ h5: ({children, value}) => (
+
+ {children}
+
+ ),
+ h6: ({children, value}) => (
+
+ {children}
+
+ ),
+ },
+ list: {
+ number: ({children, value}) =>
{children}
,
+ bullet: ({children, value}) =>
,
+ },
+ listItem: ({children, value}) => (
+
+ {children}
+
+ ),
+ // @TODO use a client component and `useId()` to generate a unique view transition name
+ // hardBreak: () =>
,
+
+ // unknownType: DefaultUnknownType,
+ // unknownMark: DefaultUnknownMark,
+ unknownList: ({children, value}) =>
,
+ unknownListItem: ({children, value}) => (
+
+ {children}
+
+ ),
+ unknownBlockStyle: ({children, value}) =>
{children}
,
+ }
+
+ return (
+
+ )
+}
diff --git a/apps/remix/package.json b/apps/remix/package.json
index e4aa6a5fa..c69d0d1a2 100644
--- a/apps/remix/package.json
+++ b/apps/remix/package.json
@@ -19,11 +19,14 @@
"@sanity/image-url": "^1.0.2",
"@sanity/react-loader": "workspace:*",
"@sanity/visual-editing": "workspace:*",
+ "@tailwindcss/typography": "^0.5.15",
"@vercel/remix": "^2.12.0",
"@vercel/stega": "0.1.2",
"isbot": "^5.1.17",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-fast-compare": "^3.2.2",
+ "react-live-transitions": "workspace:*",
"source-map-support": "^0.5.21"
},
"devDependencies": {
diff --git a/apps/remix/tailwind.config.ts b/apps/remix/tailwind.config.ts
index 0d0116f98..3972cae2f 100644
--- a/apps/remix/tailwind.config.ts
+++ b/apps/remix/tailwind.config.ts
@@ -1,3 +1,4 @@
+import typography from '@tailwindcss/typography'
import type {Config} from 'tailwindcss'
export default {
@@ -5,5 +6,5 @@ export default {
theme: {
extend: {},
},
- plugins: [],
+ plugins: [typography],
} satisfies Config
diff --git a/package.config.ts b/package.config.ts
deleted file mode 100644
index 30b466ef1..000000000
--- a/package.config.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import {defineConfig} from '@sanity/pkg-utils'
-
-export default defineConfig({
- minify: !!process.env.GITHUB_ACTIONS,
- extract: {
- bundledPackages: ['@repo/visual-editing-helpers'],
- rules: {
- 'ae-forgotten-export': 'error',
- 'ae-incompatible-release-tags': 'warn',
- 'ae-internal-missing-underscore': 'off',
- 'ae-missing-release-tag': 'warn',
- },
- },
- tsconfig: 'tsconfig.build.json',
-})
diff --git a/packages/@repo/package.config/src/package.config.ts b/packages/@repo/package.config/src/package.config.ts
index 30b466ef1..e945d59fc 100644
--- a/packages/@repo/package.config/src/package.config.ts
+++ b/packages/@repo/package.config/src/package.config.ts
@@ -1,7 +1,6 @@
import {defineConfig} from '@sanity/pkg-utils'
export default defineConfig({
- minify: !!process.env.GITHUB_ACTIONS,
extract: {
bundledPackages: ['@repo/visual-editing-helpers'],
rules: {
diff --git a/packages/insert-menu/package.json b/packages/insert-menu/package.json
index b798841cb..65ad2f11d 100644
--- a/packages/insert-menu/package.json
+++ b/packages/insert-menu/package.json
@@ -1,6 +1,6 @@
{
"name": "@sanity/insert-menu",
- "version": "1.0.10-canary.74",
+ "version": "1.0.10-canary.81",
"description": "",
"keywords": [],
"homepage": "https://github.com/sanity-io/visual-editing/tree/main/packages/insert-menu#readme",
diff --git a/packages/next-live-transitions/.eslintignore b/packages/next-live-transitions/.eslintignore
new file mode 100644
index 000000000..53c37a166
--- /dev/null
+++ b/packages/next-live-transitions/.eslintignore
@@ -0,0 +1 @@
+dist
\ No newline at end of file
diff --git a/packages/next-live-transitions/README.md b/packages/next-live-transitions/README.md
new file mode 100644
index 000000000..a4c37044a
--- /dev/null
+++ b/packages/next-live-transitions/README.md
@@ -0,0 +1,13 @@
+# next-live-transitions
+
+[![npm stat](https://img.shields.io/npm/dm/next-live-transitions.svg?style=flat-square)](https://npm-stat.com/charts.html?package=next-live-transitions)
+[![npm version](https://img.shields.io/npm/v/next-live-transitions.svg?style=flat-square)](https://www.npmjs.com/package/next-live-transitions)
+[![gzip size][gzip-badge]][bundlephobia]
+[![size][size-badge]][bundlephobia]
+
+@TODO
+
+[`next-sanity`]: https://github.com/sanity-io/next-sanity
+[gzip-badge]: https://img.shields.io/bundlephobia/minzip/next-live-transitions?label=gzip%20size&style=flat-square
+[size-badge]: https://img.shields.io/bundlephobia/min/next-live-transitions?label=size&style=flat-square
+[bundlephobia]: https://bundlephobia.com/package/next-live-transitions
diff --git a/packages/next-live-transitions/package.config.ts b/packages/next-live-transitions/package.config.ts
new file mode 100644
index 000000000..eeef179e2
--- /dev/null
+++ b/packages/next-live-transitions/package.config.ts
@@ -0,0 +1,41 @@
+import path from 'node:path'
+import baseConfig from '@repo/package.config'
+import {defineConfig} from '@sanity/pkg-utils'
+
+const MODULE_PATHS_WHICH_USE_CLIENT_DIRECTIVE_SHOULD_BE_ADDED = [path.join('src', 'index.ts')]
+
+// const MODULE_PATHS_WHICH_USE_SERVER_DIRECTIVE_SHOULD_BE_ADDED = []
+
+export default defineConfig({
+ ...baseConfig,
+ minify: false,
+ rollup: {
+ output: {
+ banner: (chunkInfo) => {
+ if (
+ MODULE_PATHS_WHICH_USE_CLIENT_DIRECTIVE_SHOULD_BE_ADDED.find((modulePath) =>
+ chunkInfo.facadeModuleId?.endsWith(modulePath),
+ )
+ ) {
+ return `"use client"`
+ }
+ // if (
+ // MODULE_PATHS_WHICH_USE_SERVER_DIRECTIVE_SHOULD_BE_ADDED.find((modulePath) =>
+ // chunkInfo.facadeModuleId?.endsWith(modulePath),
+ // )
+ // ) {
+ // return `"use server"`
+ // }
+ return ''
+ },
+ },
+ },
+ extract: {
+ rules: {
+ 'ae-forgotten-export': 'error',
+ 'ae-incompatible-release-tags': 'warn',
+ 'ae-internal-missing-underscore': 'off',
+ 'ae-missing-release-tag': 'warn',
+ },
+ },
+})
diff --git a/packages/next-live-transitions/package.json b/packages/next-live-transitions/package.json
new file mode 100644
index 000000000..e9cfa8c83
--- /dev/null
+++ b/packages/next-live-transitions/package.json
@@ -0,0 +1,74 @@
+{
+ "name": "next-live-transitions",
+ "version": "0.0.1-canary.1",
+ "homepage": "https://github.com/sanity-io/visual-editing/tree/main/packages/next-live-transitions#readme",
+ "bugs": {
+ "url": "https://github.com/sanity-io/visual-editing/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+ssh://git@github.com/sanity-io/visual-editing.git",
+ "directory": "packages/next-live-transitions"
+ },
+ "license": "MIT",
+ "author": "Sanity.io
",
+ "sideEffects": false,
+ "type": "module",
+ "exports": {
+ ".": {
+ "source": "./src/index.ts",
+ "import": "./dist/index.js",
+ "require": "./dist/index.cjs",
+ "default": "./dist/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "main": "./dist/index.cjs",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "files": [
+ "dist",
+ "src",
+ "CHANGELOG.md"
+ ],
+ "scripts": {
+ "build": "pkg build --strict --check --clean",
+ "dev": "pkg build --strict",
+ "lint": "eslint --cache .",
+ "prepack": "turbo run build",
+ "test": "vitest --pass-with-no-tests --typecheck"
+ },
+ "browserslist": "extends @sanity/browserslist-config",
+ "prettier": "@repo/prettier-config",
+ "eslintConfig": {
+ "extends": [
+ "@repo/eslint-config"
+ ],
+ "root": true
+ },
+ "dependencies": {
+ "react-live-transitions": "workspace:*"
+ },
+ "devDependencies": {
+ "@repo/eslint-config": "workspace:*",
+ "@repo/package.config": "workspace:*",
+ "@repo/prettier-config": "workspace:*",
+ "@sanity/pkg-utils": "6.11.4",
+ "@types/react": "^18.3.11",
+ "eslint": "^8.57.1",
+ "next": "^14.2.15",
+ "react": "^18.3.1",
+ "typescript": "5.6.3",
+ "vitest": "^2.1.3"
+ },
+ "peerDependencies": {
+ "next": "^14.1 || ^15.0.0-0",
+ "react": "^18.3 || ^19.0.0-0"
+ },
+ "engines": {
+ "node": ">=18.18"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/next-live-transitions/src/index.ts b/packages/next-live-transitions/src/index.ts
new file mode 100644
index 000000000..476eaa1b5
--- /dev/null
+++ b/packages/next-live-transitions/src/index.ts
@@ -0,0 +1 @@
+export {TransitionLayoutShift} from 'react-live-transitions'
diff --git a/packages/next-live-transitions/tsconfig.base.json b/packages/next-live-transitions/tsconfig.base.json
new file mode 100644
index 000000000..268d2a4c7
--- /dev/null
+++ b/packages/next-live-transitions/tsconfig.base.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@sanity/pkg-utils/tsconfig/strictest.json",
+ "compilerOptions": {
+ "paths": {
+ "next-live-transitions": ["./src/index"]
+ },
+ "rootDir": ".",
+ "outDir": "dist",
+ "noUnusedLocals": false,
+ "noUnusedParameters": false
+ }
+}
diff --git a/packages/next-live-transitions/tsconfig.build.json b/packages/next-live-transitions/tsconfig.build.json
new file mode 100644
index 000000000..627936209
--- /dev/null
+++ b/packages/next-live-transitions/tsconfig.build.json
@@ -0,0 +1,8 @@
+{
+ "extends": "./tsconfig.base",
+ "compilerOptions": {
+ "rootDir": "src"
+ },
+ "include": ["src/**/*.ts", "src/**/*.tsx"],
+ "exclude": ["dist", "node_modules"]
+}
diff --git a/packages/next-live-transitions/tsconfig.json b/packages/next-live-transitions/tsconfig.json
new file mode 100644
index 000000000..9426a7d6c
--- /dev/null
+++ b/packages/next-live-transitions/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "./tsconfig.base",
+ "include": [
+ "**/*.ts",
+ "**/*.tsx"
+ ],
+ "exclude": ["dist", "node_modules"]
+}
diff --git a/packages/next-live-transitions/turbo.json b/packages/next-live-transitions/turbo.json
new file mode 100644
index 000000000..ce1cc5c6e
--- /dev/null
+++ b/packages/next-live-transitions/turbo.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://turbo.build/schema.json",
+ "extends": ["//"],
+ "tasks": {
+ "build": {
+ "env": ["NODE_ENV"]
+ },
+ "dev": {
+ "env": ["NODE_ENV"],
+ "persistent": false
+ }
+ }
+}
diff --git a/packages/next-live-transitions/vitest.config.ts b/packages/next-live-transitions/vitest.config.ts
new file mode 100644
index 000000000..b94ac31fc
--- /dev/null
+++ b/packages/next-live-transitions/vitest.config.ts
@@ -0,0 +1,9 @@
+import {defineConfig} from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ typecheck: {
+ tsconfig: 'tsconfig.build.json',
+ },
+ },
+})
diff --git a/packages/next-loader/package.config.ts b/packages/next-loader/package.config.ts
index bd2b17aa3..d8f3bbd73 100644
--- a/packages/next-loader/package.config.ts
+++ b/packages/next-loader/package.config.ts
@@ -14,7 +14,6 @@ const MODULE_PATHS_WHICH_USE_SERVER_DIRECTIVE_SHOULD_BE_ADDED = [
export default defineConfig({
...baseConfig,
- minify: false,
bundles: [
{
source: './src/index.ts',
diff --git a/packages/presentation/package.config.ts b/packages/presentation/package.config.ts
index 757bfb2d4..8454e08c2 100644
--- a/packages/presentation/package.config.ts
+++ b/packages/presentation/package.config.ts
@@ -3,7 +3,6 @@ import {defineConfig} from '@sanity/pkg-utils'
export default defineConfig({
...baseConfig,
- minify: false,
external: ['@sanity/ui', 'react', 'react-dom', 'sanity', 'styled-components'],
define: {
PRESENTATION_ENABLE_LIVE_DRAFT_EVENTS: process.env['PRESENTATION_ENABLE_LIVE_DRAFT_EVENTS'],
diff --git a/packages/react-live-transitions/.eslintignore b/packages/react-live-transitions/.eslintignore
new file mode 100644
index 000000000..b448b494b
--- /dev/null
+++ b/packages/react-live-transitions/.eslintignore
@@ -0,0 +1,2 @@
+dist
+storybook-static
diff --git a/packages/react-live-transitions/.gitignore b/packages/react-live-transitions/.gitignore
new file mode 100644
index 000000000..e1b64d590
--- /dev/null
+++ b/packages/react-live-transitions/.gitignore
@@ -0,0 +1,2 @@
+*storybook.log
+storybook-static
diff --git a/packages/react-live-transitions/.storybook/main.ts b/packages/react-live-transitions/.storybook/main.ts
new file mode 100644
index 000000000..2c515c098
--- /dev/null
+++ b/packages/react-live-transitions/.storybook/main.ts
@@ -0,0 +1,27 @@
+import {dirname, join} from 'path'
+import type {StorybookConfig} from '@storybook/react-vite'
+
+/**
+ * This function is used to resolve the absolute path of a package.
+ * It is needed in projects that use Yarn PnP or are set up within a monorepo.
+ */
+function getAbsolutePath(value: string): any {
+ return dirname(require.resolve(join(value, 'package.json')))
+}
+const config: StorybookConfig = {
+ stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
+ addons: [
+ getAbsolutePath('@storybook/addon-links'),
+ getAbsolutePath('@storybook/addon-essentials'),
+ getAbsolutePath('@storybook/addon-interactions'),
+ ],
+ framework: {
+ name: getAbsolutePath('@storybook/react-vite'),
+ options: {},
+ },
+ typescript: {
+ // Not needed for this package, as we don't export internal components
+ reactDocgen: false,
+ },
+}
+export default config
diff --git a/packages/react-live-transitions/.storybook/preview.ts b/packages/react-live-transitions/.storybook/preview.ts
new file mode 100644
index 000000000..30faaf724
--- /dev/null
+++ b/packages/react-live-transitions/.storybook/preview.ts
@@ -0,0 +1,15 @@
+import './style.css'
+import type {Preview} from '@storybook/react'
+
+const preview: Preview = {
+ parameters: {
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/i,
+ },
+ },
+ },
+}
+
+export default preview
diff --git a/packages/react-live-transitions/.storybook/style.css b/packages/react-live-transitions/.storybook/style.css
new file mode 100644
index 000000000..33db16920
--- /dev/null
+++ b/packages/react-live-transitions/.storybook/style.css
@@ -0,0 +1,49 @@
+.cards {
+ padding: 0;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ width: 100%;
+ gap: 2rem;
+ max-width: 1000px;
+}
+
+.card {
+ width: 100%;
+ aspect-ratio: 2/3;
+ display: block;
+ position: relative;
+ border-radius: 1rem;
+ max-width: 220px;
+}
+
+.delete-btn {
+ cursor: pointer;
+ position: absolute;
+ bottom: -0.75rem;
+ right: -0.75rem;
+ width: 3rem;
+ height: 3rem;
+ padding: 0.5rem;
+ border: 4px solid;
+ border-radius: 100%;
+ background: steelblue;
+ color: white;
+
+ & img {
+ filter: invert();
+ }
+}
+
+.sr-only {
+ border: 0;
+ clip: rect(1px, 1px, 1px, 1px);
+ clip-path: inset(50%);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+ white-space: nowrap;
+}
diff --git a/packages/react-live-transitions/README.md b/packages/react-live-transitions/README.md
new file mode 100644
index 000000000..6e0b6b79d
--- /dev/null
+++ b/packages/react-live-transitions/README.md
@@ -0,0 +1,10 @@
+# react-live-transitions
+
+[![npm stat](https://img.shields.io/npm/dm/react-live-transitions.svg?style=flat-square)](https://npm-stat.com/charts.html?package=react-live-transitions)
+[![npm version](https://img.shields.io/npm/v/react-live-transitions.svg?style=flat-square)](https://www.npmjs.com/package/react-live-transitions)
+[![gzip size][gzip-badge]][bundlephobia]
+[![size][size-badge]][bundlephobia]
+
+[gzip-badge]: https://img.shields.io/bundlephobia/minzip/react-live-transitions?label=gzip%20size&style=flat-square
+[size-badge]: https://img.shields.io/bundlephobia/min/react-live-transitions?label=size&style=flat-square
+[bundlephobia]: https://bundlephobia.com/package/react-live-transitions
diff --git a/packages/react-live-transitions/package.config.ts b/packages/react-live-transitions/package.config.ts
new file mode 100644
index 000000000..ebbc2cc8d
--- /dev/null
+++ b/packages/react-live-transitions/package.config.ts
@@ -0,0 +1,7 @@
+import baseConfig from '@repo/package.config'
+import {defineConfig} from '@sanity/pkg-utils'
+
+export default defineConfig({
+ ...baseConfig,
+ minify: false,
+})
diff --git a/packages/react-live-transitions/package.json b/packages/react-live-transitions/package.json
new file mode 100644
index 000000000..b086f30f7
--- /dev/null
+++ b/packages/react-live-transitions/package.json
@@ -0,0 +1,83 @@
+{
+ "name": "react-live-transitions",
+ "version": "0.0.1-canary.1",
+ "homepage": "https://github.com/sanity-io/visual-editing/tree/main/packages/react-live-transitions#readme",
+ "bugs": {
+ "url": "https://github.com/sanity-io/visual-editing/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+ssh://git@github.com/sanity-io/visual-editing.git",
+ "directory": "packages/react-live-transitions"
+ },
+ "license": "MIT",
+ "author": "Sanity.io ",
+ "sideEffects": false,
+ "type": "module",
+ "exports": {
+ ".": {
+ "source": "./src/index.ts",
+ "require": "./dist/index.cjs",
+ "import": "./dist/index.js",
+ "default": "./dist/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "main": "./dist/index.cjs",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "files": [
+ "dist",
+ "src",
+ "!src/**/stories/"
+ ],
+ "scripts": {
+ "build": "pkg build --strict --check --clean",
+ "build-storybook": "storybook build",
+ "dev": "pkg build --strict",
+ "lint": "eslint --cache .",
+ "prepack": "turbo run build",
+ "storybook": "storybook dev -p 6006",
+ "test": "vitest --pass-with-no-tests --typecheck"
+ },
+ "browserslist": "extends @sanity/browserslist-config",
+ "prettier": "@repo/prettier-config",
+ "eslintConfig": {
+ "extends": [
+ "@repo/eslint-config",
+ "plugin:storybook/recommended"
+ ],
+ "root": true
+ },
+ "devDependencies": {
+ "@repo/eslint-config": "workspace:*",
+ "@repo/package.config": "workspace:*",
+ "@repo/prettier-config": "workspace:*",
+ "@sanity/pkg-utils": "6.11.4",
+ "@storybook/addon-essentials": "^8.3.5",
+ "@storybook/addon-interactions": "^8.3.5",
+ "@storybook/addon-links": "^8.3.5",
+ "@storybook/react": "^8.3.5",
+ "@storybook/react-vite": "^8.3.5",
+ "@storybook/test": "^8.3.5",
+ "@types/react": "^18.3.11",
+ "eslint": "^8.57.1",
+ "eslint-plugin-storybook": "^0.9.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-fast-compare": "^3.2.2",
+ "storybook": "^8.3.5",
+ "typescript": "5.6.3",
+ "vitest": "^2.1.3"
+ },
+ "peerDependencies": {
+ "react": "^18.3 || >=19.0.0-rc",
+ "react-dom": "^18.3 || >=19.0.0-rc"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/react-live-transitions/src/DebugLayoutShift.tsx b/packages/react-live-transitions/src/DebugLayoutShift.tsx
new file mode 100644
index 000000000..b1783a14d
--- /dev/null
+++ b/packages/react-live-transitions/src/DebugLayoutShift.tsx
@@ -0,0 +1,26 @@
+import type React from 'react'
+
+export function DebugLayoutShift(): React.JSX.Element {
+ return (
+
+ )
+}
diff --git a/packages/react-live-transitions/src/TransitionLayoutShift.tsx b/packages/react-live-transitions/src/TransitionLayoutShift.tsx
new file mode 100644
index 000000000..1538a5fb0
--- /dev/null
+++ b/packages/react-live-transitions/src/TransitionLayoutShift.tsx
@@ -0,0 +1,87 @@
+import {useEffect, useState, useSyncExternalStore} from 'react'
+import {flushSync} from 'react-dom'
+
+const noop = () => () => {}
+
+/**
+ * @public
+ */
+export function TransitionLayoutShift(props: {children: React.ReactNode}): React.JSX.Element {
+ const isViewTransitionsSupported = useSyncExternalStore(
+ noop,
+ () => 'startViewTransition' in document,
+ () => true,
+ )
+ const [children, setChildren] = useState(() => props.children)
+ // const [transition, setTransition] = useState(undefined)
+
+ // const transitionRef = useRef(undefined)
+ // const trailingRef = useRef(false)
+ // const willTransitionRef = useRef(false)
+ // const transitionInProgress = Boolean(transition)
+ useEffect(() => {
+ if (!isViewTransitionsSupported || props.children === children) {
+ return
+ }
+ // const controller = new AbortController()
+ // const {signal} = controller
+
+ // Check if using type is supported
+ const isTypeSupported = CSS.supports('selector(html:active-view-transition-type(live))')
+ const update = () => {
+ flushSync(() => {
+ setChildren(() => props.children)
+ })
+ }
+
+ document.startViewTransition(
+ isTypeSupported
+ ? {
+ // @ts-expect-error - this is fine, TSC types lag behind
+ update,
+ types: ['live'],
+ }
+ : update,
+ )
+ // startTransition(() => setTransition(transition))
+ // transition.ready.then(() => {
+ // console.log('transition: ready, can run animation logic', {aborted: signal.aborted})
+ // })
+ // transition.updateCallbackDone.then(() => {
+ // console.log('transition: updateCallbackDone, dom is updated, call setTransition?', {
+ // aborted: signal.aborted,
+ // })
+ // // if (signal.aborted) return
+ // // startTransition(() => setTransition(transition))
+ // setTransition(transition)
+ // })
+ // transition.finished.then(() => {
+ // console.log('transition: finished, dom is updated, call setTransition?', {
+ // aborted: signal.aborted,
+ // })
+ // // if (signal.aborted) return
+ // // startTransition(() => setTransition(undefined))
+ // // transitionRef.current = undefined
+ // })
+
+ // return () => {
+ // timeoutRef.current = setTimeout(() => transition.skipTransition(), 100)
+ // }
+ return () => {
+ // controller.abort()
+ // transitionRef.current = undefined
+ }
+ }, [children, isViewTransitionsSupported, props.children])
+
+ // if (transition) {
+ // console.log('transition: delaying render until finished', transition)
+ // // Delay render until the transition is finished
+ // use(transition.finished)
+ // }
+
+ if (!isViewTransitionsSupported) {
+ return <>{props.children}>
+ }
+
+ return <>{children}>
+}
diff --git a/packages/react-live-transitions/src/index.ts b/packages/react-live-transitions/src/index.ts
new file mode 100644
index 000000000..e9060b5cf
--- /dev/null
+++ b/packages/react-live-transitions/src/index.ts
@@ -0,0 +1,2 @@
+export * from './TransitionLayoutShift'
+export * from './DebugLayoutShift'
diff --git a/packages/react-live-transitions/src/stories/Cards.stories.tsx b/packages/react-live-transitions/src/stories/Cards.stories.tsx
new file mode 100644
index 000000000..2f7d16fbf
--- /dev/null
+++ b/packages/react-live-transitions/src/stories/Cards.stories.tsx
@@ -0,0 +1,168 @@
+import type {Meta, StoryObj} from '@storybook/react'
+import {memo, useCallback, useMemo, useState} from 'react'
+import isEqual from 'react-fast-compare'
+import {DebugLayoutShift} from '../DebugLayoutShift'
+import {TransitionLayoutShift} from '../TransitionLayoutShift'
+
+const TransitionLayoutShiftMemo = memo(TransitionLayoutShift, isEqual)
+
+function TransitionLayoutShiftOptimized(
+ props: React.ComponentProps & {
+ cards: number
+ deletedCards: Set
+ },
+) {
+ return {props.children}
+}
+const TransitionLayoutShiftMemoOptimized = memo(
+ TransitionLayoutShiftOptimized,
+ (prevProps, nextProps) =>
+ isEqual(
+ {cards: prevProps.cards, deletedCards: prevProps.deletedCards},
+ {cards: nextProps.cards, deletedCards: nextProps.deletedCards},
+ ),
+)
+
+function Template(props: {cards: number; count: number; debug: boolean}) {
+ const {cards: _cards, count, debug} = props
+ const [deletedCards, setDeletedCards] = useState(() => new Set())
+
+ const cards = useMemo(() => {
+ return Array.from({length: _cards}, (_, index) => ({
+ id: `${index + 1}`,
+ color: index % 0 ? 'tan' : index % 1 ? 'khaki' : index % 2 ? 'thistle' : 'wheat',
+ }))
+ }, [_cards])
+
+ const handleDeleteCard = useCallback((id: string) => {
+ setDeletedCards((deletedCards) => {
+ if (!deletedCards.has(id)) {
+ const nextCards = new Set(deletedCards)
+ nextCards.add(id)
+ return nextCards
+ }
+
+ return deletedCards
+ })
+ }, [])
+
+ const ul = useMemo(
+ () => (
+
+ {cards.map(
+ ({id, color}) =>
+ !deletedCards.has(id) && (
+ -
+
+
+ ),
+ )}
+
+ ),
+ [cards, deletedCards, handleDeleteCard],
+ )
+ const children = useMemo(
+ () => (
+
+ ),
+ [ul],
+ )
+
+ return (
+ <>
+ {debug && }
+ {/* {children} */}
+ {/*
+ {
+
+ }
+ */}
+
+
+
+ {cards.map(
+ ({id, color}) =>
+ !deletedCards.has(id) && (
+ -
+
+
+ ),
+ )}
+
+
+
+ {/* count: {count}
*/}
+ >
+ )
+}
+
+const meta = {
+ title: 'Cards',
+ component: Template,
+ args: {
+ debug: true,
+ cards: 4,
+ count: 0,
+ },
+ parameters: {
+ layout: 'fullscreen',
+ },
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const TransitionLayoutShiftExample: Story = {}
diff --git a/packages/react-live-transitions/tsconfig.base.json b/packages/react-live-transitions/tsconfig.base.json
new file mode 100644
index 000000000..2967d47ce
--- /dev/null
+++ b/packages/react-live-transitions/tsconfig.base.json
@@ -0,0 +1,9 @@
+{
+ "extends": "@sanity/pkg-utils/tsconfig/strictest.json",
+ "compilerOptions": {
+ "rootDir": ".",
+ "outDir": "dist",
+ "noUnusedLocals": false,
+ "noUnusedParameters": false
+ }
+}
diff --git a/packages/react-live-transitions/tsconfig.build.json b/packages/react-live-transitions/tsconfig.build.json
new file mode 100644
index 000000000..627936209
--- /dev/null
+++ b/packages/react-live-transitions/tsconfig.build.json
@@ -0,0 +1,8 @@
+{
+ "extends": "./tsconfig.base",
+ "compilerOptions": {
+ "rootDir": "src"
+ },
+ "include": ["src/**/*.ts", "src/**/*.tsx"],
+ "exclude": ["dist", "node_modules"]
+}
diff --git a/packages/react-live-transitions/tsconfig.json b/packages/react-live-transitions/tsconfig.json
new file mode 100644
index 000000000..48918095b
--- /dev/null
+++ b/packages/react-live-transitions/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.base",
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ ],
+ "exclude": ["dist", "node_modules"],
+ "compilerOptions": {
+ "jsx": "react-jsx"
+ }
+}
diff --git a/packages/react-live-transitions/turbo.json b/packages/react-live-transitions/turbo.json
new file mode 100644
index 000000000..12fe39b73
--- /dev/null
+++ b/packages/react-live-transitions/turbo.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://turbo.build/schema.json",
+ "extends": ["//"],
+ "tasks": {
+ "build-storybook": {
+ "outputs": ["storybook-static/**"]
+ },
+ "storybook": {
+ "persistent": false
+ },
+ "dev": {
+ "persistent": false
+ }
+ }
+}
diff --git a/packages/react-live-transitions/vitest.config.ts b/packages/react-live-transitions/vitest.config.ts
new file mode 100644
index 000000000..b94ac31fc
--- /dev/null
+++ b/packages/react-live-transitions/vitest.config.ts
@@ -0,0 +1,9 @@
+import {defineConfig} from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ typecheck: {
+ tsconfig: 'tsconfig.build.json',
+ },
+ },
+})
diff --git a/packages/visual-editing-helpers/src/comlinkCompatibility.ts b/packages/visual-editing-helpers/src/comlinkCompatibility.ts
index 22f5c3dcb..834b36eea 100644
--- a/packages/visual-editing-helpers/src/comlinkCompatibility.ts
+++ b/packages/visual-editing-helpers/src/comlinkCompatibility.ts
@@ -115,6 +115,10 @@ const convertToChannelsMessage = (message: ProtocolMessage): ProtocolMessage =>
message.type = comlinkToChannelsMap[message.type as ComlinkMessageType] ?? message.type
+ if (message.type === 'channel/response' && message.responseTo && !message.data) {
+ message.data = {responseTo: message.responseTo}
+ }
+
return message
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fa5f5f54f..ba4e9c1d6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -179,6 +179,9 @@ importers:
next:
specifier: 15.0.0-canary.202
version: 15.0.0-canary.202(@babel/core@7.25.8)(babel-plugin-react-compiler@0.0.0-experimental-fa06e2c-20241016)(react-dom@19.0.0-rc-fb9a90fa48-20240614(react@19.0.0-rc-fb9a90fa48-20240614))(react@19.0.0-rc-fb9a90fa48-20240614)
+ next-live-transitions:
+ specifier: workspace:*
+ version: link:../../packages/next-live-transitions
next-sanity:
specifier: 9.7.0-canary.23
version: 9.7.0-canary.23(jeiaes6hv4t56bmyltysw6k5gm)
@@ -564,6 +567,9 @@ importers:
'@sanity/visual-editing':
specifier: workspace:*
version: link:../../packages/visual-editing
+ '@tailwindcss/typography':
+ specifier: ^0.5.15
+ version: 0.5.15(tailwindcss@3.4.14)
'@vercel/remix':
specifier: ^2.12.0
version: 2.12.0(@remix-run/dev@2.13.1(@remix-run/react@2.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(@remix-run/serve@2.13.1(typescript@5.6.3))(@types/node@22.5.5)(terser@5.33.0)(typescript@5.6.3)(vite@5.4.9(@types/node@22.5.5)(terser@5.33.0)))(@remix-run/node@2.13.1(typescript@5.6.3))(@remix-run/server-runtime@2.13.1(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -579,6 +585,12 @@ importers:
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
+ react-fast-compare:
+ specifier: ^3.2.2
+ version: 3.2.2
+ react-live-transitions:
+ specifier: workspace:*
+ version: link:../../packages/react-live-transitions
source-map-support:
specifier: ^0.5.21
version: 0.5.21
@@ -1132,6 +1144,43 @@ importers:
specifier: 5.6.3
version: 5.6.3
+ packages/next-live-transitions:
+ dependencies:
+ react-live-transitions:
+ specifier: workspace:*
+ version: link:../react-live-transitions
+ devDependencies:
+ '@repo/eslint-config':
+ specifier: workspace:*
+ version: link:../@repo/eslint-config
+ '@repo/package.config':
+ specifier: workspace:*
+ version: link:../@repo/package.config
+ '@repo/prettier-config':
+ specifier: workspace:*
+ version: link:../@repo/prettier-config
+ '@sanity/pkg-utils':
+ specifier: 6.11.4
+ version: 6.11.4(@types/babel__core@7.20.5)(@types/node@22.5.5)(typescript@5.6.3)
+ '@types/react':
+ specifier: ^18.3.11
+ version: 18.3.11
+ eslint:
+ specifier: ^8.57.1
+ version: 8.57.1
+ next:
+ specifier: ^14.2.15
+ version: 14.2.15(@babel/core@7.25.8)(react-dom@19.0.0-rc-fb9a90fa48-20240614(react@18.3.1))(react@18.3.1)
+ react:
+ specifier: ^18.3.1
+ version: 18.3.1
+ typescript:
+ specifier: 5.6.3
+ version: 5.6.3
+ vitest:
+ specifier: ^2.1.3
+ version: 2.1.3(@types/node@22.5.5)(happy-dom@15.7.4)(jsdom@23.2.0)(terser@5.33.0)
+
packages/next-loader:
dependencies:
'@sanity/comlink':
@@ -1360,6 +1409,66 @@ importers:
specifier: ^2.1.3
version: 2.1.3(@types/node@22.5.5)(happy-dom@15.7.4)(jsdom@23.2.0)(terser@5.33.0)
+ packages/react-live-transitions:
+ devDependencies:
+ '@repo/eslint-config':
+ specifier: workspace:*
+ version: link:../@repo/eslint-config
+ '@repo/package.config':
+ specifier: workspace:*
+ version: link:../@repo/package.config
+ '@repo/prettier-config':
+ specifier: workspace:*
+ version: link:../@repo/prettier-config
+ '@sanity/pkg-utils':
+ specifier: 6.11.4
+ version: 6.11.4(@types/babel__core@7.20.5)(@types/node@22.5.5)(typescript@5.6.3)
+ '@storybook/addon-essentials':
+ specifier: ^8.3.5
+ version: 8.3.6(storybook@8.3.6)(webpack-sources@3.2.3)
+ '@storybook/addon-interactions':
+ specifier: ^8.3.5
+ version: 8.3.6(storybook@8.3.6)
+ '@storybook/addon-links':
+ specifier: ^8.3.5
+ version: 8.3.6(react@18.3.1)(storybook@8.3.6)
+ '@storybook/react':
+ specifier: ^8.3.5
+ version: 8.3.6(@storybook/test@8.3.6(storybook@8.3.6))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.6)(typescript@5.6.3)
+ '@storybook/react-vite':
+ specifier: ^8.3.5
+ version: 8.3.6(@storybook/test@8.3.6(storybook@8.3.6))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.0)(storybook@8.3.6)(typescript@5.6.3)(vite@5.4.9(@types/node@22.5.5)(terser@5.33.0))(webpack-sources@3.2.3)
+ '@storybook/test':
+ specifier: ^8.3.5
+ version: 8.3.6(storybook@8.3.6)
+ '@types/react':
+ specifier: ^18.3.11
+ version: 18.3.11
+ eslint:
+ specifier: ^8.57.1
+ version: 8.57.1
+ eslint-plugin-storybook:
+ specifier: ^0.9.0
+ version: 0.9.0(eslint@8.57.1)(typescript@5.6.3)
+ react:
+ specifier: ^18.3.1
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.3.1
+ version: 18.3.1(react@18.3.1)
+ react-fast-compare:
+ specifier: ^3.2.2
+ version: 3.2.2
+ storybook:
+ specifier: ^8.3.5
+ version: 8.3.6
+ typescript:
+ specifier: 5.6.3
+ version: 5.6.3
+ vitest:
+ specifier: ^2.1.3
+ version: 2.1.3(@types/node@22.5.5)(happy-dom@15.7.4)(jsdom@23.2.0)(terser@5.33.0)
+
packages/react-loader:
dependencies:
'@sanity/core-loader':
@@ -4796,6 +4905,9 @@ packages:
peerDependencies:
storybook: ^8.3.6
+ '@storybook/csf@0.0.1':
+ resolution: {integrity: sha512-USTLkZze5gkel8MYCujSRBVIrUQ3YPBrLOx7GNk/0wttvVtlzWXAq9eLbQ4p/NicGxP+3T7KPEMVV//g+yubpw==}
+
'@storybook/csf@0.1.11':
resolution: {integrity: sha512-dHYFQH3mA+EtnCkHXzicbLgsvzYjcDJ1JWsogbItZogkPHgSJM/Wr71uMkcvw8v9mmCyP4NpXJuu6bPoVsOnzg==}
@@ -7478,6 +7590,12 @@ packages:
peerDependencies:
eslint: '>=6'
+ eslint-plugin-storybook@0.9.0:
+ resolution: {integrity: sha512-qOT/2vQBo0VqrG/BhZv8IdSsKQiyzJw+2Wqq+WFCiblI/PfxLSrGkF/buiXF+HumwfsCyBdaC94UhqhmYFmAvA==}
+ engines: {node: '>= 18'}
+ peerDependencies:
+ eslint: '>=6'
+
eslint-plugin-svelte@2.46.0:
resolution: {integrity: sha512-1A7iEMkzmCZ9/Iz+EAfOGYL8IoIG6zeKEq1SmpxGeM5SXmoQq+ZNnCpXFVJpsxPWYx8jIVGMerQMzX20cqUl0g==}
engines: {node: ^14.17.0 || >=16.0.0}
@@ -17682,6 +17800,10 @@ snapshots:
transitivePeerDependencies:
- webpack-sources
+ '@storybook/csf@0.0.1':
+ dependencies:
+ lodash: 4.17.21
+
'@storybook/csf@0.1.11':
dependencies:
type-fest: 2.19.0
@@ -21376,6 +21498,17 @@ snapshots:
- supports-color
- typescript
+ eslint-plugin-storybook@0.9.0(eslint@8.57.1)(typescript@5.6.3):
+ dependencies:
+ '@storybook/csf': 0.0.1
+ '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.6.3)
+ eslint: 8.57.1
+ requireindex: 1.2.0
+ ts-dedent: 2.2.0
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
eslint-plugin-svelte@2.46.0(eslint@8.57.1)(svelte@4.2.19):
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1)
diff --git a/release-please-config.json b/release-please-config.json
index ea8593287..a094c2dc4 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -8,10 +8,12 @@
"packages/comlink": {},
"packages/core-loader": {},
"packages/insert-menu": {},
+ "packages/next-live-transitions": {},
"packages/next-loader": {},
"packages/presentation": {},
"packages/preview-kit-compat": {},
"packages/preview-url-secret": {},
+ "packages/react-live-transitions": {},
"packages/react-loader": {},
"packages/svelte-loader": {},
"packages/visual-editing": {},