@my-bid/ui
v0.14.0
Published
MyBid storefront design system — Tailwind v4 tokens, shadcn/Radix primitives, and generalized Header/Footer shells.
Readme
@my-bid/ui
MyBid storefront design system: Tailwind v4 design tokens, shadcn/Radix
primitives, and generalized (prop-driven) Header/Footer shells. Extracted
from mybid-landing (the most mature MFE) — this package does not redesign
anything, it packages the existing system so every other MFE can consume it
instead of re-implementing it.
Source of the design system remains docs/frontend/frontend-architecture-standard.md
§8. This package is the distribution of that system, not a second spec.
Install
@my-bid/ui is published to GitHub Packages under the @mybid scope.
Add a project-level .npmrc (do not put the token in it — it is read from
the environment):
@my-bid:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}NODE_AUTH_TOKEN must be a GitHub token with read:packages (CI already
exports one via secrets.GITHUB_TOKEN; locally, use a personal access token
with that scope).
pnpm add @my-bid/uiPeer dependencies
react, react-dom, lucide-react, radix-ui, class-variance-authority,
clsx, tailwind-merge are required peers. next and swiper are
optional peers (peerDependenciesMeta.optional):
nextis optional because no primitive or shell in this package importsnext/imageornext/link— the shells acceptLinkComponent/ImageComponentoverrides instead (see below), so the package itself has no hard runtime dependency on Next. It remains a listed peer because every real consumer today is a Next MFE and version alignment still matters.swiperis optional because only theSwiper/SwiperSlideprimitives need it — a consumer that never renders a carousel shouldn't be forced to install it.
Tailwind v4 wiring (tokens)
This package ships only the token layer — not tailwindcss itself, not
tw-animate-css, not shadcn/tailwind.css. Your MFE's own
app/styles/globals.css still owns those app-level imports; @my-bid/ui
just supplies the @theme inline mapping + :root/.dark variable values
- base layer rules on top:
/* app/styles/globals.css */
@import "tailwindcss";
@import "tw-animate-css"; /* if you use it, same as before */
@import "@my-bid/ui/tokens.css"; /* @theme inline + :root + .dark + base */
/* your own MFE-specific tokens/overrides, if any, go after this line */Tailwind v4 scans classes at build time by walking @source globs; add
this so Tailwind also scans the compiled classes shipped inside
@my-bid/ui's dist/:
@source "../node_modules/@my-bid/ui/dist";(adjust the relative path to wherever your globals.css lives relative to
node_modules).
If you use the Swiper/SwiperSlide primitives, additionally import:
@import "@my-bid/ui/swiper.css";Usage
import { Button, Container, cn } from "@my-bid/ui";
export function Example() {
return (
<Container>
<Button variant="outline" className={cn("mt-4")}>
Click me
</Button>
</Container>
);
}Header/Footer shells
HeaderShell and FooterShell are presentational, prop-driven — they
know nothing about next-intl, routing, or @my-bid/config URL maps. Every
MFE resolves its own translations/URLs and passes plain strings/hrefs in.
import { HeaderShell } from "@my-bid/ui";
import { Link } from "@/shared/i18n"; // your MFE's own locale-aware Link
import { getTranslations, getLocale } from "next-intl/server";
export async function Header() {
const t = await getTranslations("Header");
const locale = await getLocale();
return (
<HeaderShell
LinkComponent={Link}
logo={{ src: "/assets/images/mybid-logo.webp", alt: t("logoAlt"), href: "/", ariaLabel: t("logoAria") }}
infoLinks={[{ label: t("info.about"), href: "/about" }]}
channelLinks={[{ label: t("nav.marketplace"), href: urls.channel("marketplace", locale) }]}
infoNavAriaLabel={t("info.ariaLabel")}
channelNavAriaLabel={t("nav.ariaLabel")}
localeSlot={<LocaleSwitcher />}
searchSlot={<SearchBar />}
actionsSlot={<AuthEntryButton locale={locale} />}
/>
);
}FooterShell follows the same pattern — see
src/shells/footer-shell.tsx for the full prop contract
(FooterShellProps).
ImageComponent
Both shells default ImageComponent to a plain <img>. To get next/image
(recommended for the logo, for LCP), pass a thin wrapper from your MFE so
this package never has to agree with your images.remotePatterns/loader
config:
import Image from "next/image";
const NextLogo = (props: { src: string; alt: string; width?: number; height?: number; className?: string }) => (
<Image {...props} priority />
);
<HeaderShell ImageComponent={NextLogo} /* ... */ />Composed (@my-bid/ui/composed)
HeaderShell/FooterShell above are generic, nav-content-agnostic
primitives. @my-bid/ui/composed ships MyBid's actual site navigation
pre-wired on top of them — SiteHeader/SiteFooter — so every storefront
MFE renders the identical header/footer instead of re-declaring the same
info/channel-nav/footer-column config per app. Ported 1:1 from
mybid-landing's widgets/header/widgets/footer.
Requires @my-bid/config (optional peer — only needed if you import this
subpath) for urls.channel(...) / siteConfig. No next-intl dependency:
you inject t already bound to the "Header"/"Footer" namespace.
import { SiteHeader } from "@my-bid/ui/composed";
import { Link } from "@/shared/i18n";
import { getTranslations, getLocale } from "next-intl/server";
export async function Header() {
const t = await getTranslations("Header");
const locale = await getLocale();
return (
<SiteHeader
t={t}
locale={locale}
LinkComponent={Link}
localeSlot={<LocaleSwitcher />}
searchSlot={<SearchBar />}
actionsSlot={<AuthEntryButton locale={locale} />}
/>
);
}SiteFooter follows the same pattern (t, locale, LinkComponent,
optional ImageComponent/logoSrc/year) — see src/composed/site-footer.tsx
for the full prop contract (SiteFooterProps).
SiteCategoryNav
The header-bottom category bar — a "Kateqoriyalar" mega-menu (CategoryMenu)
plus the top-category strip, all linking out to the marketplace MFE. Ported
1:1 from mybid-landing's widgets/category-nav. Client component with a
progressive top→full-tree fetch (via @tanstack/react-query, a new
optional peer), a skeleton while loading, and renders nothing (null)
if the catalog is unavailable.
Since a server→client boundary can't carry a function reference through
next-intl, SiteCategoryNav takes resolved labels (plain strings, not a
TranslateFn) and a fetchCategories transport you inject — typically your
own same-origin /api/categories route handler called through your app's
shared/api client (this component never calls fetch/axios itself):
"use client";
import { SiteCategoryNav } from "@my-bid/ui/composed";
import type { CategoryMenuNodeInterface } from "@my-bid/ui/composed";
import { useLocale, useTranslations } from "next-intl";
import { requestInstanceSelf } from "@/shared/api/client";
async function fetchCategories(locale: string, depth?: 1): Promise<CategoryMenuNodeInterface[]> {
return requestInstanceSelf.get<CategoryMenuNodeInterface[]>(
`/api/categories?lang=${encodeURIComponent(locale)}${depth ? `&depth=${depth}` : ""}`
);
}
export function CategoryNav() {
const locale = useLocale();
const t = useTranslations("Categories");
return (
<SiteCategoryNav
locale={locale}
fetchCategories={fetchCategories}
labels={{
all: t("all"),
menuAriaLabel: t("menuAriaLabel"),
topAriaLabel: t("topAriaLabel"),
close: t("close"),
}}
/>
);
}Requires a QueryClientProvider ancestor (the same TanStack Query client the
rest of your app already uses).
Versioning
Strict semver via changesets.
Breaking changes bump major and add an entry to MIGRATION.md. Run
pnpm changeset alongside any PR that touches src/ or styles/. Pushing
to main runs the publish workflow: build → typecheck → consumer smoke
test → token-compliance lint → changeset publish (or opens a release PR
if there are pending changesets).
Scope — what this package does NOT include
- No
Field/Label/form-validation primitives — those live withmybid-web-forms(react-hook-form + zod wiring is MFE-local). - No i18n catalogs, no locale-aware routing —
mybid-web-i18n/@my-bid/configown that; shells only accept resolved strings/hrefs. - No data fetching —
@my-bid/api/@my-bid/auth(separatemybid-frontend-corepackage) own that.
