diff --git a/site/content/tutorial/10-transitions/02-adding-parameters/app-a/App.svelte b/site/content/tutorial/10-transitions/02-adding-parameters-to-transitions/app-a/App.svelte
similarity index 100%
rename from site/content/tutorial/10-transitions/02-adding-parameters/app-a/App.svelte
rename to site/content/tutorial/10-transitions/02-adding-parameters-to-transitions/app-a/App.svelte
diff --git a/site/content/tutorial/10-transitions/02-adding-parameters/app-b/App.svelte b/site/content/tutorial/10-transitions/02-adding-parameters-to-transitions/app-b/App.svelte
similarity index 100%
rename from site/content/tutorial/10-transitions/02-adding-parameters/app-b/App.svelte
rename to site/content/tutorial/10-transitions/02-adding-parameters-to-transitions/app-b/App.svelte
diff --git a/site/content/tutorial/10-transitions/02-adding-parameters/text.md b/site/content/tutorial/10-transitions/02-adding-parameters-to-transitions/text.md
similarity index 100%
rename from site/content/tutorial/10-transitions/02-adding-parameters/text.md
rename to site/content/tutorial/10-transitions/02-adding-parameters-to-transitions/text.md
diff --git a/site/content/tutorial/11-animations/01-animate/app-a/App.svelte b/site/content/tutorial/11-animations/01-animate/app-a/App.svelte
new file mode 100644
index 0000000000..c46096c204
--- /dev/null
+++ b/site/content/tutorial/11-animations/01-animate/app-a/App.svelte
@@ -0,0 +1,146 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/site/content/tutorial/11-animations/01-animate/app-a/crossfade.js b/site/content/tutorial/11-animations/01-animate/app-a/crossfade.js
new file mode 100644
index 0000000000..e11e18b60e
--- /dev/null
+++ b/site/content/tutorial/11-animations/01-animate/app-a/crossfade.js
@@ -0,0 +1,65 @@
+import { quintOut } from 'svelte/easing';
+
+export default function crossfade({ send, receive, fallback }) {
+ let requested = new Map();
+ let provided = new Map();
+
+ function crossfade(from, node) {
+ const to = node.getBoundingClientRect();
+ const dx = from.left - to.left;
+ const dy = from.top - to.top;
+
+ const style = getComputedStyle(node);
+ const transform = style.transform === 'none' ? '' : style.transform;
+
+ return {
+ duration: 400,
+ easing: quintOut,
+ css: (t, u) => `
+ opacity: ${t};
+ transform: ${transform} translate(${u * dx}px,${u * dy}px);
+ `
+ };
+ }
+
+ return {
+ send(node, params) {
+ provided.set(params.key, {
+ rect: node.getBoundingClientRect()
+ });
+
+ return () => {
+ if (requested.has(params.key)) {
+ const { rect } = requested.get(params.key);
+ requested.delete(params.key);
+
+ return crossfade(rect, node);
+ }
+
+ // if the node is disappearing altogether
+ // (i.e. wasn't claimed by the other list)
+ // then we need to supply an outro
+ provided.delete(params.key);
+ return fallback(node, params);
+ };
+ },
+
+ receive(node, params) {
+ requested.set(params.key, {
+ rect: node.getBoundingClientRect()
+ });
+
+ return () => {
+ if (provided.has(params.key)) {
+ const { rect } = provided.get(params.key);
+ provided.delete(params.key);
+
+ return crossfade(rect, node);
+ }
+
+ requested.delete(params.key);
+ return fallback(node, params);
+ };
+ }
+ };
+}
\ No newline at end of file
diff --git a/site/content/tutorial/11-animations/01-animate/app-b/App.svelte b/site/content/tutorial/11-animations/01-animate/app-b/App.svelte
new file mode 100644
index 0000000000..c46096c204
--- /dev/null
+++ b/site/content/tutorial/11-animations/01-animate/app-b/App.svelte
@@ -0,0 +1,146 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/site/content/tutorial/11-animations/01-animate/app-b/crossfade.js b/site/content/tutorial/11-animations/01-animate/app-b/crossfade.js
new file mode 100644
index 0000000000..e11e18b60e
--- /dev/null
+++ b/site/content/tutorial/11-animations/01-animate/app-b/crossfade.js
@@ -0,0 +1,65 @@
+import { quintOut } from 'svelte/easing';
+
+export default function crossfade({ send, receive, fallback }) {
+ let requested = new Map();
+ let provided = new Map();
+
+ function crossfade(from, node) {
+ const to = node.getBoundingClientRect();
+ const dx = from.left - to.left;
+ const dy = from.top - to.top;
+
+ const style = getComputedStyle(node);
+ const transform = style.transform === 'none' ? '' : style.transform;
+
+ return {
+ duration: 400,
+ easing: quintOut,
+ css: (t, u) => `
+ opacity: ${t};
+ transform: ${transform} translate(${u * dx}px,${u * dy}px);
+ `
+ };
+ }
+
+ return {
+ send(node, params) {
+ provided.set(params.key, {
+ rect: node.getBoundingClientRect()
+ });
+
+ return () => {
+ if (requested.has(params.key)) {
+ const { rect } = requested.get(params.key);
+ requested.delete(params.key);
+
+ return crossfade(rect, node);
+ }
+
+ // if the node is disappearing altogether
+ // (i.e. wasn't claimed by the other list)
+ // then we need to supply an outro
+ provided.delete(params.key);
+ return fallback(node, params);
+ };
+ },
+
+ receive(node, params) {
+ requested.set(params.key, {
+ rect: node.getBoundingClientRect()
+ });
+
+ return () => {
+ if (provided.has(params.key)) {
+ const { rect } = provided.get(params.key);
+ provided.delete(params.key);
+
+ return crossfade(rect, node);
+ }
+
+ requested.delete(params.key);
+ return fallback(node, params);
+ };
+ }
+ };
+}
\ No newline at end of file
diff --git a/site/content/tutorial/11-animations/01-animate/text.md b/site/content/tutorial/11-animations/01-animate/text.md
new file mode 100644
index 0000000000..7b8268e37b
--- /dev/null
+++ b/site/content/tutorial/11-animations/01-animate/text.md
@@ -0,0 +1,5 @@
+---
+title: The animate directive
+---
+
+TODO fix https://github.com/sveltejs/svelte/issues/2159 before working on `animate`
\ No newline at end of file
diff --git a/site/content/tutorial/11-animations/meta.json b/site/content/tutorial/11-animations/meta.json
new file mode 100644
index 0000000000..c71301495e
--- /dev/null
+++ b/site/content/tutorial/11-animations/meta.json
@@ -0,0 +1,3 @@
+{
+ "title": "Animations"
+}
\ No newline at end of file
diff --git a/site/content/tutorial/12-actions/01-actions/app-a/App.svelte b/site/content/tutorial/12-actions/01-actions/app-a/App.svelte
new file mode 100644
index 0000000000..cbd1f81282
--- /dev/null
+++ b/site/content/tutorial/12-actions/01-actions/app-a/App.svelte
@@ -0,0 +1,47 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/site/content/tutorial/12-actions/01-actions/app-a/pannable.js b/site/content/tutorial/12-actions/01-actions/app-a/pannable.js
new file mode 100644
index 0000000000..332cd3e147
--- /dev/null
+++ b/site/content/tutorial/12-actions/01-actions/app-a/pannable.js
@@ -0,0 +1,9 @@
+export function pannable(node) {
+ // setup work goes here...
+
+ return {
+ destroy() {
+ // ...cleanup goes here
+ }
+ };
+}
\ No newline at end of file
diff --git a/site/content/tutorial/12-actions/01-actions/app-b/App.svelte b/site/content/tutorial/12-actions/01-actions/app-b/App.svelte
new file mode 100644
index 0000000000..1324277bac
--- /dev/null
+++ b/site/content/tutorial/12-actions/01-actions/app-b/App.svelte
@@ -0,0 +1,49 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/site/content/tutorial/12-actions/01-actions/app-b/pannable.js b/site/content/tutorial/12-actions/01-actions/app-b/pannable.js
new file mode 100644
index 0000000000..f7d15328be
--- /dev/null
+++ b/site/content/tutorial/12-actions/01-actions/app-b/pannable.js
@@ -0,0 +1,47 @@
+export function pannable(node) {
+ let x;
+ let y;
+
+ function handleMousedown(event) {
+ x = event.clientX;
+ y = event.clientY;
+
+ node.dispatchEvent(new CustomEvent('panstart', {
+ detail: { x, y }
+ }));
+
+ window.addEventListener('mousemove', handleMousemove);
+ window.addEventListener('mouseup', handleMouseup);
+ }
+
+ function handleMousemove(event) {
+ const dx = event.clientX - x;
+ const dy = event.clientY - y;
+ x = event.clientX;
+ y = event.clientY;
+
+ node.dispatchEvent(new CustomEvent('panmove', {
+ detail: { x, y, dx, dy }
+ }));
+ }
+
+ function handleMouseup(event) {
+ x = event.clientX;
+ y = event.clientY;
+
+ node.dispatchEvent(new CustomEvent('panend', {
+ detail: { x, y }
+ }));
+
+ window.removeEventListener('mousemove', handleMousemove);
+ window.removeEventListener('mouseup', handleMouseup);
+ }
+
+ node.addEventListener('mousedown', handleMousedown);
+
+ return {
+ destroy() {
+ node.removeEventListener('mousedown', handleMousedown);
+ }
+ };
+}
\ No newline at end of file
diff --git a/site/content/tutorial/12-actions/01-actions/text.md b/site/content/tutorial/12-actions/01-actions/text.md
new file mode 100644
index 0000000000..279dc3123f
--- /dev/null
+++ b/site/content/tutorial/12-actions/01-actions/text.md
@@ -0,0 +1,87 @@
+---
+title: The use directive
+---
+
+Actions are essentially element-level lifecycle functions. They're useful for things like:
+
+* interfacing with third-party libraries
+* lazy-loaded images
+* tooltips
+* adding custom event handlers
+
+In this app, we want to make the orange box 'pannable'. It has event handlers for the `panstart`, `panmove` and `panend` events, but these aren't native DOM events. We have to dispatch them ourselves. First, import the `pannable` function...
+
+```js
+import { pannable } from './pannable.js';
+```
+
+...then use it with the element:
+
+```html
+
+```
+
+Open the `pannable.js` file. Like transition functions, an action function receives a `node` and some optional parameters, and returns an action object. That object must have a `destroy` function, which is called when the element is unmounted.
+
+We want to fire `panstart` event when the user mouses down on the element, `panmove` events (with `dx` and `dy` properties showing how far the mouse moved) when they drag it, and `panend` events when they mouse up. One possible implementation looks like this:
+
+```js
+export function pannable(node) {
+ let x;
+ let y;
+
+ function handleMousedown(event) {
+ x = event.clientX;
+ y = event.clientY;
+
+ node.dispatchEvent(new CustomEvent('panstart', {
+ detail: { x, y }
+ }));
+
+ window.addEventListener('mousemove', handleMousemove);
+ window.addEventListener('mouseup', handleMouseup);
+ }
+
+ function handleMousemove(event) {
+ const dx = event.clientX - x;
+ const dy = event.clientY - y;
+ x = event.clientX;
+ y = event.clientY;
+
+ node.dispatchEvent(new CustomEvent('panmove', {
+ detail: { x, y, dx, dy }
+ }));
+ }
+
+ function handleMouseup(event) {
+ x = event.clientX;
+ y = event.clientY;
+
+ node.dispatchEvent(new CustomEvent('panend', {
+ detail: { x, y }
+ }));
+
+ window.removeEventListener('mousemove', handleMousemove);
+ window.removeEventListener('mouseup', handleMouseup);
+ }
+
+ node.addEventListener('mousedown', handleMousedown);
+
+ return {
+ destroy() {
+ node.removeEventListener('mousedown', handleMousedown);
+ }
+ };
+}
+```
+
+Update the `pannable` function and try moving the box around.
+
+> This implementation is for demonstration purposes — a more complete one would also consider touch events.
+
diff --git a/site/content/tutorial/12-actions/02-adding-parameters-to-actions/app-a/App.svelte b/site/content/tutorial/12-actions/02-adding-parameters-to-actions/app-a/App.svelte
new file mode 100644
index 0000000000..30404ce4c5
--- /dev/null
+++ b/site/content/tutorial/12-actions/02-adding-parameters-to-actions/app-a/App.svelte
@@ -0,0 +1 @@
+TODO
\ No newline at end of file
diff --git a/site/content/tutorial/12-actions/02-adding-parameters-to-actions/app-b/App.svelte b/site/content/tutorial/12-actions/02-adding-parameters-to-actions/app-b/App.svelte
new file mode 100644
index 0000000000..30404ce4c5
--- /dev/null
+++ b/site/content/tutorial/12-actions/02-adding-parameters-to-actions/app-b/App.svelte
@@ -0,0 +1 @@
+TODO
\ No newline at end of file
diff --git a/site/content/tutorial/12-actions/02-adding-parameters-to-actions/text.md b/site/content/tutorial/12-actions/02-adding-parameters-to-actions/text.md
new file mode 100644
index 0000000000..5631bebbc5
--- /dev/null
+++ b/site/content/tutorial/12-actions/02-adding-parameters-to-actions/text.md
@@ -0,0 +1,5 @@
+---
+title: Adding parameters
+---
+
+TODO example with Prism highlighting
\ No newline at end of file
diff --git a/site/content/tutorial/12-actions/meta.json b/site/content/tutorial/12-actions/meta.json
new file mode 100644
index 0000000000..a154610283
--- /dev/null
+++ b/site/content/tutorial/12-actions/meta.json
@@ -0,0 +1,3 @@
+{
+ "title": "Actions"
+}
\ No newline at end of file
diff --git a/site/content/tutorial/99-todo/99-todo/text.md b/site/content/tutorial/99-todo/99-todo/text.md
index 93cbdb6121..17c4cbb5e2 100644
--- a/site/content/tutorial/99-todo/99-todo/text.md
+++ b/site/content/tutorial/99-todo/99-todo/text.md
@@ -85,6 +85,7 @@ Maybe lifecycle should go first, since we're using `onMount` in the `this` demo?
* [ ] custom stores
* [ ] `bind:value={$foo}`
* [ ] `$foo += 1` (if we implement it)
+* [ ] Adapting Immer, Redux, Microstates, xstate etc
## Motion
@@ -119,7 +120,7 @@ Maybe lifecycle should go first, since we're using `onMount` in the `this` demo?
## use: directive
-* [ ] `use:foo`
+* [x] `use:foo`
* [ ] `use:foo={bar}`
## class: directive
diff --git a/site/src/routes/tutorial/[slug]/index.svelte b/site/src/routes/tutorial/[slug]/index.svelte
index 082c9a98bc..23222045d2 100644
--- a/site/src/routes/tutorial/[slug]/index.svelte
+++ b/site/src/routes/tutorial/[slug]/index.svelte
@@ -89,7 +89,7 @@
function handle_change(event) {
completed = event.detail.components.every((file, i) => {
const expected = chapter.app_b[i];
- return (
+ return expected && (
file.name === expected.name &&
file.type === expected.type &&
file.source === expected.source