TIP
This page mirrors .agents/ui.md from the repository. Edit it at the source, not here.
UI Guidelines
Use this when adding or refactoring UI in apps/web or reusable components in packages/ui.
This file is the source of truth for app UI composition, extraction decisions, and shared component boundaries. For route/file placement, follow TanStack patterns. For complex client-owned state shared across components, follow Zustand state management.
Goals
- Prefer existing primitives from
@saasweave/uibefore creating new app-local ones. - Keep
packages/uireusable and app-agnostic. - Keep route files thin and UI composition in pages, features, widgets, or
shared/ui. - Extract shared UI only when reuse and app-agnostic boundaries are real.
Default Sources
- Prefer existing components from
@saasweave/ui/components/*before creating new app-local primitives. - Prefer app wrappers from
apps/web/src/shared/uiwhen the app already owns routing, image, locale, or other app-specific integration details. - Use
lucide-reactfor icons unless an existing asset or brand graphic is the better fit. - Use
@saasweave/ui/lib/utilscn(...)for class composition.
App Composition
- Build page UI in
pages/, composite sections inwidgets/andfeatures/, and app-level primitives inshared/ui. - Prefer composing
@saasweave/uiprimitives instead of duplicating styling across many leaf components. - Keep route files thin. Put UI composition in page, feature, widget, or shared components, not in route files.
Extraction Rule
- Extract a component to
packages/uiwhen it is reusable, app-agnostic, and the shared package can own its styling and accessibility. - Keep a component app-local when it depends on route params, current locale, auth state, app SEO, app config, or other app-owned integrations.
- If reuse is still speculative, keep the component app-local first and extract after a second real use case.
Shared Package Boundaries
packages/uimust not import fromapps/*, TanStack Router, or app env modules.packages/uiexposes explicit component/hook/style subpaths; do not add a wildcard export or import package internals outsidepackage.json#exports.- Keep semantic HTML, styling, layout, and accessibility inside the shared component.
- Keep CDN, proxy, locale, router, and analytics behavior outside shared UI primitives.
- Provide sensible defaults so shared components still work without injected app-specific dependencies.
- Keep colocated shared UI state near the component family that owns it.
Dependency Injection
When a shared component needs an app-specific primitive, inject it instead of importing it.
- Navigation: accept
linkComponent?: React.ElementType, default to"a". - Media: accept
imageComponent?: React.ElementType, default to the sharedpackages/uiimage primitive or a semantic fallback. - Future wrappers: apply the same pattern for things like video, avatar, markdown, or analytics-aware buttons.
- Keep injected component props optional and forward the smallest useful prop surface.
- When router links may need either router
toor plainhref, support both and let the injected link decide what to use. - For configurable media, keep related props grouped in a nested object instead of scattering env-specific props across the component API.
Decision Rule
- Use prop injection when only data or one-off config changes per usage.
- Use component injection when behavior or implementation changes per app.
- Prefer component injection when the same app-specific dependency would otherwise be repeated across many call sites.
- For shared UI state that coordinates sibling components inside
packages/ui, prefer a small colocated Zustand store over prop drilling or React context only to shuttle simple open/close state. Follow Zustand state management for store shape and selectors.
Reference pattern:
tsx
type CardProps = {
linkComponent?: React.ElementType;
imageComponent?: React.ElementType;
href: string;
image?: {
src: string;
alt?: string;
siteBaseUrl?: string;
imgProxyBaseUrl?: string;
};
};shadcn Usage
- The repo uses shadcn with the
base-maiastyle,neutralbase color, CSS variables, and Lucide icons. - Add reusable shadcn-derived components to
packages/uiwhen they can stay app-agnostic. - Keep a component in
apps/webwhen it depends directly on app routing, auth, SEO, locale, or app config. - Keep app-level wrappers and glue code in
apps/web/src/shared/ui. - Keep reusable app composites that depend on product copy, routing, permissions, or domain records in
apps/web/src/shared/ui; they are not genericpackages/uiprimitives merely because several pages use them.
Images And Links
- Prefer app-owned wrappers for images or routing-aware links when the app needs locale, router, or env-specific behavior.
- Shared components should accept injected link or image components instead of importing app wrappers directly.
- Keep base URL, CDN, and proxy decisions outside shared primitives.
Do And Don't
- Do keep
packages/uireusable and free of app imports. - Do keep styling and accessibility inside the shared component.
- Do extract app-specific wrappers into
apps/web/src/shared/ui. - Don't hardcode router, locale, analytics, or env behavior into
packages/ui. - Don't introduce React context or excessive prop drilling for shared UI state when a colocated store is enough.
- Don't expose app-specific implementation details unless reuse requires them.