Split into a workspace with a React package and a web component - #11
Open
mikebarkmin wants to merge 27 commits into
Open
Split into a workspace with a React package and a web component#11mikebarkmin wants to merge 27 commits into
mikebarkmin wants to merge 27 commits into
Conversation
The playground was a single Vite app whose store was a module-level singleton wired directly to location.hash. That made it impossible to embed: it would take over the host page's URL, and two playgrounds on one page would share one diagram. Restructure into a pnpm workspace, mirroring the layout of openpatch/learningmap: - packages/java-memory-playground - the React library, exporting a MemoryPlayground component driven by a `memory` prop and an `onChange` callback - packages/web-component - registers <java-memory-playground> via @r2wc/react-to-web-component, shipped as a self-contained UMD bundle - platforms/web - the standalone app at jmp.openpatch.org Each playground now creates its own store through a React context, so a page can host several of them independently. URL persistence became opt-in via setPersistence, which the standalone app enables during bootstrap. Fixes found along the way: - new global variables were always typed "List" regardless of the class - CustomNodeType included React Flow's BuiltInNode, which broke type checking against current @xyflow/react and left the build red - PNG export and the change event resolved their element via a document-wide query, so they hit the wrong playground when a page had more than one - handleDeclareLocalVariable changed identity whenever its dialog toggled, rebuilding nodeTypes and remounting every node - a malformed URL hash threw instead of falling back to the default diagram - component CSS styled bare button/input selectors, leaking into host pages Also adds CI, changesets, tests for the new code, and package READMEs, and drops dead files (config.json template, the eslint config whose plugins were never installed, the npm lockfile alongside the pnpm one). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
The playground kept its nodes and edges in React Flow's local state and only copied them into the store when Save was pressed. Anything not saved was lost by switching to the config view or reloading, and there was no undo. Follow learningmap's editor store instead: the store owns nodes, edges, classes and options, with onNodesChange/onEdgesChange, loadMemory and getMemory replacing the copy-on-save. MemoryView reads and writes it directly, which also retires the key-remount that was needed to reload a diagram from props. On top of that state model: - Undo/redo via zundo, with toolbar buttons and Ctrl/Cmd+Z / Ctrl/Cmd+Y. The temporal partialize drops React Flow's own bookkeeping (measurements, selection, the dragging flag) so mounting no longer lands in the history and a drag is one step rather than one per frame. - KeyboardShortcuts for save, undo, redo, config and zoom, overridable through keyBindings, ignored while an input has focus. - English and German translations picked by a language prop/attribute or the browser, mirroring learningmap's translations module. - Save is now a commit: it bumps saveCount, which is what fires onChange and the web component's change event. URL persistence keeps the same Memory format, so links shared before this still open, but it now syncs on every edit rather than only on Save. Writes are throttled and go through history.replaceState, so continuous syncing does not push a history entry per edit. Also fixes Variable.name being typed as the boxed String, exports the memory sub-interfaces so declaration emit can name them, and adds round-trip tests for getEdgesAndNodes/getMemory. Verified in Chromium: edits survive a config round trip, the URL syncs without adding history entries, undo/redo works by button and keyboard, and the German UI renders. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
React 18 -> 19, pako 2 -> 3, vitest 2 -> 4, TypeScript 5 -> 7, @types/node
22 -> 26, plus the smaller ones. The React peer range still covers 18 and 19.
Fallout handled:
- pako 3 renamed the decode option from `{ to: "string" }` to `{ toText: true }`.
The compressed format is unchanged, so shared links still decode.
- @types/pako is deprecated now that pako ships its own types; removed.
- React 19 requires an argument to useRef.
- TypeScript 7 reports side-effect CSS imports without declarations, so the
two packages that were missing a vite-env.d.ts now have one.
- The store moved off zustand's legacy `zustand/traditional` entry to
`useStore` + `useShallow`, the recommended zustand 5 API.
Vite stays on 7 (with @vitejs/plugin-react 5) on purpose. Vite 8 bundles with
Rolldown, which leaves the `require("react")` inside use-sync-external-store's
CJS shim unresolved, and the bundle throws on load with a blank page. That shim
arrives via @xyflow/react, which depends on zustand 4, so it cannot be avoided
from here. The reason is recorded next to the config.
Separately, this fixes a bug in the previous commit that the upgrade testing
uncovered: diagrams shared by early versions have no `methodCalls` section,
building the graph from one threw inside the persist merge, and the failure was
swallowed — so an old link silently opened the default diagram instead. Persisted
state now goes through the same normalization as the `memory` prop, and
getEdgesAndNodes tolerates missing sections. Covered by a test using a real
pre-existing link.
Verified in Chromium on the upgraded stack: a legacy link restores its eight
objects and two variables, edits survive a config round trip, the URL syncs
without adding history entries, undo/redo works by button and keyboard, the
German UI renders, and two playgrounds on one page stay independent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
String was in `primitveDataTypes`, so a String value was stored inside the object holding it and drawn as one of its fields. That is the shape of the misconception behind `==` versus `.equals()`, and because the simplification lived in the data model rather than the view, the saved diagram had no way to express a String reference at all — teaching the string pool later would have meant changing the save format. A String value is now a heap object (`klass: "String"` with a `literal`), and the new `inlineStrings` option decides whether it is drawn as its own box or shown inside its owner. It defaults to on, so diagrams look exactly as they did: the linked-list example does not sprout a box per name. Turning it off draws the String objects, which is what makes two references to one String — and so the pool — possible to show at all. - InlineString renders and edits the referenced String object in place. It reads the store rather than React Flow, because while the option is on the String nodes and their edges are deliberately kept out of the drawing. - A String field starts as a null reference; the object is allocated on the first keystroke, which also matches a field defaulting to null. - parseMemory converts inline String values on read, so existing shared links keep working. The legacy linked-list link in the tests carries twelve of them. - Quotes were previously typed into the value by hand, so hand-authored examples displayed `"mike"` and student-created ones displayed `mike`. They are no longer stored, only rendered. Verified in Chromium: the default diagram is unchanged (same five nodes, same layout, values in place), a legacy link restores all twelve literals with quotes stripped and nothing new drawn, typing allocates the object without revealing it, and with the option off two objects can be shown sharing one String. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
A stack is defined by pushing and popping, and a single frozen picture shows
neither. The diagram is now a list of steps with a bar to walk through them,
and Add step duplicates the step on screen so a trace is authored by changing
what the next line did rather than by redrawing.
That makes showable a set of things that were not: a frame appearing on a call
and gone after the return, the assignment that drops the last reference to an
object, and what reassigning a parameter does and does not do to the caller.
Format: `Memory.steps` is a list of `{ label?, note?, objects, variables,
methodCalls }`. A one-step diagram is still written in the shape it has always
had, without a `steps` key, so a link to a single picture stays readable by
older versions; parseMemory reads either shape and normalises to steps. The URL
cost of a story is small because steps compress against each other — a 40-step
trace of the linked-list example is 2.5x a single diagram, not 40x.
- `step` / `onStepChange` on the component, `step` and a `stepchange` event on
the custom element, so a page can drive the diagram from its prose and follow
along. steps.html demonstrates both directions.
- The store keeps `steps` and `currentStep`; setNodes/setEdges and the React
Flow change handlers write into the current step, which left MemoryView's call
sites untouched.
- Node positions are shared across steps: dragging moves a node everywhere it
appears, so the picture does not jump while stepping.
- Class definitions are reconciled across every step, so the reconciliation
moved out of ConfigView into an applyKlasses action.
- Walking through a diagram is not undoable; changing one is.
Verified in Chromium: a five-step trace pushes a frame at step 2 and pops it at
step 5 with the prose staying in sync in both directions, and in the app Add
step duplicates, an edit to the copy leaves the original intact, and the story
survives the URL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
Configuring classes and authoring the steps of a trace are the teacher's work, and having them on screen while a student works through a diagram is noise. Following how learningmap separates its viewer from its editor: - MemoryPlayground and <java-memory-playground> are the student's: the whole diagram, every edit, and the steps of a trace to walk through. - MemoryPlaygroundEditor and <java-memory-playground-editor> add class configuration and step authoring on top. - The standalone app serves the student's playground, and the teacher's at `?edit`. A `/edit` path works too, for whenever a rewrite rule exists; the app is served statically today, so only the query flag works without one. The split is about which tools are on screen, not about what a student may touch: a student still builds objects, connects references, walks the steps and runs the garbage collector. `setRoute` refuses to enter the configuration outside edit mode, so the shortcut cannot reach it either — the route is closed rather than merely hidden. This also subsumes the step-editing option I had been about to add: Add step, Delete step and the step label are the teacher's, while walking through a trace is everyone's, and a student sees the step label as text rather than a field. Mode is per store, so a page can host a student's and a teacher's playground at once without them sharing anything, which modes.html demonstrates. Verified in Chromium: both elements upgrade; the student has no Config and no step authoring but keeps the sidebar, Save, garbage collector and step navigation; the teacher has all of it; and in the app Ctrl+, opens the configuration at ?edit and does nothing on the student's route. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
Two stack bugs first: - Return popped whichever frame you clicked, so a frame in the middle could vanish while the calls above it stayed. Only the top frame can return now; the others disable the button and say why rather than hiding it, because a call having to finish before the one below it resumes is the lesson. Returning also removes the references the frame held, which is what leaves an object unreachable for the garbage collector to find. - A new frame took `index = count of frames`, which handed out an index a surviving frame already had as soon as one in the middle was gone. It is one past the deepest frame now. Then phase 2 of stepping: walking a trace only helps if you can see what moved, so each step is marked against the one before it — a green outline for what appeared, a dashed amber one for what changed, an amber reference for one that was assigned or repointed. The first step marks nothing, because nothing has happened yet. `hideStepChanges` turns it off; `diffSteps` is exported. The comparison ignores position, since layout is shared across steps and a node cannot move between them, and it identifies a reference by the slot it leaves from so that retargeting one is a change rather than a delete and an add. Rendering the marks meant untangling the class assignment, which mutated the store's nodes during render and had the edges read those mutations back. Both now derive from one computed map. Verified in Chromium: on the five-step trace the marks follow the story — a frame appears at step 2, the reference assigned at step 4 is amber — and with three frames only the deepest has Return enabled, leaving f0 and f1 behind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
- Exercises. A step can be marked as one: the teacher authors it as the answer, and a student's playground starts them from the step before it and checks what they build. The comparison is by shape reachable from the named roots — variables, and each frame's locals — not by address, because a student who allocates an object gets whatever address the playground handed out; the same diagram built independently has to match. The report names the root that is wrong rather than only saying no. Saving from a student's playground writes the exercise back as authored, so a shared link stays the exercise. - Garbage prediction. With `gcPrediction` on, the collector asks first: mark what you think is unreachable, and the score is worked out before the sweep, while there is still something to have been wrong about. - Download all steps: one image with every step under its label, for a worksheet. Exporting gave you the step on screen, which is the wrong picture. - Presets naming the option combinations a course moves through, as buttons in the config view, instead of a teacher remembering which flags go together. Also fixes a crash the garbage-prediction demo turned up. The reachability walk behind the collector and the stack fading recursed through references guarding only against self-loops, so any cycle between two or more objects recursed forever and took the playground down with a stack overflow. A circular linked list did it — an ordinary teaching example. It tracks visited nodes now, which also makes an unreachable cycle collectable, the demo that shows why tracing beats reference counting. Verified in Chromium: a student sees the previous step and gets "Not right yet: head", revealing the solution makes the check pass; marking one node of an unreachable pair scores "Found 1, missed 1" and collects both; the references preset ticks the right options; and exporting a two-step trace downloads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
The palette was a flex column beside the canvas, taking 200px of width permanently — costly in an embed, and inconsistent with every other control, which floats. It is a top-left Panel now, styled like the step bar, capped to the canvas height and scrolling when a diagram has many classes. Dragging still works: useDnD captures the pointer on the chip and decides where to drop with elementFromPoint, neither of which cares that the chip now lives inside the flow. Panning is unaffected — a Panel is not the pane. Exports had to change with it. Photographing the whole canvas would now include the palette, so both exports capture the viewport framed to the diagram's nodes instead, via getNodesBounds and getViewportForBounds. That also crops away the empty canvas and every floating panel, which is what an exported diagram wants anyway — no filter list to keep in step with the panels. Verified in Chromium: the palette renders inside the flow, the canvas keeps the full width, dragging "new Message" out of it creates the object, panning still works on empty canvas, and an exported PNG is a tight crop with no chrome. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
An exported diagram had its objects and frames but no arrows between them,
which is most of what a memory diagram says — and the worksheet export shipped
earlier today inherited it.
Cause: html-to-image deep-clones SVG subtrees and only inlines computed styles
for the HTML it walks, so nothing a stylesheet contributed reaches an SVG
descendant. Dumping the clone showed the paths present and correct:
<path marker-end="url('#1__color=...')" d="M182..." fill="none"
class="react-flow__edge-path"/>
with no stroke at all, which in SVG means none. Our CSS supplied the stroke, so
it was dropped, and the lines were drawn invisibly. Edges now carry stroke,
width and opacity as an inline style, derived from the same state that picks
their class, which survives the clone.
The capture changed with it. Photographing only the viewport lost the
arrowheads, whose markers are defined outside it, so the whole flow is
photographed with panels filtered out — `react-flow__panel` covers palette,
toolbar, step bar, collector and zoom controls at once — and the result is
cropped to the nodes. Each capture frames the diagram first and restores the
view afterwards, so nothing scrolled out of sight is missing and every step of
a worksheet is framed the same way.
Verified in Chromium: a single export is a tight crop with arrows and
arrowheads and no chrome, and a two-step worksheet exports both steps under
their captions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
A teacher already has the classes written down, so the config view takes them as Java rather than asking for each field through a dialog. Only the structure is read — class names, and the name and type of each field. Method bodies are skipped whole and nothing is executed or interpreted. Source that cannot be read yet is reported without throwing the classes away, and the class list stays as a tab for changing a single field. long, short and byte count as primitives now. They were missing, so a field declared with one of them became a reference instead of a number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
Classes belong to the whole diagram, so saving them reaches every step, and pasting a new file over the old one can quietly delete what the objects were holding. Save names it first: the fields that go and how many objects lose a value or a reference with them, and the objects whose class is gone, which stay but can never be made again. It only asks when there is something to lose. Adding a field still saves without a dialog, because a warning that always appears is one nobody reads. A removed reference field now takes its edge with it. The attribute was deleted but the edge stayed behind, drawn from a handle that no longer existed, and the reference came back if the field ever did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
An int shows a 0 and a boolean shows a box, but that is what the field starts out holding rather than something to lose, so removing one no longer asks. A teacher replacing a large set of classes was reading a list of every untouched primitive in the diagram. defaultValueFor says what a field of a given type starts out holding. New objects and new arrays were each deciding that for themselves, in the same way, in two places. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
A diagram is read as a whole, so padding inside a node is diagram that has to go somewhere else. Rows, headers, bodies, handles, the palette and the step bar are all tighter: an object with two fields went from 102x135 to 96x85 and a frame with three locals from 184x276 to 182x198, with nothing removed. How tight is four custom properties on the container rather than a number repeated down the stylesheet, so a projector or a touch screen can loosen everything at once. Framing the diagram now reserves the space the floating panels occupy. The palette is drawn on top of the canvas, so fitting the nodes edge to edge parked the first frame underneath it and hid its name. A String is as wide as what it holds. It was a fixed 80px, too much for an empty one and not enough for "Hello World!". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
The collector was pinned bottom-right and the step bar bottom-centre, as separate panels that could not see each other. Below about 1000px of canvas they slid into each other and the collector sat on top of "Add step" and "Delete step" — at 768px "Delete step" could not be clicked. They share one row now, so they cannot overlap at any width: side by side when there is room, the collector on its own line when there is not. The row also gets the width it is entitled to. React Flow centres a bottom-centre panel with left: 50%, which caps how wide it can shrink-to-fit at half the canvas, so the row wrapped with the whole right half of the screen still empty. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
Side by side, the step controls and the collector were separate cards 38px and 33px tall with a 4px gap between them. They share one card now, and every control in the row is 26px, so a row of them reads as a row. The collector wears the colour the diagram uses for garbage, which is what tells it apart from the step controls beside it. That rule existed but never applied: `.button-gc` is one class and `.java-memory-playground button` is a class plus a type, so the collector had been taking the default button background all along. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
The bottom row reached the left edge of the canvas on a narrow screen and ran into React Flow's zoom controls sitting in that corner. It stops short of them now, reserved on both sides so the row stays centred. Checking every overlay against every other turned up the same defect at the top: below 480px the toolbar slid underneath the palette opposite it. It wraps onto a second line instead, and only gives up the width when the palette is actually there to give it up to. Verified from 1400px down to 380px: no overlay overlaps another, and nothing is pushed off the edge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
React Flow ships its ARIA descriptions and control labels in English, so a German playground announced "Zoom In", "Fit View" and "Press enter or space to select a node" beside its own translated labels. They go through ariaLabelConfig now and follow the playground's language. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
Opening a .jmp file shows the diagram instead of the JSON, with the teacher's tools on. Built like the learningmap extension: an extension host registering a custom editor, and a webview hosting the playground, sharing a build script. The document is the single source of truth. Every edit is written into it, which is what gets the dirty marker, Ctrl+S, the editor's undo stack and side-by-side source editing without the extension reimplementing any of them. Two things follow: the provider ignores the text it just wrote, so our own edit coming back does not reload the webview and discard the user's undo history; and a save asks the webview to flush the debounce and returns the result as a TextEdit from waitUntil, so it cannot miss the last keystroke. The playground's own Save button writes the file and asks VS Code to save it, rather than sitting there doing nothing. Commands are limited to the three that are actually registered — New Diagram, Show Source, Show Diagram — and the ones needing the current file ask the tab, because a custom editor means there is no active text editor to ask. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
The MIT text named webkid GmbH, React Flow's copyright holder — left over from the template this repository started from in 2023. The terms were always right; the holder was not. The extension carries a copy, which is also what stops vsce warning that a published extension has no licence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
Both declared "license": "MIT" but published only dist, so neither tarball carried the licence text it was pointing at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
Mirrors the learningmap release workflow: a push to main opens a "Version Packages" pull request, and merging that one publishes the npm packages and then the extension, to the Visual Studio Marketplace and Open VSX. The vsix is built before the changesets step on purpose. On the run that publishes, package.json already carries the new version, because that run is the merge of the pull request that bumped it. Three things differ from the learningmap workflow. Permissions are contents: write and pull-requests: write, because the changesets action has to push a branch and open a pull request and cannot with contents: read. pnpm and setup-node match what this repository's own pull request workflow already uses. And the tests run before anything is published. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Holding the up arrow of a numeric field and letting go left the value climbing until the next click somewhere else. React Flow starts a node drag on mousedown and, while dragging, swallows mouseup in the capture phase on window, so the spin button began its auto-repeat but was never told to stop. Every control inside a node now carries React Flow's nodrag class, which keeps the drag from starting over a control at all. That also stops a node being dragged around by its own buttons, and lets text be selected inside an inline String. The configuration view is a form, and as soon as a class has a few fields it is taller than the frame. It sat in a height: 100% box with nothing to scroll it, so anything past the bottom edge was unreachable wherever the playground is clipped: a VS Code webview, or an embedding page. The scroll container wraps the route rather than living inside ConfigView, so the scrollbar sits at the frame edge instead of inside the centred column. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
The packages typecheck against each other through their built dist/index.d.ts, so linting a dependent before its dependency is built cannot resolve it. From a clean checkout `pnpm lint` fails with TS2307 in the web component, the web platform and the vscode platform alike; CI only reported the first because pnpm stops at the first failure. `pnpm -r build` already runs in topological order, so building first gives every dependent the declarations it needs. The release workflow gets the same ordering, so that a test added to a dependent later does not fail there for this reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
pnpm 11 renamed `onlyBuiltDependencies` to `allowBuilds` and takes a decision per package rather than a list, so a dependency that starts shipping an install script is refused until it is answered rather than running silently. Under pnpm 11 the old key is not read, which would have left esbuild, keytar and vsce-sign unbuilt. CI moves with it, so that the version running the release is the version the workspace is configured for. The lockfile is unchanged: pnpm 11 reads the 9.0 format as it stands. Verified from an empty node_modules and no dist: install, build, lint, test and packaging the extension all pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
Vercel installs with pnpm 9, so 9 is what the workspace has to work on, whatever a contributor has locally. The install-script allow list is now spelled for three majors at once: pnpm 9 runs install scripts anyway and reads neither key, pnpm 10 reads onlyBuiltDependencies, and pnpm 11 reads allowBuilds and refuses an install that wants to build something it has not been told about. The two lists have to be kept in step. CI drops back to 9 so that it runs what the deployment runs. The lockfile stays at 9.0 and neither version rewrites it. Verified on both: from an empty node_modules and no dist, install, build, lint and the 130 tests pass under pnpm 9.15.9 and under pnpm 11.3.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
The toolbar gains a `?` opening a single static page served with the app, covering reading a diagram and building one, values against references, steps, the collector, the configuration view and every option, the shortcuts, sharing, embedding and .jmp files. It explains the ?edit URL, which nothing until now did: that the app is two playgrounds, that appending ?edit turns the student's into the teacher's, and that because the diagram lives in the fragment and the mode does not, one picture has both an editing link and a working link. The button is a link rather than a button, so that whatever owns navigation around the playground opens it the way it opens any link: a new tab in a browser, the external browser from a VS Code webview, where window.open is blocked. The URL is absolute for the same reason, since an embedded playground would resolve a relative one against a page that is not the app. `return` on a stack frame is no longer translated. It is the keyword the student would write, spelled the same in a German lesson as an English one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The playground was a single Vite app whose store was a module-level
singleton wired directly to location.hash. That made it impossible to embed:
it would take over the host page's URL, and two playgrounds on one page
would share one diagram.
Restructure into a pnpm workspace, mirroring the layout of openpatch/learningmap:
MemoryPlayground component driven by a
memoryprop and anonChangecallback
@r2wc/react-to-web-component, shipped as a self-contained UMD bundle
Each playground now creates its own store through a React context, so a page
can host several of them independently. URL persistence became opt-in via
setPersistence, which the standalone app enables during bootstrap.
Fixes found along the way:
against current @xyflow/react and left the build red
query, so they hit the wrong playground when a page had more than one
rebuilding nodeTypes and remounting every node
Also adds CI, changesets, tests for the new code, and package READMEs, and
drops dead files (config.json template, the eslint config whose plugins were
never installed, the npm lockfile alongside the pnpm one).
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01UPSkhpmU2ZHLQvJTLfmPib