Skip to content

fix: text and icons read on what they are drawn on; reduced motion (qorus#646) - #717

Closed
davidnich wants to merge 66 commits into
developfrom
bugfix/contrast-readable-text-and-icons
Closed

davidnich wants to merge 66 commits into
developfrom
bugfix/contrast-readable-text-and-icons

Conversation

@davidnich

@davidnich davidnich commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Text and icons read on what they are drawn on: the qorus#646 contrast work, with the reqore fixes found since then collected here (David's choice), and Foxhoundn's review of it.

Base: develop. #708 merged (0183173, released as 0.78.5) and was merged into this branch (52add6e), so the diff is only what is listed here.

Links: #708 · qoretechnologies/qorus#646

Version: none set here. This PR takes the next free version when it merges (#715 claims 0.78.6).

What is in it

  • Text and icons read on what they are drawn on (§1–§12, §14): colours are picked by WCAG contrast against what is actually drawn, with floors for faded text (4.5:1) and icons (3:1), and the story run checks every story's contrast. This is what most qlip diffs come from.
  • Reduced motion (§13): Reqore honours prefers-reduced-motion; the story run asks for it while it checks contrast.
  • The ControlGroup fold fix (§15).
  • Foxhoundn's review of the contrast work (§16).
  • Row-menu names (§17): a folded row action keeps its own accessible name, and the actions menu is named for its row.
  • A popover takes a click as soon as it is drawn (§18): the ref race behind triggers that opened nothing when clicked straight away.
  • The breadcrumbs' folded menus open at a tap and say they are menus (§19).

It comes from David's review of qorus#646: "black text on a dark grey background — extremely difficult to read". It changes how Reqore picks text and icon colours everywhere, so qlip shows broad changes. Below is what changed and why each kind of diff is expected.

Commits

qlip: on build #384 every changed and new snapshot (179 + 2) is flagged with its cause for review; none is accepted by us.

1. Colours are picked against what is actually drawn, not against the colour a surface is made from (helpers/colors.ts)

  • getReadableColorFrom and getReadableColor choose by WCAG contrast instead of a luminance threshold.
    • They blend a translucent background over what is under it. The muted intent is #f2f2f230. Its opaque hue is near-white, so it used to pick black text even though it is drawn at 19% over a dark page.
    • Where the dimmed #f2f2f2/#0d0d0d does not reach 4.5:1, they fall back to pure white or black. For example, #f2f2f2 on the info blue is 4.2:1 and white is 4.7:1.
    • qlip: on mid-tone intents (info, warning, the lightened accordion and active colours), text switches between near-white and white. It flips to black where black is the colour that reads.
  • New helpers:
    • getDrawnColor
    • getContrastRatio: unrounded, because polished rounds 4.495 up to 4.5.
    • getReadableColorOn: one colour that reads on every colour of a gradient.
    • getReadableAlpha and getReadableFadedColor: fade only as far as the result stays readable.
    • getReadableAccentColor(On): keeps the hue and moves the lightness toward the side that can read on every background.
    • getReadableIntentColor, getReadableThemeColor, getReadableIconLook, getReadableTextOn
    • getReadableGroupOpacity: how far something drawn as a whole can fade before a mark in it stops reading.
    • getThemeSurface(s), fadeColor
  • One-colour gradients (gradient: 'info') keep their text readable at both ends, including one step lighter when hovered or pressed. qlip: such gradients span a little less on the light side.

2. Surface context (ReqoreSurfaceProvider / useReqoreSurface, exported)

  • Components that paint a background hand what they are drawn in (one colour, or every colour of a gradient) to what they hold. These are:
    • Panel content and bar, Button, Message, Callout, EntityRow, Statistic, Testimonial, FeatureCard
    • the Accordion body, NavRail, the Breadcrumbs bar, Table cells, notifications
    • a ControlGroup or Menu drawn in a gradient
  • So an intent span inside a message, a badge on a button, or a tag in a coloured panel now reads against that, not against the page.
  • An app can describe its own background the same way. The notification stories' busy backdrop now does.

3. Faded or secondary text and icons have a floor: 4.5:1 for text and 3:1 for icons (WCAG 1.4.3 / 1.4.11).

  • Covered: descriptions, metadata, timestamps, uppercase labels, placeholders, control icons (CONTROL_ICON_OPACITY), the clear-input button, checkbox and switch words, breadcrumb separators, and the knob icon of an unset switch.
  • qlip: these get somewhat brighter on dark themes and darker on light themes, but only where they were under the floor.

4. Intent-coloured text and icons use a readable shade of the intent

  • Covered:
    • intent on Span, P, Heading and Icon
    • intent-named effect colours
    • text drawn in a gradient
    • Tree, Rating, Callout, EntityRow, Testimonial, Timeline and FeatureCard markers
    • a notification's iconColor
  • qlip: on dark surfaces these are a lighter shade of the same hue.

5. Behaviour changes worth a look

  • NavRail idle: an idleReveal rail rests at idleOpacity (0.34) or less faded, as far as its icons need to still stand out by 3:1. In the default theme it now rests at about 0.64. The idle stories now assert "dimmed but readable" instead of exactly 0.34.
  • Switch: the word on the knob is read against the knob and the other word against the track. Before, one colour was used for both, which put white text on the light info knob at 2.56:1. An unset knob is now faint (stripes at 20%) while its icon stays readable. The whole knob used to fade, icon included.
  • Tag key: the key is darkened only as far as its text needs. Over a light colour, the fixed 30% black left the key in mid-tones where no text colour reaches 4.5:1.
  • Text glow: the letters sit in a halo of the glow colour and are drawn in the colour that reads on it. Before, white letters sat in a white halo inside the glow.
  • Notifications: content you pass yourself, such as a tag, is no longer faded together with the body text under a title. The close button and the pending spinner read on the notification.

6. Bugs found along the way

  • A translucent theme drawn at a panel's opacity became opaque, because rgba(color, opacity) replaced the alpha. EmptyState "Muted" was white on white.
  • Breadcrumbs forced the bar's text colour onto every crumb, including a crumb with its own intent.
  • StackedBar wrote white on every segment.
  • Disabled components that are not form controls now say so with aria-disabled: Tag, Callout, Panel, the Accordion item, EntityRow, Statistic, CollapsibleContent, the Checkbox row, Progress, SeverityRow, StackedBar, Testimonial, the Textarea and RichTextEditor wrappers, and the table cells of a disabled row.

7. The story run's contrast gate (src/stories/utils/contrastGate.ts, an afterEach in .storybook/vitest.setup.ts, and axe-core 4.10.2 as a dev dependency). It runs after transitions finish.

  • Text is checked by axe's color-contrast.
  • Text on a gradient is measured against the gradient's colours where the text actually sits.
  • Text drawn in a gradient is measured in each of its colours.
  • Icons are checked at 3:1.
  • The background is composed the way the browser draws it:
    • translucent layers are blended;
    • a fill over a ring counts alone;
    • an element with an opacity is drawn as a whole and then faded, so a mark and what it is on fade together;
    • a floating layer is drawn over the app layout's surface.
  • Exempt: disabled content, content that is not drawn, and decoration marked data-decorative.
  • Fail-first: the first gated run failed 524 of 1186 story tests in 62 of 75 files. With these commits, every story passes.

Some stories put colours together that no text colour can read on. Examples:

  • a cyan→pink badge gradient
  • purple→pink groups and menus
  • a light green backdrop blob
  • dark icons on a dark panel
  • a muted wash on a mid-blue picker

Those stories now use the same kind of colour in a shade the text reads on, so the demos model readable choices.

8. An icon's colour given in its props (d3c8108, 8ad3d8c)

  • A button, an input, a tag and a panel's title draw an icon at CONTROL_ICON_OPACITY and pick its colour and opacity together so that it stands out by 3:1. A colour given in the icon's own props (leftIconProps={{ intent }}, a panel's iconProps) was spread over that pick: the icon made it readable at full opacity, and drawn at 70% a warning icon was 2.4:1 (814 findings in the Qorus IDE's contrast gate).
  • Each now takes that colour as the icon's own (it still wins over the component's own, as it did when spread last) and picks it like leftIconColor; the rest of the props pass on. One helper, splitIconColor (helpers/iconProps).

9. A link's intent reads on the surface it is drawn on (e8b4ec2): on a warning-tinted table cell it was 3.5:1, over an entity row's gradient 2.8:1.

10. An active button draws what it holds on its fill (2a15c0f): a selected menu item's or an active sidebar toggle's description, badges and icons were picked for the resting wash (2.0–2.7:1 on the info blue). The NavRail's idle floor reads its active mark the same way. qlip: the NavRail idle stories rest brighter.

11. An entity row fades its description's and metadata's text, not what they hold (0fda83b): a tag or a link inside was faded twice (4.0:1, 2.8:1).

12. A translucent gradient is read with the lighter ring that shows through it (aeb3d8c): a statistic's label on a translucent glow read 3.6:1.

13. Reduced motion (79b6fe6)

  • Under prefers-reduced-motion: reduce (WCAG 2.3.3), Reqore's springs settle at once (react-spring's skipAnimation, set by ReqoreUIProvider as it renders and kept current), and CSS transitions inside the layout and the portal take 0.01ms (not none: a transition of no time sends no transitionend). Keyframe animations are as they were.
  • The contrast gate asks for reduced motion while it measures (a browser command, emulateReducedMotion), so it measures what stays on screen, never a frame of a fade. A story tagged reduced-motion runs under it; the others run with the motion a user sees. qlip captures before the gate, and each story resets to normal motion first, so this changes no existing snapshot.

14. DeepScan: readable-colour props are one object while what they hold holds (e563e64)

  • DeepScan reported 22 new issues in the contrast work: 21 REACT_INEFFICIENT_PURE_COMPONENT_PROP (a faded description, label or icon effect, a style or a handler built inline and handed to a memo component) and 1 INSUFFICIENT_NULL_CHECK in Panel.
  • Each is memoized on what it is made of, in Button, Input, Tag, Checkbox, Callout, EntityRow, FeatureCard, RichTextEditor, SegmentedControl, Statistic and Timeline; Panel reads the description effect null-safely.

15. ControlGroup: a responsive group folds only what it has, and each child once (2f2bbcf)

  • A responsive group kept folding forever once every child was in its menu and its overflow button alone still did not fit (an endless fold loop).
  • It drew a child twice, in the row and in the menu, when its children had holes.
  • It crashed at its first fold with a single child.
  • The fold code it changes is the same in develop.

16. Foxhoundn's review of the contrast work (b824566, 11fdbfa, 951549c, cbcd1ad)

  • Colour helpers: a colour they cannot parse (a CSS variable, currentColor, a name) is drawn as given instead of throwing; a short hex is read as CSS reads it (#baf is #bbaaff, not a green); results are cached.
  • Components: explicit values (idleOpacity, an explicit effect opacity, a hex icon colour) are the caller's, and the floors apply to defaults only; own fills and surfaces are honoured (a button's or tag's effect.color, a customTheme, a popover's and a drawer's own fill); colours are worked out once per render; reduced motion stays inside Reqore.
  • Stories: icon colours and opacities given as values are ones that read where they are drawn.
  • DeepScan: memo components get the same props while what they show holds.

17. Row-menu names (e970c1f)

  • When a narrow EntityRow or SeverityRow folds its actions into one menu, an item keeps its action's own aria-label as its accessible name ("Show analytics for Short copy", not only "Show analytics").
  • The menu button, and the menu it opens, are named for their row when the row's label is text: "Actions: Short copy" (rowLabel on ReqoreRowActionsMenu, rowLabelText). A row whose label is an element keeps the plain "Actions". A table's row menu is unchanged.
  • No pixels change.

18. A popover takes a click as soon as it is drawn (dceb77d)

  • useCombinedRefs handed a component's element to its refs in an effect, after the paint. A popover around a Reqore button or a dropdown learnt of its trigger only then, so a trigger clicked straight away opened nothing.
  • useCombinedRefs now returns setRef, a ref callback React calls in the commit that attaches the element. Button, Input, Textarea, Tabs item, FadeScroller, Table body and header, Panel, Menu and Menu item use it.
  • Test: popoverArmedWhenDrawn (the dropdown case fails before the fix). No pixels change.

19. The breadcrumbs' folded menus open at a tap and say they are menus (4193ebc)

  • A dropdown opens at a tap since 26327a4 (in develop, 0.78.5), the breadcrumbs' two fold menus included; nothing tested it for them.
  • Both fold menus (the folded current crumb and the "…") now say aria-haspopup="menu" and aria-expanded; the "…" dropdown has the class reqore-breadcrumbs-overflow-menu.
  • New touch story Navigation/Breadcrumbs › Folded Trail Opens At A Tap (390 px): a finger opens each menu and reaches "Back to chats". Its aria checks fail before the change, and its tap checks fail with the dropdown's tap opening taken out. qlip: one new snapshot.

Checks (head 4193ebc)

  • Typecheck and lint pass; 1883 unit tests pass.
  • The gated story run (every story file, one at a time, with the contrast gate): 1193 passed, 0 failed.
  • CI is green; DeepScan: 0 new issues, 26 fixed.
  • Every fix since the first gated run has a test that fails first.

🤖 Generated with Claude Code

David Nichols and others added 12 commits October 15, 2026 12:46
…wn on (qorus#646)

David's review found black text on a dark gray background: the muted
intent (#f2f2f230) picked its text from the near-white it is made of, but
is drawn at 19% over a dark page. Text and icon colors are now picked
against what is actually drawn, to WCAG AA: 4.5:1 for text, 3:1 for icons
and large text.

- getReadableColorFrom / getReadableColor choose the candidate with the
  higher contrast (not a luminance threshold), draw a translucent
  background over what is under it (the theme it was made from), and give
  up the dimmed #f2f2f2 / #0d0d0d for pure white / black where the dimmed
  one is under 4.5:1 (#f2f2f2 on the info blue is 4.2:1).
- New: getDrawnColor, getContrastRatio (unrounded: polished rounds 4.495
  up to 4.5), getReadableColorOn (one color for every color of a
  gradient), getReadableAlpha / getReadableFadedColor (fade only as far
  as it reads), getReadableAccentColor(On) (keep the hue, move toward the
  side that can read on every background), getReadableIntentColor,
  getReadableThemeColor, getReadableIconLook, getReadableTextOn,
  getReadableGroupOpacity (how far a group drawn as a whole can fade and
  every mark in it still read), getThemeSurface(s), fadeColor (a
  translucent color at a panel's opacity stays translucent; rgba()
  replaced its alpha).
- A one-color gradient (buildGradientColorsFromString) keeps its text
  readable at both ends, and a step lighter as it is drawn hovered or
  pressed: info's light end was 3.25:1 with white.
- ReqoreSurfaceProvider / useReqoreSurface (exported, with
  TReqoreSurface): what a component paints - one color, or each color of
  its gradient - is handed to what it holds, which reads against it
  rather than the page. An app can tell Reqore about a background of its
  own the same way.

Tests that fail before: __tests__/helpers/readableColors.test.tsx (the
helpers missing, black picked for the muted intent, #f2f2f2 kept on the
info blue, info's gradient 3.86:1 a step lighter) and
__tests__/surfaceContext.test.tsx.
…orus#646)

Each component picks its text and icon colors against the background it
actually draws (a minimal wash, a translucent intent, a gradient, a
lightened header), hands that background to what it holds, and fades
secondary text and icons only as far as they stay readable (4.5:1 text,
3:1 icons and large text; WCAG 1.4.3 / 1.4.11).

- Button: text, description and icons against its drawn fill at rest,
  hovered and active; a minimal or transparent button keeps its color in
  a readable shade; badges read on the button, every color of it.
- Tag: solid, minimal, soft and text tags, against every color they are
  drawn on; the key is darkened as far as its text needs (over a light
  color 30% black left it in the mid-tones, where nothing reads); icons
  and actions; label and key effects read on the tag.
- Input / Textarea / RichTextEditor: text, placeholder, icons and the clear
  button against the field's own wash.
- Span / Paragraph / Heading / Icon: an intent, or a theme color named by
  name, in a shade of it that reads where it is drawn; text drawn in a
  gradient is drawn in colors that read, hovered and pressed too; a text
  glow's letters sit in a halo of the glow and are drawn in the color
  that reads on it (white letters in a white halo inside a red glow were
  3.85:1).
- An effect with several gradients is read as the layers they are drawn
  as, each over the ones under it.
- Panel (content, the title bar over the content's gradient, description,
  title icon, a title rendered as a link), Message, notifications,
  Callout, EntityRow, Statistic, Testimonial, FeatureCard, Accordion,
  Timeline, Table cells, NavRail, Breadcrumbs (the separator too),
  SegmentedControl, CollapsibleContent's reveal button, Rating, Tree,
  StackedBar, DescriptionList, Bubble timestamps: each reads against its
  own surface; one with a gradient hands every color of it to what it
  holds. A ControlGroup or a Menu drawn in a gradient does too, and a
  menu divider's label reads on the menu.
- Checkbox: the box against every color under it; a switch's word on the
  knob reads on the knob and the other on the track (one color for both
  was white on the light info knob, 2.56:1), a gradient track and a knob
  in the effect's colors are read as drawn, and an unset knob is drawn
  faint without fading its icon, which still stands out by 3:1.
- Notifications: the text, icon, close button and pending spinner read on
  the notification as drawn over every color under it; content of the
  caller's own (a tag) is no longer faded with the text under a title; an
  iconColor is drawn in a shade of it that stands out from its tile.
- NavRail: an idle rail rests at idleOpacity, or as little less faded as
  its icons need to stand out by 3:1 - the marks' and the active mark's on
  its fill, faded into the page with the rail (it rested at 0.34: 2.3:1).
- A translucent theme drawn at a panel's opacity stays translucent (the
  muted EmptyState was white on white).
- Breadcrumbs no longer force the bar's text color onto a crumb with its
  own intent (dark text on the warning crumb).
- StackedBar writes each segment in what reads on it (white on a light
  segment was 1.9:1).
- Disabled components that are not form controls say so (aria-disabled):
  Tag, Callout, Panel, Accordion item, EntityRow, Statistic,
  CollapsibleContent, the Checkbox row, Progress, SeverityRow, StackedBar,
  Testimonial, the Textarea wrapper, a disabled row's table cells. WCAG
  exempts disabled controls from contrast; assistive technology is told.

Tests that fail before: __tests__/componentSurfaces.test.tsx (a group or
a menu drawn in a gradient hands its colors on), __tests__/tagColors.test.tsx
(a tag's key darkened as far as it takes, a tag reads on every color it
is drawn on). The story run's contrast gate (the last commit) failed 524
of 1186 story tests before these and the story colors, and none after.
…us#646)

Some stories put text or icons on colors no text color reads on: a
cyan-to-pink badge gradient, a purple-to-teal item, purple-to-pink groups
and menus, a pink message, an orange badge under a darkened key, a light
green backdrop blob, a magenta sphere, dark icon colors on a dark panel, a
red spinner and a red bold leaf on dark gray, a muted wash on a mid-blue
picker. Each keeps the same kind of color in a shade the text reads on,
so the demos show readable choices. The notification stories' busy
backdrop tells the notifications on it what it is drawn in
(ReqoreSurfaceProvider), as an app would for a background of its own,
and its row labels are a shade less faint.
…orus#646)

After each story settles (its transitions and finite animations
finished), in its theme:
- text by axe-core's color-contrast rule (4.5:1, 3:1 when large);
- text on a gradient, which axe cannot decide, against the gradient's
  colors where the text is drawn on it, and text drawn in a gradient
  (background-clip: text) in each of its colors;
- icons (an svg in .reqore-icon) at 3:1 against what they are drawn on
  (WCAG 1.4.11), which axe does not check.
What a mark is drawn on is composed as the browser draws it: each
ancestor's paint over what is under it (a translucent color blended, a
gradient as its colors along the part the mark covers, a fill over a ring
counting alone), an element at an opacity drawn as a whole - its paint
and everything in it - then faded into what is under it, and a floating
layer (a popover, a modal) over the app layout's surface. Disabled
content is exempt (WCAG), as is anything not drawn and decoration the
page marks as such (data-decorative).

Fail-first: the first gated run failed 524 of 1186 story tests in 62 of
75 story files (Button 22/25, Tag 18/22, Table 49/71, Panel 32/59, ...);
with the previous commits every story passes. Unit tests:
__tests__/contrastGate.test.ts (the gradient position, faded group and
settling cases fail on the gate before them). axe-core 4.10.2 is a dev
dependency.

src/components/COMPONENTS.md has a section on how Reqore picks text and
icon colors now: against what is drawn, the surface handed down
(ReqoreSurfaceProvider), the floors on fades, what stays the caller's.
…city the button draws it at (qorus#646)

A button draws its icons at CONTROL_ICON_OPACITY and picks an icon's own
color (leftIconColor, iconColor) in the shade and at the opacity that
stand out by 3:1. A color given in the icon's own props instead -
leftIconProps={{ intent: 'warning' }}, as the Qorus IDE's readiness rows
give it - was spread over that look: the icon made it readable at full
opacity and the button drew it at 70%, a warning icon at 2.4:1 (814
findings in the IDE's contrast gate).

The button now takes the intent or color in leftIconProps /
rightIconProps as the icon's own color (they still win over leftIconColor,
as they did when spread last) and picks it as it does leftIconColor; the
rest of the props are passed on as before.

Test (fails before): an icon colored through its props is drawn exactly
like one colored through leftIconColor, left and right; that look stands
out by 3:1 at rest and hovered, on the dark theme and a light one; the
other icon props (its size) still reach it.
…ld holds (qorus#646)

The contrast work (85f6099..98c46c3) computes each faded description,
label and icon opacity in the render and hands it to a memo component
(ReqoreIcon, ReqoreP, ReqoreSpan, ReqoreButton) as a new object every
time, and DeepScan reported 21 REACT_INEFFICIENT_PURE_COMPONENT_PROP and one
INSUFFICIENT_NULL_CHECK as new on #708 (FAILURE at 98c46c3, 6e203e6 and
5960623).

Each such effect, style and handler is memoized on what it is made of:
Button, Input and Tag's icon effects; Checkbox's knob and box icons;
Callout, EntityRow and FeatureCard's descriptions (and EntityRow's
metadata, Callout's close handler); RichTextEditor's placeholder and
paragraph styles; SegmentedControl's item refs (one callback per item);
Statistic's label; Timeline's timestamp and content styles. Panel reads
the description effect's color and opacity null-safely, as it reads its
textSize below.
…ks for it (qorus#646)

A user who asks for reduced motion (WCAG 2.3.3) still got every spring
Reqore runs - an input's clear button fading in, a drawer sliding, a
reveal - and every CSS transition. A contrast check measured them on
their way: the clear button, which rests at 4.8:1, at 1.4-2.6:1 in the
IDE's gate (a react-spring fade, which no gate can wait on).

- ReqoreUIProvider turns react-spring's skipAnimation on while the user
  prefers reduced motion (set as it renders, so the first render's
  springs settle too, and again when the preference changes; it turns
  off only what it turned on): every spring settles at once, at its end.
- Its global style takes every CSS transition inside the layout and the
  portal to 0.01ms under the preference (not none: a transition of no
  time sends no transitionend, which some components wait for).
  Keyframe animations (a spinner, a pulse) are as they were.
- The story run's contrast gate asks for reduced motion while it
  measures (a browser command, emulateReducedMotion), so it measures
  what stays on screen, never a frame of a fade. A story tagged
  `reduced-motion` runs under it; the others run with the motion a user
  sees, as their checks of a marquee, a count-up or a sheet expect.

Tests (fail before): under reduced motion an input's clear button is at
its settled opacity on first paint and Reqore's springs skip; the story
Form/Input "Transitions Under Reduced Motion" finds the clear button's
transition at 0.01ms (0.1s without the preference).
…put, a tag or a panel draws it faded (qorus#646)

As with the button (5960623): an input's icons, a tag's and a panel's
title icon are drawn at CONTROL_ICON_OPACITY, and the component picks an
icon's own color and that opacity together so that it stands out by
3:1. A color given in the icon's props - leftIconProps={{ intent }},
iconProps={{ color }} - was spread over the pick: the icon made it
readable at full opacity, and drawn at 70% it fell short (a collection's
filter icon given leftIconProps={{ intent: 'muted' }} at 2.2:1, the IDE's
gate). Each now takes it as the icon's own color (it still wins over the
component's own, as it did when spread last) and passes the rest on. The
split is one helper, splitIconColor (helpers/iconProps), the button uses
it too.

Test (fails before): an input's left and right icon, a tag's and a
panel's title icon colored through their props are drawn exactly like
ones colored through the component's own prop.
…#646)

A link with an intent was written in the shade of it that reads on the
theme's page, wherever it was: on a table cell tinted with the warning
intent 3.5:1, over an entity row's gradient 2.8:1 (the IDE's gate). It
now reads the surface a component around it hands down, as a span does.

Test (fails before): an info link reads 4.5:1 on a warning-tinted cell
and on every color of a gradient, in a shade of blue.
…646)

An active button (a selected menu item, a sidebar section on the active
path) is filled with its color, but its description, its badges and its
icons were picked for the button at rest - its color's wash over the
page: a light blue description 2.0:1 and badge 2.7:1 on the info blue
of the IDE's account menu, a grey description 4.4:1 on a sidebar's
section toggle. The description is now picked on the active fill, the
button hands that fill to what it holds, and its icons are picked on it.
The nav rail reads its active mark as the button now draws it (its icon
on the fill, in the rail's activeEffect), so it still rests only as
faded as that icon stands out by 3:1.

Tests (fail before): an active button with a color writes its
description and draws its badge on its fill; one without a color writes
its description on its fill; at rest the description reads as before.
The NavRail idle stories fail with the old rail and this button.
…at they hold (qorus#646)

An entity row faded its description and metadata as a whole, so what
they hold with a color of its own was faded a second time: a minimal
warning tag in the metadata at 4.0:1, a link in the description at
2.8:1 (the IDE's gate). Their text is now written in the faded color
(the readable floor as before; an opacity a caller gives is still that
floor, a color a caller gives is the caller's), and a tag or a link in
them is drawn as it is.

Test (fails before): both slots are drawn whole in a faded color, a tag
and a link in them are not faded, and a caller's opacity is kept.
…through it (qorus#646)

A gradient effect paints its fill over a lighter ring of the same
gradient (border-box). With an opaque fill the ring is hidden; with a
translucent one - a glow of 'info:lighten:1:0.5' over the page - it
shows through, but the colors a component read its text against left
it out: a statistic's label faded for the blue over the page read 3.6:1
on what was painted (the IDE's support hero). The effect's own readable
colors and getEffectBackgrounds now draw the fill over the ring (one
fill layer stop by stop, as they share the gradient's geometry).

Test (fails before): a statistic's label stands out by 4.5:1 from the
corner of a translucent radial glow as it is painted, ring included.
@davidnich

Copy link
Copy Markdown
Member Author

CI note: this PR's base is #708's branch (bugfix/focus-popover-opens-when-focused), and tests.yml and pr_prerelease.yml run only for pull requests based on develop or main. So Tests, the PR prerelease and the qlip build do not run here yet. Once #708 merges, this PR is retargeted to develop; CI, the prerelease and the qlip build (contrast and reduced-motion changes only) run then.

Checked locally at aeb3d8c, a tree byte-identical to #708's 0fba3d8, which CI built as 0.78.5-pr.708.g0fba3d8:

  • typecheck and lint pass;
  • 147 test files, 1747 unit tests pass;
  • the gated story run passes 1192/0.

DeepScan has run here: 0 new issues.

…ch child once (qorus#646)

A responsive group folds its children into a `…` menu one at a time while
its row overflows. Three things were wrong with how it counted:

- Nothing stopped the count once every child was in the menu. A group
  whose overflow button alone is wider than its room (a labelled or large
  button, a narrow container) kept counting. Each step re-rendered the same
  row and measured it again, forever. In a browser that is a render loop at
  frame rate; under act it never returns. The Qorus IDE's agent found the
  unbounded counter while chasing a frozen story.
- The row sliced the raw `children` array, but the menu took from
  React.Children.toArray(children), which leaves out the holes a caller
  leaves (null, false: an action it decided not to render). With a hole in
  front, a child was drawn in both the row and the menu.
- A group with one child, which is not an array, threw at its first fold
  (`children.slice` is not a function).

The row, the menu and the count now read one list, `foldable`: the
children as React renders them, holes left out. The count stops when the
row fits or when nothing is left in the row to fold. That bound is the end
of the list, not an iteration cap. The old comment explained why the count
could not be bounded by the number of children: the row and the count read
different lists. They no longer do.

Tests: controlGroupFoldIsBounded. Three cases fail before the fix: the
group measures itself past the test's limit, `children.slice is not a
function`, and a child is in both the row and the menu. A fourth case, the
row still stops as soon as it fits, passes both before and after.
@Foxhoundn

Foxhoundn commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Heads-up @davidnich: I'm fixing the contrast findings from my review of #708 here. Please hold pushes until I comment that it's in. First I'll merge #708's new commit (26327a4, the touch fixes) into this branch. Then:

  • Crashes: colours the helpers can't parse ('red', var(--x), #f00a in descriptionEffect.color, an intent) no longer throw.
  • Wrong text colours:
    • 3- and 4-digit hex parses correctly;
    • a transparent or minimal element with its own customTheme reads its text on that colour;
    • theme.breadcrumbs.item.color reaches the crumbs;
    • a transparent Statistic, Callout's own text, and the EntityRow actions inside the row's surface.
  • Render cost: cached colour helpers, a surface provider that keeps its value while its colours are equal, and colours memoised per component.
  • Explicit values honoured: idleOpacity, an explicit opacity, explicit hex icon colours, and a caller's ifLight/ifDark pair.
  • Popovers and drawers provide their own surface.
  • aria-disabled: a consumer's own value is kept.
  • Reduced motion reaches only Reqore's own springs and elements, not the app's.

…focused' into bugfix/contrast-readable-text-and-icons
Base automatically changed from bugfix/focus-popover-opens-when-focused to develop October 15, 2026 13:44
Foxhoundn and others added 5 commits October 15, 2026 15:51
…rt hex as CSS reads it, and results are cached (qorus#646)

Review of the contrast pass found these in the color helpers:

- A color the helpers cannot parse (a CSS variable, `currentColor`, a
  name, a typo, `#f00a` before this fix) threw. It is now returned as it
  is given: `getContrastRatio` is NaN for it, a surface it names is left
  out, and a wash of it is mixed by the browser (`getColorAtAlpha` writes
  `color-mix(...)` for it). `isReadableColor` tells whether the helpers
  can read a color.
- A short hex was repeated instead of doubled (`#fdc` read as `#fdcfdc`).
  polished writes colors short where it can (`#bbaaff` as `#baf`), so a
  minimal button's text came out green instead of the lavender it was
  picked as. Every helper now reads and writes the long form.
- `getContrastRatio` is unrounded (polished rounds luminance to three
  places: `#595959` on black was 3.0, it is 2.998).
- An explicit hex icon color is drawn as given, as a standalone
  ReqoreIcon draws it and as COMPONENTS.md says; only a theme color (an
  intent, `main`, a shade of one) is moved to a readable shade
  (`getReadableIconLook` -> `getReadableThemeColor`).
- A caller's own `ifLight` / `ifDark` pair stays theirs when neither
  reaches 4.5:1: the better of the two is returned.
- Each pure helper keeps its last 2000 results (`createColorCache`):
  every Button, Tag, Effect and Input re-render ran the color maths again.
- `ReqoreSurfaceProvider` keeps its value while its colors are the same,
  so an inline effect does not re-render everything below it.

Tests: helpers/colorParsing, helpers/explicitColors,
surfaceProviderIdentity.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, colors are worked out once per render, and reduced motion stays inside Reqore (qorus#646)

The rest of the review of the contrast pass:

- Crashes: `descriptionEffect.color: 'red'`, a CSS variable or an intent
  named where a hex was expected no longer throw (entity row, callout,
  feature card, panel, statistic).
- Explicit values are the caller's: a nav rail's `idleOpacity`, an
  explicit `descriptionEffect` / `labelEffect` / `metadataEffect`
  `opacity`, a hex icon color on a button, a tag or an input. The
  floors apply to the defaults only.
- Own fills and surfaces:
  - `effect.color` on a button or a tag is read on its own fill, at
    rest and hovered; a soft or text tag reads on every stop of a
    gradient it sits on;
  - a transparent or minimal element with its own `customTheme` reads
    on that color; a callout's own text, a transparent statistic or
    testimonial, and an entity row's actions read on their own surface;
  - a popover's content and a drawer read on their own fill, not on
    whatever opened them;
  - a nav rail's active group hands its gradient (or tint) to its marks:
    a resting section mark picked its text for the rail around the
    group, and only read on the Effects story because `#baf` was misread
    as a light green;
  - `theme.breadcrumbs.item.color` reaches the crumbs; description list,
    rich-text placeholder, stacked bar, timeline, message, tree and the
    segmented control's More button read on what they are drawn on.
- `aria-disabled`: a component that is not disabled keeps the one its
  caller gives.
- Render cost: Button and Tag work their colors out once per render
  (`getButtonPaint`, `getTagPaint`) instead of in each style rule, Tag
  is a `memo` component, Effect remembers its gradient pass, Span, P and
  Link memoise their color, Input and Textarea use `getInputLook`. A
  hovered button's text color declaration lost its semicolon and was
  dropped with button animations on.
- Reduced motion reaches Reqore's own springs and `reqore-` elements
  only; the app's own react-spring animations and transitions are left
  alone (no `Globals.assign`).

COMPONENTS.md describes each. Tests: ariaDisabledPassThrough,
breadcrumbsColors, colorTolerance, controlOwnColors,
descriptionColorTolerance, explicitOpacityAndAria, fieldColors,
messageColors, navRailActiveGroupSurface, notificationColors,
portalSurfaces, reducedMotionSprings, stackedBarColors, timelineColors,
unpaintedSurfaceText, and updates to DescriptionList, reducedMotion and
segmentedControl.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ere they are drawn (qorus#646)

A hex icon color and an explicit opacity are now drawn as given, so the
stories that passed values which only read after Reqore changed them
fail the contrast gate. Each keeps the same kind of value in one that
reads:

- Button: the minimal button's left and right icons in a light purple
  and blue (they were near-black on the dark washes); the described
  button's icon in `success:lighten:2`, a theme color, since one hex
  cannot read on both the white and the dark fills the template is drawn
  in.
- Tag and Input: a lighter pink and purple.
- EntityRow and Testimonial With Effects: metadata and role at 0.7
  opacity (0.6 read at 4.18:1 and 4.13:1).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d-icons' into bugfix/contrast-readable-text-and-icons

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#717 now targets develop: #708 merged as 0183173. The only commit new to this
branch is 26327a4 (a way in opens at a tap; hydration keeps the server's media
answer). It merged without conflicts in Panel, SegmentedControl and
COMPONENTS.md.
@Foxhoundn

Copy link
Copy Markdown
Member

It's in, at 473ada3. @davidnich, you can push again.

The fixes listed in my last comment are in three commits, on top of a merge of #708 (26327a4, the touch fixes) and of your 2f2bbcf:

  • b824566 fix(colors): the helpers;
  • 11fdbfa fix(components): the components, their tests and COMPONENTS.md;
  • 951549c fix(stories): story values (below).

Two things you didn't see in my last comment

  • Stories with explicit values. A hex icon colour and an explicit opacity are now drawn as given, so 39 stories failed your contrast gate. They had passed only because Reqore changed the values. I changed the stories' values, not the fix, in the same way as 76de251:
    • Button: light purple and blue icons on the minimal button; success:lighten:2 on the described one, because one hex can't read on both the white and the dark fills;
    • Tag and Input: a lighter pink and purple;
    • EntityRow and Testimonial "With Effects": opacity 0.7 instead of 0.6, which read at 4.18:1 and 4.13:1.
  • Nav rail active group (in 11fdbfa). The group paints activeEffect's gradient (or its intent's tint) but never handed it to its marks. A resting section mark therefore picked its text for the rail around the group.
    • NavRail "Effects" only passed before because of the short-hex bug: #bbaaff is written #baf, which was read as #bafbaf, a light green that happened to stand out on the purple. Read correctly, it was 2.86:1.
    • The group now provides its surface. Test: navRailActiveGroupSurface (1.15:1 before the fix).

Checks at 473ada3: lint and tsc pass; unit tests 1874/1874; stories 1192/1192 locally (desktop and touch, no upload). I looked at the changed stories' captures. CI and qlip don't run here until this PR targets develop, as you noted.

Left open: the version. These are changed defaults (explicit values drawn as given), so it's your call, as on #708.

…w holds (qorus#646)

DeepScan's REACT_INEFFICIENT_PURE_COMPONENT_PROP, 13 new on 473ada3:

- ReqoreTag is a memo component now, so every new object it is handed
  is flagged: DataView's bold and type-label effects are module
  constants, and the dropdown's trail of levels takes its click handlers
  from a `useCallback` and a `useMemo`'d list instead of new arrows.
- The segmented control's More button had the same three issues before
  (its props, its menu and the menu items' click handlers); its tap and
  surface changes moved their lines, so DeepScan counted them as new.
  They are memoised now.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@qlip-qore-technologies

qlip-qore-technologies Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

qlip visual regression

🔍 183 changed, 33 new of 1224 snapshots.
⏳ Awaiting review.

Review build 20261019-191102 in the dashboard →

David Nichols added 2 commits October 15, 2026 17:15
…s named for its row (qorus#646)

When a narrow EntityRow or SeverityRow folds its actions into one menu, the
action's own aria-label was not passed on to its menu item. reqraft's
experiment card has "Show analytics for Short copy" on a button; folded, the
item was called only "Show analytics", the same as the item in every other
row's menu. Every menu button was also called "Actions", so a list of
versions read as a list of identical buttons.

- An item now keeps its action's own aria-label as its accessible name. Its
  words still come from its label, tooltip or aria-label, as before.
- The menu button, and the menu it opens, are named for their row when the
  row's label is text: "<actionsMenuLabel>: <row label>", for example
  "Actions: Short copy" (rowLabel on ReqoreRowActionsMenu, rowLabelText).
  A row whose label is an element keeps the plain "Actions". A colon rather
  than "for", so a host's own word ("Akce") reads right in its language.
- Applies to ReqoreEntityRow and ReqoreSeverityRow. A table's row menu is
  unchanged, since its rows have no label of their own.

Tests: rowActionsMenu, EntityRow and SeverityRow. Eight cases fail before
the fix: items keep their names, the button and menu are named for the row
(also with a host word), a blank row label names nothing, rowLabelText, and
the rows' folded menus. The EntityRow and SeverityRow narrow-menu stories
check the row-named button. COMPONENTS.md documents both.
…over takes a click at once (qorus#646)

useCombinedRefs handed a component's element to the refs it was given in an
effect, after the browser had painted it. A popover around a Reqore button
or a dropdown learnt of its trigger only then, and armed its click listener
in the render after that. So a trigger on screen opened nothing when it was
clicked straight away, and nothing on the page said when it would. The Qorus
IDE's story harness kept sleeps for it (a dropdown's .template-selector,
among others). The effect also ran after every render, since its list of
refs is a new array each time.

useCombinedRefs now returns setRef, a ref callback. React calls it in the
commit that attaches the element, so every ref holds the element before the
paint. It changes only when a ref given changes, and then React hands the
element over as it does for any new ref callback. Button, Input, Textarea,
Tabs item, FadeScroller, Table body and header, Panel, Menu and Menu item
draw their element with it. The four that draw through a ref callback of
their own (Popover, Panel, Menu, Menu item) call it from a callback that is
one function for the component's life: a new callback each render would
detach and attach the element every time. The unused `_innerRef` state, set
again on every render, is gone.

Tests: popoverArmedWhenDrawn. The page is drawn and committed with flushSync
and nothing more, which is the state of a page as it is first painted. The
trigger is then clicked at once. A dropdown's popover opens; it stayed shut
before the fix. A popover around a Reqore button opens too, which already
held before the fix.
@davidnich

Copy link
Copy Markdown
Member Author

Pushed ae33bdc2: round 3, the two hover leads of this PR's /audit (qorus#646). Prerelease: @qoretechnologies/reqore@0.78.7-pr.717.gae33bdc. No version bump.

  • 4a4039c, B7: a clickable card's text reads on its hover fill too. A clickable entity row's metadata was faded only as far as it read on the resting fill, and was 4.18:1 hovered. The cards' hover fills are one helper on a shared getCardHoverFill. A clickable card's surface hook reads its text on that fill as well (hoverFill).
  • ae33bdc, B6: a hovered button's description reads on its hover fill. It was faded for the resting fill (3.13–4.38:1 hovered on Button · Default, Info and White). Hovered, it is faded from the hovered text, as far as it reads on the hover fill.
  • Test: cardHoverFill. It reads the :hover fill the stylesheet paints, and fails before both fixes: 4.19:1, then 4.39 and 3.83:1.
  • Measured hovered in the browser (playwright hover + axe): no text under 4.5:1 on any of those stories.
  • Gates at ae33bdc: precheck 1929/0. Every story file, run one at a time: 1201 passed, 0 failed. CI green.
  • qlip build Added new flag that allows tabs to stay mounted only to have display set to none when inactive #391: no frame changes at rest. 186 frames are identical to chore: Update package version to 0.46.6 and add hover show option for tag action #390, and 6 differ by at most 5 color levels, which is rendering noise. All 192 are flagged with their causes.

David Nichols added 6 commits October 17, 2026 17:50
…e end of the label's line (qorus#646)

In the opt-in 'menu' mode, two or more actions fold into one menu button at
the end of the label's line when the row is narrow. A row of that mode with
one action had no menu to fold into, so it wrapped the action under the
label: a line of its own for one button, where a row with more had none.

Now that one action stays a button at the end of the label's line, where the
menu would be. The row says so: `data-actions='end'`. Only 'menu' rows
change. The default ('row', visible buttons under the label) and 'stack' are
as they were (David, qorus#646).

Stories, both failing before (the action was under the label):
- Entity Row > Narrow Container Lone Action In Menu Mode (a 400px container);
- Entity Row > Narrow Container Lone Action In Menu Mode On A Phone: a
  finger's size, and a tap runs the action without opening the row.

Narrow Container Actions Menu (two or more) is unchanged. The prop's docs,
TReqoreNarrowRowActions and COMPONENTS.md say it.
…om its render (qorus#646)

The date picker read its value, and its minValue and maxValue, with
parseAbsoluteToLocal, which throws for anything but an ISO date-time with a
zone. Any other string threw from the render, and so did an invalid Date
(through toISOString). The Qorus IDE's Workflow Orders page went to its
router's error page for "2026-06-02T08:00:00".

The rule, in one reader (readDate) that never throws:
- A date-time with a zone or an offset is that instant, in the user's
  zone, as before.
- A date-time without one is that wall-clock time in the user's zone: ISO
  8601 reads a date-time with no offset as local time.
- A date alone is that day at midnight, local.
- Anything else, text that is no date or an invalid Date, is no date. The
  field is empty, and says it was given a value it cannot read the way it
  says a value out of its bounds: it is invalid (aria-invalid,
  data-invalid), drawn in the danger intent.
- A bound it cannot read is no bound. The year and month dropdowns fall
  back to their default range.

Other inputs that read a value while rendering, checked:
- DataView's scalar threw for an invalid Date (toISOString); it now shows
  it as it is ("Invalid Date").
- TimeAgo already handled an unreadable time.
- There is no other date, time or date-range input, and the number inputs
  parse nothing while rendering.

Test: datePickerUnreadableValues, 7 cases, all failing before:
- a date-time with no zone, and a date alone, shown as local time;
- text and an invalid Date: empty and invalid;
- unreadable bounds, and an invalid Date as a bound;
- DataView with an invalid Date.
…ot read (qorus#646)

Form/DatePicker > Values Without A Zone Or Unreadable renders three pickers:
- "2026-06-02T08:00:00" shows 08:00, local;
- "2026-10-17" shows that day at midnight;
- text that is no date is an empty field, drawn as invalid.

Before a6def18 the story threw from its render: "Invalid ISO 8601 date time
string: 2026-06-02T08:00:00".
…, and a pinned column's header hides what scrolls under it (qorus#646)

Scrolled, a table's pinned columns were drawn over its sticky header: the
label was cut off and the menu button covered, and only the middle showed
the header. The panel's sticky header and a pinned cell were both at
z-index 2 in one stacking context, and the cells come later in the
document, so they won. A panel with a sticky header now draws its body in
a stacking context of its own (isolation: isolate), so nothing in the body
is drawn over the header, whatever its z-index. It is not a containing
block: nothing in the body is placed differently, and popovers and menus
are drawn outside the panel.

A pinned column's header cell painted nothing, so the column headers that
scroll under it showed through ("ORDER DA" under "ACTIONS"). It now paints
what the header row is drawn on (the first color of a gradient), opaque as
a pinned body cell is, and tells what it holds that it is drawn on that.

Stories: Layout/Panel "Sticky Header Above Its Body" and Collections/Table
"Sticky Header Above Pinned Columns" scroll each layer under the header and
read every point across it, and require pinned column headers to be opaque
and on top. Unit: __tests__/stickyLayers.test.tsx.
…the end of the label's line (qorus#646)

As on ReqoreEntityRow (da508bf), at David's direction: in the opt-in 'menu'
mode, two or more actions fold into one menu button at the end of the
label's line when the row is narrow. A severity row of that mode with one
action had no menu to fold into, so it stacked the action under the label:
a line of its own for one button, where a row with more had none.

Now that one action stays a button at the end of the label's line, where
the menu would be, and the row says so: data-actions='end'. It is not
stacked, so an icon-only action stays its icon with its tooltip, as at full
width. Only 'menu' rows change: the default ('stack') and 'row' are as they
were.

Stories, both failing before (data-actions was 'stack', the action under
the label):
- Severity Row > Narrow Container Lone Action In Menu Mode: a 400px
  container, beside the same row with the default, still stacked;
- Severity Row > Narrow Container Lone Action In Menu Mode On A Phone: a
  finger's size, and a tap runs the action without opening the row.
Unit (SeverityRow.test.tsx): 'end' for 'menu' with one action, labeled or
icon-only (fails before); one action unchanged by default, with 'stack',
with 'row', and wide with 'menu'.

The prop's docs, RowActionsMenu's TReqoreNarrowRowActions and COMPONENTS.md
say it.
… label (qorus#646)

David, on the EntityRow phone story ("the button takes up too much space on
a narrow screen; on a narrow screen it should read 'Test'"), as a general
rule: a row-end action on a narrow screen uses a short label.

Every row action type takes an optional `shortLabel`
(IReqoreRowActionShortLabel: IReqoreEntityRowAction, IReqoreSeverityRowAction,
TReqoreTableCellAction). Where reqore draws an action compactly, on a line
it shares with the row's label, the action says it:
- the one action of a narrow narrowActions='menu' ReqoreEntityRow or
  ReqoreSeverityRow, at the end of the label's line (data-actions='end');
- a narrow table's folded column, for an action with no icon (one with an
  icon is its icon alone there, as before: the column is the menu button's
  width).
Its label is then its tooltip (unless it has one) and its accessible name
(unless it has one), so a short label is written as words of its label.

With no short label (getCompactRowAction), an action with an icon and a
tooltip is its icon alone there, named by its label: its author gave it the
words it reads by without its label. Any other says its label, as before.
So no row drawn today changes unless it gives an action a short label or a
tooltip. Wide, and on a line of its own (stacked, or in a row under the
label), every action says its label. Menus name each action by its label.

Stories:
- Entity Row > Narrow Container Lone Action In Menu Mode On A Phone: the
  action reads "Test", and is still called "Test the connection" (fails
  before: it read "Test the connection");
- Entity Row > Narrow Container Lone Action Short Label and Severity Row >
  Narrow Container Lone Action Short Label: three rows in a 720px box and a
  400px one; wide each says its label, narrow its short label, its icon
  alone (an icon and a tooltip), or its label (neither). Both fail before.
Unit: __tests__/rowActionShortLabel.test.tsx (getCompactRowAction, both
rows, the table's folded column; 10 of 16 fail before, the 6 that pass are
the cases that do not change).

COMPONENTS.md documents shortLabel on EntityRow, SeverityRow and Table; the
type is exported from the index.
@davidnich

Copy link
Copy Markdown
Member Author

Pushed 5a02458d: rounds 4 to 7 from the Qorus IDE's round-5 review, approved by David (qorus#646). Prerelease: @qoretechnologies/reqore@0.78.7-pr.717.g5a02458. No version bump.

  • f303c5a: a field's clear button is a named button. The × is role="button" with the accessible name "Clear". Input and Textarea take clearLabel for a host's words.
  • da508bf and 5a02458: a lone action in narrowActions='menu' stays at the end of the label's line. This applies to EntityRow and SeverityRow (data-actions='end'). A row of that mode with one action had nothing to fold, so it put the action on a line of its own under the label. The default and the other modes are unchanged.
  • a6def18 and c08c7ee: a date picker never throws from its render.
    • A value with no zone is read as wall-clock time in the user's zone.
    • A date alone is read as midnight local.
    • A value it cannot read is no date: the field is empty and drawn in danger.
    • DataView shows an invalid Date as "Invalid Date".
  • 268e2fb: a table's sticky header stays above its pinned columns, and a pinned column's header hides what scrolls under it.
    • A panel with stickyHeader draws its body in a stacking context of its own (isolation: isolate).
    • A pinned header cell paints what the header row is drawn on, so it is opaque.
  • Tests:
    • Every new story fails before its fix.
    • New or extended tests, all failing before their fix: inputClearButtonName, datePickerUnreadableValues, stickyLayers and SeverityRow.
  • Gates at 5a02458: precheck 1948/0. Every story file, run one at a time: 1208 passed, 0 failed. CI green.
  • qlip build Feature/allow-hide-show-tabs #392:

…ose does, as prettier writes it (qorus#646)

90f64b0 escaped the double quotes around "Test" and "Test the connection" in
the description, which prettier writes as a single-quoted string: two more
deviations in a file whose count must not go up. Typographic quotes need no
escape. EntityRow.stories: 36 deviations, as before 90f64b0.
@davidnich

Copy link
Copy Markdown
Member Author

Pushed 1cf24cb1: round 8, approved by David (qorus#646). Prerelease: @qoretechnologies/reqore@0.78.7-pr.717.g1cf24cb. No version bump.

  • 90f64b0: a row-end action on a narrow screen says its short label. This is David's general rule.
    • Every row action type takes an optional shortLabel (IReqoreRowActionShortLabel, exported).
    • Where reqore draws an action on the same narrow line as the row's label, it says its shortLabel: "Test" for "Test the connection". That is the one action of a narrow narrowActions='menu' EntityRow or SeverityRow, and a narrow table's folded column for an action with no icon.
    • The full label stays its tooltip and its accessible name.
    • With no short label, an action with an icon and a tooltip is its icon alone there, and any other says its label, as before. So nothing drawn today changes unless an action gets one or the other.
    • Wide rows, stacked or in-a-row actions, and menus keep the full label.
  • 1cf24cb: a story description's quotes, so the file's prettier count does not go up.
  • Tests:
    • rowActionShortLabel (10 of 16 fail before; the 6 that pass are the cases that do not change).
    • Stories, failing before: EntityRow · Narrow Container Lone Action In Menu Mode On A Phone (now "Test"), and EntityRow / SeverityRow · Narrow Container Lone Action Short Label.
  • Gates at 90f64b0: precheck 1964/0. Every story file, run one at a time: 1210 passed, 0 failed. CI green.
  • qlip build Add datepicker year/month dropdowns #393:
    • One frame changed from Feature/allow-hide-show-tabs #392: the EntityRow phone story, where the action now reads "Test" and the label and its description take the room back.
    • 5 others have a different hash, but their captures compare identical pixel for pixel.
    • The 2 new frames are the short-label stories.
    • All 201 are flagged with their causes. Nothing is accepted.

…roup (qorus#646)

A wrapped ReqoreControlGroup gave its children no cap. A child as wide as
content that does not wrap stayed that wide, past the group and the card it
was in: the Qorus IDE's data provider path (a `fluid wrap fill` group whose
steps are `fluid={false}` groups holding a select that says its value and
description) ran 487-712px wide in a 346px card on a phone, and the
favorites dialog's Apply was cut off.

Now each child of a wrapped group is capped at the row (max-width: 100%)
and may shrink below its content to fit it (min-width: 0). A capped button
says its description on as many lines as it takes, as a fluid button in a
wrapped group already did (fluid buttons always had max-width: 100%).

The rule is reqore's global style (UIProvider,
WRAPPED_CONTROL_GROUP_CHILD_SELECTOR, `:where(.reqore-control-group-wrapped)
> *`), and weighs nothing: a child that sets its own width limits (a
button's maxWidth) keeps them. A group's own styles could not weigh less
than the child's: styled-components scopes them to the group's class. The
group marks itself `reqore-control-group-wrapped` when it wraps.

Stories (Form/Control Group):
- Wrapped Group Keeps A Long Button Inside: a picked path in a 346px card;
  fails before (children 488, 351 and 382px wide in 346px);
- Wrapped Group Keeps A Long Button Inside On A Phone: the same in a card
  on a phone, with the card's Apply; fails before (488px in 332px).
Unit: __tests__/controlGroupWrapCap.test.tsx (which children are capped,
the rule's weight; without the class, the two that cap fail).
@davidnich

Copy link
Copy Markdown
Member Author

Pushed e12ed1d2: round 9, approved by David (qorus#646). Prerelease: @qoretechnologies/reqore@0.78.7-pr.717.ge12ed1d. No version bump.

  • e12ed1d: a wrapped group's child is never wider than the group. This is the root cause of the Qorus IDE's data provider path running 487-712px wide in a 346px card on a phone.
    • A wrapped ReqoreControlGroup gave its children no cap, so a child as wide as content that does not wrap (a select saying its value and description, or a group holding one) stayed that wide.
    • Now each child of a wrapped group is capped at the row (max-width: 100%) and may shrink below its content to fit it (min-width: 0). A capped button's description takes as many lines as it needs, as a fluid button's in a wrapped group always did.
    • The rule is in the provider's global style, :where(.reqore-control-group-wrapped) > *, and weighs nothing, so a child's own width limits (a button's maxWidth) win. Written in the group's own styles, it would weigh as much as the child's and win or lose by stylesheet order. A wrapped group has the class reqore-control-group-wrapped; there is no new prop.
  • Tests:
    • controlGroupWrapCap (without the class, the 2 that cap fail; the 2 that pass are the negative cases).
    • Stories, failing before: Control Group · Wrapped Group Keeps A Long Button Inside (488, 351 and 382px in a 346px card) and its phone story (488px in a 332px card).
    • The 19 story files that draw a wrapped group, captured before and after: 414 captures, and only the two new stories changed.
  • Gates at e12ed1d: precheck 1968/0. Every story file, run one at a time: 1212 passed, 0 failed. CI: Tests and PR prerelease green.
  • qlip build chore: Update Tabs component to include useReactTransition prop #394:
    • 197 frames are identical to Add datepicker year/month dropdowns #393, and 4 more have a different hash but compare identical pixel for pixel.
    • The 2 new frames are the wrapped-group stories.
    • Notifications Item · Swipe In Progress differs from its baseline in the half-swiped card's text only (anti-aliasing, at most 52 of 255). The same story captured locally before and after e12ed1d is identical, so it is the capture, not the change.
    • All 204 are flagged with their causes. Nothing is accepted.

David Nichols added 2 commits October 18, 2026 17:21
…s its own maxWidth (qorus#646)

A button had max-width: 100% only when it was fluid and not fixed
(`maxWidth || (fluid && !fixed ? '100%' : undefined)`). A fixed button had
no cap at all, and fixed reaches a select's button directly or through a
group (a group hands its children `fixed`). So in the Qorus IDE's data
provider path, a step's select saying its value and description stayed as
wide as both on one line, 487-712px in a 346px card on a phone, even inside
the group round 9 capped at the row.

Now every button is capped at its container (max-width: 100%) unless it
sets its own maxWidth, which stays its limit; a square button keeps its
side. fixed keeps meaning that the button does not stretch. A capped
button's label is clipped as any button's is, and its description takes as
many lines as it needs, as in round 9.

What else this changes (every story file captured before and after, 1188
captures): a panel's single action that the title bar's responsive group
folded into a menu of one, because its labelled button overflowed the
group, now stays its icon, as the panel's own stories describe (Panel ·
Compact Title When Narrow, Fit Label Comparison); and a drawer sheet's
header icons sit half a pixel lower (82.64 to 83.14px; the buttons' boxes
are unchanged), which shows in Drawer · Responsive Sheet Mobile.

Stories, failing before: Button · Fixed Button Stays Inside Its Container
(476px in a 346px card) and its phone story.
Unit: __tests__/buttonNeverWiderThanItsContainer.test.tsx (fixed, fixed and
fluid, and neither fail before; fluid, maxWidth and square are the cases
that do not change).
…rus#646)

ReqoreColumns, and ReqoreCollection with it, laid its columns out at
`minmax(<minColumnWidth>, <maxColumnWidth>)`. A collection's minimum is its
zoom width, 400px at 100%, so in a container narrower than that the column
stayed 400px and its cards ran past it: in the Qorus IDE's favorites dialog
on a phone, a card was laid out at 63-433px in a dialog whose content ends at
342px, and its Apply was out of reach.

Now the minimum is `min(<minColumnWidth>, 100%)` (getColumnMinimum): the
width is the narrowest a column is where the container has room for it, not
a width it keeps where there is less. A keyword minimum (auto, min-content,
max-content, fit-content(...)) is kept as it is. Callers that already wrote
min(<width>, 100%) themselves are unaffected.

What else this changes: nothing else drawn (every story file captured before
and after, 1188 captures).

Stories, failing before: Collection · Narrow Container Keeps Its Items Inside
(cards 420px in a 342px box, Apply and Remove past it) and its phone story
(the favorites in a dialog on a phone).
Unit: __tests__/columnNeverWiderThanItsContainer.test.tsx (the minimum and
the rule fail before; the keywords are the case that does not change).
@davidnich

Copy link
Copy Markdown
Member Author

Pushed 374d40f6: round 10, approved by David (qorus#646). Prerelease: @qoretechnologies/reqore@0.78.7-pr.717.g374d40f. No version bump.

  • 2789e17: a button is never wider than its container unless it sets its own maxWidth.
    • A button had max-width: 100% only when it was fluid and not fixed, so a fixed button had no cap at all. A select's button is often fixed, set directly or handed on by its group. In the Qorus IDE's data provider path, it stayed 487-712px wide in a 346px card on a phone.
    • Now every button is capped at its container, and a square button keeps its side. fixed keeps meaning "does not stretch". A capped label is clipped as any button's is, and a description takes as many lines as it needs.
  • 374d40f: a column's minimum is never more than its container.
    • ReqoreColumns, and with it ReqoreCollection, used minmax(<minColumnWidth>, …). A collection's minimum is its zoom width (400px at 100%), so in a 342px dialog on a phone the cards ran past the dialog.
    • The minimum is now min(<width>, 100%) (getColumnMinimum). Keyword minimums (auto, min-content, …) are kept.
  • What else changes. Every story file was captured before and after: 1188 captures, 11 differ.
    • 4 are the new stories.
    • Panel · Compact Title When Narrow and Fit Label Comparison: a panel's single action now stays its icon. Before, its labelled button overflowed the title bar's group and was folded into a ☰ menu of one. The stories' own descriptions say the icon stays.
    • Drawer · Responsive Sheet Mobile: the header icons sit half a pixel lower; the buttons' boxes are unchanged.
    • Not from the change: Drawer · Sheet Swipe In Progress (a flaky capture in the before run; a clean recapture is identical), Comments · Basic (an avatar loaded in one run only) and Error Boundary · Show Details (stack trace URLs).
  • Tests:
    • buttonNeverWiderThanItsContainer: 3 of 6 fail with the old rule. The 3 that pass are the cases that do not change: fluid, an own maxWidth, and square.
    • columnNeverWiderThanItsContainer: 2 of 3 fail with the old minimum. The keyword case does not change.
    • Stories that fail before: Button · Fixed Button Stays Inside Its Container (476px in a 346px card) and its phone story; Collection · Narrow Container Keeps Its Items Inside (cards 420px in a 342px box) and its phone story.
  • Gates at 374d40f: precheck 1977/0. Every story file, run one at a time: 1216 passed, 0 failed. CI: Tests and PR prerelease green.
  • qlip build Editable Tree component #395:
    • 198 frames are identical to chore: Update Tabs component to include useReactTransition prop #394, and 5 more have a different hash but compare identical pixel for pixel.
    • The 4 new frames are the new stories.
    • In CI the two Panel stories and Drawer · Responsive Sheet Mobile are identical to their baselines (auto-passed): the single action already stayed its icon in CI's browser, and the changes above showed only in the local captures.
    • All 207 are flagged with their causes. Nothing is accepted.

@davidnich

Copy link
Copy Markdown
Member Author

@Foxhoundn this PR is ready for your review, merge and release as 0.78.7. David approved every round's stories, and it is green at 374d40f6. Qlip build #395 has nothing accepted; every changed frame is flagged with its cause.

reqraft (toolkit-react) #137 depends on it. This PR's narrowActions makes folding a narrow row's actions into a menu opt-in ('row' is the default). That is what answers your 16 ExperimentCard/Overlay rejections on #137. #137 will take reqore 0.78.7 and go back to checking the visible Show analytics, Accept and Remove, so those frames return to the baseline.

David Nichols added 7 commits October 19, 2026 13:01
…e field out of view (qorus#646)

Root-caused by the Qorus IDE agent (David's round-7 review): in a short
window - a 1280x720 Storybook window's ~380px canvas, a phone in landscape -
a click into an editor opened no list at all.

A popover held below its target (keepPlacement) makes room by scrolling the
target's scroller by what the list lacks. A list taller than the window
scrolled the field from y=209 to y=-59: out of view, so Popper marked the
reference hidden, and the popover closed as it opened.

The field is now scrolled up at most to the top of what shows it (its
scroller, or the window where the scroller starts above it). What is still
missing is taken from the list's height (it is clamped to the room under the
field), or the list flips where that room is under a hundred pixels, as
before.

Stories (Form/RichTextEditor), both failing before (the list never opens):
- The List Opens In A Short Window: a field 200px down a scrolling page in a
  980x380 window, a list of twenty templates;
- The List Opens On A Phone In Landscape: the same on a touch screen at
  844x390, opened with a tap.
…pened level is the same list (qorus#646)

Found building the Qorus IDE's field picker, and in released reqore with
plain nested `items`.

The list of an opened level is drawn by the list of the level above it,
which kept its document key listener and its `passKeyHandler`. With the
focus outside the list - a text field or an editor it is opened from,
which keeps the focus - every key was handled by both levels: ↓ moved the
highlight in both, and Enter in the opened level also selected the row the
level above had highlighted ("Inner 1 | Plain" for one Enter). Only the
level the list shows now listens, and hands its controls to
`passKeyHandler`.

An opened level was also handed only some of the list's options, so it
dropped `filterRanking`, `filterPlaceholderBuilder`, `noItemsFoundText`,
`listId` and `passKeyHandler`: a list in German said "Search 2 items..."
and "No items found" one level down, ranked nothing, and its rows had no
ids for `aria-activedescendant`. It is handed them all now.

Stories (Form/Dropdown), both failing before:
- Only The Open Level Hears The Keys: a group opened from a text field
  that keeps the focus; ↓ Enter picks "Inner 1" only;
- An Opened Level Says And Ranks As The List Does: a German list ranked by
  relevance with a `listId`, opened at its group.
…as it (qorus#646)

Root-caused by the Qorus IDE agent from David's IDE round-1 review: a
checkbox with `wrapLabel` in less room than its box and its label on one
line - the IDE's "Skip totals rows" in a 38px row - kept its label on one
line, past the row.

`wrapLabel` only stopped the label's text from being kept on one line. The
label sits in two control groups (on either side of the box), and a group
that is not fluid is `flex: 0 0 auto`: it kept the width of the label's
one line, so there was never less room for the label to wrap in. The
checkbox was also exactly one line high, so a label that did wrap would
have run out of it.

With `wrapLabel` now:
- the groups the label is in shrink (`flex-shrink: 1; min-width: 0`);
- the checkbox is never wider than its container and shrinks in a row
  (`max-width: 100%`, `min-width: 0`, `flex: 0 1 auto` unless fluid or
  fixed);
- it is as high as its label, at least one line (`min-height`).
A checkbox whose label does not wrap is drawn as before.

Stories (Form/Checkbox), each failing before (the label runs to 338px or
632px in a 160px box, or past a 390px screen):
- A Long Label Wraps In A Narrow Box (beside one that does not wrap);
- A Long Label On The Left Wraps In A Narrow Box;
- A Three Line Label Makes The Checkbox As High (a checkbox and a switch);
- Long Labels Wrap On A Phone (touch, 390px; a tap on a wrapped label
  checks its box).
Unit: __tests__/checkboxWrapLabel.test.tsx; its three wrapping tests fail
before, and a checkbox that does not wrap keeps its height and width.
… saying what is missing (qorus#646)

DeepScan on #717 (NULL_POINTER, __tests__/stickyLayers.test.tsx:61): the
header row was asserted with `expect(row).toBeTruthy()` and then read
through `row!`, and each header cell's probe through `!` too. An assertion
that does not narrow the type leaves the next line reading a property of
what may be nothing.

`requiredElement(selector, what, parent)` and
`requiredAttribute(element, name)` (src/stories/utils/required.ts) return
what they find, or throw with what is missing. stickyLayers uses them for
every element and attribute it reads, and its header cells are told from
their row's children by `instanceof HTMLElement`, not a cast.

The same in round 11's own tests and stories, where a `!` or a cast read
an element that might not be there: checkboxWrapLabel.test, the Checkbox
stories' boxes and phone story, the Dropdown stories' search field, and
the RichTextEditor short-window stories' page, list and window.

Nothing drawn changes: the Checkbox, Dropdown and RichTextEditor
TemplateFieldUX story files pass as before (61 stories) with captures
identical to the last ones.
…s the same (qorus#646)

Found with DeepScan's REACT_INEFFICIENT_PURE_COMPONENT_PROP on #717 (a
table header cell's inline click handler for the memo ReqoreButton):
handing the button a stable handler did not stop it being drawn again,
because the stacked control group it is in handed it a new style object
on every render. A stacked group squares the corners that meet and tucks
each border under the one before, in a style it makes anew each time; it
was a prop that always changed, so a memo child - a button, a dropdown -
was drawn again with its group whatever else it was handed.

useCloneThroughFragments (the control group's, the button rail's and the
menu's) now keeps the style each child was last handed, by its place, and
hands it again while it has the same properties with the same values. What
is drawn does not change.

Unit: __tests__/controlGroupKeepsChildStyle.test.tsx - a memo child is not
drawn again when its group is (fails before), and is when its place in the
group changes its style (its corners rounded at the new end).
…s and objects while they are the same (qorus#646)

DeepScan on #717 (REACT_INEFFICIENT_PURE_COMPONENT_PROP,
src/components/Table/headerCell.tsx:430): the cell handed the memo
ReqoreButton a click handler made anew on every render, so the button was
drawn again each time the cell was - when another column was sorted, or
the cell moved - for nothing. Its effect object, and the menus'
(ReqoreDropdown is a memo too) handlers, input props, effect and button
style were made anew the same way.

Now:
- the click (⌘-click sorts, then the column's onClick) and the filter
  handler are useCallbacks of what they use;
- the menus' input props, the menu's effect (the filter's glow, the sorted
  column's gradient) and the button's effect are memos of what they are
  made of;
- whether a menu opens (not on a ⌘-click) and the ⋯'s style are module
  constants. A ⌘ check of an event that is not there no longer throws.
What is drawn does not change.

Unit: __tests__/tableHeaderCellKeepsItsControls.test.tsx - the header
cell's button, and its menu's, are not drawn again when another column is
sorted or the cell moves (both fail before, with the cell's handlers made
inline), and are when the cell's own column is sorted or it is handed
another click handler.
01ad747 read the sort direction in the menu effect's dependencies,
whatever the column: a table that does not sort hands its header cells no
sort data, and every such table threw "Cannot read properties of undefined
(reading 'direction')" (42 table unit tests). The cell now reads what it
is told of the sort only when it is told it: whether it is the column
sorted by, and its direction, both of sort data that may be absent.

Unit: tableHeaderCellKeepsItsControls - a cell with no sort data is drawn,
and drawn again (fails on 01ad747).
@davidnich

Copy link
Copy Markdown
Member Author

Pushed 053b4e41: round 11 and the two DeepScan issues, approved by David (qorus#646). Prerelease: @qoretechnologies/reqore@0.78.7-pr.717.g053b4e4. No version bump.

  • a188444: making room for a list below its field never scrolls the field out of view.
    • In a short window (a 1280×720 Storybook window's ~380px canvas, or a phone in landscape), clicking into an editor opened no list at all.
    • A popover held below its field (keepPlacement) makes room by scrolling the field's scroller by however much the list lacks. A list taller than the window scrolled the field out of view, Popper marked the reference hidden, and the list closed as it opened.
    • Now the field is scrolled at most to the top of what shows it. The list's height absorbs the rest, or the list flips where the room under the field is less than 100px, as before.
  • 9bd654e: only the level the dropdown shows hears the keys, and an opened level gets all of the list's options. This was in released reqore, with plain nested items.
    • The list for an opened level is drawn by the list of the level above it, which kept its document key listener and its passKeyHandler.
    • With the focus outside the list (a text field or editor that keeps it), every key was handled by both levels. Enter in an opened level also selected the row the level above had highlighted ("Inner 1 | Plain" for one Enter).
    • An opened level also dropped filterRanking, filterPlaceholderBuilder, noItemsFoundText, listId and passKeyHandler, so a translated list went back to English one level down.
  • 54547ae: a checkbox label with wrapLabel wraps, and the checkbox grows to fit it.
    • The label sat in two non-fluid control groups (flex: 0 0 auto) that kept the width of its single line, and the checkbox was exactly one line tall.
    • With wrapLabel set:
      • the label groups shrink;
      • the checkbox is never wider than its container and shrinks in a row;
      • it is at least as tall as its label (min-height).
    • Without wrapLabel, nothing changes.
  • DeepScan (2 new issues on 374d40f6):
    • NULL_POINTER, __tests__/stickyLayers.test.tsx:61 → a621281.
      • requiredElement / requiredAttribute (src/stories/utils/required.ts) return what they find or throw naming what is missing.
      • stickyLayers and round 11's own tests and stories now read every element and attribute through them, with no ! or cast on a query result.
    • REACT_INEFFICIENT_PURE_COMPONENT_PROP, src/components/Table/headerCell.tsx:430 → 01ad747, d76200e, 053b4e4.
      • 01ad747: the header cell's click and filter handlers are useCallbacks. Its menus' input props and both effects are memoized, and its ⌘ open check and ⋯ style are module constants. The menu's ReqoreDropdown is a memo too, and had the same inline props.
      • d76200e: a render-count test showed the button still redrawing. The stacked control group it sits in handed it a new style object on every render, and style was the only prop still changing. useCloneThroughFragments (used by ControlGroup, ButtonRail and Menu) now reuses a child's style object while its values are equal.
      • 053b4e4: 01ad747 read sortData.direction for every column, which threw for tables that do not sort; that is now read as optional.
  • What else changes. Every story file was captured (1196 captures). Compared with the previous round:
    • the 8 new stories;
    • Dropdown · Keyboard Navigation With Left Arrow Navigates Back. Back at the top level, its row is no longer highlighted: the top level had been hearing the ↓ pressed in the sublevel.
    • Not from the changes:
      • Paging · Infinite With Auto Load (two scroll positions, each seen with both the old and the new sources);
      • Progress · Live Update;
      • two Drawer sheet header icons (each state seen with and without d76200e);
      • Comments · Basic (an avatar);
      • Error Boundary · Show Details (stack URLs).
  • Tests. Each of these new stories fails before its fix:
    • RichTextEditor: The List Opens In A Short Window, and On A Phone In Landscape.
    • Dropdown: Only The Open Level Hears The Keys, and An Opened Level Says And Ranks As The List Does.
    • Checkbox: A Long Label Wraps In A Narrow Box, its left-label version, A Three Line Label Makes The Checkbox As High, and Long Labels Wrap On A Phone.
  • New unit tests:
    • checkboxWrapLabel: 3 of 5 fail before; the other 2 guard what must not change.
    • controlGroupKeepsChildStyle.
    • tableHeaderCellKeepsItsControls: the cases where the button should not redraw fail if either 01ad747 or d76200e is reverted.
  • Gates at 053b4e41: precheck 1989/0. Every story file, run one at a time: 1224 passed, 0 failed. CI: Tests, PR prerelease, CodeQL and both analyses green. DeepScan: 0 new and 31 fixed issues.
  • qlip build Feature/rich-text-actions-position #402:
    • 200 frames are identical to Editable Tree component #395, and 7 more have a different hash but compare identical pixel for pixel.
    • The 8 new frames are the new stories.
    • Keyboard Navigation With Left Arrow Navigates Back is the one changed frame (cause above).
    • All 216 are flagged with their causes. Nothing is accepted.

@davidnich

Copy link
Copy Markdown
Member Author

@Foxhoundn this PR is ready for your review, merge and release as 0.78.7, now with round 11 and the two DeepScan fixes. David approved every round's stories.

It is green at 053b4e41: Tests, the prerelease, CodeQL, and DeepScan with 0 new issues (31 fixed). On qlip build #402, every changed frame is flagged with its cause, and nothing is accepted.

What round 11 adds:

  • a list that opens in a short window instead of closing as it opens;
  • a dropdown whose opened level is the only one that hears the keys, and gets the list's options;
  • wrapLabel that wraps a checkbox's label;
  • for DeepScan: no unchecked query results in the tests, and the table header cell's button and menus are no longer redrawn on every render. Fixing that took two changes: stable props from the cell, and a stable style from the stacked control group.

reqraft (toolkit-react) #137 is waiting on this release. It will pin 0.78.7 next.

@Foxhoundn

Copy link
Copy Markdown
Member

@Foxhoundn this PR is ready for your review, merge and release as 0.78.7, now with round 11 and the two DeepScan fixes. David approved every round's stories.

It is green at 053b4e41: Tests, the prerelease, CodeQL, and DeepScan with 0 new issues (31 fixed). On qlip build #402, every changed frame is flagged with its cause, and nothing is accepted.

What round 11 adds:

  • a list that opens in a short window instead of closing as it opens;
  • a dropdown whose opened level is the only one that hears the keys, and gets the list's options;
  • wrapLabel that wraps a checkbox's label;
  • for DeepScan: no unchecked query results in the tests, and the table header cell's button and menus are no longer redrawn on every render. Fixing that took two changes: stable props from the cell, and a stable style from the stacked control group.

reqraft (toolkit-react) #137 is waiting on this release. It will pin 0.78.7 next.

This PR would introduce changes in every single product we have, making it impossible to properly review the current branches / PRs. This PR will be merged after every open IDE / toolkit-react branch is merged.

@davidnich

Copy link
Copy Markdown
Member Author

Closed in favour of two PRs, at the front-end team's request to keep the contrast work apart from the fixes reqraft and the Qorus IDE depend on:

Each of #717's 62 commits went to one side, with 3 split in two; its 4 merges were not picked. The classification, one line per commit with its reason and the commit it is now, is in #722's description. Nothing is lost: the contrast branch's final tree is #717's head, plus one story fix made in review and one popover fix that reqraft's tests found on #722's first prerelease, on develop's current dependencies (#718). Every story was approved by David again on both PRs.

@davidnich davidnich closed this Oct 20, 2026
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.

2 participants