feat(style): add selectable style palettes via new <stylePalette> component#1523
Open
dqnykamp wants to merge 25 commits into
Open
feat(style): add selectable style palettes via new <stylePalette> component#1523dqnykamp wants to merge 25 commits into
dqnykamp wants to merge 25 commits into
Conversation
…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
force-pushed
the
style-palette-component
branch
from
July 21, 2026 01:10
ffa060d to
88ee715
Compare
- 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
…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
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 selectable style palettes: named, coordinated sets of style definitions that an author swaps in with a single tag.
What changed
<stylePalette palette="..."/>component, placed in a document/section directly or in<setup>, scoping exactly like<styleDefinition>. (The attribute ispaletterather thannamebecausenameis the reserved component-naming attribute.)<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.((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.selectedStyle.textColor, and content with nostyleNumberlands 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.<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 (viavalidValues).textColorDarkModederives 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;highContrastColorDarkModederives 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*Worddescriptors still claim four different grays.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. Earlierocean/sunsetpalettes 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.categorical, not CVD-engineered, and documented as such.lineOpacity/markerOpacity(fills are unaffected —fillOpacitydoes 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-in0.7and 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. (defaultstates0.7on every style, so its softer look is unchanged.)textColorandhighContrastColorone light-mode value keeps them paired in dark mode: an unstatedhighContrastColorDarkModetakes the palette's authoredtextColorDarkModerather 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.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.activeStylePaletteName/localStylePaletteNamestate variables on document and sectioning components drive the reset and cycling.reference/stylePalettepage, a palette section in the styling guide, an updated concepts page, and a note on thestyleDefinitionpage that the styles a<styleDefinition>starts from are the active palette's.Tests
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 reachingselectedStyleat full opacity while the default palette stays at 0.7, last-wins with warning.resolve-active-style.test.ts.🤖 Generated with Claude Code