Skip to main content

UI Component Library

packages/ui (@repo/ui) is the shared React component library for Closegram. It is the single source of truth for the look of buttons, form controls, avatars, badges, spinners and other primitives used by the main app (apps/frontend-nextjs) and the admin panel, so both consume the same components instead of re-declaring the same Tailwind strings per file.

The package is developed and previewed in isolation with Storybook (pnpm --filter @repo/ui dev, port 6006), which lets each component render without depending on any app. Components are grouped under the Primitives/ and Foundations/ titles in Storybook.

Importing

The package name is @repo/ui. Everything on the public surface is re-exported from src/index.ts:

import { Button, Badge, Avatar, Spinner, Tooltip } from '@repo/ui';
import { fieldClass, TextInput, Switch, Select } from '@repo/ui';

There are three entry points defined in package.json:

EntryImportContents
Main@repo/uiAll components, class-string constants and the cx helper
Icons@repo/ui/iconsThe full icon set (generic + brand icons)
Styles@repo/ui/styles.cssCompiled Tailwind stylesheet (import once at the app root)

react and react-dom (^18.3.1) are peer dependencies — the consuming app provides them.

The cx helper

cx(...parts) is a tiny className joiner that drops falsy values, used internally by every component and re-exported for call sites:

import { cx } from '@repo/ui';

cx('rounded-lg', isActive && 'bg-cg-blue', className);

Components

Button

Canonical action button — a superset of the variants used across the main app and admin panel. Extends the native <button> attributes (via React.ButtonHTMLAttributes) and forwards a ref.

PropTypeDefaultNotes
variant'primary' | 'secondary' | 'ghost' | 'danger' | 'success' | 'warning' | 'outline-danger' | 'outline-warning''secondary'Visual style
size'sm' | 'md''md'
iconReact.ReactNodeOptional leading icon rendered before children
...restnative <button> propsonClick, disabled, type (defaults to 'button'), etc.
<Button variant="primary" size="md">Guardar</Button>

For the handful of settings/payouts pages that still declare the button style inline, the raw class strings are also exported: primaryButtonClass, secondaryButtonClass, dangerButtonClass, linkButtonClass.

IconButton

Round, ghost icon button that consolidates the repeated w-9 h-9 flex items-center justify-center rounded-full hover:bg-gray-100 … pattern used across headers, modals and toolbars. Extends native <button> attributes and forwards a ref.

PropTypeDefault
size'sm' | 'md' | 'lg' (w-8/9/10)'md'
...restnative <button> props
<IconButton size="md" aria-label="More"><MoreHorizontal /></IconButton>

Badge

Small pill/label. Superset used by the main app and the admin panel.

PropTypeDefault
tone'neutral' | 'accent' | 'info' | 'success' | 'warning' | 'danger''neutral'
iconReact.ReactNode
childrenReact.ReactNode— (required)
classNamestring
<Badge tone="success">Verified</Badge>

Avatar

Circular avatar with a purple→pink gradient-initial fallback when no image is set.

PropTypeDefaultNotes
srcstring | nullFalls back to an initial when absent
altstring''
namestringFirst character shown as the fallback initial
size'xs' | 'sm' | 'md' | 'lg' | 'xl''md'
classNamestring
<Avatar src={user.photo} name={user.name} size="lg" />

StoryRingAvatar

Instagram-style story ring around an avatar, coloured by the story's audience/visibility. visibility comes from User.activeStoryVisibility; null/undefined means no active story, which renders a plain avatar with no ring.

PropTypeDefaultNotes
srcstring | null
altstring''
visibilitystring | nullpublic, followers, close_friends, subscribers, private each get a distinct gradient
sizeClassstring— (required)Tailwind size classes for the avatar itself, e.g. "w-10 h-10"
fallbackSrcstring'/default-avatar.png'Image used when src is empty
classNamestring
<StoryRingAvatar src={user.photo} sizeClass="w-14 h-14" visibility="close_friends" />

