diff --git a/.env b/.env
index 905d17ed1..e10a70f1c 100644
--- a/.env
+++ b/.env
@@ -1,6 +1,3 @@
VITE_VERSION=$npm_package_version
VITE_NAME=$npm_package_name
VITE_FULL_URL=/
-VITE_SANITY_PROJECT=9y5xmu9m
-VITE_SANITY_DATASET=production
-VITE_SANITY_PREVIEW_DATASET=production
\ No newline at end of file
diff --git a/content/de/grundlagen/ausgabe.md b/content/de/grundlagen/ausgabe.md
new file mode 100644
index 000000000..3488f74f5
--- /dev/null
+++ b/content/de/grundlagen/ausgabe.md
@@ -0,0 +1,32 @@
+---
+name: Ausgabe
+subtitle: Text und Werte in der Konsole anzeigen
+image: ../../images/ausgabe.svg
+---
+
+Mit `print` gibst du Text und Messwerte in der seriellen Konsole aus.
+
+## Text ausgeben
+
+Schreibe den Text in Anführungszeichen:
+
+```python
+print("Hallo senseBox!")
+```
+
+
+
+Die Ausgabe siehst du in der **Konsole** unterhalb des Editors, sobald die senseBox verbunden ist.
+
+## Werte ausgeben
+
+
+
+Du kannst auch Zahlen und Variablen ausgeben. Mehrere Werte trennst du mit Kommas:
+
+```python
+temperatur = 21.5
+print("Temperatur:", temperatur)
+```
+
+Mehr über Messwerte erfährst du unter [Temperatur](doc:sensoren/temperatur).
diff --git a/content/de/grundlagen/schleifen.md b/content/de/grundlagen/schleifen.md
new file mode 100644
index 000000000..8793cf194
--- /dev/null
+++ b/content/de/grundlagen/schleifen.md
@@ -0,0 +1,43 @@
+---
+name: Schleifen
+subtitle: Code wiederholen
+image: ../../images/schleifen.svg
+---
+
+Schleifen führen Code mehrfach aus.
+
+## Endlosschleife
+
+Eine `while True`-Schleife läuft für immer. So misst die senseBox z. B. dauerhaft:
+
+```python
+import time
+
+while True:
+ print("Messung ...")
+ time.sleep(1)
+```
+
+## Zählschleife
+
+
+
+### 3 Mal
+
+```python
+for i in range(3):
+ print(i)
+```
+
+### 10 Mal
+
+
+
+```python
+for i in range(10):
+ print(i)
+```
+
+
+
+`range(n)` zählt von 0 bis n - 1.
diff --git a/content/de/projekte/wetterstation.md b/content/de/projekte/wetterstation.md
new file mode 100644
index 000000000..692261f02
--- /dev/null
+++ b/content/de/projekte/wetterstation.md
@@ -0,0 +1,22 @@
+---
+name: Wetterstation
+image: ../../images/wetterstation.svg
+---
+
+Baue eine einfache Wetterstation, die regelmäßig die Temperatur ausgibt.
+
+
+
+### So funktioniert's
+
+1. Schließe den [Temperatursensor](doc:sensoren/temperatur) an.
+2. Öffne den Code mit dem Button unten.
+3. Übertrage ihn auf die senseBox.
+
+```python
+import time
+
+while True:
+ print("Temperatur: 21.5")
+ time.sleep(10)
+```
diff --git a/content/de/sensoren/temperatur.md b/content/de/sensoren/temperatur.md
new file mode 100644
index 000000000..e1d0e2b85
--- /dev/null
+++ b/content/de/sensoren/temperatur.md
@@ -0,0 +1,30 @@
+---
+name: Temperatur
+subtitle: Temperatur und Luftfeuchtigkeit messen
+image: ../../images/temperatur.svg
+---
+
+Der Temperatur- und Luftfeuchtigkeitssensor wird über I²C angeschlossen.
+
+## Temperatur messen
+
+
+
+Dieses Beispiel liest den Sensor jede Sekunde aus:
+
+```python
+import time
+import board
+
+i2c = board.I2C()
+
+while True:
+ # Sensor auslesen und Wert ausgeben
+ print("Temperatur messen ...")
+ time.sleep(1)
+```
+
+
+
+Die Werte kannst du mit [print](doc:grundlagen/ausgabe) ausgeben.
+Weitere Informationen findest du in der [senseBox-Dokumentation](https://docs.sensebox.de).
diff --git a/content/images/ausgabe.svg b/content/images/ausgabe.svg
new file mode 100644
index 000000000..4916c0b50
--- /dev/null
+++ b/content/images/ausgabe.svg
@@ -0,0 +1 @@
+
diff --git a/content/images/schleifen.svg b/content/images/schleifen.svg
new file mode 100644
index 000000000..ff42cadcc
--- /dev/null
+++ b/content/images/schleifen.svg
@@ -0,0 +1 @@
+
diff --git a/content/images/temperatur.svg b/content/images/temperatur.svg
new file mode 100644
index 000000000..5099468c8
--- /dev/null
+++ b/content/images/temperatur.svg
@@ -0,0 +1 @@
+
diff --git a/content/images/wetterstation.svg b/content/images/wetterstation.svg
new file mode 100644
index 000000000..d04c11f1d
--- /dev/null
+++ b/content/images/wetterstation.svg
@@ -0,0 +1 @@
+
diff --git a/content/mapping.json b/content/mapping.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/content/mapping.json
@@ -0,0 +1 @@
+{}
diff --git a/content/sidebar.json b/content/sidebar.json
new file mode 100644
index 000000000..8929d2067
--- /dev/null
+++ b/content/sidebar.json
@@ -0,0 +1,33 @@
+{
+ "fallbackLanguage": "de",
+ "sections": [
+ {
+ "id": "grundlagen",
+ "layout": "reference",
+ "icon": "VscLibrary",
+ "title": { "de": "Grundlagen", "en": "Basics" },
+ "description": {
+ "de": "Die wichtigsten Python-Bausteine für die senseBox.",
+ "en": "The most important Python building blocks for the senseBox."
+ },
+ "order": ["ausgabe", "schleifen"]
+ },
+ {
+ "id": "sensoren",
+ "layout": "reference",
+ "icon": "RiSensorLine",
+ "title": { "de": "Sensoren", "en": "Sensors" }
+ },
+ {
+ "id": "projekte",
+ "layout": "cards",
+ "icon": "RiLightbulbFlashLine",
+ "title": { "de": "Projektideen", "en": "Project ideas" },
+ "description": {
+ "de": "Fertige Projekte zum Ausprobieren und Weiterentwickeln.",
+ "en": "Complete projects to try out and build on."
+ }
+ },
+ { "builtin": "api" }
+ ]
+}
diff --git a/docs/analytics-events.md b/docs/analytics-events.md
index 0e0eae1f1..a9c19800f 100644
--- a/docs/analytics-events.md
+++ b/docs/analytics-events.md
@@ -157,7 +157,7 @@ back/forward and typed URLs don't fire it (they are `page_view` only).
| Param | Values |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `via` | `user` (clicked in the sidebar), `search` (picked a search result), `code` (from a code hover/help link), `simulator` (from the simulator) |
-| `surface` | `reference` / `ideas` / `api` |
+| `surface` | The sidebar section id from `content/sidebar.json` (e.g. `grundlagen`) or `api` |
| `id` | The page slug, e.g. `buttons`; absent when navigating to a tab root |
### `docs_search`
@@ -179,8 +179,9 @@ success rate.
| `code_copy` | `surface`, `id` | User used the snippet's copy button |
| `code_paste` | `surface`, `id` | A copied snippet was pasted into the editor (via our own clipboard context) |
-- `surface` — `reference` / `ideas` / `api`.
-- `id` — the documentation slug for Reference / Ideas, or the fully qualified
+- `surface` — `reference` (snippets in a section with the reference layout),
+ `ideas` (snippets in a section with the cards layout) or `api`.
+- `id` — the documentation slug for sections, or the fully qualified
name for API (e.g. `microbit.display.scroll`). Bounded cardinality, but
large; expect `(other)` in standard reports and use Explorations.
diff --git a/docs/content.md b/docs/content.md
new file mode 100644
index 000000000..ac532de28
--- /dev/null
+++ b/docs/content.md
@@ -0,0 +1,185 @@
+# Documentation content
+
+The documentation tabs in the sidebar and everything in them are read from the
+`content/` folder. Content is written in Markdown and bundled with the app at
+build time. To change it, edit the files and redeploy.
+
+```
+content/
+ sidebar.json # which tabs exist, in which order
+ mapping.json # optional: API entries → documentation (editor "Help" links)
+ images/ # images, referenced relatively from Markdown
+ de/ # one folder per language
+ grundlagen/ # one folder per section (the section id)
+ ausgabe.md # reference layout: one file per topic
+ schleifen.md
+ projekte/
+ wetterstation.md # cards layout: one file per card
+ en/
+ grundlagen/
+ ausgabe.md # translations use the same file names
+```
+
+## Sidebar (`content/sidebar.json`)
+
+```json
+{
+ "fallbackLanguage": "de",
+ "sections": [
+ {
+ "id": "grundlagen",
+ "layout": "reference",
+ "icon": "VscLibrary",
+ "title": { "de": "Grundlagen", "en": "Basics" },
+ "description": { "de": "…", "en": "…" },
+ "order": ["ausgabe", "schleifen"]
+ },
+ {
+ "id": "projekte",
+ "layout": "cards",
+ "icon": "RiLightbulbFlashLine",
+ "title": "Projekte"
+ },
+ { "builtin": "api" }
+ ]
+}
+```
+
+- Each entry in `sections` is a tab, in this order. The **Project** (files) tab
+ always comes last.
+- **`id`** is the folder name and appears in URLs (`/grundlagen/ausgabe`). Use
+ lowercase letters, digits and `-`. `api` and `project` are reserved.
+- **`layout`**:
+ - `reference`: a list of topics. Each topic has entries with code you can
+ drag into the editor.
+ - `cards`: a grid of cards. Each card opens a page whose code can be opened
+ as a project.
+- **`icon`**: one of the names in `src/documentation/sections/icons.ts`. To
+ use another [react-icons](https://react-icons.github.io/react-icons/) icon,
+ add it there.
+- **`title`** / **`description`**: a string, or an object with a value per
+ language.
+- **`order`** (optional): file names without `.md`. Files that aren't listed
+ come after the listed ones, in alphabetical order.
+- **`{ "builtin": "api" }`** shows the API tab, which is generated from the
+ type stubs. Leave it out to hide the tab and the API search results.
+
+### Languages
+
+For each file, the editor uses the version in the user's language
+(`content//…`). If there isn't one, it tries the base language (e.g.
+`pt` for `pt-BR`), then `fallbackLanguage`. Partial translations are fine.
+
+## Reference topics
+
+One Markdown file per topic:
+
+````markdown
+---
+name: Schleifen
+subtitle: Code wiederholen
+image: ../../images/schleifen.svg
+compatibility: [microbitV2]
+---
+
+Introduction shown at the top of the topic.
+
+## Endlosschleife
+
+
+
+Text shown before the code. When an entry has code, this text collapses to
+one line until the user clicks "More".
+
+```python
+while True:
+ pass
+```
+
+
+
+Only shown after clicking "More".
+
+## Zählschleife
+
+
+
+### 3 Mal
+
+```python
+for i in range(3): print(i)
+```
+
+### 10 Mal
+
+```python
+for i in range(10): print(i)
+```
+````
+
+- **Front matter**:
+ - `name`: defaults to the file name.
+ - `slug`: defaults to the file name.
+ - `subtitle`
+ - `image`: the topic icon.
+ - `compatibility`: optional.
+- Everything before the first `##` is the **introduction**.
+- Each **`##` heading starts an entry**. The entry's slug is the heading,
+ lower-cased and with umlauts replaced, e.g. `zaehlschleife`.
+- **Directives** are HTML comments placed after an entry heading:
+ - `` sets the entry's slug. Placed after a `###`
+ alternative heading, it sets that alternative's slug instead.
+ - ``
+ - ``: the `###` headings that follow become
+ choices in a dropdown labelled "Label".
+ - ``: the content that follows is shown only when expanded.
+ - Several directives can share one comment:
+ ``.
+- Slugs must be unique within a section. A duplicate entry slug is
+ automatically prefixed with the topic slug.
+
+## Cards
+
+One Markdown file per card:
+
+```markdown
+---
+name: Wetterstation
+image: ../../images/wetterstation.svg
+---
+
+Page content, including `###` headings, images and code.
+```
+
+## Markdown support
+
+| Markdown | Result |
+| ------------------------------------ | ----------------------------------------------------------------------------------------------------- |
+| `**bold**`, `*italic*`, `` `code` `` | Inline formatting |
+| Lists (`-`, `1.`), `> quote`, `###` | Lists, quotes, headings |
+| ` ```python ` (or no language) | Code snippet: drag/copy (reference), open (cards) |
+| ` ```text ` (any other language) | Plain code text |
+| `` | Image (path relative to the `.md` file, or `/images/x.png` for `content/images/x.png`, or a full URL) |
+| `[text](https://…)` | External link (opens a new tab) |
+| `[text](doc:section/slug)` | Link to a topic, entry or card in a section |
+| `[text](doc:slug)` | Same, searching all sections for the slug |
+| `[text](api:time.sleep)` | Link to an entry in the API tab |
+
+Inline HTML and tables aren't supported.
+
+## Help links from the editor (`content/mapping.json`)
+
+The editor's autocomplete and signature help show a "Help" link when an API
+entry is mapped to documentation:
+
+```json
+{
+ "time": {
+ "sleep": "grundlagen/warten",
+ "monotonic": { "reference": "grundlagen/zeit", "alternative": "ms" }
+ }
+}
+```
+
+The target is `section/slug`. The `alternative` field preselects a choice in an
+entry that has `@alternatives`.
diff --git a/docs/tech-overview.md b/docs/tech-overview.md
index 6f42d37ff..41bd23c32 100644
--- a/docs/tech-overview.md
+++ b/docs/tech-overview.md
@@ -32,11 +32,7 @@ To produce the correct data format to flash a micro:bit, we use the Foundation's
## The sidebar and educational content
-The Reference and Ideas sidebar tabs show educational content that is managed in the Micro:bit Educational Foundation's content management system (Sanity CMS). This content is not Open Source. The content is sourced live from the CMS APIs. It will work for local development but non-Foundation deployments will see CORS errors.
-
-You can substitute your own content by setting `VITE_SANITY_PROJECT` and `VITE_SANITY_DATASET` environment variables, overriding the defaults in `.env`. The schemas we use for Sanity CMS are [packaged as a plugin in this GitHub project](https://github.com/microbit-foundation/sanity-plugin-python-editor-v3/).
-
-We also plan to explore adding Markdown documentation support as an alternative to Sanity CMS. You can follow the discussion on [#1160](https://github.com/microbit-foundation/python-editor-v3/issues/1160).
+The documentation tabs in the sidebar are configured in `content/sidebar.json` and their content is written as Markdown files in the `content/` folder. The content is bundled with the app at build time; there is no CMS. See [content.md](./content.md) for the format.
The API tab shows detailed documentation of the MicroPython API for users who need more detail than the curated content in the Reference tab provides. The API tab content is generated at runtime from the bundled type stubs for MicroPython. We do this using an enhancement to the Foundation's fork of Pyright. For more details see [Python code intelligence](#python-code-intelligence).
diff --git a/package-lock.json b/package-lock.json
index 69a652d28..fba301887 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,7 +22,6 @@
"@microbit/ui": "^0.5.0",
"@microbit/ui-patterns": "^0.7.0",
"@sanity/block-content-to-react": "^3.0.0",
- "@sanity/image-url": "^1.0.1",
"@sentry/browser": "^10.71.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^14.0.0",
@@ -4561,15 +4560,6 @@
"integrity": "sha512-H/G/WA9S22TXcXST52CIiTsHx3S2hH0gvK7LnI5w76vfKS0obnDPh8jrPg4xeNRYGPuV9MHYRlyERGpRGoo4Qw==",
"license": "MIT"
},
- "node_modules/@sanity/image-url": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@sanity/image-url/-/image-url-1.2.0.tgz",
- "integrity": "sha512-pYRhti+lDi22it+npWXkEGuYyzbXJLF+d0TYLiyWbKu46JHhYhTDKkp6zmGu4YKF5cXUjT6pnUjFsaf2vlB9nQ==",
- "license": "MIT",
- "engines": {
- "node": ">=10.0.0"
- }
- },
"node_modules/@sentry/browser": {
"version": "10.76.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.76.0.tgz",
diff --git a/package.json b/package.json
index a78da2b9f..81f4a1e87 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,6 @@
"@microbit/ui": "^0.5.0",
"@microbit/ui-patterns": "^0.7.0",
"@sanity/block-content-to-react": "^3.0.0",
- "@sanity/image-url": "^1.0.1",
"@sentry/browser": "^10.71.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^14.0.0",
diff --git a/src/common/documentation-styles.ts b/src/common/documentation-styles.ts
index b11e869b9..04c161c25 100644
--- a/src/common/documentation-styles.ts
+++ b/src/common/documentation-styles.ts
@@ -5,26 +5,6 @@
*/
import { css } from "styled-system/css";
-// Follows the pattern used by the AspectRatio component which uses a
-// percentage-padding spacer rather than the aspect-ratio property, which is
-// not supported in Safari 14. Only apply when --aspect-ratio-padding is set or
-// the wrapper collapses to zero height.
-export const runtimeAspectRatioClass = css({
- position: "relative",
- _before: {
- content: '""',
- display: "block",
- height: 0,
- paddingBottom: "var(--aspect-ratio-padding)",
- },
- "& > *": {
- position: "absolute",
- inset: 0,
- width: "100%",
- height: "100%",
- },
-});
-
// A class rather than a style object: Panda can only statically extract
// styles written literally at the definition site, not objects spread into
// a css prop elsewhere.
diff --git a/src/common/imageUrlBuilder.ts b/src/common/imageUrlBuilder.ts
deleted file mode 100644
index e02992b84..000000000
--- a/src/common/imageUrlBuilder.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * (c) 2022, Micro:bit Educational Foundation and contributors
- *
- * SPDX-License-Identifier: MIT
- */
-
-import unconfiguredImageUrlBuilder from "@sanity/image-url";
-import { dataset, project } from "./sanity";
-
-export const defaultQuality = 80;
-
-export const imageUrlBuilder = unconfiguredImageUrlBuilder()
- // Hardcoded for now as there's no practical alternative.
- .projectId(project)
- .dataset(dataset)
- .auto("format")
- .dpr(window.devicePixelRatio ?? 1)
- .quality(defaultQuality);
-
-/**
- * Percentage padding-bottom matching the image's aspect ratio, read from the
- * dimensions embedded in a Sanity image reference. Used to reserve an image's
- * height before it loads via a padding spacer rather than the aspect-ratio
- * property, which Safari 14 does not support.
- */
-export const getAspectRatioPadding = (imageRef: string): string | undefined => {
- const dimensionsArr = imageRef.match(/\d+x\d+/g);
- if (!dimensionsArr) {
- return undefined;
- }
- const dimensions = dimensionsArr.join().split("x");
- const [width, height] = dimensions.map((n: string) => Number(n));
- return `${(height / width) * 100}%`;
-};
diff --git a/src/common/portable-text.ts b/src/common/portable-text.ts
new file mode 100644
index 000000000..25e060750
--- /dev/null
+++ b/src/common/portable-text.ts
@@ -0,0 +1,43 @@
+/**
+ * Portable Text types used for documentation content.
+ *
+ * Content is authored as Markdown in the content/ folder and converted to
+ * this structure at load time so the existing renderer, collapse handling
+ * and search indexing can be shared.
+ *
+ * (c) 2021-2026, Micro:bit Educational Foundation and contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+export interface PortableTextBlock {
+ _type: "block";
+ _key: string;
+ // Partial/lax modelling. We pass this straight to the rendering API.
+ children: any;
+ markDefs: any;
+ style: string;
+ listItem?: "bullet" | "number";
+ level?: number;
+}
+
+export type PortableText = Array<
+ PortableTextBlock | { _type: string; children?: any; [other: string]: any }
+>;
+
+/**
+ * Common image type.
+ */
+export interface SimpleImage {
+ _type: "simpleImage";
+ alt?: string;
+ /**
+ * The resolved image URL.
+ */
+ src: string;
+}
+
+export interface Slug {
+ _type: "slug";
+ current: string;
+}
diff --git a/src/common/sanity.ts b/src/common/sanity.ts
deleted file mode 100644
index bfc15f952..000000000
--- a/src/common/sanity.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-/**
- * Common sanity types.
- */
-
-import { flags } from "../flags";
-
-export interface PortableTextBlock {
- _type: "block";
- _key: string;
- // Partial/lax modelling. We pass this straight to Sanity's rendering API.
- children: any;
- markDefs: any;
- style: string;
-}
-
-export type PortableText = Array<
- PortableTextBlock | { _type: string; children?: any; [other: string]: any }
->;
-
-/**
- * Common image type.
- */
-export interface SimpleImage {
- _type: "simpleImage";
- alt?: string;
- // The Sanity image asset.
- asset: any;
-}
-
-/**
- * Sanity's slug type.
- */
-export interface Slug {
- _type: "slug";
- current: string;
-}
-
-/**
- * Query CMS content preferring the given language but
- * falling back to "en" if it is not present.
- *
- * @param languageId The preferred language.
- * @param adaptContent Validates and converts the result.
- * @returns The content.
- */
-export const fetchContent = async (
- languageId: string,
- query: (languageId: string) => string,
- adaptContent: (result: any) => T | undefined
-): Promise => {
- try {
- const preferred = adaptContent(
- await fetchContentInternal(query(sanityLanguageId(languageId)))
- );
- if (preferred) {
- return preferred;
- }
- } catch {
- // Fall through to fallback without crashing if user is offline.
- }
- const fallback = adaptContent(await fetchContentInternal(query("en")));
- if (!fallback) {
- throw new Error("English content must exist");
- }
- return fallback;
-};
-
-const fetchContentInternal = async (query: string): Promise => {
- const response = await fetch(queryUrl(query));
- if (response.ok) {
- const { result } = await response.json();
- if (!result) {
- throw new Error("Unexpected response format");
- }
- return result;
- }
- throw new Error("Error fetching content: " + response.status);
-};
-
-export const sanityLanguageId = (locale: string): string => {
- if (!locale) {
- return "";
- }
- if (locale && !locale.match(/^[A-Za-z-]+$/g)) {
- throw new Error(`Invalid language id: ${locale}`);
- }
- const parts = locale.split("-");
- if (parts.length !== 2) {
- return locale;
- }
- return `${parts[0]}-${parts[1].toUpperCase()}`;
-};
-
-export const project = import.meta.env.VITE_SANITY_PROJECT;
-export const dataset = flags.cmsPreview
- ? import.meta.env.VITE_SANITY_PREVIEW_DATASET
- : import.meta.env.VITE_SANITY_DATASET;
-
-const queryUrl = (query: string): string => {
- return `https://${project}.apicdn.sanity.io/v1/data/query/${dataset}?query=${encodeURIComponent(
- query
- )}`;
-};
diff --git a/src/documentation/DocsSectionArea.tsx b/src/documentation/DocsSectionArea.tsx
new file mode 100644
index 000000000..ed85b47bb
--- /dev/null
+++ b/src/documentation/DocsSectionArea.tsx
@@ -0,0 +1,47 @@
+/**
+ * (c) 2021-2026, Micro:bit Educational Foundation and contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+import { Text } from "@microbit/ui";
+import { FormattedMessage } from "react-intl";
+import Spinner from "../common/Spinner";
+import { useDocumentation } from "./documentation-hooks";
+import IdeasDocumentation from "./ideas/IdeasDocumentation";
+import ReferenceToolkit from "./reference/ReferenceDocumentation";
+import { DocsSectionConfig } from "./sections/config";
+
+interface DocsSectionAreaProps {
+ section: DocsSectionConfig;
+}
+
+/**
+ * A sidebar tab for a documentation section from content/sidebar.json.
+ */
+const DocsSectionArea = ({ section }: DocsSectionAreaProps) => {
+ const { sections } = useDocumentation();
+ switch (sections.status) {
+ case "loading":
+ return ;
+ case "error":
+ return (
+
+
+
+ );
+ case "ok": {
+ const content = sections.content[section.id];
+ if (content?.layout === "cards") {
+ return ;
+ }
+ if (content?.layout === "reference") {
+ return ;
+ }
+ return null;
+ }
+ default:
+ throw new Error();
+ }
+};
+
+export default DocsSectionArea;
diff --git a/src/documentation/IdeasArea.tsx b/src/documentation/IdeasArea.tsx
deleted file mode 100644
index cd00fcf49..000000000
--- a/src/documentation/IdeasArea.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * (c) 2021-2022, Micro:bit Educational Foundation and contributors
- *
- * SPDX-License-Identifier: MIT
- */
-import { Text } from "@microbit/ui";
-import { FormattedMessage } from "react-intl";
-import IdeasDocumentation from "./ideas/IdeasDocumentation";
-import Spinner from "../common/Spinner";
-import { useDocumentation } from "./documentation-hooks";
-
-const IdeasArea = () => {
- const { ideas } = useDocumentation();
- switch (ideas.status) {
- case "loading":
- return ;
- case "error":
- return (
-
-
-
- );
- case "ok":
- return ;
- default:
- throw new Error();
- }
-};
-
-export default IdeasArea;
diff --git a/src/documentation/ReferenceArea.tsx b/src/documentation/ReferenceArea.tsx
deleted file mode 100644
index 55a21ba8c..000000000
--- a/src/documentation/ReferenceArea.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * (c) 2021, Micro:bit Educational Foundation and contributors
- *
- * SPDX-License-Identifier: MIT
- */
-import { Text } from "@microbit/ui";
-import { FormattedMessage } from "react-intl";
-import ReferenceToolkit from "./reference/ReferenceDocumentation";
-import Spinner from "../common/Spinner";
-import { useDocumentation } from "./documentation-hooks";
-
-const ReferenceArea = () => {
- const { reference } = useDocumentation();
- switch (reference.status) {
- case "loading":
- return ;
- case "error":
- return (
-
-
-
- );
- case "ok":
- return ;
- default:
- throw new Error();
- }
-};
-
-export default ReferenceArea;
diff --git a/src/documentation/api/ApiDocumentation.tsx b/src/documentation/api/ApiDocumentation.tsx
index 4bb5ba7e3..2820366eb 100644
--- a/src/documentation/api/ApiDocumentation.tsx
+++ b/src/documentation/api/ApiDocumentation.tsx
@@ -14,6 +14,7 @@ import HeadedScrollablePanel from "../../common/HeadedScrollablePanel";
import { splitDocString } from "../../editor/codemirror/language-server/docstrings";
import { ApiDocsEntry, ApiDocsResponse } from "../../language-server/apidocs";
import { Anchor, useRouterTabSlug, useRouterState } from "../../router-hooks";
+import { docsSections } from "../sections/config";
import DocString from "../common/DocString";
import { useAnimationDirection } from "../common/documentation-animation-hooks";
import DocumentationBreadcrumbHeading from "../common/DocumentationBreadcrumbHeading";
@@ -66,7 +67,7 @@ const ActiveLevel = ({
const handleReferenceLink = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
- setParams({ tab: "reference" });
+ setParams({ tab: referenceSectionId });
},
[setParams]
);
@@ -130,6 +131,10 @@ const ActiveLevel = ({
);
};
+const referenceSectionId = docsSections.find(
+ (s) => s.layout === "reference"
+)?.id;
+
interface ShortModuleDescriptionProps {
value: ApiDocsEntry;
as?: "div" | "span";
diff --git a/src/documentation/common/DocumentationBreadcrumbHeading.tsx b/src/documentation/common/DocumentationBreadcrumbHeading.tsx
index 0dd8dbdc0..63b2f3494 100644
--- a/src/documentation/common/DocumentationBreadcrumbHeading.tsx
+++ b/src/documentation/common/DocumentationBreadcrumbHeading.tsx
@@ -7,7 +7,7 @@ import { Button, Collapse, Text } from "@microbit/ui";
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
import { RiArrowLeftSFill } from "react-icons/ri";
import { Box, HStack, Stack, VStack } from "styled-system/jsx";
-import { SimpleImage } from "../../common/sanity";
+import { SimpleImage } from "../../common/portable-text";
import { useScrollablePanelAncestor } from "../../common/ScrollablePanel";
import { useResizeObserverContentRect } from "../../common/use-resize-observer";
import DocumentationIcon from "./DocumentationIcon";
diff --git a/src/documentation/common/DocumentationContent.test.tsx b/src/documentation/common/DocumentationContent.test.tsx
index 906054d77..6e437b6df 100644
--- a/src/documentation/common/DocumentationContent.test.tsx
+++ b/src/documentation/common/DocumentationContent.test.tsx
@@ -6,7 +6,7 @@
* @vitest-environment jsdom
*/
import { render } from "@testing-library/react";
-import { PortableText } from "../../common/sanity";
+import { PortableText } from "../../common/portable-text";
import DocumentationContent from "./DocumentationContent";
describe("DocumentationContent", () => {
@@ -50,15 +50,12 @@ describe("DocumentationContent", () => {
{
_type: "simpleImage",
alt: "micro:bit showing X axis going across the front, Y axis going down and up, Z axis going back to front",
- asset: {
- _ref: "image-9fccaf51a164fedc98662188593de19bfb9be8ad-435x512-png",
- _type: "reference",
- },
+ src: "/content/images/axes.png",
},
];
const view = render();
expect(view.container.innerHTML).toMatchInlineSnapshot(
- `"