From 624c459d05f10ff55e33ab901d8cd2496f729908 Mon Sep 17 00:00:00 2001 From: Nicolas Bennett <3158446+nbenn@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:06:45 +0000 Subject: [PATCH 1/3] Draw the dock header's tools in text tokens, one step below a tool At rest the "..." took border-strong, which the dark scheme keeps faint for lines: 1.4:1 on bg-surface, where the icon all but vanished. The tools now take text-disabled at rest and text-muted on hover. --- vignettes/articles/design-system.Rmd | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/vignettes/articles/design-system.Rmd b/vignettes/articles/design-system.Rmd index ee8fe57..b7b3f60 100644 --- a/vignettes/articles/design-system.Rmd +++ b/vignettes/articles/design-system.Rmd @@ -832,9 +832,12 @@ icon-only button has a tooltip. Tools do not shrink with the panel. The dock header has the "…" menu, and next to it a block's own control where the block has one (the AI assistant's sparkle, at every width, until a global -assistant replaces it). They are quieter than other tools: at rest their icon -is `border-strong` grey, on hover `text-disabled` on the `bg-hover` wash, so -the header reads as the block's name. The preview's eye stays in the header +assistant replaces it). They are one step quieter than other tools: at rest +their icon is `text-disabled`, on hover `text-muted` on the `bg-hover` wash, +so the header reads as the block's name. A border colour would be quieter +still on white, but the dark scheme keeps its borders faint on purpose, and +an icon in `border-strong` all but vanishes there (1.4:1 on `bg-surface`). +The preview's eye stays in the header too, left of the block's own control: while building, the preview is what gets switched most. Controls is the menu's first row, with its familiar icon and a check when on, then a rule, then the block's actions. The header has one size at every From 870ce4756b09572d4d05da340dc17208c02830e7 Mon Sep 17 00:00:00 2001 From: Nicolas Bennett <3158446+nbenn@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:06:45 +0000 Subject: [PATCH 2/3] Specify the status dot on meaning tokens, with no dock-local ones The dot's fills point straight at the meaning tokens, which block_status_style() names next to their light values, so blockr.dock declares no status tokens and the DAG draws from the same spec. --- vignettes/articles/design-system.Rmd | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/vignettes/articles/design-system.Rmd b/vignettes/articles/design-system.Rmd index b7b3f60..29df564 100644 --- a/vignettes/articles/design-system.Rmd +++ b/vignettes/articles/design-system.Rmd @@ -1170,20 +1170,21 @@ Specified under "A pill that opens a menu" in [Controls](#controls). ### Block status An 8px dot on the block's mark with a 2px `bg-surface` ring, one spec for dock -and the DAG (`block_status_style()`). Its fills are local tokens owned by -blockr.dock, each pointing at a meaning token: +and the DAG (`block_status_style()`). Its fills are meaning tokens, with no +status tokens of their own: the spec names each token next to its light +value, which a renderer that cannot read CSS (the DAG's canvas) draws with. -| Token | Points at | State | +| State | Fill | When | |---|---|---| -| `--blockr-dock-status-stale` | `text-muted` | stale | -| `--blockr-dock-status-waiting` | `border-warning`, drawn as a hollow ring (1.5px, `bg-surface` inside) | an input is not ready: nothing linked in, or the block upstream is unset, waiting or failed | -| `--blockr-dock-status-failed` | `border-danger` | failed | -| `--blockr-dock-status-unset` | `border-warning` | a required input is empty; the same amber as the empty field's cue | +| stale | `text-muted` | the inputs changed since the block last ran | +| waiting | `border-warning`, drawn as a hollow ring (1.5px, `bg-surface` inside) | an input is not ready: nothing linked in, or the block upstream is unset, waiting or failed | +| failed | `border-danger` | the block failed | +| unset | `border-warning` | a required input is empty; the same amber as the empty field's cue | Unset and waiting share the amber and differ by shape: the block that needs you has the solid dot, and the blocks below it, which wait for it, have rings. -`-unset` replaces the off-palette `#eab308` in blockr.dock -`block_status_style()`. +The amber replaces the off-palette `#eab308` blockr.dock's +`block_status_style()` gave unset. ### Status line and plain text @@ -1508,9 +1509,9 @@ rules (today declared twice, in blockr.viz `chart.css` and blockr.dm `crossfilter-block.css`), the gear button, the tool button and the action menu (`tool_button()`, `action_menu()`), the button classes (`.blockr-btn`), the chevron (`Blockr.icons.chevron`), and the column-label reader. blockr.dock owns the dock -header, the "…" menu, the block status dot, the category colours -(`blk_color()`), the status dot's `--blockr-dock-status-*` tokens and the -rule that hides the gear in simplified mode. blockr.theme owns data colours. +header, the "…" menu, the block status dot and its spec +(`block_status_style()`, which the DAG draws from too), the category colours +(`blk_color()`) and the rule that hides the gear in simplified mode. blockr.theme owns data colours. The token tests in blockr.ui check that the vocabulary changes only deliberately, that every name follows the grammar or is a legacy alias, that From 84f236f0028d7a1bd3cc8a8d36cc1c3f679a1fbc Mon Sep 17 00:00:00 2001 From: Nicolas Bennett <3158446+nbenn@users.noreply.github.com> Date: Wed, 30 Sep 2026 09:19:56 +0000 Subject: [PATCH 3/3] Bring the header tools and the warning amber to 3:1, and test it The dock header's tools were a step quieter than other tools, in text-disabled: 2.5:1 on white, under the 3:1 an active control's icon needs (WCAG 1.4.11), and a colour the token table keeps for disabled controls. They now look like every other tool. The warning border was amber-500, 2.2:1 on white. The unset and waiting status dots are drawn in it, and so is a required empty field's edge. It is now amber-600, #d97706, a new step of the ramp, at 3.2:1 on white. Amber-700, the warning text, would clear 3:1 too, but it is as dark as the danger red (1.04:1 between them), so an unset dot and a failed one would differ by hue alone. Dark mode is unchanged. A test holds icons and status borders to 3:1 on bg-surface in both schemes, the non-text half of what the Chrome check's axe covers for text. It fails on the old amber. The spec also names block_status_badge(), the function the DAG calls, where it said block_status_style(), the dock's internal helper. --- NEWS.md | 6 ++++++ inst/assets/css/blockr-tokens.css | 3 ++- tests/testthat/_snaps/tokens.md | 8 +++++--- tests/testthat/helper-tokens.R | 24 ++++++++++++++++++++++ tests/testthat/test-contrast.R | 30 ++++++++++++++++++++++++++++ vignettes/articles/design-system.Rmd | 30 ++++++++++++++++------------ 6 files changed, 84 insertions(+), 17 deletions(-) diff --git a/NEWS.md b/NEWS.md index 631086d..570f445 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,11 @@ # blockr.ui (development version) +* The warning border, `--blockr-color-border-warning`, is amber-600 + (`#d97706`) in the light scheme, a new step of the amber ramp, up from + amber-500: a warning border, and a status dot drawn in one, now clears 3:1 + on white (WCAG 1.4.11). A test holds icons and status borders to 3:1 on the + surface in both schemes. + * The design system's menu of actions, `Blockr.menu()`, joins `controls_dep()`: rows with an icon, label and meta text, dividers, group titles, a head (a name with a badge and a line of text), current, disabled diff --git a/inst/assets/css/blockr-tokens.css b/inst/assets/css/blockr-tokens.css index 0c19da9..c8a055a 100644 --- a/inst/assets/css/blockr-tokens.css +++ b/inst/assets/css/blockr-tokens.css @@ -49,6 +49,7 @@ --blockr-amber-50: #fffbeb; --blockr-amber-100: #fef3c7; --blockr-amber-500: #f59e0b; + --blockr-amber-600: #d97706; --blockr-amber-700: #b45309; --blockr-green-50: #f0fdf4; @@ -115,7 +116,7 @@ --blockr-color-border-strong: var(--blockr-grey-300); --blockr-color-border-accent: var(--blockr-accent-600); --blockr-color-border-danger: var(--blockr-red-600); - --blockr-color-border-warning: var(--blockr-amber-500); + --blockr-color-border-warning: var(--blockr-amber-600); --blockr-color-border-success: var(--blockr-green-600); /* ------------------------------------------------------------ tints */ diff --git a/tests/testthat/_snaps/tokens.md b/tests/testthat/_snaps/tokens.md index 81876d3..4b319d9 100644 --- a/tests/testthat/_snaps/tokens.md +++ b/tests/testthat/_snaps/tokens.md @@ -25,6 +25,7 @@ --blockr-amber-50: #fffbeb --blockr-amber-100: #fef3c7 --blockr-amber-500: #f59e0b + --blockr-amber-600: #d97706 --blockr-amber-700: #b45309 --blockr-green-50: #f0fdf4 --blockr-green-100: #dcfce7 @@ -61,13 +62,13 @@ --blockr-color-border-strong: #d1d5db --blockr-color-border-accent: #2563eb --blockr-color-border-danger: #dc2626 - --blockr-color-border-warning: #f59e0b + --blockr-color-border-warning: #d97706 --blockr-color-border-success: #16a34a --blockr-color-bg-accent-subtle: color-mix(in srgb, #2563eb 7%, transparent) --blockr-color-bg-accent-subtle-hover: color-mix(in srgb, #2563eb 13%, transparent) --blockr-color-border-accent-subtle: color-mix(in srgb, #2563eb 35%, transparent) --blockr-color-border-danger-subtle: color-mix(in srgb, #dc2626 35%, transparent) - --blockr-color-border-warning-subtle: color-mix(in srgb, #f59e0b 45%, transparent) + --blockr-color-border-warning-subtle: color-mix(in srgb, #d97706 45%, transparent) --blockr-color-border-success-subtle: color-mix(in srgb, #16a34a 35%, transparent) --blockr-focus-ring: 0 0 0 3px rgba(37, 99, 235, 0.12) --blockr-focus-outline: 2px solid #2563eb @@ -116,7 +117,7 @@ --blockr-color-error: #dc2626 --blockr-color-danger: #dc2626 --blockr-color-success: #16a34a - --blockr-color-warning: #f59e0b + --blockr-color-warning: #d97706 --blockr-color-warning-bg: #fffbeb --blockr-color-warning-text: #b45309 --blockr-font-size-section: 1rem @@ -144,6 +145,7 @@ --blockr-amber-50: #fffbeb --blockr-amber-100: #fef3c7 --blockr-amber-500: #f59e0b + --blockr-amber-600: #d97706 --blockr-amber-700: #b45309 --blockr-green-50: #f0fdf4 --blockr-green-100: #dcfce7 diff --git a/tests/testthat/helper-tokens.R b/tests/testthat/helper-tokens.R index 300ceaa..2786290 100644 --- a/tests/testthat/helper-tokens.R +++ b/tests/testthat/helper-tokens.R @@ -245,6 +245,30 @@ expand_hex <- function(hex) { tolower(hex) } +# WCAG 2's contrast ratio of two opaque hex colours. +contrast_ratio <- function(colour, background) { + + luminance <- blockr.core::dbl_ply(c(colour, background), relative_luminance) + + (max(luminance) + 0.05) / (min(luminance) + 0.05) +} + +relative_luminance <- function(hex) { + + channel <- strtoi( + substring(expand_hex(hex), c(2L, 4L, 6L), c(3L, 5L, 7L)), + 16L + ) / 255 + + linear <- ifelse( + channel <= 0.04045, + channel / 12.92, + ((channel + 0.055) / 1.055)^2.4 + ) + + sum(c(0.2126, 0.7152, 0.0722) * linear) +} + # The selectors a stylesheet styles, one per comma-separated part, with the # rules inside an at-rule read like any other. css_selectors <- function(file) { diff --git a/tests/testthat/test-contrast.R b/tests/testthat/test-contrast.R index 809e952..bcf23ff 100644 --- a/tests/testthat/test-contrast.R +++ b/tests/testthat/test-contrast.R @@ -26,3 +26,33 @@ test_that("text on the accent tint is text-accent-strong", { character() ) }) + +test_that("icons and status borders clear 3:1 on the surface", { + + # What marks a control or its state (an icon, a status border, the status + # dot drawn in one) needs 3:1 against the surface (WCAG 1.4.11), which the + # Chrome check's axe does not cover. The disabled colour is exempt, as it is + # for text. + marks <- paste0( + "--blockr-color-", + c( + "text-muted", "text-default", "border-accent", + "border-danger", "border-warning", "border-success" + ) + ) + + for (scheme in c("light", "dark")) { + + tokens <- blockr_tokens(scheme) + ratios <- blockr.core::dbl_ply( + tokens[marks], + contrast_ratio, + tokens[["--blockr-color-bg-surface"]] + ) + + expect_identical( + sprintf("%s %s: %.2f:1", scheme, marks, ratios)[ratios < 3], + character() + ) + } +}) diff --git a/vignettes/articles/design-system.Rmd b/vignettes/articles/design-system.Rmd index 0e1aa11..027281c 100644 --- a/vignettes/articles/design-system.Rmd +++ b/vignettes/articles/design-system.Rmd @@ -71,7 +71,7 @@ because a colour chosen for text is not automatically right as a fill. | grey | 50 100 200 300 400 500 600 700 800 900 | `#f9fafb` `#f3f4f6` `#e5e7eb` `#d1d5db` `#9ca3af` `#6b7280` `#4b5563` `#374151` `#1f2937` `#111827` | | blue | 50 100 500 600 700 | `#eff6ff` `#dbeafe` `#3b82f6` `#2563eb` `#1d4ed8` | | red | 50 100 600 700 | `#fef2f2` `#fee2e2` `#dc2626` `#b91c1c` | -| amber | 50 100 500 700 | `#fffbeb` `#fef3c7` `#f59e0b` `#b45309` | +| amber | 50 100 500 600 700 | `#fffbeb` `#fef3c7` `#f59e0b` `#d97706` `#b45309` | | green | 50 100 600 700 | `#f0fdf4` `#dcfce7` `#16a34a` `#15803d` | | accent | 50 100 500 600 700 | points at blue | @@ -131,7 +131,7 @@ lands on grey-100. | `--blockr-color-border-default` | grey-200 | every border, input borders included | | `--blockr-color-border-strong` | grey-300 | hovered borders, stronger lines, a border drawn with a text colour | | `--blockr-color-border-accent` | accent-600 | focus, full-strength accent edge | -| `--blockr-color-border-danger` / `-warning` / `-success` | red-600 / amber-500 / green-600 | status edges | +| `--blockr-color-border-danger` / `-warning` / `-success` | red-600 / amber-600 / green-600 | status edges | | `--blockr-color-border-accent-subtle` | `color-mix(in srgb, var(--blockr-color-border-accent) 35%, transparent)` | the accent tint's edge | | `--blockr-color-border-danger-subtle` / `-success-subtle` | the status border at 35% | the edge of a status badge, a message, the destructive button | | `--blockr-color-border-warning-subtle` | the warning border at 45% | the same; amber is paler, so its edge is stronger | @@ -151,7 +151,11 @@ without the edge). Hovering a selected thing keeps its look. Every status role has a text, a background and a border token: danger, warning, success. `error` is `danger`; there is one red. Warning's border is -amber-500, `#f59e0b`. Blue is never a status colour, because blue means "on". +amber-600, `#d97706`: a status border, and a status dot drawn in one, marks a +state, so it clears 3:1 on white (WCAG 1.4.11), where amber-500 is 2.2:1. +Amber-700, the warning text, would clear it too, but it is as dark as the red, +and an amber dot and a red one would then differ by hue alone. Blue is never a +status colour, because blue means "on". Negative numbers in a table are data, and their colour belongs to blockr.theme. @@ -631,7 +635,7 @@ selected row, the active group's front tab. padding, 14px `text-default`. Placeholder `text-muted`. - Focus: `border-accent` and `--blockr-focus-ring`, on any focus. - Disabled: `text-disabled`. -- Required and empty: `border-warning` (amber-500, `#f59e0b`), `bg-warning` +- Required and empty: `border-warning` (amber-600, `#d97706`), `bg-warning` fill, and a `*` after the label. The cue clears when the field has a value. - Commit on Enter or blur. The ↵ button (24px square, the accent tint) is armed while the field is dirty. Escape reverts. See [Keyboard @@ -837,11 +841,10 @@ icon-only button has a tooltip. Tools do not shrink with the panel. The dock header has the "…" menu, and next to it a block's own control where the block has one (the AI assistant's sparkle, at every width, until a global -assistant replaces it). They are one step quieter than other tools: at rest -their icon is `text-disabled`, on hover `text-muted` on the `bg-hover` wash, -so the header reads as the block's name. A border colour would be quieter -still on white, but the dark scheme keeps its borders faint on purpose, and -an icon in `border-strong` all but vanishes there (1.4:1 on `bg-surface`). +assistant replaces it). They look like the tools above: the title's size and +weight make the header read as the block's name, and a quieter icon would +fall under the 3:1 an active control needs (WCAG 1.4.11), as `text-disabled` +(2.5:1) and `border-strong` (1.5:1) do on white. The preview's eye stays in the header too, left of the block's own control: while building, the preview is what gets switched most. Controls is the menu's first row, with its familiar icon @@ -1182,7 +1185,7 @@ Specified under "A pill that opens a menu" in [Controls](#controls). ### Block status An 8px dot on the block's mark with a 2px `bg-surface` ring, one spec for dock -and the DAG (`block_status_style()`). Its fills are meaning tokens, with no +and the DAG (`block_status_badge()`). Its fills are meaning tokens, with no status tokens of their own: the spec names each token next to its light value, which a renderer that cannot read CSS (the DAG's canvas) draws with. @@ -1196,7 +1199,7 @@ value, which a renderer that cannot read CSS (the DAG's canvas) draws with. Unset and waiting share the amber and differ by shape: the block that needs you has the solid dot, and the blocks below it, which wait for it, have rings. The amber replaces the off-palette `#eab308` blockr.dock's -`block_status_style()` gave unset. +`block_status_badge()` gave unset. ### Status line and plain text @@ -1522,12 +1525,13 @@ rules (today declared twice, in blockr.viz `chart.css` and blockr.dm menu (`tool_button()`, `action_menu()`), the button classes (`.blockr-btn`), the chevron (`Blockr.icons.chevron`), and the column-label reader. blockr.dock owns the dock header, the "…" menu, the block status dot and its spec -(`block_status_style()`, which the DAG draws from too), the category colours +(`block_status_badge()`, which the DAG draws from too), the category colours (`blk_color()`) and the rule that hides the gear in simplified mode. blockr.theme owns data colours. The token tests in blockr.ui check that the vocabulary changes only deliberately, that every name follows the grammar or is a legacy alias, that -the controls read meaning tokens only, and that consumers' fallbacks agree with +the controls read meaning tokens only, that icons and status borders clear +3:1 on the surface in both schemes, and that consumers' fallbacks agree with the tokens. Palette and legacy reads elsewhere in blockr.ui change only deliberately too. To do: warn on every palette or legacy read in the packages that consume the tokens.