The helper storyRingClass(visibility) is also exported and returns the ring gradient classes (or null when there's no active story).

Spinner

Inline loading spinner that inherits the current text color (border-current). Renders a role="status" element with aria-label="Loading".

PropTypeDefault
classNamestring'w-4 h-4'
<Spinner className="w-5 h-5" />

Skeleton

Base skeleton block — a pulsing gray placeholder. Compose width/height at the call site. If the className already contains a rounded-* utility, the default rounded-md is skipped so a caller's rounded-full wins.

PropTypeDefault
classNamestring''
<Skeleton className="h-3 w-32" />

Tooltip

Lightweight tooltip: wraps an element and shows a label on hover/focus. Renders nothing extra when label is empty.

PropTypeDefault
labelstring— (required)
side'top' | 'bottom' | 'left' | 'right''top'
childrenReact.ReactNode
classNamestring''
<Tooltip label="Copy link" side="bottom"><IconButton><Copy /></IconButton></Tooltip>

VerifiedBadge

Instagram/Twitter-style verified seal with a white checkmark. Also re-exported from @repo/ui/icons.

PropTypeDefault
classNamestring'w-3.5 h-3.5 text-cg-blue flex-shrink-0'
<span>{user.name}{user.verified && <VerifiedBadge />}</span>

AnimatedCount

Animated counter that pops and nudges up/down whenever value changes (using the Web Animations API). Skips the animation on first mount.

PropTypeDefaultNotes
valuenumber— (required)
formatbooleantrueToggles compact K/M abbreviation
classNamestring
<AnimatedCount value={likeCount} />

The pure helper formatCount(n) (e.g. 1200 → "1.2K", 3_400_000 → "3.4M") is also exported.

RollingNumber

Rolls/counts from its previous value to the new one, showing every intermediate number via requestAnimationFrame. Skips the roll on first mount and starts each roll from whatever is currently on screen so rapid changes stay smooth.

PropTypeDefault
valuenumber— (required)
classNamestring
<RollingNumber value={followerCount} />

DateTimePicker

Custom date + time picker with no browser-native calendar and no external dependency — a month grid plus hour/minute selects. It returns the same local 'YYYY-MM-DDTHH:mm' string an <input type="datetime-local"> uses, so existing callers don't change. Marked 'use client'.

PropTypeDefaultNotes
valuestring— (required)'YYYY-MM-DDTHH:mm' local string, or ''
onChange(value: string) => void— (required)
minDateEarliest selectable datetime
placeholderstring'Fecha y hora'
panelbooleanfalseInline always-visible panel (grid scrolls, time row + "Done" pinned) vs. a trigger button + floating popover
onDone() => voidCalled by the panel's "Done" button (e.g. to close an overlay)
classNamestring''
<DateTimePicker value={when} onChange={setWhen} min={new Date()} />

Form primitives

Form.tsx is the single source of truth for the look of inputs, textareas, selects, labels and switches. It exports both ready-made components and the raw class-string constants they are built from.

Class constants: fieldClass (full-width bordered field), labelClass, inlineInputClass, inlineSelectClass — import these instead of re-declaring the same strings.

Components:

ComponentKey propsNotes
FieldLabelhtmlFor, className, children<label> styled with labelClass
TextInputnative <input> props (ref-forwarded)Styled with fieldClass
TextAreanative <textarea> propsfieldClass + resize-none
SearchInputvalue, onChange(value), placeholder, onClear, inputClassName (SearchInputProps)Rounded search field with magnifier icon and an optional clear button (shown when onClear is set and there is text)
Switchchecked, onChange(), disabled, ariaLabelToggle; role="switch", canonical checked/onChange API
Selectvalue, onChange(value), options? (SelectOption[]), children?, variant ('field' | 'inline'), disabledPass either options or <option> children

SelectOption is { value: string; label: string }.

<FieldLabel>Idioma</FieldLabel>
<Select
value={lang}
onChange={setLang}
options={[
{ value: 'es', label: 'Español' },
{ value: 'en', label: 'English' },
]}
/>

<SearchInput value={q} onChange={setQ} onClear={() => setQ('')} placeholder="Buscar" />

<Switch checked={on} onChange={() => setOn((v) => !v)} ariaLabel="toggle" />

Callout / box styles

styles/boxes.ts exports inline callout class strings reused across the settings and payouts forms, so the same strings aren't re-declared per file:

  • successBoxClass (green)
  • errorBoxClass (red)
  • neutralBoxClass (gray)
  • warningBoxClass (amber)
<div className={successBoxClass}>Saved.</div>

Icons

Imported from @repo/ui/icons. There are two groups.

Generic icon set (icons/lucide.tsx)

An auto-generated custom SVG icon library that is a drop-in replacement for lucide-react — path data is derived from lucide (ISC licensed) and rendered by an in-house wrapper, so the app no longer depends on the lucide-react package. Each icon accepts IconProps (native SVG props plus):

PropTypeDefault
sizenumber | string24
strokeWidthnumber | string2
absoluteStrokeWidthboolean
colorstring'currentColor'
import { Bell, Heart, Search } from '@repo/ui/icons';

<Heart size={20} className="text-cg-red" />

The set includes (among others) Activity, Archive, ArrowLeft, Bell, Bookmark, Calendar, Check, CheckCircle2, ChevronDown/Left/Right/Up, Circle, Clapperboard, Clock, Coins, Compass, Copy, CreditCard, Crown, DollarSign, Download, ExternalLink, Eye, Facebook, FileText, Gift, Globe, Grid3x3, Hand, Hash, Heart, Image, Languages, Link, Link2, Loader2, Lock, LogOut, Mail, Maximize2, Megaphone, MapPin, MessageCircle, Mic, MicOff, Minus, Monitor, Moon, MoreHorizontal, Package, Pencil, Phone, Pin, Play, Plus, Radio, Receipt, Repeat2, RotateCcw, Search, Send, Settings, Share2, ShieldAlert/Check/Off, ShoppingBag, Smile, Square, SquarePen, Star, Store, Sun, Ticket, Trash2, TrendingUp, Trophy, Truck, Twitter, Upload, UploadCloud, UserPlus, UserSquare2, Users, Video, VideoOff, X, XCircle, plus a custom square-cornered RepostSquare.

Brand icons

Hand-authored icons matching the app's navigation and post-action design. Most take { className?: string; filled?: boolean } (a filled variant for active/selected states); a few are outline-only (className only):

IconVariants
HomeIconoutline / filled
SearchIconoutline / filled
ExploreIconoutline / filled
ClipIconoutline / filled
MessagesIconoutline / filled
NotificationIconoutline / filled
HeartIconoutline / filled
ProfileIconoutline / filled
CoinIconoutline / filled
LanguageIconoutline / filled
CommentIconoutline only
CreateIconoutline only
EditIconoutline only
RepostIconoutline only
ShareIconoutline only

VerifiedBadge is also re-exported from @repo/ui/icons.

import { HomeIcon } from '@repo/ui/icons';

<HomeIcon filled={isActive} className="w-6 h-6" />

Design tokens & Tailwind

The library ships its own tailwind.config.ts so it renders correctly in isolation (Storybook) without depending on any app. Dark mode uses the darkMode: 'class' strategy — every component is styled for both light and dark: variants. See Theming for how the theme class is toggled at runtime.

The palette is defined under the cg (Closegram) color namespace and mirrors the tokens used by apps/frontend-nextjs. Key tokens:

TokenValueUse
cg-blue#3797f0Primary brand blue (+ -dark, -hover, -light, -action #0095f6)
cg-green#58c322Success
cg-red#ed4956Danger
cg-bg#ffffff / dark #0c1014Page background
cg-dark-*primary #121212, secondary, elevated #262626, modal, border, hover, input, …Dark-mode surfaces
cg-text-*primary, secondary #a8a8a8, tertiary, placeholderText colors
cg-separator / cg-muted#262626 / #555555Dividers, muted elements

The config file notes a follow-up to lift this palette into a shared @repo/tailwind-config so the apps and this package share a single source of truth instead of duplicating it.

Consuming apps that don't compile the package's Tailwind classes themselves can import the pre-built stylesheet once at the root:

import '@repo/ui/styles.css';