Mall-OS UI (@mall-os/ui)
The dashboard and many tools are built on the common package @mall-os/ui (Tailwind v4, Radix-Primitives, shadcn-like components). Styling and behavior should be adjusted centrally, not duplicated per page.
Theme & Colors
- Single Source of Truth:
packages/ui/src/cockpit-dashboard-theme.css— all design tokens as HSL components (--background: 216 20% 97%…), ensuring the dashboard (Tailwind v3) and@mall-os/ui(Tailwind v4) utilize the same values. - Dashboard: imports the file in
apps/dashboard/src/app/globals.cssvia@import "@mall-os/ui/cockpit-dashboard-theme.css". - UI Package:
packages/ui/src/styles.cssimports the same file and maps@theme inlinetohsl(var(--…)). - Page Background (light):
--backgroundcorresponds to about#f6f7f9; Cards (--card) and Sidebar remain white for clear contrast. - Primary Color: clear blue for CTAs, focus, and active sidebar elements (light/dark adjusted accordingly).
- Radius:
--radiuscontrolsrounded-*using the theme scale (radius-sm…radius-xl). - Card Shadow:
--shadow-cardand--shadow-card-hoverincockpit-dashboard-theme.css;Cardusesshadow-[var(--shadow-card)]. - Secondary Text:
--muted-foreground(and related areas like--muted) are set so helper text and descriptions remain readable on light backgrounds (sufficient contrast to gray areas).
Changes to the “global appearance” should be made in cockpit-dashboard-theme.css (not duplicated in globals.css).
Dialog, AlertDialog & Sheet (Modals)
- Content:
bg-card/text-card-foreground(notbg-background), so the window stands out from the page area. - Overlay: about
bg-black/55andbackdrop-blur-smfor a clear dim layer. - Elevation:
shadow-2xland a subtleringfor depth. - z-index:
z-[10400]— above dropdown popovers, which often use~9999in the dashboard; same layer for Dialog, AlertDialog, and Sheet.
Implementation: packages/ui/src/components/ui/dialog.tsx, alert-dialog.tsx, sheet.tsx. Custom “fullscreen” overlays in the dashboard should use the same z-layer and similar card appearance, instead of z-[200] + page background color.
Dashboard Audit (manual): among others, content/hot-picks, scheduling/videos, gallery lightbox (gallery-block.plugin) — overlays at z-[10400] + bg-black/55 / backdrop-blur-sm or lightbox intentionally dark. Other apps (center-website template copies of dialog.tsx, digital-signage, center-manager) still use their own z-50 variants; update if necessary or switch to @mall-os/ui.
- Form controls:
Input,SelectTrigger, andTextareaconsistently usebg-card,border-border,h-11(Input/Select default),shadow-sm, and the same focus rings — so that search and dropdowns appear the same on gray page backgrounds. Filter bars: icon left in the trigger viaflex min-w-0 flex-1+SelectValue; search with magnifying glass icon (Search), not with filter icon. - Select & DropdownMenu (no layout jump): both use
modal={false}by default in@mall-os/ui(select.tsx,dropdown-menu.tsx). Thus, Radix does not set a body scroll lock; the page does not jump horizontally when a menu opens. Additionally,SelectContent/DropdownMenuContentprevent browser-side scrolling to the focused portal (typical “jolt” on long pages) viaonOpenAutoFocus→preventDefault(). For specific cases,modal={true}can be set (e.g., stronger focus trap). Additionally, the dashboard reserves space inglobals.csswithscrollbar-gutter: stablefor the scrollbar.
Dashboard Page Shell (PageContainer)
Standard for content pages in the dashboard (not for hub tiles or redirects):
import { PageContainer, PageContent, PageHeader } from "@mall-os/ui";
export default function ExamplePage() {
return (
<PageContainer>
<PageHeader title="Title" description="Short description" actions={…} />
<PageContent>{/* cards, tables, forms */}</PageContent>
</PageContainer>
);
}
| Pattern | When |
|---|---|
PageContainer + PageHeader + PageContent | Lists, detail, forms, settings |
CockpitSectionHub | Module hubs (Website, Staff, Organisation, Editorial flow) |
SocialPageShell / SocialHubView | Social cockpit |
redirect() / CenterTabRedirect | Legacy paths, tab deep links |
Thin re-export (import X; export default X) | Route → shared component with shell |
Status badges centralized: apps/dashboard/src/lib/content-status-badges.ts — shared chip components under apps/dashboard/src/components/badges/ (e.g. template approval, QR type). Imports: always @/lib/content-status-badges, not relative ../../../lib/… paths.
Check:
node scripts/check-dashboard-page-shell.mjs # report
node scripts/check-dashboard-page-shell.mjs --strict # CI / prebuild
node scripts/check-dashboard-import-aliases.mjs --strict
Dashboard Layout Shell
The fixed chrome (sidebar inset, sticky header, content padding) is centralized in apps/dashboard/src/components/cockpit-dashboard-shell.tsx (CockpitDashboardShell, cockpitDashboardShellClasses). The dashboard layout binds this component rather than duplicating classes.
Action Buttons
ActionButton / CreateButton / … in packages/ui/src/components/ui/action-button.tsx utilize button variants (default, outline, destructive, ghost, success) and supplementary className tokens (primary, success, muted, border …) instead of fixed Tailwind colors like blue-600.
Bunny Upload Types
BunnyUploadFolderTypeis exported inpackages/ui/src/hooks/use-bunny-upload.tsand describes allowedtypevalues for uploads (including dashboard-specific folders liketile,shop-chain,themes,dooh,wayfinding,videos,images).- The dashboard imports this type for
apps/dashboard/src/hooks/use-bunny-upload.tsandbunny-upload.tsx, so props and hooks use the same union.
Button & Dashboard-@/components/ui/*
- Primitives: form, layout, and overlay components (Input, Card, Dialog, Tabs, Sidebar, Toast, …) are presented in the dashboard as re-exports from
@mall-os/ui— an implementation underpackages/ui/src/components/ui/, paths like@/components/ui/inputremain valid. - Exceptions (local in the dashboard):
bunny-upload.tsx(extended upload types + dashboard hook),simple-rich-text-editor.tsx,rich-text-editor.tsx,safe-html-content.tsx. - Button:
packages/ui/src/components/ui/button.tsx(Button,buttonVariants); dashboard file re-exports it. - Tailwind: dashboard
contentincludes../../packages/ui/src/**so that classes from the package come through in JIT.
Core Components (Excerpt)
| Area | File under packages/ui/src/components/ui/ |
|---|---|
| Buttons, Inputs, Textarea | button.tsx, input.tsx, textarea.tsx |
| Selection | select.tsx, checkbox.tsx, radio-group.tsx, switch.tsx |
| Overlays | dialog.tsx, alert-dialog.tsx, sheet.tsx, popover.tsx |
| Menus & Toasts | dropdown-menu.tsx, sonner.tsx, toast.tsx |
| Tabs | tabs.tsx |
| Cards & Layout | card.tsx, separator.tsx, page-layout.tsx |
| Avatar | avatar.tsx |
| Alerts | alert.tsx, alert-banner.tsx |
Notes (Alert & AlertBanner)
- Files:
packages/ui/src/components/ui/alert.tsx,alert-banner.tsx. - Alert: shadcn pattern — rounded edge, subtle background, icon on the left, title + description. Variants:
default(info/blue),destructive,warning,success. No manualborder-blue-200 bg-blue-50or similar. - AlertBanner: page notice with optional action on the right (button/link). Builds on
Alert— for messages like “Many approvals pending”, pending drafts, setup notices. - Form Sections (e.g., “Quick Creation”): normal
Cardwithoutborder-l-4, shadow comes fromcard.tsx. - Quick Action Tiles (news list):
Cardwithhover:shadow-md, icon inbg-primary/10— no colored border/background combinations.
import { AlertBanner, Button } from "@mall-os/ui";
<AlertBanner
variant="warning"
title="Many approvals pending!"
description="26 news items waiting for approval."
action={<Button size="sm" variant="outline">Edit all</Button>}
/>
Simple inline notices without action: Alert + AlertTitle + AlertDescription directly.
Semantic Surfaces & Layout Building Blocks
Centralized in @mall-os/ui — no duplicated bg-*-50 classes in the dashboard.
| Component | File | Use |
|---|---|---|
Surface | surface.tsx | Highlighted sections (info, success, warning, destructive, muted) |
StatTile | stat-tile.tsx | KPI / metric (number + label, optional icon) |
SelectableCard | selectable-card.tsx | Selection tiles (AI mode, quick setup) |
KanbanColumn | kanban-column.tsx | Board columns with token-based header/body |
StatusBadge | status-badge.tsx | Unified status chips (published, pending, …) |
Badge variants (outline-success, active, …) use design tokens (--success, --warning, --info) — changes in cockpit-dashboard-theme.css apply globally.
Hub Pages (CockpitSectionHub)
- File:
apps/dashboard/src/components/cockpit-section-hub.tsx - Tile icons via
surfaceVariant+Surfacefrom@mall-os/ui(info,success,warning,muted, …) — not viacolorTailwind strings. - Used in: Center website, Staff, Editorial flow, Organisation.
Content Status Badges (Dashboard)
- File:
apps/dashboard/src/lib/content-status-badges.ts— badge variants (news, events, pages, jobs, offers, …) - File:
apps/dashboard/src/lib/content-ui-tokens.ts— links (contentDetailLinkClass), metric colors (contentMetric), list icons, form helpers, detail view tokens - File:
apps/dashboard/src/components/content/category-select-option.tsx— unified category row in create/edit selects - File:
apps/dashboard/src/components/content/content-form-footer.tsx— fixed form footer (center, status, scheduled publishing) using the same tokens; e.g. shops, jobs, services, offices, hot picks, offers, news, events, shop locations and categories (edit). PropshowCenterSelect={false}when center assignment is fixed.
Social Hub (SocialHubView)
- File:
apps/dashboard/src/components/social/social-hub-view.tsx - KPI tiles and action cards use
Surfacefrom@mall-os/ui; “More” tiles shareHUB_ICON_TONEwithCockpitSectionHub.
Quality Assurance
pnpm check:dashboard-ui # exit 1 on violations (strict, repo root)
pnpm check:dashboard-ui-slop # AI slop patterns (spinners, side tabs, …)
pnpm check:dashboard-page-shell # page shell for dashboard/**/page.tsx
pnpm check:dashboard-import-aliases # no relative ../lib imports
pnpm --filter @mall-os/dashboard run check:ui-tokens # like prebuild
pnpm --filter @mall-os/dashboard run check:ui-slop # like prebuild
node scripts/check-dashboard-ui-tokens.mjs --strict
node scripts/check-dashboard-ui-slop.mjs --strict
node scripts/check-dashboard-import-aliases.mjs --strict
node scripts/migrate-dashboard-color-classes.mjs # codemod (optional --dry-run)
The UI token and slop scripts run automatically in dashboard prebuild with --strict. GitHub workflow .github/workflows/dashboard-quality.yml provides fast PR feedback (UI tokens, slop check, page shell, import aliases, dynamic route slug check).
Cursor rule for new dashboard UI: .cursor/rules/dashboard-ui-tokens.mdc.
Avatar
- File:
packages/ui/src/components/ui/avatar.tsx(Radix Avatar). - Display: root as
inline-flexwithitems-center,justify-center,leading-none,overflow-hidden,rounded-full— avoids layout gaps due toinline-IMG baseline. - Image:
AvatarImagewithblock,h-full,w-full,object-cover,object-center— circle is reliably filled;max-h-full/max-w-fullagainst conflicts with global IMG rules (e.g., Preflight).
Switch (Toggle)
- File:
packages/ui/src/components/ui/switch.tsx. - Off: track
bg-muted-foreground/25(light) or/40in dark mode,border-muted-foreground/45— clear gray “trench”; thumbbg-white(orzinc-100dark) withborder-2,shadow-md. - On: track
bg-primary, thumbbg-primary-foreground; slightly larger hitbox (h-7,w-[3.25rem], thumbh-6 w-6) for better left/right recognizability.
Tabs
<Tabs> uses only the segmented look (Social Cockpit) — rounded bar bg-muted/30, border, horizontal scroll when needed; inactive text-muted-foreground, active bg-background, shadow-sm, ring-1 ring-primary/40. Styles live in packages/ui/src/segmented-tabs.css (data-slot), independent of the Tailwind content scan.
Route-based sub-navigation (e.g. Social Cockpit) uses SegmentedNav / SegmentedNavLink in apps/dashboard/src/components/dashboard/segmented-nav.tsx.
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@mall-os/ui";
<Tabs defaultValue="a">
<TabsList className="w-full">
<TabsTrigger value="a">One</TabsTrigger>
<TabsTrigger value="b">Two</TabsTrigger>
</TabsList>
…
</Tabs>
Do not override TabsList with grid / flex-wrap — spacing (mb-4, max-w-3xl) is fine.
Spinner & anti-slop
What: Unified loading indicator <Spinner /> from @mall-os/ui instead of hand-rolled border-b-2 divs, side-tab border-l-4, side-stripe divs (absolute inset-y-0 left-0 w-1 … — same slop pattern as border-l-4, implemented as a pseudo bar), gradient headings, or hardcoded bg-blue-* / bg-gray-* in core UI components.
Why: These patterns (“AI slop”) look generic, break the token system, and diverge across pages.
Where: packages/ui/src/components/ui/spinner.tsx — sizes xs | sm | default | lg | xl. In buttons: className="mr-2 text-primary-foreground" or text-destructive instead of border-white.
import { Spinner } from "@mall-os/ui";
{loading ? <Spinner className="mx-auto" /> : null}
Check: pnpm check:dashboard-ui-slop (strict in CI and dashboard prebuild). Script: scripts/check-dashboard-ui-slop.mjs.
v0 frontends (Vercel, not this repo): Impeccable — npx impeccable detect app/ components/ lib/. See v0 Instructions Part C/J.
Do not use: contentLoadingSpinnerClass / ring variants in content-ui-tokens.ts — removed in favor of <Spinner />.
Where Not to Touch
- Center-Website Templates can have their own copies under
apps/center-website/.../components/ui/— not all changes to@mall-os/uiapply automatically there. - One-off layout special cases in the dashboard: prefer to set
classNameon the page rather than duplicating new variants without need.
See Also
packages/ui/src/components/ui/index.ts— export listpackages/ui/src/cockpit-dashboard-theme.css— design tokens (SSoT)packages/ui/src/styles.css— Tailwind v4 +@theme(imports the theme)
Nutzungsstatistik: Seitenaufrufe werden anonymisiert erfasst. Im Umami-Dashboard nach diesem Pfad filtern: /en/developer-guide/mall-os-ui