Skip to Content
DocsFrontend DevelopmentOther ComponentsCommon Components

common — Generic UI widgets

Plain visual widgets with no model awareness. Drop-in usable from anywhere — pages, layouts, dialogs, view components. Inputs are simple data (strings, numbers, callbacks); none of them know about modelName, MetaModel, or FilterCondition.

For the broader taxonomy (where common/ sits among the five layers), see Index.

FileExportCategory
pagination-bar.tsxPaginationBarPagination
empty-state.tsxEmptyStateEmpty / loading
loading-skeleton.tsxLoadingSkeletonEmpty / loading
full-screen-loading.tsxFullScreenLoading (default export)Empty / loading
status-badge.tsxStatusBadgeStatus / identity
user-avatar.tsxUserAvatarStatus / identity
timeline.tsxTimelineDisplay
check-list.tsxCheckListDisplay
date-picker.tsxDatePickerInput
datetime-picker.tsxDateTimePickerInput
time-picker.tsxTimePickerInput
time-column-panel.tsxTimeColumnPanelInput (building block)
time-utils.tsresolveTimeConfig, typesUtility
option-select.tsxOptionSelectInput
density-switcher.tsxDensitySwitcherApp control

Imports: import { PaginationBar } from "@/components/common/pagination-bar";


Pagination

PaginationBar

Standalone pagination row (page nav buttons + size dropdown + record count). Used by ModelTable / ModelCard toolbars, but also usable in custom lists.

<PaginationBar pageNumber={page} totalPages={totalPages} pageSize={pageSize} totalCount={totalCount} selectedCount={selectedIds.length} onPageChange={setPage} onPageSizeChange={setPageSize} />
PropTypeRequiredDefaultNotes
pageNumbernumberYes-1-based current page
totalPagesnumberYes-Total page count (clamped to ≥ 1 in display)
pageSizenumberYes-Current rows per page
onPageChange(n: number) => voidYes-Receives 1-based new page
onPageSizeChange(n: number) => voidYes-New page size value
totalCountnumberNo-Shown as Total N records; omitted hides summary
selectedCountnumberNo0Appended as · Selected N when > 0
availablePageSizesnumber[]No[10,20,50,100]Dropdown options
classNamestringNo-

Empty / loading

EmptyState

Centered “no data / no result” placeholder. Defaults to a database icon when icon is omitted.

<EmptyState title="No deployments yet" description="Pick an environment to see deployments." action={<Button>Get started</Button>} /> // compact variant — for inline empty states inside cards/panels <EmptyState compact title="No items" />
PropTypeRequiredDefaultNotes
titlestringYes-Primary text
descriptionstringNo-Secondary explanation (max-width: max-w-sm)
iconReactNodeNo<Database>Custom icon
actionReactNodeNo-Button or link below the text
compactbooleanNofalseSmaller paddings / fonts; for inline use
classNamestringNo-

LoadingSkeleton

Pre-baked full-page skeleton (input row + body + pagination footer placeholders). Use for pages that are about to render a Model* view but haven’t loaded data yet.

if (isLoading) return <LoadingSkeleton />;

No props.

FullScreenLoading

Modal full-screen overlay with a spinner. Default export. Use for blocking operations that span the whole app.

import FullScreenLoading from "@/components/common/full-screen-loading"; {isSaving && <FullScreenLoading />}

No props.


Status / identity

StatusBadge

Colored pill badge backed by class-variance-authority variants. Doesn’t look up status meaning — caller picks the variant.

<StatusBadge variant="success">Active</StatusBadge> <StatusBadge variant="warning">Pending</StatusBadge> <StatusBadge variant="error">Failed</StatusBadge> <StatusBadge variant="info">Draft</StatusBadge> <StatusBadge variant="neutral">Archived</StatusBadge>
PropTypeRequiredDefaultNotes
variant"success" | "warning" | "error" | "info" | "neutral"No"neutral"Color theme
classNamestringNo-Merged with variant classes
…HTMLSpanAttributes---Forwarded to underlying <span>

UserAvatar

Avatar circle. Renders a photo if photoUrl loads; otherwise falls back to a User icon. The photoUrl host doesn’t need to be in next.config.js’s remotePatterns (uses plain <img>).

