shinyblocks (development version)
Components
block_pagination()andupdate_block_pagination()are new. The standalone paging input reports an integer active page, renders previous/next bounds and ellipsis truncation, and supports server updates for page count, selection, and disabled state (issue #94).New
block_accordion()/block_accordion_item()/update_block_accordion(): vertically stacked collapsible content sections (shadcn accordion) for FAQs, grouped settings, and filters. Supportstype = "single"(one open at a time, optionallycollapsibleto none) andtype = "multiple"(independent), an initialopenset, per-itemiconanddisabled, and animated panel height. Panel bodies are arbitrary live Shiny/htmltoolscontent, so reactive outputs inside a closed section keep working....supports rlang's!!!splice, so a programmatically built list of items can be passed directly (block_accordion(!!!items)). Whenidis set,input$<id>reports the open value(s) - a string/NULL(single) or a character vector (multiple) - andupdate_block_accordion()opens/closes items from the server. Triggers carryaria-expanded/aria-controls, closed panels areinert, and arrow/home/end move focus between headers. The lower-levelblock_collapsible()is deferred (#91).New
block_dropdown_menu()/update_block_dropdown_menu(): a portal-rendered action menu (shadcn dropdown-menu) built fromdropdown_menu_item(),dropdown_menu_label(), anddropdown_menu_separator()parts. The trigger accepts a string label or an arbitrary tag (icon button, avatar); items support leading icons, right-aligned shortcut hints,disabled, and adestructivevariant. Choosing an item reports itsvaluetoinput$<id>as an event (fires again on a repeat choice, like an action button that carries a value). Full keyboard support — arrow/home/end navigation, enter/space to activate, escape/tab to close, and typeahead — withrole="menu"semantics and focus return to the trigger. Checkbox/radio items and sub-menus are deferred to follow-up work (#86).New
block_combobox()/update_block_combobox(): a searchable select (shadcn Command + Popover) with type-to-filter over choices, keyboard navigation, and a no-results empty state. Supports single and multiple modes (chips with an optionalmax_itemscap),search_placeholder/empty_messagetext, and the same hidden-native value bridge and updater contract asblock_select(). Closes the biggest catalog gap forselectizeInput()migration (#85).New
block_toggle_group()/update_block_toggle_group(): a segmented single/multiple toggle input (view switchers, formatting toolbars) reporting a string (single) or character vector (multiple) toinput$<id>. Supportsdefault/outlinevariants, three sizes, per-choice icons (vendored names or tags) with optional icon-only items, and group-wide or per-item disabling (#93).block_breadcrumb(),block_breadcrumb_item(), andblock_breadcrumb_ellipsis()are new (issue #92): a static page-shell breadcrumb trail following the shadcn breadcrumb pattern. The trail renders anav[aria-label="breadcrumb"]landmark; separators (default chevron, customizable per trail viaseparator =as a string or tag) are auto-inserted between entries and hidden from assistive technology;current = TRUEmarks the current page with a non-interactivearia-current="page"span; andblock_breadcrumb_ellipsis()collapses long middles behind a decorative glyph with a visually hidden label. Purehtmltoolsmarkup — no runtime payload or Shiny binding (static links are v1 per the issue; nav-input integration was deliberately deferred).block_input(type = "number")now has a first-class numeric contract (issue #96). Newmin,max, andsteparguments (number type only) drive increment/decrement stepper buttons rendered inside the control's right edge, the native arrow-key stepping, and stepper bound clamping;update_block_input()accepts the same arguments (NULLclears a bound).input$<id>for number inputs now reports a numeric value likeshiny::numericInput()—NAwhile the field is empty or unparseable — instead of a character string; other types are unchanged.
Bug fixes
Horizontal
block_tabs()lists now stay within narrow cards and responsive grid columns. Their compact intrinsic width is preserved when labels fit; longer tab rows scroll horizontally instead of overflowing their container.block_select()andblock_combobox()now reject malformed logical flags instead of silently treating them as false; their shared native-select and multi-selection internals have also been consolidated.block_page()no longer applies Tailwind Preflight or!importantfocus overrides to every descendant. The generated reset now targets only the app root and shinyblocks-ownedsb-*elements, so native Shiny controls, bslib content, and htmlwidgets retain host typography, spacing, borders, media display, and focus treatment in content slots (issue #116).Alert-dialog focus trapping now uses the same live focusable-element utility as dialog. Native and dynamically rendered inputs, selects, and textareas participate in both tab directions, while disabled, hidden, inert,
aria-hidden, and negative-tabindex controls are skipped (issue #115).block_button()now keeps constructor style in the same controlled channel asupdate_block_button(), so initial style can be replaced or cleared. Runtime-owned button attributes win over passthrough collisions whiletitle,name, ARIA, and custom data attributes remain supported (issue #114).Dialogs and alert dialogs now share a LIFO modal manager. In mixed or nested modal stacks, only the top modal handles Escape and traps focus, body scrolling stays locked until the final modal closes, layers follow stack order, and dynamic UI removal safely releases its modal entry (issue #113).
Portaled components now render into a portal owned by their originating runtime mount instead of the first document-global portal. Selects, comboboxes, dialogs, popovers, tooltips, menus, date pickers, and toasters therefore retain the correct scoped theme/style in multi-region dashboards, and dynamic mount removal removes its portal automatically (issue #112).
block_image_output()andblock_plot_output()now apply aspect ratio, fit, border, radius, caption, and fill sizing when embedded directly influidPage(), bslib, or another host layout; their CSS is rooted narrowly at.sb-output-frameand does not target unrelated Shiny outputs (issue #111).R-side composition components such as cards, tabs, accordions, breadcrumbs, fields, input groups, navigation, and layout primitives now establish an isolated standalone token scope, so their default theme and style-profile declarations work inside
fluidPage(), bslib, and other host layouts without requiringblock_page()or styling unrelated host markup (issue #110).block_date_picker()andblock_date_range_picker()now formatPOSIXct/POSIXltvalues in the value's own timezone. Previously they routed throughas.Date(), which assumes UTC and could report the wrong calendar date (e.g. a morning timestamp in a UTC+ timezone normalized to the previous day).block_field_invalid()now generates its error-message ids from an internal counter instead ofrunif(). Ids stay unique when the app seeds the RNG (previously two fields built after the sameset.seed()shared an id, breakingaria-describedbywiring), and building UI no longer consumes from the user's RNG stream.update_block_*()helpers no longer fail when called through a...-forwarding wrapper (function(...) update_block_input(...)), a common pattern in module helper code.Runtime mount ids are now collision-free across input ids that sanitize to the same slug (e.g.
"a.b"vs"a-b"); a short hash disambiguates them, so server updates always reach the right element. Ids without special characters are unchanged.block_select()andupdate_block_select()now reject grouped (nested)choiceswith a clear error instead of silently flattening them into mangled"Group.label"labels. Grouped choices were never supported; previously they produced wrong output without warning.Choice-style arguments are matched exactly everywhere: passing a partial vector of otherwise-valid values (e.g.
size = c("lg", "sm")) or a partial string (e.g.type = "pass") is now an error instead of silently resolving to one of them. Function-signature defaults are unaffected.block_alert_dialog()adds an accessible confirm/cancel overlay with destructive confirmation styling, explicit"confirm"/"cancel"Shiny events, Escape-to-cancel, and no outside-click dismissal (#90).Server updates for
update_block_dialog(class = )andupdate_block_file_input(class = )now use the same payload key as every other component.The dependency asset-version stamp is computed once per R process instead of stat-ing every bundled asset file on each
block_*()call. Setoptions(shinyblocks.asset_version_cache = FALSE)to recompute per call during asset development.
0.0.0.9003
Components
block_nav()now acceptsblock_nav_group()collapsible groups andblock_nav_label()section captions, letting sidebars express nested navigation while preserving the existing leaf-only Shiny input contract. Nestedblock_nav_item()values are validated for non-empty unique values in input navs (#80).block_nav()can now act as a Shiny input. Passingblock_nav(id = "page")registersinput$page; clicking ablock_nav_item("Users", value = "users")reports that value, moves the selected highlight, and no longer follows the link — so a sidebar can drive page navigation the way a Shiny tabset does, with pure-Rshiny::conditionalPanel()/renderUI()and no custom JavaScript. The newupdate_block_nav()selects an item from the server (mirroringupdate_block_tabs()), andblock_nav_item()gains avalueargument (defaulting to a string label). Without anidthe items stay plain links, unchanged. The layout showcase and docs-site playground use it for a working Dashboard/Users switch.- The standalone Nav Item playground has been retired from both the showcase and the docs site;
block_nav()/block_nav_item()are now demonstrated and documented inside the Layout shell, whose playground (showcase and docs-site) carries a matching "Toggle nav" server-update action and folds theblock_nav/block_nav_itemAPI reference into the Layout page. block_tabs(id = ...)andblock_nav(id = ...)are now backed by real ShinyInputBindings (shinyblocks.tabs/shinyblocks.nav) instead of the previousShiny.setInputValue+MutationObserver+sb:tabs/sb:navcustom-message wiring. Behaviour is unchanged for app authors — the inputs still report the selectedvalue,update_block_tabs()/update_block_nav()still select from the server (now viasendInputMessage()), andnotify = FALSEstill moves the highlight silently — but selection and keyboard handling are delegated at the document, so dynamically inserted tabsets and navs bind and re-bind throughShiny.bindAllwith no per-element rewiring. Selecting the already-active item no longer re-fires the input (change semantics, matching Shiny's own tabset input).- Fixed
block_card()spacing after its conversion to plain htmltools markup. Shared style-profile defaults now live on.sb-appas well as runtime roots, restoring card padding, region gaps, heading font, and surface shadow while preservingblock_style()overrides. - Fixed the responsive
block_sidebar()drawer so its token-backed surface and explicit shell layer remain opaque and interactive above the backdrop, form controls, and plot outputs in narrow app and embedded Shinylive viewports. Desktop collapse styling is now breakpoint-scoped, so a collapsed sidebar opened as a mobile drawer restores its title and navigation labels instead of rendering as an oversized icon-only panel. block_spinner()now supports aniconargument for choosing among loader, pinwheel, refresh, rotate, and dashed-circle spinner glyphs.block_task_button()andupdate_block_task_button()are new. A package-owned action input that locks itself synchronously on click (rejecting a same-tick double click before React reconciles), shows a busy label and decorative spinner, and reports ashinyActionButtonValuesoinput$<id>reads likeshiny::actionButton(). By default it returns to ready after the reactive flush the click triggered;auto_reset = FALSEkeeps it busy until the server releases it.update_block_task_button(state = "busy")takes manual control (an automatic reset will not release it) untilstate = "ready". The busy state is announced once via a persistent visually-hiddenrole="status"region; the spinner is decorative. The showcase and docs-site ship an interactive playground (issue #69).block_image_output()andblock_plot_output()are new. They add shadcn-styled frames (aspect box,object-fit, border, radius, optional caption) around Shiny's two reactive raster outputs —block_image_output()wrapsshiny::imageOutput()(renderImage());block_plot_output()wrapsshiny::plotOutput()(renderPlot(), covering base graphics, ggplot2, and lattice). They are R-side composition primitives: app-author server code stays vanilla Shiny (output$id <- renderImage(...)/renderPlot(...)unchanged) and Shiny keeps owning content serving, resize/recalc, and click/hover/brush coordinates.filldefaults match each underlying Shiny output (FALSEfor image,!inlinefor plot). The image's accessible name (alt) remains server-controlled viarenderImage(alt=)/renderPlot(alt=). The showcase and docs-site ship a full interactive playground (Content/State/Styling controls, liverenderPlot()/renderImage()demos, and an API table) (issue #67).- Added
block_stack(),block_cluster(), andblock_grid()as package-owned R layout primitives with semantic spacing, alignment, wrapping, and responsive grid composition (issue #72).block_grid(min_width =)is strictly validated as a single non-negative CSS length or percentage — it rejectscalc(), CSS-wide keywords, and any value containing extra declarations, closing the inline-style injection vector thatvalidateCssUnit()left open — and the grid lets its items shrink to their track so wide or unbreakable children no longer overflow on narrow viewports.
0.0.0.9002
Highlights
- shinyblocks is now safer to embed in existing Shiny and bslib apps: package resets, design tokens, and theme storage are scoped to shinyblocks instead of the whole page (issue #40).
- The showcase is no longer included in the built package tarball.
run_showcase()remains available from a source checkout and now explains clearly when showcase assets are absent. - The docs site and local showcase now share more consistent interactive playgrounds, including better light/dark behavior in embedded Shinylive examples (issue #21, issue #32).
Components
block_alert()gained anactionslot viablock_alert_action(), plussuccess,warning, andinfovariants backed by semantic theme tokens (issue #32, issue #36).block_button()can now be updated from the server withupdate_block_button(), including label, variant, size, icon, disabled state, style, and class.block_code()gained line numbers, copy-to-clipboard support, an optional editor-style header, and broader syntax highlighting for HTML/XML/SVG, CSS/SCSS/Less, JSON, SQL, shell, R, Python, and JavaScript/TypeScript (issue #32).block_date_picker()andupdate_block_date_picker()are new. A package-owned date input whoseinput$<id>is a length-1Date, matchingshiny::dateInput(), but rendered as a shadcn-style trigger button + portaled calendar instead of wrapping the native widget. The calendar is hand-rolled (noreact-day-picker/date-fns) with month navigation, keyboard movement (arrows / PageUp / PageDown / Enter / Escape), andmin/maxday disabling. AcceptsDate/POSIX/"yyyy-mm-dd"values,min/maxbounds,weekstart, displayformat, and disabled/invalid states;value = NULLstarts empty. The updater supports aclear = TRUEsentinel and clearablemin/max. The trigger and portaled calendar are token-driven for full light/dark andblock_theme()parity (selected day uses--primary, today and hover use--accent). The showcase and docs-site ship a full interactive playground (Content/State/Actions/Styling controls, liveinput$value, and an API table) (issue #59).block_date_range_picker()andupdate_block_date_range_picker()are new. A package-owned range date input whoseinput$<id>is a length-2Datec(start, end), matchingshiny::dateRangeInput(), transported over the sameshiny.datebinding (an empty or incomplete range reportsNULL). AcceptsDate/POSIX/"yyyy-mm-dd"endpoints,min/maxbounds, a displayseparator,weekstart, displayformat, and disabled/invalid states;start = NULL/end = NULLstarts empty and a reversedstart/endis silently ordered. The updater supports independentstart/endchanges, aclear = TRUEsentinel, and clearablemin/max. The trigger and portaled range calendar are token-driven for full light/dark andblock_theme()parity (the committed range uses--primary/--accent, the in-progress hover band uses a reduced-emphasis tint). The showcase and docs-site ship a full interactive playground (Content/State/Actions/Styling controls, liveinput$value, and an API table) (issue #61).block_file_input()is new. It uses Shiny's native upload binding, supportsmultiple,accept, disabled/invalid states, custom button text, placeholder text, upload progress, and an optional drag-and-dropvariant = "dropzone"(issue #54, issue #56).update_block_file_input()is new. It can update file-input labels, variant, dropzone content, accept/multiple settings, disabled/invalid state, style, class, and can clear the current selection withreset = TRUE(issue #54, issue #56).block_icon()gained semanticcolorchoices and a realsizeargument ("sm","default","lg", and"xl") instead of relying on ad hoc inline dimensions (issue #32).block_progress(),update_block_progress(), andinc_block_progress()are new. An embedded, shadcn-style progress bar that renders inline where it is placed rather than as a ShinyProgressnotification panel. It is display-only and server-addressable (addressed byid, with no meaningfulinput$<id>value):update_block_progress()sets any field (omitted args preserve the client value,NULLclears text fields) andinc_block_progress()applies a signed, saturating increment. Supports a distinctlabel/message/detailtext model,show_valuepercent, amin/maxscale, anindeterminatesweep, anddefault/success/warning/info/destructivevariants. The runtime owns merged-state validity (clamps into the merged[min, max]and repairs an inverted range), the determinate fill animation and the indeterminate sweep both honorprefers-reduced-motion, and the bar is token-driven for full light/dark andblock_theme()parity. The showcase and docs-site ship a full interactive playground with Content/State/Actions/Styling controls and an API table (issue #62).block_select()gainedmultiple = TRUE, matching Shiny'sselectInput(multiple = TRUE)value semantics (input$<id>is a length-1 string in single mode, a character vector in multiple mode). The trigger shows selected items as wrapping removable chips and the popup is a checkable, multi-selectable listbox that stays open on toggle, with an optionalmax_itemscap andcharacter(0)defaults. Themax_itemscap is enforced everywhere: an over-cap initialselectedis an error and any later over-cap serverselectedis clamped. A vectorselectedreaching a single select collapses to its first element (never"a,b"). For assistive tech,aria-activedescendantlives on the focused combobox trigger. Chips and the multi trigger are fully token-driven for light/dark andblock_theme()parity (issue #64).block_select()dropdowns now measure and clamp to the available viewport, fixing clipped options in short embedded playgrounds and under roomier style profiles.block_slider()gained vertical orientation, optional current-value and min/max labels, improved drag update throttling, and better shrink-wrapped layout behavior (issue #22, issue #25).block_spinner()now supports the same semantic foreground colors asblock_icon().block_switch()gainedsize = c("default", "sm", "lg")support in bothblock_switch()andupdate_block_switch().block_table()is new. It renders data frames with column formatting, captions, alignment, width overrides,NAhandling, max-row truncation notes, reactive server updates viaupdate_block_table(), optional row selection, and theme-safe styling intents (issue #49, issue #51, issue #53).block_textarea()andblock_input()now report their initial server value correctly even before React has finished mounting.block_toaster(),show_toast(),dismiss_toast(), andupdate_block_toaster()are new. Mount ablock_toaster()and fire transient, auto-dismissing toast notifications from the server withshow_toast(). Toasts stack, pause on hover/focus, supportEscape-to-dismiss, and reuse theblock_alert()variants (default,destructive,success,warning,info) and icon system.update_block_toaster()moves the region to a new position without re-mounting it, andinput$<id>reports the latest toast lifecycle event as a list{action, id, seq}.block_tooltip()is new. It supports hover/focus activation, configurable side and alignment, delayed opening, Escape-to-close, and portal rendering.block_value_box()gainedvariant = c("default", "accent", "destructive")for token-backed metric emphasis.
Theming
block_theme()gained scoped overrides, dark-mode-specific token values, and built-in semantic color presets:neutral,stone,zinc,mauve,olive,mist, andtaupe(issue #32).block_style()is new, withblock_page(style = )support for visual style profiles that control sizing, spacing, surfaces, radius, elevation, focus rings, and disabled treatment separately from semantic colors (issue #33).block_style_profiles()now reflects the official shadcn/ui v4 style registry:default,luma,lyra,maia,mira,nova,rhea,sera, andvega(issue #48).block_style("luma"),block_style("lyra"), andblock_style("rhea")now have broader component and shell coverage, including tables, form controls, tabs, navigation, sidebars, fields, and input groups (issue #33, issue #36).
Showcase and docs
- Added or refreshed interactive docs-site playgrounds for Alert, Card, Code, Empty, File Input, Input Group, Radio Group, Skeleton, Slider, Spinner, Switch, Tabs, Table, and Value Box pages (issue #21).
- Component specs were refreshed across the runtime components to describe the shipped R APIs, update contracts, accessibility behavior, and known divergences from shadcn/ui.
- The old dev-only Quarto gallery and copied vignette articles were removed; the custom docs site is now the maintained component gallery.
Developer infrastructure
- Runtime input updates now use a shared
sendInputMessage()path, stricter component-name validation, synchronous DOM value writes, and less duplicated Shiny binding boilerplate (issue #23, issue #24, issue #26, issue #27, issue #28, issue #30, issue #41). - The theme and style-profile test harnesses now check token usage, light/dark theme response, palette coverage, style-profile parity, and style-profile leanness so future components are less likely to drift (issue #32, issue #33, issue #34).
- Runtime CSS/JS, showcase smoke tests, parity tests, spell checks, and package checks were tightened so generated assets and browser behavior are easier to verify before release.
update_block_*()helpers now deliver server-driven updates correctly inside amoduleServer(). Previously the runtime mount id (which already carries the module namespace) was re-namespaced by the module session, double-prefixing the target so updates were silently dropped; updates now route through the root session. A new module-routing regression test and a runtime-shiny smoke assertion guard it (issue #63).
0.0.0.9000
- Architecture pivot: ADR 0017 adopts the full runtime shadcn port. Future component work moves to an R-facing adapter over package-local shadcn/Radix runtime assets, with scoped CSS, Shiny input/update semantics, per-component showcase pages, cleanup gates, and bundle-size reporting. The earlier native-CSS and wrap-by-default input decisions remain historical migration scaffolding.
- Added the first runtime foundation slice: package-local runtime CSS/JS assets, internal R payload and update-message helpers, scoped mount markup, runtime build targets, a browser smoke-test scaffold, and tests for dependency attachment and Shiny updater semantics.
- Added Phase 1 runtime asset guardrails: runtime CSS isolation tests block unscoped selectors and host-framework selectors, and
tools/budget.Rnow reports runtime JS/CSS raw and gzip sizes separately from legacy compatibility assets. - Added Phase 1 runtime lifecycle guardrails:
block_page()now includes a package-owned portal root, runtime mounts without Shiny input ids receive unique ids, and static runtime JS tests cover the Shiny bridge, dynamic UI hooks, child binding hooks, and portal setup. - Added a Shiny-backed runtime browser smoke fixture that verifies runtime input initialization, server updates, disabled state, dynamic
renderUI()removal/reinsertion, and Shiny children inside runtime mounts. - Expanded the Shiny-backed runtime browser smoke fixture to cover explicit
insertUI()/removeUI()and id reuse. - Expanded the runtime input-update browser fixture to cover clear/reset updates, enable-after-disable, stale update rejection, and Shiny module namespacing.
- Added browser-backed runtime collision coverage for host Bootstrap-style selectors, Selectize-style selectors, bslib card markup, DT tables, plotly-style htmlwidget hosts, htmlwidget children inside runtime mounts, and pre-existing portal-root content.
- Switched the runtime JavaScript foundation from concatenated vanilla browser scripts to a Vite-built React/ReactDOM bundle, with React-owned mount slots kept separate from Shiny child slots so Shiny outputs and htmlwidgets continue to bind inside runtime components.
- Added scoped runtime token defaults on
[data-shinyblocks-root]and[data-shinyblocks-portal-root], plus static checks that the runtime asset does not write theme tokens to:root. - Added hard Phase 1 runtime asset budgets for the Vite-built runtime bundle and scoped runtime CSS, recorded in ADR 0017 and enforced by
tools/budget.R. block_badge()andblock_button()now render through the package-local React runtime while preserving their R-facing variant, size, icon, custom class, disabled, and passthrough-attribute contracts. Native badge CSS was removed from the legacy stylesheet; native button CSS remains temporarily forblock_dark_mode_toggle().block_select()now renders a package-local shadcn-style overlay backed by a hidden native<select>and a component-specific Shiny input binding.update_block_select()now routes throughsendInputMessage()and can update value, choices, placeholder, disabled state, styling metadata, and optional Shiny notification.block_separator()now renders through the package-local runtime while preserving horizontal/vertical orientation and decorative/semantic ARIA behavior.block_spinner()now renders through the package-local runtime while preserving therole="status"and accessible label contract.block_skeleton()now renders through the package-local runtime while preserving passthrough attributes and normalized runtime inline-style handling.block_empty()now renders through the package-local runtime while preserving icon, description, extra content, and action composition.block_value_box()now renders through the package-local runtime while preserving title, value, description, icon, and extra-content composition.block_alert()now renders through the package-local runtime while preserving role, variant, icon, title, description, and extra-content composition.block_card()now renders through the package-local runtime. Card body flows through[data-shinyblocks-children]so nested Shiny outputs, htmlwidgets, and inputs bind without a serialized HTML slot. Legacy.sb-card*Tailwind rules have been removed frominst/www/src/shinyblocks.css; runtime CSS now owns card styling under[data-shinyblocks-root].- Phase 4.1 skeleton: introduced
block_dialog(). A modal portal-rendered through[data-shinyblocks-portal-root]with title, description, and serialized body. Initial open state only — no Shiny input binding, no trigger, no escape/outside-click, no focus management. - Phase 4.2:
block_dialog()becomes a Shiny input.input$<id>reports the open/closed state as a boolean and updates when the user clicks the trigger, overlay, or close button. Newupdate_block_dialog()updater changes open state, title, and description from the server with explicitnotifysemantics. The dialog ships its ownShiny.InputBinding(shinyblocks.dialog) and uses the same mount-id-based message routing asblock_select(). - Phase 5.1 skeleton: introduced
block_popover(). A non-modal, portal-rendered popover anchored to a trigger button via fixed positioning against the trigger'sgetBoundingClientRect(). Configurableside(bottom/top/left/right) andalign(center/start/end). Initial open state only — no Shiny input binding, no outside-click, no escape, no focus management. These arrive in later sub-phases. - Phase 5.4 checkbox runtime migration:
block_checkbox()now renders through the package runtime (component = "checkbox") with a hidden native checkbox input, a dedicatedshinyblocks.checkboxinput binding, and browser smoke coverage for user toggles and native-input synchronization. - Phase 5.4 checkbox parity hardening: runtime checkbox markup and scoped CSS now align with the current shadcn checkbox visual contract (single indicator box, compact control geometry, and state styling), while explicitly neutralizing legacy checkbox pseudo-element styles inside runtime mounts.
- Phase 5.5 switch runtime migration:
block_switch()now renders through the package runtime (component = "switch") with a hidden native checkbox input, a dedicatedshinyblocks.switchinput binding, and a newupdate_block_switch()helper for checked/disabled/style/class server updates with optional notify semantics. - Phase 5.9 radio group: introduced
block_radio_group()andupdate_block_radio_group(). Shadcn-style radio group rendered through the package runtime (component = "radio-group") with a hidden native<input type="hidden">for form-submission, a dedicatedshinyblocks.radio-groupinput binding, full arrow-key navigation (Up/Down/Left/Right),orientationarg ("vertical"/"horizontal"),invalidflag, and a server updater covering selected/choices/disabled/invalid/orientation/style/class with optional notify semantics. Scoped.sb-radio-group-controlstyles under[data-shinyblocks-root]follow the shadcn radio contract. - Phase 5.8 text input: introduced
block_input()andupdate_block_input(). Single-line text input rendered through the package runtime (component = "input") with a hidden native<input>, a dedicatedshinyblocks.inputbinding (debounced 250 ms),typeargument supportingtext/password/email/url/tel/search/number,invalidflag, and full server updater for value/placeholder/type/disabled/invalid/style/class with optional notify semantics. Scoped.sb-input-controlstyles under[data-shinyblocks-root]mirror the shadcn input contract. Showcase ships a full Content/State/Actions/Styling playground. - Phase 5.7 textarea runtime migration:
block_textarea()now renders through the package runtime (component = "textarea") with a hidden native<textarea>, a dedicatedshinyblocks.textareainput binding (debounced 250 ms), a newinvalidargument that setsaria-invalid="true", and a newupdate_block_textarea()helper for value/placeholder/rows/disabled/invalid/style/class server updates with optional notify semantics. Legacy.sb-textarearules removed frominst/www/src/shinyblocks.css; runtime CSS owns textarea styling under[data-shinyblocks-root]. Server-drivenvalue/rowsupdates clear any drag-resized inlinestyle.heightso server changes are visually authoritative; the showcase Clear button snaps the preview back to whatever the controls panel currently states. - Phase 5.6 popover/checkbox/switch cleanup: removed the dead pre-runtime
boolean_control_input()helper and the legacy.sb-checkbox-shell/.sb-checkbox-control/.sb-checkbox-indicator/.sb-switch-shell/.sb-switch-control/.sb-switch-trackrules frominst/www/src/shinyblocks.cssnow that all three controls render through the scoped runtime under[data-shinyblocks-root]. Component specs harmonized to the same status header. - Phase 4.4:
block_dialog()gains asizeargument ("sm","default","lg","xl") that adjusts the content max-width, plus afooterslot that renders below the body in a right-aligned, wrapping flex row.update_block_dialog()learns matchingsizeandfooterarguments so server code can resize the dialog or swap action rows without a re-render. Passingfooter = NULLto the updater clears an existing footer. - Phase 4.3:
block_dialog()is now keyboard- and screen-reader-ready. Escape closes the dialog, Tab/Shift+Tab cycle focus inside the content, focus is moved to the first focusable element on open and returned to whichever element held it before open. Body scroll locks while open with scrollbar-width padding compensation to avoid layout shift. The content carriesrole="dialog",aria-modal="true", andaria-labelledby/aria-describedbywired to the title and description slots. Newhide_title = TRUEargument hides the title visually while keeping it as the accessible name, and the trigger button advertisesaria-haspopup="dialog"plus a livearia-expanded. - Added the initial ADR 0016 visual-parity harness scaffold: a
dev-only
parity/React reference app, shared Playwright capture/diff scripts, parity make targets, and the first committed computed-style baseline forbutton. - Internal:
make paritynow acceptsCOMPONENT=<name>, andmake parity-ciiterates over every component currently registered intools/parity/registry.mjsinstead of pretending the button-only baseline covers the whole package. Phase-exit docs and theshinyblocks-componentskill now describe the actual shared-harness workflow (parity/src/main.js, registered components, manual spec + screenshot backstop for everything not yet migrated). block_select()is now migrated into the shared ADR 0016 parity registry. The dev-only React reference app exposes a local select trigger in closed and open states, the select parity baseline is committed as the baseline, andmake parity-cinow verifies bothbuttonandselect. The selectize wrapper CSS was tightened at the same time so the visible trigger now matches the shadcn contract more closely:shadow-xsat rest,justify-between+gap-2, zero vertical padding inside the fixed 32px shell, and a token-driven open ring that suppresses Selectize's default blue focus shadow.block_slider()is now migrated into the shared ADR 0016 parity registry as the first multi-role component. The shared harness now captures and diffsroot,rail,range, andthumbroles across light/dark, default/hover/disabled states, with extra live checks for thumb-vs-rail centering and hidden ion.rangeSlider labels. The local baseline lives at the slider parity baseline, and the disabled thumb cursor is now forced tonot-allowedwith!importantso the live widget matches the parity contract.block_checkbox()is now migrated into the shared ADR 0016 parity registry. The shared harness captures unchecked, checked, and disabled states across light/dark mode using the visible checkbox shell plus label-text opacity, with a new disabled checkbox fixture in the field showcase to anchor the live comparison. The baseline lives at the checkbox parity baseline.block_switch()is now migrated into the shared ADR 0016 parity registry. The shared harness captures off, on, and disabled states across light/dark mode using the visible switch track plus label-text opacity, with a new disabled switch fixture in the field showcase to anchor the live comparison. The baseline lives at the switch parity baseline.block_textarea()is now migrated into the shared ADR 0016 parity registry. The shared harness captures default, focus-visible, disabled, and invalid textarea states across light/dark mode, with stable field-showcase fixtures and a new baseline at the textarea parity baseline. The shared text-control CSS was tightened at the same time so text inputs keepshadow-xsunder focus/invalid rings, disabled controls get the shadcnnot-allowed+opacity-50contract, andblock_textarea()defaults to the 2-row shadcn baseline instead of a taller native shell.- New agent skill
shinyblocks-componentlands at the local shinyblocks-component agent skill (the tracked canonical copy), with amake skills-installtarget that mirrors it into.claude/skills/shinyblocks-component/SKILL.mdand.agents/skills/shinyblocks-component/SKILL.mdso Claude Code and Codex pick it up locally. End-to-end recipe for adding (or refactoring) ablock_*()component: per-gate sync rule (R + CSS- showcase + tests + spec + pkgdown + NEWS), shadcn-fidelity
workflow against the
apps/v4/registry/new-york-v4source, the parity-harness template (including the positioning + cross-element bounding-rect checks the 2026-05-11 slider POC surfaced as necessary), common-pitfall list, and pre-commit checklist. Triggered by user prompts like "add a component", "port shadcn X to shinyblocks", or "wrap shiny::Y as a block". the internal roadmap Current Status and the Phase 5 hand-off doc both point at the skill as the canonical workflow.make setupnow runsskills-installautomatically so a fresh checkout has the skill registered after one command.
- showcase + tests + spec + pkgdown + NEWS), shadcn-fidelity
workflow against the
tools/parity/slider-poc.mjsgrew positioning properties (position,top,marginTop,transform), agetBoundingClientRect()cross-check asserting the slider thumb is vertically centred on the rail (delta ≤ 1.5px), and a dark/ light theme toggle that captures all three roles (rail, range, thumb) in both modes. The original property-only diff had missed an off-centre thumb that visibly floated above the rail; the geometry assertion now catches it.block_slider()CSS hardened to match shadcn's contract under any container height:.sb-slider .irs--shiny { height: 1.5rem !important; }, inner.irswrapper expanded to fill viaheight: 100% !important, and every role centred withtop: 50% !important; transform: translateY(-50%) !important;so ion.rangeSlider's inlinetoprewrites cannot offset them again.- Visual-parity harness adopted per
ADR 0016. Mechanical
computed-style + DOM diff against a pinned shadcn-react reference
replaces reviewer-only parity for the bulk of drift. An initial
proof of concept validated the approach against
block_select()— caught 18 trigger drifts and a double-hover dropdown bug that the spec-doc review missed. A follow-up CSS fix on the same select reduced trigger drift from 18 to 7 properties (the remaining ones are colour-space normalisation, expected state mismatches, and structural divergences by design), and eliminated the double-hover (dropdown now lights exactly one row on mouse hover instead of two). The full harness is Slice 6 in the internal Phase 5 hand-off plan. block_select()trigger and dropdown now match shadcn more closely:rounded-lg(wasrounded-md— 10px vs 4px), transparent background (was solid), 32px height (was 34px), absolutely- positioned 16px chevron with the Lucidechevron-downmask, and a selected option that usesfont-weight: 500instead of a fill so it no longer competes with the keyboard-cursor / pointer-hover state.- Phase 5 hand-off plan landed at
the internal Phase 5 hand-off plan:
six slice-sized implementation steps (focus-visible redesign,
aria-invalidcross-cut, tabs refactor, reference screenshots, spec backfill, gallery resumption) with files to edit, concrete CSS patterns, tests to update, and definition-of-done per slice. The next implementer can pick up Phase 5 finish from there without rereading the conversation history. - First shadcn-fidelity audit pass per
the internal shadcn-fidelity audit plan.
Compared against the canonical
apps/v4/registry/new-york-v4source, three classes of drift were fixed ininst/www/src/shinyblocks.css: badges now userounded-full+text-xs(the current shadcn pill shape, not the earlier rounded-rectangle),block_button(variant = "link")picks uptext-primary,block_button(variant = "outline")carriesshadow-xs, and destructive button + badge variants usetext-whitewith a[data-theme="dark"]60%-opacity dim that matches shadcn's dark-mode treatment. Three cross-cutting drifts (focus-ring redesign to the shadcn 3px ring,aria-invalidstyling, tabs data-attribute refactor) are queued as the next Phase 5 slices. - Visual-parity contract per ADR 0015:
every exported
block_*()ships with a short component spec docs capturing the shadcn reference link, visual states, token contract, deliberate divergences, and a reference screenshot.tests/testthat/test-doc-coverage.Renforces the rule; existing components are backfilled incrementally via a shrinkingbackfill_pending_specsallowlist. Quality Gate item 15 walks every state listed in the spec against the live showcase during phase exit. Seed specs land forblock_button()andblock_card(); the template lives at the component spec template. - Initial R package scaffold with exported Shiny/htmltools helpers for pages, sidebars, headers, navigation items, cards, and buttons.
block_card()now ships with styling for its title, value, and body slots so it renders as a tokenised surface instead of an unstyled<article>.block_nav_item()advertises itself as anav-itemchild viadata-sb-child, so a futureblock_nav()parent can validate its contents.- Documentation gains a Quarto + Shinylive component gallery
(
gallery/components/) modelled on https://shiny.posit.co/r/components/. Each exported component has a page with an embedded live demo and visible source. See ADR 0013. - The dogfooded showcase under
inst/showcase/is now a proper component gallery — its own UI is built entirely with shinyblocks primitives, the sidebar filters one component at a time, and every section is deep-linkable via the URL hash.test-showcase.Renforces an authoring contract: any new exportedblock_*()must land with a matching example file underinst/showcase/R/examples/and a row in the showcase's sections list, or the test suite fails. block_card()gains composition primitives —block_card_header(),block_card_title(),block_card_description(),block_card_content(),block_card_footer()— alongside the flat-argument convenience form. Pre-built region tags are reused viadata-sb-childmarkers; bare strings/tags are wrapped automatically.block_field(),block_field_group(),block_field_invalid(),block_field_label(),block_field_description(),block_field_set(),block_field_legend(),block_input_group(), andblock_input_group_addon()add Phase 5 form wrappers around standard Shiny inputs, including helper text, addons, fieldset composition, and invalid-state markup.block_select()now follows the new wrap-by-default input policy: it is a thin wrapper around Shiny's select/selectize path with token-driven Selectize styling, instead of a package-owned select runtime.block_tab()andblock_tabs()add shadcn-style tab triggers and content styling on top of Shiny's existing tabset binding, preserving reactive tab switching without a custom input runtime.block_theme(),block_dark_mode_toggle(), andupdate_block_theme()add page-scoped token overrides, a persistent dark-mode toggle, and a server-side theme mode updater.block_tabs()now ships local tab activation behavior and hides inactive panels without relying on Bootstrap tab runtime.- Tabs trigger sizing, spacing, and showcase content now track the shadcn tabs contract more closely.
block_textarea(),block_checkbox(), andblock_switch()add first-class wrapped Shiny form controls to the forms layer.- Focus-visible styling now lives on the component bases themselves
instead of a global
.sb-app *:focus-visibleoutline fallback. - Interactive controls now honour
aria-invalid="true"with a destructive ring on the visible shell, including wrapped select, textarea, checkbox, and switch inputs. - Tabs now use a shadcn-style data-attribute contract
(
data-state,data-orientation,data-variant) and include the line variant in the showcase. - Component spec backfill now covers alert, alert-title, alert-description, input-group, input-group-addon, and select.
- The showcase theme example is now locally scoped and no longer
changes the global primary button colour;
block_select()also gets a stronger native-select fallback shell for dark mode. block_select()now applies stronger dark-mode styling to the Selectize-enhanced trigger itself, and hides the fallback chevron once Selectize has taken over the control.- Component spec backfill now covers the field family:
block_field_group(),block_field(),block_field_label(),block_field_description(),block_field_set(),block_field_legend(), andblock_field_invalid(). - Component spec backfill now also covers the layout shell:
block_page(),block_body(),block_header(), andblock_sidebar(). - Component spec backfill now also covers navigation and utility
primitives:
block_icon(),block_nav(),block_nav_item(),block_separator(),block_skeleton(),block_spinner(),block_empty(), andblock_value_box(). - Component spec backfill is now complete for every exported
block_*();test-doc-coverage.Renforces the spec-doc contract without a temporary allowlist. - Internal: the asset budget gate now enforces CSS on gzipped delivery
size (
≤10 KB) instead of a stale raw/minified threshold. - Internal: added
tools/spec-screenshots.Randmake spec-screenshotsto report which component-spec screenshots are still missing. - Internal: added
make spec-screenshots-mdto generate a committed screenshot queue. - Internal: added
make spec-screenshots-checkso stale screenshot queue docs fail fast. - Internal:
make gateand the phase-exit checklist now include the screenshot-queue freshness check. - Internal: added
make spec-screenshots-seed, a macOS/Safari helper for first-pass capture of the seed screenshot set (button,card,select,tabs). - Internal: the component-spec screenshot queue is now fully populated
(
40captured-dated references), and the Safari helper has been generalized tomake spec-screenshots-high-riskandmake spec-screenshots-allfor repeatable refreshes. - Internal: added
make showcase-capture/tools/capture-showcase-parity.shso local light/dark showcase sections can be captured reproducibly during the parity pass. - New components round out Phase 3:
block_value_box()for high-signal metrics,block_separator()(horizontal + vertical, ARIA-aware),block_skeleton()for loading placeholders,block_spinner()withrole="status", andblock_empty()with optional icon, description, and action slots. - Internal: deduplicated
as_alert_child()andas_component_child()into a single helper; both call sites now share the same code path. tests/testthat/test-doc-coverage.Renforces a per-gate sync rule: every exportedblock_*()must appear in_pkgdown.yml'sreference:section (active) and have a gallery.qmdpage undergallery/components/(currentlyskip()'d pending the WASM resolution per ADR 0013). The ROADMAP grows a §Per-gate component-sync rule section spelling out which artifacts must land in the same commit as a new component.- The component gallery moves from
vignettes/articles/to top-levelgallery/. pkgdown 2.x requires Quarto 1.5+ to render.qmdfiles insidevignettes/, and the project pins Quarto 1.4 today — relocating sidesteps the version requirement until the gallery's WASM blocker (ADR 0013) is resolved.make galleryand the shinylive Quarto extension move with the directory. - New
make verifyandmake verify-stoptargets.verifybuilds pkgdown, launches the showcase on:4321and the pkgdown site on:4322in the background, HTTP-checks both for200, and leaves them running so the maintainer can eyeball them.make gatenow ends withverify, so a phase exit cannot tag without both servers responding. Quality Gate item 14 documents this rule. - Added an interactive Shinylive playground to the docs site for the Theme component page (issue #21), and aligned the local Theme showcase tab with the same unboxed controls and preview layout.
- Added an interactive Shinylive playground to the docs site for the Nav Item component page (issue #21), and aligned the local Nav Item showcase tab with the same unboxed controls and preview layout.
- Added an interactive Shinylive playground to the docs site for the Layout component page (issue #21), and aligned the local Layout showcase tab with the same unboxed controls and preview layout.
- Added an interactive Shinylive playground to the docs site for the Tooltip component page (issue #21), and aligned the local Tooltip showcase tab with the same unboxed controls and preview layout.
- Added an interactive Shinylive playground to the docs site for the Popover component page (issue #21), and aligned the local Popover showcase tab with the same unboxed controls and preview layout.
- Added an interactive Shinylive playground to the docs site for the Dialog component page (issue #21), and aligned the local Dialog showcase tab with the same unboxed controls and preview layout.
- Replaced the docs home page's static Gallery preview with a live Shinylive dashboard playground composed from shinyblocks inputs, cards, tabs, alerts, and reactive server actions (issue #21).