Skip to content

feat(style): add selectable style palettes via new <stylePalette> component#1523

Open
dqnykamp wants to merge 25 commits into
Doenet:mainfrom
dqnykamp:style-palette-component
Open

feat(style): add selectable style palettes via new <stylePalette> component#1523
dqnykamp wants to merge 25 commits into
Doenet:mainfrom
dqnykamp:style-palette-component

Conversation

@dqnykamp

@dqnykamp dqnykamp commented Jul 20, 2026

Copy link
Copy Markdown
Member

Add selectable style palettes: named, coordinated sets of style definitions that an author swaps in with a single tag.

What changed

  • New <stylePalette palette="..."/> component, placed in a document/section directly or in <setup>, scoping exactly like <styleDefinition>. (The attribute is palette rather than name because name is the reserved component-naming attribute.)
  • Semantics:
    • A palette selection resets the base styles for its subtree — ancestor <styleDefinition> overrides are discarded (they were tuned against a different palette's colors and could silently break contrast); <styleDefinition>s in the same section or below apply on top.
    • Style numbers beyond the palette's size cycle through the palette (((n−1) mod N)+1), so every number stays on-palette and keeps its accessibility guarantees. Documents with no palette tag keep today's fall-back-to-default behavior, unchanged.
    • Style number 1 always renders text in the ordinary document text color (black, or white in dark mode). Text and math outside a graph take their color from selectedStyle.textColor, and content with no styleNumber lands on style 1 — so without this rule, choosing a palette would recolor every unstyled <text> and <m> in the document. Palette expansion forces it, including for style numbers that wrap onto style 1. Style 1 keeps its own color everywhere else (lines, markers, fills, highContrastColor), styles 2 and up still color their text, and a <styleDefinition styleNumber="1" textColor="..."> still overrides it.
    • Several <stylePalette>s in one section: last wins, earlier ones get a warning diagnostic. Palette names match case-insensitively; unknown names fall back to the default palette with a diagnostic (via validValues).
  • Eight new built-in palettes, all meeting the WCAG thresholds in both light and dark mode; marker shapes and line widths (and, in several palettes, line styles) vary across styles so distinctions don't rest on color alone. The registry-wide accessibility guard test enforces this for any future palette.
    • okabeito — colorblind-friendly, adapted from the Okabe–Ito palette: light mode darkens each hue only as far as the contrast thresholds require (with separate darker text variants where a graphic anchor sits below 4.5:1); dark mode pins the canonical Okabe–Ito colors, which all clear the thresholds on a dark canvas.
    • tolbright (7 styles), tolmuted (9), and tolhighcontrast (4) — adapted from Paul Tol's colorblind-safe qualitative palettes, and ibm (5) from the IBM Design Library palette, using the same adaptation recipe; tolhighcontrast keeps every pair above dE 14 under every simulated deficiency, and rebuilds its lightness ladder in dark mode so it stays legible in grayscale there too. (Tol's vibrant palette was evaluated and rejected: post-adaptation it degrades to dE 4 and duplicates tolbright.) The reference page leads with a short recommended list (default, okabeito, tolhighcontrast) so the larger registry stays navigable.
    • grayscale — four hueless grays at even CIELAB-lightness steps across the full contrast-allowed range in each mode; the best option for achromatopsia, and the motivation for a new guarantee: every palette has at least four styles (guard-tested), and docs now advise authors to reserve style numbers 1–4 for their most important distinctions since higher numbers wrap on a four-style palette. Its dark-mode text and high-contrast grays are pinned rather than derived, for two different reasons: textColorDarkMode derives by inverting the light-mode lightness, which keeps the four grays distinct but re-steps them ~15 then ~8 L* instead of the even ~20 the palette is built on; highContrastColorDarkMode derives by lightening the light-mode color only as far as 4.5:1 on the dark canvas requires, and every one of these grays starts below that, so with nothing pinned all four collapse onto the same minimum gray while their *Word descriptors still claim four different grays.
    • categorical — ten maximally varied hues adapted from the Category10 sequence (D3 / matplotlib tab10), with conventional color names. No pair falls below CIEDE2000 dE 12 for typical vision in either mode — the widest spread of any built-in palette — for documents needing many obviously distinct styles. Explicitly not CVD-engineered (green/olive and blue/purple converge under red-green deficiency); documented as such, with all ten styles varying marker shape and line style. Earlier ocean/sunset palettes were dropped: staying within one hue family made adjacent style numbers read as shades of each other. A registry-wide distinctness guard test (paletteColorDistinctness.test.ts) enforces the dE floor on every palette in both modes.
    • grumpynarwhal (6 styles) — saturated 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. Its own background and body-text colors were dropped (Doenet owns the canvas), and dark-mode purple's text variant was lightened to clear 4.5:1 on Doenet's dark canvas. Like categorical, not CVD-engineered, and documented as such.
    • Color words (used in the core-computed style descriptions) are pinned wherever the nearest-anchor hex-to-word derivation misnames a color — it called okabeito's blue "cyan" and its reddish purple "red"; derivation remains the default where it is accurate. Fixing the matcher itself is tracked in Improve the hex-to-color-word algorithm used for style descriptions #1527. Words are guard-tested to be unique per style within a palette, and to agree across the keys of a style that share a color, so style descriptions can always tell two styles apart and never describe one style's lines and markers by different names — this surfaced and fixed a historical flaw where the default palette's style 4 dark-mode purple derived the word "blue".
    • Palette lines and markers render fully opaque unless a palette states lineOpacity/markerOpacity (fills are unaffected — fillOpacity does not feed the contrast diagnostic). Palette colors are chosen against the 3:1 graphic threshold at full strength, but an unstated opacity inherits the built-in 0.7 and blends the color 70% into the canvas, landing most styles of every new palette around 2.1-2.9:1. The accessibility guard now composites at the renderer's own opacity fallback instead of assuming 1, so the gap can't reopen silently. (default states 0.7 on every style, so its softer look is unchanged.)
    • A style that gives textColor and highContrastColor one light-mode value keeps them paired in dark mode: an unstated highContrastColorDarkMode takes the palette's authored textColorDarkMode rather than deriving from the light-mode color alone. Deriving independently ignored the brighter dark anchor the palette pinned, so okabeito's and tolhighcontrast's black styles reported a mid-gray high-contrast color while the rest of the style was white, and okabeito's and tolbright's yellow styles reported a dark olive. A guard now requires a style's high-contrast color to answer to the same word as its lines, in both modes.
    • Palette expansion lowercases the enum-valued keys (markerStyle, lineStyle, fillStyle). Authored attribute values are lowercased on their way to a renderer, but palette data doesn't travel that path, so a palette written with the schema's documented camelCase spelling (markerStyle: "triangleDown") reached JSXGraph as an unknown face name and its points rendered invisible. Guard-tested for every palette.
  • New activeStylePaletteName/localStylePaletteName state variables on document and sectioning components drive the reset and cycling.
  • The LSP static style resolver mirrors the palette walk (reset, registry lookup, cycling, case-insensitive name matching), so editor context help matches the runtime; palette names autocomplete with per-palette descriptions (schema regenerated).
  • Docs: new reference/stylePalette page, a palette section in the styling guide, an updated concepts page, and a note on the styleDefinition page that the styles a <styleDefinition> starts from are the active palette's.

