Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e07d754
implemented demos page
MartinNang Jan 23, 2026
a1765e6
added next and last buttons to pagination
MartinNang Jan 26, 2026
e04cc62
updated card styling and animations for dropdown menus on my projects…
MartinNang Jan 26, 2026
1748af7
filter demos when clicking on icons; updated card styling and animati…
MartinNang Jan 26, 2026
0d231a7
created demo sidebar element to activity bar (wip)
MartinNang Jan 26, 2026
19aa89d
designed sidebar
MartinNang Jan 27, 2026
7ac2725
implemented demo linked content for flat projects
MartinNang Feb 5, 2026
5b9e22c
create new demo projects from discoverable demos page
MartinNang Feb 17, 2026
14b9caf
implemented demos page
MartinNang Jan 23, 2026
2e9e5c3
created demo sidebar element to activity bar
MartinNang Jan 26, 2026
ebe0859
Update marked
bennorth Dec 12, 2025
c138781
markedParse(): Define helper
bennorth Dec 12, 2025
15751b2
Update marked usages to markedParse
bennorth Dec 12, 2025
b1bd24f
Added better keyboard navigation to demo sidebar;
MartinNang Feb 20, 2026
e249fb6
fixup! Added better keyboard navigation to demo sidebar; Added commen…
MartinNang Feb 23, 2026
f3e25ba
Remove unused file
bennorth Feb 28, 2026
6fdeeab
(Reformat)
bennorth Feb 28, 2026
adcf603
(Reformat / reorder some code)
bennorth Mar 4, 2026
2ee859d
(Reformat)
bennorth Mar 4, 2026
ebed8fc
Note some BN comments
bennorth Mar 4, 2026
8b6575b
Add/expand comments
bennorth Mar 6, 2026
86303de
Implementing BN's suggestions: removed placeholders; refactoring; cre…
MartinNang Mar 20, 2026
e7bacf8
updated IDE stylings
MartinNang Mar 25, 2026
c8ec8c7
added parameter for toggle icon element to CaptiveContextMenu
MartinNang Mar 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,313 changes: 1,236 additions & 77 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"easy-peasy": "^6.1.0",
"file-saver": "^2.0.5",
"jszip": "^3.10.1",
"marked": "^5.1.0",
"marked": "^17.0.1",
"mime-types": "^2.1.35",
"react": "^19.2.0",
"react-ace": "^14.0.1",
Expand All @@ -27,6 +27,7 @@
"react-dom": "^19.2.0",
"react-error-boundary": "^6.0.0",
"react-image-crop": "^11.0.10",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.9.6",
"scratchblocks": "^3.6.2",
Expand Down
46 changes: 46 additions & 0 deletions public/data/demos/demos.json

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions public/data/demos/firing-projectile/description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Basic Firing
## Player
The script for the player isn't very important for this example.
Just glide left and right across the bottom of the screen.

## Bullet
### When green start button is pressed
The bullet can be in one of two states. Either we're waiting to be fired, or we're in the middle of being fired up the screen. Keep
track of which state we're in, by remembering whether we're in the middle of being fired. At the start, we're NOT in the middle of
being fired.

While we're waiting to be fired, we don't want to be visible.

### When "space" key is pressed
Once we've started being fired up the screen, we want pressing space
to have no effect. So quit this script before doing any real work
if we are already in the middle of being fired.

Once we get here, we ARE in the middle of being fired. Remember
that fact.

Find the player sprite and move ourselves to its location, except a bit higher ("+20" for the Y coordinate) so the bullet appears at the top of the ship.

Become visible.

Move quite quickly up the screen, until we're well off the top.

We are no longer in the middle of being fired, so remember that.

# Advanced Firing
## Player
The script for the player isn't very important for this example.
Just glide left and right across the bottom of the screen.

## Bullet
### When green start button is pressed
This variable doesn't change, but it's helpful to have a named value
for the maximum number of Bullet clones we want to be allowed to
exist at one time.

### When "space" key is pressed
Make sure that only the original (hidden) Bullet responds to the
keypress. Without this, every clone makes a new clone and we soon
have thousands of clones.

If there are already the maximum number of Bullet clones being fired, stop here.

Make the new Bullet clone; the "when I start as a clone" script below does the rest of the work.

### When bullet starts as a clone
Find the player sprite and move ourselves to its location, except a bit higher ("+20" for the Y coordinate) so the bullet appears at the top of the ship.

Become visible.

Move quite quickly up the screen, until we're well off the top.

This clone has finished its job, so delete it to avoid cluttering up
the game with lots of clones.
Binary file added public/data/demos/firing-projectile/project.zip
Binary file not shown.
31 changes: 31 additions & 0 deletions public/data/demos/jumping/description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Basic Jumping
Thanks [GrafxKid](https://opengameart.org/content/green-robot) for costume!

Start off in the middle left/right, and quite near the bottom,
to stand on the earth.

We need to remember whether we're currently in the middle of a jump
or not. We start NOT in the middle of a jump.

Continuously check for left/right keys being pressed, and adjust our
X coordinate if needed. This simple version does not check for
where the player is on the screen, so you can move the ship right
outside the screen. It does choose a different costume for moving
left vs moving right, though, to look better.

If we're already in the middle of jumping, quit this script
immediately, so the player can't stack jumps.

Remember that we ARE now jumping.

# Smooth Jumping

To get a smooth jump, start by moving up quite quickly, then reduce
how much we move up every frame. At some point, the y_velocity
variable will become NEGATIVE, meaning we will move DOWN. We stop
this whole process just after we've done the movement with a velociy
equal to the negative of the starting velocity, when we will be
vertically back where we started.

Record the fact that we have finished jumping, and so it's OK to
jump again next time the player presses space.
Binary file added public/data/demos/jumping/project.zip
Binary file not shown.
35 changes: 35 additions & 0 deletions public/data/demos/smooth-movement/description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Version 1
Keep the speed in a variable. Even though we don't change the value, giving it a name makes the code easier to read and easier to
change.

Continuously check for movement keys being pressed, and adjust our X
or Y coordinate if needed. This simple version does not check for
where the player is on the screen, so you can move the ship right
outside the screen.

# Version 2
## `x_limit`, `y_limit`
Likewise, keep the X and Y coordinate limits (the furthest we want the player to be able to go left and right, and up
and down) in variables.

# Version 3
## `x_velocity`, `y_velocity`
These variables DO change as the player plays the game. They track
the current speed of the player in each dimension.

## `accel`
This variable doesn't change value, but (as above) it's useful to
give it a name. This is the acceleration, which says how quickly
the player's speed changes.

### Drift
Continuously check for movement keys being pressed, and adjust our X
or Y velocity if needed, up to the "speed" limit. <mark>If neither
left/right key is pressed, drift the horizontal velocity
(x_velocity) towards zero. And if neither up/down key is pressed,
drift the vertical velocity (y_velocity) towards zero. Then update
the position according to the velocity, making sure we don't go
outside the screen limits.</mark>

It might also be nice to eventually have a pytch filetype if the project structure become more complex and we ever ended
up needing different filetypes for different purposes (like maybe for raspberry pi projects or )
Binary file added public/data/demos/smooth-movement/project.zip
Binary file not shown.
6 changes: 6 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import "./pytch-ide.scss";
import "./pytch-jr-ide.scss";
import "./pytch-jr-lesson.scss";
import "./help-sidebar.scss";
import "./demo-sidebar.scss";
import "./font-awesome-lib";

import { AllModals } from "./components/AllModals";
Expand All @@ -32,6 +33,7 @@ import { DeliberateFailureWithBoundary } from "./components/DeliberateFailure";
import { fireAndForgetEvent } from "./model/anonymous-instrumentation";
import { StandalonePlayDemo } from "./components/StandalonePlayDemo";
import { StartTutorialAtCheckpoint } from "./components/StartTutorialAtCheckpoint";
import { DemosList } from "./components/discoverable-demos-page/DemosList";

const UnknownRoute: React.FC<EmptyProps> = () => {
return (
Expand Down Expand Up @@ -98,6 +100,10 @@ function App() {
path: "tutorials/",
element: <TutorialList />,
},
{
path: "demos/",
element: <DemosList />,
},
{
path: "ide/:projectIdString",
element: <IDE />,
Expand Down
13 changes: 11 additions & 2 deletions src/components/CaptiveContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import React, {
import { Dropdown } from "react-bootstrap";
import { useNonNullContext } from "./hooks/non-null-context";
import { handleMovementKeys } from "./CaptiveContextMenu-utils";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";

/** Context for internal use by dropdown items within the container.
* Allows items to, e.g., dismiss the dropdown menu. */
Expand Down Expand Up @@ -219,11 +220,17 @@ const Container: React.FC<PropsWithChildren<ContainerProps>> = ({
};

////////////////////////////////////////////////////////////////////////
type MenuProps = {
toggle?: React.ReactNode;
};

/** Menu of choices relevant to the element which has a captive context
* menu. Should be rendered somewhere within a
* `CaptiveContextMenu.Container`. */
const DropdownMenu: React.FC<PropsWithChildren<object>> = ({ children }) => {
const DropdownMenu: React.FC<PropsWithChildren<MenuProps>> = ({
toggle,
children,
}) => {
const ctx = useNonNullContext(Context);

const onKeydown: KeyboardEventHandler = (evt) => {
Expand All @@ -245,7 +252,9 @@ const DropdownMenu: React.FC<PropsWithChildren<object>> = ({ children }) => {
onKeyDown={onKeydown}
data-captive-context-menu-container-id={ctx.containerId}
>
<Dropdown.Toggle as="div">⋮</Dropdown.Toggle>
<Dropdown.Toggle as="div" className={"captive-dropdown-toggle"}>
{toggle || "⋮"}
</Dropdown.Toggle>
<Dropdown.Menu align="end">{children}</Dropdown.Menu>
</Dropdown>
);
Expand Down
1 change: 1 addition & 0 deletions src/components/IDELayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const IDELayout: React.FC<EmptyProps> = () => {
case "expanded-specimen":
case "expanded-lesson":
case "expanded-tutorial":
case "expanded-demo":
return 1;
case "expanded-keynavhelp":
console.warn("should not have expanded-keynavhelp on first render");
Expand Down
6 changes: 5 additions & 1 deletion src/components/Junior/ActivityBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useJrEditActions, useJrEditState } from "./hooks";
import classNames from "classnames";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconName } from "@fortawesome/fontawesome-common-types";
import { useHasLinkedLesson, useHasLinkedSpecimen } from "./lesson/hooks";
import { useHasLinkedDemo, useHasLinkedLesson, useHasLinkedSpecimen } from "./lesson/hooks";
import { EmptyProps } from "../../utils";
import { useStoreState } from "../../store";
import { Nav } from "react-bootstrap";
Expand All @@ -23,6 +23,7 @@ const uiDetailsFromTabKeyLut = new Map<ActivityBarTabKey, TabKeyUiDetails>([
["lesson", { icon: "book", tooltip: "Lesson content" }],
["tutorial", { icon: "book", tooltip: "Tutorial content" }],
["specimen", { icon: "book", tooltip: "Lesson information" }],
["demo", { icon: "play", tooltip: "Demo content" }],
]);

function uiDetailsFromTabKey(tab: ActivityBarTabKey): TabKeyUiDetails {
Expand Down Expand Up @@ -86,13 +87,16 @@ export const ActivityBar: React.FC<EmptyProps> = () => {
const hasLinkedTutorial = useStoreState(
(state) => state.activeProject.project?.trackedTutorial != null
);
const hasLinkedDemo = useHasLinkedDemo();

const tabs: Array<ActivityBarTabKey> = hasLinkedLesson
? ["helpsidebar", "lesson", "keynavhelp"]
: hasLinkedSpecimen
? ["helpsidebar", "specimen", "keynavhelp"]
: hasLinkedTutorial
? ["helpsidebar", "tutorial", "keynavhelp"]
: hasLinkedDemo
? ["helpsidebar", "demo", "keynavhelp"]
: ["helpsidebar", "keynavhelp"];

const focusGroupExtraClass =
Expand Down
22 changes: 21 additions & 1 deletion src/components/Junior/ActivityContent.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React from "react";
import { EmptyProps, assertNever } from "../../utils";
import { useJrEditState } from "./hooks";
import { MaybeContent as MaybeLessonContent } from "./lesson/MaybeContent";
import {
MaybeContent,
MaybeContent as MaybeLessonContent,
} from "./lesson/MaybeContent";
import { WidthMonitor } from "./WidthMonitor";
import { HelpSidebar } from "../HelpSidebar";
import Tutorial from "../Tutorial";
Expand All @@ -14,8 +17,25 @@ export const ActivityContent: React.FC<EmptyProps> = () => {
return <WidthMonitor nonStageWd={576} />;
}

/* TODO There are, on the surface, two places the DemoSidebar is rendered.
The one which is followed is under case "demo" here. There is another
one, though, in MaybeLessonContent (nb that is the name given on
import), but that is dead code because we never get to
MaybeLessonContent unless we're in tab "lesson" or "specimen". It
would be easier to go through MaybeContent, because that takes care of
the loading machinery. - Done*/

const content = (() => {
switch (s.tab) {
case "demo":
return (
<>
<WidthMonitor nonStageWd={980} />
<div className={"bg-white h-100"}>
<MaybeContent />
</div>
</>
);
case "helpsidebar":
return (
<>
Expand Down
5 changes: 4 additions & 1 deletion src/components/Junior/ActorsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { CaptiveContextMenu } from "../CaptiveContextMenu";
import { kFocusGroupItemClassName } from "../../model/junior/grouped-focus";
import { useFocusContext } from "../hooks/focus-steering";
import { FocusGroupContainer } from "../FocusGroupContainer";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";

type ActorThumbnailProps = { id: Uuid };
const ActorThumbnail: React.FC<ActorThumbnailProps> = ({ id }) => {
Expand Down Expand Up @@ -131,7 +132,9 @@ const ActorCardDropdown: React.FC<ActorCardDropdownProps> = ({
});

return (
<CaptiveContextMenu.DropdownMenu>
<CaptiveContextMenu.DropdownMenu
toggle={<FontAwesomeIcon icon={"caret-down"} />}
>
<CaptiveContextMenu.DropdownItem {...onInvokeProps("code")}>
Go to code
</CaptiveContextMenu.DropdownItem>
Expand Down
4 changes: 3 additions & 1 deletion src/components/Junior/AssetCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,9 @@ const AssetCardDropdown: React.FC<AssetCardDropdownProps> = ({
);

return (
<CaptiveContextMenu.DropdownMenu>
<CaptiveContextMenu.DropdownMenu
toggle={<FontAwesomeIcon icon={"caret-down"} />}
>
<CopyAssetNameDropdownItem
operationScope={operationScope}
assetName={displayName}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Junior/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ const ScriptsEditor = () => {
groupedFocusKey={`ActorProperties/${actorId}/code`}
opts={{ onReorder }}
>
<div ref={scriptsDivRef} className="pt-2 pb-5 Junior-ScriptsEditor">
<div ref={scriptsDivRef} className="pt-2 pb-5 Junior-ScriptsEditor flex-grow-1">
{maybeNoContentHelp}
<ol className="Junior-ScriptsList">{scriptsContent}</ol>
</div>
Expand Down
5 changes: 4 additions & 1 deletion src/components/Junior/HatBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from "./hooks";
import { CaptiveContextMenu } from "../CaptiveContextMenu";
import { useFocusContext } from "../hooks/focus-steering";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";

/** See docstring for `HatBlockContent`. */
type DisplayVariant = "kind-chosen" | "fully-specified" | "in-editor";
Expand Down Expand Up @@ -179,7 +180,9 @@ export const HatBlock: React.FC<HatBlockProps> = ({
event={event}
variant="in-editor"
/>
<CaptiveContextMenu.DropdownMenu>
<CaptiveContextMenu.DropdownMenu
toggle={<FontAwesomeIcon icon={"caret-down"} />}
>
<CaptiveContextMenu.DropdownItem onInvoke={onChangeHatBlock}>
Change hat block
</CaptiveContextMenu.DropdownItem>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Junior/InfoPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const InfoDisclosure: React.FC<InfoDisclosureProps> = ({ tabContentId }) => {
const toggleState = () => toggleStateAction();

return (
<div>
<div className={"h-100 d-flex"}>
<Button
variant="outline-secondary"
size="sm"
Expand Down
5 changes: 2 additions & 3 deletions src/components/Junior/KeyNavHelpSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { marked } from "marked";
import { markedParse } from "../hooks/sync-marked";
import { assertNever, EmptyProps } from "../../utils";
import { Row, Col, Container, Spinner } from "react-bootstrap";
import {
Expand Down Expand Up @@ -66,8 +66,7 @@ const Key: React.FC<{ keyDescr: KeyDescriptor }> = ({ keyDescr }) => {
};

const TextContent: React.FC<{ markdown: string }> = ({ markdown }) => {
marked.use({ mangle: false, headerIds: false });
const html = marked.parse(markdown);
const html = markedParse(markdown);
return <div dangerouslySetInnerHTML={{ __html: html }} />;
};

Expand Down
Loading