<UserAvatar photoUrl={user.avatarUrl} /> <UserAvatar /> {/* fallback icon */} <UserAvatar className="h-12 w-12" /> {/* custom size */}
PropTypeRequiredDefaultNotes
photoUrlstringNo-Avatar URL; auto-fallback on load error
classNamestringNoh-(--ds-h-xl) w-(--ds-h-xl)Override size with Tailwind classes

Display

Timeline

Vertical event timeline with a connecting border. The first event is highlighted; later events render in muted color.

<Timeline events={[ { idField: "evt-1", timeField: "2026-04-30 09:30", userField: "Alice Lee", actionField: "submitted the request", detailsField: "Reason: vacation", }, { idField: "evt-2", timeField: "2026-04-30 10:15", userField: "Bob Manager", actionField: "approved the request", }, ]} />
PropTypeRequiredDefaultNotes
eventsTimelineEvent[]Yes-Empty list renders nothing
classNamestringNo-

TimelineEvent shape (each field already holds a formatted display value):

FieldTypeNotes
idFieldstringUsed as React key; typically the event record id
timeFieldstringPre-formatted timestamp text
userFieldstringActor name
actionFieldstringVerb phrase, e.g. "approved the request"
detailsFieldstring?Optional secondary line

CheckList

Vertical checklist. Checked items show a green check + line-through; unchecked show an outlined circle.

<CheckList items={[ { idField: "1", labelField: "Submit form", statusField: true }, { idField: "2", labelField: "Wait for approval", statusField: false, descriptionField: "Manager review pending" }, ]} />
PropTypeRequiredDefaultNotes
itemsCheckListItem[]Yes-Empty list renders nothing
classNamestringNo-

CheckListItem shape:

FieldTypeNotes
idFieldstringReact key
labelFieldstringItem label
statusFieldbooleantrue = checked (line-through, green); false = unchecked
descriptionFieldstring?Optional secondary line

Input

All three pickers (DatePicker / DateTimePicker / TimePicker) share the same shape: string in, string out. The trigger label can be rendered in a different display format (e.g. 12-hour or locale-specific) via displayFormat, but the machine value always uses the canonical machine format so downstream pipelines (form values, FilterCondition, backend contract) never need to know about display mode.

The triggerWrapper prop lets a form adapter inject <FormControl>{button}</FormControl> so react-hook-form’s a11y wiring (id / aria-describedby / aria-invalid) reaches the trigger button. Standalone callers omit it and the button is the trigger directly.

DatePicker

Calendar-only date picker.

<DatePicker value={value} onChange={setValue} /> <DatePicker value={value} onChange={setValue} dateFormat="yyyy-MM" />
PropTypeRequiredDefaultNotes
valuestringYes-Date string in dateFormat; "" = no selection
onChange(next: string) => voidYes-New value; "" when cleared
dateFormatstringNoconfigs.dateTimeFormats.date (yyyy-MM-dd)Machine format. Pass "yyyy-MM" / "MM-dd" for partial-date pickers
displayFormatstringNo= dateFormatTrigger-label format
disabledbooleanNofalse
placeholderstringNo"Pick a date"
classNamestringNo-Applied to trigger button
triggerWrapper(button) => ReactElementNo-Form adapter wraps with <FormControl>
triggerStyleCSSPropertiesNo-Inline style on trigger button

DateTimePicker

Calendar + 24h time-column panel side-by-side. Footer hosts Clear / Now / Apply.

<DateTimePicker value={value} onChange={setValue} /> <DateTimePicker value={value} onChange={setValue} defaultTime="23:59:59" />

Machine format is fixed at yyyy-MM-dd HH:mm:ss (matches configs.dateTimeFormats.dateTime).

PropTypeRequiredDefaultNotes
valuestringYes-yyyy-MM-dd HH:mm:ss; "" = no selection
onChange(next: string) => voidYes-
displayFormatstringNoconfigs.dateTimeFormats.dateTimeTrigger-label format. Pass e.g. "yyyy-MM-dd hh:mm:ss a" for 12-hour
defaultTimestringNo"00:00:00"Time-of-day to seed when the user picks a date but value has no time yet. Range filter end-points pass "23:59:59"
disabledbooleanNofalse
placeholderstringNo"Pick date & time"
classNamestringNo-
triggerWrapper(button) => ReactElementNo-
triggerStyleCSSPropertiesNo-

