Skip to content

feat(style): reader style overrides via a styleOverrides viewer prop#1524

Open
dqnykamp wants to merge 29 commits into
Doenet:mainfrom
dqnykamp:reader-style-overrides
Open

feat(style): reader style overrides via a styleOverrides viewer prop#1524
dqnykamp wants to merge 29 commits into
Doenet:mainfrom
dqnykamp:reader-style-overrides

Conversation

@dqnykamp

@dqnykamp dqnykamp commented Jul 20, 2026

Copy link
Copy Markdown
Member

Add reader (end-user) style overrides: a styleOverrides prop 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-component head).

Format

<DoenetViewer
    doenetML={source}
    styleOverrides={{
        styles: {
            1: { lineColor: "#0072b2", markerColor: "#0072b2" },
            2: { lineColor: "#d55e00", lineWidth: 6 },
        },
    }}
/>

ReaderStyleOverrides (exported from @doenet/utils, re-exported by @doenet/doenetml and @doenet/doenetml-iframe) is plain JSON: every style-definition key is overridable except the *Word descriptors. A reader can also select one of the built-in palettes wholesale via a palette field (case-insensitive name, validated against the registry — resolveReaderPaletteName is 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-style grayscale palette), and styles overrides 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 installs window.getDoenetStylePalettes / window.getDoenetStylePalette for CDN pages. @doenet/doenetml-iframe adds an onStylePalettes callback 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 reporting null for bundles too old to provide it.

Behavior

  • Overrides apply per-key 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 (they are also stored positionless as a second guard).
  • *Word keys 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.
  • Plumbing mirrors the theme/darkMode precedent: initial value rides in the generateJavascriptDast args; live prop changes push a setStyleOverrides action into a UI-only, never-saved readerStyleOverrides state variable on the document (mirrored into sections). The iframe wrapper forwards the prop through its existing serializable-props path with no changes.
  • The editor's rendered preview applies the overrides; the code pane and context help keep showing authored truth.
  • Authored per-component non-color attributes (e.g. <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

  • Worker (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, live setStyleOverrides update 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, styles on top).
  • utils (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-iframe README; the styling concepts page now describes reader remapping as available.

🤖 Generated with Claude Code

@dqnykamp
dqnykamp force-pushed the reader-style-overrides branch from 2cc0510 to 25e179e Compare July 20, 2026 13:22
dqnykamp and others added 11 commits July 20, 2026 20:09
…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
dqnykamp force-pushed the reader-style-overrides branch 2 times, most recently from f5e0c1d to 5cbd021 Compare July 21, 2026 07:51
…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
dqnykamp force-pushed the reader-style-overrides branch from c4a4d61 to b33c53b Compare July 21, 2026 17:55
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
dqnykamp force-pushed the reader-style-overrides branch from b33c53b to b260ac7 Compare July 21, 2026 18:16
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
dqnykamp force-pushed the reader-style-overrides branch from b260ac7 to 6780792 Compare July 21, 2026 19:24
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
dqnykamp force-pushed the reader-style-overrides branch from 6780792 to 8e71471 Compare July 21, 2026 22:36
dqnykamp and others added 4 commits July 21, 2026 18:00
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
dqnykamp force-pushed the reader-style-overrides branch from 8e71471 to 6e579f7 Compare July 22, 2026 00:03
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
dqnykamp force-pushed the reader-style-overrides branch from 6e579f7 to 01add50 Compare July 22, 2026 03:34
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
dqnykamp force-pushed the reader-style-overrides branch from 01add50 to 886529b Compare July 22, 2026 03:44
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
dqnykamp force-pushed the reader-style-overrides branch from 886529b to e09a535 Compare July 22, 2026 04:48
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
dqnykamp force-pushed the reader-style-overrides branch from e09a535 to 9d7c7ba Compare July 22, 2026 05:20
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
dqnykamp force-pushed the reader-style-overrides branch from 9d7c7ba to e0f044d Compare July 22, 2026 05:35
dqnykamp and others added 5 commits July 22, 2026 00:46
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
dqnykamp force-pushed the reader-style-overrides branch from e0f044d to b35cbd9 Compare July 22, 2026 05:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant