@happnest/core
~54 components migrated from hardcoded Tailwind color/radius/shadow utilities to a
CSS-variable contract. A hand-authored dist/styles/components.css ships
semantic BEM recipes — consumers override --accent, --ink,
--surface, etc. to apply any theme without rebuilding.
The one-line idea
Three views of the same component, three sets of CSS variables — without any conditional code in core.
Reserva tu estancia
Monochrome look — preserved by the backward-compat aliases.
Reserva tu estancia
Same JSX, Fraunces display + Manrope body.
Reserva tu estancia
Hotel branding flips this further at runtime.
Architecture
The crucial split: legacy globals.css stays (admin + enterprise depend on it), components.css is added (the new recipe layer).
Now also
@import "./components.css".Reads
hsl(var(--accent)), var(--radius-md), …core-btn core-btn--primary.No Tailwind color classes. No hex.
(unchanged for them)
(bring their own tokens)
The public contract
The complete list of CSS variables every refactored component reads. Consumers must provide all of them; sane fallbacks ship inside components.css.
Surfaces · 6 tokens
Accent · 3 tokens
Status · 4 × 3 tokens
--success-fg
--success-soft
--warning-fg
--warning-soft
--danger-fg
--danger-soft
--info-fg
--info-soft
Geometry · 4 + 3 tokens
In total: ~30 variables. No --ds-* prefix; the names already match what client + webcheckin adopted.
In-between shades come from color-mix() at recipe time — no more --primary-50…900 scale.
The redo, side by side
The same Button component, before and after. Every refactored component follows the same five hard rules.
Hard rules every refactored component must follow
- Zero Tailwind utility classes for color, radius, shadow, font, or theme-dependent spacing in JSX.
- Zero
#hex/rgb()/rgba()literals in JSX. - No inline
style={{ }}for color/spacing/radius/font (layout-from-prop values likewidth: progress + '%'stay). - Every recipe class is prefixed
core-. - Public API of every component (props, ref, displayName) stays identical. This is a style refactor, not an API refactor.
The recipe
/* src/styles/components.css — the new home for component styles */ .core-btn { display: inline-flex; align-items: center; min-height: 40px; padding: 0 1rem; border-radius: var(--radius-md); font-family: var(--font-body); font-size: 0.875rem; font-weight: 500; transition: background-color var(--motion-base); } .core-btn--primary { background: hsl(var(--accent)); color: hsl(var(--accent-fg)); } .core-btn--primary:hover { background: color-mix(in srgb, hsl(var(--accent)) 88%, black); } .core-btn--ghost { background: transparent; color: hsl(var(--ink)); } .core-btn--ghost:hover { background: color-mix(in srgb, hsl(var(--ink)) 6%, transparent); } .core-btn--sm { min-height: 36px; padding: 0 0.75rem; } .core-btn--lg { min-height: 44px; padding: 0 1.25rem; }
Button.tsx
// packages/core/src/components/Button.tsx return ( <button className={clsx( "inline-flex items-center justify-center", "h-10 px-4 rounded-md font-medium", variant === "primary" && "bg-primary-600 hover:bg-primary-700 text-white", variant === "ghost" && "bg-transparent hover:bg-gray-100 text-gray-900", variant === "danger" && "bg-red-600 hover:bg-red-700 text-white", size === "sm" && "h-9 px-3 text-sm", size === "lg" && "h-11 px-5 text-base", )} {...props} /> );
// packages/core/src/components/Button.tsx return ( <button className={clsx( "core-btn", `core-btn--${variant}`, size !== "md" && `core-btn--${size}`, className, )} aria-busy={loading || undefined} {...props} /> ); // — Public API unchanged: variant, size, loading, ref, displayName. // — Colors now come from consumer's --accent etc. // — No hex, no Tailwind utilities for color/radius/font.
The plan, walked through
From packages/core/DESIGN_SYSTEM_REFACTOR_PLAN.md. None of these steps have been executed yet — the plan is the artifact.
Step 1 Baseline audit (scratch file, no commit)
style={{}}, and every existing Storybook story.
Produce packages/core/.audit.md as the checklist for Step 5. Acceptance: ≥ 60 component files inventoried.
Step 2 Build pipeline for components.css
src/styles/components.css with the contract fallback under :where(:root). Wire "./styles/components.css" into package.json#exports. cp step in build copies it to dist/styles/. Add @import "./components.css" at the top of globals.css so admin + enterprise pick it up for free.
Step 3 Extend globals.css with the contract
--bg, --surface, --ink, --accent, …) inside the existing :root, defaulted to admin's monochrome look. Do not delete any existing variable.
Step 4 Pick a naming convention
core-<component> base · --<variant> · --<size> · __<part> · is-<state>.
Carlos's locked table lists Button, Card, Modal, Input, Badge, Tabs, Toggle, Spinner, Alert. New components extend the table in-PR.
Step 5 Refactor in 5 batches — COMPLETE
| Batch | Components | Tests | Status |
|---|---|---|---|
| A | Button, Badge, 4 spinners, ErrorAlert, ProgressBar, Toggle, Counter, EmptyState | Contract tests ✓ | ✅ Done |
| B | Input, TextArea, Select, SearchInput, FilterChips, MultiLanguageInput, form primitives | Contract tests ✓ | ✅ Done |
| C | Card, Modal, Sidebar, TopBar, BottomNav, Tabs, Table, MobileTable, PageHeader, FilterContainer, PublicLayout, Footer, HeroSection, MobileSidebar | Contract tests ✓ | ✅ Done |
| D | FeatureCard, FloatingActionButton, VersionDisplay, PullToRefresh, PageActionBar, SidePanel/Layout, Timeline, SwipeCard, PreReservaConfirmation, PrintableComponent, LoginExtrasPanel, WizardContainer, WizardProgress, UploadProgressBar, form primitives | 96 tests ✓ | ✅ Done |
| E | AvatarDisplay, AvatarSelector, UserAvatarManager, CoreIconPicker, SystemBrandingHeader, StylesInitializer, BrandingLogo, HappnestBrandmark, OptimizedImage | — | ✅ Done |
Deferred (Batch F): experiences/steps/*.tsx (6 large form components), UniversalLoginForm, UniversalRegisterForm, RegisterEnterpriseForm.
These are ESLint-ignored or heavily Tailwind-coupled forms that require a dedicated PR.
HappNest yellow #f7cf08 intentionally preserved in wordmark SVG.
Step 6 Update Storybook
data-theme on <html>, toggles default / client / webcheckin from the toolbar. Minimum: a story per top-10 component with each variant + size.
Step 7 Tests — Jest contract tests complete
classNamematchescore-<name>BEM conventions- Variant/size modifiers applied correctly
- No Tailwind color or hex utilities in rendered
classNamestrings - Props (ref, disabled, aria, children) respected
Playwright visual regression: planned for follow-up PR (each variant × each theme ≈ 90 screenshots).
Step 8 Wire consumers — Done for client
packages/client/src/app/layout.tsx imports
@pamaconu/core/styles/globals.css which in turn @imports the component recipes.
Webchecking: pending — needs one import line. Admin + enterprise: no change needed.
Recipe catalog (2 300+ lines in
dist/styles/components.css):
core-btn, core-badge, core-spinner, core-error-alert,
core-progress-bar, core-toggle, core-counter, core-empty-state,
core-input, core-select, core-textarea, core-search-input,
core-filter-chips, core-multilang, core-form-*,
core-card, core-modal, core-tabs, core-table,
core-mobile-table, core-sidebar, core-mobile-sidebar,
core-topbar, core-bottom-nav, core-page-header,
core-filter-container, core-hero-section, core-footer,
core-public-layout,
core-feature-card, core-fab, core-version-display,
core-pull-to-refresh, core-page-action-bar,
core-side-panel, core-side-panel-layout,
core-timeline, core-swipe-card,
core-prereserva-confirm, core-printable,
core-login-extras, core-wizard, core-wizard-progress,
core-upload-progress,
core-avatar, core-avatar-selector, core-user-avatar-manager,
core-icon-picker, core-styles-loading.
Step 9 Documentation
packages/core/README.md. Point the follow-up doc here. Delete .audit.md.
Step 10 Verification gate (before opening PR)
Things to not do
Don't delete any variable from globals.css
Admin + enterprise still reference --primary-50…900. Add the new contract alongside.
Don't ship a second Tailwind build
components.css is hand-authored. A second compiled Tailwind output would grow uncontrolled.
Don't touch admin or enterprise files
They're explicitly out of scope. They keep their monochrome look via the legacy variables.
Don't invent variables outside the contract
If a recipe needs an in-between shade, compute it with color-mix(). New tokens require Carlos's sign-off.