Tailwind & shadcn UI Architecture
Current architecture and implementation guidance for Tailwind CSS v4, shadcn UI, and Radix UI in Waldur Homeport. Covers framework coexistence, design tokens, component standards, styling conventions, linting guardrails, and testing.
[!NOTE] Status: every button in the app is rendered by
BaseButton(or bybuttonVariants()on an element that cannot be aBaseButton), and the legacy Bootstrap/Metronic.btnCSS has been deleted. Every menu and menu-like popover is built fromwaldur-ui'sMenu/MenuPopover. Every checkbox, radio and switch is awaldur-uiCheckbox/Radio/Switch, and the Bootstrap/Metronic.form-checkCSS has been deleted. The remaining Bootstrap-backed UI is layout, forms, tables, modals and the shells listed under Component Systems Overview. New UI is built onwaldur-ui.
Table of Contents
- UI Architecture & Framework Coexistence
- Component Systems Overview
- Component Standards & Usage Guide
- Cascade Layers
- The
!important& Class-Name Collision Rules - Root Font-Size & Fractional Rem Scaling
- The Preflight Reset Shim
- Grid Breakpoint Synchronization
- Design Tokens & Color Systems
- Brand Color Token Bridge
- Dark Mode Mechanics
- Component Architecture & Guidelines
- AlertItem
- Badge
- Tooltip
- Popover
- Checkbox, Radio & Switch
- Progress
- Buttons
- Sidebar & Mobile Sheet
- Content Drawer (
#kt_drawer) - Dropdown & Menu System Map
- ActionsMenu & ActionItem
- Nav menus
- Linting & Guardrails
- Restricted Imports
- Custom ESLint Rules Matrix
- Storybook & Testing Toolchain
- Storybook Environment & Vitest
- Visual E2E Specs
- Testing Gotchas & Pitfalls
UI Architecture & Framework Coexistence
Component Systems Overview
Waldur Homeport operates on two active UI component patterns:
- Modern Tailwind / Radix Primitives (
packages/ui, exported aswaldur-ui) - Built with pure Tailwind v4 utilities and CSS design tokens from
packages/design-tokens. - Free of all Bootstrap and Metronic classes, SCSS variables, and runtime mixins.
- Includes
BaseButton,SegmentedControl,AlertItem,Badge,Tooltip,Popover,Sidebar,Sheet,Dialog,Menu,MenuPopover,Checkbox,Radio,Switch,RadioGroup,Progress,Tag,Card,Avatar,Toast,FeaturedIcon,StatCard,CopyButton,LoadingSpinner,Accordion,AccordionCard,Collapsible, andTablePagination(withPaginationandPageSizeSelect). -
Exports
buttonVariants(),ButtonVariantandButtonSizeso elements that cannot be aBaseButton(links, Radix triggers that need a specific child shape) still get the exact same classes. -
Transitional Shells (Radix Engine with Themed Skins)
- Used where extensive surface area requires maintaining existing container styling while leveraging accessible Radix primitives:
#kt_drawer(DrawerRoot.tsx): RadixDialogdriving slide-over content panels.
- These still wear Bootstrap/Metronic panel classes; only their buttons have moved to
waldur-ui. - The header, page-tab and footer menus, the table filter popovers and the action menus used to be shells too. They are now
waldur-ui'sMenu/MenuPopover, whose looks keep the Metronic and Bootstrap originals; see Dropdown & Menu System Map.
Component Standards & Usage Guide
| UI Element | Standard Component | Package | Usage & Styling Notes | Prohibited |
|---|---|---|---|---|
| Alert / Banner | AlertItem |
waldur-ui |
Pure Tailwind, --border-secondary, 5 variants |
react-bootstrap Alert |
| Badge / Pill | Badge |
waldur-ui |
15 variants × 3 tones, structural border-[1px] |
react-bootstrap Badge |
| Tooltip | Tooltip |
waldur-ui |
Radix Tooltip (hover/focus) + Popover (click fallback) | react-bootstrap Tooltip, OverlayTrigger |
| Popover | Popover, PopoverContent |
waldur-ui |
Radix Popover with --card-bg / --border-secondary tokens |
react-bootstrap Popover |
| Button | BaseButton |
waldur-ui |
12 variants × 3 sizes, inset box-shadow border, outline focus ring, semantic button tokens |
react-bootstrap Button, hand-written btn class markup |
| Link styled as button | Link with buttonVariant |
@/core/Link |
Same classes as BaseButton via buttonVariants(); keeps anchor semantics and routing |
<a className="btn …"> |
| Checkbox / radio | Checkbox, Radio |
waldur-ui |
Hidden native input + drawn box and SVG mark, md 20px / sm 16px, --check-* tokens; label / description / tooltip props |
react-bootstrap FormCheck / Form.Check, form-check* classes |
| Progress meter | Progress |
waldur-ui |
One value, stacked segments or indeterminate; neutral track, variant colours the fill; xs 4 / sm 6 / md 8 / lg 24px; --progress-* tokens |
react-bootstrap ProgressBar, .progress / .progress-bar markup |
| On/off toggle | Switch |
waldur-ui |
Hidden native checkbox + drawn track and knob, md 44×24 / sm 36×20, --switch-* tokens |
type="switch" FormCheck, form-switch* classes |
| Mutually exclusive choice | SegmentedControl |
waldur-ui |
Radix RadioGroup, neutral / brand variants, button sizes |
react-bootstrap ToggleButtonGroup, btn-group markup |
| Expandable panel group | Accordion |
waldur-ui |
Radix Accordion, single / multiple, open header in runtime brand colour; closed panels unmount |
react-bootstrap Accordion |
| Single expandable panel | Collapsible |
waldur-ui |
Radix Collapsible, unstyled; keepMounted keeps form fields mounted while closed |
react-bootstrap Collapse, useAccordionButton |
| Sidebar Navigation | Sidebar, Sheet |
waldur-ui |
Collapsible desktop rail + mobile Radix Sheet | Metronic sidebar JS |
| Table pagination | TablePagination |
waldur-ui |
--pagination-* tokens; bordered={false} under a non-table list; desktop/mobile layouts |
react-bootstrap Pagination, page-link markup |
| Table Actions | ActionsMenu |
@/table |
Menu in the actions look, with the kebab / labeled / Add toggles |
react-bootstrap DropdownButton |
| Header Chrome Menu | Menu |
waldur-ui |
Menu in the nav look; openOnHover="desktop" for hover triggers |
N/A |
| Slide-Over Drawer | DrawerRoot |
@/drawer |
Radix Dialog with CSS keyframe transitions | N/A |
The Prohibited column is convention; the subset that lint enforces is listed under Linting & Guardrails (ToggleButtonGroup and btn-group markup are not).
Cascade Layers
src/tailwind.css imports Tailwind's theme, preflight, and utilities chunks separately, defining an explicit layer order:
1 | |
Compiled Metronic and Bootstrap styles are wrapped in @layer bootstrap. The browser merges all occurrences into one virtual layer order:
@layer theme(Tailwind design tokens)@layer base(Tailwind preflight reset)@layer bootstrap(Bootstrap 5 & Metronic compiled styles)@layer utilities(Tailwind generated utilities)
Placing bootstrap below utilities ensures Tailwind utility classes win specificity ties against Bootstrap rules, while remaining above base so preflight resets do not clobber core layout rules.
The Unlayered SCSS Specificity Trap
Unlayered CSS always beats layered CSS, regardless of selector specificity.
Vite injects component-level stylesheets (import './Foo.scss') as unlayered styles into <head>.
Consequently, any selector in a component SCSS file will override styles inside @layer bootstrap.
[!IMPORTANT] Scoping Rule: Component SCSS must be strictly scoped under its own unique component class (e.g.
.my-component .title), never directly under a layout root (.aside,.header,.toolbar,.card). Nesting a layout class inside your own root is safe; styling an ancestor layout root directly in component SCSS breaks layout rules app-wide. Global layout overrides belong exclusively insrc/metronic/sass/custom/inside the bootstrap layer.
Production Layer Optimization
In production bundles, Vite's CSS minifier removes the explicit @layer theme, base, bootstrap, utilities; statement because dist/assets/index-*.css already emits the layers in that physical order. Metronic's stylesheet is imported right after tailwind.css in src/index.tsx, so it lands later in the same bundle and the layer order established by Tailwind's blocks remains authoritative.
The !important & Class-Name Collision Rules
CSS Cascade Layers do not outrank !important. Bootstrap 5's utility API emits utilities with !important by default. Because Tailwind and Bootstrap share identical utility names, collisions occur on shared class strings:
| Colliding Class | Bootstrap Rule (!important) |
Tailwind Expected Rule | Rendered on App Pages |
|---|---|---|---|
.border |
border: 1px solid var(--bs-border-color) !important; |
border-width: 1px; |
Bootstrap |
.p-1 through .p-5 |
padding: $spacers[N] !important; |
padding: calc(var(--spacing) * N); |
Bootstrap |
.gap-1 through .gap-5 |
gap: $spacers[N] !important; |
gap: calc(var(--spacing) * N); |
Bootstrap |
No colour class collides any more: Bootstrap emits no text-/bg-/border- colour utility at
all (nor .border-top/-bottom/-start/-end, which set a colour), so every colour class is
Tailwind's, follows the design tokens and takes dark:/hover: variants. Bootstrap's old names
(text-danger, bg-light, …) paint nothing; src/tailwindBootstrapColors.test.ts keeps it that
way. See
design-tokens.md § Bootstrap colour utilities.
Spacing Anomaly
Bootstrap defines $spacers: (0: 0, 1: $spacer * .25, 2: $spacer * .5, 3: $spacer * .75, 4: $spacer * 0.154 * 6, 5: $spacer * 1.25).
At Metronic's forced root font size of 13px (and 12px on mobile):
p-1,p-2,p-3,p-5render at0.25rem × N(3.25px,6.5px,9.75px,16.25px).p-4renders at0.924rem(12.012pxon desktop,11.088pxon mobile), whereas Tailwind'sp-4expects16px.- Steps above 20 fall through to Tailwind because Bootstrap's
$spacersmap stops at 20.
Practical Workaround Rules
- Exact Pixel Sizing: For spacing or padding that must match design tokens precisely, use Tailwind arbitrary values:
p-[12px],gap-[8px],px-[16px]. - Structural Borders: Use
border-[1px]instead of the bare.borderutility. Bootstrap's.borderalso sets the colour with!important, so any Tailwind border colour next to it (border border-brand,border border-transparent hover:border-secondary) never paints. Give every state an explicit colour:border-[1px] border-secondary, thenborder-brandwhen selected. Enforced (with autofix) byno-bootstrap-border-with-tailwind-color. - Layout & Display Utilities: Legacy Bootstrap layout classes are retired in favor of native Tailwind equivalents:
- Display:
d-flex→flex,d-block→block,d-none→hidden,d-inline→inline,d-inline-block→inline-block,d-inline-flex→inline-flex,d-grid→grid, and responsive breakpoints (d-md-none→md:hidden,d-lg-block→lg:block,d-sm-flex→sm:flex). - Flexbox:
flex-column→flex-col,align-items-*→items-*,align-self-*→self-*,justify-content-*→justify-*,flex-grow-1→grow,flex-shrink-0→shrink-0. - Sizing & Text:
w-100→w-full,h-100→h-full,mw-100→max-w-full,mh-100→max-h-full,text-truncate→truncate. - Enforced (with automated autofix) by
waldur-custom/no-bootstrap-layout-utility.
Root Font-Size & Fractional Rem Scaling
Metronic's compiled CSS forces a non-standard root font size:
- Desktop (
>= 768px):html, body { font-size: 13px !important; } - Mobile (
< 768px):html, body { font-size: 12px !important; }
Tailwind's default rem-based scale assumes a 16px root. In src/tailwind.css, @theme overrides standard tokens (--spacing, --text-sm, --text-base, --radius-md, --radius-lg) with explicit values to compensate.
[!WARNING] Inline Styles & Rem Trap: While Tailwind utilities can be adjusted via
@theme, inline styles likestyle={{ width: '18rem' }}bypass Tailwind entirely. In standard browsers18rem = 288px, but in Waldur18rem = 18 × 13px = 234px(and216pxon mobile). Always use explicit pixel values (style={{ width: '250px' }}) for inline sizing.
The Preflight Reset Shim
Tailwind Preflight resets two core element behaviors that have no counterpart in Bootstrap:
img, svg, video, canvas { display: block; }— Drops inline SVG icons below the text baseline.ol, ul, menu { list-style: none; }— Strips list formatting from Markdown user content (offering descriptions, Terms of Service).
src/tailwind.css includes a targeted @layer bootstrap block that restores browser defaults using revert:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Using revert respects user-agent specifics (e.g. audio:not([controls]) { display: none; }) while neutralizing unwanted Preflight side-effects.
Grid Breakpoint Synchronization
src/tailwind.css aligns Tailwind breakpoints with Bootstrap's $grid-breakpoints and GRID_BREAKPOINTS in src/core/constants.ts:
1 2 3 4 5 6 7 | |
This guarantees lg resolves to 992px identically across Tailwind lg: variants, Bootstrap .d-lg-* classes, SCSS @include media-breakpoint-up(lg), and TSX useMediaQuery() hooks.
Design Tokens & Color Systems
- Single Source of Truth: All color ramps originate in
packages/design-tokens/tokens/colors.json. Runningyarn tokens:generateoutputs: src/metronic/sass/_color-ramps.scss(consumed by Bootstrap & Metronic SCSS)packages/design-tokens/src/css/primitives/colorRamps.css(consumed by Tailwind)- Theme Color Semantics:
- In Tailwind CSS:
--color-gray-Nmaintains physical lightness across themes (e.g.,gray-900is always dark). Dark UI uses a dedicated inverted ramp:--color-gray-dark-N. - In Metronic SCSS:
$gray-Nvariables invert automatically in dark mode ($gray-900becomes light in dark mode). - Semantic & Component Tokens (reference: design-tokens.md § Semantic roles):
- Neutral text and icons: one scale,
--text-primary…--text-senary(gray-900 … 300), plus--text-disabled. The former--fg-*neutrals,--text-body,--text-subduedand--text-mutedwere merged into it. - Surfaces
--bg-primary…--bg-quaternary, borders--border-primary…--border-tertiary,--border-divider*, plus--card-bg,--dropdown-shadow. - States: the status matrix
--status-{success,warning,danger}-{bg,border,solid,fg}and--status-neutral-{bg,fg}, each-fgAA-checked against every surface. - Component tokens (
components/button.css): Per-variant button backgrounds, borders, hover states, and focus rings. primitives/zIndex.css: Coordinated stack order across Bootstrap and Radix layers.- Semantic utilities:
packages/design-tokens/src/css/tailwind/theme.css(imported by the package'sindex.css, so every consumer gets it) exposes the roles through property-scoped theme keys (--text-color-*,--background-color-*,--border-color-*), sotext-tertiaryisvar(--text-tertiary),bg-secondaryisvar(--bg-secondary),border-primaryisvar(--border-primary), with no exceptions. Brand istext-brand/border-brand/outline-brand.
Brand Color Token Bridge
packages/design-tokens/src/css/tailwind/theme.css bridges the runtime brand variables into
Tailwind, next to the semantic roles:
1 2 3 4 5 6 7 8 9 | |
text-brand, border-brand and outline-brand read --text-brand (semantic/brand.css), the
brand border for a selected card or option: the tenant colour in light, --brand-300 in dark,
where the tenant colour is too dark to read on the dark surfaces. These three are for text and
lines; a solid brand fill is bg-[var(--bg-brand-solid)], which stays the tenant colour in both
themes. A brand-tinted surface is bg-brand-subtle, not a raw bg-brand-50, which stays pale in
dark mode. In SCSS, never paint brand with $primary: it compiles to a static
default green. Use the runtime tokens instead (see
design-tokens.md § The brand colour in SCSS).
These CSS variables are initialized at runtime by afterBootstrap.tsx (initCssVariables()). Storybook decorators and test harnesses must seed --brand-* variables so brand-dependent components render valid colors.
Dark Mode Mechanics
Waldur does not toggle dark mode via a .dark HTML class. Instead:
applyTheme()insrc/theme/utils.tssets thedata-theme="dark"attribute on<html>.- The Metronic stylesheet is one file for both themes: its colours read the design tokens, and
its dark-only rules use the same attribute (the
dark-thememixin). - Tailwind dark mode is configured via an explicit variant:
1 | |
Component Architecture & Guidelines
AlertItem
packages/ui/src/AlertItem/ (exported from waldur-ui).
- Architecture: Pure Tailwind utilities and CSS design tokens (
--border-secondary). - Typography & Proportions:
- Title:
text-[1.077rem] font-medium leading-[1.43]to match Metronic typography across desktop and mobile root font sizes. - Border:
var(--border-secondary)(#E4E7EClight,#1F242Fdark). - Slot spacing:
gap-[0.924rem]. - Enforcement:
react-bootstrapAlertis prohibited inRESTRICTED_IMPORTS(eslint.config.js), blocking direct imports and guiding call sites toAlertItemfromwaldur-ui.
Badge
packages/ui/src/Badge/ (exported from waldur-ui).
- Variants & Tones: 15 semantic variants (
primary,secondary,success,warning,danger,info,neutral,purple,blue,indigo,moss,pink,teal,orange,rose) × 3 tones (solid,light,outline) × 4 shapes (rounded,pill,circle,roundless). - Structural 1px Border: Uses
border-[1px]paired withborder-transparenton solid/light tones to maintain uniform dimensions without subpixel shift across tones. - Status tones:
success/warning/dangersolidandlightread the status matrix (--status-*-solid,--status-*-bg+--status-*-fg), so they match alerts and Progress fills. Theoutlinetone keeps its own ramp steps (aneslint-disableforno-raw-status-ramprecords why). - Enforcement:
react-bootstrapBadgeis prohibited inRESTRICTED_IMPORTS.enforce-badge-icon-patterns: Enforces 12px icons inside badges.enforce-badge-props-consistency: Enforces variant/tone prop rules.no-manual-icon-colors-in-badges: Prevents manual color classes on icons rendered insideBadge.enforce-badge-right-icon-pattern: Standardizes dismiss/action icons.
Tooltip
packages/ui/src/Tooltip/ (exported from waldur-ui).
- Dual Engine Architecture:
trigger="hover"(default): Uses@radix-ui/react-tooltipwith pointer enter/leave delays and keyboard focus detection.trigger="click": Uses@radix-ui/react-popoverfor click-to-open tooltips (Radix Tooltip lacks click-trigger support). Styled identically as a bubble surface.- Seam-Free Arrow Construction: Standard Radix SVG arrows leave an anti-aliasing hairline seam between the SVG path and the HTML popup.
Tooltip.tsxuses a custom polygon that extends 1 viewBox unit (~0.5px) past its box withoverflow: visibleto prevent rasterization seams. - Adaptive Color Inversion:
theme="dark"(default): Dark bubble withtext-whitein light mode; light bubble with dark text in dark mode.theme="light": Fixed dark bubble regardless of active mode.- Enforcement:
TooltipandOverlayTriggerfromreact-bootstrapare prohibited inRESTRICTED_IMPORTS.
Popover
packages/ui/src/Popover/ (exported from waldur-ui).
- Design Token Styling: Styled with semantic tokens:
1 2 | |
- Menu vs. Popover:
[!TIP] When to use
Menuvs. a popover:Menu: Use when every child is a command row. A Radix DropdownMenu owns focus with a roving tabindex and treats keystrokes as item typeahead.MenuPopover(orPopover): Use if the panel contains form inputs, filters, date pickers, or interactive search fields. A menu will intercept keystrokes typed into a nested input if they match a menu item!
- Enforcement:
react-bootstrap/Popoveris prohibited inRESTRICTED_IMPORTS.
Accordion & Collapsible
packages/ui/src/Accordion/ and packages/ui/src/Collapsible/ (exported from waldur-ui).
- Accordion is the shadcn recipe on Radix Accordion: each trigger is a real
<button>inside an<h3>(Bootstrap/Metronic heading margin and type reset), witharia-expanded/aria-controlsand arrow/Home/End keys between headers. Usetype="multiple"where several panels stay open (table filters sidebar). - Brand colour: the open header is
text-brand-700/dark:text-brand-200, the runtime tenant ramp. The chevron is a Phosphor icon incurrentColor, so it always matches — react-bootstrap's version baked the build-time default green into a data-URI SVG. - Motion: 250ms ease-out slide. Accordion and plain Collapsible use
waldur-collapsible-down/-upinwaldur-design-tokens/animations.css, driven by Radix's--radix-collapsible-content-height;overflow: hiddenlives in the keyframes so an open panel lets inline select menus overflow it.Collapsible keepMounted(its children stay mounted, so Radix's Presence can't drive keyframes) transitions grid rows 0fr ↔ 1fr instead, clipping only mid-slide and applyinghiddenonce closed. Both respectprefers-reduced-motion. - Unmount vs.
keepMounted:[!WARNING] Radix renders
isOpen && childreninside its Content, so a closed panel's children are unmounted even withforceMount. react-final-form fields inside it unregister and lose their values and validation. For panels holding form fields, use<Collapsible keepMounted>: children stay mounted and are onlyhiddenwhile closed.Accordionhas no such option — none of its intended users hold form fields. - Call sites: the table filters sidebar (
Accordion type="multiple"),AccordionCard(now inwaldur-ui;Collapsible keepMounted, same props as before; pure Tailwind with rem spacing ported from Metronic's card, and--card-header-text/--card-title-text/--card-title-secondary-text/--card-header-solid-bgtokens incard.css— measured identical to the Metronic card in light and dark),CategoriesPanel(Collapsible) and a handful of standalone accordions, which add an outer border viaclassName(rounded-md border-[1px] border-solid border-[var(--border-secondary)]) — the component itself draws only separators between items. - Enforcement:
Accordion,AccordionContextanduseAccordionButtonfromreact-bootstrap(andreact-bootstrap/Accordion) are prohibited inRESTRICTED_IMPORTS.
Progress
packages/ui/src/Progress/ (exported from waldur-ui); tokens in packages/design-tokens/src/css/components/progress.css.
- Three modes:
<Progress value max variant />,<Progress segments={[{ value, variant, label? }]} max />for stacked bars, and<Progress indeterminate />for an unknown amount (a sweeping bar; a static half-opaque bar underprefers-reduced-motion). - No height override:
sizeisxs(4px),sm(6px),md(8px, default) orlg(24px, the only size meant for text inside a segment). There is nostyle/h-*escape hatch; pick a size. - Neutral track: the track never takes the variant's colour (a tinted track made an empty meter look full).
variantis aBadgeVariantminussecondary, and the fills are Badge'ssolidcolours in both themes.primaryreads--brand-600, so tenant re-branding applies. - Accessibility: the track carries
role="progressbar"witharia-valuenow/min/max;labelis its accessible name. A non-finitevalueor amax <= 0(a 0/0 quota) reads as 0, neverNaN. Segments are clamped so they never exceed the track. PercentProgress(src/core/PercentProgress.tsx) is the bar plus its share ofmaxprinted under it (unit,compact). Use plainProgresswhen the surrounding markup already prints the figure.- Inner labels: each variant has its own
--progress-label-{variant}(white or gray-900, whichever reads better on the fill); a token test keeps every pair at or above 4.4:1. - Enforcement: the custom rule
no-bootstrap-progressreports react-bootstrap'sProgressBar(by name or deep path) and anyprogress,progress-bar*orprogress-themedclass token. The Bootstrap/Metronic.progressstyles and the$progress-*variables are deleted.
Checkbox, Radio & Switch
packages/ui/src/Check/: Checkbox.tsx, Radio.tsx, RadioGroup.tsx and Switch.tsx (exported from waldur-ui through Check/index.ts), plus the internal CheckLabel.tsx (label row), CheckMark.tsx (the check path, also drawn by the select) and checkStyles.ts (class recipes); colours in waldur-design-tokens/check.css.
- Hidden input +
peer: each control is a box holding a native<input>that is invisible (opacity-0, notsr-only) and on top, so it still takes clicks, focus, keyboard and form state, followed by elements that draw the control and read the input's state withpeer-*: the box and the check/dash SVGs (Checkbox), the circle and dot (Radio), the track and knob (Switch). No background images or data URIs; marks arecurrentColor(--check-mark/--check-mark-disabled). Compound states use arbitrary peer variants (peer-[:checked:disabled]:,peer-[:hover:enabled:not(:checked)]:).classNameandhiddengo on the box (layout); every other prop, and theref, reach the input. Indeterminate styling is checkbox-only::indeterminatealso matches every radio in a group with nothing selected. - Motion matches the legacy
$form-check-transition: colours and border fade and the switch knob slides over 0.15sease-in-out(writtenease-[ease-in-out]; Tailwind'sease-in-outis a different curve), andmotion-reduce:transition-noneturns it off, as Bootstrap'stransitionmixin did. The marks appear instantly, as the background images did. SwitchVisualis the switch's look as a<span>with no input, forMenu.CheckboxItem, which already hasrole="menuitemcheckbox".- Sizes are the legacy ones at the 13px root, in px: checkbox/radio
md20px (radius 6px) andsm16px (radius 4px); switchmd44×24 andsm36×20. The legacy controls were rem-sized, so they shrank about 8% at the 12px mobile root; these do not. - Tokens (
--check-*,--switch-*) map one-to-one onto the SCSS tokens the old stylesheet used ($border-primary,$bg-brand-solid,$bg-disabled_subtle,$fg-senary, …); the file's header lists every mapping. Only colours live there: the marks are elements (below). - Labels are props:
Checkbox,RadioandSwitchtakelabel,description,tooltip,alignandinline. With any of them the control renders its whole row (the internalCheckLabel): control, then a 14px/20px<label htmlFor>naming it and a muted description linked witharia-describedby, 8px apart. Without them it renders bare, for tables (give it anaria-label).aligndefaults tostart(the control stays on the first line when the label wraps), except aSwitchwithout a description, which centres; a 16px control instartdrops 2px on its own;Switchrows areinlineby default, as the legacy switches were. All three reportonCheckedChange(boolean)(Checkboxalso keeps nativeonChange);classNamegoes on the outermost element. RadioGroup: options in, one value out (value/onValueChange), likeSegmentedControl, but as native radios in a<fieldset>whose<legend>islabel, withdescription, per-optiondescription/tooltip/disabled,orientationand a generatedname. Disabling the group disables every option through the fieldset. UseSegmentedControlto switch a view,RadioGroupfor a form question.- react-final-form:
SwitchFieldandRadioGroupField(src/form) only mapinputonto the control.BooleanGroup/BooleanEditField/BooleanFilterand the form-levelRadioGroupkeep their names; withpassLabelToControlthe control draws the field'slabel,tooltipanddescription, andFormGroupadds only spacing, required mark and errors. The oldAwesomeCheckbox,AwesomeCheckboxFieldandAwesomeRadioButtonare gone, along with theirhelp_text(nowdescription),alignMiddle(nowalign="center"),tooltipEnd,direction(noworientation),justifyandgapprops. - Enforcement: the custom rule
no-bootstrap-form-checkreportsFormCheck,Form.Check(under any local name forForm) and anyform-check*/form-switch*class token.
Buttons
Three primitives in waldur-ui (packages/ui/src) and Link in @/core/Link make up the application's unified button architecture:
| Primitive | Role & Use Case | Package / Location |
|---|---|---|
BaseButton |
Standard action buttons across dialogs, tables, and page headers | waldur-ui |
buttonVariants() |
Shared CVA styling applied to non-button elements (router links, dropdown toggles) | waldur-ui |
SegmentedControl |
Mutually exclusive view switchers (Radix RadioGroup) | waldur-ui |
Link (with buttonVariant) |
State-based router navigation styled with button aesthetics | @/core/Link |
ButtonVariant and ButtonSize are derived directly from the cva definition (VariantProps<typeof buttonVariants>), guaranteeing types stay synchronized with rendered classes.
Architectural Highlights
- Integer Heights & Inset Borders: Decoupled from physical CSS
border-boxcalculations viashadow-[inset_0_0_0_1px_var(...)], guaranteeing exact heights: 28px (sm), 36px (md, default), and 44px (lg). - Direct Tooltip Integration:
buttonVariantsdoes not declaredisabled:pointer-events-none; instead, it pairsdisabled:cursor-not-allowedwith matchingdata-disabled:*styles.<BaseButton>passesdata-disabledand wraps<button>inside<Tooltip>with zero wrapper<span>elements, eliminating flexbox alignment bugs (self-center,w-100). - Outline Focus Rings: Focus indicators use native CSS
outline(focus-visible:[outline:2px_solid_var(...)]) for forced-colors compatibility and resistance to container resets. - Toggles in
ActionsDropdown: Dropdown toggles render a direct<button>withbuttonVariants()(rather thanBaseButton) so the rotating caret (rotate-toggle-180) remains an immediate child for CSS animation. - SegmentedControl: Built on Radix RadioGroup (←/→ select, single Tab stop) with
inline-flex self-centerpreventing layout stretching.
[!TIP] Authoritative Guide: For complete variant tokens, full prop specifications, icon sizing rules, dropdown toggle architecture, and the call-site conversion guide, see the dedicated Button UI Guide.
Sidebar & Mobile Sheet
packages/ui/src/Sidebar.tsx + Sheet.tsx.
- Separate Desktop and Mobile Trees:
useIsMobile()splits execution into two distinct branches: - Desktop: Collapsible rail with
group/panelvariants, hover expansion, and fixed positioning over an invisible spacer element. - Mobile: Rendered inside a Radix
Sheet/Dialogdrawer pinned towidth: 250px(avoiding rem shrinkage). - Hover Expansion Mechanics: Synchronized via React state (
isHoverExpanded) rather than pure CSS:hoverto prevent desynchronization during collapse animations. - Radix Collapsible Tree: Built using
@radix-ui/react-collapsiblerather thanAccordionto avoid wrapper DOM nodes that interfere with menu styling. Heights are measured dynamically viaResizeObserverinto--sidebar-accordion-height. - The Mobile Sheet Animation Trap:
[!CAUTION] Never use
forceMounton the mobile Sheet: Radix'sDismissableLayerregisters globalpointerdownlisteners while mounted. Force-mounting the closed sheet causes itsDismissableLayerto intercept clicks across the entire application, breaking unrelated header buttons. Instead, the Sheet uses Tailwind v4'sstarting:variant (@starting-style), allowing elements to animate from a defined initial frame upon dynamic mounting.
Content Drawer (#kt_drawer)
src/drawer/DrawerRoot.tsx wraps the shared drawer panel in Radix Dialog.Root and Dialog.Content.
- Animation Detection via
@keyframes: RadixPresencemonitors CSS@keyframes(animationstart/animationend) to delay unmounting. Plain CSS transitions are ignored._shell.scssattaches explicit@keyframes kt-drawer-slide-inandkt-drawer-slide-outto#kt_drawer. - Width Custom Property: Sizing is controlled via
--drawer-width, allowing responsive full-screen toggles without direct inline style overrides.
Dropdown & Menu System Map
Every menu and menu-like popover is built from one set of parts in waldur-ui (see the Menus guide for which part to use, recipes and rules):
Menu(packages/ui/src/Menu/): Radix DropdownMenu with the defaults built in:modal={false}, a Portal (with an optionalcontainer), the look's z-index layer, scrolling inside the space Radix measures,sideOffset={2}and the keyframe entrance. Parts:Menu.Trigger,Menu.Content,Menu.Item,Menu.Group,Menu.RadioGroup/Menu.RadioItem,Menu.Sub/Menu.SubTrigger/Menu.SubContent,Menu.Label,Menu.Separator,Menu.CheckboxItem(a setting, shown as a switch) andMenu.CopyItem(copies a value).MenuPopover: the same surfaces on a Radix Popover, for panels holding inputs. Parts:Trigger,Anchor,Close,Content(withforceMountandcontainerfor the table filter panels) andItem(a button row that does not close the panel, since the filter menus use it as a nested popover's trigger).-
App components in
src/table/ActionsDropdown.tsx:ActionsMenuand its rows (below), andNavMenuLinkinsrc/navigation/NavMenu.tsx, a nav row that is a ui-router link. -
Looks:
Menu.Content look="nav"(the default; header, page tabs, footer, filter popovers) orlook="actions"(row actions, "Add" menus). Popovers with a layout of their own (the column picker, search, breadcrumb, call settings) are plainPopovers, whosePopoverContentis a bordered card onz-popover. The looks are cva recipes collocated with each component inpackages/ui/src/Menu/(menuSurfaceinMenuContent.tsx,menuIteminMenuItem.tsx,menuTextinMenuStatic.tsx,menuLabelinMenuLabel.tsx,menuSeparatorinMenuSeparator.tsx), measured against the original Metronic and Bootstrap menus and pinned bymenuLooks.test.ts. - Look and kind in context: a panel provides its look and its kind (
menu,popoveror, with no panel,plain) through React context (packages/ui/src/Menu/menuContext.tsx), so rows in a portaled submenu still inherit them.Menu.Itemrenders for its kind: a Radix item in a menu, a button that closes the panel in a popover, a plain<button role="menuitem">anywhere else.onSelectworks the same in all three; callingevent.preventDefault()keeps the panel open. - Placement: Radix
sideandalignonly. Radix's defaultaligniscenter, so menus that open from the start edge sayalign="start". - Hover to open:
<Menu openOnHover="desktop">opens on hover atlg+ (clicking below it) with Metronic's 200ms close delay (TabsList.tsx,FooterDropdown.tsx). - Shell:
packages/shelland the micro-app use the same parts in the nav look. They have no Bootstrap, so their panels set their typography with Tailwind (py-[12px] text-[14px] font-medium, as the main app'sfw-bold fs-6 py-4).
asChild and forwardRef Requirement
Radix's Slot clones trigger elements and attaches positioning refs and event handlers. Every intermediate trigger component in an asChild hierarchy must be wrapped in forwardRef and spread ...props, or clicks will silently fail.
Bootstrap utilities on panels
Bootstrap's spacing utilities are !important, so a Bootstrap class on a panel (py-4, p-0) beats any Tailwind spacing. The surfaces reset their own margin and padding with arbitrary values (m-[0px] p-[0px]) rather than m-0/p-0 for that reason: Bootstrap's .p-0 would otherwise cancel a caller's Tailwind py-*. Tailwind-merge also reads text-anchor and text-primary as two colours and drops the first; put such classes on the asChild child (Slot joins class names without merging them) rather than on the row (see ResourceAccessButton.tsx).
ActionsMenu & ActionItem
src/table/ActionsDropdown.tsx / src/resource/actions/ActionItem.tsx.
ActionsMenu: a trigger plus an actions-look panel.toggleis"kebab"(the default),"labeled"(withlabel),"add"or an element of your own (which must forward its ref). The panel opens to the left of the toggle, aligned to its start, as row actions always have; "Add" and toolbar menus passside="bottom". For panels with search inputs, useMenuPopoverwithTableDropdownToggledirectly.ActionsDropdownis a wrapper over it. For standalone dropdown buttons, useMenu.TriggerButton.Menu.Content look="actions": the panel on its own, for menus whose trigger sits elsewhere in the markup (WaldurSidebarBrand,ResourceUsageForm).- Rows:
Menu.Item,Menu.GroupwithMenu.Label, andMenu.Separatorfromwaldur-uiare used directly withlook="actions"(or inheriting it from the parent panel).Menu.Itemautomatically receivesdata-testid="action-item"in the actions look. ActionItem: aMenu.Itemwith an icon, tooltip and staff indicator, or aBaseButtonwithas. It hides itself whenisActionVisiblesays so: an action switched off for the resource's offering (actionId+resource), or one that fails the enclosingActionList's filter (query,hideDisabled,hideNonImportant;hideGroupNamehidesActionGroupcaptions).ActionListis insrc/marketplace/resources/actions/ActionList.tsx.- Highlighted rows: hover, focus, Radix
[data-highlighted]and.activeshare one style; disabled rows (data-disabled,:disabledor.disabled) are skipped. - Isolated Row Testing:
inActionsMenu(children)(src/test/harness.tsx) wraps action rows in an openMenuwith<Menu.Content look="actions">, so they render as real menu items in Vitest without a table harness. - Toggle buttons use
buttonVariants(), notBaseButton:TableDropdownToggle/AddDropdownTogglein this file render a raw<button>carryingbuttonVariants()classes. The toggle is agroup/toggle, andButtonCaretflips while it isdata-state="open". Their icons are sized with the Phosphorsizeprop (16.25pxsm, 19.5px otherwise) rather than a.svg-iconwrapper. Standalone dropdown buttons useMenu.TriggerButtonwhich wrapsBaseButton. - Unavailable offerings: the page wraps itself in
ActionsUnavailable, and every action menu inside shows its toggle disabled with the reason in a tooltip. This replaced adata-actions-toggleattribute and a.disabled-viewrule that hid the toggles.
Nav menus
The header user menu, the language submenu, page-tab submenus, the footer menus and the filter popovers.
- Nav look: Metronic's
.menu-sub-dropdownpanel and.menu-linkrow, measured against the live app. Colours come from the--menu-*tokens inmenu.css; the shadow is--dropdown-shadow. - Row options instead of theme classes:
density(default10×16px,compact8×9.75px,base8×12px) andtone(gray-600 or gray-700). Every highlighted row is gray-700 on gray-50. A panel sets them for its rows withdensity/toneprops, asdata-*attributes the rows' group variants read (the footer usescompact;MenuPopover.Contentdefaults tobase/gray-700 for the filter rows). - Highlighted rows: hover, Radix
[data-highlighted], an open submenu (data-[state=open]), a checked radio row (data-[state=checked], e.g. the current language) and.active(whatUISrefActiveadds) share one style (themenu-row-active:variant); disabled rows (data-disabled) are skipped. NavMenuLink: a row that is a ui-routerLink. It wraps its label in a span soLinkdoesn't addtext-anchor.- Page tabs and footer bar (
TabsList.tsx,footer/MenuItem.tsx) are plain<nav><ul>link lists with their own Tailwind classes and--page-tab-*tokens (the footer links usetext-brand-600 dark:text-brand-300directly). The current tab is marked witharia-current.
Linting & Guardrails
Restricted Imports
Configured in eslint.config.js via no-restricted-imports (RESTRICTED_IMPORTS). Each entry has a message that names the replacement:
react-bootstrap import |
Use instead |
|---|---|
Badge |
Badge from waldur-ui |
Tooltip, OverlayTrigger |
Tooltip (or Popover) from waldur-ui |
Popover |
Popover from waldur-ui |
Alert |
AlertItem from waldur-ui |
Button |
BaseButton from waldur-ui (or SubmitButton / CloseDialogButton where they fit) |
DropdownButton |
ActionsDropdown |
Accordion, AccordionContext, useAccordionButton |
Accordion or Collapsible from waldur-ui |
Pagination, PageItem |
TablePagination (or Pagination for page numbers alone) from waldur-ui |
Two Radix packages are restricted too, everywhere except packages/ui (where Menu / MenuPopover wrap them):
| Import | Use instead |
|---|---|
@radix-ui/react-dropdown-menu |
Menu from waldur-ui, or ActionsMenu / ActionsDropdown for action menus |
@radix-ui/react-popover |
MenuPopover (or Popover) from waldur-ui |
Each react-bootstrap entry is blocked both as a named import from react-bootstrap and as a deep import (react-bootstrap/Badge, …). A genuinely new exception belongs on the offending line as // eslint-disable-next-line no-restricted-imports -- <reason>.
Import restrictions only see imports. A button hand-written as <button className="btn btn-danger"> imports nothing, so it is caught by the custom rule no-bootstrap-button-markup below.
Custom ESLint Rules Matrix
Implemented in packages/eslint-plugin-waldur and enabled as waldur-custom/* in eslint.config.js. Rules marked warn steer new code while existing violations (over a hundred hand-rolled tables, for example) are converted one screen at a time; promote them to error when the count reaches zero.
Buttons, dialogs and tables
| Rule Name | Severity | Enforced Pattern |
|---|---|---|
no-bootstrap-button-markup |
error |
Flags btn as a class token on a native <button>, <a>, <input> or <label>. No allowlist: use BaseButton, or Link with buttonVariant. |
enforce-disabled-button-tooltip |
error |
A disabled BaseButton needs a tooltip or disabledReason explaining why. |
no-edit-button-size-override |
error |
Prevents size="sm" on EditButton; use CompactEditButton. |
enforce-dialog-button-order |
error |
Dismissive actions first, primary submission/approval last (rightmost) in dialog footers. |
no-hand-rolled-modal-footer |
error |
Prefer ModalDialog's footer prop over hand-rolling action buttons inside its children. |
enforce-actions-dropdown-in-tables |
warn |
Steers table row actions toward ActionsDropdown. |
no-hand-rolled-table |
warn |
Prefer @/table/Table over react-bootstrap Table or raw <table> markup. |
enforce-noresult-with-cta |
error |
NoResult empty states need an actionable call-to-action button. |
enforce-render-field-or-dash |
error |
Table cells use renderFieldOrDash() for empty values. |
Visual patterns
| Rule Name | Severity | Enforced Pattern |
|---|---|---|
enforce-badge-icon-patterns |
error |
Enforces 12px icon sizing inside Badge components. |
enforce-badge-props-consistency |
error |
Validates variant, tone and shape prop combinations on Badge. |
no-manual-icon-colors-in-badges |
error |
Prevents manual colour classes on icons rendered inside Badge. |
enforce-badge-right-icon-pattern |
error |
Standardizes right-side action icon styling in badges. |
enforce-featured-icon |
error |
Requires FeaturedIcon (with tone/size) for highlighted icon emblems. |
enforce-phosphor-icon-weight |
error |
Consistent weight prop on Phosphor icons. |
enforce-border-radius-tokens |
error |
Enforces pinned Tailwind border radius tokens (rounded-none … rounded-modal, rounded-full). Autofixes retired Bootstrap rounded-0, rounded-pill, rounded-circle, arbitrary matching pixel classes (rounded-[6px] -> rounded-md), and flags inline style={{ borderRadius }}. |
no-bootstrap-tabs |
error |
Tabs / TabNav from waldur-ui; no react-bootstrap Tab/Tabs/Nav or nav-* classes. |
enforce-breadcrumb-colors |
error |
Breadcrumb links use the $text-brand-secondary token. |
no-bootstrap-form-check |
error |
Checkbox / Radio / Switch from waldur-ui; no FormCheck, Form.Check or form-check* / form-switch* classes. |
no-bootstrap-progress |
error |
Progress from waldur-ui; no react-bootstrap ProgressBar or progress / progress-bar* classes. |
no-bootstrap-border-with-tailwind-color |
error |
No Bootstrap border / border-top / … next to a Tailwind border colour (Bootstrap's !important colour wins). Autofixes to border-[1px] when the same string sets a base colour. |
no-bootstrap-layout-utility |
error |
Retires Bootstrap layout, display, flexbox, sizing and text-truncate utilities (d-block, d-flex, d-none, flex-column, w-100, mw-100, text-truncate, responsive variants) in favor of Tailwind standard classes (block, flex, hidden, flex-col, w-full, max-w-full, truncate). Autofixes across the codebase. |
no-raw-status-ramp |
error |
State colours come from --status-*, not raw success-N / warning-N / error-N utilities. Deliberate exceptions carry an eslint-disable with the reason. |
no-retired-bootstrap-utility |
error |
Bootstrap utilities the Metronic build no longer generates (never used: vh-100, align-content-*, mx-n*, ls-*; or used only at the base size, so their breakpoint variants px-lg-3, min-h-md-100px, svg-icon-lg-3 are dropped) compile to nothing; names the Tailwind replacement. |
require-font-size-scale |
error |
Font size follows the pinned scale (text-xs … text-4xl). Autofixes retired Bootstrap fs-* classes (fs-6 -> text-sm, fs-7/8 -> text-xs, etc.) and arbitrary text-[14px]. Flags inline style={{ fontSize }}. |
require-weight-scale |
error |
Font weight is font-light … font-bold. Reports Bootstrap fw-* (autofixed to the same weight: fw-bold was 500, so font-medium), off-scale steps, font-[N] and inline fontWeight. |
require-dark-pair |
error |
A physical colour (ramp step, white/black, hex, raw var(--brand-N)) needs a dark: counterpart in its class list, or a semantic utility. Solid fills and white text on them pass. |
no-bootstrap-css-vars |
error |
No --bs-* reads in TS/TSX; use the design token (table in design-tokens.md). |
Code hygiene
| Rule Name | Severity | Enforced Pattern |
|---|---|---|
no-direct-client-usage |
error |
No direct use of low-level API client methods. |
no-direct-field-adapter |
error |
Adapter components are not passed straight to Field's component prop. |
no-template-in-translate |
error |
No template literals or concatenation inside translate() calls. |
prefer-classnames-utility |
error |
Use classNames() instead of manual className string concatenation. |
no-redundant-vi-mock |
error |
No vi.mock for modules that are already mocked globally. |
no-undefined-in-mutation-body |
warn |
Do not collapse a request-body field to undefined: the key is dropped and clearing the field becomes a silent no-op. |
prefer-mutate-over-mutateAsync |
warn |
Buttons and action items (BaseButton, ActionItem, …) call .mutate(); keep .mutateAsync() for forms that need the promise. |
The former enforce-button-variants rule was deleted: the variant list lives in the ButtonVariant type, so the compiler enforces it.
Storybook & Testing Toolchain
Storybook Environment & Vitest
- Dev Server:
yarn storybook(port 6006) - Production Build:
yarn build-storybook - Vitest Runner:
yarn test:storybook(runs every story through Playwright browser assertions). - Concurrency Gotcha: When running Vitest against Storybook while a Storybook dev server is active, always prefix the command with
CHOKIDAR_USEPOLLING=1to prevent inotify file-watcher limits:
1 | |
- Stories to look at:
Actions/BaseButton(packages/ui/src/Button/BaseButton.stories.tsx: playground, every state, sizes, icon-only, realistic usage),Actions/SegmentedControl(variants, sizes,BesideTallerSibling,AsTabsWithPanels), andsrc/table/ActionsMenu.stories.tsx. - Storybook is not the app: it applies the theme through
storybook-addon-pseudo-states, which rewrites:focus-visible/:hoverselectors. See the pitfall below before trusting a:not(:focus-visible)result there.
Visual E2E Specs
yarn test:e2e:visual runs e2e-visual/*.spec.ts through Playwright (project visual, against the app on :8001 and Storybook on :6006).
focus-ring.spec.ts: asserts that keyboard focus produces a visible indicator with at least 3:1 contrast (WCAG 2.4.7 and 1.4.11), in light and dark. Its fixtures are built frombuttonVariants()on theactions-basebutton--playgroundstory's stylesheet and cover the places a ring was once suppressed: elevation utilities (shadow-sm), an unlayered page stylesheet setting abox-shadow, a menu row (navMenuRowClassName()) andnav-line-tabs. Theprimarycase is a recorded contrast gap (test.fail()): a brand ring on a brand-filled button is about 1.6:1..btn-no-focusis deliberately not a fixture — see the pseudo-states pitfall.stat-card-parity.spec.ts,charts.spec.ts,wizards.spec.ts: parity and screenshot checks for other components;visualParityHarness.tsholds the shared pixel-diff and chromaticity helpers.
Run one spec with yarn playwright test focus-ring --project=visual --workers=1 (a filter, not a path; passing a path is read as a project name). Playwright executes specs in Node, so a spec may import packages/ui/src/Button directly but not the waldur-ui barrel, whose modules touch document at import time.
Testing Gotchas & Pitfalls
- CSS Transitions Require a Paint: Calling
getComputedStyle()synchronously after.hover()or.focus()returns pre-transition values. Test harnesses must disable animations via injected CSS or wait for transition settling. - jsdom Mocking:
-
ResizeObserver: jsdom lacksResizeObserver. Tests renderingCollapsibleorSidebarmust mock it:1 2 3 4 5 6 7 8
vi.stubGlobal( 'ResizeObserver', class { observe() {} unobserve() {} disconnect() {} }, ); -
matchMedia& Virtual Viewport:waldur-ui'suseMediaQuery,useBreakpointDown, anduseBreakpointUpreadwindow.matchMediadynamically via React 18'suseSyncExternalStore. In tests (jsdom),test/setupTests.jsinitializes the query-aware virtual viewport controller (setupVirtualViewport()), defaulting to 1440px desktop with hover enabled and automaticafterEachteardown (resetViewport()). In responsive test suites, never stubmatchMediablindly withvi.stubGlobal; instead callsetViewport('mobile' | 'desktop' | BreakpointKey | px | { hover: false })fromwaldur-ui/testing(or re-exported from@/test/harness). -
Headless Browser Animation Polling: CDP/Playwright tabs in automated modes skip compositing animation frames if backgrounded.
setTimeoutpolling ofgetAnimations()may readcurrentTime: 0. Trigger a screenshot capture or force a reflow to guarantee paint completion. - Tailwind only generates classes it can read literally: Tailwind v4 scans source files for complete class strings. A class assembled at runtime (
`[&>svg]:h-[${px}]`) is never generated, so it silently does nothing.BaseButton's icons rendered at 16px instead of 20px for exactly this reason. Write the class as a literal and pass the varying part through a CSS custom property (style={{ '--icon-size': px }}with[&>svg]:h-[var(--icon-size)]), or choose between whole literal classes. Classes inside HTML strings (chart tooltips) are scanned too, provided they are literals —buttonVariants()returns literals. - Compound Bootstrap selectors are dead without the parent class: most Metronic rules are written
.btn.btn-sm { … }or.btn { &.btn-icon { … } }. Addingbtn-smto an element that lacks.btndoes nothing, and a hand-rolledLink/divthat dropped.btnloses the rule with no error. Grep the SCSS for the class before relying on it; prefervariant/sizeprops. - A different tree shape at the same position remounts: a component that returns
<Wrapper>{child}</Wrapper>on one render and barechildon the next makes React discard and recreatechild, losing focus and any ref held on it. The filter button's count badge did this until it got a wrapper that is always mounted;BaseButton's tooltip usesalwaysMountfor the same reason. If a control's focus is lost when a badge or tooltip appears, look for this. - A flex row stretches its children:
align-items: stretchis the default, so a control with no explicit height grows to the tallest sibling. ASegmentedControlmeasured 36px in isolation and 44px in a toolbar. Give shared controlsself-center(or an explicit size) instead of relying on the host row. - Cascade layers invert for
!important: in@layer bootstrap, an!importantdeclaration beats Tailwind utilities (a later layer) because important declarations reverse layer order. That is why.btn-no-focus { box-shadow: none !important }removes aBaseButton's inset border, and why an override meant to win over a Tailwind utility from thebootstraplayer must itself be!important. - Storybook rewrites pseudo-classes:
storybook-addon-pseudo-statesturns:not(:focus-visible)into:not(.pseudo-focus-visible), which matches every element, so a rule such as.btn-no-focus:not(:focus-visible) { outline: none !important }suppresses the outline permanently in Storybook while behaving correctly in the app. Measure focus and hover behaviour against the running app (the dev server on :8001) before changing CSS on the strength of a Storybook ore2e-visualresult. - Radix RadioGroup in jsdom: arrow keys move and select only while the key is held. With
userEvent, usekeyboard('{ArrowRight>}')(press without release) followed bywaitFor, notkeyboard('{ArrowRight}'). RadixTabsreportsdata-state="active", aRadioGroupitemdata-state="checked". - Test conventions: mock the current user with
vi.mocked(useUser).mockReturnValue(…)—@/workspace/hooksis mocked globally, and a localvi.mockof it tripsno-redundant-vi-mock. Testing Library'sno-node-accessrule forbids.querySelector/.closestin tests; reach elements by role, label or text. Prove a new regression test by reverting the fix and watching it fail.