adres-design-system
v0.1.0
Published
Adres design system monorepo — white-label, multi-brand, Vignelli-rooted.
Readme
Adres Design System
A white-label, multi-brand design system for Adres (proptech, part of Aldar). Built on Next.js 16 · React 19 · Tailwind CSS 4 · Radix · Motion · class-variance-authority · TypeScript strict.
The default brand is Adres (Airbnb-DLS-inspired palette + Switzer typography). New brands are added by dropping in a new CSS variable file — components never know which brand they're wearing.
First-time setup
This repo uses npm workspaces (npm 10+). The repository was scaffolded inside a Cowork sandbox whose filesystem couldn't complete the install, so clean any leftover artifacts and install fresh:
cd ~/repos/adres-design-system
# Remove any partial install state from the scaffold step
rm -rf node_modules package-lock.json pnpm-lock.yaml pnpm-workspace.yaml _tmp_*
# Install everything
npm installThen:
npm run docs # legacy docs at http://localhost:3000
npm run docs:v2 # Fumadocs at http://localhost:3003
npm run playground # playground at http://localhost:3001 (brand switcher)
npm run storybook # storybook at http://localhost:6006 (install docs + components)
npm run build-storybook # rebuild packages/design-system/storybook-static for deployment
npm run storybook:static # serve prebuilt static only (no live reload)
npm run build # turbo: build every package and app
npm run typecheck # strict TypeScript across the workspaceWorkspace deps use
"@adres/design-system": "*"— npm 7+ resolves the*to the local workspace package automatically.
Consuming from npmjs (apps outside this monorepo)
Packages are published publicly to npmjs.com under the @adres scope. No special .npmrc is required for install.
npm install @adres/design-system @adres/brand-adres
# peers: react@19, react-dom@19, tailwindcss@4Wire CSS (order matters):
@import "tailwindcss";
@import "@adres/brand-adres/theme.css";
@import "@adres/brand-adres/overrides.css";
@import "@adres/design-system/styles";Activate the brand:
<html data-brand="adres">import { Button } from "@adres/design-system"Consumers need a bundler that can compile TypeScript from node_modules (Next.js, Vite, etc.). Package exports currently point at src/ (no separate dist build).
Publishing (maintainers)
- Own or create the
@adresorg on npmjs; enable 2FA. npm loginnpm run typecheck && npm run validate:brands && npm run sync:skills && npm run validate:skills- Bump package versions before re-publishing (npm rejects duplicate versions).
npm run publish:npm— publishes in order:brand-contract→design-system→ brands.
Patch-releasing a single package
Most changes (e.g. a design-system component tweak) only touch one or
two packages. Don't run the full npm run publish:npm script for
these — npm rejects re-publishing a version that hasn't changed, so it
will error on every other package that wasn't bumped.
Not sure which packages actually changed? Diff the local source against what's currently published — don't trust memory or a stale CHANGELOG:
npm pack @adres/brand-thinkprop@<published-version>
tar -xzf adres-brand-thinkprop-*.tgz
diff -rq package/src packages/brand-thinkprop/src# 1. Confirm the working tree is clean and the change is committed
git status
# 2. Bump each affected workspace's version — updates its package.json,
# and by default also commits + tags "<name>@<version>" (one bump
# commit covering all `-w` targets when passed together)
npm version patch -w @adres/design-system -w @adres/brand-thinkprop
# 3. Push the commit and the tags
git push && git push --tags
# 4. Verify, then publish only the changed packages
npm run typecheck && npm run validate:brands
npm publish -w @adres/design-system --access public
npm publish -w @adres/brand-thinkprop --access public
# 5. Confirm they landed
npm view @adres/design-system version
npm view @adres/brand-thinkprop versionIf the change touches @adres/brand-contract, publish that one first —
every other package depends on it (see publish order above). Consumers
then pick up the fix with a normal npm update @adres/design-system.
No changesets/lerna here — versions are hand-bumped per workspace. See
.cursor/skills/adresx-release-deploy/SKILL.mdfor the target state (adopting Changesets) once manual bumps start drifting.
Dual registry later (Azure Artifacts)
npmjs is the primary registry for testing. When you are ready for a private Aldar feed, publish the same version again with a user/CI Azure .npmrc (never commit tokens):
npm publish -w @adres/design-system \
--registry=https://pkgs.dev.azure.com/adres-dari-dev/Adres-Design/_packaging/adres-packages/npm/registry/Repository layout
adres-design-system/
├── apps/
│ ├── docs/ # Next.js docs site (legacy hand-rolled)
│ ├── docs-v2/ # Fumadocs docs (same content, MDX + search)
│ ├── playground/ # Next.js live preview with brand switcher
│ └── playbook/ # Config-driven guidelines (playbook-kit)
├── packages/
│ ├── design-system/ # Components, hooks, utilities. Brand-agnostic.
│ ├── brand-adres/ # Adres default brand tokens (Rausch / Babu / Arches / Switzer)
│ └── brand-template/ # Skeleton brand for cloning when adding a new white-label theme
├── package.json # workspaces + turbo scripts
├── turbo.json
├── tsconfig.base.json
└── README.mdArchitecture in three sentences
- Brand is a layer, not a fork. Components reference semantic CSS variables (
--primary,--background, …) — never brand colours. - One source of truth per token. Spacing, radius, motion, type, colours all live as named tokens. No magic numbers in components.
- Vignelli, not maximalism. Typography first, grid is law, hierarchy through scale and weight, motion only when it communicates.
Three token layers
| Layer | Lives in | Example |
|---|---|---|
| 1 — Brand primitives | packages/brand-* | --rausch-600: 358 100% 68% |
| 2 — Semantic tokens (the contract) | packages/brand-* | --primary: var(--rausch-600) |
| 3 — Component tokens | packages/design-system | --button-padding-x (when introduced) |
Components consume layer 2 only, via Tailwind utilities (bg-primary). The mapping from semantic vars to utilities lives in packages/design-system/src/styles/base.css (@theme inline).
How a colour reaches a pixel
1. Brand defines the primitive: --rausch-600: 358 100% 68%
2. Brand wires the semantic: --primary: var(--rausch-600)
3. Design system maps it: --color-primary: hsl(var(--primary)) /* @theme inline */
4. Component consumes it: className="bg-primary"Steps 1 + 2 are brand-specific. Steps 3 + 4 never change. To re-skin, only steps 1 + 2 change.
Adding a new brand
cp -R packages/brand-template packages/brand-acmeThen in packages/brand-acme:
- Update
package.jsonnameto@adres/brand-acme. - In
src/theme.css, change every:root[data-brand="template"]selector to:root[data-brand="acme"]. - Replace primitives (your brand's colour scales, fonts, radius).
- Wire every semantic token (
--background,--primary, etc.) — keep the names identical. - Provide a parallel
.darkblock. - Update
src/tokens.tswith the typed metadata mirror.
Wire it into the app's globals.css:
@import "tailwindcss";
@import "@adres/brand-adres/theme.css";
@import "@adres/brand-acme/theme.css"; /* new */
@import "@adres/brand-acme/overrides.css"; /* optional */
@import "@adres/design-system/styles";Register in apps that ship a brand switcher (playground):
"@adres/brand-acme": "*"in the apppackage.json@importthe brand CSS files inglobals.css(see above){ id: "acme", name: "Acme" }inapps/playground/app/themes.ts
Activate at runtime by setting <html data-brand="acme">. The playground ThemeProvider syncs data-brand on <html> — your app does the same.
Validate with npm run validate:brands (checks REQUIRED_SEMANTIC_CSS_VARS and playground wiring). After structural changes, also run npm run sync:skills && npm run validate:skills to keep agent docs in sync.
v1 component inventory
Web App — Button, Input + Field + Label, Card (CardHeader/Title/Description/Content/Footer), Dialog, Tabs, Badge, Alert, Toaster (sonner), Sidebar (collapsible), TopNav + Breadcrumbs.
Marketing — Hero, FeatureGrid, CTA.
All components:
- Composable (CVA where variants apply)
- Accessible (Radix where appropriate, semantic HTML, ARIA, keyboard, focus-visible)
- Responsive (mobile-first, sensible breakpoints)
- Motion-aware (uses
useReducedMotionand themotion-reduce:Tailwind utilities)
Planned for v2 (not yet implemented)
- Web App: Dropdown menu, Select, Combobox, Data table (sortable/filterable), Empty state, Loading skeletons, Tooltip, Popover, Switch, Checkbox, Radio group, Avatar, Separator
- Marketing: Pricing, Testimonials, FAQ accordion, Footer, Logo cloud, Stats section, Long-form content section
The patterns are established; each component is ~30 minutes of work.
Default Adres brand (Airbnb DLS lineage)
| Token | Value | Note |
|---|---|---|
| Primary | Rausch #FF5A5F | actions, links, brand spark |
| Secondary | Babu #00A699 | secondary actions, data contrast |
| Accent | Arches #FC642D | emphasis highlights |
| Foreground | Hof-warm #484848 | body text |
| Neutral surface | warm gray (24° hue, 4% sat) | not cool blue |
| Typeface | Switzer (Fontshare) | weights 300–700 |
| Radius | 8 / 12 / 16 / pill | generous, not sharp |
| Shadows | warm-tinted, soft | not heavy |
Apps load Switzer from Fontshare CDN by default. For production deployments, self-host the font files and adjust the @import in app/globals.css.
Accessibility
- WCAG 2.1 AA target. Default Adres pairings (Rausch/white, white/Rausch, Hof/neutral-50) clear AA at 14px+.
- Visible 2px focus ring in
--ringon:focus-visibleonly. - Radix primitives handle keyboard and focus management for Dialog, Tabs, Dropdown.
- All inputs and dialogs use proper ARIA. The
Fieldprimitive auto-wiresaria-describedbyfor help/error text andaria-invalidon error. - Icon-only buttons require an
aria-label(TS strict surfaces missing labels). prefers-reduced-motionis respected globally (CSS) and in motion-driving components (viauseReducedMotion).
Responsive
- Mobile-first; everything degrades gracefully from large desktop down to 360px.
- Sidebar collapses to icon-only via the
collapsedprop. - TopNav has start/center/end slots that wrap.
- Hero auto-stacks below
md. FeatureGrid drops 3 → 2 → 1 columns.
Verification status (this scaffold)
| Check | Result | Notes |
|---|---|---|
| design-system source typechecks | ✅ pass | 2 type errors found and fixed during scaffold |
| brand-adres typechecks | ✅ pass | no external deps |
| brand-template typechecks | ✅ pass | no external deps |
| App typechecks | ⚠ deferred | requires complete npm install; sandbox time-budget prevented finishing in-place |
| next build | ⚠ deferred | same reason |
| npm run lint | ⚠ deferred | ESLint configs are wired but not run |
Action item for first local install: run the cleanup + install commands at the top of this README, then npm run typecheck && npm run build to confirm everything compiles in your environment.
Recommended next steps
- Run
npm installlocally — confirm the workspace resolves and apps start. - Self-host Switzer — replace the Fontshare CDN
@importinapp/globals.csswithnext/font/localor self-hosted@font-facedeclarations. - Ship the v2 components in priority order: Dropdown, Select, Tooltip, Skeleton, Avatar — these unblock most app surfaces.
- Add a Data Table with sort/filter/pagination — Adres listing dashboards will need this immediately.
- Add Storybook or a route-per-component docs view so each component has live + code in one place.
- Set up a visual regression suite (Chromatic, Percy, or Playwright snapshots) — multi-brand systems are a perfect fit.
- Add a ColorPair contrast test in CI that ensures every brand's
--primary / --primary-foreground,--background / --foreground, etc. clear WCAG AA. - Wire up
next/fontin both apps so font loading is optimized rather than CDN-blocked. - Token-doc generator — emit a JSON token reference from each brand's
tokens.tsfor use by Figma plugins or other tooling. - Internationalisation + RTL — Adres operates in the UAE; an RTL switch and Arabic typography pairing belong on the v2 roadmap.
License
Proprietary — internal Adres / Aldar use.