Tests

  • Worker: stylePalette.test.ts — selection (document, section, <setup>), scoping, ancestor-override reset, layering, subsection re-selection, cycling (including inherited-palette cycling inside a section), cycled seeding of out-of-range <styleDefinition>s, unknown-name fallback and its diagnostic, case-insensitive names, explicit default-palette selection via a bare <stylePalette/> (still a reset, still cycles), palette-aware style descriptions, camelCase marker styles reaching the renderer in JSXGraph's spelling, neutral style-1 text for unstyled prose (and a <styleDefinition> still overriding it), palette lines/markers reaching selectedStyle at full opacity while the default palette stays at 0.7, last-wins with warning.
  • LSP: palette-aware resolution cases in resolve-active-style.test.ts.
  • utils: registry/expansion/cycling units — including that a palette painting style 1's text has it overwritten, not merely defaulted — plus registry-wide guards that every palette is WCAG-accessible in both modes, keeps its colors pairwise distinct, never collapses two styles onto one high-contrast color, has at least four styles, describes its colors consistently and uniquely, normalizes its enum values, leaves style 1's text neutral, and describes its high-contrast color the way it describes its lines; plus units for the full-opacity and dark high-contrast pairing rules (each verified to fill in only what a palette leaves unstated).

🤖 Generated with Claude Code

dqnykamp and others added 2 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>
@dqnykamp
dqnykamp force-pushed the style-palette-component branch from ffa060d to 88ee715 Compare July 21, 2026 01:10
dqnykamp and others added 6 commits July 20, 2026 20:24
- 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
dqnykamp and others added 17 commits July 20, 2026 22:19
…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
…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
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
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
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
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>
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
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
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
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
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
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
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