Skip to content

Commit 8e88cb2

Browse files
FelixTJDietrichclaudegithub-actions[bot]
authored
fix(library): make embedded editors reliable in host applications (#841)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 382513a commit 8e88cb2

173 files changed

Lines changed: 4885 additions & 1383 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/free-squids-like.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tumaet/webapp": minor
3+
---
4+
5+
Test embedded-editor overlays and drag interactions directly in the playground with its new fullscreen workspace action.

.changeset/loose-hounds-cry.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@tumaet/apollon": minor
3+
---
4+
5+
Embed Apollon reliably in host pages: fullscreen overlays stay visible, scrolling and shortcuts remain host-safe, assessment feedback is easier to open and navigate through `revealAssessment(id)`, host controls gain a labeled chrome action, exports no longer expand the page, and diagram interactions remain aligned, sharp, and responsive.
6+
Labels now also stay vertically centered in SVG, PNG, and PDF exports exactly as they appear in the editor.

docs/library/api.md

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -198,13 +198,14 @@ regions, the `<ApollonControl>` React component, and the "make room" model.
198198

199199
### Assessment
200200

201-
| Member | Type | Purpose |
202-
| ----------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
203-
| `addOrUpdateAssessment(assessment)` | `(Assessment) => void` | Attach or update a score/feedback assessment on an element. |
204-
| `setElementHighlights(highlights)` | `(Map<string, string> \| Record<string, string> \| null) => void` | Paint a translucent highlight overlay over the given element ids (id → CSS color) — e.g. to flag elements missing feedback or carrying suggestions. Host-driven and ephemeral: never written to the model, serialized, or shared with collaborators. Each call replaces the previous set; pass `null` or an empty map to clear. |
205-
| `getElementHighlights()` | `() => Record<string, string>` | The current highlight map (element id → CSS color). |
206-
| `getElementIdsByTag(tag)` | `(string) => string[]` | Ids of every element carrying the host-defined `tag` — a node, or one of its members (class attribute, method, SFC action row). Exact and case-sensitive apart from surrounding whitespace; `[]` for an unknown or blank tag. Pair with `setElementHighlights` to color a group — see [Element tags](/library/api/element-tags). |
207-
| `getInteractiveForSerialization()` | `InteractiveElements \| undefined` | Interactive-element flags for inclusion in a saved model. |
201+
| Member | Type | Purpose |
202+
| ----------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
203+
| `addOrUpdateAssessment(assessment)` | `(Assessment) => void` | Attach or update a score/feedback assessment on an element. |
204+
| `setElementHighlights(highlights)` | `(Map<string, string> \| Record<string, string> \| null) => void` | Ring the given element ids (id → CSS color) — e.g. to flag elements missing feedback or carrying suggestions. Drawn as an outline, never a fill, so an element's own text and its assessment badge stay legible. Host-driven and ephemeral: never written to the model, serialized, or shared with collaborators. Each call replaces the previous set; pass `null` or an empty map to clear. |
205+
| `revealAssessment(id, options?)` | `(string \| null, { reveal?: boolean }) => void` | Select one element, open its feedback popover, and pan the canvas to it at the current zoom. Lets a host's feedback list drive the canvas — click an entry, the diagram answers where it applies. `null` clears the selection and closes the popover; `{ reveal: false }` skips the pan. The popover only opens in assessment mode. |
206+
| `getElementHighlights()` | `() => Record<string, string>` | The current highlight map (element id → CSS color). |
207+
| `getElementIdsByTag(tag)` | `(string) => string[]` | Ids of every element carrying the host-defined `tag` — a node, or one of its members (class attribute, method, SFC action row). Exact and case-sensitive apart from surrounding whitespace; `[]` for an unknown or blank tag. Pair with `setElementHighlights` to color a group — see [Element tags](/library/api/element-tags). |
208+
| `getInteractiveForSerialization()` | `InteractiveElements \| undefined` | Interactive-element flags for inclusion in a saved model. |
208209

209210
## Subscriptions
210211

@@ -267,7 +268,11 @@ SVG/PNG/PDF over HTTP via the standalone server.
267268

268269
`Mod` is Ctrl on Windows/Linux and Cmd on macOS; combos marked _view_ work on
269270
read-only diagrams too. Nothing fires while the user is typing in a field or
270-
while a dialog or menu is open.
271+
while a dialog or menu is open. Shortcuts belong to the editor that has focus:
272+
click or tap the canvas to activate it, then move focus outside the editor to
273+
return every shortcut to the surrounding page. Pointer-acquired canvas focus is
274+
released when the pointer leaves, so browser zoom remains available around an
275+
embedded editor. Keyboard users keep ownership until they move focus normally.
271276

272277
| Combo | Action |
273278
| ------------------------------ | ---------------------------------------- |
@@ -286,17 +291,18 @@ whose keys produce a printable character fails
286291
[WCAG 2.1.4](https://www.w3.org/WAI/WCAG21/Understanding/character-key-shortcuts)
287292
unless it can be turned off, remapped, or scoped to focus.
288293

289-
Pass `keyboardShortcuts: false` to keep the editor's hands off every key above
290-
for a host that binds them itself, or that mounts more than one editor (they
291-
listen on `document`, so two would both answer).
294+
Pass `keyboardShortcuts: false` to keep the editor's hands off every key above
295+
when the host binds them itself. Multiple editors need no special coordination:
296+
only the focused editor answers.
292297

293298
`APOLLON_SHORTCUTS` is the list the editor runs, so a host can render a sheet
294299
that tracks it, or check it before binding a key of its own. Each entry's
295300
**first** combo is the primary one — a sheet should render only that; the rest
296301
are aliases (`Mod+Y` redo, layout variants of `Mod+=`). Entries flagged
297-
`canvasHandled` are React Flow's, not the editor's own handler. `shortcutKeyName`
298-
turns a combo into the key it names, so a sheet renders "1" rather than the
299-
`Digit1` code that combo matches on.
302+
`canvasHandled` are handled directly by React Flow on a focused canvas element,
303+
not by the editor-root dispatcher. `shortcutKeyName` turns a combo into
304+
the key it names, so a sheet renders "1" rather than the `Digit1` code that combo
305+
matches on.
300306

301307
`matchesShortcutCombo`, `isTypingTarget` and `isInsideOverlay` are the
302308
primitives that handler matches and stands down with, exported so a host's own

docs/library/api/assessment.md

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,49 @@ const all: Record<string, Assessment> = editor.model.assessments
7676
| `labelColor` | `string?` | CSS color for the label. |
7777
| `correctionStatus` | `{ status: "CORRECT" \| "INCORRECT" \| "NOT_VALIDATED"; description?: string }?` | For automated/suggested feedback review. |
7878

79+
## Drive the canvas from a feedback list
80+
81+
A host that lists feedback beside the diagram has a problem the list alone
82+
cannot solve: an entry says _what_ a tutor wrote, never _where_ it applies. The
83+
reader is left matching text against boxes by eye.
84+
85+
`revealAssessment` closes that loop. It selects the element, opens its feedback
86+
popover, and pans the canvas to it — keeping the reader's zoom, because the zoom
87+
is theirs.
88+
89+
```ts
90+
import { ApollonEditor } from "@tumaet/apollon"
91+
92+
function showFeedbackFor(
93+
editor: ApollonEditor,
94+
feedback: { elementId: string }
95+
) {
96+
editor.revealAssessment(feedback.elementId)
97+
}
98+
99+
// Closing the list, or deselecting, puts the canvas back:
100+
function clearFeedback(editor: ApollonEditor) {
101+
editor.revealAssessment(null)
102+
}
103+
```
104+
105+
Pass `{ reveal: false }` to select without panning — useful when the element is
106+
already on screen and moving the canvas would be disorienting.
107+
108+
Pair it with `subscribeToAssessmentSelection` for the other direction, so
109+
clicking an element marks its entry in the list. Together the two halves explain
110+
each other instead of sitting side by side.
111+
112+
The popover only opens in `ApollonMode.Assessment`. In assessment **with**
113+
`readonly` — a student reading a graded diagram — it opens the read-only
114+
feedback popover; without `readonly`, the tutor's editable one.
115+
79116
## Highlight elements
80117

81-
`setElementHighlights` tints elements by id with a CSS color — e.g. to mark the
82-
elements that still need feedback. Use **translucent** colors so the element's
83-
own text stays readable. Highlights are ephemeral: they are not serialized into
118+
`setElementHighlights` rings elements by id with a CSS color — e.g. to mark the
119+
elements that still need feedback. It draws an outline rather than a fill, so use
120+
**opaque** colors: the element's own content, including its assessment badge,
121+
stays fully visible underneath. Highlights are ephemeral: they are not serialized into
84122
the model and not shared over collaboration. Pass `null` or an empty map to clear.
85123

86124
To color a whole **group** of attributes/methods at once — e.g. by a build
@@ -93,8 +131,8 @@ import { ApollonEditor } from "@tumaet/apollon"
93131
function highlightMissing(editor: ApollonEditor) {
94132
editor.setElementHighlights(
95133
new Map([
96-
["node-2", "rgba(0, 123, 255, 0.35)"],
97-
["node-5", "rgba(0, 123, 255, 0.35)"],
134+
["node-2", "#0d6efd"],
135+
["node-5", "#0d6efd"],
98136
])
99137
)
100138
}

docs/library/api/overlay-controls.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,30 @@ function HostRail({ editor }) {
273273
}
274274
```
275275

276+
Host controls can use the same framework-agnostic chrome classes as Apollon's
277+
built-ins:
278+
279+
```html no-check
280+
<div
281+
class="apollon-glass apollon-chrome-cluster"
282+
role="group"
283+
aria-label="Editor actions"
284+
>
285+
<button class="apollon-chrome-iconbtn" type="button" aria-label="Undo">
286+
<!-- icon -->
287+
</button>
288+
<button class="apollon-chrome-actionbtn" type="button">
289+
<!-- icon -->
290+
<span>Fullscreen</span>
291+
</button>
292+
</div>
293+
```
294+
295+
Use `.apollon-chrome-iconbtn` only when the icon has an unambiguous accessible
296+
name. Use `.apollon-chrome-actionbtn` when a visible label makes a host action
297+
clearer. Both use neutral hover/active layers and the same 32px chrome geometry;
298+
reserve `.apollon-chrome-accent-btn` for a genuine primary action.
299+
276300
`getRegionElement` registers a host control under the reserved id
277301
`apollon:host:<region>` with `inset: "auto"`, so the diagram makes room for
278302
whatever you mount. The host mount is pointer-transparent by default so empty

docs/library/theming.md

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -156,25 +156,27 @@ drop shadow, and a few state tints. They are **not** on `ApollonTheme` (the type
156156
surface stays focused on color), but the names are part of the public contract —
157157
set them directly through `style` or a stylesheet rule.
158158

159-
| CSS variable | Used for |
160-
| ------------------------------------ | ------------------------------------------------------------------------------------ |
161-
| `--apollon-radius-sm` | Small radius — node hover/selection rings, chips, pills (default `4px`). |
162-
| `--apollon-radius-md` | Control radius — inputs, toggles. Routes through `--apollon-radius` (default `6px`). |
163-
| `--apollon-radius-lg` | Panel / popover / menu radius (default `8px`). |
164-
| `--apollon-shadow` | Drop shadow for floating surfaces — menus, popovers, select listboxes. |
165-
| `--apollon-interactive-selection` | Accent ring/fill marking interactive (quiz-pickable) elements (default amber). |
166-
| `--apollon-hover-neutral` | Neutral hover wash for quiet controls, derived off `--apollon-foreground`. |
167-
| `--apollon-dropzone-accent` | Ring/stroke shown on an assessment feedback drop target on hover (default blue). |
168-
| `--apollon-dropzone-accent-fill` | Translucent fill (40% of `--apollon-dropzone-accent`) outlining box/div targets. |
169-
| `--apollon-on-collaboration-cursor` | Ink drawn on a collaborator's colored cursor/avatar (default white). |
170-
| `--apollon-assessment-positive-text` | Glyph/text for a positive (rewarded) score — popover pill AND on-canvas badge. |
171-
| `--apollon-assessment-positive-bg` | Soft tint behind a positive score (popover pill + canvas badge). |
172-
| `--apollon-assessment-negative-text` | Glyph/text for a negative (penalty) score. |
173-
| `--apollon-assessment-negative-bg` | Soft tint behind a negative score. |
174-
| `--apollon-assessment-zero-text` | Glyph/text for a zero score. |
175-
| `--apollon-assessment-zero-bg` | Soft tint behind a zero score. |
176-
| `--apollon-assessment-ungraded-text` | Text for the "Not graded" pill (popover only — ungraded elements get no badge). |
177-
| `--apollon-assessment-ungraded-bg` | Soft tint behind the "Not graded" pill. |
159+
| CSS variable | Used for |
160+
| ------------------------------------ | ---------------------------------------------------------------------------------------------------- |
161+
| `--apollon-radius-sm` | Small radius — node hover/selection rings, chips, pills (default `4px`). |
162+
| `--apollon-radius-md` | Control radius — inputs, toggles. Routes through `--apollon-radius` (default `6px`). |
163+
| `--apollon-radius-lg` | Panel / popover / menu radius (default `8px`). |
164+
| `--apollon-shadow` | Drop shadow for floating surfaces — menus, popovers, select listboxes. |
165+
| `--apollon-interactive-selection` | Accent ring/fill marking highlighted elements — the picker and assessment (default amber). |
166+
| `--apollon-scroll-lock-veil` | Veil over the canvas while a scroll-locked editor explains the zoom gesture. Darkens in both themes. |
167+
| `--apollon-scroll-lock-text` | Ink for that message, paired with the veil above. |
168+
| `--apollon-hover-neutral` | Neutral hover wash for quiet controls, derived off `--apollon-foreground`. |
169+
| `--apollon-dropzone-accent` | Ring/stroke shown on an assessment feedback drop target on hover (default blue). |
170+
| `--apollon-dropzone-accent-fill` | Translucent fill (40% of `--apollon-dropzone-accent`) outlining box/div targets. |
171+
| `--apollon-on-collaboration-cursor` | Ink drawn on a collaborator's colored cursor/avatar (default white). |
172+
| `--apollon-assessment-positive-text` | Glyph/text for a positive (rewarded) score — popover pill AND on-canvas badge. |
173+
| `--apollon-assessment-positive-bg` | Soft tint behind a positive score (popover pill + canvas badge). |
174+
| `--apollon-assessment-negative-text` | Glyph/text for a negative (penalty) score. |
175+
| `--apollon-assessment-negative-bg` | Soft tint behind a negative score. |
176+
| `--apollon-assessment-zero-text` | Glyph/text for a zero score. |
177+
| `--apollon-assessment-zero-bg` | Soft tint behind a zero score. |
178+
| `--apollon-assessment-ungraded-text` | Text for the "Not graded" pill (popover only — ungraded elements get no badge). |
179+
| `--apollon-assessment-ungraded-bg` | Soft tint behind the "Not graded" pill. |
178180

179181
```css
180182
/* Sharpen the editor's corners and lift its menus a touch more. */

0 commit comments

Comments
 (0)