feat(style): reader style overrides via a styleOverrides viewer prop#1524
Open
dqnykamp wants to merge 29 commits into
Open
feat(style): reader style overrides via a styleOverrides viewer prop#1524dqnykamp wants to merge 29 commits into
dqnykamp wants to merge 29 commits into
Conversation
dqnykamp
force-pushed
the
reader-style-overrides
branch
from
July 20, 2026 13:22
2cc0510 to
25e179e
Compare
…ponent Introduce named style palettes as the base layer of the style system. A <stylePalette palette="..."/> in a document or section (directly or in <setup>) selects a built-in palette for that subtree: - Selection is a reset: the palette replaces the subtree's base style map, discarding ancestor <styleDefinition> overrides (tuned against a different palette's colors); local and descendant <styleDefinition>s apply on top. - Style numbers beyond the palette's size cycle through the palette, so every number stays on-palette; without a palette, the historical default fallback is unchanged. - Unknown palette names fall back to the default palette with a diagnostic (validValues), and palette names autocomplete in the editor with per-palette descriptions. - New activeStylePaletteName / localStylePaletteName state variables on document and sectioning components drive the reset and cycling. Ship two new palettes, ocean and sunset (eight styles each, varied marker shapes and line styles), tuned so every style meets WCAG contrast thresholds in both light and dark mode; the registry-wide accessibility guard test enforces this for any future palette. Mirror the palette walk (reset, registry lookup, cycling) in the LSP static style resolver so editor context help matches the runtime, and regenerate the schema. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The schema was generated before the reference/stylePalette docs page existed, leaving docsSlug null; regenerating with the page present fills it in, as CI's schema-verification job expects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Match palette names case-insensitively (toLowerCase on the palette attribute, mirrored in the LSP resolver) per the convention for validValues text attributes. - Extract the duplicated setup-child flattening in style.ts into a childrenExpandingSetups helper. - Fix stale comments referencing the old single-default preset cache; update the resolveActiveStyle docstring for the palette walk. - Document the unknown-name fallback on the stylePalette reference page. - Add tests: cycleStyleNumberForPalette boundary units, inherited-palette cycling inside a section, case-insensitive names, and diagnostics for multiple palettes / unknown names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
A <stylePalette/> with no palette attribute explicitly selects the default palette, which is still a full reset (ancestor styleDefinition overrides discarded) and still activates out-of-range cycling. Neither the worker suite nor the LSP resolver suite covered this contract, so a refactor treating "default" as "no palette" would have passed every test while changing behavior. Add mirrored worker and LSP tests, and a sentence in the reference page documenting the bare-tag reset. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
… scope When resolving the active style for an element inside a <setup> whose parent also owns the <stylePalette> (via that same setup), the deepest- owner scan treated the setup itself as the palette's scope and reset the merge walk there, dropping sibling <styleDefinition> blocks owned by the parent. The runtime attributes setup children to the setup's parent section, so skip <setup> entries in the scan; the parent's setup route already covers them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
Add worker and LSP tests for the discriminating ordering case: a setup-hosted <stylePalette> earlier in the document must lose to a direct one later in it, proving the two arrival routes interleave in document order rather than grouping by route. Also note the macro-valued palette divergence in the LSP resolver's header. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
…e colors for CVD distinctness Evaluate all palettes with a Machado-model color-vision-deficiency simulation plus CIEDE2000 distances, and act on the findings: - New okabeito palette adapted from the Okabe-Ito colorblind-friendly palette: light mode darkens each hue only as far as Doenet's contrast thresholds require (with separate darker text variants where the graphic anchor sits below 4.5:1); dark mode pins the canonical Okabe-Ito colors, which all clear the thresholds on a dark canvas. - Retune ocean and sunset anchors whose pairs fell below CIEDE2000 dE 8 for typical vision, and pin dark-mode colors for both palettes: the automatic lighten-to-threshold derivation flattened the lightness ladder that keeps styles apart. - Add a registry-wide distinctness guard test (min pairwise dE 8 on line colors, light and dark) so future palette edits cannot silently reintroduce near-identical colors. - Document the built-in palettes on the stylePalette reference page and point colorblind readers at okabeito in the styling guide. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
The nearest-anchor word matcher works in raw RGB distance over a small anchor set, and it misdescribed several palette colors: okabeito's blue came back as "cyan" and its reddish purple as "red" (with the text variant as "gray"), ocean's steel blue as "green", sunset's crimson and wine as "brown", and its warm gray as "yellow". Pin explicit words for those styles (using CSS-named vocabulary like teal, olive, and gold where it is the accurate family), keep derivation everywhere it was right, and update the palette-authoring guidance: derive by default, pin only what the derivation gets wrong. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
…lind-friendly palettes Add four palettes adapted from well-researched colorblind-safe schemes — Paul Tol's bright (7 styles), muted (9), and high-contrast (4) palettes and the IBM Design Library palette (5) — using the same recipe as okabeito: canonical colors kept wherever they meet the contrast thresholds, darkened chromaticity-preserving in light mode where they do not, canonical colors pinned in dark mode, and lightness ladders tuned with the CVD-simulation + CIEDE2000 analysis. tolhighcontrast keeps every pair above dE 14 under every simulated deficiency; Tol's vibrant palette was evaluated and rejected (post-adaptation it degrades to dE 4 and duplicates tolbright). Also guarantee each palette's styles carry UNIQUE color words per mode, so style descriptions can always tell styles apart: a new guard test enforces uniqueness, palette words are curated accordingly (okabeito sky blue and goldenrod, ocean cyan/slategray/steelblue, sunset olive/magenta/sienna/maroon), and the default palette's style 4 dark purple no longer derives the wrong word "blue". The stylePalette reference now leads with a short recommended list (default, okabeito, tolhighcontrast) to keep the growing registry navigable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
…olhighcontrast tolhighcontrast's defining property is its lightness ladder, but the dark-mode pins kept hue fidelity while flattening it: the gray standing in for black landed at nearly the same luminance as the yellow, and blue/red sat close together, so the palette's grayscale legibility did not survive the theme an author cannot predict. Dark mode now re-ladders instead of pinning canonical values — blue just above the graphic threshold, red at 4.9:1, yellow at 8.8:1, and white as black's inversion — restoring grayscale gaps comparable to light mode's while every hue-CVD pair stays above dE 22. okabeito's black style similarly becomes white (not gray) in dark mode, clearing its near-collision with the equi-luminant orange and sky blue in grayscale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
…nimum Add a grayscale palette of four hueless grays laddered at even CIELAB lightness steps across the full range the contrast thresholds allow (black to the 3:1 graphic threshold in light mode; white down to the threshold in dark mode, ~20 L* per step both ways). Because it spends its entire distinguishing budget on luminance, it is the best choice for readers with achromatopsia — a host can offer it through the reader style-overrides mechanism. Establish four styles as the guaranteed palette minimum: a guard test enforces it for every registered palette, the StylePalette type documents the contract, and the documentation now instructs authors to reserve style numbers 1-4 for their most important distinctions, since a reader may view a document with a four-style palette onto which higher numbers wrap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
dqnykamp
force-pushed
the
reader-style-overrides
branch
2 times, most recently
from
July 21, 2026 07:51
f5e0c1d to
5cbd021
Compare
…palette Remove `ocean` and `sunset`. Both deliberately stayed within a single hue family (all-cool, all-warm), which makes adjacent style numbers read as shades of one another — the opposite of the semantic distinction style numbers are supposed to carry, and a poor illustration of what palettes are for. Add `categorical` in their place: ten maximally varied hues adapted from the Category10 sequence (D3 / matplotlib `tab10`), with conventional color names. No two styles fall below CIEDE2000 dE 12 for typical color vision in either mode — the widest spread of any built-in palette — so a document needing many styles gets distinctions that are obvious at a glance. It is explicitly not CVD-engineered (green/olive and blue/purple converge under red-green deficiency); the docs say so and point authors at the colorblind-friendly palettes, all ten styles vary marker shape and line style, and a reader can override the palette entirely. Evaluated and rejected: Paul Tol's light scheme (no color survives the light-mode contrast floor; pear/olive collapse to dE 0.6 after adaptation), ColorBrewer Dark2 (gold/brown at dE 8.5 for typical vision, dE 2.1 under protanopia), Tol vibrant (degrades to dE 4 and duplicates tolbright), Petroff 10 (superb in dark mode but its warm hues collide after light-mode darkening), and Petroff 6 and Tol medium-contrast (both robust, but too close to palettes already shipped to add variety). Tests, docs, and examples that used ocean/sunset now use okabeito and tolbright; the style-description test asserts okabeito's curated word rather than deriving it, since the derivation misnames that blue. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
dqnykamp
force-pushed
the
reader-style-overrides
branch
from
July 21, 2026 17:55
c4a4d61 to
b33c53b
Compare
Points using the categorical palette's styles 7-9 rendered invisible. Those styles set markerStyle to triangleDown/triangleLeft/triangleRight — the exact spelling the schema's validValues advertise — but palette data does not travel the attribute path that lowercases such values, and renderers hand markerStyle straight to JSXGraph, whose face names are all lowercase. JSXGraph received an unknown face and drew nothing rather than falling back to a default shape, so the failure was silent. Normalize at expansion instead: expandStylePalette now lowercases the values of text attributes whose spec sets toLowerCase (markerStyle, lineStyle, fillStyle), which is what authored values already get. That fixes every consumer at once — runtime, LSP, and the host-facing palette info — and lets palette modules keep the documented camelCase spelling. Guarded at two levels: a utils test that every palette's enum values name a real value AND arrive in the case renderers require, and a worker test pinning that the categorical palette's styles 7-9 reach selectedStyle as triangledown/triangleleft/triangleright. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
dqnykamp
force-pushed
the
reader-style-overrides
branch
from
July 21, 2026 18:16
b33c53b to
b260ac7
Compare
Selecting a palette recolored ordinary prose. Text and math outside a graph render with selectedStyle.textColor, and content that specifies no styleNumber falls on style 1 — so a palette that painted style 1's text turned every unstyled <text> and <m> in the document that color. Six of the eight palettes did. The `default` palette has always encoded the rule (its style 1 pairs textColor black with highContrastColor #2963FF); the newer palettes just had no way to know it. Apply it in expansion instead, so it is uniform and cannot be forgotten: style 1's text color is always the canvas text color, with its color words re-derived so descriptions stay truthful. Style numbers that wrap onto style 1 inherit the neutral text, which matches what out-of-range style numbers already do when no palette is selected (they fall back to DEFAULT_STYLE_VALUES, also black). A style's own color remains available for text through highContrastColor, and styles 2 and up are untouched, so `<text styleNumber="2">` still renders in the palette's color. Guarded in utils (every palette's style 1 is neutral in both modes, with matching words, while styles 2+ are not) and at the worker level with the reported case: unstyled text and math, an explicitly marked style, and a wrapped style number. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
dqnykamp
force-pushed
the
reader-style-overrides
branch
from
July 21, 2026 19:24
b260ac7 to
6780792
Compare
Six saturated hues — burnt orange, forest green, deep pink, arctic teal, gold, and purple — that swap for neon counterparts in dark mode, contributed as a CSS custom-property set. The contribution's background and body-text colors were dropped: Doenet owns the canvas, and style 1's text is already forced to the canvas text color so a palette never recolors unstyled prose. Its colors were rated against the contribution's own #F5F5F5/#0D0D0D canvases; Doenet's are #ffffff and #121212, so the ratios shift. Every light color still clears 4.5:1, but dark-mode purple measures 4.07:1 on our lighter dark canvas — fine as a graphic, short for text — so its text variant is lightened to #b445ff (4.7:1) while lines and markers keep the contributed #B026FF. Not CVD-engineered: orange, green, and gold collapse together under red-green deficiency (dE ~1-2), so the docs group it with categorical and all six styles vary marker shape and line width. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
dqnykamp
force-pushed
the
reader-style-overrides
branch
from
July 21, 2026 22:36
6780792 to
8e71471
Compare
Palette expansion forces style number 1's text color to the canvas text color, so the `textColor`/`textColorWord`/`textColorDarkMode`/ `textColorWordDarkMode` entries every built-in palette authored on style 1 were silently overwritten -- including dark-mode text variants that had been tuned for contrast and were therefore unused and unverified. Remove them (expansion output is value-identical) and state the rule in the StylePalette contract instead. Correct the palette documentation where it no longer matched the data: - The StylePalette doc told authors never to hand-author `*Word` fields, which every palette does; it now explains that pinning is per color key and why. A new guard test enforces that keys of a style sharing a color also share a word. - `name` was documented as lower-camel-case, but the `palette` attribute lowercases author input, so an uppercase letter would make a palette unselectable. - Four palette doc comments had a stray `*` where a paragraph break belonged, and ibm/tolhighcontrast/grayscale claimed varying line styles they do not have. tolhighcontrast also both denied and asserted pinning canonical dark-mode colors. - `expandStylePalette` did not document its lowercasing or neutral-text steps, and `applyNeutralTextColor` said cycled style numbers resolve to style 1 rather than only those that wrap onto it. - The docs claimed every palette varies line styles, that tolmuted is the largest built-in set (categorical has ten styles), and that dark mode always shows published colors. Also hoist the lowercase-able style keys to a module constant, and cover the `<styleDefinition>` escape hatch over style 1's neutral text with a test. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…trast Every grayscale style's light-mode `highContrastColor` sits below the 4.5:1 text threshold, so leaving `highContrastColorDarkMode` to derivation lightened all four to the same minimum gray (#7d7d7d) -- collapsing the luminance ladder this palette exists to provide, while its `*Word` descriptors still claimed four different grays. Pin the dark-mode high-contrast grays to the palette's own ladder, and guard registry-wide that no two styles of a palette share a high-contrast color in either mode. Also cover the neutral style-1 text rule where the built-in palettes no longer can: none of them author style 1's text keys anymore, so the registry-wide check can no longer tell "overwrite" from "fill in when missing". A local palette that paints style 1's text pins the overwrite, along with the drop of the authored words that described the discarded colors. Finally, note in the reference docs that the neutral style-1 text is a property of the palettes rather than a lock -- a `<styleDefinition styleNumber="1" textColor="...">` still overrides it. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Palette colors are chosen against the WCAG 3:1 graphic threshold as they composite onto the canvas, but a palette that stated no lineOpacity or markerOpacity inherited the built-in 0.7 and blended 70% into the canvas, landing most styles of the eight new palettes around 2.1-2.9:1. The accessibility guard hid this by assuming an opacity of 1 instead of the renderer's own fallback. Expansion now states full opacity for a palette that leaves it out, and the guard composites the way the renderer does. Also pair a style's dark-mode high-contrast color with its dark-mode text color when the two share a light-mode value, so the pair cannot split apart: okabeito and tolhighcontrast reported a mid-gray high-contrast color for a style that is white in dark mode, and okabeito and tolbright a dark olive for one that is bright yellow. A new guard requires a style's high-contrast color to answer to the same word as its lines. The default palette states its own opacities and dark high-contrast colors on every style, so its expansion is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n order The grayscale palette's module doc (and the PR description) explained both of its dark-mode text pins with one mechanism: that derivation lightens a light-mode color only as far as the 4.5:1 threshold requires, so all four grays would collapse onto one. That is true of `highContrastColorDarkMode` only. `textColorDarkMode` derives by inverting the light-mode lightness, which lands on #cccccc / #9e9e9e / #8a8a8a — distinct, but re-stepped ~15 then ~8 L* instead of the even ~20 the palette is built on. Note too that with `textColorDarkMode` pinned, `pairDarkModeHighContrastWithText` now carries the pin over for styles 2-4 on its own, so only style 1's high-contrast pin is strictly load-bearing. Palette expansion now runs five fill-in rules whose relative order decides which one gets to set a value; spell out the four constraints that make the current order the correct one rather than leaving them to be rediscovered. Also record the user-visible half of the full-opacity rule: palette lines and markers draw at full strength while the default styles mostly draw at 70%, so switching palettes visibly densifies strokes. Documented on the stylePalette reference page and asserted at the worker layer, where it proves the stated opacity survives `resolveStyleDefinition` instead of being refilled with the built-in 0.7. The styleDefinition reference page now says which styles a `<styleDefinition>` starts from. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
dqnykamp
force-pushed
the
reader-style-overrides
branch
from
July 22, 2026 00:03
8e71471 to
6e579f7
Compare
Replace every palette's use of `cross` and `plus` with the remaining directional triangles, so styles 1-7 now cycle through all seven closed shapes (circle, square, triangle, diamond, then the down/left/right triangles) before repeating with line-style variation. Both replaced markers are drawn as bare strokes, which costs them the filled/open distinction that marks a closed versus open endpoint or a stable versus unstable equilibrium — `markerFilled` documents having no effect on them — and both read as the mathematical symbols for multiplication and division. The default style definitions already avoided them for these reasons; the newer palettes reached for them only because they have more styles than the four shapes those definitions use. A guard test pins the rule so an interior-less marker cannot return. Note for PreFigure output: it has no triangles, and our converter maps any `triangle*` to `diamond` with a warning, so palettes now resolve to fewer distinct shapes there than they do in JSXGraph. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
dqnykamp
force-pushed
the
reader-style-overrides
branch
from
July 22, 2026 03:34
6e579f7 to
01add50
Compare
The palette examples used points only, which show a style's marker color and shape but nothing of its line or fill color. Pair each point with a small filled circle, whose outline is the style's line color and whose interior is its fill color, so one graph shows what a palette actually sets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
dqnykamp
force-pushed
the
reader-style-overrides
branch
from
July 22, 2026 03:44
01add50 to
886529b
Compare
The graphs spanned 5 units horizontally and 9 vertically, so the circles added to show line and fill color rendered as ellipses. Match the x-span to the y-span and set identicalAxisScales, and spread the two columns to use the width now available. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
dqnykamp
force-pushed
the
reader-style-overrides
branch
from
July 22, 2026 04:48
886529b to
e09a535
Compare
The opacity note demonstrated `<styleDefinition lineOpacity="0.7" />` with no style number, which reads as a document-wide switch. It is not: styleNumber defaults to 1, so that block silently softens style 1 alone and leaves every other style at full strength. Show the attribute and say what omitting it means. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
dqnykamp
force-pushed
the
reader-style-overrides
branch
from
July 22, 2026 05:20
e09a535 to
9d7c7ba
Compare
The divergence note read as though a reference-valued `palette` falls back to the default palette everywhere. It does not: the attribute is a `text` component, so the runtime resolves the reference and selects that palette. Only the LSP falls back, because it reads attribute text and the literal "$p" fails the registry lookup. Name both sides. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
dqnykamp
force-pushed
the
reader-style-overrides
branch
from
July 22, 2026 05:35
9d7c7ba to
e0f044d
Compare
The previous wording claimed the two diverge for different reasons. They do not: styleNumber and palette are both component-typed attributes, so the runtime resolves references in both and the LSP, reading attribute text, resolves neither. Only the shape of the fallback differs — an unparseable integer versus an unregistered palette name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
Let host applications pass reader (end-user) style overrides into <DoenetViewer>/<DoenetEditor>: per-styleNumber remappings of colors and other style settings (the ReaderStyleOverrides format, exported from @doenet/utils and the viewer packages). This realizes the accessibility goal behind style numbers — a reader (e.g. one who cannot distinguish certain colors) remaps what a style number looks like, every object of that style shifts together, and the author's distinctions survive. - Overrides apply as the final layer of every section's merged style definitions, after authored <styleDefinition>s/<stylePalette>s and after the contrast diagnostics, so reader values win over everything authored, never emit author-facing diagnostics, and cannot mask diagnostics about authored values. Values are stored positionless. - *Word keys are rejected and always re-derived, keeping core-computed style descriptions truthful; missing dark-mode colors are derived from the reader's light colors with the same accessibility-aware derivation authored styles get. - Plumbing mirrors the theme precedent: a readerStyleOverrides UI-only state variable (document-set, section-mirrored), a styleOverrides init arg in generateJavascriptDast, and a setStyleOverrides action for live updates (never saved). The iframe wrapper forwards the prop through its existing serializable-props path. - Per-component authored non-color attributes still win over reader overrides in v1; colors are fully reader-controlled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ides Add a `palette` field to `ReaderStyleOverrides`: a host can pass the name of any built-in palette (case-insensitive) and the reader's palette replaces the document's base styles everywhere — authored `<stylePalette>` selections and `<styleDefinition>` customizations are discarded, since the reader's palette (e.g. `grayscale` for a reader who distinguishes styles by lightness alone) is a coherent accessible set on its own. Style numbers beyond the reader palette's size cycle onto it, taking precedence over the authored palette's cycle size, and any per-styleNumber `styles` overrides apply on top. Unregistered names are ignored; `resolveReaderPaletteName` is exported for hosts that want to validate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
Hosts building a reader-facing palette picker need the palette list and enough per-style data to draw swatches. Add `getStylePalettes()` and `getStylePalette(name)`, returning each palette's name, description, and per-style *resolved* definitions — colors for both themes, line/marker settings, and the curated color words, suitable for accessible swatch labels. The result is plain JSON, so it crosses worker and iframe boundaries unchanged. Surface it on each embedding path: - `@doenet/doenetml` re-exports both functions and the `StylePaletteInfo` type. - `@doenet/standalone` exports them and installs `window.getDoenetStylePalettes` / `window.getDoenetStylePalette`, so a page loading the bundle from a CDN can list the palettes that version ships. - `@doenet/doenetml-iframe` adds an `onStylePalettes` callback to `<DoenetViewer>`/`<DoenetEditor>`, reporting the palettes of the bundle actually booted in the iframe — the wrapper and the bundle ship separately, so the pinned version is the only correct source. The srcdoc scripts feature-detect the bundle global and report `null` for bundles too old to provide it, rather than gating on a version number. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
…lette's removal Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3ozujKjWpm1bvBSLn9wJ7
dqnykamp
force-pushed
the
reader-style-overrides
branch
from
July 22, 2026 05:46
e0f044d to
b35cbd9
Compare
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.
Add reader (end-user) style overrides: a
styleOverridesprop on<DoenetViewer>/<DoenetEditor>that lets a host application remap what each style number looks like on behalf of a reader — e.g. colors a color-blind reader can better distinguish. This realizes the accessibility goal behind style numbers: the reader's remapping wins, every object of that style shifts together, and the author's distinctions survive.Stacked on #1523 (#1522 is merged) — merge #1523 first; this PR's diff includes its commits until then (the branch is rebased onto the current
style-palette-componenthead).Format
ReaderStyleOverrides(exported from@doenet/utils, re-exported by@doenet/doenetmland@doenet/doenetml-iframe) is plain JSON: every style-definition key is overridable except the*Worddescriptors. A reader can also select one of the built-in palettes wholesale via apalettefield (case-insensitive name, validated against the registry —resolveReaderPaletteNameis exported): the reader's palette replaces authored<stylePalette>/<styleDefinition>styling everywhere, style numbers beyond its size cycle onto it (taking precedence over the authored palette's cycle size — essential when a reader picks the four-stylegrayscalepalette), andstylesoverrides apply on top. Unregistered names are silently ignored.Palette discovery for hosts. So a host can render a palette picker with swatches,
getStylePalettes()/getStylePalette(name)return each palette's name, description, and per-style resolved definitions (colors for both themes, line/marker settings, and the curated color words for accessible swatch labels) as plain JSON. Exported from@doenet/doenetml; the standalone bundle also installswindow.getDoenetStylePalettes/window.getDoenetStylePalettefor CDN pages.@doenet/doenetml-iframeadds anonStylePalettescallback to<DoenetViewer>/<DoenetEditor>that reports the palettes of the bundle actually booted inside the iframe — the wrapper and bundle ship separately, so the pinned version is the only correct source — feature-detecting the bundle global and reportingnullfor bundles too old to provide it.Behavior
<styleDefinition>s/<stylePalette>s and after the contrast diagnostics, so reader values win over everything authored, never emit author-facing diagnostics, and cannot mask diagnostics about authored values (they are also stored positionless as a second guard).*Wordkeys are rejected and always re-derived, keeping core-computed style descriptions ("thick red line") truthful for reader-chosen colors; a missing dark-mode color is derived from the reader's light color with the same accessibility-aware derivation authored styles get.theme/darkModeprecedent: initial value rides in thegenerateJavascriptDastargs; live prop changes push asetStyleOverridesaction into a UI-only, never-savedreaderStyleOverridesstate variable on the document (mirrored into sections). The iframe wrapper forwards the prop through its existing serializable-props path with no changes.<point markerSize="8">) still win over reader overrides in v1; colors — the accessibility-critical channel — are fully reader-controlled since no per-component color attributes exist.Tests
readerStyleOverrides.test.ts): init-arg overrides beating document- and section-level<styleDefinition>s and palettes, hex → color-word/dark-mode derivation, sanitization of word/unknown keys, livesetStyleOverridesupdate and clear, no accessibility diagnostics from deliberately low-contrast reader values, and reader palette selection (replaces authored styling, cycles style numbers onto the reader palette's size, live switch and clear, unregistered names ignored,styleson top).readerOverrides.test.ts): per-key overlay, word/dark re-derivation, explicit-dark preservation, positionless storage, unknown-style-number and empty-input tolerance, lowercase normalization.Host-facing documentation added to the
@doenet/doenetml-iframeREADME; the styling concepts page now describes reader remapping as available.🤖 Generated with Claude Code