From c1864854e244ef4ff590221814eb1c45f068b2eb Mon Sep 17 00:00:00 2001 From: Robert Knight Date: Tue, 20 Oct 2026 11:51:25 +0100 Subject: [PATCH 1/8] Open ideas from a dialog, with a guide beside the project Idea cards on the home page now open a dialog with the idea's text and its code, read-only. Create makes a new project with the idea's code in main.py, through the editor route's action, and opens it on the Guide. Projects record the idea they were created from (ProjectMeta.ideaSlug, kept by duplicates), and those projects get a Guide tab first in the sidebar: the idea's content without its code. The Ideas tab is no longer in the sidebar; its components are left for now. --- docs/analytics-events.md | 3 +- lang/ui.en.json | 6 +- src/common/AreaHeading.tsx | 4 +- src/documentation/GuideArea.tsx | 58 ++++++++++++ src/documentation/ideas/IdeaGuideContent.tsx | 69 +++++++++++++++ src/documentation/ideas/idea-code.ts | 19 ++++ src/e2e/app.ts | 6 +- src/e2e/documentation.test.ts | 15 ---- src/e2e/home.test.ts | 23 +++++ src/fs/projects-db.ts | 2 + src/pages/HomePage.tsx | 19 +++- src/pages/IdeaDialog.tsx | 92 ++++++++++++++++++++ src/pages/project-page-actions.tsx | 82 ++++++++++++++--- src/pages/resource-cards.tsx | 15 +--- src/project/projects-hooks.tsx | 16 ++++ src/project/projects.test.ts | 36 ++++++++ src/project/projects.ts | 34 ++++++-- src/router-hooks.tsx | 10 ++- src/workbench/SideBar.tsx | 29 +++--- 19 files changed, 470 insertions(+), 68 deletions(-) create mode 100644 src/documentation/GuideArea.tsx create mode 100644 src/documentation/ideas/IdeaGuideContent.tsx create mode 100644 src/documentation/ideas/idea-code.ts create mode 100644 src/pages/IdeaDialog.tsx diff --git a/docs/analytics-events.md b/docs/analytics-events.md index 857b98e3d..f6db65279 100644 --- a/docs/analytics-events.md +++ b/docs/analytics-events.md @@ -181,7 +181,8 @@ Projects page only. Fires when the user changes the sort field or direction. ### `idea_open` -User opened an idea from the documentation as a new project. +User opened an idea as a new project, from the home page idea dialog or the +documentation. | Param | Values | | ----- | -------------------------- | diff --git a/lang/ui.en.json b/lang/ui.en.json index c352d45b2..494fde5e7 100644 --- a/lang/ui.en.json +++ b/lang/ui.en.json @@ -245,7 +245,7 @@ }, "create-project-action": { "defaultMessage": "Create", - "description": "Confirm button of the dialog naming a new project" + "description": "Button creating a new project: confirms the dialog naming a new project, and creates one from the home page idea dialog" }, "create-python": { "defaultMessage": "Create a new Python file in this project", @@ -391,6 +391,10 @@ "defaultMessage": "For more support, see the Python Editor guide", "description": "Text with an external link to support/help content" }, + "guide-tab": { + "defaultMessage": "Guide", + "description": "Sidebar tab button text. Tab shows the guide for the idea the project was opened from." + }, "help": { "defaultMessage": "Help", "description": "Help menu label" diff --git a/src/common/AreaHeading.tsx b/src/common/AreaHeading.tsx index 63b9a9904..a4f00a182 100644 --- a/src/common/AreaHeading.tsx +++ b/src/common/AreaHeading.tsx @@ -9,7 +9,7 @@ import { Box } from "styled-system/jsx"; interface AreaHeadingProps { name: string; - description: ReactNode; + description?: ReactNode; } const AreaHeading = ({ name, description }: AreaHeadingProps) => ( @@ -17,7 +17,7 @@ const AreaHeading = ({ name, description }: AreaHeadingProps) => ( {name} - {description} + {description !== undefined && {description}} ); diff --git a/src/documentation/GuideArea.tsx b/src/documentation/GuideArea.tsx new file mode 100644 index 000000000..ad50c7480 --- /dev/null +++ b/src/documentation/GuideArea.tsx @@ -0,0 +1,58 @@ +/** + * (c) 2026, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { Text } from "@microbit/ui"; +import { FormattedMessage } from "react-intl"; +import { Stack } from "styled-system/jsx"; +import AreaHeading from "../common/AreaHeading"; +import { docStylesClass } from "../common/documentation-styles"; +import HeadedScrollablePanel from "../common/HeadedScrollablePanel"; +import Spinner from "../common/Spinner"; +import { useOpenIdeaSlug } from "../project/projects-hooks"; +import { useDocumentation } from "./documentation-hooks"; +import IdeaGuideContent from "./ideas/IdeaGuideContent"; +import { Idea } from "./ideas/model"; + +/** + * The guide for the idea the open project came from: its content without + * the program, which is in the editor. + */ +const GuideArea = () => { + const slug = useOpenIdeaSlug(); + const { ideas } = useDocumentation(); + if (ideas.status === "loading") { + return ; + } + const idea = + ideas.status === "ok" + ? ideas.content.find((idea) => idea.slug.current === slug) + : undefined; + if (!idea) { + return ( + + + + ); + } + return ; +}; + +const Guide = ({ idea }: { idea: Idea }) => ( + }> + + + + +); + +export default GuideArea; diff --git a/src/documentation/ideas/IdeaGuideContent.tsx b/src/documentation/ideas/IdeaGuideContent.tsx new file mode 100644 index 000000000..6db314d33 --- /dev/null +++ b/src/documentation/ideas/IdeaGuideContent.tsx @@ -0,0 +1,69 @@ +/** + * (c) 2026, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { CSSProperties } from "react"; +import { Box } from "styled-system/jsx"; +import { runtimeAspectRatioClass } from "../../common/documentation-styles"; +import { + getAspectRatioPadding, + imageUrlBuilder, +} from "../../common/imageUrlBuilder"; +import DocumentationContent, { + DocumentationContextProvider, +} from "../common/DocumentationContent"; +import ImageWithFallback from "../common/ImageWithFallback"; +import OfflineImageFallback from "../OfflineImageFallback"; +import { withoutCode } from "./idea-code"; +import { Idea } from "./model"; + +interface IdeaGuideContentProps { + idea: Idea; + /** The idea's image above its content; the guide panel shows it. */ + showImage?: boolean; +} + +/** + * An idea's content without its program, for the guide panel and the home + * page's idea dialog. Callers supply the surrounding stack and + * docStylesClass. + */ +const IdeaGuideContent = ({ + idea, + showImage = true, +}: IdeaGuideContentProps) => { + // Runtime value derived from the image reference. + const imagePadding = idea.image + ? getAspectRatioPadding(idea.image.asset._ref) + : undefined; + return ( + <> + {showImage && idea.image && ( + + } + alt="" + borderTopRadius="lg" + /> + + )} + + + + + ); +}; + +export default IdeaGuideContent; diff --git a/src/documentation/ideas/idea-code.ts b/src/documentation/ideas/idea-code.ts new file mode 100644 index 000000000..c5982ffad --- /dev/null +++ b/src/documentation/ideas/idea-code.ts @@ -0,0 +1,19 @@ +/** + * (c) 2026, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { PortableText } from "../../common/sanity"; +import { Idea } from "./model"; + +const isCode = ( + block: PortableText[number] +): block is { _type: "python"; main: string } => block._type === "python"; + +/** The idea's program: its content's first code block. */ +export const ideaCode = (idea: Idea): string | undefined => + idea.content?.find(isCode)?.main; + +/** The idea's content without its code, for the guide beside the editor. */ +export const withoutCode = (content: PortableText | undefined): PortableText => + (content ?? []).filter((block) => !isCode(block)); diff --git a/src/e2e/app.ts b/src/e2e/app.ts index 5449ffc2d..0743a94d6 100644 --- a/src/e2e/app.ts +++ b/src/e2e/app.ts @@ -301,7 +301,7 @@ export class App { } } - async switchTab(tabName: "Files" | "API" | "Reference" | "Ideas") { + async switchTab(tabName: "Files" | "API" | "Reference" | "Guide") { await this.page.getByRole("tab", { name: tabName }).click(); } @@ -626,10 +626,6 @@ export class App { await results.first().click(); } - async selectDocumentationIdea(name: string): Promise { - await this.page.getByRole("button", { name }).click(); - } - async connect(): Promise { await this.moreConnectionOptionsButton.click(); await this.page.getByRole("menuitem", { name: "Connect" }).click(); diff --git a/src/e2e/documentation.test.ts b/src/e2e/documentation.test.ts index 7a63976a6..375f7a632 100644 --- a/src/e2e/documentation.test.ts +++ b/src/e2e/documentation.test.ts @@ -68,19 +68,4 @@ test.describe("documentation", () => { "Count and repeat sets of instructions" ); }); - - test("Ideas tab navigation", async ({ app }) => { - await app.switchTab("Ideas"); - await app.expectDocumentationTopLevelHeading( - "Ideas", - "Try out these projects, modify them and get inspired" - ); - }); - - test("Select an idea", async ({ app }) => { - const ideaName = "Emotion badge"; - await app.switchTab("Ideas"); - await app.selectDocumentationIdea(ideaName); - await app.expectDocumentationTopLevelHeading(ideaName); - }); }); diff --git a/src/e2e/home.test.ts b/src/e2e/home.test.ts index 9c6c3a5d2..c05ea8069 100644 --- a/src/e2e/home.test.ts +++ b/src/e2e/home.test.ts @@ -42,6 +42,29 @@ test.describe("home page", () => { await app.expectEditorContainText("from microbit import"); }); + test("opens an idea from its dialog as a project with its guide", async ({ + app, + homePage, + }) => { + const ideaName = "Emotion badge"; + await homePage.goto(); + await homePage.page.getByRole("button", { name: ideaName }).click(); + const dialog = homePage.page.getByRole("dialog"); + await expect( + dialog.getByRole("heading", { name: ideaName, exact: true }) + ).toBeVisible(); + + await dialog.getByRole("button", { name: "Create", exact: true }).click(); + + await expect(app.page).toHaveURL(appUrlPattern("project/guide")); + await app.expectProjectName(ideaName); + await expect( + app.page.getByRole("tab", { name: "Guide", selected: true }) + ).toBeVisible(); + await app.expectDocumentationTopLevelHeading(ideaName); + await app.expectEditorContainText("from microbit import"); + }); + test("returns home from the editor with the project listed", async ({ app, homePage, diff --git a/src/fs/projects-db.ts b/src/fs/projects-db.ts index 98ed3223e..3f3339a28 100644 --- a/src/fs/projects-db.ts +++ b/src/fs/projects-db.ts @@ -17,6 +17,8 @@ export interface ProjectMeta { name: string | undefined; /** Last modified or opened, for ordering by recency. */ timestamp: number; + /** The idea the project was opened from, whose guide it shows. */ + ideaSlug?: string; } /** A project as listed on the pages: its metadata plus what it contains. */ diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index ad1de444a..7ddcefce8 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -31,11 +31,11 @@ import { useSettings } from "../settings/settings"; import { createProjectsPageUrl } from "../urls"; import ActionCard from "./ActionCard"; import DefaultPageLayout from "./DefaultPageLayout"; +import IdeaDialog from "./IdeaDialog"; import ProjectIcon from "./ProjectIcon"; import HomepageBanner from "./HomepageBanner"; import HomepageFooter from "./HomepageFooter"; import { - useCreateProject, useImportProjectFiles, usePageProjects, useProjectPageActions, @@ -55,7 +55,14 @@ const HomePage = () => { const intl = useIntl(); const [{ languageId }] = useSettings(); const brand = useDeployment(); - const createProject = useCreateProject("home"); + const [selectedIdea, setSelectedIdea] = useState<{ + slug: string; + title: string; + }>(); + const selectIdea = useCallback( + (slug: string, title: string) => setSelectedIdea({ slug, title }), + [] + ); const importFiles = useImportProjectFiles(); const handleDrop = useCallback( (files: File[]) => void importFiles(files, "drop"), @@ -63,6 +70,12 @@ const HomePage = () => { ); return ( + {selectedIdea && ( + setSelectedIdea(undefined)} + /> + )} { } navigation /> diff --git a/src/pages/IdeaDialog.tsx b/src/pages/IdeaDialog.tsx new file mode 100644 index 000000000..ff8f3a2fd --- /dev/null +++ b/src/pages/IdeaDialog.tsx @@ -0,0 +1,92 @@ +/** + * (c) 2026, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { + Button, + Modal, + ModalBody, + ModalCloseButton, + ModalFooter, + ModalHeader, + Text, +} from "@microbit/ui"; +import { FormattedMessage } from "react-intl"; +import { Box, Grid, Stack } from "styled-system/jsx"; +import { docStylesClass } from "../common/documentation-styles"; +import Spinner from "../common/Spinner"; +import { useDocumentation } from "../documentation/documentation-hooks"; +import { ideaCode } from "../documentation/ideas/idea-code"; +import IdeaGuideContent from "../documentation/ideas/IdeaGuideContent"; +import CodeMirrorView from "../editor/codemirror/CodeMirrorView"; +import { useOpenIdea } from "./project-page-actions"; + +interface IdeaDialogProps { + slug: string; + /** The idea's name, shown before its content loads and given to the project. */ + title: string; + onClose: () => void; +} + +/** + * An idea from the home page: its program beside text explaining it, and a + * way to open it as a new project. + */ +const IdeaDialog = ({ slug, title, onClose }: IdeaDialogProps) => { + const { ideas } = useDocumentation(); + const openIdea = useOpenIdea(); + const idea = + ideas.status === "ok" + ? ideas.content.find((idea) => idea.slug.current === slug) + : undefined; + const code = idea && ideaCode(idea); + return ( + + + + {title} + + + + {/* Always rendered, so the code stays in the right-hand column. */} + + {idea && } + + + {ideas.status === "loading" ? ( + + ) : code !== undefined ? ( + + ) : ( + + + + )} + + + + + + + + ); +}; + +export default IdeaDialog; diff --git a/src/pages/project-page-actions.tsx b/src/pages/project-page-actions.tsx index 0e1f95dd9..4e21a345a 100644 --- a/src/pages/project-page-actions.tsx +++ b/src/pages/project-page-actions.tsx @@ -17,6 +17,7 @@ import { useNavigation, } from "react-router"; import useActionFeedback from "../common/use-action-feedback"; +import { MAIN_FILE } from "../fs/fs"; import { useLogging } from "../logging/logging-hooks"; import { useProjectImporter } from "../project/project-hooks"; import { ImportSource } from "../project/project-import"; @@ -44,19 +45,41 @@ interface CreateProjectFailure { error: unknown; } +/** What the editor route's action reads from its form. */ +interface CreateProjectFields { + name: string; + /** An idea's program for main.py, with the idea's slug. */ + idea?: { code: string; slug: string }; +} + /** - * The editor route's action: creates a project named by the form and opens - * the editor on it at the same URL. Part of the navigation, so the router - * knows a create is in progress until the editor is showing. + * The editor route's action: creates a project from the form and opens the + * editor on it at the same URL. Part of the navigation, so the router knows + * a create is in progress until the editor is showing. */ export const createProjectAction = (projects: Projects) => async ({ request, }: ActionFunctionArgs): Promise => { - const name = (await request.formData()).get("name"); + const form = await request.formData(); + const field = (key: string) => { + const value = form.get(key); + return typeof value === "string" ? value : undefined; + }; + const name = field("name") ?? ""; + const code = field("code"); + const ideaSlug = field("ideaSlug"); try { - await projects.create(typeof name === "string" ? name : ""); + if (code !== undefined && ideaSlug !== undefined) { + await projects.createFromFiles( + name, + { [MAIN_FILE]: new TextEncoder().encode(code) }, + ideaSlug + ); + } else { + await projects.create(name); + } } catch (error) { return { error }; } @@ -124,6 +147,37 @@ const useAttempt = () => { */ export const useCreateProject = (surface: ProjectSurface) => { const logging = useLogging(); + const submit = useSubmitCreateProject(); + return useCallback( + async (name: string, state?: RouterState) => { + logging.event({ type: "project_create", detail: { surface } }); + await submit({ name }, state); + }, + [logging, submit, surface] + ); +}; + +/** + * Opens an idea as a new project with its program in main.py, showing its + * guide beside the editor. + */ +export const useOpenIdea = () => { + const logging = useLogging(); + const submit = useSubmitCreateProject(); + return useCallback( + async (name: string, slug: string, code: string) => { + logging.event({ type: "idea_open", detail: { id: slug } }); + await submit({ name, idea: { code, slug } }, { tab: "guide" }); + }, + [logging, submit] + ); +}; + +/** + * Submits to the editor route's action (createProjectAction), reporting a + * failure as a toast. + */ +const useSubmitCreateProject = () => { const reportError = useReportError(); const fetcher = useFetcher({ key: createProjectFetcherKey, @@ -136,14 +190,18 @@ export const useCreateProject = (surface: ProjectSurface) => { }, [failure, reportError]); const { submit } = fetcher; return useCallback( - async (name: string, state?: RouterState) => { - logging.event({ type: "project_create", detail: { surface } }); - await submit( - { name }, + (fields: CreateProjectFields, state?: RouterState) => + submit( + { + name: fields.name, + ...(fields.idea && { + code: fields.idea.code, + ideaSlug: fields.idea.slug, + }), + }, { method: "post", action: createEditorUrl(state) } - ); - }, - [logging, submit, surface] + ), + [submit] ); }; diff --git a/src/pages/resource-cards.tsx b/src/pages/resource-cards.tsx index ed94ca714..93bbc32cf 100644 --- a/src/pages/resource-cards.tsx +++ b/src/pages/resource-cards.tsx @@ -37,13 +37,12 @@ import { microbitOrgLessonUrl, microbitOrgProjectUrl, } from "../external-links"; -import { RouterState } from "../router-hooks"; import { Level } from "./LevelIndicator"; import ResourceCard from "./ResourceCard"; interface Idea { titleId: string; - /** The idea's slug in the editor's ideas tab. */ + /** The idea's slug in the CMS ideas. */ slug: string; imgSrc: string; } @@ -82,23 +81,17 @@ const ideas: Idea[] = [ }, ]; -/** - * Idea cards open the idea in a new project, so following one can't change - * the user's existing projects. - */ +/** Idea cards show the idea, by slug, for the user to open or not. */ export const createIdeaCards = ( intl: IntlShape, - createProject: (name: string, state: RouterState) => Promise + onSelect: (slug: string, title: string) => void ) => ideas.map((idea) => ( - void createProject(intl.formatMessage({ id: idea.titleId }), { - tab: "ideas", - slug: { id: idea.slug }, - }) + onSelect(idea.slug, intl.formatMessage({ id: idea.titleId })) } imgSrc={idea.imgSrc} // The ideas tab's images are 2:1; show them whole. diff --git a/src/project/projects-hooks.tsx b/src/project/projects-hooks.tsx index 11be741e9..a5d5afcc3 100644 --- a/src/project/projects-hooks.tsx +++ b/src/project/projects-hooks.tsx @@ -49,3 +49,19 @@ export const useProjectList = (): ProjectListEntry[] => { ); return useSyncExternalStore(subscribe, () => projects.projects); }; + +/** + * The idea the editor's open project was opened from, if any. Undefined in + * iframe controller mode, which has no projects. + */ +export const useOpenIdeaSlug = (): string | undefined => { + const projects = useProjectsIfAvailable(); + const subscribe = useCallback( + (listener: () => void) => { + projects?.addEventListener("open", listener); + return () => projects?.removeEventListener("open", listener); + }, + [projects] + ); + return useSyncExternalStore(subscribe, () => projects?.currentIdeaSlug); +}; diff --git a/src/project/projects.test.ts b/src/project/projects.test.ts index 9fe3d9045..85a0aa7b3 100644 --- a/src/project/projects.test.ts +++ b/src/project/projects.test.ts @@ -107,6 +107,42 @@ describe("Projects", () => { expect(fileNames(fs)).toEqual([MAIN_FILE, "helper.py"]); }); + it("knows the idea the open project came from", async () => { + const { fs, projects } = await setup(); + await projects.openCurrent(); + await fs.initialize(); + const plain = projects.currentId!; + const opened = vi.fn(); + projects.addEventListener("open", opened); + + await projects.createFromFiles( + "Dice", + { [MAIN_FILE]: new TextEncoder().encode("# dice") }, + "dice" + ); + expect(projects.currentIdeaSlug).toEqual("dice"); + expect(opened).toHaveBeenCalledTimes(1); + + await projects.open(plain); + expect(projects.currentIdeaSlug).toBeUndefined(); + expect(opened).toHaveBeenCalledTimes(2); + }); + + it("duplicates keep the idea", async () => { + const { db, fs, projects } = await setup(); + await projects.openCurrent(); + await fs.initialize(); + const idea = await projects.createFromFiles( + "Dice", + { [MAIN_FILE]: new TextEncoder().encode("# dice") }, + "dice" + ); + + const copy = await projects.duplicate(idea, "Dice copy"); + + expect((await db!.get(copy))?.ideaSlug).toEqual("dice"); + }); + it("renames the open project through the file system and others directly", async () => { const { db, fs, projects } = await setup(); await projects.openCurrent(); diff --git a/src/project/projects.ts b/src/project/projects.ts index c89c8c842..40dad662c 100644 --- a/src/project/projects.ts +++ b/src/project/projects.ts @@ -41,9 +41,17 @@ export class ProjectSaveErrorEvent extends Event { } } +/** The editor's open project changed. */ +export class ProjectOpenedEvent extends Event { + constructor() { + super("open"); + } +} + interface EventMap { change: ProjectsChangedEvent; saveerror: ProjectSaveErrorEvent; + open: ProjectOpenedEvent; } /** @@ -72,6 +80,7 @@ export class Projects extends TypedEventTarget { private readonly ready: Promise; private firstStorageResolved = false; private openId: string | undefined; + private openIdeaSlug: string | undefined; private openStorage: IndexedDBFSStorage | undefined; private opening: Promise | undefined; private cachedList: ProjectListEntry[] = []; @@ -125,6 +134,11 @@ export class Projects extends TypedEventTarget { return this.openId; } + /** The idea the open project was opened from, if any. */ + get currentIdeaSlug(): string | undefined { + return this.openIdeaSlug; + } + async refresh(): Promise { await this.ready; this.cachedList = this.db ? await this.db.listWithFileNames() : []; @@ -182,16 +196,17 @@ export class Projects extends TypedEventTarget { } /** - * Creates a project with the given files, e.g. from an imported hex, and - * opens it in the editor. + * Creates a project with the given files, e.g. from an imported hex or an + * idea, and opens it in the editor. */ async createFromFiles( name: string | undefined, - files: Record + files: Record, + ideaSlug?: string ): Promise { const db = await this.requireDb(); const id = generateId(); - await db.create({ id, name, timestamp: Date.now() }, files); + await db.create({ id, name, timestamp: Date.now(), ideaSlug }, files); await this.switchTo(id); await this.changed([id]); return id; @@ -215,7 +230,13 @@ export class Projects extends TypedEventTarget { await this.openStorage?.flush(); } const newId = generateId(); - await db.duplicate(id, { id: newId, name, timestamp: Date.now() }); + const { ideaSlug } = (await db.get(id)) ?? {}; + await db.duplicate(id, { + id: newId, + name, + timestamp: Date.now(), + ideaSlug, + }); await this.changed([newId]); return newId; } @@ -266,8 +287,10 @@ export class Projects extends TypedEventTarget { this.logging ); this.openId = id; + this.openIdeaSlug = (await db.get(id))?.ideaSlug; this.openStorage = indexed; setCurrentProjectId(this.session, id); + this.dispatchTypedEvent("open", new ProjectOpenedEvent()); if (this.firstStorageResolved) { await this.fs.switchStorage(storage); } else { @@ -282,6 +305,7 @@ export class Projects extends TypedEventTarget { */ private letGoOfOpenProject(): void { this.openId = undefined; + this.openIdeaSlug = undefined; this.openStorage = undefined; if (getCurrentProjectId(this.session)) { setCurrentProjectId(this.session, undefined); diff --git a/src/router-hooks.tsx b/src/router-hooks.tsx index 62eb7d14f..2786580fe 100644 --- a/src/router-hooks.tsx +++ b/src/router-hooks.tsx @@ -15,9 +15,15 @@ import { useLogging } from "./logging/logging-hooks"; import { createEditorUrl, createIframeEditorUrl } from "./urls"; import { useIframeMode } from "./iframe-mode-hooks"; -export type TabName = "api" | "ideas" | "reference" | "project"; +export type TabName = "api" | "guide" | "ideas" | "reference" | "project"; -const tabNames: readonly string[] = ["api", "ideas", "reference", "project"]; +const tabNames: readonly string[] = [ + "api", + "guide", + "ideas", + "reference", + "project", +]; export const isTabName = (value: string | undefined): value is TabName => value !== undefined && tabNames.includes(value); diff --git a/src/workbench/SideBar.tsx b/src/workbench/SideBar.tsx index 335593577..952e5ff29 100644 --- a/src/workbench/SideBar.tsx +++ b/src/workbench/SideBar.tsx @@ -14,7 +14,7 @@ import { } from "react"; import { TabList, TabPanel, TabPanels, Tabs } from "react-aria-components"; import { IconType } from "react-icons"; -import { RiHome2Line, RiLightbulbFlashLine } from "react-icons/ri"; +import { RiBookOpenLine, RiHome2Line } from "react-icons/ri"; import { VscFiles, VscLibrary } from "react-icons/vsc"; import { useIntl } from "react-intl"; import { Link as RouterLink } from "react-router"; @@ -25,12 +25,13 @@ import { SystemStyleObject } from "styled-system/types"; import ErrorBoundary from "../common/ErrorBoundary"; import PythonLogo from "../common/PythonLogo"; import ApiArea from "../documentation/ApiArea"; -import IdeasArea from "../documentation/IdeasArea"; +import GuideArea from "../documentation/GuideArea"; import ReferenceArea from "../documentation/ReferenceArea"; import { flags } from "../flags"; import { useProjectsDatabaseActive } from "../fs/storage-status"; import { useIframeMode } from "../iframe-mode-hooks"; import ProjectArea from "../project/ProjectArea"; +import { useOpenIdeaSlug } from "../project/projects-hooks"; import { TabName, useRouterState } from "../router-hooks"; import SettingsMenu from "../settings/SettingsMenu"; import { createHomePageUrl } from "../urls"; @@ -81,8 +82,21 @@ const SideBar = ({ }: SideBarProps) => { const intl = useIntl(); const [releaseDialog, setReleaseDialog] = useReleaseDialogState(); + // Only projects opened from an idea have its guide. + const hasGuide = useOpenIdeaSlug() !== undefined; const panes: Pane[] = useMemo(() => { - const result = [ + const result: Pane[] = [ + ...(hasGuide + ? [ + { + id: "guide" as const, + title: intl.formatMessage({ id: "guide-tab" }), + icon: RiBookOpenLine, + contents: , + color: "gray.75", + }, + ] + : []), { id: "reference" as const, title: intl.formatMessage({ id: "reference-tab" }), @@ -90,13 +104,6 @@ const SideBar = ({ contents: , color: "gray.75", }, - { - id: "ideas" as const, - title: intl.formatMessage({ id: "ideas-tab" }), - icon: RiLightbulbFlashLine, - contents: , - color: "gray.75", - }, { id: "api" as const, title: intl.formatMessage({ id: "api-tab" }), @@ -119,7 +126,7 @@ const SideBar = ({ }, ]; return result; - }, [onSelectedFileChanged, selectedFile, intl]); + }, [hasGuide, onSelectedFileChanged, selectedFile, intl]); const [{ tab, slug, focus }, setParams] = useRouterState(); const tabPanelsRef = useRef(null); const setPanelFocus = () => { From 6a5f57bf7288c24b499353717922bf62887d322f Mon Sep 17 00:00:00 2001 From: Robert Knight Date: Tue, 20 Oct 2026 11:58:22 +0100 Subject: [PATCH 2/8] Remove the old Ideas panel The sidebar no longer has an Ideas tab, and the Guide shows ideas without their code, so the panel's components, the "Open" action on idea code blocks and their strings are unused. --- docs/analytics-events.md | 3 +- lang/ui.en.json | 20 -- src/documentation/IdeasArea.tsx | 30 --- src/documentation/api/ApiNode.tsx | 1 - src/documentation/common/CodeActionButton.tsx | 15 +- src/documentation/common/CodeEmbed.tsx | 36 ++-- .../common/DocumentationContent.tsx | 5 +- src/documentation/ideas/IdeaCard.tsx | 55 ----- src/documentation/ideas/IdeaGuideContent.tsx | 1 - .../ideas/IdeasDocumentation.tsx | 195 ------------------ .../reference/ReferenceTopicEntry.tsx | 1 - src/project/project-actions.tsx | 31 --- src/router-hooks.test.tsx | 2 +- src/router-hooks.tsx | 10 +- 14 files changed, 21 insertions(+), 384 deletions(-) delete mode 100644 src/documentation/IdeasArea.tsx delete mode 100644 src/documentation/ideas/IdeaCard.tsx delete mode 100644 src/documentation/ideas/IdeasDocumentation.tsx diff --git a/docs/analytics-events.md b/docs/analytics-events.md index f6db65279..888510bb9 100644 --- a/docs/analytics-events.md +++ b/docs/analytics-events.md @@ -181,8 +181,7 @@ Projects page only. Fires when the user changes the sort field or direction. ### `idea_open` -User opened an idea as a new project, from the home page idea dialog or the -documentation. +User created a project from an idea, in the home page idea dialog. | Param | Values | | ----- | -------------------------- | diff --git a/lang/ui.en.json b/lang/ui.en.json index 494fde5e7..e2d4c6184 100644 --- a/lang/ui.en.json +++ b/lang/ui.en.json @@ -159,10 +159,6 @@ "defaultMessage": "Confirm replace project", "description": "Confirmation message title for replacing project dialog" }, - "confirm-replace-with-idea": { - "defaultMessage": "Replace all files with {ideaName}?", - "description": "Confirmation message body for replacing project dialog with an idea" - }, "confirm-save-action": { "defaultMessage": "Confirm and save", "description": "Confirm and save action label" @@ -467,14 +463,6 @@ "defaultMessage": "Ideas", "description": "Heading of the home page row of idea cards" }, - "ideas-tab": { - "defaultMessage": "Ideas", - "description": "Ideas tab button text. Tab shows programs to give students ideas." - }, - "ideas-tab-description": { - "defaultMessage": "Try out these projects, modify them and get inspired", - "description": "Ideas tab description text. Tab shows programs to give students ideas." - }, "import-file-action": { "defaultMessage": "Import", "description": "Import file action" @@ -646,10 +634,6 @@ "defaultMessage": "More connect options", "description": "Aria label for the additional actions menu to the right of the Send to micro:bit button" }, - "more-ideas": { - "defaultMessage": "More ideas", - "description": "Link to more ideas on the microbit.org website at the end of the ideas section" - }, "more-save-options": { "defaultMessage": "More save options", "description": "Aria label for the additional actions menu to the right of the Save button" @@ -726,10 +710,6 @@ "defaultMessage": "Image unavailable offline", "description": "Alt text for an image placeholder when the user is offline" }, - "open-action": { - "defaultMessage": "Open", - "description": "Open button text" - }, "open-file-dropped": { "defaultMessage": "Open file when dropped", "description": "Aria label for file drop target" 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/api/ApiNode.tsx b/src/documentation/api/ApiNode.tsx index dfa3901a9..ecd375e9c 100644 --- a/src/documentation/api/ApiNode.tsx +++ b/src/documentation/api/ApiNode.tsx @@ -555,7 +555,6 @@ const DraggableSignature = ({ toDefault={() => setHighlighted(false)} codeAction={handleCopyCode} borderAdjustment={false} - toolkitType="api" /> ); diff --git a/src/documentation/common/CodeActionButton.tsx b/src/documentation/common/CodeActionButton.tsx index 4235a83be..8f1c48c3f 100644 --- a/src/documentation/common/CodeActionButton.tsx +++ b/src/documentation/common/CodeActionButton.tsx @@ -5,7 +5,7 @@ */ import { Button, Collapse, Icon } from "@microbit/ui"; import { useState } from "react"; -import { RiFileCopy2Line, RiFolderOpenLine } from "react-icons/ri"; +import { RiFileCopy2Line } from "react-icons/ri"; import { FormattedMessage } from "react-intl"; import { HStack } from "styled-system/jsx"; @@ -15,7 +15,6 @@ interface CodeActionButtonProps { toDefault: () => void; codeAction: () => void; borderAdjustment: boolean; - toolkitType?: string; } const CodeActionButton = ({ @@ -24,7 +23,6 @@ const CodeActionButton = ({ toDefault, codeAction, borderAdjustment, - toolkitType, }: CodeActionButtonProps) => { const [hovered, setHovered] = useState(false); return ( @@ -51,16 +49,9 @@ const CodeActionButton = ({ ml: "5", }} onPress={codeAction} - startIcon={ - toolkitType === "ideas" ? : undefined - } - endIcon={ - toolkitType !== "ideas" ? : undefined - } + endIcon={} > - + diff --git a/src/documentation/common/CodeEmbed.tsx b/src/documentation/common/CodeEmbed.tsx index 105f3eb25..2c0924338 100644 --- a/src/documentation/common/CodeEmbed.tsx +++ b/src/documentation/common/CodeEmbed.tsx @@ -23,7 +23,6 @@ import { useActiveEditorActions } from "../../editor/active-editor-hooks"; import CodeMirrorView from "../../editor/codemirror/CodeMirrorView"; import { debug as dndDebug, setDragContext } from "../../editor/codemirror/dnd"; import { useLogging } from "../../logging/logging-hooks"; -import { useProjectActions } from "../../project/project-hooks"; import { useSessionSettings } from "../../settings/session-settings"; import { useClipboard } from "@microbit/ui"; import DragHandle from "../common/DragHandle"; @@ -36,7 +35,6 @@ interface CodeEmbedProps { code: string; parentSlug?: string; toolkitType?: string; - title?: string; } type CodeEmbedState = @@ -57,7 +55,6 @@ const CodeEmbed = ({ code: codeWithImports, toolkitType, parentSlug, - title, }: CodeEmbedProps) => { const [copyCodeOpen, setCopyCodeOpen] = useState(false); const [state, originalSetState] = useState("default"); @@ -122,10 +119,6 @@ const CodeEmbed = ({ toolkitType ); }, [actions, code, codeWithImports, onCopy, parentSlug, toolkitType]); - const projectActions = useProjectActions(); - const handleOpenIdea = useCallback(async () => { - projectActions.openIdea(parentSlug, codeWithImports, title!); - }, [codeWithImports, parentSlug, projectActions, title]); const lineCount = code.trim().split("\n").length; const codeRef = useRef(null); const textHeight = lineCount * 1.375 + "em"; @@ -134,8 +127,7 @@ const CodeEmbed = ({ const hotKeysRef = useHotkeys(keyboardShortcuts.copyCode, handleCopyCode, { preventDefault: true, }) as Ref; - const raisedLook = - toolkitType === "ideas" ? state === "highlighted" : state !== "default"; + const raisedLook = state !== "default"; return ( ); @@ -219,7 +210,7 @@ const CodePopUp = ({ concise={concise} full={full} // We're always "raised" as this is the pop-up. - raisedLook={toolkitType !== "ideas"} + raisedLook highlightDragHandle toolkitType={toolkitType} parentSlug={parentSlug} @@ -298,14 +289,13 @@ const Code = React.forwardRef( ); const intl = useIntl(); const [{ dragDropSuccess }] = useSessionSettings(); - const tooltipDisabled = - toolkitType === "ideas" ? true : Boolean(dragDropSuccess); + const tooltipDisabled = Boolean(dragDropSuccess); const content = ( ( ref={ref} gap="0" onClick={() => onToggle(!isOpen)} - onDragStart={toolkitType === "ideas" ? () => {} : handleDragStart} + onDragStart={handleDragStart} onDragEnd={handleDragEnd} - cursor={toolkitType === "ideas" ? "default" : "grab"} + cursor="grab" position="absolute" fontSize={isPopUp ? "md" : undefined} zIndex={isPopUp ? "codePopUp" : "code"} @@ -332,12 +322,10 @@ const Code = React.forwardRef( - {toolkitType !== "ideas" && ( - - )} + ({}); @@ -63,8 +62,8 @@ const useIsExpanded = () => { }; const useCodeEmbedContext = () => { - const { parentSlug, toolkitType, title } = useContext(DocumentationContext); - return { parentSlug, toolkitType, title }; + const { parentSlug, toolkitType } = useContext(DocumentationContext); + return { parentSlug, toolkitType }; }; const DocumentationApiLinkMark = ( diff --git a/src/documentation/ideas/IdeaCard.tsx b/src/documentation/ideas/IdeaCard.tsx deleted file mode 100644 index 11f6d1b38..000000000 --- a/src/documentation/ideas/IdeaCard.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/** - * (c) 2021-2022, Micro:bit Educational Foundation and contributors - * - * SPDX-License-Identifier: MIT - */ -import { styled } from "styled-system/jsx"; -import { imageUrlBuilder } from "../../common/imageUrlBuilder"; -import { SimpleImage } from "../../common/sanity"; -import DocumentationHeading from "../common/DocumentationHeading"; -import ImageWithFallback from "../common/ImageWithFallback"; -import OfflineImageFallback from "../OfflineImageFallback"; - -interface IdeaCardProps { - name: string; - image: SimpleImage; - isV2Only?: boolean; - onClick: () => void; -} - -const IdeaCard = ({ name, image, isV2Only, onClick }: IdeaCardProps) => { - return ( - - } - alt="" - borderTopRadius="lg" - width="500px" - /> - - - ); -}; - -export default IdeaCard; diff --git a/src/documentation/ideas/IdeaGuideContent.tsx b/src/documentation/ideas/IdeaGuideContent.tsx index 6db314d33..7fcdc2695 100644 --- a/src/documentation/ideas/IdeaGuideContent.tsx +++ b/src/documentation/ideas/IdeaGuideContent.tsx @@ -58,7 +58,6 @@ const IdeaGuideContent = ({ diff --git a/src/documentation/ideas/IdeasDocumentation.tsx b/src/documentation/ideas/IdeasDocumentation.tsx deleted file mode 100644 index 7a6db029e..000000000 --- a/src/documentation/ideas/IdeasDocumentation.tsx +++ /dev/null @@ -1,195 +0,0 @@ -/** - * (c) 2021-2022, Micro:bit Educational Foundation and contributors - * - * SPDX-License-Identifier: MIT - */ -import { Link, Text } from "@microbit/ui"; -import { CSSProperties, ReactNode, useCallback, useRef } from "react"; -import { FormattedMessage, useIntl } from "react-intl"; -import { Box, Grid, Stack } from "styled-system/jsx"; -import AreaHeading from "../../common/AreaHeading"; -import { - docStylesClass, - runtimeAspectRatioClass, -} from "../../common/documentation-styles"; -import HeadedScrollablePanel from "../../common/HeadedScrollablePanel"; -import { - getAspectRatioPadding, - imageUrlBuilder, -} from "../../common/imageUrlBuilder"; -import { useResizeObserverContentRect } from "../../common/use-resize-observer"; -import { Anchor, useRouterTabSlug } from "../../router-hooks"; -import { useAnimationDirection } from "../common/documentation-animation-hooks"; -import DocumentationBreadcrumbHeading from "../common/DocumentationBreadcrumbHeading"; -import DocumentationContent, { - DocumentationContextProvider, -} from "../common/DocumentationContent"; -import ImageWithFallback from "../common/ImageWithFallback"; -import { isV2Only } from "../common/model"; -import IdeaCard from "./IdeaCard"; -import { Idea } from "./model"; -import OfflineImageFallback from "../OfflineImageFallback"; -import { microbitOrgMiciProjectsUrl } from "../../external-links"; -import { useSettings } from "../../settings/settings"; - -interface IdeasDocumentationProps { - ideas: Idea[]; -} - -const IdeasDocumentation = ({ ideas }: IdeasDocumentationProps) => { - const [anchor, setAnchor] = useRouterTabSlug("ideas"); - const direction = useAnimationDirection(anchor); - const ideaId = anchor?.id; - const handleNavigate = useCallback( - (ideaId: string | undefined) => { - setAnchor(ideaId ? { id: ideaId } : undefined, "user"); - }, - [setAnchor] - ); - return ( - - ); -}; - -interface ActiveLevelProps extends IdeasDocumentationProps { - anchor: Anchor | undefined; - ideaId: string | undefined; - onNavigate: (ideaId: string | undefined) => void; - direction: "forward" | "back" | "none"; -} - -const ActiveLevel = ({ - ideaId, - onNavigate, - ideas, - direction, -}: ActiveLevelProps) => { - const activeIdea = ideas.find((idea) => idea.slug.current === ideaId); - const intl = useIntl(); - const [{ languageId }] = useSettings(); - const headingString = intl.formatMessage({ id: "ideas-tab" }); - const ref = useRef(null); - const contentRect = useResizeObserverContentRect(ref); - const contentWidth = contentRect?.width ?? 0; - const numCols = - !contentWidth || contentWidth > 1100 ? 3 : contentWidth > 550 ? 2 : 1; - if (activeIdea) { - // Runtime value derived from the image reference. - const imagePadding = activeIdea.image - ? getAspectRatioPadding(activeIdea.image.asset._ref) - : undefined; - return ( - onNavigate(undefined)} - isV2Only={isV2Only(activeIdea)} - /> - } - > - {activeIdea.content && ( - - {activeIdea.image && ( - - } - alt="" - borderTopRadius="lg" - /> - - )} - - - - - - )} - - ); - } - return ( - - } - > - - {ideas.map((idea) => ( - onNavigate(idea.slug.current)} - /> - ))} - - - ( - - {chunks} - - ), - }} - /> - - - ); -}; - -export default IdeasDocumentation; diff --git a/src/documentation/reference/ReferenceTopicEntry.tsx b/src/documentation/reference/ReferenceTopicEntry.tsx index 6f7b3e1dd..aa6c60a0f 100644 --- a/src/documentation/reference/ReferenceTopicEntry.tsx +++ b/src/documentation/reference/ReferenceTopicEntry.tsx @@ -92,7 +92,6 @@ const ReferenceTopicEntry = ({ parentSlug={entry.slug.current} toolkitType={toolkitType} isExpanded={disclosure.isOpen} - title={topic.name} > => this.importer.importIntoEditor(files, source); - /** - * Opens an idea from the documentation as a new project. - */ - openIdea = async (slug: string | undefined, code: string, title: string) => { - this.logging.event({ - type: "idea_open", - detail: { id: slug }, - }); - try { - const opened = await this.importer.newProject( - title, - { [MAIN_FILE]: new TextEncoder().encode(code) }, - this.intl.formatMessage( - { id: "confirm-replace-with-idea" }, - { ideaName: title } - ) - ); - if (!opened) { - return; - } - this.actionFeedback.success({ - title: this.intl.formatMessage( - { id: "loaded-file-feedback" }, - { filename: title } - ), - }); - } catch (e) { - this.actionFeedback.unexpectedError(e); - } - }; - /** * Flash the device, reporting progress via a dialog. */ diff --git a/src/router-hooks.test.tsx b/src/router-hooks.test.tsx index 1062efd46..9dcd5066e 100644 --- a/src/router-hooks.test.tsx +++ b/src/router-hooks.test.tsx @@ -112,7 +112,7 @@ describe("useRouterState", () => { it("does not log without a source", async () => { const { logging } = renderAt("/project"); await act(async () => { - setState({ tab: "ideas" }); + setState({ tab: "guide" }); }); expect(logging.events).toEqual([]); }); diff --git a/src/router-hooks.tsx b/src/router-hooks.tsx index 2786580fe..a79eab5da 100644 --- a/src/router-hooks.tsx +++ b/src/router-hooks.tsx @@ -15,15 +15,9 @@ import { useLogging } from "./logging/logging-hooks"; import { createEditorUrl, createIframeEditorUrl } from "./urls"; import { useIframeMode } from "./iframe-mode-hooks"; -export type TabName = "api" | "guide" | "ideas" | "reference" | "project"; +export type TabName = "api" | "guide" | "reference" | "project"; -const tabNames: readonly string[] = [ - "api", - "guide", - "ideas", - "reference", - "project", -]; +const tabNames: readonly string[] = ["api", "guide", "reference", "project"]; export const isTabName = (value: string | undefined): value is TabName => value !== undefined && tabNames.includes(value); From c731876533555531f192368f3589db482a6ad0f6 Mon Sep 17 00:00:00 2001 From: Robert Knight Date: Tue, 20 Oct 2026 12:04:25 +0100 Subject: [PATCH 3/8] Run i18n:tidy --- lang/ui.ca.json | 12 ------------ lang/ui.de.json | 12 ------------ lang/ui.es-ES.json | 12 ------------ lang/ui.fr.json | 12 ------------ lang/ui.ga-IE.json | 12 ------------ lang/ui.ja.json | 12 ------------ lang/ui.ko.json | 12 ------------ lang/ui.lol.json | 12 ------------ lang/ui.nl.json | 12 ------------ lang/ui.pl.json | 12 ------------ lang/ui.pt-BR.json | 15 --------------- lang/ui.zh-CN.json | 12 ------------ lang/ui.zh-TW.json | 12 ------------ 13 files changed, 159 deletions(-) diff --git a/lang/ui.ca.json b/lang/ui.ca.json index be9646282..97b510517 100644 --- a/lang/ui.ca.json +++ b/lang/ui.ca.json @@ -266,12 +266,6 @@ "highlight-code-structure-simple": { "defaultMessage": "Senzill" }, - "ideas-tab": { - "defaultMessage": "Idees" - }, - "ideas-tab-description": { - "defaultMessage": "Prova aquests projectes, modifica'ls i inspira't" - }, "insert-code-action": { "defaultMessage": "Insereix codi" }, @@ -320,9 +314,6 @@ "more-connect-options": { "defaultMessage": "Més opcions de connexió" }, - "more-ideas": { - "defaultMessage": "Més idees" - }, "more-save-options": { "defaultMessage": "Més opcions de desar" }, @@ -371,9 +362,6 @@ "offline-image-alt": { "defaultMessage": "La imatge no està disponible fora de línia" }, - "open-action": { - "defaultMessage": "Obert" - }, "open-file-dropped": { "defaultMessage": "Obre el fitxer quan es deixa anar" }, diff --git a/lang/ui.de.json b/lang/ui.de.json index 20fa82cbd..31700f99a 100644 --- a/lang/ui.de.json +++ b/lang/ui.de.json @@ -266,12 +266,6 @@ "highlight-code-structure-simple": { "defaultMessage": "Einfach" }, - "ideas-tab": { - "defaultMessage": "Ideen" - }, - "ideas-tab-description": { - "defaultMessage": "Probiere diese Projekte aus, ändere sie und lasse dich inspirieren" - }, "insert-code-action": { "defaultMessage": "Code einfügen" }, @@ -320,9 +314,6 @@ "more-connect-options": { "defaultMessage": "Mehr Verbindungsoptionen" }, - "more-ideas": { - "defaultMessage": "Mehr Ideen" - }, "more-save-options": { "defaultMessage": "Mehr Speicheroptionen" }, @@ -371,9 +362,6 @@ "offline-image-alt": { "defaultMessage": "Bild offline nicht verfügbar" }, - "open-action": { - "defaultMessage": "Öffnen" - }, "open-file-dropped": { "defaultMessage": "Datei nach Ablegen öffnen" }, diff --git a/lang/ui.es-ES.json b/lang/ui.es-ES.json index 34a1f029f..6a4c5f603 100644 --- a/lang/ui.es-ES.json +++ b/lang/ui.es-ES.json @@ -266,12 +266,6 @@ "highlight-code-structure-simple": { "defaultMessage": "Simple" }, - "ideas-tab": { - "defaultMessage": "Ideas" - }, - "ideas-tab-description": { - "defaultMessage": "Prueba estos proyectos y modifícalos para inspirarte" - }, "insert-code-action": { "defaultMessage": "Insertar código" }, @@ -320,9 +314,6 @@ "more-connect-options": { "defaultMessage": "Más opciones de conexión" }, - "more-ideas": { - "defaultMessage": "Más ideas" - }, "more-save-options": { "defaultMessage": "Más opciones para guardar" }, @@ -371,9 +362,6 @@ "offline-image-alt": { "defaultMessage": "Imagen no disponible sin conexión" }, - "open-action": { - "defaultMessage": "Abrir" - }, "open-file-dropped": { "defaultMessage": "Abrir archivo al soltarlo" }, diff --git a/lang/ui.fr.json b/lang/ui.fr.json index b26ec02be..d6a4391ac 100644 --- a/lang/ui.fr.json +++ b/lang/ui.fr.json @@ -266,12 +266,6 @@ "highlight-code-structure-simple": { "defaultMessage": "Simple" }, - "ideas-tab": { - "defaultMessage": "Idées" - }, - "ideas-tab-description": { - "defaultMessage": "Essayez ces projets, modifiez-les et inspirez-vous en" - }, "insert-code-action": { "defaultMessage": "Insérer le code" }, @@ -320,9 +314,6 @@ "more-connect-options": { "defaultMessage": "Plus d'options de connexion" }, - "more-ideas": { - "defaultMessage": "Plus d'idées" - }, "more-save-options": { "defaultMessage": "Plus d'options d'enregistrement" }, @@ -371,9 +362,6 @@ "offline-image-alt": { "defaultMessage": "Image indisponible hors ligne" }, - "open-action": { - "defaultMessage": "Ouvrir" - }, "open-file-dropped": { "defaultMessage": "Ouvrir le fichier lorsqu’il est déposé" }, diff --git a/lang/ui.ga-IE.json b/lang/ui.ga-IE.json index fa3af4b16..91b4dfba0 100644 --- a/lang/ui.ga-IE.json +++ b/lang/ui.ga-IE.json @@ -266,12 +266,6 @@ "highlight-code-structure-simple": { "defaultMessage": "Simplí" }, - "ideas-tab": { - "defaultMessage": "Smaointe" - }, - "ideas-tab-description": { - "defaultMessage": "Bain triail as na tionscadail seo, athraigh iad agus spreag iad" - }, "insert-code-action": { "defaultMessage": "Ionsáigh cód" }, @@ -320,9 +314,6 @@ "more-connect-options": { "defaultMessage": "Tuilleadh roghanna ceangail" }, - "more-ideas": { - "defaultMessage": "Tuilleadh smaointe" - }, "more-save-options": { "defaultMessage": "Tuilleadh roghanna sábhála" }, @@ -371,9 +362,6 @@ "offline-image-alt": { "defaultMessage": "Níl an íomhá ar fáil as líne" }, - "open-action": { - "defaultMessage": "Oscail" - }, "open-file-dropped": { "defaultMessage": "Oscail comhad nuair a thit sé" }, diff --git a/lang/ui.ja.json b/lang/ui.ja.json index 128cc1337..31113a36f 100644 --- a/lang/ui.ja.json +++ b/lang/ui.ja.json @@ -266,12 +266,6 @@ "highlight-code-structure-simple": { "defaultMessage": "簡易" }, - "ideas-tab": { - "defaultMessage": "アイデア" - }, - "ideas-tab-description": { - "defaultMessage": "これらのプロジェクトを試し、修正し、インスピレーションを得ることができます。" - }, "insert-code-action": { "defaultMessage": "コードを挿入" }, @@ -320,9 +314,6 @@ "more-connect-options": { "defaultMessage": "その他の接続オプション" }, - "more-ideas": { - "defaultMessage": "他のアイデア" - }, "more-save-options": { "defaultMessage": "その他の保存オプション" }, @@ -371,9 +362,6 @@ "offline-image-alt": { "defaultMessage": "画像がオフラインで利用できません" }, - "open-action": { - "defaultMessage": "開く" - }, "open-file-dropped": { "defaultMessage": "ドロップ時にファイルを開く" }, diff --git a/lang/ui.ko.json b/lang/ui.ko.json index fda2ab891..3c7cb926f 100644 --- a/lang/ui.ko.json +++ b/lang/ui.ko.json @@ -263,12 +263,6 @@ "highlight-code-structure-simple": { "defaultMessage": "단순" }, - "ideas-tab": { - "defaultMessage": "아이디어들" - }, - "ideas-tab-description": { - "defaultMessage": "다양한 프로젝트를 체험해보고 수정하며 아이디어를 얻어보세요" - }, "insert-code-action": { "defaultMessage": "코드 삽입" }, @@ -317,9 +311,6 @@ "more-connect-options": { "defaultMessage": "연결 옵션 더 보기" }, - "more-ideas": { - "defaultMessage": "아이디어 더 보기" - }, "more-save-options": { "defaultMessage": "저장 옵션 더 보기" }, @@ -368,9 +359,6 @@ "offline-image-alt": { "defaultMessage": "이미지는 오프라인에서 사용할 수 없습니다." }, - "open-action": { - "defaultMessage": "열기" - }, "open-file-dropped": { "defaultMessage": "드롭될 때 파일 열기" }, diff --git a/lang/ui.lol.json b/lang/ui.lol.json index e68fba758..0624ea9a5 100644 --- a/lang/ui.lol.json +++ b/lang/ui.lol.json @@ -266,12 +266,6 @@ "highlight-code-structure-simple": { "defaultMessage": "crwdns331540:0crwdne331540:0" }, - "ideas-tab": { - "defaultMessage": "crwdns331542:0crwdne331542:0" - }, - "ideas-tab-description": { - "defaultMessage": "crwdns331544:0crwdne331544:0" - }, "insert-code-action": { "defaultMessage": "crwdns331546:0crwdne331546:0" }, @@ -320,9 +314,6 @@ "more-connect-options": { "defaultMessage": "crwdns331576:0crwdne331576:0" }, - "more-ideas": { - "defaultMessage": "crwdns331578:0crwdne331578:0" - }, "more-save-options": { "defaultMessage": "crwdns331580:0crwdne331580:0" }, @@ -371,9 +362,6 @@ "offline-image-alt": { "defaultMessage": "crwdns359874:0crwdne359874:0" }, - "open-action": { - "defaultMessage": "crwdns331610:0crwdne331610:0" - }, "open-file-dropped": { "defaultMessage": "crwdns331614:0crwdne331614:0" }, diff --git a/lang/ui.nl.json b/lang/ui.nl.json index 25c1ba9ad..39ffb0f19 100644 --- a/lang/ui.nl.json +++ b/lang/ui.nl.json @@ -266,12 +266,6 @@ "highlight-code-structure-simple": { "defaultMessage": "Eenvoudig" }, - "ideas-tab": { - "defaultMessage": "Ideeën" - }, - "ideas-tab-description": { - "defaultMessage": "Probeer deze projecten uit, verander ze en laat je inspireren" - }, "insert-code-action": { "defaultMessage": "Code invoegen" }, @@ -320,9 +314,6 @@ "more-connect-options": { "defaultMessage": "Meer verbindings opties" }, - "more-ideas": { - "defaultMessage": "Meer ideeën" - }, "more-save-options": { "defaultMessage": "Meer opslagopties" }, @@ -371,9 +362,6 @@ "offline-image-alt": { "defaultMessage": "Afbeelding offline niet beschikbaar" }, - "open-action": { - "defaultMessage": "Open" - }, "open-file-dropped": { "defaultMessage": "Open bestand wanneer gedropt" }, diff --git a/lang/ui.pl.json b/lang/ui.pl.json index f3dbdf330..cee6cfb54 100644 --- a/lang/ui.pl.json +++ b/lang/ui.pl.json @@ -266,12 +266,6 @@ "highlight-code-structure-simple": { "defaultMessage": "Prosty" }, - "ideas-tab": { - "defaultMessage": "Pomysły" - }, - "ideas-tab-description": { - "defaultMessage": "Wypróbuj te projekty, zmodyfikuj je i poczuj się zainspirowany" - }, "insert-code-action": { "defaultMessage": "Wstaw kod" }, @@ -320,9 +314,6 @@ "more-connect-options": { "defaultMessage": "Więcej opcji połączenia" }, - "more-ideas": { - "defaultMessage": "Więcej pomysłów" - }, "more-save-options": { "defaultMessage": "Więcej opcji zapisu" }, @@ -371,9 +362,6 @@ "offline-image-alt": { "defaultMessage": "Obraz niedostępny w trybie offline" }, - "open-action": { - "defaultMessage": "Otwórz" - }, "open-file-dropped": { "defaultMessage": "Otwórz plik po upuszczeniu" }, diff --git a/lang/ui.pt-BR.json b/lang/ui.pt-BR.json index 5e75e4b48..03d1f09b2 100644 --- a/lang/ui.pt-BR.json +++ b/lang/ui.pt-BR.json @@ -101,9 +101,6 @@ "confirm-replace-title": { "defaultMessage": "Confirmar substituição do projeto" }, - "confirm-replace-with-idea": { - "defaultMessage": "Substituir todos os arquivos por {ideaName}?" - }, "confirm-save-action": { "defaultMessage": "Confirmar e salvar" }, @@ -278,12 +275,6 @@ "highlight-code-structure-simple": { "defaultMessage": "Simples" }, - "ideas-tab": { - "defaultMessage": "Ideias" - }, - "ideas-tab-description": { - "defaultMessage": "Experimente estes projetos, modifique-os e inspire-se" - }, "insert-code-action": { "defaultMessage": "Inserir código" }, @@ -332,9 +323,6 @@ "more-connect-options": { "defaultMessage": "Mais opções de conexão" }, - "more-ideas": { - "defaultMessage": "Mais ideias" - }, "more-save-options": { "defaultMessage": "Mais opções de salvamento" }, @@ -383,9 +371,6 @@ "offline-image-alt": { "defaultMessage": "Imagem indisponível offline" }, - "open-action": { - "defaultMessage": "Abrir" - }, "open-file-dropped": { "defaultMessage": "Abrir arquivo ao soltá-lo" }, diff --git a/lang/ui.zh-CN.json b/lang/ui.zh-CN.json index 6980e8746..69dc4a536 100644 --- a/lang/ui.zh-CN.json +++ b/lang/ui.zh-CN.json @@ -266,12 +266,6 @@ "highlight-code-structure-simple": { "defaultMessage": "简单" }, - "ideas-tab": { - "defaultMessage": "创意" - }, - "ideas-tab-description": { - "defaultMessage": "尝试这些项目,修改它们并获得灵感" - }, "insert-code-action": { "defaultMessage": "插入代码" }, @@ -320,9 +314,6 @@ "more-connect-options": { "defaultMessage": "更多连接选项" }, - "more-ideas": { - "defaultMessage": "More ideas" - }, "more-save-options": { "defaultMessage": "更多保存选项" }, @@ -371,9 +362,6 @@ "offline-image-alt": { "defaultMessage": "图片无法离线使用图像" }, - "open-action": { - "defaultMessage": "打开" - }, "open-file-dropped": { "defaultMessage": "放下时打开文件" }, diff --git a/lang/ui.zh-TW.json b/lang/ui.zh-TW.json index 0a4ead808..b13e40fc0 100644 --- a/lang/ui.zh-TW.json +++ b/lang/ui.zh-TW.json @@ -263,12 +263,6 @@ "highlight-code-structure-simple": { "defaultMessage": "簡單" }, - "ideas-tab": { - "defaultMessage": "靈感" - }, - "ideas-tab-description": { - "defaultMessage": "嘗試這些專案,進行修改並獲得靈感" - }, "insert-code-action": { "defaultMessage": "插入程式碼" }, @@ -317,9 +311,6 @@ "more-connect-options": { "defaultMessage": "更多連線選項" }, - "more-ideas": { - "defaultMessage": "更多構想" - }, "more-save-options": { "defaultMessage": "更多儲存選項" }, @@ -368,9 +359,6 @@ "offline-image-alt": { "defaultMessage": "圖像無法離線使用" }, - "open-action": { - "defaultMessage": "開啟" - }, "open-file-dropped": { "defaultMessage": "放置時開啟檔案" }, From 8f57a2de4159534b9442de209d111ef91a4ba8d2 Mon Sep 17 00:00:00 2001 From: Robert Knight Date: Tue, 20 Oct 2026 16:29:43 +0100 Subject: [PATCH 4/8] Remove links to reference and API in ideas dialogs These render as plain text instead --- .../common/DocumentationContent.tsx | 16 ++++++++++++++++ src/documentation/ideas/IdeaGuideContent.tsx | 15 ++++++++------- src/pages/IdeaDialog.tsx | 2 +- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/documentation/common/DocumentationContent.tsx b/src/documentation/common/DocumentationContent.tsx index 1ac1af49c..88ae5c6f7 100644 --- a/src/documentation/common/DocumentationContent.tsx +++ b/src/documentation/common/DocumentationContent.tsx @@ -42,6 +42,11 @@ interface DocumentationContextValue { isExpanded?: boolean; parentSlug?: string; toolkitType?: string; + /** + * Links to Reference and API, which navigate the editor's sidebar. False + * outside the editor, where they show as plain text. + */ + editorLinks?: boolean; } const DocumentationContext = React.createContext({}); @@ -66,10 +71,17 @@ const useCodeEmbedContext = () => { return { parentSlug, toolkitType }; }; +const useEditorLinks = () => + useContext(DocumentationContext).editorLinks !== false; + const DocumentationApiLinkMark = ( props: SerializerMarkProps ) => { const [, setState] = useRouterState(); + const editorLinks = useEditorLinks(); + if (!editorLinks) { + return <>{props.children}; + } return ( ) => { const [state, setState] = useRouterState(); + const editorLinks = useEditorLinks(); + if (!editorLinks) { + return <>{props.children}; + } return ( { +const IdeaGuideContent = ({ idea, inEditor = true }: IdeaGuideContentProps) => { // Runtime value derived from the image reference. const imagePadding = idea.image ? getAspectRatioPadding(idea.image.asset._ref) : undefined; return ( <> - {showImage && idea.image && ( + {inEditor && idea.image && ( diff --git a/src/pages/IdeaDialog.tsx b/src/pages/IdeaDialog.tsx index ff8f3a2fd..acba418dd 100644 --- a/src/pages/IdeaDialog.tsx +++ b/src/pages/IdeaDialog.tsx @@ -51,7 +51,7 @@ const IdeaDialog = ({ slug, title, onClose }: IdeaDialogProps) => { {/* Always rendered, so the code stays in the right-hand column. */} - {idea && } + {idea && } Date: Tue, 20 Oct 2026 16:41:28 +0100 Subject: [PATCH 5/8] Bring back the Ideas tab for projects not opened from an idea Reverts the removal of the old Ideas panel and its strings. Projects created from the home page idea dialog keep the Guide; other projects show the Ideas tab after Reference, as before. --- docs/analytics-events.md | 3 +- lang/ui.ca.json | 12 ++ lang/ui.de.json | 12 ++ lang/ui.en.json | 20 ++ lang/ui.es-ES.json | 12 ++ lang/ui.fr.json | 12 ++ lang/ui.ga-IE.json | 12 ++ lang/ui.ja.json | 12 ++ lang/ui.ko.json | 12 ++ lang/ui.lol.json | 12 ++ lang/ui.nl.json | 12 ++ lang/ui.pl.json | 12 ++ lang/ui.pt-BR.json | 15 ++ lang/ui.zh-CN.json | 12 ++ lang/ui.zh-TW.json | 12 ++ src/documentation/IdeasArea.tsx | 30 +++ src/documentation/api/ApiNode.tsx | 1 + src/documentation/common/CodeActionButton.tsx | 15 +- src/documentation/common/CodeEmbed.tsx | 36 ++-- .../common/DocumentationContent.tsx | 5 +- src/documentation/ideas/IdeaCard.tsx | 55 +++++ src/documentation/ideas/IdeaGuideContent.tsx | 1 + .../ideas/IdeasDocumentation.tsx | 195 ++++++++++++++++++ .../reference/ReferenceTopicEntry.tsx | 1 + src/e2e/app.ts | 6 +- src/e2e/documentation.test.ts | 15 ++ src/project/project-actions.tsx | 31 +++ src/router-hooks.test.tsx | 2 +- src/router-hooks.tsx | 10 +- src/workbench/SideBar.tsx | 21 +- 30 files changed, 582 insertions(+), 24 deletions(-) create mode 100644 src/documentation/IdeasArea.tsx create mode 100644 src/documentation/ideas/IdeaCard.tsx create mode 100644 src/documentation/ideas/IdeasDocumentation.tsx diff --git a/docs/analytics-events.md b/docs/analytics-events.md index 888510bb9..8e3648be0 100644 --- a/docs/analytics-events.md +++ b/docs/analytics-events.md @@ -181,7 +181,8 @@ Projects page only. Fires when the user changes the sort field or direction. ### `idea_open` -User created a project from an idea, in the home page idea dialog. +User opened an idea as a new project, from the home page idea dialog or the +editor's Ideas tab. | Param | Values | | ----- | -------------------------- | diff --git a/lang/ui.ca.json b/lang/ui.ca.json index 97b510517..be9646282 100644 --- a/lang/ui.ca.json +++ b/lang/ui.ca.json @@ -266,6 +266,12 @@ "highlight-code-structure-simple": { "defaultMessage": "Senzill" }, + "ideas-tab": { + "defaultMessage": "Idees" + }, + "ideas-tab-description": { + "defaultMessage": "Prova aquests projectes, modifica'ls i inspira't" + }, "insert-code-action": { "defaultMessage": "Insereix codi" }, @@ -314,6 +320,9 @@ "more-connect-options": { "defaultMessage": "Més opcions de connexió" }, + "more-ideas": { + "defaultMessage": "Més idees" + }, "more-save-options": { "defaultMessage": "Més opcions de desar" }, @@ -362,6 +371,9 @@ "offline-image-alt": { "defaultMessage": "La imatge no està disponible fora de línia" }, + "open-action": { + "defaultMessage": "Obert" + }, "open-file-dropped": { "defaultMessage": "Obre el fitxer quan es deixa anar" }, diff --git a/lang/ui.de.json b/lang/ui.de.json index 31700f99a..20fa82cbd 100644 --- a/lang/ui.de.json +++ b/lang/ui.de.json @@ -266,6 +266,12 @@ "highlight-code-structure-simple": { "defaultMessage": "Einfach" }, + "ideas-tab": { + "defaultMessage": "Ideen" + }, + "ideas-tab-description": { + "defaultMessage": "Probiere diese Projekte aus, ändere sie und lasse dich inspirieren" + }, "insert-code-action": { "defaultMessage": "Code einfügen" }, @@ -314,6 +320,9 @@ "more-connect-options": { "defaultMessage": "Mehr Verbindungsoptionen" }, + "more-ideas": { + "defaultMessage": "Mehr Ideen" + }, "more-save-options": { "defaultMessage": "Mehr Speicheroptionen" }, @@ -362,6 +371,9 @@ "offline-image-alt": { "defaultMessage": "Bild offline nicht verfügbar" }, + "open-action": { + "defaultMessage": "Öffnen" + }, "open-file-dropped": { "defaultMessage": "Datei nach Ablegen öffnen" }, diff --git a/lang/ui.en.json b/lang/ui.en.json index e2d4c6184..494fde5e7 100644 --- a/lang/ui.en.json +++ b/lang/ui.en.json @@ -159,6 +159,10 @@ "defaultMessage": "Confirm replace project", "description": "Confirmation message title for replacing project dialog" }, + "confirm-replace-with-idea": { + "defaultMessage": "Replace all files with {ideaName}?", + "description": "Confirmation message body for replacing project dialog with an idea" + }, "confirm-save-action": { "defaultMessage": "Confirm and save", "description": "Confirm and save action label" @@ -463,6 +467,14 @@ "defaultMessage": "Ideas", "description": "Heading of the home page row of idea cards" }, + "ideas-tab": { + "defaultMessage": "Ideas", + "description": "Ideas tab button text. Tab shows programs to give students ideas." + }, + "ideas-tab-description": { + "defaultMessage": "Try out these projects, modify them and get inspired", + "description": "Ideas tab description text. Tab shows programs to give students ideas." + }, "import-file-action": { "defaultMessage": "Import", "description": "Import file action" @@ -634,6 +646,10 @@ "defaultMessage": "More connect options", "description": "Aria label for the additional actions menu to the right of the Send to micro:bit button" }, + "more-ideas": { + "defaultMessage": "More ideas", + "description": "Link to more ideas on the microbit.org website at the end of the ideas section" + }, "more-save-options": { "defaultMessage": "More save options", "description": "Aria label for the additional actions menu to the right of the Save button" @@ -710,6 +726,10 @@ "defaultMessage": "Image unavailable offline", "description": "Alt text for an image placeholder when the user is offline" }, + "open-action": { + "defaultMessage": "Open", + "description": "Open button text" + }, "open-file-dropped": { "defaultMessage": "Open file when dropped", "description": "Aria label for file drop target" diff --git a/lang/ui.es-ES.json b/lang/ui.es-ES.json index 6a4c5f603..34a1f029f 100644 --- a/lang/ui.es-ES.json +++ b/lang/ui.es-ES.json @@ -266,6 +266,12 @@ "highlight-code-structure-simple": { "defaultMessage": "Simple" }, + "ideas-tab": { + "defaultMessage": "Ideas" + }, + "ideas-tab-description": { + "defaultMessage": "Prueba estos proyectos y modifícalos para inspirarte" + }, "insert-code-action": { "defaultMessage": "Insertar código" }, @@ -314,6 +320,9 @@ "more-connect-options": { "defaultMessage": "Más opciones de conexión" }, + "more-ideas": { + "defaultMessage": "Más ideas" + }, "more-save-options": { "defaultMessage": "Más opciones para guardar" }, @@ -362,6 +371,9 @@ "offline-image-alt": { "defaultMessage": "Imagen no disponible sin conexión" }, + "open-action": { + "defaultMessage": "Abrir" + }, "open-file-dropped": { "defaultMessage": "Abrir archivo al soltarlo" }, diff --git a/lang/ui.fr.json b/lang/ui.fr.json index d6a4391ac..b26ec02be 100644 --- a/lang/ui.fr.json +++ b/lang/ui.fr.json @@ -266,6 +266,12 @@ "highlight-code-structure-simple": { "defaultMessage": "Simple" }, + "ideas-tab": { + "defaultMessage": "Idées" + }, + "ideas-tab-description": { + "defaultMessage": "Essayez ces projets, modifiez-les et inspirez-vous en" + }, "insert-code-action": { "defaultMessage": "Insérer le code" }, @@ -314,6 +320,9 @@ "more-connect-options": { "defaultMessage": "Plus d'options de connexion" }, + "more-ideas": { + "defaultMessage": "Plus d'idées" + }, "more-save-options": { "defaultMessage": "Plus d'options d'enregistrement" }, @@ -362,6 +371,9 @@ "offline-image-alt": { "defaultMessage": "Image indisponible hors ligne" }, + "open-action": { + "defaultMessage": "Ouvrir" + }, "open-file-dropped": { "defaultMessage": "Ouvrir le fichier lorsqu’il est déposé" }, diff --git a/lang/ui.ga-IE.json b/lang/ui.ga-IE.json index 91b4dfba0..fa3af4b16 100644 --- a/lang/ui.ga-IE.json +++ b/lang/ui.ga-IE.json @@ -266,6 +266,12 @@ "highlight-code-structure-simple": { "defaultMessage": "Simplí" }, + "ideas-tab": { + "defaultMessage": "Smaointe" + }, + "ideas-tab-description": { + "defaultMessage": "Bain triail as na tionscadail seo, athraigh iad agus spreag iad" + }, "insert-code-action": { "defaultMessage": "Ionsáigh cód" }, @@ -314,6 +320,9 @@ "more-connect-options": { "defaultMessage": "Tuilleadh roghanna ceangail" }, + "more-ideas": { + "defaultMessage": "Tuilleadh smaointe" + }, "more-save-options": { "defaultMessage": "Tuilleadh roghanna sábhála" }, @@ -362,6 +371,9 @@ "offline-image-alt": { "defaultMessage": "Níl an íomhá ar fáil as líne" }, + "open-action": { + "defaultMessage": "Oscail" + }, "open-file-dropped": { "defaultMessage": "Oscail comhad nuair a thit sé" }, diff --git a/lang/ui.ja.json b/lang/ui.ja.json index 31113a36f..128cc1337 100644 --- a/lang/ui.ja.json +++ b/lang/ui.ja.json @@ -266,6 +266,12 @@ "highlight-code-structure-simple": { "defaultMessage": "簡易" }, + "ideas-tab": { + "defaultMessage": "アイデア" + }, + "ideas-tab-description": { + "defaultMessage": "これらのプロジェクトを試し、修正し、インスピレーションを得ることができます。" + }, "insert-code-action": { "defaultMessage": "コードを挿入" }, @@ -314,6 +320,9 @@ "more-connect-options": { "defaultMessage": "その他の接続オプション" }, + "more-ideas": { + "defaultMessage": "他のアイデア" + }, "more-save-options": { "defaultMessage": "その他の保存オプション" }, @@ -362,6 +371,9 @@ "offline-image-alt": { "defaultMessage": "画像がオフラインで利用できません" }, + "open-action": { + "defaultMessage": "開く" + }, "open-file-dropped": { "defaultMessage": "ドロップ時にファイルを開く" }, diff --git a/lang/ui.ko.json b/lang/ui.ko.json index 3c7cb926f..fda2ab891 100644 --- a/lang/ui.ko.json +++ b/lang/ui.ko.json @@ -263,6 +263,12 @@ "highlight-code-structure-simple": { "defaultMessage": "단순" }, + "ideas-tab": { + "defaultMessage": "아이디어들" + }, + "ideas-tab-description": { + "defaultMessage": "다양한 프로젝트를 체험해보고 수정하며 아이디어를 얻어보세요" + }, "insert-code-action": { "defaultMessage": "코드 삽입" }, @@ -311,6 +317,9 @@ "more-connect-options": { "defaultMessage": "연결 옵션 더 보기" }, + "more-ideas": { + "defaultMessage": "아이디어 더 보기" + }, "more-save-options": { "defaultMessage": "저장 옵션 더 보기" }, @@ -359,6 +368,9 @@ "offline-image-alt": { "defaultMessage": "이미지는 오프라인에서 사용할 수 없습니다." }, + "open-action": { + "defaultMessage": "열기" + }, "open-file-dropped": { "defaultMessage": "드롭될 때 파일 열기" }, diff --git a/lang/ui.lol.json b/lang/ui.lol.json index 0624ea9a5..e68fba758 100644 --- a/lang/ui.lol.json +++ b/lang/ui.lol.json @@ -266,6 +266,12 @@ "highlight-code-structure-simple": { "defaultMessage": "crwdns331540:0crwdne331540:0" }, + "ideas-tab": { + "defaultMessage": "crwdns331542:0crwdne331542:0" + }, + "ideas-tab-description": { + "defaultMessage": "crwdns331544:0crwdne331544:0" + }, "insert-code-action": { "defaultMessage": "crwdns331546:0crwdne331546:0" }, @@ -314,6 +320,9 @@ "more-connect-options": { "defaultMessage": "crwdns331576:0crwdne331576:0" }, + "more-ideas": { + "defaultMessage": "crwdns331578:0crwdne331578:0" + }, "more-save-options": { "defaultMessage": "crwdns331580:0crwdne331580:0" }, @@ -362,6 +371,9 @@ "offline-image-alt": { "defaultMessage": "crwdns359874:0crwdne359874:0" }, + "open-action": { + "defaultMessage": "crwdns331610:0crwdne331610:0" + }, "open-file-dropped": { "defaultMessage": "crwdns331614:0crwdne331614:0" }, diff --git a/lang/ui.nl.json b/lang/ui.nl.json index 39ffb0f19..25c1ba9ad 100644 --- a/lang/ui.nl.json +++ b/lang/ui.nl.json @@ -266,6 +266,12 @@ "highlight-code-structure-simple": { "defaultMessage": "Eenvoudig" }, + "ideas-tab": { + "defaultMessage": "Ideeën" + }, + "ideas-tab-description": { + "defaultMessage": "Probeer deze projecten uit, verander ze en laat je inspireren" + }, "insert-code-action": { "defaultMessage": "Code invoegen" }, @@ -314,6 +320,9 @@ "more-connect-options": { "defaultMessage": "Meer verbindings opties" }, + "more-ideas": { + "defaultMessage": "Meer ideeën" + }, "more-save-options": { "defaultMessage": "Meer opslagopties" }, @@ -362,6 +371,9 @@ "offline-image-alt": { "defaultMessage": "Afbeelding offline niet beschikbaar" }, + "open-action": { + "defaultMessage": "Open" + }, "open-file-dropped": { "defaultMessage": "Open bestand wanneer gedropt" }, diff --git a/lang/ui.pl.json b/lang/ui.pl.json index cee6cfb54..f3dbdf330 100644 --- a/lang/ui.pl.json +++ b/lang/ui.pl.json @@ -266,6 +266,12 @@ "highlight-code-structure-simple": { "defaultMessage": "Prosty" }, + "ideas-tab": { + "defaultMessage": "Pomysły" + }, + "ideas-tab-description": { + "defaultMessage": "Wypróbuj te projekty, zmodyfikuj je i poczuj się zainspirowany" + }, "insert-code-action": { "defaultMessage": "Wstaw kod" }, @@ -314,6 +320,9 @@ "more-connect-options": { "defaultMessage": "Więcej opcji połączenia" }, + "more-ideas": { + "defaultMessage": "Więcej pomysłów" + }, "more-save-options": { "defaultMessage": "Więcej opcji zapisu" }, @@ -362,6 +371,9 @@ "offline-image-alt": { "defaultMessage": "Obraz niedostępny w trybie offline" }, + "open-action": { + "defaultMessage": "Otwórz" + }, "open-file-dropped": { "defaultMessage": "Otwórz plik po upuszczeniu" }, diff --git a/lang/ui.pt-BR.json b/lang/ui.pt-BR.json index 03d1f09b2..5e75e4b48 100644 --- a/lang/ui.pt-BR.json +++ b/lang/ui.pt-BR.json @@ -101,6 +101,9 @@ "confirm-replace-title": { "defaultMessage": "Confirmar substituição do projeto" }, + "confirm-replace-with-idea": { + "defaultMessage": "Substituir todos os arquivos por {ideaName}?" + }, "confirm-save-action": { "defaultMessage": "Confirmar e salvar" }, @@ -275,6 +278,12 @@ "highlight-code-structure-simple": { "defaultMessage": "Simples" }, + "ideas-tab": { + "defaultMessage": "Ideias" + }, + "ideas-tab-description": { + "defaultMessage": "Experimente estes projetos, modifique-os e inspire-se" + }, "insert-code-action": { "defaultMessage": "Inserir código" }, @@ -323,6 +332,9 @@ "more-connect-options": { "defaultMessage": "Mais opções de conexão" }, + "more-ideas": { + "defaultMessage": "Mais ideias" + }, "more-save-options": { "defaultMessage": "Mais opções de salvamento" }, @@ -371,6 +383,9 @@ "offline-image-alt": { "defaultMessage": "Imagem indisponível offline" }, + "open-action": { + "defaultMessage": "Abrir" + }, "open-file-dropped": { "defaultMessage": "Abrir arquivo ao soltá-lo" }, diff --git a/lang/ui.zh-CN.json b/lang/ui.zh-CN.json index 69dc4a536..6980e8746 100644 --- a/lang/ui.zh-CN.json +++ b/lang/ui.zh-CN.json @@ -266,6 +266,12 @@ "highlight-code-structure-simple": { "defaultMessage": "简单" }, + "ideas-tab": { + "defaultMessage": "创意" + }, + "ideas-tab-description": { + "defaultMessage": "尝试这些项目,修改它们并获得灵感" + }, "insert-code-action": { "defaultMessage": "插入代码" }, @@ -314,6 +320,9 @@ "more-connect-options": { "defaultMessage": "更多连接选项" }, + "more-ideas": { + "defaultMessage": "More ideas" + }, "more-save-options": { "defaultMessage": "更多保存选项" }, @@ -362,6 +371,9 @@ "offline-image-alt": { "defaultMessage": "图片无法离线使用图像" }, + "open-action": { + "defaultMessage": "打开" + }, "open-file-dropped": { "defaultMessage": "放下时打开文件" }, diff --git a/lang/ui.zh-TW.json b/lang/ui.zh-TW.json index b13e40fc0..0a4ead808 100644 --- a/lang/ui.zh-TW.json +++ b/lang/ui.zh-TW.json @@ -263,6 +263,12 @@ "highlight-code-structure-simple": { "defaultMessage": "簡單" }, + "ideas-tab": { + "defaultMessage": "靈感" + }, + "ideas-tab-description": { + "defaultMessage": "嘗試這些專案,進行修改並獲得靈感" + }, "insert-code-action": { "defaultMessage": "插入程式碼" }, @@ -311,6 +317,9 @@ "more-connect-options": { "defaultMessage": "更多連線選項" }, + "more-ideas": { + "defaultMessage": "更多構想" + }, "more-save-options": { "defaultMessage": "更多儲存選項" }, @@ -359,6 +368,9 @@ "offline-image-alt": { "defaultMessage": "圖像無法離線使用" }, + "open-action": { + "defaultMessage": "開啟" + }, "open-file-dropped": { "defaultMessage": "放置時開啟檔案" }, diff --git a/src/documentation/IdeasArea.tsx b/src/documentation/IdeasArea.tsx new file mode 100644 index 000000000..cd00fcf49 --- /dev/null +++ b/src/documentation/IdeasArea.tsx @@ -0,0 +1,30 @@ +/** + * (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/api/ApiNode.tsx b/src/documentation/api/ApiNode.tsx index ecd375e9c..dfa3901a9 100644 --- a/src/documentation/api/ApiNode.tsx +++ b/src/documentation/api/ApiNode.tsx @@ -555,6 +555,7 @@ const DraggableSignature = ({ toDefault={() => setHighlighted(false)} codeAction={handleCopyCode} borderAdjustment={false} + toolkitType="api" /> ); diff --git a/src/documentation/common/CodeActionButton.tsx b/src/documentation/common/CodeActionButton.tsx index 8f1c48c3f..4235a83be 100644 --- a/src/documentation/common/CodeActionButton.tsx +++ b/src/documentation/common/CodeActionButton.tsx @@ -5,7 +5,7 @@ */ import { Button, Collapse, Icon } from "@microbit/ui"; import { useState } from "react"; -import { RiFileCopy2Line } from "react-icons/ri"; +import { RiFileCopy2Line, RiFolderOpenLine } from "react-icons/ri"; import { FormattedMessage } from "react-intl"; import { HStack } from "styled-system/jsx"; @@ -15,6 +15,7 @@ interface CodeActionButtonProps { toDefault: () => void; codeAction: () => void; borderAdjustment: boolean; + toolkitType?: string; } const CodeActionButton = ({ @@ -23,6 +24,7 @@ const CodeActionButton = ({ toDefault, codeAction, borderAdjustment, + toolkitType, }: CodeActionButtonProps) => { const [hovered, setHovered] = useState(false); return ( @@ -49,9 +51,16 @@ const CodeActionButton = ({ ml: "5", }} onPress={codeAction} - endIcon={} + startIcon={ + toolkitType === "ideas" ? : undefined + } + endIcon={ + toolkitType !== "ideas" ? : undefined + } > - + diff --git a/src/documentation/common/CodeEmbed.tsx b/src/documentation/common/CodeEmbed.tsx index 2c0924338..105f3eb25 100644 --- a/src/documentation/common/CodeEmbed.tsx +++ b/src/documentation/common/CodeEmbed.tsx @@ -23,6 +23,7 @@ import { useActiveEditorActions } from "../../editor/active-editor-hooks"; import CodeMirrorView from "../../editor/codemirror/CodeMirrorView"; import { debug as dndDebug, setDragContext } from "../../editor/codemirror/dnd"; import { useLogging } from "../../logging/logging-hooks"; +import { useProjectActions } from "../../project/project-hooks"; import { useSessionSettings } from "../../settings/session-settings"; import { useClipboard } from "@microbit/ui"; import DragHandle from "../common/DragHandle"; @@ -35,6 +36,7 @@ interface CodeEmbedProps { code: string; parentSlug?: string; toolkitType?: string; + title?: string; } type CodeEmbedState = @@ -55,6 +57,7 @@ const CodeEmbed = ({ code: codeWithImports, toolkitType, parentSlug, + title, }: CodeEmbedProps) => { const [copyCodeOpen, setCopyCodeOpen] = useState(false); const [state, originalSetState] = useState("default"); @@ -119,6 +122,10 @@ const CodeEmbed = ({ toolkitType ); }, [actions, code, codeWithImports, onCopy, parentSlug, toolkitType]); + const projectActions = useProjectActions(); + const handleOpenIdea = useCallback(async () => { + projectActions.openIdea(parentSlug, codeWithImports, title!); + }, [codeWithImports, parentSlug, projectActions, title]); const lineCount = code.trim().split("\n").length; const codeRef = useRef(null); const textHeight = lineCount * 1.375 + "em"; @@ -127,7 +134,8 @@ const CodeEmbed = ({ const hotKeysRef = useHotkeys(keyboardShortcuts.copyCode, handleCopyCode, { preventDefault: true, }) as Ref; - const raisedLook = state !== "default"; + const raisedLook = + toolkitType === "ideas" ? state === "highlighted" : state !== "default"; return ( ); @@ -210,7 +219,7 @@ const CodePopUp = ({ concise={concise} full={full} // We're always "raised" as this is the pop-up. - raisedLook + raisedLook={toolkitType !== "ideas"} highlightDragHandle toolkitType={toolkitType} parentSlug={parentSlug} @@ -289,13 +298,14 @@ const Code = React.forwardRef( ); const intl = useIntl(); const [{ dragDropSuccess }] = useSessionSettings(); - const tooltipDisabled = Boolean(dragDropSuccess); + const tooltipDisabled = + toolkitType === "ideas" ? true : Boolean(dragDropSuccess); const content = ( ( ref={ref} gap="0" onClick={() => onToggle(!isOpen)} - onDragStart={handleDragStart} + onDragStart={toolkitType === "ideas" ? () => {} : handleDragStart} onDragEnd={handleDragEnd} - cursor="grab" + cursor={toolkitType === "ideas" ? "default" : "grab"} position="absolute" fontSize={isPopUp ? "md" : undefined} zIndex={isPopUp ? "codePopUp" : "code"} @@ -322,10 +332,12 @@ const Code = React.forwardRef( - + {toolkitType !== "ideas" && ( + + )} { }; const useCodeEmbedContext = () => { - const { parentSlug, toolkitType } = useContext(DocumentationContext); - return { parentSlug, toolkitType }; + const { parentSlug, toolkitType, title } = useContext(DocumentationContext); + return { parentSlug, toolkitType, title }; }; const useEditorLinks = () => diff --git a/src/documentation/ideas/IdeaCard.tsx b/src/documentation/ideas/IdeaCard.tsx new file mode 100644 index 000000000..11f6d1b38 --- /dev/null +++ b/src/documentation/ideas/IdeaCard.tsx @@ -0,0 +1,55 @@ +/** + * (c) 2021-2022, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { styled } from "styled-system/jsx"; +import { imageUrlBuilder } from "../../common/imageUrlBuilder"; +import { SimpleImage } from "../../common/sanity"; +import DocumentationHeading from "../common/DocumentationHeading"; +import ImageWithFallback from "../common/ImageWithFallback"; +import OfflineImageFallback from "../OfflineImageFallback"; + +interface IdeaCardProps { + name: string; + image: SimpleImage; + isV2Only?: boolean; + onClick: () => void; +} + +const IdeaCard = ({ name, image, isV2Only, onClick }: IdeaCardProps) => { + return ( + + } + alt="" + borderTopRadius="lg" + width="500px" + /> + + + ); +}; + +export default IdeaCard; diff --git a/src/documentation/ideas/IdeaGuideContent.tsx b/src/documentation/ideas/IdeaGuideContent.tsx index 5dffeab4c..f8f157684 100644 --- a/src/documentation/ideas/IdeaGuideContent.tsx +++ b/src/documentation/ideas/IdeaGuideContent.tsx @@ -58,6 +58,7 @@ const IdeaGuideContent = ({ idea, inEditor = true }: IdeaGuideContentProps) => { diff --git a/src/documentation/ideas/IdeasDocumentation.tsx b/src/documentation/ideas/IdeasDocumentation.tsx new file mode 100644 index 000000000..7a6db029e --- /dev/null +++ b/src/documentation/ideas/IdeasDocumentation.tsx @@ -0,0 +1,195 @@ +/** + * (c) 2021-2022, Micro:bit Educational Foundation and contributors + * + * SPDX-License-Identifier: MIT + */ +import { Link, Text } from "@microbit/ui"; +import { CSSProperties, ReactNode, useCallback, useRef } from "react"; +import { FormattedMessage, useIntl } from "react-intl"; +import { Box, Grid, Stack } from "styled-system/jsx"; +import AreaHeading from "../../common/AreaHeading"; +import { + docStylesClass, + runtimeAspectRatioClass, +} from "../../common/documentation-styles"; +import HeadedScrollablePanel from "../../common/HeadedScrollablePanel"; +import { + getAspectRatioPadding, + imageUrlBuilder, +} from "../../common/imageUrlBuilder"; +import { useResizeObserverContentRect } from "../../common/use-resize-observer"; +import { Anchor, useRouterTabSlug } from "../../router-hooks"; +import { useAnimationDirection } from "../common/documentation-animation-hooks"; +import DocumentationBreadcrumbHeading from "../common/DocumentationBreadcrumbHeading"; +import DocumentationContent, { + DocumentationContextProvider, +} from "../common/DocumentationContent"; +import ImageWithFallback from "../common/ImageWithFallback"; +import { isV2Only } from "../common/model"; +import IdeaCard from "./IdeaCard"; +import { Idea } from "./model"; +import OfflineImageFallback from "../OfflineImageFallback"; +import { microbitOrgMiciProjectsUrl } from "../../external-links"; +import { useSettings } from "../../settings/settings"; + +interface IdeasDocumentationProps { + ideas: Idea[]; +} + +const IdeasDocumentation = ({ ideas }: IdeasDocumentationProps) => { + const [anchor, setAnchor] = useRouterTabSlug("ideas"); + const direction = useAnimationDirection(anchor); + const ideaId = anchor?.id; + const handleNavigate = useCallback( + (ideaId: string | undefined) => { + setAnchor(ideaId ? { id: ideaId } : undefined, "user"); + }, + [setAnchor] + ); + return ( + + ); +}; + +interface ActiveLevelProps extends IdeasDocumentationProps { + anchor: Anchor | undefined; + ideaId: string | undefined; + onNavigate: (ideaId: string | undefined) => void; + direction: "forward" | "back" | "none"; +} + +const ActiveLevel = ({ + ideaId, + onNavigate, + ideas, + direction, +}: ActiveLevelProps) => { + const activeIdea = ideas.find((idea) => idea.slug.current === ideaId); + const intl = useIntl(); + const [{ languageId }] = useSettings(); + const headingString = intl.formatMessage({ id: "ideas-tab" }); + const ref = useRef(null); + const contentRect = useResizeObserverContentRect(ref); + const contentWidth = contentRect?.width ?? 0; + const numCols = + !contentWidth || contentWidth > 1100 ? 3 : contentWidth > 550 ? 2 : 1; + if (activeIdea) { + // Runtime value derived from the image reference. + const imagePadding = activeIdea.image + ? getAspectRatioPadding(activeIdea.image.asset._ref) + : undefined; + return ( + onNavigate(undefined)} + isV2Only={isV2Only(activeIdea)} + /> + } + > + {activeIdea.content && ( + + {activeIdea.image && ( + + } + alt="" + borderTopRadius="lg" + /> + + )} + + + + + + )} + + ); + } + return ( + + } + > + + {ideas.map((idea) => ( + onNavigate(idea.slug.current)} + /> + ))} + + + ( + + {chunks} + + ), + }} + /> + + + ); +}; + +export default IdeasDocumentation; diff --git a/src/documentation/reference/ReferenceTopicEntry.tsx b/src/documentation/reference/ReferenceTopicEntry.tsx index aa6c60a0f..6f7b3e1dd 100644 --- a/src/documentation/reference/ReferenceTopicEntry.tsx +++ b/src/documentation/reference/ReferenceTopicEntry.tsx @@ -92,6 +92,7 @@ const ReferenceTopicEntry = ({ parentSlug={entry.slug.current} toolkitType={toolkitType} isExpanded={disclosure.isOpen} + title={topic.name} > { + await this.page.getByRole("button", { name }).click(); + } + async connect(): Promise { await this.moreConnectionOptionsButton.click(); await this.page.getByRole("menuitem", { name: "Connect" }).click(); diff --git a/src/e2e/documentation.test.ts b/src/e2e/documentation.test.ts index 375f7a632..7a63976a6 100644 --- a/src/e2e/documentation.test.ts +++ b/src/e2e/documentation.test.ts @@ -68,4 +68,19 @@ test.describe("documentation", () => { "Count and repeat sets of instructions" ); }); + + test("Ideas tab navigation", async ({ app }) => { + await app.switchTab("Ideas"); + await app.expectDocumentationTopLevelHeading( + "Ideas", + "Try out these projects, modify them and get inspired" + ); + }); + + test("Select an idea", async ({ app }) => { + const ideaName = "Emotion badge"; + await app.switchTab("Ideas"); + await app.selectDocumentationIdea(ideaName); + await app.expectDocumentationTopLevelHeading(ideaName); + }); }); diff --git a/src/project/project-actions.tsx b/src/project/project-actions.tsx index f30fc7672..9b8ecdbce 100644 --- a/src/project/project-actions.tsx +++ b/src/project/project-actions.tsx @@ -250,6 +250,37 @@ export class ProjectActions { load = (files: File[], source: ImportSource = "file_picker"): Promise => this.importer.importIntoEditor(files, source); + /** + * Opens an idea from the documentation as a new project. + */ + openIdea = async (slug: string | undefined, code: string, title: string) => { + this.logging.event({ + type: "idea_open", + detail: { id: slug }, + }); + try { + const opened = await this.importer.newProject( + title, + { [MAIN_FILE]: new TextEncoder().encode(code) }, + this.intl.formatMessage( + { id: "confirm-replace-with-idea" }, + { ideaName: title } + ) + ); + if (!opened) { + return; + } + this.actionFeedback.success({ + title: this.intl.formatMessage( + { id: "loaded-file-feedback" }, + { filename: title } + ), + }); + } catch (e) { + this.actionFeedback.unexpectedError(e); + } + }; + /** * Flash the device, reporting progress via a dialog. */ diff --git a/src/router-hooks.test.tsx b/src/router-hooks.test.tsx index 9dcd5066e..1062efd46 100644 --- a/src/router-hooks.test.tsx +++ b/src/router-hooks.test.tsx @@ -112,7 +112,7 @@ describe("useRouterState", () => { it("does not log without a source", async () => { const { logging } = renderAt("/project"); await act(async () => { - setState({ tab: "guide" }); + setState({ tab: "ideas" }); }); expect(logging.events).toEqual([]); }); diff --git a/src/router-hooks.tsx b/src/router-hooks.tsx index a79eab5da..2786580fe 100644 --- a/src/router-hooks.tsx +++ b/src/router-hooks.tsx @@ -15,9 +15,15 @@ import { useLogging } from "./logging/logging-hooks"; import { createEditorUrl, createIframeEditorUrl } from "./urls"; import { useIframeMode } from "./iframe-mode-hooks"; -export type TabName = "api" | "guide" | "reference" | "project"; +export type TabName = "api" | "guide" | "ideas" | "reference" | "project"; -const tabNames: readonly string[] = ["api", "guide", "reference", "project"]; +const tabNames: readonly string[] = [ + "api", + "guide", + "ideas", + "reference", + "project", +]; export const isTabName = (value: string | undefined): value is TabName => value !== undefined && tabNames.includes(value); diff --git a/src/workbench/SideBar.tsx b/src/workbench/SideBar.tsx index 952e5ff29..adde153e7 100644 --- a/src/workbench/SideBar.tsx +++ b/src/workbench/SideBar.tsx @@ -14,7 +14,11 @@ import { } from "react"; import { TabList, TabPanel, TabPanels, Tabs } from "react-aria-components"; import { IconType } from "react-icons"; -import { RiBookOpenLine, RiHome2Line } from "react-icons/ri"; +import { + RiBookOpenLine, + RiHome2Line, + RiLightbulbFlashLine, +} from "react-icons/ri"; import { VscFiles, VscLibrary } from "react-icons/vsc"; import { useIntl } from "react-intl"; import { Link as RouterLink } from "react-router"; @@ -26,6 +30,7 @@ import ErrorBoundary from "../common/ErrorBoundary"; import PythonLogo from "../common/PythonLogo"; import ApiArea from "../documentation/ApiArea"; import GuideArea from "../documentation/GuideArea"; +import IdeasArea from "../documentation/IdeasArea"; import ReferenceArea from "../documentation/ReferenceArea"; import { flags } from "../flags"; import { useProjectsDatabaseActive } from "../fs/storage-status"; @@ -82,7 +87,8 @@ const SideBar = ({ }: SideBarProps) => { const intl = useIntl(); const [releaseDialog, setReleaseDialog] = useReleaseDialogState(); - // Only projects opened from an idea have its guide. + // Projects opened from an idea on the home page have its guide instead of + // the list of ideas. const hasGuide = useOpenIdeaSlug() !== undefined; const panes: Pane[] = useMemo(() => { const result: Pane[] = [ @@ -104,6 +110,17 @@ const SideBar = ({ contents: , color: "gray.75", }, + ...(hasGuide + ? [] + : [ + { + id: "ideas" as const, + title: intl.formatMessage({ id: "ideas-tab" }), + icon: RiLightbulbFlashLine, + contents: , + color: "gray.75", + }, + ]), { id: "api" as const, title: intl.formatMessage({ id: "api-tab" }), From 0af4a7cfded64a3e24b9973911e6346e35dbb5f0 Mon Sep 17 00:00:00 2001 From: Robert Knight Date: Tue, 20 Oct 2026 16:44:24 +0100 Subject: [PATCH 6/8] Move the sidebar home link down beside settings and help Same size and style as the settings and help menus, and the tab strip's spacer is back to how it was. --- src/workbench/SideBar.tsx | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/workbench/SideBar.tsx b/src/workbench/SideBar.tsx index adde153e7..570718cc4 100644 --- a/src/workbench/SideBar.tsx +++ b/src/workbench/SideBar.tsx @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: MIT */ -import { Icon, usePrevious } from "@microbit/ui"; +import { usePrevious } from "@microbit/ui"; import { ReactNode, useCallback, @@ -23,7 +23,7 @@ import { VscFiles, VscLibrary } from "react-icons/vsc"; import { useIntl } from "react-intl"; import { Link as RouterLink } from "react-router"; import { css, cx } from "styled-system/css"; -import { Flex, styled, VStack } from "styled-system/jsx"; +import { Box, Flex, styled, VStack } from "styled-system/jsx"; import { button } from "styled-system/recipes"; import { SystemStyleObject } from "styled-system/types"; import ErrorBoundary from "../common/ErrorBoundary"; @@ -264,16 +264,7 @@ const SideBar = ({ branded background (a wrapper because react-aria's TabList may only contain tabs). */} - - - + + @@ -350,8 +342,8 @@ const SideBar = ({ }; /** - * Home, above the tabs and apart from them. Only where there is a home page, - * as for the header logo. + * Home, above the settings and help menus and styled as they are. Only where + * there is a home page, as for the header logo. */ const SideBarHomeLink = () => { const intl = useIntl(); @@ -366,11 +358,11 @@ const SideBarHomeLink = () => { aria-label={intl.formatMessage({ id: "home-action" })} className={cx( button({ variant: "sidebar", size: "lg" }), - css({ px: "0" }) + // As IconButton (px) and the menus' fontSize. + css({ px: "0", fontSize: "xl" }) )} > - {/* Sized as the tab icons rather than the menus below. */} - + ); }; From eb18b980020925db9baed1d439f3224dd7ba58c0 Mon Sep 17 00:00:00 2001 From: Robert Knight Date: Tue, 20 Oct 2026 16:53:16 +0100 Subject: [PATCH 7/8] Bump theme version --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ab75d222..58de1ef37 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ env: # renovate: datasource=npm depName=npm NPM_VERSION: "11" # renovate: datasource=npm depName=@microbit-foundation/python-editor-v3-microbit registryUrl=https://npm.pkg.github.com - THEME_VERSION: 0.0.0-home.page.124 + THEME_VERSION: 0.0.0-home.page.125 jobs: stages: From 1af8ab37a36540bbef4d7b059e148ce5fdb7be32 Mon Sep 17 00:00:00 2001 From: Robert Knight Date: Wed, 21 Oct 2026 13:10:20 +0100 Subject: [PATCH 8/8] Bring files tab up so it's grouped with the rest --- src/workbench/SideBar.tsx | 6 +----- src/workbench/SideBarTab.tsx | 3 --- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/workbench/SideBar.tsx b/src/workbench/SideBar.tsx index 570718cc4..04b2742a7 100644 --- a/src/workbench/SideBar.tsx +++ b/src/workbench/SideBar.tsx @@ -54,7 +54,6 @@ export interface Pane { title: string; contents: ReactNode; color: string; - mb?: string; } interface SideBarProps { @@ -127,7 +126,6 @@ const SideBar = ({ icon: PythonLogo as IconType, contents: , color: "gray.75", - mb: "auto", }, { id: "project" as const, @@ -266,9 +264,7 @@ const SideBar = ({ void; active: boolean; tabIndex: number; @@ -23,7 +22,6 @@ const SideBarTab = ({ icon, title, color, - mb, handleTabClick, active, }: SideBarTabProps) => { @@ -51,7 +49,6 @@ const SideBarTab = ({ p: "0", position: "relative", ml: "6px", - mb: mb ? "auto" : "0", borderRadius: "32px 0 0 32px", transition: "none", cursor: "pointer",