Skip to content

feat(tw): add Tailwind CSS v4 module#43

Merged
JumpLink merged 20 commits into
mainfrom
feat/tailwind
Apr 15, 2026
Merged

feat(tw): add Tailwind CSS v4 module#43
JumpLink merged 20 commits into
mainfrom
feat/tailwind

Conversation

@JumpLink
Copy link
Copy Markdown

@JumpLink JumpLink commented Apr 2, 2026

Summary

Adds a new @ribajs/tw package — a Tailwind CSS v4.2 alternative to @ribajs/bs5 with no Bootstrap dependency.

  • 7 services: TwService (breakpoints), ThemeService (class-based dark mode), CollapseService, ModalService, DropdownService (Floating UI), ToastService, TooltipService, PopoverService — all pure JS, no Bootstrap
  • 13 binders: responsive breakpoint attrs (tw-attr-*-*, tw-co-*-*), 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 with drag-scroll, slider, form, colorpicker, share, tagged-image, contents, theme-button), 13 new TW-native (alert, badge, avatar, card, skeleton, breadcrumb, pagination, steps, progress, rating, swap, tooltip, kbd)
  • 10 demo apps: tw-basics, tw-accordion, tw-dropdown, tw-form, tw-notifications, tw-slideshow, tw-sidebar, tw-tabs, tw-theme, and tw-interactive (documentation layout with scrollspy, tagged image, share, content slider, collapse/FAQ)
  • 156 unit tests across services, constants, and component templates
  • Custom CSS utilities (scrollbar-none, drag-none) shipped via utilities.css
  • Framework-agnostic binders (scroll-to-on-event, toggle-attribute, toggle-class) extracted to @ribajs/extras
  • Tailwind v4 content detection via @source directives and @custom-variant dark for class-based dark mode

Follow-up fixes and improvements

Documented rv-* binder gotchas (AGENTS.md)

  • rv-if / rv-unless inside rv-each don't reactively update — the initial render works but property mutations on iterated items are ignored. Use rv-show / rv-hide or wrap in a child element instead.
  • rv-class is not a real binder — it falls through to the generic attribute binder which overwrites the entire class attribute (destroying static Tailwind classes). Use rv-add-class to preserve static classes.

Both rules are now documented and applied globally across tw, bs5 and shopify-tda packages.

Component bug fixes

  • tw-rating: click now updates stars (opacity-based, not DOM swap); args formatter signature fix
  • tw-steps, tw-pagination, tw-tagged-image: fix args formatter method signatures; pagination mutates array in place for reactive updates
  • tw-breadcrumb: compute per-item mode (link/active/plain) to avoid duplicate rendering caused by non-mutually-exclusive rv-show conditions; parse <template> children in connectedCallback before template replaces them
  • tw-tagged-image: parse child <tag> elements in connectedCallback; remove overflow-hidden so popovers can overflow
  • tw-progress: combine height/color into a single barClass (no duplicate rv-class)
  • tw-toast-item: type-based color styling (info/success/warning/error)
  • tw-notification-container: new kind field (toast/modal) separate from visual type; keep rv-if inside rv-each for notifications (data is immutable after push, avoids instantiating modal backdrops for toasts)
  • tw-sidebar: fixed positioning on host element; backdrop fade animation; auto-injected close button; proper side vs overlap mode
  • tw-swap: opacity + position: absolute for proper animation transitions
  • tw-form: visual validation feedback on invalid submit (red border + inline error messages)
  • tw-slider: new pass-through mode (mirrors tw-slideshow); keyboard arrow-key navigation; disabled state for passthrough controls; scroll-snap disabled during drag
  • tw-slideshow: 50% visibility threshold for active slides (tolerant to gaps/rounding); keyboard navigation; scrollended event now dispatches after updateSlides so listeners see current active state; disabled state for passthrough controls
  • tw-carousel: pause autoplay on mouseenter
  • tw-tabs: reactive tab panel switching (use rv-show instead of rv-if inside rv-each)
  • tw-avatar: full-text placeholder for short strings (≤ 3 chars)
  • tw-tooltip: renamed tag from tw-tooltip-componenttw-tooltip; class-based dark mode instead of prefers-color-scheme
  • tw-dropdown: service now removes Tailwind's hidden class on show (was overriding inline display style)
  • tw-collapse, tw-accordion, tw-swap, tw-scrollspy, tw-contents, etc.: proper display: block / inline-block defaults via utilities.css (custom elements default to display: inline which breaks space-y, flex, grid utilities)

Developer experience

  • Clean package imports in demos: all 10 tw demos now use @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: pointer globally via [rv-on-click] CSS selector — no need to add cursor-pointer manually on every clickable element.
  • Unrelated CI-breaking fix: fuse.js 7.x removed the generic type arg on Fuse.search() — updated @ribajs/fuse to match.

Test plan

  • yarn install resolves the new workspace package (including the new tw-interactive demo)
  • yarn run check:all — type-checks the full monorepo
  • yarn vitest run packages/tw/src/ — all 156 tw unit tests pass
  • All 10 tw demos build successfully (yarn build)
  • cd demos/tw-theme && yarn start — theme switcher toggles dark/light mode
  • cd demos/tw-basics && yarn start — alerts, badges, avatars, cards, skeleton, progress, rating, breadcrumbs, buttons, tooltips, swap, pagination, color picker, collapse
  • cd demos/tw-interactive && yarn start — tagged image, share, content slider, collapse/FAQ, scrollspy TOC
  • cd 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 navigation
  • cd demos/tw-sidebar && yarn start — overlap mode with backdrop fade; side mode pushing content
  • cd demos/tw-notifications && yarn start — type-colored toasts; modal notifications
  • cd demos/tw-tabs && yarn start — tabs switch content reactively; steps wizard navigates between steps
  • cd demos/tw-accordion && yarn start — accordion items collapse/expand correctly
  • cd demos/tw-form && yarn start — invalid submit shows red borders and inline errors
  • cd demos/tw-dropdown && yarn start — dropdowns open/close; keyboard navigation

JumpLink added 20 commits April 2, 2026 09:10
…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.
@JumpLink JumpLink merged commit 2c798ef into main Apr 15, 2026
4 checks passed
@JumpLink JumpLink deleted the feat/tailwind branch April 15, 2026 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant