@busflow/ui-core
Generic, framework-agnostic ShadCN/Tailwind components shared across all busflow applications. Strictly presentational — no API awareness, no store access, no route knowledge.
Reference Documents
- [frontend-architecture.md](file:///Users/julianbruning/Projekte/busflow/busflow/docs/2-areas/architecture/frontend.md) — Package constraints and boundaries
- [config-tailwind](file:///Users/julianbruning/Projekte/busflow/busflow/packages/config-tailwind/README.md) — Visual identity, design tokens
- [guidelines.md](file:///Users/julianbruning/Projekte/busflow/busflow/docs/2-areas/process/guidelines.md) — Documentation standards
Package Rules
From [frontend-architecture.md](file:///Users/julianbruning/Projekte/busflow/busflow/docs/2-areas/architecture/frontend.md):
- Exports raw
.vueand.tsfiles via centralindex.ts "sideEffects": falseinpackage.jsonfor tree-shakingvuelisted aspeerDependency- No API calls, no store access, no route awareness — data via props and events only
Component Inventory
The following components are currently migrated to the new design system architecture (based on shadcn-vue/Reka UI):
| Component | Status | Description | |----------------_|--------|-------------| | Button | ✅ Ready | Standard button with standard variants (default, destructive, outline, secondary, ghost, link) and sizes (default, sm, lg, icon). | | Badge | ✅ Ready | Standard badge with variants (default, secondary, destructive, outline). | | Alert | ✅ Ready | Standard alert component containing Alert, AlertTitle, and AlertDescription. Variants: default, destructive. | | Card | ✅ Ready | Generic content container holding Card, CardHeader, CardTitle, CardDescription, CardContent, and CardFooter. | | Input | ✅ Ready | Standard text input field. | | SectionHeader | ✅ Ready | Custom composite component for section titles with label, title, and subtitle props. |
More components will follow as we migrate them from busflow-ui.
File Structure
packages/ui-core/
├── src/
│ ├── components/
│ │ ├── ui/ # shadcn-vue components
│ │ │ ├── button/
│ │ │ │ ├── Button.vue
│ │ │ │ ├── Button.story.vue
│ │ │ │ └── index.ts
│ │ │ ├── card/
│ │ │ │ ├── Card.vue
│ │ │ │ ├── CardHeader.vue
│ │ │ │ ├── ... # CardTitle, CardContent, etc.
│ │ │ │ └── index.ts
│ │ │ └── ... # alert/, badge/, input/
│ │ └── SectionHeader/ # Custom (non-shadcn) components
│ │ ├── SectionHeader.vue
│ │ └── index.ts
│ ├── lib/
│ │ └── utils.ts # cn() helper (clsx + tailwind-merge)
│ ├── style.css # Tailwind + busflow tokens + shadcn CSS vars
│ └── index.ts # Central barrel export
├── components.json # shadcn-vue CLI config
├── package.json
├── tsconfig.json
└── README.mdAuthoring Conventions
Naming
- Component files: PascalCase (
Button.vue,SectionHeader.vue) - Component directories: lowercase for shadcn (
ui/button/), PascalCase for custom (SectionHeader/) - Props: camelCase, typed with TypeScript interfaces
- Events: kebab-case,
emit('update:modelValue')for v-model - CSS: Tailwind semantic tokens only (
bg-primary,text-foreground); no hardcoded hex in components
Component API
defineProps<T>()with TypeScript genericsdefineEmits<T>()for typed eventscn()from@/lib/utilsfor class merging- CVA (
class-variance-authority) for variant definitions - Slots for composition:
default,header,footer,icon,actions - Variants follow shadcn conventions:
'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
Accessibility
- All interactive components: keyboard navigable
aria-*attributes, WCAG 2.1 AA contrast ratios:focus-visiblefor keyboard-only focus rings- Form components:
id/forlabel association
Theme Polymorphism
For the full architecture see design-system architecture. Theme scopes are defined in
config-tailwind.
Components strictly use semantic tokens (bg-primary, text-foreground) — hardcoded hex values are forbidden. The parent .theme-* class swaps the entire palette automatically. Components may also adapt their physical shape per environment:
| Component | .theme-passenger | .theme-dispatcher | .theme-driver |
|---|---|---|---|
Button | shadow-soft-warm, 300ms hover | Sharp 4px, ring-offset focus | min-h-14 (56px), active:translate-y-[1px] |
Card | font-serif header, warm shadow, no border | Tight p-4, 1px border, no shadow | p-6, dark gray bg, semantic left-borders |
Dialog | Centered, glassmorphism backdrop | Centered, sharp, snappy animations | Bottom-docked Action Sheet, 100% width |
Color Resolution Rules
Components resolve colors through three mechanisms, listed in order of preference:
1. Semantic CSS Variables (primary mechanism)
All surfaces, text, and borders must use semantic Tailwind tokens that map to CSS variables defined in config-tailwind and style.css:
<!-- ✅ Correct — automatically adapts to theme + mode -->
<div class="bg-primary text-primary-foreground border-border">
<!-- ❌ Forbidden — hardcoded values bypass the token system -->
<div class="bg-blue-500 text-white border-gray-200">
<div class="bg-[#2E8B6E] text-[#fff]">These variables change automatically when .dark or .theme-* classes are applied at the layout level. Components never need to know which theme or mode is active.
2. currentColor (icon and SVG inheritance only)
Use currentColor exclusively for inline SVGs and icon strokes so they inherit the parent element's color. Lucide icons already follow this convention:
<!-- ✅ Correct — icon matches surrounding text color -->
<svg stroke="currentColor" ...>
<!-- ✅ Correct — Tailwind shorthand -->
<Loader2 class="text-current" />currentColor is not a general theming strategy — it answers "match my parent's text color" but cannot express backgrounds, borders, or semantic intent.
3. dark: Variant (last resort — structural adjustments only)
The dark: Tailwind prefix is restricted to cases where a structural property (opacity, visibility, ring width) genuinely differs between light and dark mode and the token system cannot express the difference:
// ✅ Acceptable — opacity ratio changes by mode
"border-destructive/50 text-destructive dark:border-destructive"
// ❌ Forbidden — swapping colors belongs in the token layer
"bg-white dark:bg-gray-900 text-black dark:text-white"IMPORTANT
If you find yourself reaching for dark: to swap a color, the correct fix is to add or adjust a semantic CSS variable in config-tailwind/theme.css and its .dark override. Components must remain mode-agnostic.
Summary
| Mechanism | Scope | Example |
|---|---|---|
| Semantic CSS vars | All colors, backgrounds, borders | bg-primary, text-foreground, border-border |
currentColor | Icons and SVG strokes only | stroke="currentColor", text-current |
dark: prefix | Opacity / visibility adjustments only | dark:border-destructive (removes /50) |
Layout Patterns
Shared layout primitives used across apps:
Container
| Property | Desktop | Mobile (≤768px) |
|---|---|---|
max-width | 1120px | 100% |
padding-x | space-xl (32px) | space-lg (24px) |
Section
| Property | Desktop | Mobile (≤768px) |
|---|---|---|
padding-y | space-5xl (112px) | space-4xl (80px) |
Common Grid Patterns
| Pattern | Columns | Gap | Usage |
|---|---|---|---|
| Feature grid | 3-col | space-xl | Feature cards, benefit lists |
| Content split | 2-col (60/40) | space-2xl | Text + visual pairings |
| Card grid | auto-fit, min 280px | space-lg | Responsive card layouts |
Section Header Composition
SECTION LABEL — xs, semibold, accent, uppercase, letter-spacing: 0.12em
Section Title — 3xl, extrabold, letter-spacing: -0.025em
Subtitle text, constrained to max-width 620px — lg, secondary colorAnimation Patterns
| Animation | Duration | Usage |
|---|---|---|
fadeInUp | 600ms | Section content on scroll (IntersectionObserver) |
fadeIn | 400ms | Subtle content appearance |
| Stagger delays | 100ms increments | Grouped elements entering sequentially |
Interaction Micro-Animations
- Button hover:
translateY(-1px)+ shadow increase - Card hover:
translateY(-2px)+shadow-md - Focus ring:
0 0 0 3pxaccent @ 0.2 opacity
Responsive Breakpoints
| Name | Value | Target |
|---|---|---|
sm | 640px | Small phones |
md | 768px | Large phones, small tablets |
lg | 1024px | Tablets, small laptops |
xl | 1280px | Desktop |
Usage
<script setup lang="ts">
import {
Button,
Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter,
Badge
} from '@busflow/ui-core'
</script>
<template>
<Card>
<CardHeader>
<div class="flex items-center justify-between">
<CardTitle>Trip Details</CardTitle>
<Badge variant="default">Active</Badge>
</div>
<CardDescription>Berlin → Munich via Nuremberg</CardDescription>
</CardHeader>
<CardContent>
<p>Passenger manifest and route information.</p>
</CardContent>
<CardFooter>
<Button variant="outline" size="sm">View Details</Button>
</CardFooter>
</Card>
</template>