diff --git a/docs/docs/start/interactive-tutorial/dice-roller.mdx b/docs/docs/start/interactive-tutorial/dice-roller.mdx
new file mode 100644
index 000000000000..ae56e73fa73c
--- /dev/null
+++ b/docs/docs/start/interactive-tutorial/dice-roller.mdx
@@ -0,0 +1,11 @@
+---
+title: "Dice Roller"
+sidebar_position: 1
+hide_table_of_contents: true
+---
+
+import { TutorialPlayground } from "@site/src/components/playground";
+
+# Dice Roller
+
+
diff --git a/docs/docs/start/interactive-tutorial/index.mdx b/docs/docs/start/interactive-tutorial/index.mdx
new file mode 100644
index 000000000000..ffa781f2d04d
--- /dev/null
+++ b/docs/docs/start/interactive-tutorial/index.mdx
@@ -0,0 +1,14 @@
+---
+title: Interactive Tutorial
+sidebar_position: 5
+hide_table_of_contents: true
+---
+
+import { ModuleSelector } from "@site/src/components/playground";
+
+# Interactive Tutorial
+
+Learn Fluid Framework hands-on by writing real code in your browser — no local setup required.
+Choose a tutorial module to get started. Each module walks you through building a real Fluid application step by step.
+
+
diff --git a/docs/docs/start/interactive-tutorial/shared-tree-todo.mdx b/docs/docs/start/interactive-tutorial/shared-tree-todo.mdx
new file mode 100644
index 000000000000..15f696448563
--- /dev/null
+++ b/docs/docs/start/interactive-tutorial/shared-tree-todo.mdx
@@ -0,0 +1,11 @@
+---
+title: "SharedTree Todo App"
+sidebar_position: 2
+hide_table_of_contents: true
+---
+
+import { TutorialPlayground } from "@site/src/components/playground";
+
+# SharedTree Todo App
+
+
diff --git a/docs/package.json b/docs/package.json
index 25b4e900d0c2..c5d5d9f50613 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -60,6 +60,8 @@
},
"devDependencies": {
"@azure/static-web-apps-cli": "^2.0.1",
+ "@codesandbox/sandpack-react": "^2.19.0",
+ "@codesandbox/sandpack-themes": "^2.0.21",
"@docusaurus/core": "^3.6.2",
"@docusaurus/eslint-plugin": "^3.6.2",
"@docusaurus/module-type-aliases": "^3.6.2",
diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml
index e7d2798b91d7..931dbb31044c 100644
--- a/docs/pnpm-lock.yaml
+++ b/docs/pnpm-lock.yaml
@@ -22,6 +22,12 @@ importers:
'@azure/static-web-apps-cli':
specifier: ^2.0.1
version: 2.0.1
+ '@codesandbox/sandpack-react':
+ specifier: ^2.19.0
+ version: 2.20.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@codesandbox/sandpack-themes':
+ specifier: ^2.0.21
+ version: 2.0.21
'@docusaurus/core':
specifier: ^3.6.2
version: 3.6.2(@mdx-js/react@3.1.0(@types/react@19.2.14)(react@18.3.1))(acorn@8.15.0)(eslint@8.57.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4)
@@ -976,6 +982,48 @@ packages:
'@chevrotain/utils@11.0.3':
resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==}
+ '@codemirror/autocomplete@6.20.1':
+ resolution: {integrity: sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==}
+
+ '@codemirror/commands@6.10.2':
+ resolution: {integrity: sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==}
+
+ '@codemirror/lang-css@6.3.1':
+ resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==}
+
+ '@codemirror/lang-html@6.4.11':
+ resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==}
+
+ '@codemirror/lang-javascript@6.2.5':
+ resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==}
+
+ '@codemirror/language@6.12.2':
+ resolution: {integrity: sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==}
+
+ '@codemirror/lint@6.9.5':
+ resolution: {integrity: sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==}
+
+ '@codemirror/state@6.5.4':
+ resolution: {integrity: sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==}
+
+ '@codemirror/view@6.39.16':
+ resolution: {integrity: sha512-m6S22fFpKtOWhq8HuhzsI1WzUP/hB9THbDj0Tl5KX4gbO6Y91hwBl7Yky33NdvB6IffuRFiBxf1R8kJMyXmA4Q==}
+
+ '@codesandbox/nodebox@0.1.8':
+ resolution: {integrity: sha512-2VRS6JDSk+M+pg56GA6CryyUSGPjBEe8Pnae0QL3jJF1mJZJVMDKr93gJRtBbLkfZN6LD/DwMtf+2L0bpWrjqg==}
+
+ '@codesandbox/sandpack-client@2.19.8':
+ resolution: {integrity: sha512-CMV4nr1zgKzVpx4I3FYvGRM5YT0VaQhALMW9vy4wZRhEyWAtJITQIqZzrTGWqB1JvV7V72dVEUCUPLfYz5hgJQ==}
+
+ '@codesandbox/sandpack-react@2.20.0':
+ resolution: {integrity: sha512-takd1YpW/PMQ6KPQfvseWLHWklJovGY8QYj8MtWnskGKbjOGJ6uZfyZbcJ6aCFLQMpNyjTqz9AKNbvhCOZ1TUQ==}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+ react-dom: ^16.8.0 || ^17 || ^18 || ^19
+
+ '@codesandbox/sandpack-themes@2.0.21':
+ resolution: {integrity: sha512-CMH/MO/dh6foPYb/3eSn2Cu/J3+1+/81Fsaj7VggICkCrmRk0qG5dmgjGAearPTnRkOGORIPHuRqwNXgw0E6YQ==}
+
'@colors/colors@1.5.0':
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
engines: {node: '>=0.1.90'}
@@ -1543,6 +1591,27 @@ packages:
'@leichtgewicht/ip-codec@2.0.5':
resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==}
+ '@lezer/common@1.5.1':
+ resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==}
+
+ '@lezer/css@1.3.1':
+ resolution: {integrity: sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg==}
+
+ '@lezer/highlight@1.2.3':
+ resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
+
+ '@lezer/html@1.3.13':
+ resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==}
+
+ '@lezer/javascript@1.5.4':
+ resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==}
+
+ '@lezer/lr@1.4.8':
+ resolution: {integrity: sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==}
+
+ '@marijn/find-cluster-break@1.0.2':
+ resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
+
'@mdx-js/mdx@3.1.0':
resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==}
@@ -1670,6 +1739,9 @@ packages:
resolution: {integrity: sha512-mDYOl8RmldLkOg9i9YKgyBlpcyi/bNySoIVHJ2EJd2qCmZaXRKQKRW2Zkx92bwjik8jfs/A3EFI+p4DsrXi57g==}
engines: {node: '>=18.0.0'}
+ '@open-draft/deferred-promise@2.2.0':
+ resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==}
+
'@parcel/watcher-android-arm64@2.5.6':
resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==}
engines: {node: '>= 10.0.0'}
@@ -1786,6 +1858,16 @@ packages:
'@polka/url@1.0.0-next.28':
resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==}
+ '@react-hook/intersection-observer@3.1.2':
+ resolution: {integrity: sha512-mWU3BMkmmzyYMSuhO9wu3eJVP21N8TcgYm9bZnTrMwuM818bEk+0NRM3hP+c/TqA9Ln5C7qE53p1H0QMtzYdvQ==}
+ peerDependencies:
+ react: '>=16.8'
+
+ '@react-hook/passive-layout-effect@1.2.1':
+ resolution: {integrity: sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==}
+ peerDependencies:
+ react: '>=16.8'
+
'@rushstack/eslint-patch@1.12.0':
resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==}
@@ -1850,6 +1932,9 @@ packages:
'@slorber/remark-comment@1.0.0':
resolution: {integrity: sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==}
+ '@stitches/core@1.2.8':
+ resolution: {integrity: sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==}
+
'@svgr/babel-plugin-add-jsx-attribute@8.0.0':
resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==}
engines: {node: '>=14'}
@@ -2666,6 +2751,9 @@ packages:
resolution: {integrity: sha512-Yf3Swz1s63hjvBVZ/9f2P1Uu48GjmjCN+Esxb6MAONMGtZB1fRX8/S1AhUTtsuTlcGovbYLxpHgc7wEzstDZBw==}
engines: {node: '>= 14.0.0'}
+ anser@2.3.5:
+ resolution: {integrity: sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==}
+
ansi-align@3.0.1:
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
@@ -2909,6 +2997,9 @@ packages:
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
+ buffer@6.0.3:
+ resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+
builtin-modules@3.3.0:
resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
engines: {node: '>=6'}
@@ -3065,6 +3156,9 @@ packages:
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
engines: {node: '>=4'}
+ clean-set@1.1.2:
+ resolution: {integrity: sha512-cA8uCj0qSoG9e0kevyOWXwPaELRPVg5Pxp6WskLMwerx257Zfnh8Nl0JBH59d7wQzij2CK7qEfJQK3RjuKKIug==}
+
clean-stack@2.2.0:
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
engines: {node: '>=6'}
@@ -3314,6 +3408,9 @@ packages:
typescript:
optional: true
+ crelt@1.0.6:
+ resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
+
cross-env@7.0.3:
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
@@ -3604,6 +3701,10 @@ packages:
resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==}
engines: {node: '>=12'}
+ d@1.0.2:
+ resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==}
+ engines: {node: '>=0.12'}
+
dagre-d3-es@7.0.11:
resolution: {integrity: sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==}
@@ -3997,10 +4098,21 @@ packages:
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
engines: {node: '>= 0.4'}
+ es5-ext@0.10.64:
+ resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==}
+ engines: {node: '>=0.10'}
+
+ es6-iterator@2.0.3:
+ resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==}
+
es6-promisify@7.0.0:
resolution: {integrity: sha512-ginqzK3J90Rd4/Yz7qRrqUeIpe3TwSXTPPZtPne7tGBPeAaQiU8qt4fpKApnxHcq1AwtUdHVg5P77x/yrggG8Q==}
engines: {node: '>=6'}
+ es6-symbol@3.1.4:
+ resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==}
+ engines: {node: '>=0.12'}
+
esast-util-from-estree@2.0.0:
resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==}
@@ -4011,6 +4123,9 @@ packages:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
+ escape-carriage@1.3.1:
+ resolution: {integrity: sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw==}
+
escape-goat@4.0.0:
resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==}
engines: {node: '>=12'}
@@ -4181,6 +4296,10 @@ packages:
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
hasBin: true
+ esniff@2.0.1:
+ resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==}
+ engines: {node: '>=0.10'}
+
espree@10.4.0:
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -4250,6 +4369,9 @@ packages:
resolution: {integrity: sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==}
engines: {node: '>= 0.8'}
+ event-emitter@0.3.5:
+ resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==}
+
event-stream@3.3.4:
resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==}
@@ -4275,6 +4397,9 @@ packages:
exsolve@1.0.7:
resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==}
+ ext@1.7.0:
+ resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==}
+
extend-shallow@2.0.1:
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
engines: {node: '>=0.10.0'}
@@ -4996,6 +5121,10 @@ packages:
resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==}
engines: {node: '>= 0.10'}
+ intersection-observer@0.10.0:
+ resolution: {integrity: sha512-fn4bQ0Xq8FTej09YC/jqKZwtijpvARlRp6wxL5WTA6yPe2YWSJ5RJh7Nm79rK2qB0wr6iDQzH60XGq5V/7u8YQ==}
+ deprecated: The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019.
+
invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
@@ -5637,6 +5766,10 @@ packages:
lunr@2.3.9:
resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==}
+ lz-string@1.5.0:
+ resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
+ hasBin: true
+
make-dir@1.3.0:
resolution: {integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==}
engines: {node: '>=4'}
@@ -6096,6 +6229,9 @@ packages:
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
+ next-tick@1.1.0:
+ resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
+
no-case@3.0.4:
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
@@ -6254,6 +6390,9 @@ packages:
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'}
+ outvariant@1.4.0:
+ resolution: {integrity: sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==}
+
own-keys@1.0.1:
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
engines: {node: '>= 0.4'}
@@ -7051,6 +7190,9 @@ packages:
typescript:
optional: true
+ react-devtools-inline@4.4.0:
+ resolution: {integrity: sha512-ES0GolSrKO8wsKbsEkVeiR/ZAaHQTY4zDh1UW8DImVmm8oaGLl3ijJDvSGe+qDRKPZdPRnDtWWnSvvrgxXdThQ==}
+
react-dom@18.3.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:
@@ -7076,6 +7218,9 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+ react-is@17.0.2:
+ resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+
react-json-view-lite@1.5.0:
resolution: {integrity: sha512-nWqA1E4jKPklL2jvHWs6s+7Na0qNgw9HCP6xehdQJeg6nPBTFZgGwyko9Q0oj+jQWKTTVRS30u0toM5wiuL3iw==}
engines: {node: '>=14'}
@@ -7676,6 +7821,9 @@ packages:
engines: {node: '>=16'}
hasBin: true
+ static-browser-server@1.0.3:
+ resolution: {integrity: sha512-ZUyfgGDdFRbZGGJQ1YhiM930Yczz5VlbJObrQLlk24+qNHVQx4OlLcYswEUo3bIyNAbQUIUR9Yr5/Hqjzqb4zA==}
+
statuses@1.5.0:
resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
engines: {node: '>= 0.6'}
@@ -7698,6 +7846,9 @@ packages:
stream-combiner@0.0.4:
resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==}
+ strict-event-emitter@0.4.6:
+ resolution: {integrity: sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==}
+
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@@ -7784,6 +7935,9 @@ packages:
stubborn-fs@1.2.5:
resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==}
+ style-mod@4.1.3:
+ resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
+
style-to-object@0.4.4:
resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==}
@@ -8037,6 +8191,9 @@ packages:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
+ type@2.7.3:
+ resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==}
+
typed-array-buffer@1.0.3:
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
engines: {node: '>= 0.4'}
@@ -8331,6 +8488,9 @@ packages:
vscode-uri@3.0.8:
resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
+ w3c-keyname@2.2.8:
+ resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
+
wait-on@7.2.0:
resolution: {integrity: sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==}
engines: {node: '>=12.0.0'}
@@ -9746,6 +9906,116 @@ snapshots:
'@chevrotain/utils@11.0.3': {}
+ '@codemirror/autocomplete@6.20.1':
+ dependencies:
+ '@codemirror/language': 6.12.2
+ '@codemirror/state': 6.5.4
+ '@codemirror/view': 6.39.16
+ '@lezer/common': 1.5.1
+
+ '@codemirror/commands@6.10.2':
+ dependencies:
+ '@codemirror/language': 6.12.2
+ '@codemirror/state': 6.5.4
+ '@codemirror/view': 6.39.16
+ '@lezer/common': 1.5.1
+
+ '@codemirror/lang-css@6.3.1':
+ dependencies:
+ '@codemirror/autocomplete': 6.20.1
+ '@codemirror/language': 6.12.2
+ '@codemirror/state': 6.5.4
+ '@lezer/common': 1.5.1
+ '@lezer/css': 1.3.1
+
+ '@codemirror/lang-html@6.4.11':
+ dependencies:
+ '@codemirror/autocomplete': 6.20.1
+ '@codemirror/lang-css': 6.3.1
+ '@codemirror/lang-javascript': 6.2.5
+ '@codemirror/language': 6.12.2
+ '@codemirror/state': 6.5.4
+ '@codemirror/view': 6.39.16
+ '@lezer/common': 1.5.1
+ '@lezer/css': 1.3.1
+ '@lezer/html': 1.3.13
+
+ '@codemirror/lang-javascript@6.2.5':
+ dependencies:
+ '@codemirror/autocomplete': 6.20.1
+ '@codemirror/language': 6.12.2
+ '@codemirror/lint': 6.9.5
+ '@codemirror/state': 6.5.4
+ '@codemirror/view': 6.39.16
+ '@lezer/common': 1.5.1
+ '@lezer/javascript': 1.5.4
+
+ '@codemirror/language@6.12.2':
+ dependencies:
+ '@codemirror/state': 6.5.4
+ '@codemirror/view': 6.39.16
+ '@lezer/common': 1.5.1
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.8
+ style-mod: 4.1.3
+
+ '@codemirror/lint@6.9.5':
+ dependencies:
+ '@codemirror/state': 6.5.4
+ '@codemirror/view': 6.39.16
+ crelt: 1.0.6
+
+ '@codemirror/state@6.5.4':
+ dependencies:
+ '@marijn/find-cluster-break': 1.0.2
+
+ '@codemirror/view@6.39.16':
+ dependencies:
+ '@codemirror/state': 6.5.4
+ crelt: 1.0.6
+ style-mod: 4.1.3
+ w3c-keyname: 2.2.8
+
+ '@codesandbox/nodebox@0.1.8':
+ dependencies:
+ outvariant: 1.4.0
+ strict-event-emitter: 0.4.6
+
+ '@codesandbox/sandpack-client@2.19.8':
+ dependencies:
+ '@codesandbox/nodebox': 0.1.8
+ buffer: 6.0.3
+ dequal: 2.0.3
+ mime-db: 1.54.0
+ outvariant: 1.4.0
+ static-browser-server: 1.0.3
+
+ '@codesandbox/sandpack-react@2.20.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@codemirror/autocomplete': 6.20.1
+ '@codemirror/commands': 6.10.2
+ '@codemirror/lang-css': 6.3.1
+ '@codemirror/lang-html': 6.4.11
+ '@codemirror/lang-javascript': 6.2.5
+ '@codemirror/language': 6.12.2
+ '@codemirror/state': 6.5.4
+ '@codemirror/view': 6.39.16
+ '@codesandbox/sandpack-client': 2.19.8
+ '@lezer/highlight': 1.2.3
+ '@react-hook/intersection-observer': 3.1.2(react@18.3.1)
+ '@stitches/core': 1.2.8
+ anser: 2.3.5
+ clean-set: 1.1.2
+ dequal: 2.0.3
+ escape-carriage: 1.3.1
+ lz-string: 1.5.0
+ react: 18.3.1
+ react-devtools-inline: 4.4.0
+ react-dom: 18.3.1(react@18.3.1)
+ react-is: 17.0.2
+
+ '@codesandbox/sandpack-themes@2.0.21': {}
+
'@colors/colors@1.5.0':
optional: true
@@ -11012,6 +11282,36 @@ snapshots:
'@leichtgewicht/ip-codec@2.0.5': {}
+ '@lezer/common@1.5.1': {}
+
+ '@lezer/css@1.3.1':
+ dependencies:
+ '@lezer/common': 1.5.1
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.8
+
+ '@lezer/highlight@1.2.3':
+ dependencies:
+ '@lezer/common': 1.5.1
+
+ '@lezer/html@1.3.13':
+ dependencies:
+ '@lezer/common': 1.5.1
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.8
+
+ '@lezer/javascript@1.5.4':
+ dependencies:
+ '@lezer/common': 1.5.1
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.8
+
+ '@lezer/lr@1.4.8':
+ dependencies:
+ '@lezer/common': 1.5.1
+
+ '@marijn/find-cluster-break@1.0.2': {}
+
'@mdx-js/mdx@3.1.0(acorn@8.15.0)':
dependencies:
'@types/estree': 1.0.8
@@ -11269,6 +11569,8 @@ snapshots:
dependencies:
'@oclif/core': 4.0.33
+ '@open-draft/deferred-promise@2.2.0': {}
+
'@parcel/watcher-android-arm64@2.5.6':
optional: true
@@ -11353,6 +11655,16 @@ snapshots:
'@polka/url@1.0.0-next.28': {}
+ '@react-hook/intersection-observer@3.1.2(react@18.3.1)':
+ dependencies:
+ '@react-hook/passive-layout-effect': 1.2.1(react@18.3.1)
+ intersection-observer: 0.10.0
+ react: 18.3.1
+
+ '@react-hook/passive-layout-effect@1.2.1(react@18.3.1)':
+ dependencies:
+ react: 18.3.1
+
'@rushstack/eslint-patch@1.12.0': {}
'@rushstack/eslint-plugin-security@0.11.0(eslint@8.57.1)(typescript@5.5.4)':
@@ -11429,6 +11741,8 @@ snapshots:
micromark-util-character: 1.2.0
micromark-util-symbol: 1.1.0
+ '@stitches/core@1.2.8': {}
+
'@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.26.0)':
dependencies:
'@babel/core': 7.26.0
@@ -12430,6 +12744,8 @@ snapshots:
'@algolia/requester-fetch': 5.15.0
'@algolia/requester-node-http': 5.15.0
+ anser@2.3.5: {}
+
ansi-align@3.0.1:
dependencies:
string-width: 4.2.3
@@ -12730,6 +13046,11 @@ snapshots:
base64-js: 1.5.1
ieee754: 1.2.1
+ buffer@6.0.3:
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+
builtin-modules@3.3.0: {}
bytes@3.0.0: {}
@@ -12896,6 +13217,8 @@ snapshots:
dependencies:
escape-string-regexp: 1.0.5
+ clean-set@1.1.2: {}
+
clean-stack@2.2.0: {}
clean-stack@3.0.1:
@@ -13135,6 +13458,8 @@ snapshots:
optionalDependencies:
typescript: 5.5.4
+ crelt@1.0.6: {}
+
cross-env@7.0.3:
dependencies:
cross-spawn: 7.0.6
@@ -13470,6 +13795,11 @@ snapshots:
d3-transition: 3.0.1(d3-selection@3.0.0)
d3-zoom: 3.0.0
+ d@1.0.2:
+ dependencies:
+ es5-ext: 0.10.64
+ type: 2.7.3
+
dagre-d3-es@7.0.11:
dependencies:
d3: 7.9.0
@@ -14055,8 +14385,26 @@ snapshots:
is-date-object: 1.1.0
is-symbol: 1.1.1
+ es5-ext@0.10.64:
+ dependencies:
+ es6-iterator: 2.0.3
+ es6-symbol: 3.1.4
+ esniff: 2.0.1
+ next-tick: 1.1.0
+
+ es6-iterator@2.0.3:
+ dependencies:
+ d: 1.0.2
+ es5-ext: 0.10.64
+ es6-symbol: 3.1.4
+
es6-promisify@7.0.0: {}
+ es6-symbol@3.1.4:
+ dependencies:
+ d: 1.0.2
+ ext: 1.7.0
+
esast-util-from-estree@2.0.0:
dependencies:
'@types/estree-jsx': 1.0.5
@@ -14073,6 +14421,8 @@ snapshots:
escalade@3.2.0: {}
+ escape-carriage@1.3.1: {}
+
escape-goat@4.0.0: {}
escape-html@1.0.3: {}
@@ -14331,6 +14681,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ esniff@2.0.1:
+ dependencies:
+ d: 1.0.2
+ es5-ext: 0.10.64
+ event-emitter: 0.3.5
+ type: 2.7.3
+
espree@10.4.0:
dependencies:
acorn: 8.15.0
@@ -14405,6 +14762,11 @@ snapshots:
'@types/node': 22.9.1
require-like: 0.1.2
+ event-emitter@0.3.5:
+ dependencies:
+ d: 1.0.2
+ es5-ext: 0.10.64
+
event-stream@3.3.4:
dependencies:
duplexer: 0.1.2
@@ -14471,6 +14833,10 @@ snapshots:
exsolve@1.0.7: {}
+ ext@1.7.0:
+ dependencies:
+ type: 2.7.3
+
extend-shallow@2.0.1:
dependencies:
is-extendable: 0.1.1
@@ -15337,6 +15703,8 @@ snapshots:
interpret@1.4.0: {}
+ intersection-observer@0.10.0: {}
+
invariant@2.2.4:
dependencies:
loose-envify: 1.4.0
@@ -15914,6 +16282,8 @@ snapshots:
lunr@2.3.9: {}
+ lz-string@1.5.0: {}
+
make-dir@1.3.0:
dependencies:
pify: 3.0.0
@@ -16721,6 +17091,8 @@ snapshots:
neo-async@2.6.2: {}
+ next-tick@1.1.0: {}
+
no-case@3.0.4:
dependencies:
lower-case: 2.0.2
@@ -16896,6 +17268,8 @@ snapshots:
os-tmpdir@1.0.2: {}
+ outvariant@1.4.0: {}
+
own-keys@1.0.1:
dependencies:
get-intrinsic: 1.3.0
@@ -17760,6 +18134,10 @@ snapshots:
- supports-color
- vue-template-compiler
+ react-devtools-inline@4.4.0:
+ dependencies:
+ es6-symbol: 3.1.4
+
react-dom@18.3.1(react@18.3.1):
dependencies:
loose-envify: 1.4.0
@@ -17789,6 +18167,8 @@ snapshots:
react-is@16.13.1: {}
+ react-is@17.0.2: {}
+
react-json-view-lite@1.5.0(react@18.3.1):
dependencies:
react: 18.3.1
@@ -18574,6 +18954,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ static-browser-server@1.0.3:
+ dependencies:
+ '@open-draft/deferred-promise': 2.2.0
+ dotenv: 16.4.7
+ mime-db: 1.54.0
+ outvariant: 1.4.0
+
statuses@1.5.0: {}
statuses@2.0.1: {}
@@ -18591,6 +18978,8 @@ snapshots:
dependencies:
duplexer: 0.1.2
+ strict-event-emitter@0.4.6: {}
+
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
@@ -18709,6 +19098,8 @@ snapshots:
stubborn-fs@1.2.5: {}
+ style-mod@4.1.3: {}
+
style-to-object@0.4.4:
dependencies:
inline-style-parser: 0.1.1
@@ -18947,6 +19338,8 @@ snapshots:
media-typer: 0.3.0
mime-types: 2.1.35
+ type@2.7.3: {}
+
typed-array-buffer@1.0.3:
dependencies:
call-bound: 1.0.4
@@ -19360,6 +19753,8 @@ snapshots:
vscode-uri@3.0.8: {}
+ w3c-keyname@2.2.8: {}
+
wait-on@7.2.0:
dependencies:
axios: 1.12.2(debug@4.3.7)
diff --git a/docs/src/components/playground/ModuleSelector.tsx b/docs/src/components/playground/ModuleSelector.tsx
new file mode 100644
index 000000000000..12cbb7422eae
--- /dev/null
+++ b/docs/src/components/playground/ModuleSelector.tsx
@@ -0,0 +1,42 @@
+/*!
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+import Link from "@docusaurus/Link";
+import React from "react";
+
+import { moduleList } from "./data/modules";
+
+import "@site/src/css/playground.css";
+
+/**
+ * Renders a card grid for selecting a tutorial module.
+ * Each card links to the module's dedicated sub-page.
+ */
+export function ModuleSelector(): React.ReactElement {
+ return (
+
+ {moduleList.map((mod) => (
+
+
+
{mod.title}
+
+ {mod.difficulty}
+
+
+
{mod.description}
+
+ {mod.steps.length} steps
+
+
+ ))}
+
+ );
+}
diff --git a/docs/src/components/playground/PlaygroundWorkspace.tsx b/docs/src/components/playground/PlaygroundWorkspace.tsx
new file mode 100644
index 000000000000..4d300380058d
--- /dev/null
+++ b/docs/src/components/playground/PlaygroundWorkspace.tsx
@@ -0,0 +1,162 @@
+/*!
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+import type { SandpackFiles } from "@codesandbox/sandpack-react";
+import {
+ SandpackCodeEditor,
+ SandpackLayout,
+ SandpackPreview,
+ SandpackProvider,
+ useSandpack,
+} from "@codesandbox/sandpack-react";
+import React from "react";
+
+/**
+ * Internal component that syncs external file changes into the live Sandpack
+ * instance and reports code edits back to the parent via {@link onCodeChange}.
+ *
+ * By calling `sandpack.updateFile` imperatively we avoid destroying and
+ * recreating the SandpackProvider (which would re-download and re-bundle every
+ * npm dependency from scratch — the main cause of ~60 s load times).
+ */
+function SandpackBridge({
+ files,
+ activeFile,
+ onCodeChange,
+}: {
+ files: SandpackFiles;
+ activeFile: string;
+ onCodeChange: (code: string) => void;
+}): null {
+ const { sandpack } = useSandpack();
+
+ // Track previous files so we only call updateFile when they actually change.
+ const prevFilesRef = React.useRef(files);
+
+ React.useEffect(() => {
+ if (prevFilesRef.current === files) return;
+ prevFilesRef.current = files;
+
+ // Push every file into the running sandbox.
+ for (const [path, content] of Object.entries(files)) {
+ const code = typeof content === "string" ? content : content.code;
+ sandpack.updateFile(path, code);
+ }
+
+ // Switch to the requested active file.
+ sandpack.setActiveFile(activeFile);
+ }, [files, activeFile, sandpack]);
+
+ // Report edits back for validation.
+ React.useEffect(() => {
+ const code = sandpack.files[activeFile]?.code ?? "";
+ onCodeChange(code);
+ }, [sandpack.files, activeFile, onCodeChange]);
+
+ return null;
+}
+
+/**
+ * {@link PlaygroundWorkspace} component props.
+ */
+export interface PlaygroundWorkspaceProps {
+ /**
+ * Sandpack file map for the current step.
+ */
+ files: SandpackFiles;
+
+ /**
+ * Which file is active in the editor.
+ */
+ activeFile: string;
+
+ /**
+ * NPM dependencies for the Sandpack sandbox.
+ */
+ dependencies: Record;
+
+ /**
+ * Callback when the user's code changes.
+ */
+ onCodeChange: (code: string) => void;
+}
+
+/**
+ * Vite entry HTML that references /main.tsx as the module entry point.
+ * Provided to ensure the vite-react-ts template loads our custom entry file.
+ */
+const indexHtml = `
+
+
+
+
+
+
+`;
+
+/**
+ * Wraps Sandpack editor and preview in a side-by-side layout.
+ *
+ * Uses the vite-react-ts template so that code is transpiled by esbuild
+ * instead of Babel. Babel's ES5 class transpilation adds _classCallCheck
+ * which is incompatible with SharedTree's Reflect.construct-based proxy
+ * node system, causing "Cannot call a class as a function" errors.
+ *
+ * IMPORTANT: This component keeps a single SandpackProvider mounted for the
+ * lifetime of the tutorial. Step changes push new files via the imperative
+ * `updateFile` API so the bundler stays warm and dependencies are not
+ * re-downloaded.
+ */
+export function PlaygroundWorkspace({
+ files,
+ activeFile,
+ dependencies,
+ onCodeChange,
+}: PlaygroundWorkspaceProps): React.ReactElement {
+ // Initial files are used only for the first mount of SandpackProvider.
+ const initialFilesRef = React.useRef({ "/index.html": indexHtml, ...files });
+ const customSetup = React.useMemo(() => ({ dependencies }), [dependencies]);
+
+ const options = React.useMemo(
+ () => ({
+ activeFile,
+ visibleFiles: [activeFile],
+ recompileMode: "delayed" as const,
+ recompileDelay: 500,
+ }),
+ [activeFile],
+ );
+
+ // Merge index.html into the file map so the bridge pushes it too.
+ const allFiles = React.useMemo(
+ () => ({ "/index.html": indexHtml, ...files }),
+ [files],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/src/components/playground/StepGuide.tsx b/docs/src/components/playground/StepGuide.tsx
new file mode 100644
index 000000000000..06493ace4411
--- /dev/null
+++ b/docs/src/components/playground/StepGuide.tsx
@@ -0,0 +1,231 @@
+/*!
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+import Link from "@docusaurus/Link";
+import React from "react";
+
+import { StepIndicator } from "./StepIndicator";
+import { ValidationBadge } from "./ValidationBadge";
+import type { TutorialStep } from "./data/types";
+
+/**
+ * {@link StepGuide} component props.
+ */
+export interface StepGuideProps {
+ /**
+ * The current tutorial step.
+ */
+ step: TutorialStep;
+
+ /**
+ * Current step index (0-based).
+ */
+ currentStepIndex: number;
+
+ /**
+ * Total number of steps in the module.
+ */
+ totalSteps: number;
+
+ /**
+ * Validation results for each pattern (parallel array to step.validationPatterns).
+ */
+ validationResults: boolean[];
+
+ /**
+ * Whether the solution is currently shown.
+ */
+ showSolution: boolean;
+
+ /**
+ * Set of step indices that have been completed.
+ */
+ completedSteps: Set;
+
+ /**
+ * Callback when user navigates to a step.
+ */
+ onNavigate: (stepIndex: number) => void;
+
+ /**
+ * Callback to toggle showing the solution.
+ */
+ onToggleSolution: () => void;
+
+ /**
+ * URL path to the tutorial module index page (for "Back to Tutorials" link).
+ */
+ moduleIndexUrl: string;
+
+ /**
+ * Callback to reset the current step to boilerplate.
+ */
+ onResetStep: () => void;
+}
+
+/**
+ * Renders a string containing inline markdown (backtick code and **bold**)
+ * as React elements.
+ */
+function renderInlineMarkdown(text: string): React.ReactNode {
+ // Split on `code` and **bold** tokens, preserving delimiters
+ const parts = text.split(/(`[^`]+`|\*\*[^*]+\*\*)/g);
+ return parts.map((part, i) => {
+ if (part.startsWith("`") && part.endsWith("`")) {
+ return {part.slice(1, -1)};
+ }
+ if (part.startsWith("**") && part.endsWith("**")) {
+ return {part.slice(2, -2)};
+ }
+ return part;
+ });
+}
+
+/**
+ * Renders the step instructions, hints, validation checklist, and navigation.
+ */
+export function StepGuide({
+ step,
+ currentStepIndex,
+ totalSteps,
+ validationResults,
+ showSolution,
+ completedSteps,
+ moduleIndexUrl,
+ onNavigate,
+ onToggleSolution,
+ onResetStep,
+}: StepGuideProps): React.ReactElement {
+ const [expandedHints, setExpandedHints] = React.useState>(new Set());
+
+ const toggleHint = (index: number): void => {
+ setExpandedHints((prev: Set) => {
+ const next = new Set(prev);
+ if (next.has(index)) {
+ next.delete(index);
+ } else {
+ next.add(index);
+ }
+ return next;
+ });
+ };
+
+ // Reset expanded hints when step changes
+ React.useEffect(() => {
+ setExpandedHints(new Set());
+ }, [step.id]);
+
+ const allPassed =
+ step.validationPatterns.length === 0 ||
+ (validationResults.length > 0 && validationResults.every(Boolean));
+
+ return (
+
+
+
+
{step.title}
+
{renderInlineMarkdown(step.description)}
+
+ {step.hints.length > 0 && (
+
+
Hints
+ {step.hints.map((hint, i) => (
+
+
+ {expandedHints.has(i) && (
+
+ {renderInlineMarkdown(hint)}
+
+ )}
+
+ ))}
+
+ )}
+
+ {step.validationPatterns.length > 0 && (
+
+
Checklist
+ {step.validationPatterns.map((pattern, i) => (
+
+ ))}
+ {allPassed && (
+
+ All checks passed!
+
+ )}
+
+ )}
+
+
+
+
+
+ {step.solution !== undefined && (
+
+ )}
+
+
+
+ {currentStepIndex === totalSteps - 1 && allPassed && (
+
+ Back to Tutorials
+
+ )}
+
+
+ {currentStepIndex < totalSteps - 1 ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/docs/src/components/playground/StepIndicator.tsx b/docs/src/components/playground/StepIndicator.tsx
new file mode 100644
index 000000000000..a7abaf35e3b7
--- /dev/null
+++ b/docs/src/components/playground/StepIndicator.tsx
@@ -0,0 +1,57 @@
+/*!
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+import React from "react";
+
+/**
+ * {@link StepIndicator} component props.
+ */
+export interface StepIndicatorProps {
+ /**
+ * Current step index (0-based).
+ */
+ currentStep: number;
+
+ /**
+ * Total number of steps.
+ */
+ totalSteps: number;
+
+ /**
+ * Set of step indices that have been completed.
+ */
+ completedSteps?: Set;
+}
+
+/**
+ * Renders a step progress indicator bar.
+ */
+export function StepIndicator({
+ currentStep,
+ totalSteps,
+ completedSteps,
+}: StepIndicatorProps): React.ReactElement {
+ return (
+
+
+ Step {currentStep + 1} of {totalSteps}
+
+
+ {Array.from({ length: totalSteps }, (_, i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/docs/src/components/playground/TutorialPlayground.tsx b/docs/src/components/playground/TutorialPlayground.tsx
new file mode 100644
index 000000000000..5660b4878521
--- /dev/null
+++ b/docs/src/components/playground/TutorialPlayground.tsx
@@ -0,0 +1,197 @@
+/*!
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+import React from "react";
+
+import { PlaygroundWorkspace } from "./PlaygroundWorkspace";
+import { StepGuide } from "./StepGuide";
+import { modulesById } from "./data/modules";
+import type { ValidationPattern } from "./data/types";
+
+import "@site/src/css/playground.css";
+
+/**
+ * Path to the tutorial module index page (used for "Back to Tutorials" link).
+ */
+const MODULE_INDEX_PATH = "/docs/start/interactive-tutorial/";
+
+/**
+ * Runs validation patterns against code, stripping comments first.
+ * Returns false for any pattern that fails to compile as a regex.
+ */
+function runValidation(code: string, patterns: ValidationPattern[]): boolean[] {
+ const stripped = code
+ .replace(/\/\*[\s\S]*?\*\//g, "")
+ .replace(/\/\/.*$/gm, "");
+ return patterns.map((vp) => {
+ try {
+ const regex = new RegExp(vp.pattern, vp.flags ?? "s");
+ return regex.test(stripped);
+ } catch {
+ return false;
+ }
+ });
+}
+
+/**
+ * {@link TutorialPlayground} component props.
+ */
+export interface TutorialPlaygroundProps {
+ /**
+ * The module to render (e.g. "dice-roller" or "shared-tree-todo").
+ */
+ moduleId: string;
+}
+
+/**
+ * Interactive tutorial playground for a single module.
+ *
+ * @remarks
+ * Manages step navigation, code validation, and solution display for the
+ * given module. Module selection is handled at the page level via Docusaurus
+ * routing and the {@link ModuleSelector} component.
+ */
+export function TutorialPlayground({
+ moduleId,
+}: TutorialPlaygroundProps): React.ReactElement {
+ const [currentStepIndex, setCurrentStepIndex] = React.useState(0);
+ const [validationResults, setValidationResults] = React.useState([]);
+ const [showSolution, setShowSolution] = React.useState(false);
+ const [resetCounter, setResetCounter] = React.useState(0);
+
+ // Per-step saved code (user's own edits or last editor state)
+ const codeSnapshotsRef = React.useRef