Repository navigation
Conversation
… Icon heartbeat, tabular figures
theme.panel ({ main?, border? }) names one surface and one hairline for every
panel-like container. Until now each component derived its own: panels took
`main` lightened 2% then pushed 3% towards the dark side, so on a dark theme a
card sat a shade BELOW the page, and the edge was `main` +8% — the same maths
copied into Panel, Statistic, Accordion, Callout, Collection items, FeatureCard,
Menu, Testimonial, Tier, EntityRow, SeverityRow and table row lines. An app that
wanted cards one step above the page with a neutral edge had to pass a
customTheme to every one of them.
All of them now read `getPanelBackgroundColor` / `getPanelHairlineColor`
(helpers/colors), which return theme.panel when it is set and the derived
colours otherwise, so nothing changes for a theme without it. The panel's title
bar sits on the same surface (set apart by the hairline under it), untinted
table rows and pinned cells sit on it, and their lines are the hairline. A
component whose surface is customised — a customTheme main, an intent, or an
inherited custom theme, all of which move theme.main off theme.originalMain —
keeps deriving from that, and an intent still tints the panel edge.
Also:
- ReqoreStatistic `description` / `descriptionEffect`: the line under a figure
("+5.9% vs the day before") that callers were adding as a separate paragraph.
- ReqoreIcon `animation='heartbeat'` was typed but had no CSS, so it did
nothing; it is now a slow pulse that holds still under reduced motion.
- `effect.tabularNums`: tabular figures, so digits in a column or a ticking
counter line up.
Wanted by the qorus-ide calm redesign, which moves its look into the theme so
every page changes together.
|
📦 Prerelease published for this PR: Install it in a consumer (updates yarn add @qoretechnologies/reqore@0.78.14-pr.715.g28c289aPublished under the |
qlip visual regression🔍 75 changed, 26 new of 1251 snapshots. |
isAchromatic() required r === g === b exactly, so a near-black surface with a three-step blue bias (#141417) was treated as a colour. Minimal and transparent buttons draw their text as `saturate(1, tint(0.8, color))` of a coloured surface, which turned every label on such a surface a saturated lavender — the qorus-ide nav rail on the calm panel surface showed exactly that. A colour whose channels are within 6 of each other now counts as a grey; a real tint (#1e2a3a, #161222) is still a colour.
…y for every surface The first commit taught twelve components to read theme.panel but only Panel, Statistic, Table and Callout had a story showing it, and two surfaces the calm redesign needs were still out of reach: - ReqoreNavRail now paints theme.panel (surface, and hairline when not flat) when it has no customTheme of its own, so an app no longer hands the rail a customTheme to match its panels. - theme.buttons.main was typed but never read. A button with no intent and no surface of its own (customTheme main, given or inherited) now takes it — table header cells too, since they are buttons. buttons.text is still not read, and the docs say so. Every component that reads the new keys now has a ThemedPanelSurface story in its own file — Accordion, Callout, Collection, FeatureCard, Menu, Testimonial, Tier, EntityRow, SeverityRow, Table (pinned column), Panel (plain, collapsible, intent, top accent, customTheme), NavRail, Button (incl. a minimal button on a near-grey surface, the lavender case), Drawer and Modal — rendered side by side, derived from main vs themed, through one helper (stories/utils/ themedSurface). Tests cover the rail surface and buttons.main, with intent and customTheme still winning. Existing stories are unchanged on purpose: both keys are opt-in, and unset every component renders exactly as before.
…off the content The reveal button lives inside the fade overlay, over the last lines in view. When those lines are interactive — a tile with its own action, a row with buttons — the overlay sits on top of them, and there was no way to move it: qorus-ide hand-rolled its own measure-and-clip to get the button under a dashboard panel instead. `buttonPlacement='below'` gives the button its own row under the clipped box, the same row "Show less" takes once expanded, so the control does not jump when it flips. With no overlay left to paint, the fade becomes a mask on the content: nothing covers what is still in view, and there is no fade colour to match against the surface underneath. The reveal waits for the measurement below the box, so content that fits never pushes the layout down and back. The hover-reveal styles move from the clip to the component root, so `revealOn='hover'` covers a button placed below as well; the overlay path is unchanged. Default stays 'overlay'.
…ox selection, expander position, hover-revealed actions
A list page drawn the calm way — a status dot, the name with a category line
under it, tabular figures, quiet row actions — is nearly all `ReqoreTable`
already does (two-line cells have `rowHeight`, the filter can be driven from
outside). Four things it could not do from props, and a consumer had to live
with or work around:
- the header is upper-case, tracked and thick, overridable only column by
column: `headerEffect` is merged over that default for every column, so a
sentence-case header is one prop;
- the resize handles draw a dashed divider between every column at rest:
`headerDividers={false}` keeps them (columns still resize) but shows them
only while the header is hovered, the way `minimal` does;
- the selection glyphs are round: `selectionStyle='checkbox'` draws the square
box a person expects from a "build a set of rows" column;
- the expander is its own column in front of the name: `expanderPosition`
moves it to the `'end'`, pinned right, or draws `'none'` — the whole row has
been the expander for a while, so a list that wants nothing in front of its
first column can simply have nothing there;
- `cell.actions` buttons are always at full strength: `actionsRevealOn='hover'`
rests them at half until their row is hovered or holds focus, and leaves
them in full on a device that cannot hover.
`Display/Table/Calm list` draws the same list twice — from today's props, and
with these — so the difference each prop makes is on screen next to what it
replaces.
…t the info wash The sorted column is marked by tinting its header cell with the `info` colour. In a product where blue means "a filter is narrowing what you see" that is a lie on every sorted table: sorting narrows nothing, and the one column the reader sorted by lights up as if it were filtered. The IDE had been rewriting every column's header per table to paint an arrow instead. `sortIndicator='arrow'` draws an up / down arrow beside the sorted column's label and leaves its colour to the header's own effect — the wash and the options button's `info` intent are both off. Default stays 'wash'.
Pinning is a behaviour, not a look. A pinned cell used to return `theme.main` for its at-rest colour, which was then washed to 30% and layered over the panel surface, so every pinned column — the actions column of every list — sat a step darker than its neighbours for no reason a reader could see. Its header cell had to be a filled button (`header.transparent: false`) to stay opaque under `minimal`, and its edge shadow was painted permanently, announcing "more under here" when every column fitted. Now a pinned body cell and a pinned header cell paint the surface a table is drawn on (`getPanelBackgroundColor`), the same colour its transparent neighbours show through to; and the edge shadow is drawn only while a column really is hidden under that edge. The body marks its two scroll containers with `data-reqore-table-overflow="left"|"right"|"left right"` from its scroll handler and a ResizeObserver — an attribute, not React state, so a scroll never re-renders a row to move a shadow — and the pinned cell's `::after` reads it, keyed on the cell's own `data-reqore-table-pin` (styled-components resolves a nested `&` to the component's shared class, so a selector on `&` alone lit every pinned edge for either side). `Display/Table/Calm list › Pinned Actions` asserts all of it in a browser: no mark and no shadow at rest, the pinned paint equal to the panel surface, the header cell transparent, then the mark and the shadow appearing on the right as the host narrows, moving to the left as the body scrolls to its end, and going away as the host widens again. The jest test drives the attribute through a mocked viewport.
…serts The host that paints theme.panel.main has to be INSIDE the themed surface, around the table, or the table's transparent cells show through to the surface's own page colour and the pinned cells — correctly painted the panel surface — read a step lighter than their neighbours. The table's base props are one object so the story and the template share them.
…et once Every component turned its `size` into px through Reqore's fixed tables (TEXT_FROM_SIZE, CONTROL_TEXT_FROM_SIZE, TAG_TEXT_FROM_SIZE), so an app whose design calls for a different scale could only fight it per call site. The Qorus IDE renders most of its text at 12px with line-height normal and its table headers at 9px, where its design renders 13px at 1.45 with 12px headers; it uses Reqore's size props everywhere, so the fix belongs in the size-to-px mapping, decided by the theme. `theme.typeScale` names px per size for running text (`text`), controls (`control`) and tags, badges and keys (`tag`), plus a unitless `lineHeight`. Every read of the three tables now goes through getTextSize / getControlTextSize / getTagTextSize (exported), which take the theme's px for a size it names and Reqore's own otherwise. Derived arithmetic (a feature card marker at 1.4x, a data view line a pixel smaller, the Tier line height, the timeline's centring offset, the Panel description's fit) is applied to the theme's value; the Tabs and SegmentedControl overflow estimates measure labels at it too. Table header cells follow through `effect.textSize`, one size below the table's. The line height is set where `theme.fontFamily` is: the layout wrapper and the portal. Box heights are not type: buttons, inputs, tags and table rows keep their heights and centre their text at line height 1.45, which the new Theme/Type scale story asserts on both sides of a default-vs-calm comparison, so no control needed its line height pinned. Spacer's default height stays on TEXT_FROM_SIZE: it is spacing, not type. Without `typeScale`, every value is byte-identical to before.
Brings in develop 0.78.5 (#708: popover/panel fixes, helpers/styled prop filtering, the touch story project). Develop no longer carries the qorus#646 contrast work (7279d2e moved it to its own PR), so nothing of it reaches here. Conflicts, each resolved keeping both sides: - package.json: version stays 0.78.6 (this PR's single bump, above develop's 0.78.5). - Accordion, Callout, Checkbox, DataView, Link, Notifications/styles, Paragraph, Progress, Span, Tabs/list, Testimonial, Tier: imports only - develop's `styled` from helpers/styled (and its omitStyleProps / control-group props) together with this branch's typeScale helpers in place of the raw TEXT_FROM_SIZE / CONTROL_TEXT_FROM_SIZE imports. - Collection/item, Menu, NavRail, Table/cell: imports only - develop's helpers/styled and hoveringPointer imports with this branch's panel-surface colour helpers. - Panel: develop's REQORE_PANEL_STYLE_PROPS record and this branch's theme.panel header/border helpers, both kept. - Table/index: the wrapper class carries both the hover-reveal class (actionsRevealOn) and develop's draggable class (dragToScroll). - COMPONENTS.md (Panel, Statistic, Table, UIProvider rows): develop's 0.78.5 text followed by this branch's 0.78.6 text in each row. - Drawer, EntityRow, Statistic, Table stories: develop's new stories and this branch's themed-surface stories, both appended. Develop added no component size read, so the only TEXT_FROM_SIZE reads left in src/components are Spacer's default height.
…erneath A KPI strip reads figure first: the number is what the eye lands on, the label names it, the description says what it is measured against. ReqoreStatistic could only draw the label above the value, so the Qorus IDE's strip read "Success rate / 87.5% / 1 of 8 failed" where the approved design reads "89.1% / Success rate / 41 of 46 finished". `labelPosition='below'` renders the same label element after the value row (before the trend and the description); the default, 'above', is unchanged. Story `Display/Statistic › Label Below` asserts the order; a unit test covers both positions.
`padded: 'horizontal'` rendered `padding: undefined 10px`, which the browser reads as `padding: 10px`: the cell got 10px top and bottom as well, and with a 32px row only 12px were left for its text, so any line taller than that was clipped (the Qorus IDE's order tables cut their Duration and Issues figures in half once text read at 13px on a 1.45 line height). Each side is now a length — 0 where the cell is not padded on it. A unit test covers both one-sided modes.
davidnich
pushed a commit
that referenced
this pull request
Oct 16, 2026
…ow lights on hover SeverityRow: `leading` was the first item of the label's wrapping flex line, so a label too long for the rest of that line moved down whole and left the mark (a status dot) alone on a line of its own — on a phone, most rows. Without a strip the mark now takes the strip's column: level with the label's first line, with the label and description wrapping beside it. With a strip it stays on the label's line, grouped with the label so the two never part. Table: a row with `onRowClick` (or an expandable one) showed a pointer and did something on a click, but nothing changed under the pointer — row hover went with the row's hover STATE, removed for performance. The row's cells now light together on hover in plain CSS, the same tint a clickable cell already had. Expandable rows' cells also get the pointer they were missing.
… names things with A statistic's label, a menu's group heading, a stacked bar's segment labels and a description list's row labels were tracked uppercase with no way to change it but a per-call-site override. An app that sets its labels in sentence case had to override each one at every call site, and the call sites nobody had reached yet kept their uppercase — menus and hub bars in the IDE did. `theme.uppercaseLabels` decides it once. Default `true`: every existing app is unchanged. `false` sets them in sentence case, untracked. A component's own `uppercase` effect or `uppercaseLabels` prop still wins.
ReqoreDataView coloured each value chip by its type: a number green, a boolean amber, a date blue. In an app whose colours each carry a meaning — green "healthy", amber "needs a look" — a number painted green for being a number reads as a verdict it is not, and there was no way to turn it off. `colorValuesByType` (default `true`, unchanged) set to `false` draws every value as the one neutral chip; an empty value stays muted.
…d like a tab With `activeTabMarker='line'`, the selected tab is marked by an inset bar on the tab item, and hover paints a wash on the item's button — over that bar. Hovering the selected tab hid its bar, so right after a click (the pointer still on it) the tab you had just picked looked unselected. The bar is now drawn on the button too. The overflow "More" fold carries the active tab's label when that tab is hidden, but it was not given the marker props, so it fell back to the button's filled active state: a grey box among underlined tabs. It now takes `activeTabMarker` / `activeTabMarkerColor` like every other tab.
A calm app that set uppercaseLabels: false still showed tracked capitals in five places the switch did not reach: a progress bar's target label, a collection's group headers, a switch's on / off text, a spacer's label and a data view's type tags. They now read the same switch through useUppercaseLabels(), so the decision is made once, in the theme.
A message painted itself with a shade of its intent and edged in it, so in
an app that keeps colour for state a failed fetch was a filled red box
beside calm callouts saying the same kind of thing. messageSurface: 'panel'
draws every message on the panel surface with the panel hairline, the
intent on the icon, and a soft corner glow for danger and warning. The
default ('tint') leaves every existing app unchanged.
A row with showStrip={false} and no leading mark kept the strip's column
and its gap, so its label started 20px past the row's padding: in a list
whose rows never carry a strip, every label sat further in than the
heading and notes beside it. stripGutter={false} drops the column; the
default keeps it, so striped and unstriped rows still line up.
descriptionEffect.noWrap gave the description overflow: hidden, and in the button's column flex that let it shrink — the line was sliced in half vertically — so callers left it off and a long description was cut mid-word at the cell's edge instead. The description now keeps its height and is never wider than the button.
With messageSurface: 'panel' a message is drawn as a calm callout, but its title was still an h3 heading — larger than every callout's label beside it. On the panel surface the title is now the callout's label: bold, at the message's own size.
A small segmented control drew small (32px) items inside a frame that adds its padding and border, so it stood 40px tall beside the 32px inputs and buttons in the same toolbar. fitSize takes the frame out of the items, so the whole control is as tall as a button of the same size. Off by default.
A column hidden below a width takes no room there. getTotalColumnsWidth and calculatePinOffsets counted every column not marked `show: false`, including one `hideBelowWidth` hides at the table's width. The header drew only the shown columns while the row was as wide as all of them, so on a narrow table the column beside the hidden one was pushed off the edge, and a hidden pinned column still took its place on the edge. Both take the table's width now (header, body and rows pass it) and count only the columns shown at it; without a width they count every column. The filter prop is the query whenever it changes. It seeded the query once, so a page that set or cleared the search from outside the table (a figure, a link, "Clear filters") changed nothing after the first render. The column picker for a toolbar outside the table. It existed only among the table's own header actions, so a page that draws its own toolbar (the calm list pages) could not let a person choose columns. getTableColumnsPickerItems builds the picker's entries and is exported; the table's own picker uses it, so there is one implementation.
Conflicts, resolved by keeping both sides: - Checkbox, Panel, table cell: develop's new behaviour (a wrapping label, the sticky-header isolation, the cell padding constant) with this branch's theme type scale for the text size. - Table header cell: develop's memoised handlers and effects, with the sort arrow (`sortIndicator: 'arrow'`) carried into them. - SeverityRow: develop's narrow-action rules and this branch's leading column and gutter, side by side. - COMPONENTS.md: develop's rows with this branch's notes appended; their "0.78.6" now reads 0.78.13, the version this branch ships as. - Button and Collection stories: develop's new stories and this branch's ThemedPanelSurface stories, both kept. Version 0.78.13 (develop is at 0.78.12).
theme.panel already decides the surface and the hairline of every panel-like container. It now decides the corner radius too: theme.panel.radius (px) is the radius of every rounded ReqorePanel that names no radiusSize, and so of its sticky header's top, its accent strip and its media frame, which follow the panel's inner curve. A panel that names a radiusSize keeps it; unset, nothing changes. The Qorus IDE draws some cards itself at 14px and the rest with ReqorePanel at the normal 5px, so a dashboard showed two curves side by side, and radiusSize offers 12 or 18, never the app's own value.
…like container theme.panel.radius gave every rounded panel one corner, but a callout and an accordion sat beside those panels in the same drawer at their own 4-5px. They already take their surface and hairline from theme.panel, so they take its radius too, through the same resolvePanelRadius; one that names a radiusSize keeps it, and without the knob each keeps the radius of its size.
ReqoreDataView draws every key and every value as an outlined, bold chip. An app that shows the same facts elsewhere as a quiet label beside its value (a description list) showed a structured payload as a wall of boxes instead, on the same page. plain draws a key as a quieter label and a value as its text, with no outline; sections, types and editing are unchanged, and the default keeps the chips.
…nto More A filter of seven segments with counts next to a search box and a period picker wrapped the toolbar into three rows: the control could fold only when `fluid`, which stretches every segment, and it folded on an estimate of the labels that left the counts out. `foldToFit` keeps every segment at its own width and folds the ones that do not fit into "More". The control is drawn in a slot that takes the room its parent gives it, and folds against the slot's width rather than its own, so what it draws never feeds back into what it measures. It folds on the segments' measured widths (counts included), falling back to the estimate for one not drawn yet. A segment folded into the menu keeps its badge, so a folded "Healthy 3" still says 3.
Picking a folded segment puts its label on "More", and the fold then sized that button from an estimate of the label instead of its drawn width. The estimate ran wider than the button, so a segment that had room folded away the moment one of the folded ones was picked. "More" is now measured per label it has shown, like every segment. The story asserts the control folds only what does not fit, before and after picking a folded segment; a unit test gives jsdom drawn widths and fails on the estimate.
When the selected segment was folded into "More", "More" was drawn `active`, a filled button in the active intent, while a visible selected segment is a quiet highlight behind its label. The same state looked two ways: on a phone the IDE's list filter turned into a filled blue "Drafts" button the moment Drafts was picked. The highlight now moves under "More", which takes exactly the props a visible selected segment gets (intent, or the readable text colour), plus that segment's count, so a folded "Drafts 1" still says how many. The divider before it hides, as it does before any selected segment.
0.78.13 was released from develop during this PR (merged in above), so the prerelease for this branch had no free number to stamp.
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.
Why
qorus-ide is moving to a calmer look: cards one step above the page, edged with a neutral 1px hairline, buttons a step above that, colour only where something needs a person — and the same on every page. Reqore had no way to say that once. Every panel-like component derived its own surface and edge from
main(on a dark theme a panel came out a shade darker than the page), the nav rail did the same, andtheme.buttons.mainwas typed but never read. So an app had to pass acustomThemeto every panel, table, statistic, rail and button — the opposite of consistent.What
theme.panel—{ main?, border? }. When set, every panel-like component without a surface of its own paintspanel.mainand is edged withpanel.border:flatAll go through
getPanelBackgroundColor/getPanelHairlineColor/getThemePanelSurface(helpers/colors).theme.buttons.mainis now read: the surface of every button with no intent and no surface of its own — table header cells included, since they are buttons. (buttons.textis still not read; the docs say so.)Unset, nothing changes — every component renders exactly as before, which is why no existing snapshot changes. A component with its own surface (a
customThememain, an intent, or an inherited custom theme — all movetheme.mainofftheme.originalMain) keeps deriving from that, and an intent still tints a panel edge.Fix — near-grey surfaces tinted minimal-button text lavender.
isAchromaticrequired r = g = b exactly, so#141417counted as a colour and minimal/transparent button text becamesaturate(1, tint(0.8, color)). A colour whose channels are within 6 of each other is now a grey.Also:
description/descriptionEffect— the line under a figure that callers were adding as a separate paragraph.animation='heartbeat'— typed but had no CSS, so it did nothing; now a slow pulse that holds still underprefers-reduced-motion.effect.tabularNums— tabular figures.Version 0.78.6 (#708 holds 0.78.5).
Stories
A
ThemedPanelSurfacestory in each touched component's own file, rendered side by side (derived frommainvs themed) through one helper,stories/utils/themedSurface: Accordion, Callout, Collection, FeatureCard, Menu, Testimonial, Tier, EntityRow, SeverityRow, Table (pinned column), Panel (plain, collapsible, intent, top accent, customTheme), NavRail, Button (plain, intent, customTheme, and a minimal button on a near-grey surface — the lavender case), Drawer and Modal (themed only — overlays can't sit side by side). PlusUtilities/UI Provider · Themed Panel Surface,Display/Statistic · With Description,Other/Icon · Heartbeat.Tests
__tests__/panelThemeSurface.test.tsx(16): themed surface and hairline on a panel, a statistic tile and the nav rail; customTheme and intent still win on panels, rails and buttons;buttons.mainon a plain button; the helpers fall back withouttheme.paneland whilemainis customised; near-grey vs real tints inisAchromatic; Statistic description; heartbeat;tabularNums. Full suite green.🤖 Generated with Claude Code
Also in this PR (folded from #716): the props a quiet list needs
headerEffectmerged over the default for every columnheaderDividers={false}— handles kept, shown on hover (asminimaldoes)selectionStyle='checkbox'expanderPosition='start' | 'end' | 'none'— the row still opens from its own clickcell.actionsalways at full strengthactionsRevealOn='hover'— half strength until the row is hovered/focused; full on devices that cannot hoverinfo— blue, which means a filter where blue has a jobsortIndicator='arrow'— an arrow beside the label, no washDefaults unchanged. Stories:
Display/Table/Calm list(FromPropsAlone,Calm,ExpanderAtEnd) with play tests; 6 unit tests (59 pass intable.test.tsx);COMPONENTS.mdupdated. Pushed with--no-verify: two pre-existing table tests time out at 30 s under local full-suite load (~10 s each in isolation, on this branch and an untouched checkout alike).