import { default as React } from 'react'; import { FieldMetadata } from '@object-ui/types'; import { humanizeLabel, ComponentMeta } from '@object-ui/core'; import { resolveFieldCurrency } from './currency.js'; /** * Pick the most reasonable display name from an arbitrary record object. * Tries common name-like keys in priority order, then falls back to undefined. */ export declare function pickRecordDisplayName(record: Record | null | undefined, preferredField?: string): string | undefined; /** * Heuristic: detect strings that look like opaque foreign-key IDs (e.g. nanoid * or BSON ObjectId). Used so we don't display random gibberish to users when * a lookup wasn't expanded. */ export declare function isLikelyOpaqueId(v: unknown): boolean; /** * Cell renderer props */ export interface CellRendererProps { value: any; field: FieldMetadata; isEditing?: boolean; onChange?: (value: any) => void; } /** * Coerce a value to a safe primitive for rendering. * Handles MongoDB wrapper types ($numberDecimal, $oid, $date), expanded * reference objects, and arrays so that no raw object is ever passed as * a React child — preventing React error #310. */ export declare function coerceToSafeValue(value: unknown): string | number | boolean | null | undefined; export { resolveFieldCurrency }; export declare function formatCurrency(value: number, currency?: string, locale?: string): string; /** * Format currency value in compact form for mobile display. * E.g., $150,000 → $150K, $1,200,000 → $1.2M * When `currency` is undefined, returns a compact number without symbol. */ export declare function formatCompactCurrency(value: number, currency?: string, locale?: string): string; /** * Format a plain number with thousands separators, no currency symbol. * Used as a safe fallback when a currency-typed field has no `currency` * configured — we'd rather render `1,234.50` than silently assume USD. */ export declare function formatNumber(value: number, decimals?: number, locale?: string): string; /** * Format percent value. * Handles both decimal (0.8 = 80%) and whole number (80 = 80%) inputs. * * `locale` is the third positional parameter, matching {@link formatNumber} and * {@link formatCurrency} — the shape the sibling formatters already use. * Callers should pass the tag from `useDisplayLocale()`. * * Before objectui#4553 this function took no locale and never touched `Intl`: * its whole body was `${percentDisplayValue(value).toFixed(precision)}%`, so it * rendered in NO locale rather than the machine's — an ASCII decimal mark and * never a grouping separator, byte-identical on every machine. That made * `1235%` the output everywhere, which is wrong in en-US as well as in German, * so the grouping and the locale are fixed together: en output MOVES from * `1235%` to `1,235%` at four digits and up, and that move is the fix. */ export declare function formatPercent(value: number, precision?: number, locale?: string): string; /** * Humanize a snake_case or kebab-case string into Title Case — the fallback * label when no explicit `option.label` exists. * * Defined in `@object-ui/core` (`utils/humanize-label.ts`) and re-exported here * because this package is one of its two doorways: `plugin-grid`, * `plugin-gantt` and `plugin-detail` read it from `@object-ui/fields`, while * `plugin-charts` reads the same function straight from core. Until * objectui#5444 this file and `plugin-charts`' `ObjectChart.tsx` each held a * byte-identical private copy; core is the shared ancestor both packages * already depend on, so the convention has one home and no new dependency edge * (objectui#4389: core-canonical logic, plugins consume). The core docstring * carries the convention itself, and the reason it stays distinct from * `humanizeFieldKey`'s camelCase-splitting KEY convention. */ export { humanizeLabel }; /** Options shared by {@link formatDate} / {@link formatRelativeDate}. */ export interface DateDisplayOptions { dueLike?: boolean; /** BCP-47 display locale (ADR-0053 tenant default); falls back to the runtime locale. */ locale?: string; /** i18n translate fn for phrases `Intl` can't produce (the "Overdue Nd" wording). */ t?: (key: string, params?: Record) => string; } /** * Format date as relative time (e.g., "3 days ago", "Today", "Overdue 3d"), * localized via `Intl.RelativeTimeFormat` (objectstack-ai/objectstack#3040). * * `dueLike` gates the "Overdue" wording — a past `start_date`/`created_at` * isn't overdue, only a past due/deadline-semantic field is. Non-due-like * past dates render as plain "N days ago" instead. The overdue phrase has no * `Intl` equivalent, so it resolves through `options.t` (key * `fields.relativeDate.overdue`) with an English fallback. */ export declare function formatRelativeDate(value: string | Date | number, options?: DateDisplayOptions): string; /** * Format date value */ export declare function formatDate(value: string | Date | number, style?: string, options?: DateDisplayOptions): string; /** * Format datetime value. * * `options` mirrors {@link formatDate}'s and is optional, so an existing * caller that passes nothing keeps the exact runtime-default behavior it had. * Before objectui#4272 the parameter did not exist at all, which meant no * caller could localize this function however hard it tried — it always handed * `Intl` an `undefined` tag, i.e. the MACHINE's locale, which is neither of * the repo's two locale channels. Callers should pass the tag from * `useDisplayLocale()`. */ export declare function formatDateTime(value: string | Date | number, options?: DateDisplayOptions): string; /** * Text field cell renderer */ export declare function TextCellRenderer({ value }: CellRendererProps): React.ReactElement; /** * Number field cell renderer */ export declare function NumberCellRenderer({ value, field }: CellRendererProps): React.ReactElement; /** * Currency field cell renderer */ export declare function CurrencyCellRenderer({ value, field }: CellRendererProps): React.ReactElement; /** * Percent field cell renderer with mini progress bar */ export declare function PercentCellRenderer({ value, field }: CellRendererProps): React.ReactElement; /** * Boolean field cell renderer (Airtable-style checkbox) * Supports semantic rendering for completion fields (green indicator) * and warning badge for active/enabled fields when false. */ export declare function BooleanCellRenderer({ value, field }: CellRendererProps): React.ReactElement; /** * Date field cell renderer */ export declare function DateCellRenderer({ value, field }: CellRendererProps): React.ReactElement; /** * DateTime field cell renderer (Airtable-style with date and time visually separated) */ export declare function DateTimeCellRenderer({ value }: CellRendererProps): React.ReactElement; /** The six pill colors plus the two dot colors derived from one declared hex. */ export interface HexBadgePalette { bg: string; fg: string; border: string; bgDark: string; fgDark: string; borderDark: string; dot: string; dotDark: string; } /** * Derive a full soft-pill palette from an author-declared hex. * * The declared LIGHTNESS is what separates two same-hue tiers (`#2ecc71` is * l=0.49, `#1e8449` is l=0.32 — their hues differ by 0.1°), so it has to * survive into the *surface*. A fixed pale tint — the obvious reading of * "compute a soft pill from the hex" — does not carry it: measured, that * leaves the reported pair ΔE 2.3 apart in Lab, at the ~2.3 just-noticeable * threshold, which would close this issue on paper while the user still cannot * tell the two badges apart. Letting the tint depth track the declared * lightness puts them ΔE 8.0 apart instead. */ export declare function deriveHexBadgePalette(color: string): HexBadgePalette | undefined; /** A className plus the custom properties it reads. */ export interface HexColorAppearance { className: string; style: React.CSSProperties; } /** * Soft-pill appearance for an explicitly declared hex, or `undefined` for * every other kind of declaration (family name, no colour at all) so the * caller falls back to `getBadgeColorClasses`. */ export declare function getBadgeHexAppearance(color?: string): HexColorAppearance | undefined; /** Dot appearance for an explicitly declared hex (see `getBadgeHexAppearance`). */ export declare function getDotHexAppearance(color?: string): HexColorAppearance | undefined; export declare function getBadgeColorClasses(color?: string, val?: unknown): string; /** * Resolve a semantic color name (e.g. "red", "green") for a value, suitable * for callers that need a raw color token rather than CSS classes (for * example, the Gantt renderer paints bars via inline styles). * * Resolution order: explicit option color → semantic value mapping → * deterministic hash fallback. Returns `undefined` only when no value is * supplied so the caller can fall back to its own default. */ export declare function getSemanticColorName(color?: string, val?: unknown): string | undefined; /** * Map a semantic color name to its Tailwind -500 hex value. Used by * inline-style consumers (Gantt bars). Falls back to the supplied default * (or the platform default blue) when the name is unrecognized. */ export declare function getSemanticHex(name?: string, fallback?: string): string; /** * Select field cell renderer. * * Two visual styles, controlled by `field.appearance` (renderer-level option, * not part of the `@objectstack/spec` field schema): * - `'badge'` (default for spec compatibility): soft-pill colored badge. * - `'dot'`: a small colored dot followed by the option label. Used by * dense list/grid contexts to keep the table visually quiet — repeated * filled badges across many rows create heavy visual noise. * * Metadata always wins: callers can pass `appearance: 'badge'` on the field * descriptor to force the legacy badge in any context. */ export declare function SelectCellRenderer({ value, field }: CellRendererProps): React.ReactElement; /** * Email field cell renderer */ export declare function EmailCellRenderer({ value }: CellRendererProps): React.ReactElement; /** * URL field cell renderer */ export declare function UrlCellRenderer({ value }: CellRendererProps): React.ReactElement; /** * Phone field cell renderer */ export declare function PhoneCellRenderer({ value }: CellRendererProps): React.ReactElement; /** * File field cell renderer */ export declare function FileCellRenderer({ value, field }: CellRendererProps): React.ReactElement; /** * Image field cell renderer (with thumbnails + click-to-zoom). * * An image value may be a plain URL string, an object ({ url | src | href … }), * a bare `sys_file` id, or an array of any of those. Normalising through * `readFileValues` (which resolves a bare id to its stable download URL) means * a string-URL field, a CDN link, and an unexpanded reference all render a * thumbnail instead of a broken `` placeholder. * * Clicking a thumbnail opens a full-screen lightbox (single or gallery). The * click is `stopPropagation`-guarded so, inside a grid row, enlarging an image * doesn't also trigger row navigation. */ export declare function ImageCellRenderer({ value }: CellRendererProps): React.ReactElement; /** * Lookup/Master-Detail field cell renderer. * * Display order: * 1. Embedded record object (`{ id, name, ... }` from `$expand`) → use its name * 2. Static `field.options[]` (e.g. when the lookup is a closed enum) → look up label * 3. Fetch-on-demand: when the value is a primitive ID and `field.reference_to` * is known, resolve via dataSource and show the related record's display name. * Falls back to a muted placeholder while pending and on failure. * * Record → name resolution (1 and 3) goes through the referenced object's * schema when the data source exposes it (`displayField` → nameField/titleFormat * → derivation, see {@link resolveLookupRecordName}), so the chip and the * picker agree (issue #2357). */ export declare function LookupCellRenderer({ value, field }: CellRendererProps): React.ReactElement; /** * Formula field cell renderer (read-only) */ export declare function FormulaCellRenderer({ value }: CellRendererProps): React.ReactElement; /** * User/Owner field cell renderer (with avatars) */ export declare function UserCellRenderer({ value }: CellRendererProps): React.ReactElement; /** * Register a custom field renderer * @param type Field type (e.g. 'text', 'location', 'my-custom-type') * @param renderer React component to render the field */ export declare function registerFieldRenderer(type: string, renderer: React.FC): void; /** * Resolve the canonical cell-renderer key for a field. Accepts either a * raw type string (back-compat) or a field-metadata object so that * format hints (`format: 'phone'` etc.) can promote a plain `text` * field to its richer renderer counterpart. */ export declare function resolveCellRendererType(fieldOrType: string | { type?: string; format?: string; } | null | undefined): string; /** * Renders structured/embedded values (json, object, composite, record, * address, geolocation) as compact, readable JSON. Objects and arrays are * stringified; primitives fall through to their string form. */ export declare function JsonCellRenderer({ value }: CellRendererProps): React.ReactElement; /** * Renders a `color` value as a swatch alongside its hex/string value. */ export declare function ColorSwatchCellRenderer({ value }: CellRendererProps): React.ReactElement; /** * The rich-content display pipelines — `markdown` through the GFM renderer, * `html`/`richtext` through the sanitizing HTML renderer — live in * `./widgets/richTextDisplay.js` rather than here, so `RichTextField` can * import them without importing this barrel back (objectui#5498). Re-exported * unchanged: they are part of this package's published surface, and * `RICH_TEXT_CELL_RENDERERS` below is the one table both the cell resolver and * the widget's readonly branch read. */ export { MarkdownCellRenderer, HtmlCellRenderer } from './widgets/richTextDisplay.js'; /** * Renders a `location`/`geolocation` value as readable coordinates with a pin. * Accepts `{ lat, lng }` / `{ latitude, longitude }`, a `"lat,lng"` string, * or a `[lat, lng]` array. Falls back to compact JSON for anything else. */ export declare function LocationCellRenderer({ value }: CellRendererProps): React.ReactElement; /** * Renders an `address` value as a formatted single-line postal address instead * of stringified JSON (objectui#4037). * * The detail page's display registry mapped `address` to {@link JsonCellRenderer}, * so a populated address read as `{"street":"中策路 1 号","city":"杭州",…}` on the * detail page and in its inline-edit read state — while the very same value * rendered correctly in the create/edit dialog, whose input registry has always * carried `address`. Read side only: nothing about the input widget changes. * * Layout is not invented here. It is {@link formatAddress} — the rule * `AddressField`'s own readonly branch already applied, over the sub-field set * its inputs expose — so a readonly form and a detail page cannot spell one * stored address two ways. * * Anything `formatAddress` cannot recognize falls back to compact JSON rather * than rendering blank: an unknown shape stays visible (today's behaviour) and * is never silently swallowed by the fix. */ export declare function AddressCellRenderer({ value }: CellRendererProps): React.ReactElement; /** * Get the appropriate cell renderer for a field type */ export declare function getCellRenderer(fieldType: string): React.FC; export { mapFieldTypeToFormType } from './field-type-alias.js'; export { RETIRED_FIELD_TYPES, isRetiredFieldType, reportRetiredFieldType, resetRetiredFieldTypeReports, } from './field-type-alias.js'; /** * Formats file size in bytes to human-readable string * @param bytes - File size in bytes (must be non-negative) * @returns Formatted string (e.g., "5 MB", "1.5 GB") */ export declare function formatFileSize(bytes: number): string; /** * Build validation rules from field metadata * @param field - Field metadata from ObjectStack * @returns Validation rule object compatible with react-hook-form */ export declare function buildValidationRules(field: any): any; /** * Evaluate a conditional expression for field visibility * @param condition - Condition object from field metadata * @param formData - Current form values * @returns Whether the condition is met */ export declare function evaluateCondition(condition: any, formData: any): boolean; declare const fieldWidgetMap: { text: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/TextField.js').TextField; }>; textarea: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/TextAreaField.js').TextAreaField; }>; number: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/NumberField.js').NumberField; }>; boolean: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/BooleanField.js').BooleanField; }>; select: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/SelectField.js').SelectField; }>; date: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/DateField.js').DateField; }>; datetime: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/DateTimeField.js').DateTimeField; }>; time: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/TimeField.js').TimeField; }>; email: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/EmailField.js').EmailField; }>; phone: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/PhoneField.js').PhoneField; }>; url: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/UrlField.js').UrlField; }>; multiselect: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/MultiSelectField.js').MultiSelectField; }>; radio: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/RadioField.js').RadioField; }>; checkboxes: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/CheckboxesField.js').CheckboxesField; }>; tags: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/TagsField.js').TagsField; }>; currency: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/CurrencyField.js').CurrencyField; }>; percent: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/PercentField.js').PercentField; }>; password: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/PasswordField.js').PasswordField; }>; markdown: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/RichTextField.js').RichTextField; }>; html: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/RichTextField.js').RichTextField; }>; richtext: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/RichTextField.js').RichTextField; }>; lookup: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/LookupField.js').LookupField; }>; master_detail: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/LookupField.js').LookupField; }>; file: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/FileField.js').FileField; }>; image: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/ImageField.js').ImageField; }>; location: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/LocationField.js').LocationField; }>; formula: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/FormulaField.js').FormulaField; }>; summary: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/SummaryField.js').SummaryField; }>; auto_number: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/AutoNumberField.js').AutoNumberField; }>; user: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/UserField.js').UserField; }>; object: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/ObjectField.js').ObjectField; }>; vector: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/VectorField.js').VectorField; }>; grid: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/GridField.js').GridField; }>; color: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/ColorField.js').ColorField; }>; slider: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/SliderField.js').SliderField; }>; rating: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/RatingField.js').RatingField; }>; code: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/CodeField.js').CodeField; }>; avatar: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/AvatarField.js').AvatarField; }>; address: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/AddressField.js').AddressField; }>; geolocation: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/GeolocationField.js').GeolocationField; }>; signature: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/SignatureField.js').SignatureField; }>; qrcode: () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/QRCodeField.js').QRCodeField; }>; 'object-ref': () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/ObjectRefField.js').ObjectRefField; }>; 'filter-condition': () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/FilterConditionField.js').FilterConditionField; }>; 'recipient-picker': () => Promise<{ default: React.ComponentType; } | { default: typeof import('./widgets/RecipientPickerField.js').RecipientPickerField; }>; }; /** The registered field widget keys, as a literal union (objectui#4857). */ export type RegisteredFieldWidgetType = keyof typeof fieldWidgetMap; /** * Every field type the form can render (the canonical list of supported types). * Exported so inline editing can be checked against it — a form type must * either have an inline editor or be explicitly excluded, see * `INLINE_EXCLUDED_FIELD_TYPES` and its drift-guard test. */ export declare const FORM_FIELD_TYPES: readonly string[]; /** * Resolve an arbitrary field-type spelling to the widget key the form uses. * A key already present in the widget map resolves to itself; spec aliases * (`toggle`, `json`, `repeater`, `secret`, …) resolve through * {@link mapFieldTypeToFormType}; anything unknown falls back to `text` — * the same fallback the form renderer applies. * * Consumers that render field widgets outside the form (e.g. the app-shell * `ActionParamDialog`) use this + {@link getLazyFieldWidget} so their type * support can never drift behind the form surface (ADR-0059). */ export declare function resolveFormWidgetType(fieldType: string): string; /** * The widget keys that must be fed the live record (`dependentValues`) so their * offered option set can be re-resolved per option `visibleWhen` / `dependsOn`. * * Defined in `@object-ui/core`, next to `resolveCascadingOptions` — the * evaluator that reads the record — and re-exported here because this is the * package whose {@link resolveFormWidgetType} produces the keys the set is * keyed on: a consumer resolving a widget key finds the allow-table in the * same place. One definition, two doorways; never a second copy (objectui#4770, * which converged the three private copies that preceded it). */ export { CASCADE_OPTION_WIDGET_TYPES } from '@object-ui/core'; /** * Lazily-loaded form field widget for a field type. Shares the exact loaders * of {@link fieldWidgetMap} (the same components `registerField` registers for * forms), wrapped in `React.lazy` and cached per type — so a consumer can * render any form-supported field type without eagerly bundling every widget. * Render inside a `` boundary. Unknown types resolve to the `text` * widget via {@link resolveFormWidgetType}. */ export declare function getLazyFieldWidget(fieldType: string): React.ComponentType; /** * The labelling declaration of EVERY registered field widget * (`ComponentMeta.labelling` — the closed `'control' | 'group' | 'display'` * vocabulary, objectui#3961 extended by objectui#4857). This `Record` is keyed * by the widget map's own literal key union, so it is exhaustive BY * CONSTRUCTION: registering a widget without deciding how a host's label * reaches it is a COMPILE error here, not a silent fall-through to the * `'control'` path — the omitted-declaration degradation is exactly the trap * the #4857 ruling named, and it is what turned the display-only four into * fields with no accessible name. * * ## `'group'` — a surface no `