feat(tw): add Tailwind CSS v4 module#43
Merged
Merged
Conversation
…rs and demos New @ribajs/tw package providing a Tailwind CSS v4.2 alternative to @ribajs/bs5: - 7 services: TwService (breakpoints), ThemeService (dark mode), CollapseService, ModalService, DropdownService (Floating UI), ToastService, TooltipService, PopoverService - 13 binders: breakpoint-responsive attrs, dropdown, tooltip, popover, scrollspy, toggle-class/attribute, collapse-on-url, and more - 36 components: 8 core (icon, button, collapse, accordion, toast, modal, notifications, toggle-button), 15 interactive (dropdown, navbar, tabs, sidebar, slideshow, carousel, slider, form, colorpicker, share, etc.), 13 new TW-native (alert, badge, avatar, card, skeleton, breadcrumb, pagination, steps, progress, rating, swap, tooltip, kbd) - 9 demo apps with Tailwind v4 @source content detection and @custom-variant dark mode - 156 unit tests across services, constants, and component templates - Custom CSS utilities (scrollbar-none, drag-none) in utilities.css - Framework-agnostic binders (scroll-to-on-event, toggle-attribute, toggle-class) extracted to @ribajs/extras for shared use between bs5 and tw modules - Dragscroll desktop drag with scroll-snap and scroll-behavior fixes
Major: jsdom 26→29, vitest 3→4
Minor: eslint 10.1→10.2, sass 1.98→1.99, typescript-eslint 8.57→8.58
Patch: vite 8.0.3→8.0.8, prettier 3.8.1→3.8.2, playwright 1.59.0→1.59.1,
@types/node 24.12.0→24.12.2, ts-jest 29.4.6→29.4.9
Fix collapse service tests to expect "0px" (jsdom 29 now correctly
normalizes unitless zero to "0px", matching real browser behavior).
esbuild in dev mode injects __self (component instance) and __source into every JSX call. renderElement was serializing these into HTML attributes, producing unescaped JSON that broke HTML attribute parsing and corrupted binder attributes (rv-hide, rv-show, rv-each-*), causing e.g. the accessibility-keyboard demo to render completely broken.
- rv-if/rv-unless on children of rv-each don't reactively update — use rv-show/rv-hide or wrap in child element instead - rv-class does not exist as a binder; the generic attribute binder overwrites static classes. Use rv-add-class to preserve static classes.
… fixes
Components:
- tw-rating: event delegation via rv-each with $parent, remove rv-if (use
rv-class to toggle yellow/gray on a single SVG)
- tw-steps: fix goToStep args signature, add lastIndex to scope
- tw-pagination: fix goToPage args signature, add isCurrent per page,
mutate array in place for reactive rv-each updates
- tw-breadcrumb: compute item.mode ("link"/"active"/"plain"), parse
<template> children in connectedCallback (not beforeBind)
- tw-tagged-image: fix toggleTag/closeTag args signatures, parse child
<tag> elements in connectedCallback before template replaces them,
remove overflow-hidden so popovers can overflow
- tw-progress: combine height/color into barClass (no duplicate rv-class)
- tw-toast-item: type-based color styling (info/success/warning/error)
- tw-notification-container: separate "kind" field (toast/modal) from
visual "type", revert rv-if inside rv-each (immutable after push)
- tw-avatar: inline-size fallback via Tailwind classes, full text for
short placeholders (≤3 chars)
- tw-sidebar: fixed positioning on host element, backdrop fade animation,
auto-injected close button, proper mode support (side vs overlap)
- tw-swap: opacity + position:absolute for proper animations
- tw-form: visual validation feedback on invalid form submit
- tw-slider: pass-through mode (analog to tw-slideshow), keyboard
navigation (arrow keys), disabled state sync for passthrough controls
- tw-slideshow: 50% visibility threshold for active slides (tolerant to
gaps), keyboard navigation, onScrollEnd dispatches after updateSlides
so listeners see current active state, disabled state for passthrough
controls
- tw-carousel: pause autoplay on mouseenter
- tw-tabs: rv-show for active tab panel (was rv-if, not reactive in
rv-each)
- tw-tooltip-component: rename tagName to "tw-tooltip", class-based dark
mode instead of prefers-color-scheme
CSS:
- utilities.css: global display:block for tw-* custom elements (so
layout utilities work), inline-block for text-flow components,
cursor:pointer on [rv-on-click], @source inline for dynamic classes
used in TS files
Other:
- tw-dropdown service: remove "hidden" class in show() (Tailwind's
display:none was overriding display inline style)
- rv-if/rv-unless → rv-show/rv-hide inside rv-each across tw-share,
tw-pagination, tw-breadcrumb, tw-tagged-image, tw-steps, tw-tabs,
bs5-notification-container, shopify-tda instagram
- rv-class → rv-add-class globally (rv-class doesn't preserve static
classes)
New: - tw-interactive demo (documentation layout with sticky TOC/scrollspy, tagged-image, share, content slider, collapse/FAQ, usage example) Extended: - tw-basics: add breadcrumbs, animated buttons, tooltips, swap animations, pagination, color picker, collapse - tw-notifications: use "kind: toast"/"kind: modal" separately from visual "type" - tw-sidebar: demonstrate both overlap and side modes, add close button - tw-slideshow: add tw-slider section showing multi-column content slider with drag/keyboard navigation Also: - bs5-notifications: rv-class → rv-add-class (rv-class was overwriting static classes)
The link rel attribute should be "stylesheet", not "scss". Some demos had a stale rel="scss" that likely never actually loaded the CSS.
…aths Replaces brittle relative paths like "../../../../packages/tw/src/..." with clean package-name imports thanks to Yarn PnP resolution and Tailwind v4's Node module support: @import "@ribajs/tw/src/css/utilities.css"; @source "@ribajs/tw"; Tailwind's @source directive automatically scans the package's source files for utility class usage. Affected demos: tw-accordion, tw-basics, tw-dropdown, tw-form, tw-interactive, tw-notifications, tw-sidebar, tw-slideshow, tw-tabs, tw-theme.
fuse.js 7.x no longer accepts a generic type argument on search(). The return type is now inferred from the Fuse<T> instance. Fixes CI type-check failure: error TS2558: Expected 0 type arguments, but got 1.
…nning The previous refactor used @source "@ribajs/tw" in each demo's CSS, but Tailwind v4 doesn't fully resolve node module paths for @source globs in Yarn PnP setups — only some utility classes were generated, breaking components that used e.g. -translate-x-1/2. Fix: embed @source "../**/*.html" / "../**/*.ts" in the package's own utilities.css (relative to that file, so it scans all tw component templates and TS files). Consumers now only need: @import "tailwindcss"; @import "@ribajs/tw/src/css/utilities.css"; No @source directives or brittle relative paths required in demos.
Move the logic that is not Tailwind-specific into the right shared
packages so @ribajs/tw only keeps what is tied to Tailwind utility
classes / breakpoints / dark-mode class:
- @ribajs/router:
- new rv-dispatch-on-route-{match,unmatch} binder that fires
CustomEvents on URL match. Consumers compose with rv-on-* without
pulling in any collapse/toast service. Replaces the old
tw-collapse-on-url / tw-expand-on-url binders (no template usages
in repo).
- @ribajs/extras:
- scrollspy-class binder (rv-scrollspy-*): pure viewport detection
- show-toast-on binder (rv-show-toast-on-*): pure EventDispatcher
- ModalService: native <dialog> + scroll lock, framework-agnostic.
Events renamed tw.modal.* -> modal.* (breaking).
- Notification, ModalNotification, ToastNotification types.
- @ribajs/tw:
- re-exports ModalService and notification types from extras for
backward-compatible imports.
- tw-modal-item consumes the new modal.hidden event name.
- dropdown/popover/tooltip services: inline-arrow listeners
(trigger click, mouseenter/leave/focus/blur) were never removed on
dispose(). Switch to AbortController + { signal } for all own
addEventListener calls; dispose() aborts the controller, which also
lets us drop the manual removeEventListener pairs.
- tw-form: addEventListener("input", ...) was wired up in
addEventListeners() but never cleaned up (no disconnectedCallback
override). Replace with template-level rv-on-input="enableSubmit"
on the <form> element; the riba view lifecycle handles
attach/detach. enableSubmit() now guards disableSubmitUntilChange
itself instead of relying on the conditional attach.
Per CLAUDE.md, property mutations on items iterated by rv-each don't reliably propagate to child views. Both components previously mutated star/page objects in place, which meant clicks didn't repaint the list consistently. - tw-rating: updateStars() now replaces scope.stars wholesale via computeStars(). Drops stray console.log debug statements. - tw-pagination: updatePages() replaces scope.pages wholesale. Also pre-computes a pageClass string per page (active / inactive / ellipsis variants) in computePages, and the template consumes it via rv-add-class="page.pageClass" instead of nine inverted rv-class-* toggles.
The component's tagName was already tw-tooltip, but the directory, file, and class name were still TwTooltipComponentComponent — a stale half-rename. Finish it: - packages/tw/src/components/tw-tooltip-component/ -> tw-tooltip/ - class TwTooltipComponentComponent -> TwTooltipComponent - component export in components/index.ts - css selector in utilities.css - internal style tag id (tw-tooltip-component-styles -> tw-tooltip-styles) Breaking for external TS consumers that imported the class by name.
…hook - AbstractToggleBinder: tw-toggle-class and tw-toggle-attribute were near-identical duplicates (state machine, EventDispatcher wiring, unbind, routine). Extract shared base; subclasses now only supply applyAdd / applyRemove / detectState and the event-name pair. - tw-slideshow / tw-slider: injectControls() used to overwrite the instance method updateControls via closure to keep pass-through buttons' disabled state in sync. Replaced with a syncPassthroughControls() hook that updateControls() always calls — no more Frankenstein method replacement, TS types stay clean.
- tw-modal-item dialog: add aria-modal="true" for assistive tech.
- tw-collapse: remove static aria-expanded="false" that shadowed the
reactive rv-aria-expanded binding on first paint.
- tw-skeleton: drop hardcoded inline style="width: 48px; ..." that
conflicted with rv-style-width/-height. Use default-formatter with
valid CSS length values ('48px', '120px') so the template renders
sensibly without scope-supplied overrides.
- tw-colorpicker: the invisible <input type="color"> now has an
aria-label so screen readers can name it.
- tw-card: replace inverted rv-class-p-3 / rv-class-p-5 /
rv-class-text-lg / rv-class-text-xl pairs with computed
scope.paddingClass + scope.titleSizeClass strings consumed via
rv-add-class (which preserves the static class attribute).
The typecheck step in CI (yarn run check:all) failed because the new modal.service.spec.ts imports from "vitest" but @ribajs/extras doesn't declare vitest as a (dev-)dependency — consistent with how other packages (@ribajs/tw, @ribajs/router) exclude spec files from tsc. Vitest still runs tests via the root-level vitest binary; only the per-package tsc emit needed this exclusion.
Sidebar host used an inline bg var that bypassed Tailwind's dark: variant, and the JS-generated backdrop lacked dark:bg-black/70. Now uses classList for theme-adaptive surfaces; removes unused html template. Collapse button gains dark: hover/bg variants. tw-theme demo now exercises both backdrops (sidebar, modal) plus collapse, accordion, dropdown, alerts and toasts for broad light/dark coverage.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new
@ribajs/twpackage — a Tailwind CSS v4.2 alternative to@ribajs/bs5with no Bootstrap dependency.tw-attr-*-*,tw-co-*-*), dropdown, tooltip, popover, scrollspy, toggle-class/attribute, collapse-on-url, and moretw-basics,tw-accordion,tw-dropdown,tw-form,tw-notifications,tw-slideshow,tw-sidebar,tw-tabs,tw-theme, andtw-interactive(documentation layout with scrollspy, tagged image, share, content slider, collapse/FAQ)scrollbar-none,drag-none) shipped viautilities.cssscroll-to-on-event,toggle-attribute,toggle-class) extracted to@ribajs/extras@sourcedirectives and@custom-variant darkfor class-based dark modeFollow-up fixes and improvements
Documented
rv-*binder gotchas (AGENTS.md)rv-if/rv-unlessinsiderv-eachdon't reactively update — the initial render works but property mutations on iterated items are ignored. Userv-show/rv-hideor wrap in a child element instead.rv-classis not a real binder — it falls through to the generic attribute binder which overwrites the entireclassattribute (destroying static Tailwind classes). Userv-add-classto preserve static classes.Both rules are now documented and applied globally across
tw,bs5andshopify-tdapackages.Component bug fixes
argsformatter signature fixargsformatter method signatures; pagination mutates array in place for reactive updatesmode(link/active/plain) to avoid duplicate rendering caused by non-mutually-exclusiverv-showconditions; parse<template>children inconnectedCallbackbefore template replaces them<tag>elements inconnectedCallback; removeoverflow-hiddenso popovers can overflowbarClass(no duplicaterv-class)info/success/warning/error)kindfield (toast/modal) separate from visualtype; keeprv-ifinsiderv-eachfor notifications (data is immutable after push, avoids instantiating modal backdrops for toasts)sidevsoverlapmodeposition: absolutefor proper animation transitionstw-slideshow); keyboard arrow-key navigation; disabled state for passthrough controls; scroll-snap disabled during dragscrollendedevent now dispatches afterupdateSlidesso listeners see current active state; disabled state for passthrough controlsmouseenterrv-showinstead ofrv-ifinsiderv-each)tw-tooltip-component→tw-tooltip; class-based dark mode instead ofprefers-color-schemehiddenclass on show (was overriding inlinedisplaystyle)display: block/inline-blockdefaults viautilities.css(custom elements default todisplay: inlinewhich breaksspace-y,flex,gridutilities)Developer experience
@import "@ribajs/tw/src/css/utilities.css"and@source "@ribajs/tw"instead of brittle relative paths (../../../../packages/tw/src/...). Yarn PnP + Tailwind v4 resolve these automatically.cursor: pointerglobally via[rv-on-click]CSS selector — no need to addcursor-pointermanually on every clickable element.fuse.js7.x removed the generic type arg onFuse.search()— updated@ribajs/fuseto match.Test plan
yarn installresolves the new workspace package (including the newtw-interactivedemo)yarn run check:all— type-checks the full monorepoyarn vitest run packages/tw/src/— all 156 tw unit tests passyarn build)cd demos/tw-theme && yarn start— theme switcher toggles dark/light modecd demos/tw-basics && yarn start— alerts, badges, avatars, cards, skeleton, progress, rating, breadcrumbs, buttons, tooltips, swap, pagination, color picker, collapsecd demos/tw-interactive && yarn start— tagged image, share, content slider, collapse/FAQ, scrollspy TOCcd demos/tw-slideshow && yarn start— slideshow with controls/indicators/drag; carousel with autoplay + pause-on-hover; video slide plays when visible; content slider with keyboard navigationcd demos/tw-sidebar && yarn start— overlap mode with backdrop fade; side mode pushing contentcd demos/tw-notifications && yarn start— type-colored toasts; modal notificationscd demos/tw-tabs && yarn start— tabs switch content reactively; steps wizard navigates between stepscd demos/tw-accordion && yarn start— accordion items collapse/expand correctlycd demos/tw-form && yarn start— invalid submit shows red borders and inline errorscd demos/tw-dropdown && yarn start— dropdowns open/close; keyboard navigation