From 01e86a13dd919d3a3561b2540bdc481d214fc9f1 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 26 Aug 2026 18:38:52 -0500 Subject: [PATCH 1/7] Render provenance and citation markers as grouped icon asides An alternative to #166 built on shinychat's aside rendering; closes #148, closes #149. - Citations drop display="compact" and render as labeled identity asides: shinychat accumulates same-paragraph citations into one marker with a +N overflow and a carousel popover, instead of commons styling numbered markers itself. - Verified/Untrusted/citation markers render as bare icons (the label stays in the DOM for aria), with tone supplied only through shinychat's public --shiny-chat-aside-marker-* variables, so its own hover, focus, and expanded states keep working. Grouped markers get a stadium shape; bare icons stay circular. - Adopts #166's artwork: the warning icon's white glyph on a caution triangle (the familiar-signs idea from #149), and its quote-mark citation glyph (recolored slate, reframed for bare rendering). The aside's icon attribute carries the per-kind icon, so the popover title row shows it; the pill paints the uniform quote mark over it via CSS (a marked TRIAL block, deleted if shinychat gains a popover-icon attribute). - Provenance markers keep a one-shot pulse, disabled under prefers-reduced-motion. --- pkg-r/R/chat.R | 6 +- pkg-r/R/citations.R | 7 +- pkg-r/inst/www/commons-chat/commons-chat.css | 133 ++++++++++++++---- .../www/commons-chat/figs/citation-mark.svg | 3 + .../www/commons-chat/figs/warning-icon.svg | 12 +- pkg-r/tests/testthat/test-citation-browser.R | 74 +++------- pkg-r/tests/testthat/test-citations.R | 4 +- 7 files changed, 145 insertions(+), 94 deletions(-) create mode 100644 pkg-r/inst/www/commons-chat/figs/citation-mark.svg diff --git a/pkg-r/R/chat.R b/pkg-r/R/chat.R index dbe01753..0230a959 100644 --- a/pkg-r/R/chat.R +++ b/pkg-r/R/chat.R @@ -13,9 +13,9 @@ #' @section Citations and provenance: #' The server verifies each `` the model writes against its #' own context, measure definitions, and data documentation as the answer -#' streams, and rewrites verified citations inline as numbered, -#' server-authored `` elements. Citation details name the -#' trusted source. A provenance marker in a compact `` follows +#' streams, and rewrites verified citations inline as server-authored +#' `` elements. Citation details name the +#' trusted source. A provenance marker `` follows #' the answer when it was produced by a governed calculation, or when a #' fallback answer cites nothing verified. #' diff --git a/pkg-r/R/citations.R b/pkg-r/R/citations.R index 8b43d4d1..5a560093 100644 --- a/pkg-r/R/citations.R +++ b/pkg-r/R/citations.R @@ -95,7 +95,7 @@ citation_aside_html <- function(quote, explanation, label, kind) { blockquote <- paste0("> ", gsub("\n", "\n> ", trimws(quote), fixed = TRUE)) sprintf( paste0( - '', + '', "%s%s" ), escape_attr(label), @@ -227,7 +227,10 @@ non_citable_tool_output_text <- function(tools) { paste(items, collapse = "\n") } -# These SVGs need a fixed stroke because images cannot inherit currentColor. +# The aside's icon attribute carries the per-kind icon so the citation +# popover's title row shows it; the pill marker is the uniform quote mark, +# painted by commons-chat.css. These SVGs need a fixed stroke because +# images cannot inherit currentColor. citation_icon_url <- function(kind) { file <- switch( kind, diff --git a/pkg-r/inst/www/commons-chat/commons-chat.css b/pkg-r/inst/www/commons-chat/commons-chat.css index 4921df20..c502db16 100644 --- a/pkg-r/inst/www/commons-chat/commons-chat.css +++ b/pkg-r/inst/www/commons-chat/commons-chat.css @@ -130,56 +130,106 @@ shiny-chat-container .shiny-chat-input .tiptap { transform: translateX(-50%) rotate(225deg); } -/* Shinychat owns compact marker structure and placement through this public API. */ -shiny-chat-container [data-shinychat-aside-display="compact"] { - --shiny-chat-aside-marker-color: var(--bs-link-color); - --shiny-chat-aside-marker-hover-color: var(--bs-link-hover-color); +/* Citations are labeled identity asides: shinychat accumulates those + * sharing a paragraph into one pill with a "+N" overflow and a carousel + * popover. They render icon-only like the provenance markers, in a quieter + * slate blue — verified support, short of a governed calculation. */ +shiny-chat-container + .shiny-aside-pill:has(img[src*="/citation-"]) { + --shiny-chat-aside-marker-color: #41556e; --shiny-chat-aside-marker-bg: transparent; - --shiny-chat-aside-marker-hover-bg: transparent; - --shiny-chat-aside-marker-font-family: var(--bs-font-monospace); + --shiny-chat-aside-marker-hover-color: #2f4257; + --shiny-chat-aside-marker-hover-bg: #e9eff5; } -/* Temporary bridge until Shinychat provides a public per-aside tone API. */ +/* TRIAL: the aside's icon attribute carries the per-kind icon (so the + * popover title row shows it); the pill hides that img and paints the + * uniform quote mark instead. Caveat: shinychat unmounts the img if it + * errors, which would silently drop these selectors' match — if shinychat + * gains a popover-icon attribute, delete this block. */ shiny-chat-container - .shiny-aside-pill:has(img[src$="/trusted-icon.svg"]) { - background: #f2fbf5; - box-shadow: inset 0 0 0 1px #cfeedd; - color: #286144; + .shiny-aside-pill:has(img[src*="/citation-"]) + img { + display: none; } shiny-chat-container - .shiny-aside-pill:has(img[src$="/trusted-icon.svg"]):hover, -shiny-chat-container - .shiny-aside-pill:has(img[src$="/trusted-icon.svg"]):focus-visible, + .shiny-aside-pill:has(img[src*="/citation-"])::before { + background: url("figs/citation-mark.svg") center / contain no-repeat; + content: ""; + flex: 0 0 auto; + height: 1.35em; + width: 1.35em; +} + +/* Temporary bridge until shinychat forwards a per-aside tone hook to the + * pill: the icon src is the only per-aside signal that survives to the + * rendered button, so it selects the tone — but styling goes only through + * shinychat's public --shiny-chat-aside-marker-* variables, so its own + * hover, focus, expanded, and compact-number rules keep working. */ shiny-chat-container - .shiny-aside-pill:has(img[src$="/trusted-icon.svg"])[aria-expanded="true"] { - background: #e4f6ea; - color: #204f38; + .shiny-aside-pill:has(img[src$="/trusted-icon.svg"]) { + --shiny-chat-aside-marker-color: #286144; + --shiny-chat-aside-marker-bg: transparent; + --shiny-chat-aside-marker-hover-color: #204f38; + --shiny-chat-aside-marker-hover-bg: #e4f6ea; } shiny-chat-container .shiny-aside-pill:has(img[src$="/warning-icon.svg"]) { - background: #fff8ec; - box-shadow: inset 0 0 0 1px #f2ddbb; - color: #6b4b1b; + --shiny-chat-aside-marker-color: #6b4b1b; + --shiny-chat-aside-marker-bg: transparent; + --shiny-chat-aside-marker-hover-color: #583b14; + --shiny-chat-aside-marker-hover-bg: #fcefd8; } +/* Provenance and citation markers render as bare icons: the label stays in + * the DOM (the button's aria-label announces it) but doesn't render, and a + * grouped "+N" overflow count still shows. */ shiny-chat-container - .shiny-aside-pill:has(img[src$="/warning-icon.svg"]):hover, + .shiny-aside-pill:has( + img[src$="/trusted-icon.svg"], + img[src$="/warning-icon.svg"], + img[src*="/citation-"] + ) { + border-radius: 50%; + font-weight: 400; + gap: 0.15em; + padding: 0.2em; +} + +/* A grouped marker carries a "+N more" count, which widens the pill: give + * it a stadium shape and roomier padding instead of the bare-icon circle. */ shiny-chat-container - .shiny-aside-pill:has(img[src$="/warning-icon.svg"]):focus-visible, + .shiny-aside-pill:has( + img[src$="/trusted-icon.svg"], + img[src$="/warning-icon.svg"], + img[src*="/citation-"] + ):has(.shiny-aside-pill__overflow) { + border-radius: 999px; + gap: 0.3em; + padding: 0.2em 0.55em 0.2em 0.25em; +} + shiny-chat-container - .shiny-aside-pill:has(img[src$="/warning-icon.svg"])[aria-expanded="true"] { - background: #fcefd8; - color: #583b14; + .shiny-aside-pill:has( + img[src$="/trusted-icon.svg"], + img[src$="/warning-icon.svg"], + img[src*="/citation-"] + ) + .shiny-aside-pill__label { + display: none; } shiny-chat-container .shiny-aside-pill:has( img[src$="/trusted-icon.svg"], - img[src$="/warning-icon.svg"] - ) { - font-weight: 400; + img[src$="/warning-icon.svg"], + img[src*="/citation-"] + ) + img { + height: 1.35em; + width: 1.35em; } shiny-chat-container @@ -188,9 +238,36 @@ shiny-chat-container img[src$="/warning-icon.svg"] ) img { + animation: commons-provenance-marker-pulse 1.6s ease-in-out 1; opacity: 1; } +@keyframes commons-provenance-marker-pulse { + 0% { + transform: scale(1); + } + 35% { + transform: scale(1.4); + } + 70% { + transform: scale(0.92); + } + 100% { + transform: scale(1); + } +} + +@media (prefers-reduced-motion: reduce) { + shiny-chat-container + .shiny-aside-pill:has( + img[src$="/trusted-icon.svg"], + img[src$="/warning-icon.svg"] + ) + img { + animation: none; + } +} + /* ---- Measure display ------------------------------------------------- */ .commons-measure-display { diff --git a/pkg-r/inst/www/commons-chat/figs/citation-mark.svg b/pkg-r/inst/www/commons-chat/figs/citation-mark.svg new file mode 100644 index 00000000..be06f598 --- /dev/null +++ b/pkg-r/inst/www/commons-chat/figs/citation-mark.svg @@ -0,0 +1,3 @@ + + + diff --git a/pkg-r/inst/www/commons-chat/figs/warning-icon.svg b/pkg-r/inst/www/commons-chat/figs/warning-icon.svg index fa175077..fe74d73d 100644 --- a/pkg-r/inst/www/commons-chat/figs/warning-icon.svg +++ b/pkg-r/inst/www/commons-chat/figs/warning-icon.svg @@ -4,17 +4,21 @@ - + - + + - \ No newline at end of file + diff --git a/pkg-r/tests/testthat/test-citation-browser.R b/pkg-r/tests/testthat/test-citation-browser.R index b9026f30..4cc2abff 100644 --- a/pkg-r/tests/testthat/test-citation-browser.R +++ b/pkg-r/tests/testthat/test-citation-browser.R @@ -7,7 +7,7 @@ test_that("normal package checks can disable browser tests explicitly", { ) }) -test_that("Shiny Chat renders native numbered streamed citations", { +test_that("Shiny Chat renders streamed citations as icon markers", { skip_on_cran() skip_if_not_installed("shinytest2") skip_if_not_installed("chromote") @@ -27,10 +27,8 @@ test_that("Shiny Chat renders native numbered streamed citations", { timeout = 30 * 1000 ) marker_selector <- paste0( - 'button[data-shinychat-aside-display="compact"]', - '[aria-label="Aside 1: documentation"],', - 'button[data-shinychat-aside-display="compact"]', - '[aria-label="Aside 2: sales table"]' + 'button.shiny-aside-pill[aria-label="documentation"],', + 'button.shiny-aside-pill[aria-label="sales table"]' ) app$wait_for_js( paste0("document.querySelectorAll('", marker_selector, "').length === 2;"), @@ -46,31 +44,19 @@ test_that("Shiny Chat renders native numbered streamed citations", { ".map((node) => node.textContent).join('|');" ) ), - "[1]|[2]" - ) - expect_identical( - app$get_js( - paste0( - "Array.from(document.querySelectorAll('", - marker_selector, - "'))", - ".map((node) => node.getAttribute('aria-label')).join('|');" - ) - ), - "Aside 1: documentation|Aside 2: sales table" + "documentation|sales table" ) expect_identical( app$get_js( paste0( "(() => {", "const style = getComputedStyle(document.querySelector(", - "'button[data-shinychat-aside-display=\"compact\"]'));", + "'button.shiny-aside-pill[aria-label=\"documentation\"]'));", "return [", "'--shiny-chat-aside-marker-color',", "'--shiny-chat-aside-marker-hover-color',", "'--shiny-chat-aside-marker-bg',", - "'--shiny-chat-aside-marker-hover-bg',", - "'--shiny-chat-aside-marker-font-family'", + "'--shiny-chat-aside-marker-hover-bg'", "].every((name) => style.getPropertyValue(name).trim() !== '');", "})()" ) @@ -88,16 +74,13 @@ test_that("Shiny Chat renders native numbered streamed citations", { ) ), paste( - "Canopy weighting follows the documentation.\n[1]", - "Revenue timing follows the table definition.\n[2]", + "Canopy weighting follows the documentation.", + "Revenue timing follows the table definition.", sep = "|" ) ) - first_marker <- paste0( - 'button[data-shinychat-aside-display="compact"]', - '[aria-label="Aside 1: documentation"]' - ) - first_dialog <- '[role="dialog"][aria-label="Aside 1: documentation"]' + first_marker <- 'button.shiny-aside-pill[aria-label="documentation"]' + first_dialog <- '[role="dialog"][aria-label="documentation"]' app$get_js( paste0("document.querySelector('", first_marker, "').click();") ) @@ -143,11 +126,8 @@ test_that("Shiny Chat renders native numbered streamed citations", { timeout = 30 * 1000 ) - second_marker <- paste0( - 'button[data-shinychat-aside-display="compact"]', - '[aria-label="Aside 2: sales table"]' - ) - second_dialog <- '[role="dialog"][aria-label="Aside 2: sales table"]' + second_marker <- 'button.shiny-aside-pill[aria-label="sales table"]' + second_dialog <- '[role="dialog"][aria-label="sales table"]' app$get_js( paste0("document.querySelector('", second_marker, "').click();") ) @@ -215,8 +195,7 @@ test_that("Shiny Chat distinguishes verified, cited, and untrusted asides", { paste0( "document.querySelector('button[aria-label=\"Verified answer\"]') && ", "document.querySelector(", - "'button[data-shinychat-aside-display=\"compact\"]", - "[aria-label=\"Aside 1: documentation\"]') && ", + "'button.shiny-aside-pill[aria-label=\"documentation\"]') && ", "document.querySelector('button[aria-label=\"Untrusted\"]');" ), timeout = 30 * 1000 @@ -248,21 +227,8 @@ test_that("Shiny Chat distinguishes verified, cited, and untrusted asides", { app$get_js( paste0( "document.querySelector(", - "'button[data-shinychat-aside-display=\"compact\"]", - "[aria-label=\"Aside 1: documentation\"]'", - ").getAttribute('aria-label');" - ) - ), - "Aside 1: documentation" - ) - - expect_identical( - app$get_js( - paste0( - "document.querySelector(", - "'button[data-shinychat-aside-display=\"compact\"]", - "[aria-label=\"Aside 1: documentation\"]'", - ").closest('p')?.innerText.includes('Supported claim.');" + "'button.shiny-aside-pill[aria-label=\"documentation\"]')", + ".closest('p')?.innerText.includes('Supported claim.');" ) ), TRUE @@ -319,9 +285,8 @@ test_that("Shiny Chat preserves Markdown blocks around citations", { app$wait_for_js( paste0( "document.querySelectorAll(", - "'button[data-shinychat-aside-display=\"compact\"]", - "[aria-label=\"Aside 1: documentation\"]'", - ").length === 2;" + "'button.shiny-aside-pill[aria-label=\"documentation\"]')", + ".length === 2;" ), timeout = 30 * 1000 ) @@ -338,9 +303,8 @@ test_that("Shiny Chat preserves Markdown blocks around citations", { app$get_js( paste0( "document.querySelectorAll(", - "'button[data-shinychat-aside-display=\"compact\"]", - "[aria-label=\"Aside 1: documentation\"]'", - ").length;" + "'button.shiny-aside-pill[aria-label=\"documentation\"]')", + ".length;" ) ), 2L diff --git a/pkg-r/tests/testthat/test-citations.R b/pkg-r/tests/testthat/test-citations.R index c856be9c..e6975bb5 100644 --- a/pkg-r/tests/testthat/test-citations.R +++ b/pkg-r/tests/testthat/test-citations.R @@ -393,7 +393,7 @@ test_that("user messages reset citation requests but tool results do not", { expect_true(tracker$requested) }) -test_that("render_citation_aside emits a numbered aside with source details", { +test_that("render_citation_aside emits a labeled aside with source details", { corpus <- list(list( label = "sales table", kind = "schema", @@ -407,7 +407,7 @@ test_that("render_citation_aside emits a numbered aside with source details", { expect_match( out$html, paste0( - '^ Date: Wed, 26 Aug 2026 19:34:29 -0500 Subject: [PATCH 2/7] Name the source in singleton citation popovers via a body title MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit shinychat only renders the popover's title row (icon + label) for grouped asides, so a lone citation opened a popover with no visible source label — the pill label it treats as redundant is hidden by commons's icon-only styling. The aside body now carries its own title (per-kind icon + label), and commons-chat.css hides shinychat's title row in those popovers so grouped asides don't name the source twice. The example app gains a singleton-citation message covering the case; the browser test's label/icon expectations pass unmodified again. --- pkg-r/R/citations.R | 14 ++++++++++++- pkg-r/inst/www/commons-chat/commons-chat.css | 22 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/pkg-r/R/citations.R b/pkg-r/R/citations.R index 5a560093..7f8ff586 100644 --- a/pkg-r/R/citations.R +++ b/pkg-r/R/citations.R @@ -93,13 +93,25 @@ citation_aside_html <- function(quote, explanation, label, kind) { icon <- citation_icon_url(kind) reason <- if (nzchar(explanation)) paste0(explanation, "\n\n") else "" blockquote <- paste0("> ", gsub("\n", "\n> ", trimws(quote), fixed = TRUE)) + # shinychat only renders the popover's title row for grouped asides, so + # the body carries its own title (icon + label) to keep the source named + # for singleton citations; commons-chat.css hides shinychat's row. + title <- sprintf( + paste0( + '', + '%s%s\n\n' + ), + if (is.null(icon)) "" else sprintf('', escape_attr(icon)), + htmltools::htmlEscape(label) + ) sprintf( paste0( '', - "%s%s" + "%s%s%s" ), escape_attr(label), if (is.null(icon)) "" else sprintf(' icon="%s"', escape_attr(icon)), + title, reason, blockquote ) diff --git a/pkg-r/inst/www/commons-chat/commons-chat.css b/pkg-r/inst/www/commons-chat/commons-chat.css index c502db16..366c618c 100644 --- a/pkg-r/inst/www/commons-chat/commons-chat.css +++ b/pkg-r/inst/www/commons-chat/commons-chat.css @@ -268,6 +268,28 @@ shiny-chat-container } } +/* The citation aside body carries its own title (icon + label) because + * shinychat only renders its popover title row for grouped asides; hide + * shinychat's row so grouped popovers don't name the source twice. The + * popover is portaled to , so these rules must stay unscoped. */ +.commons-citation-title { + align-items: center; + display: flex; + font-weight: 600; + gap: 0.3em; + margin-bottom: 0.25rem; +} + +.commons-citation-title img { + height: 1.1em; + width: 1.1em; +} + +.shiny-aside-popover:has(.commons-citation-title) + .shiny-aside-popover__label { + display: none; +} + /* ---- Measure display ------------------------------------------------- */ .commons-measure-display { From fecb9c9a3c58883721c7fa151331a1fee3bd727c Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 26 Aug 2026 19:35:35 -0500 Subject: [PATCH 3/7] Render the citation pill's quote mark as a plain img MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the source named by the aside body's own title, the popover no longer uses the aside's icon attribute — so it can carry the uniform citation-mark.svg directly. The pill renders it like any other marker icon, deleting the TRIAL block that hid the per-kind img and repainted the quote mark via ::before, and tightening the bridge selectors from a citation-* prefix match to a single suffix. The repeated :has() selector lists consolidate under :is(). Same rendered result; the browser suite passes unmodified. --- pkg-r/R/citations.R | 19 +++++-- pkg-r/inst/www/commons-chat/commons-chat.css | 60 ++++++++------------ pkg-r/tests/testthat/test-citations.R | 13 ++++- 3 files changed, 48 insertions(+), 44 deletions(-) diff --git a/pkg-r/R/citations.R b/pkg-r/R/citations.R index 7f8ff586..539bbbd1 100644 --- a/pkg-r/R/citations.R +++ b/pkg-r/R/citations.R @@ -90,7 +90,11 @@ render_citation_aside <- function(quote, explanation, corpus) { } citation_aside_html <- function(quote, explanation, label, kind) { - icon <- citation_icon_url(kind) + # The pill renders the uniform quote mark; the per-kind icon goes in + # the body title (commons-chat.css hides shinychat's popover title row, + # so the aside's icon attribute is only a styling hook for the pill). + icon <- commons_icon_url("citation-mark.svg") + kind_icon <- citation_icon_url(kind) reason <- if (nzchar(explanation)) paste0(explanation, "\n\n") else "" blockquote <- paste0("> ", gsub("\n", "\n> ", trimws(quote), fixed = TRUE)) # shinychat only renders the popover's title row for grouped asides, so @@ -101,7 +105,11 @@ citation_aside_html <- function(quote, explanation, label, kind) { '', '%s%s\n\n' ), - if (is.null(icon)) "" else sprintf('', escape_attr(icon)), + if (is.null(kind_icon)) { + "" + } else { + sprintf('', escape_attr(kind_icon)) + }, htmltools::htmlEscape(label) ) sprintf( @@ -239,10 +247,9 @@ non_citable_tool_output_text <- function(tools) { paste(items, collapse = "\n") } -# The aside's icon attribute carries the per-kind icon so the citation -# popover's title row shows it; the pill marker is the uniform quote mark, -# painted by commons-chat.css. These SVGs need a fixed stroke because -# images cannot inherit currentColor. +# The per-kind icon appears in the aside body's title; the pill renders +# the uniform citation-mark.svg quote mark instead. These SVGs need a +# fixed stroke because images cannot inherit currentColor. citation_icon_url <- function(kind) { file <- switch( kind, diff --git a/pkg-r/inst/www/commons-chat/commons-chat.css b/pkg-r/inst/www/commons-chat/commons-chat.css index 366c618c..01f31607 100644 --- a/pkg-r/inst/www/commons-chat/commons-chat.css +++ b/pkg-r/inst/www/commons-chat/commons-chat.css @@ -135,33 +135,13 @@ shiny-chat-container .shiny-chat-input .tiptap { * popover. They render icon-only like the provenance markers, in a quieter * slate blue — verified support, short of a governed calculation. */ shiny-chat-container - .shiny-aside-pill:has(img[src*="/citation-"]) { + .shiny-aside-pill:has(img[src$="/citation-mark.svg"]) { --shiny-chat-aside-marker-color: #41556e; --shiny-chat-aside-marker-bg: transparent; --shiny-chat-aside-marker-hover-color: #2f4257; --shiny-chat-aside-marker-hover-bg: #e9eff5; } -/* TRIAL: the aside's icon attribute carries the per-kind icon (so the - * popover title row shows it); the pill hides that img and paints the - * uniform quote mark instead. Caveat: shinychat unmounts the img if it - * errors, which would silently drop these selectors' match — if shinychat - * gains a popover-icon attribute, delete this block. */ -shiny-chat-container - .shiny-aside-pill:has(img[src*="/citation-"]) - img { - display: none; -} - -shiny-chat-container - .shiny-aside-pill:has(img[src*="/citation-"])::before { - background: url("figs/citation-mark.svg") center / contain no-repeat; - content: ""; - flex: 0 0 auto; - height: 1.35em; - width: 1.35em; -} - /* Temporary bridge until shinychat forwards a per-aside tone hook to the * pill: the icon src is the only per-aside signal that survives to the * rendered button, so it selects the tone — but styling goes only through @@ -188,9 +168,11 @@ shiny-chat-container * grouped "+N" overflow count still shows. */ shiny-chat-container .shiny-aside-pill:has( - img[src$="/trusted-icon.svg"], - img[src$="/warning-icon.svg"], - img[src*="/citation-"] + :is( + img[src$="/trusted-icon.svg"], + img[src$="/warning-icon.svg"], + img[src$="/citation-mark.svg"] + ) ) { border-radius: 50%; font-weight: 400; @@ -202,9 +184,11 @@ shiny-chat-container * it a stadium shape and roomier padding instead of the bare-icon circle. */ shiny-chat-container .shiny-aside-pill:has( - img[src$="/trusted-icon.svg"], - img[src$="/warning-icon.svg"], - img[src*="/citation-"] + :is( + img[src$="/trusted-icon.svg"], + img[src$="/warning-icon.svg"], + img[src$="/citation-mark.svg"] + ) ):has(.shiny-aside-pill__overflow) { border-radius: 999px; gap: 0.3em; @@ -213,9 +197,11 @@ shiny-chat-container shiny-chat-container .shiny-aside-pill:has( - img[src$="/trusted-icon.svg"], - img[src$="/warning-icon.svg"], - img[src*="/citation-"] + :is( + img[src$="/trusted-icon.svg"], + img[src$="/warning-icon.svg"], + img[src$="/citation-mark.svg"] + ) ) .shiny-aside-pill__label { display: none; @@ -223,9 +209,11 @@ shiny-chat-container shiny-chat-container .shiny-aside-pill:has( - img[src$="/trusted-icon.svg"], - img[src$="/warning-icon.svg"], - img[src*="/citation-"] + :is( + img[src$="/trusted-icon.svg"], + img[src$="/warning-icon.svg"], + img[src$="/citation-mark.svg"] + ) ) img { height: 1.35em; @@ -234,8 +222,7 @@ shiny-chat-container shiny-chat-container .shiny-aside-pill:has( - img[src$="/trusted-icon.svg"], - img[src$="/warning-icon.svg"] + :is(img[src$="/trusted-icon.svg"], img[src$="/warning-icon.svg"]) ) img { animation: commons-provenance-marker-pulse 1.6s ease-in-out 1; @@ -260,8 +247,7 @@ shiny-chat-container @media (prefers-reduced-motion: reduce) { shiny-chat-container .shiny-aside-pill:has( - img[src$="/trusted-icon.svg"], - img[src$="/warning-icon.svg"] + :is(img[src$="/trusted-icon.svg"], img[src$="/warning-icon.svg"]) ) img { animation: none; diff --git a/pkg-r/tests/testthat/test-citations.R b/pkg-r/tests/testthat/test-citations.R index e6975bb5..8a0108ee 100644 --- a/pkg-r/tests/testthat/test-citations.R +++ b/pkg-r/tests/testthat/test-citations.R @@ -404,14 +404,25 @@ test_that("render_citation_aside emits a labeled aside with source details", { "The computation follows the documented recognition rule.", corpus ) + # The pill renders the uniform quote mark; the per-kind icon and the + # source label appear in the body title instead. expect_match( out$html, paste0( '^' + ), + fixed = TRUE + ) expect_no_match(out$html, "data:image", fixed = TRUE) expect_match( out$html, From dd866d03df276a82055554b9fb5dc55224145e49 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 26 Aug 2026 20:23:35 -0500 Subject: [PATCH 4/7] Redraw the citation marker as a navy tile with a Bootstrap quote MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bare slate quote glyph read as punctuation inline and broke the family's filled-sign-plus-white-glyph convention (shield, triangle). The citation marker is now Bootstrap Icons' quote glyph (MIT) in white on a mid-navy (#2e4a6b) rounded-square tile; the Untrusted triangle likewise adopts exclamation-triangle-fill, padded to a common viewBox. Plain paths throughout — no runtime font dependency, unlike #166's -based citation mark. The Verified shield keeps its existing artwork. Pill tones move from slate to matching navy. Also drops the orphaned untrusted-icon.svg. --- pkg-r/inst/www/commons-chat/commons-chat.css | 8 +++--- .../www/commons-chat/figs/citation-mark.svg | 6 +++-- .../www/commons-chat/figs/untrusted-icon.svg | 20 -------------- .../www/commons-chat/figs/warning-icon.svg | 27 +++---------------- 4 files changed, 11 insertions(+), 50 deletions(-) delete mode 100644 pkg-r/inst/www/commons-chat/figs/untrusted-icon.svg diff --git a/pkg-r/inst/www/commons-chat/commons-chat.css b/pkg-r/inst/www/commons-chat/commons-chat.css index 01f31607..7be85bca 100644 --- a/pkg-r/inst/www/commons-chat/commons-chat.css +++ b/pkg-r/inst/www/commons-chat/commons-chat.css @@ -133,13 +133,13 @@ shiny-chat-container .shiny-chat-input .tiptap { /* Citations are labeled identity asides: shinychat accumulates those * sharing a paragraph into one pill with a "+N" overflow and a carousel * popover. They render icon-only like the provenance markers, in a quieter - * slate blue — verified support, short of a governed calculation. */ + * mid navy — verified support, short of a governed calculation. */ shiny-chat-container .shiny-aside-pill:has(img[src$="/citation-mark.svg"]) { - --shiny-chat-aside-marker-color: #41556e; + --shiny-chat-aside-marker-color: #2e4a6b; --shiny-chat-aside-marker-bg: transparent; - --shiny-chat-aside-marker-hover-color: #2f4257; - --shiny-chat-aside-marker-hover-bg: #e9eff5; + --shiny-chat-aside-marker-hover-color: #223851; + --shiny-chat-aside-marker-hover-bg: #e7edf4; } /* Temporary bridge until shinychat forwards a per-aside tone hook to the diff --git a/pkg-r/inst/www/commons-chat/figs/citation-mark.svg b/pkg-r/inst/www/commons-chat/figs/citation-mark.svg index be06f598..483793d5 100644 --- a/pkg-r/inst/www/commons-chat/figs/citation-mark.svg +++ b/pkg-r/inst/www/commons-chat/figs/citation-mark.svg @@ -1,3 +1,5 @@ - - + + + + diff --git a/pkg-r/inst/www/commons-chat/figs/untrusted-icon.svg b/pkg-r/inst/www/commons-chat/figs/untrusted-icon.svg deleted file mode 100644 index bc4af8b9..00000000 --- a/pkg-r/inst/www/commons-chat/figs/untrusted-icon.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/pkg-r/inst/www/commons-chat/figs/warning-icon.svg b/pkg-r/inst/www/commons-chat/figs/warning-icon.svg index fe74d73d..5a283d5f 100644 --- a/pkg-r/inst/www/commons-chat/figs/warning-icon.svg +++ b/pkg-r/inst/www/commons-chat/figs/warning-icon.svg @@ -1,24 +1,3 @@ - - - - - - - - - - - - - + + + \ No newline at end of file From 49c05fde2dc5cfaef5aca2479bcd30665e1a22f5 Mon Sep 17 00:00:00 2001 From: Carson Date: Thu, 27 Aug 2026 10:34:34 -0500 Subject: [PATCH 5/7] Return the warning icon to #166's white-glyph circle Drops this branch's redrawn caution triangle in favor of the original yellow circle with the white exclamation glyph, matching what #166 proposes, so the two PRs differ only in mechanism, not artwork. --- .../www/commons-chat/figs/warning-icon.svg | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/pkg-r/inst/www/commons-chat/figs/warning-icon.svg b/pkg-r/inst/www/commons-chat/figs/warning-icon.svg index 5a283d5f..9f25e7fe 100644 --- a/pkg-r/inst/www/commons-chat/figs/warning-icon.svg +++ b/pkg-r/inst/www/commons-chat/figs/warning-icon.svg @@ -1,3 +1,20 @@ - - - \ No newline at end of file + + + + + + + + + + + + From a8c6f6b2bff138be0843c25a2e5367739e153709 Mon Sep 17 00:00:00 2001 From: Carson Date: Thu, 27 Aug 2026 12:27:55 -0500 Subject: [PATCH 6/7] Source all marker glyphs from Radix Icons (MIT) Unify the aside marker icons on one MIT-licensed set: quote on the navy citation tile, check on the trusted shield, exclamation on the warning disc, and file-text/table/ruler-horizontal for the per-kind citation icons. Fills and strokes are baked in since the markers render as and cannot inherit currentColor. Pill markers bump from 1.35em to 1.5em. --- pkg-r/inst/www/commons-chat/commons-chat.css | 4 ++-- .../commons-chat/figs/citation-definition.svg | 6 ++--- .../www/commons-chat/figs/citation-mark.svg | 4 ++-- .../www/commons-chat/figs/citation-prose.svg | 7 +++--- .../www/commons-chat/figs/citation-schema.svg | 6 ++--- .../www/commons-chat/figs/trusted-icon.svg | 24 ++++--------------- .../www/commons-chat/figs/warning-icon.svg | 23 ++++-------------- 7 files changed, 23 insertions(+), 51 deletions(-) diff --git a/pkg-r/inst/www/commons-chat/commons-chat.css b/pkg-r/inst/www/commons-chat/commons-chat.css index 7be85bca..3cd75809 100644 --- a/pkg-r/inst/www/commons-chat/commons-chat.css +++ b/pkg-r/inst/www/commons-chat/commons-chat.css @@ -216,8 +216,8 @@ shiny-chat-container ) ) img { - height: 1.35em; - width: 1.35em; + height: 1.5em; + width: 1.5em; } shiny-chat-container diff --git a/pkg-r/inst/www/commons-chat/figs/citation-definition.svg b/pkg-r/inst/www/commons-chat/figs/citation-definition.svg index 5c59acea..8e321d7b 100644 --- a/pkg-r/inst/www/commons-chat/figs/citation-definition.svg +++ b/pkg-r/inst/www/commons-chat/figs/citation-definition.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/pkg-r/inst/www/commons-chat/figs/citation-mark.svg b/pkg-r/inst/www/commons-chat/figs/citation-mark.svg index 483793d5..275f3d5c 100644 --- a/pkg-r/inst/www/commons-chat/figs/citation-mark.svg +++ b/pkg-r/inst/www/commons-chat/figs/citation-mark.svg @@ -1,5 +1,5 @@ - + - + diff --git a/pkg-r/inst/www/commons-chat/figs/citation-prose.svg b/pkg-r/inst/www/commons-chat/figs/citation-prose.svg index 22cc2597..1c154982 100644 --- a/pkg-r/inst/www/commons-chat/figs/citation-prose.svg +++ b/pkg-r/inst/www/commons-chat/figs/citation-prose.svg @@ -1,5 +1,4 @@ - - - - + + + diff --git a/pkg-r/inst/www/commons-chat/figs/citation-schema.svg b/pkg-r/inst/www/commons-chat/figs/citation-schema.svg index 80e196e7..41ebf3a7 100644 --- a/pkg-r/inst/www/commons-chat/figs/citation-schema.svg +++ b/pkg-r/inst/www/commons-chat/figs/citation-schema.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/pkg-r/inst/www/commons-chat/figs/trusted-icon.svg b/pkg-r/inst/www/commons-chat/figs/trusted-icon.svg index 5671efce..1aff24a7 100644 --- a/pkg-r/inst/www/commons-chat/figs/trusted-icon.svg +++ b/pkg-r/inst/www/commons-chat/figs/trusted-icon.svg @@ -1,20 +1,6 @@ - - - - - - - - - - - \ No newline at end of file + + + + diff --git a/pkg-r/inst/www/commons-chat/figs/warning-icon.svg b/pkg-r/inst/www/commons-chat/figs/warning-icon.svg index 9f25e7fe..102d73d5 100644 --- a/pkg-r/inst/www/commons-chat/figs/warning-icon.svg +++ b/pkg-r/inst/www/commons-chat/figs/warning-icon.svg @@ -1,20 +1,7 @@ - - - - - - - - - - + + + From c8d1deda5d07e8c792afe884e5e07ca78cdad718 Mon Sep 17 00:00:00 2001 From: Carson Date: Thu, 27 Aug 2026 13:16:29 -0500 Subject: [PATCH 7/7] Soften citation-mark and warning-icon palette Shrink the citation quotation glyph (0.85 -> 0.70, centered like the sibling glyphs) so its interior whitespace matches the other markers, lighten the tile from navy #2e4a6b to #55729e, and soften the warning disc from #e7b921 to #d9b84a. Addresses review feedback on #180. --- pkg-r/inst/www/commons-chat/figs/citation-mark.svg | 4 ++-- pkg-r/inst/www/commons-chat/figs/warning-icon.svg | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg-r/inst/www/commons-chat/figs/citation-mark.svg b/pkg-r/inst/www/commons-chat/figs/citation-mark.svg index 275f3d5c..0ffe1a42 100644 --- a/pkg-r/inst/www/commons-chat/figs/citation-mark.svg +++ b/pkg-r/inst/www/commons-chat/figs/citation-mark.svg @@ -1,5 +1,5 @@ - - + + diff --git a/pkg-r/inst/www/commons-chat/figs/warning-icon.svg b/pkg-r/inst/www/commons-chat/figs/warning-icon.svg index 102d73d5..bb3d1924 100644 --- a/pkg-r/inst/www/commons-chat/figs/warning-icon.svg +++ b/pkg-r/inst/www/commons-chat/figs/warning-icon.svg @@ -2,6 +2,6 @@ - +