TimePicker

Time-only picker (no calendar). Renders TimeColumnPanel inside a popover.

<TimePicker value={value} onChange={setValue} timeFormat="HH:mm:ss" /> <TimePicker value={value} onChange={setValue} timeFormat="HH:mm" config={resolveTimeConfig({ min: "08:00", max: "18:00", minuteStep: 15 }, "HH:mm")} />
PropTypeRequiredDefaultNotes
valuestringYes-24h ISO string in timeFormat; "" = no selection
onChange(next: string) => voidYes-
timeFormat"HH:mm" | "HH:mm:ss"Yes-Machine format; the panel’s columns always render 24h
displayFormatstringNo= timeFormatdate-fns format for the trigger label (e.g. "hh:mm:ss a" for 12h)
configResolvedTimeConfigNoresolveTimeConfig(undefined, timeFormat)Bounds / quick-options
disabledbooleanNofalse
placeholderstringNo"Pick time"
classNamestringNo-
triggerWrapper(button) => ReactElementNo-
triggerStyleCSSPropertiesNo-

TimeColumnPanel

The bare scrollable HH / mm / (ss) column panel that powers TimePicker and DateTimePicker. Use directly only when you need the columns embedded in custom layout. Footer (Clear / Now / Apply) is conditional on onClear / onApply / config.clearable / config.showQuickPick.

resolveTimeConfig

Pure resolver (no widget knowledge) for TimePicker / TimeColumnPanel config. Takes Partial<TimePickerConfig> and a TimeFormat, returns ResolvedTimeConfig with bounds parsed, step values validated, and defaultTime snapped to the step grid.

import { resolveTimeConfig } from "@/components/common/time-utils"; const config = resolveTimeConfig( { min: "08:00", max: "18:00", minuteStep: 15, quickOptions: ["09:00", "12:00"] }, "HH:mm", );

TimePickerConfig shape:

FieldTypeNotes
min / maxstringInclusive bounds in timeFormat
minuteStep / secondStepnumberOne of [1,2,3,4,5,6,10,12,15,20,30,60] (divisors of 60); 60 means only 00; invalid values warn and fall back to 1
defaultTimestringPre-fill on first open; auto-snapped UP to step grid
quickOptionsstring[]Quick-pick buttons rendered above the columns; out-of-range / off-grid entries auto-disable
clearablebooleanShow Clear in footer (default true)
showQuickPickbooleanShow Now in footer (default true)

OptionSelect

Standalone select bound to an option set (server-defined enum). Handles loading / error states internally.

<OptionSelect optionSetCode="DocumentStatus" value={status} onChange={setStatus} filters={[["disabled", "=", false]]} // optional client-side prune placeholder="Pick a status" />
PropTypeRequiredDefaultNotes
optionSetCodestringYes-Identifier of the option set on the server
valuestring | numberNo-Currently selected itemCode
onChange(value: string | undefined) => voidNo-New itemCode (or undefined when cleared)
placeholderstringNo"Please select..."
disabledbooleanNofalse
readOnlybooleanNofalseVisually displays without interactivity, full contrast
filtersFilterConditionNo-Client-side filter applied to fetched options
classNamestringNo-

When loading → renders a Skeleton. On error → renders a disabled select with "Failed to load options".


App control

DensitySwitcher

Toggles compact / comfortable UI density via useDensity() from @/providers/density-provider. Used in the app Header. No props besides className.

<DensitySwitcher />
PropTypeRequiredDefaultNotes
classNamestringNo-

Adding a new common widget

A widget belongs in common/ if all are true:

  • It takes plain data (no modelName / MetaModel / FilterCondition props)
  • It does not depend on Model* view containers (SidePanelContainerProvider etc.)
  • It can be used outside of a Model* host (in dialogs, layouts, custom pages)
Last updated on