@object-ui/fields 17.6.0 → 17.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +5436 -0
  2. package/README.md +108 -8
  3. package/dist/FieldEditWidget.d.ts +54 -6
  4. package/dist/coerceToSafeValue.d.ts +54 -0
  5. package/dist/currency.d.ts +1 -1
  6. package/dist/index.cjs +5 -5
  7. package/dist/index.css +26 -2
  8. package/dist/index.d.ts +229 -57
  9. package/dist/index.js +3811 -3009
  10. package/dist/widgets/AvatarField.d.ts +19 -3
  11. package/dist/widgets/BooleanField.d.ts +1 -1
  12. package/dist/widgets/DateField.d.ts +1 -1
  13. package/dist/widgets/DateTimeField.d.ts +1 -1
  14. package/dist/widgets/FilterConditionField.d.ts +21 -7
  15. package/dist/widgets/GeolocationField.d.ts +9 -3
  16. package/dist/widgets/GridField.d.ts +63 -99
  17. package/dist/widgets/LocationField.d.ts +30 -3
  18. package/dist/widgets/LookupField.d.ts +8 -0
  19. package/dist/widgets/NumberField.d.ts +1 -1
  20. package/dist/widgets/ObjectField.d.ts +9 -2
  21. package/dist/widgets/OptionsEmptyState.d.ts +29 -5
  22. package/dist/widgets/PasswordField.d.ts +14 -0
  23. package/dist/widgets/PeoplePicker.d.ts +17 -2
  24. package/dist/widgets/PercentField.d.ts +2 -1
  25. package/dist/widgets/PersonRow.d.ts +13 -2
  26. package/dist/widgets/RecipientPickerField.d.ts +43 -0
  27. package/dist/widgets/RecordPickerDialog.d.ts +31 -12
  28. package/dist/widgets/RichTextField.d.ts +1 -1
  29. package/dist/widgets/SelectionTray.d.ts +13 -2
  30. package/dist/widgets/TextField.d.ts +1 -1
  31. package/dist/widgets/TimeField.d.ts +1 -1
  32. package/dist/widgets/file-affordance.d.ts +33 -0
  33. package/dist/widgets/file-size-guard.d.ts +5 -23
  34. package/dist/widgets/file-value.d.ts +38 -9
  35. package/dist/widgets/lookupColumnDisplay.d.ts +24 -7
  36. package/dist/widgets/nativeDateValue.d.ts +9 -27
  37. package/dist/widgets/number-format.d.ts +137 -0
  38. package/dist/widgets/numberBadInput.d.ts +135 -0
  39. package/dist/widgets/personDisplay.d.ts +22 -4
  40. package/dist/widgets/richTextDisplay.d.ts +36 -0
  41. package/dist/widgets/toDomProps.d.ts +10 -1
  42. package/dist/widgets/toHostGroupProps.d.ts +53 -0
  43. package/dist/widgets/toHostProps.d.ts +105 -0
  44. package/dist/widgets/types.d.ts +11 -3
  45. package/dist/widgets/upload-error-message.d.ts +6 -0
  46. package/dist/widgets/uploadingScope.d.ts +142 -0
  47. package/dist/widgets/useCascadingOptions.d.ts +3 -2
  48. package/dist/widgets/useRecordQuery.d.ts +4 -2
  49. package/dist/widgets/useUploadingSignal.d.ts +8 -0
  50. package/package.json +11 -12
@@ -41,7 +41,12 @@ export interface RecordPickerFilterBarProps {
41
41
  * Allows plugging in ObjectGrid or any compatible table component.
42
42
  */
43
43
  export interface RecordPickerGridSlotProps {
44
- /** Resolved column definitions */
44
+ /**
45
+ * Resolved column definitions, less the ones field-level security denies
46
+ * once the permission policy has loaded, the display column included — the
47
+ * columns the built-in table draws (objectui#10373). When none survives,
48
+ * this is the id column alone.
49
+ */
45
50
  columns: LookupColumnDef[];
46
51
  /** Current page of records */
47
52
  records: any[];
@@ -93,16 +98,30 @@ export interface RecordPickerDialogProps {
93
98
  objectName: string;
94
99
  /** Columns to display. Defaults to [displayField, descriptionField]. */
95
100
  columns?: Array<string | LookupColumnDef>;
96
- /** Primary display field (default: 'name') */
101
+ /**
102
+ * The display field as the lookup DECLARES it. The display column is keyed on
103
+ * it, or on `'name'` when it is omitted. With `objectSchema` given, a declared
104
+ * value is also the first rung of the column's title (the resolver's
105
+ * `titleField`); the `'name'` default never is, because it is a guess and a
106
+ * guess never outranks the referenced object's own declarations. So pass
107
+ * `undefined` when the field declares none, never a pre-filled `'name'`.
108
+ */
97
109
  displayField?: string;
98
110
  /**
99
- * Optional `titleFormat` template (e.g. `"{full_name}"` or
100
- * `"{case_number} - {subject}"`). When set and the displayField column is
101
- * auto-inferred, the column renders via the template instead of reading
102
- * a possibly-missing field. Mirrors how DetailView/ObjectCalendar resolve
103
- * record titles.
111
+ * The referenced object's schema (objectui#10486). When given, the display
112
+ * column renders each record's title through `@object-ui/core`'s
113
+ * `getRecordDisplayName`, the call LookupField's dropdown option label and
114
+ * the read cell make: the declared `displayField`, then the object's
115
+ * `nameField`, then its deprecated `titleFormat` template, then type-aware
116
+ * derivation (ADR-0079). The title reads the row with its relations
117
+ * collapsed to ids and the fields the loaded policy denies removed, exactly
118
+ * as the dropdown's option label does.
119
+ *
120
+ * Replaces the retired `titleFormat` prop: a bare template cut out of the
121
+ * schema ranked above the `nameField` beside it, so the picker and the
122
+ * dropdown labelled one record two ways.
104
123
  */
105
- titleFormat?: string | null;
124
+ objectSchema?: Record<string, unknown> | null;
106
125
  /** Record id field (default: 'id') */
107
126
  idField?: string;
108
127
  /** Page size (default: 10) */
@@ -119,7 +138,7 @@ export interface RecordPickerDialogProps {
119
138
  onSelectRecords?: (records: any[]) => void;
120
139
  /**
121
140
  * Base filters applied to every query.
122
- * Converted from LookupFieldMetadata.lookup_filters.
141
+ * Converted from LookupFieldMetadata.lookupFilters.
123
142
  * Restricts which records are selectable (e.g. only active records).
124
143
  */
125
144
  lookupFilters?: LookupFilterDef[];
@@ -145,7 +164,7 @@ export interface RecordPickerDialogProps {
145
164
  * (`before`, `after`, `is_empty`, `is_not_empty`) the record form has no
146
165
  * `$op` for. No second operator vocabulary is introduced here: two already
147
166
  * exist (the spec's `AST_OPERATOR_MAP`, data-objectstack's
148
- * `FILTER_OPERATOR_ALIASES`) and #3948 is what a third costs.
167
+ * `FILTER_OPERATOR_ALIASES`) and objectstack-ai/objectstack#3948 is what a third costs.
149
168
  *
150
169
  * The discriminator is exact rather than heuristic — every AST node is an
151
170
  * ARRAY and a rule is a plain OBJECT, the same predicate `toFilterNode` uses.
@@ -168,7 +187,7 @@ export interface RecordPickerDialogProps {
168
187
  * The referenced object's schema `fields` map (field name → field
169
188
  * definition). When provided, cell renderers receive the FULL field
170
189
  * metadata — `options`, `currency`, `scale`, `precision`, `format`,
171
- * `reference_to`, … — exactly like the list view enriches its columns from
190
+ * `reference`, … — exactly like the list view enriches its columns from
172
191
  * the object schema. Without it a `select` column falls back to
173
192
  * title-casing the raw stored value instead of resolving the option label
174
193
  * (#3333: `manufacturing` rendered as "Manufacturing" instead of the
@@ -225,4 +244,4 @@ export interface RecordPickerDialogProps {
225
244
  * states, and single/multi-select. Responsive: mobile-friendly width
226
245
  * via Tailwind breakpoints.
227
246
  */
228
- export declare function RecordPickerDialog({ open, onOpenChange, title, multiple, dataSource, objectName, columns: columnsProp, displayField, titleFormat, idField, pageSize, value, onSelect, onSelectRecords, lookupFilters, baseFilter, cellRenderer, fieldsMeta, filterColumns, renderFilterBar, renderGrid, }: RecordPickerDialogProps): React.JSX.Element;
247
+ export declare function RecordPickerDialog({ open, onOpenChange, title, multiple, dataSource, objectName, columns: columnsProp, displayField: declaredDisplayField, objectSchema, idField, pageSize, value, onSelect, onSelectRecords, lookupFilters, baseFilter, cellRenderer, fieldsMeta, filterColumns, renderFilterBar, renderGrid, }: RecordPickerDialogProps): React.JSX.Element;
@@ -23,7 +23,7 @@ import { FieldWidgetComponentProps } from './types.js';
23
23
  * from the shared `FullscreenFieldEditor`, so one form-level setting keeps
24
24
  * producing one behaviour across both widgets.
25
25
  *
26
- * ## Readonly display (objectui#5498)
26
+ * ## Readonly display (`4bb940b6e`)
27
27
  *
28
28
  * The readonly early return used to render `{value}` as a React TEXT CHILD,
29
29
  * so a readonly field of any of the three types showed the user the markup
@@ -12,7 +12,18 @@ export interface SelectionTrayProps {
12
12
  /** Remove a selected record by its id. */
13
13
  onRemove: (id: any) => void;
14
14
  displayField?: string;
15
- avatarField?: string;
15
+ /**
16
+ * The fields each chip's name is read from, in order; defaults to the ladder
17
+ * of `displayField` (`getPersonNameFields`). PeoplePicker passes the fields
18
+ * of that ladder the loaded policy lets the viewer read (objectui#10535).
19
+ */
20
+ nameFields?: string[];
21
+ /**
22
+ * Avatar image field (default `image`). `null` draws no image, only the
23
+ * initials: what PeoplePicker passes when field-level security withholds the
24
+ * field (objectui#10433).
25
+ */
26
+ avatarField?: string | null;
16
27
  idField?: string;
17
28
  /** Header label, e.g. "Selected (3)" — omit to hide the header. */
18
29
  label?: string;
@@ -24,4 +35,4 @@ export interface SelectionTrayProps {
24
35
  emptyText?: string;
25
36
  className?: string;
26
37
  }
27
- export declare function SelectionTray({ records, onRemove, displayField, avatarField, idField, label, onClear, clearLabel, emptyText, className, }: SelectionTrayProps): React.JSX.Element;
38
+ export declare function SelectionTray({ records, onRemove, displayField, nameFields, avatarField, idField, label, onClear, clearLabel, emptyText, className, }: SelectionTrayProps): React.JSX.Element;
@@ -4,4 +4,4 @@ import { FieldWidgetComponentProps } from './types.js';
4
4
  * TextField - Standard single-line or multi-line text input
5
5
  * Automatically renders as a textarea when rows are configured in field metadata
6
6
  */
7
- export declare function TextField({ value, onChange, field, readonly, ...props }: FieldWidgetComponentProps<string>): React.JSX.Element;
7
+ export declare function TextField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<string>): React.JSX.Element;
@@ -4,4 +4,4 @@ import { FieldWidgetComponentProps } from './types.js';
4
4
  * TimeField - Time picker input widget
5
5
  * Uses native time input for hour and minute selection
6
6
  */
7
- export declare function TimeField({ value, onChange, field, readonly, ...props }: FieldWidgetComponentProps<string>): React.JSX.Element;
7
+ export declare function TimeField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<string>): React.JSX.Element;
@@ -0,0 +1,33 @@
1
+ import { default as React } from 'react';
2
+ import { FileValueView } from './file-value.js';
3
+ /**
4
+ * One file: a keyboard-reachable view/download link when it resolves, and the
5
+ * caller's own `fallback` text when it does not.
6
+ *
7
+ * The anchor's accessible name is the file NAME, carried as its link text, so
8
+ * this needs no `aria-label` and therefore no new translation key — the name
9
+ * itself already comes off the i18n channel at every call site (a value that
10
+ * carries no name of its own falls back to `fields.file.fileFallback`).
11
+ *
12
+ * `target="_blank"` + `rel="noopener noreferrer"` mirrors `UrlCellRenderer`,
13
+ * and the click is `stopPropagation`-guarded so opening a file from inside a
14
+ * grid row does not also trigger row navigation — the same guard
15
+ * `ImageCellRenderer` puts on its thumbnail.
16
+ *
17
+ * ⛔ No `download` attribute: whether the browser views the file inline or
18
+ * saves it is the storage endpoint's decision, signed into the
19
+ * `content-disposition` it returns. Forcing one of the two here would override
20
+ * a choice the platform already makes per file.
21
+ */
22
+ export declare function FileValueAffordance({ view, fallback, className, icon, }: {
23
+ view: FileValueView;
24
+ /**
25
+ * What to draw when the value resolves to no URL. Required rather than
26
+ * defaulted: every call site already had a text rendering of its own, and a
27
+ * default here would quietly replace one of them.
28
+ */
29
+ fallback: React.ReactNode;
30
+ className?: string;
31
+ /** Suppressed where the row already draws its own icon or thumbnail. */
32
+ icon?: boolean;
33
+ }): React.ReactElement;
@@ -1,28 +1,10 @@
1
+ import { TranslateFn } from '@object-ui/i18n';
1
2
  /**
2
- * The client-side `maxSize` guard shared by the file and image widgets
3
- * (objectui#4141).
4
- *
5
- * Why this is shared rather than mirrored per widget: the guard is the only
6
- * thing standing between a declared `maxSize` and a real `presigned → PUT →
7
- * complete` round trip, and `paramToField` copies `maxSize` onto the field
8
- * config for **every** param type regardless of which widget ends up rendering
9
- * it. So a widget that re-derives the check drifts silently — the image param
10
- * shipped receiving the constraint and never reading it, while the file param
11
- * rejected the identical pick, and nothing failed until QA drove the two side by
12
- * side. One exported function makes the difference visible: a widget either
13
- * calls it or conspicuously does not.
14
- *
15
- * Both widgets deliberately share the `fields.file.exceedsMaxSize` key too. The
16
- * sentence names a file and a limit and says nothing file-widget-specific, it is
17
- * already translated in all 11 built-in locales, and a parallel `fields.image.*`
18
- * twin would be the same sentence maintained twice — drifting the moment one
19
- * translator rewords one of them.
20
- */
21
- /**
22
- * Minimal shape of the i18next `t` this module needs, matching the established
23
- * `TranslateFn` pattern in `app-shell/src/providers/writeWarningToast`.
3
+ * Minimal shape of the i18next `t` this module needs — RE-EXPORTED from its one
4
+ * authority in `@object-ui/i18n`, never re-declared (objectui#8261). The
5
+ * sibling widgets that import it from here keep doing so; no import path moves.
24
6
  */
25
- export type TranslateFn = (key: string, options?: Record<string, unknown>) => string;
7
+ export type { TranslateFn } from '@object-ui/i18n';
26
8
  /**
27
9
  * The rejection message for a pick that exceeds `maxSize`, or `null` when the
28
10
  * pick is within the limit — so a caller reads it as `if (err) reject`.
@@ -3,7 +3,12 @@ import { isFileIdToken } from '@objectstack/spec/data';
3
3
  export interface FileValueView {
4
4
  /** `sys_file` id, when the value carries one. */
5
5
  id?: string;
6
- /** Best available display name. Never empty. */
6
+ /**
7
+ * Best available display name: the value's own, else the caller's
8
+ * `fallbackName`. Empty only when the caller passed an empty fallback to ask
9
+ * for "no name" (the image cell does, so a nameless image reaches its
10
+ * translated alt — objectui#10493).
11
+ */
7
12
  name: string;
8
13
  /** Resolvable URL, when the value carries one. A bare reference does not. */
9
14
  url?: string;
@@ -29,9 +34,8 @@ export interface FileValueView {
29
34
  * Why it matters that this one is shared rather than duplicated: the regex is a
30
35
  * WIRE decision. Widening it server-side (say, ids grow past 64 chars) while a
31
36
  * copy here keeps the old bound turns every new id into "not a reference", and
32
- * the widget then submits the legacy inline blob against a backend that expects
33
- * a reference. That failure surfaces as a broken thumbnail, nowhere near a
34
- * regex.
37
+ * `fileValueForSubmit` then refuses every upload as incomplete — loud, but
38
+ * pointing at the adapter while the cause is a regex two packages away.
35
39
  */
36
40
  export { isFileIdToken };
37
41
  /**
@@ -75,13 +79,38 @@ export interface UploadResultLike {
75
79
  meta?: Record<string, unknown>;
76
80
  }
77
81
  /**
78
- * What to store in the field for a completed upload.
82
+ * A completed upload that surfaced no `sys_file` id, refused at the point of
83
+ * submit (objectui#7699).
79
84
  *
80
- * Returns the bare `sys_file` id when the adapter surfaced one — the reference
81
- * form — and the legacy inline blob when it did not, so a deployment whose
82
- * upload adapter or backend predates file-as-reference keeps working unchanged.
85
+ * The adapter reported success, so this is not a transport failure and the
86
+ * widgets do not quote an adapter message for it: what the user needs to know
87
+ * is that the field was NOT changed. Named — `name` and `code` — so a caller
88
+ * can tell it from a thrown network error without reading the message, and
89
+ * carrying the file's name so a host that reports it can say which pick.
83
90
  */
84
- export declare function fileValueForSubmit(result: UploadResultLike, originalName?: string): string | Record<string, unknown>;
91
+ export declare class UploadIncompleteError extends Error {
92
+ readonly code: "UPLOAD_INCOMPLETE";
93
+ /** The pick's own name when the caller knew it, else the adapter's stored object name. */
94
+ readonly fileName: string;
95
+ constructor(fileName: string);
96
+ }
97
+ /**
98
+ * What to store in the field for a completed upload: the bare `sys_file` id
99
+ * the adapter surfaced in `meta.fileId` — the reference form, the only form
100
+ * the stored contract admits.
101
+ *
102
+ * Throws {@link UploadIncompleteError} when the adapter surfaced no id-shaped
103
+ * `fileId`. ⛔ There is no fallback: the inline blob this function used to
104
+ * build in that case (`{ name, original_name, size, mime_type, url }`) is a
105
+ * shape no deployment's stored contract accepts, and a client that sent it
106
+ * turned a successful-looking upload into a save the backend refused (see the
107
+ * module header's "Submitting"). The callers catch the refusal per pick and
108
+ * render it in the same error row a transport failure lands in.
109
+ *
110
+ * @param originalName the pick's own file name, used to name the refusal;
111
+ * the adapter's stored object name stands in when the caller has none.
112
+ */
113
+ export declare function fileValueForSubmit(result: UploadResultLike, originalName?: string): string;
85
114
  /**
86
115
  * The display view of a just-completed upload.
87
116
  *
@@ -56,7 +56,7 @@ export declare function resolveSchemaOptions(meta: {
56
56
  * referenced object's schema (`fieldsMeta`) the same way the list view enriches
57
57
  * its columns. This is what lets a `select` column resolve its option label
58
58
  * (options + i18n) instead of title-casing the raw value (#3333), and what
59
- * carries `reference` / `reference_to` through to the lookup cell renderer so
59
+ * carries `reference` through to the lookup cell renderer so
60
60
  * an unresolved foreign-key id resolves to a name (#5492).
61
61
  *
62
62
  * Columns whose def carries no `type` inherit the schema field's type, so
@@ -68,9 +68,21 @@ export interface LookupColumnRenderContext {
68
68
  descriptors: Record<string, any>;
69
69
  /** Type-aware renderer resolver; when absent every cell degrades to text. */
70
70
  cellRenderer?: LookupCellRendererResolver;
71
- /** The referenced object's `titleFormat`, applied to the display column. */
72
- titleFormat?: string | null;
73
- /** The lookup's display field — the column the title template stands in for. */
71
+ /**
72
+ * The referenced object's schema. When given, the display column renders the
73
+ * record's title through `@object-ui/core`'s `getRecordDisplayName`, the same
74
+ * call the lookup dropdown's option label and the read cell make
75
+ * (objectui#10486).
76
+ */
77
+ objectSchema?: Record<string, unknown> | null;
78
+ /**
79
+ * The lookup field's DECLARED display field, handed to the resolver as its
80
+ * `titleField`. `undefined` when the field declares none: the `name` default
81
+ * the display column is keyed on is a guess, and a guess never outranks the
82
+ * referenced object's own declarations.
83
+ */
84
+ titleField?: string;
85
+ /** The column the record title stands in for (the display column's key). */
74
86
  displayField?: string;
75
87
  /** Locale for the plain-text `$date` fallback. */
76
88
  displayLocale?: string;
@@ -79,9 +91,14 @@ export interface LookupColumnRenderContext {
79
91
  * Render one column of one candidate record.
80
92
  *
81
93
  * Order:
82
- * 1. `titleFormat` template, for the display column only — so users see a
83
- * human-readable name instead of a raw id when the display field
84
- * (commonly defaulted to `name`) does not exist on the record.
94
+ * 1. the record title, for the display column only and only when the
95
+ * referenced object's schema is given: `getRecordDisplayName` with the
96
+ * lookup's declared display field as its `titleField`, i.e. the field's
97
+ * own `displayField`, then the object's `nameField`, then its deprecated
98
+ * `titleFormat` template, then type-aware derivation (ADR-0079; the lookup
99
+ * branch of objectui#9436's ruling C1). The resolver's own floor
100
+ * (`Untitled` / `Record #<id>`) is not a title here, so a record with
101
+ * nothing nameable falls through to step 2 and keeps its column's value.
85
102
  * 2. the type-aware cell renderer for the column's field descriptor.
86
103
  * 3. a plain-text fallback for columns with no descriptor / no resolver.
87
104
  *
@@ -6,32 +6,14 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  /**
9
- * Stored value → `<input type="date">` value (`YYYY-MM-DD`).
9
+ * Value adapters for the native date/time controls (objectui#3127).
10
10
  *
11
- * A leading `YYYY-MM-DD` is passed through VERBATIM rather than re-parsed: a
12
- * date-only string is parsed as UTC midnight by `new Date()`, so reading local
13
- * calendar components back out of it shifts the day by one everywhere west of
14
- * Greenwich. `2026-06-17` must stay June 17th in every zone.
11
+ * They live in `@object-ui/core` (`utils/native-date-value.ts`), next to the
12
+ * `isRealCalendarDate` judgement they share, so `@object-ui/components`' data
13
+ * table can use the same set without a package cycle (objectui#10625). This
14
+ * module re-exports them unchanged, so every importer here, and the public
15
+ * `@object-ui/fields` barrel, names the same symbols as before. The design
16
+ * notes (the round trip, the shape kept in form state, the impossible stored
17
+ * day) are in the `core` module's header.
15
18
  */
16
- export declare function toDateInputValue(value: unknown): string;
17
- /**
18
- * Stored value → `<input type="datetime-local">` value (`YYYY-MM-DDTHH:mm`),
19
- * expressed in the viewer's local wall clock — the same instant the detail
20
- * page shows, so opening the editor never appears to move the time.
21
- *
22
- * A value that is ALREADY a zone-less local datetime is truncated to minutes
23
- * in place instead of being round-tripped through `Date`, which would
24
- * reinterpret it and then re-render it, twice risking an off-by-a-zone shift.
25
- */
26
- export declare function toDateTimeInputValue(value: unknown): string;
27
- /**
28
- * `<input type="datetime-local">` value → stored value (ISO-8601, UTC).
29
- *
30
- * The control hands us a zone-less local wall clock; `new Date()` reads that
31
- * form as local time, which is the inverse of what `toDateTimeInputValue`
32
- * wrote — the pair is what keeps read and write on one basis.
33
- *
34
- * An unparseable string is returned untouched rather than blanked: dropping a
35
- * value the user can see in the box would be a data loss dressed up as a fix.
36
- */
37
- export declare function fromDateTimeInputValue(value: string): string;
19
+ export { toDateInputValue, toDateTimeInputValue, fromDateTimeInputValue, isImpossibleStoredDay, } from '@object-ui/core';
@@ -0,0 +1,137 @@
1
+ import { FieldMetadata } from '@object-ui/types';
2
+ import { PercentScale } from '@objectstack/spec/data';
3
+ /**
4
+ * Format a numeric value the way the `number` field declaring it says to
5
+ * display it.
6
+ *
7
+ * @param value the numeric value. Callers decide what counts as empty or as
8
+ * not a number before calling: this formats a number, it does
9
+ * not judge one.
10
+ * @param field the field metadata; its `type`, `scale` and `useGrouping` are
11
+ * the inputs read.
12
+ * @param locale the display locale tag, from `useDisplayLocale()`.
13
+ */
14
+ export declare function formatNumberFieldValue(value: number, field: FieldMetadata, locale: string): string;
15
+ /**
16
+ * The display width of a currency amount: the resolved currency's own ISO 4217
17
+ * minor-unit count (2 for USD / CNY, 0 for JPY, 3 for KWD), or the historical 2
18
+ * when no currency is resolved, since there is then no minor unit to ask about.
19
+ *
20
+ * A currency's decimal places are the CURRENCY's, not a field setting: the
21
+ * maintainer ruling recorded on objectstack-ai/objectstack#19910 (batch #218
22
+ * item 2), executed by objectui#10276. ⛔ Not the field-level `precision` (the
23
+ * TOTAL digit count of a decimal(p, s) column), ⛔ not `scale` (taken off the
24
+ * `currency` type by ruling B on objectstack-ai/objectstack#19629) and ⛔ not
25
+ * `currencyConfig.precision`. `CurrencyField`'s comment beside its call carries
26
+ * the full reasoning.
27
+ *
28
+ * One function, so the width a currency amount is DISPLAYED at
29
+ * ({@link formatCurrency}) and the width `CurrencyField` EDITS it at (its
30
+ * `step` and its blur rounding) cannot part.
31
+ */
32
+ export declare function currencyDisplayWidth(currency: string | undefined): number;
33
+ /**
34
+ * Format a currency amount: the ONE call every display face of a `currency`
35
+ * field makes. `CurrencyCellRenderer` in a table, `CurrencyField`'s read-only
36
+ * branch in a form, and every face that takes the cell's formatter by
37
+ * reference all reach it, so they agree by construction (objectui#11444).
38
+ *
39
+ * When `currency` is undefined the amount is a plain number with thousands
40
+ * separators and no symbol. Silently assuming USD for unconfigured currency
41
+ * fields was the #1 source of "why is my RMB amount showing as dollars?" bug
42
+ * reports.
43
+ *
44
+ * The width is the DECLARED one, {@link currencyDisplayWidth}, for every
45
+ * amount: `$3,456.00`, `$3,456.50`, `¥3,456`, `KWD 3,456.000`. Triage ruled
46
+ * that the protocol's convention is the declared width (objectui#11444, comment
47
+ * 5946462862), the family's one width source (ruling A′, `resolveFieldScale`),
48
+ * and retired the objectui#4033 whole-amount trimming this function used to
49
+ * apply: a whole amount dropped its fraction (`$3,456`, `KWD 3,456`) while the
50
+ * read-only form showed the same amount at the currency's width (`$3,456.00`),
51
+ * so one stored value read two ways.
52
+ *
53
+ * ONE width for both `Intl` bounds, never a range (objectui#4332). The symbol
54
+ * branch once passed `minimumFractionDigits: 0` against a larger maximum, and
55
+ * `Intl` then emits the SHORTEST representation in range, so a genuine cents
56
+ * value of `.50` printed as `.5` (`$1,234.5`).
57
+ *
58
+ * The width is the CURRENCY's own minor-unit count, not a literal 2
59
+ * (objectui#4361). Passing 2 for every currency on earth overrode what `Intl`
60
+ * already knows: a yen amount was printed with cents it does not have
61
+ * (`¥1,234.50`) and a dinar amount one digit short (`KWD 1.50`).
62
+ *
63
+ * Deliberately passes NO `scale` to `formatDisplayNumber`: `scale: 0` without a
64
+ * currency reads as an ordinal there and drops the grouping separators
65
+ * (objectui#4033), and an amount is never an ordinal.
66
+ */
67
+ export declare function formatCurrency(value: number, currency?: string, locale?: string): string;
68
+ /**
69
+ * The storage scale a percent FACE reads a field's stored number at: the
70
+ * spec's `percentScaleOf` (`@objectstack/spec/data`) by reference, plus the
71
+ * one type the percent cell draws that the spec does not answer for
72
+ * (objectui#11475).
73
+ *
74
+ * `PercentCellRenderer` reads it, and so does every face that renders the
75
+ * cell's number for the same field (the grid footer, the record detail chip,
76
+ * the gantt tooltip, the mobile card, the object metric tile), so one stored
77
+ * value reads one magnitude on all of them. ⛔ None of them reads the storage
78
+ * off the VALUE: a magnitude guess cannot tell a fraction-stored `1` (100%)
79
+ * from one percentage point.
80
+ *
81
+ * - **`percent`, and a textual field the `format: 'percent'` hint promotes to
82
+ * the percent cell:** `percentScaleOf` asked about `percent`, the face's own
83
+ * type, with the field's declared `max`. A percent field stores a fraction
84
+ * unless it declares a `max` above 1. Asking about `percent` rather than the
85
+ * bag's own `type` is what lets a promoted textual field, or a bag whose
86
+ * `type` was rewritten to the renderer key, read by the percent convention
87
+ * too; it is the same reason `PercentField` asks `resolveFieldScale` about
88
+ * `percent`. `percentScaleOf` answers every percent field, so the result is
89
+ * never `undefined`.
90
+ * - **`progress`: `whole`, stated here.** The spec's `percentScaleOf` answers
91
+ * `undefined` for it (it keys on `percent` alone), so the spec states no
92
+ * percent storage for a `progress` field, and an undeclared key follows the
93
+ * implementation: the one editor a `progress` field has, `SliderField`,
94
+ * stores the slider's position on a `0`–`100` range unless the field
95
+ * declares other bounds, and every first-party `progress` field declares
96
+ * `min: 0, max: 100`. So its number IS percentage points.
97
+ *
98
+ * @param field the field definition the face holds. Only `type` and `max`
99
+ * are read, and a `max` that is not a number is not a
100
+ * declaration.
101
+ */
102
+ export declare function percentCellScale(field: {
103
+ type?: string;
104
+ max?: unknown;
105
+ } | null | undefined): PercentScale;
106
+ /**
107
+ * Render a percent on a value ALREADY in display magnitude (`80` means 80%):
108
+ * the ONE rendering call every display face of a `percent` field makes.
109
+ * `formatPercent` (the table cell, and every face that takes the cell's
110
+ * formatter by reference) and `PercentField`'s read-only branch in a form both
111
+ * end here, so
112
+ * the locale's percent convention, the grouping and the width cannot part
113
+ * between them (objectui#11444). The read-only face printed
114
+ * `toDisplay(value).toFixed(scale)` plus a literal `%`: a `scale: 2` field
115
+ * holding `0.25` read `25.00%` in a `de` form while its cell read `25,00 %`.
116
+ *
117
+ * The SCALING is the caller's, and it is a separate statement on purpose:
118
+ * both callers scale at the storage the field DECLARES, read through the
119
+ * spec's `percentScaleOf` (a fraction unless the field declares a `max` above
120
+ * 1, the rule the edit input writes by). `formatPercent` takes it as its
121
+ * `percentScale` argument, which the cell answers with
122
+ * {@link percentCellScale}; `PercentField` asks `percentScaleOf` itself, so
123
+ * its read-only text and its input never show one stored value at two
124
+ * magnitudes. Until objectui#11475 the cell guessed the storage from the
125
+ * value's magnitude instead, so a fraction-stored `1` (100%) read `100%` in a
126
+ * read-only form and `1%` in its cell, and a whole-stored `0.5` read `0.5%`
127
+ * and `50%`.
128
+ * ⛔ Do not add a caller with a scaling rule of its own, and ⛔ never one keyed
129
+ * on the column's NAME: objectui#9452 removed exactly that from
130
+ * `PercentCellRenderer`.
131
+ *
132
+ * @param displayValue the percentage points to render.
133
+ * @param precision the decimal places, fixed at both bounds: the width the
134
+ * caller resolved (`resolveFieldScale`, ruling A′).
135
+ * @param locale the display locale tag, from `useDisplayLocale()`.
136
+ */
137
+ export declare function formatPercentPoints(displayValue: number, precision: number, locale?: string): string;
@@ -0,0 +1,135 @@
1
+ import { default as React } from 'react';
2
+ import { TranslateFn } from './file-size-guard.js';
3
+ /**
4
+ * ONE reading of "this box is showing text the browser cannot read", shared by
5
+ * every `type="number"` widget in this package (objectui#6780).
6
+ *
7
+ * ## The defect this closes
8
+ *
9
+ * A `type="number"` box can DISPLAY one thing and hold another, with nothing
10
+ * said. Measured on Chromium 141.0.7390.37 (Playwright 1.62.1), typing `1e`
11
+ * into an empty number input:
12
+ *
13
+ * ```
14
+ * .value = "" <- what every widget here reads
15
+ * validity.badInput = true <- what the browser actually thinks
16
+ * pixels vs an untouched box: DIFFERENT <- the box IS still showing `1e`
17
+ * ```
18
+ *
19
+ * So the widget emitted `null`, `aria-invalid` stayed `"false"`, and no
20
+ * diagnostic was drawn: objectui#6716's silent-refusal class, on a money field.
21
+ *
22
+ * ## Why `validity.badInput` and not a regex of our own
23
+ *
24
+ * It is the PLATFORM's own predicate — the browser stating, in its own words,
25
+ * whether it can read the text — rather than a second renderer-side dialect of
26
+ * "what a number is" (AGENTS.md #0.1).
27
+ *
28
+ * ⚠️ objectui#6765 recorded `badInput` as "the ONE signal happy-dom and Chromium
29
+ * agree on". objectui#6780 measured that and NARROWED it, because the
30
+ * difference decides which strings a unit test may drive: in Chromium
31
+ * `badInput` is never true for a programmatic `.value` write (it reports that
32
+ * the UA cannot read the USER's input), so on the only route a happy-dom test
33
+ * has, the two engines agree on nothing. The agreement this guard rests on is
34
+ * between happy-dom's programmatic verdict and Chromium's TYPED verdict, and it
35
+ * holds for a measured subset — four strings agree on "bad", seven agree on
36
+ * "fine", and eight disagree. The full matrix, and the lists the tests are
37
+ * allowed to use, are in `__tests__/numberInputBrowserReadings.ts`.
38
+ *
39
+ * ⭐ And it is NOT objectui#6715's anchored guard wearing a different hat.
40
+ * That one was measured to be a provable no-op here — it rejects only strings
41
+ * this test environment fabricates. This one fires on keyboard-reachable
42
+ * states, MEASURED in Chromium by typing each key by key into an empty box:
43
+ *
44
+ * ```
45
+ * typed "1e" "1e-" "1e+" "-" "." "+" "-." "e" "5e" -> badInput TRUE
46
+ * typed "1." "1e5" "1.2.3" "0x10" "12abc" -> badInput false
47
+ * ```
48
+ *
49
+ * and never on anything the browser can actually emit: `12`, `1.23`, `010`,
50
+ * `15`, `1`, `12.345` all read `badInput === false`.
51
+ *
52
+ * ## ⛔ What this deliberately does NOT do: change what is emitted
53
+ *
54
+ * objectui#6716's shape REFUSES (its `onChange` never fires and the prior value
55
+ * stands). Here that would destroy the very text the diagnostic points at, so
56
+ * these widgets keep emitting exactly what they emitted before and only stop
57
+ * being silent about it. MEASURED, in both halves:
58
+ *
59
+ * - Chromium: after typing `1e`, a script write of `.value` clears the raw
60
+ * display and flips `badInput` back to `false` — even writing `""`.
61
+ * - React 19.2.8's own `updateInput` (`react-dom/cjs/react-dom-client.
62
+ * development.js`) restores a number input with
63
+ * `if ((0 === value && "" === element.value) || element.value != value)
64
+ * element.value = "" + getToStringValue(value);`
65
+ *
66
+ * So for a box that already held `5`, refusing leaves `props.value` at `5`,
67
+ * `element.value` at `""`, and React writes `"5"` back — wiping the `5e` the
68
+ * user is looking at and the message is about. Emitting keeps `props.value` at
69
+ * `""`, the write is skipped, and the raw text survives, which is what
70
+ * objectui#6716 requires of a refusal ("keeps the refused text in the box, so
71
+ * the message has something to point at").
72
+ *
73
+ * ⚠️ The text itself is NOT quotable. The browser displays it but never exposes
74
+ * it — `.value` is `""` and there is no other channel — so unlike
75
+ * objectui#6715's residue message this one cannot name what was typed. It
76
+ * points at the box instead.
77
+ */
78
+ /**
79
+ * The one sentence, in objectui#6716's `Not saved: …` shape, read from the
80
+ * package's locale channel (objectui#8148).
81
+ *
82
+ * objectui#6755 ruled that a widget's OWN refusal sentence goes through
83
+ * `useFieldTranslation` + `FIELD_DEFAULTS`; `320374d2a` applied it to
84
+ * `LocationField`'s residue arm. This was the FIFTH such sentence and the only
85
+ * SHARED one — the four already keyed are each one widget's, while this literal
86
+ * served `NumberField`, `CurrencyField`, `PercentField` and both of
87
+ * `GeolocationField`'s boxes through {@link useBadInputRefusal}. Its sharpest
88
+ * consequence: `GeolocationField` sits beside `LocationField`, whose refusals
89
+ * are all keyed, so two adjacent coordinate widgets refused bad input in two
90
+ * different languages on one form.
91
+ *
92
+ * ⭐ `example` stays a HOLE rather than being keyed per widget, and that is
93
+ * what keeps the decimal numerals out of every pack. Five different values
94
+ * reach this one sentence — `1234`, `1234.56`, `12.5`, `30.2741`, `120.1551` —
95
+ * so a per-widget key would need five keys times ten packs, and each pack would
96
+ * then hold a decimal numeral it could legitimately re-punctuate. A pack that
97
+ * wrote `1234,56` would read as the `latitude, longitude` PAIR the adjacent
98
+ * widget asks for; the hole is filled by the widget in ASCII and no pack ever
99
+ * spells a digit.
100
+ *
101
+ * The English value is byte-identical to the literal it replaces, so English
102
+ * and provider-less rendering are unchanged.
103
+ */
104
+ export declare function badInputMessage(t: TranslateFn, example: string): string;
105
+ /**
106
+ * The widget's OWN refusal state, and the reader that fills it.
107
+ *
108
+ * Named `refusal`, never `error`: `error` is the published validation slot on
109
+ * the widget contract (objectui#3222) with exactly one author, the form
110
+ * renderer. This is the widget's own reading, which no host can produce — the
111
+ * same two-name split `LocationField` uses (objectui#6716).
112
+ *
113
+ * `readBadInput` is called from BOTH arms, because neither alone sees every
114
+ * route (MEASURED in Chromium):
115
+ *
116
+ * - TYPED `1e` into an empty box fires 2 `input` events — the `e` keystroke
117
+ * moves `.value` from `"1"` to `""`, so React does deliver an `onChange`.
118
+ * - PASTED `1e` into an empty box fires 1 `input` event, but `.value` never
119
+ * leaves `""`; React's own input-value tracking suppresses the synthetic
120
+ * `onChange`, so the CHANGE arm never runs. Blur fires, and `badInput` is
121
+ * still `true` at blur time — which is the whole reason for the blur arm.
122
+ */
123
+ export declare function useBadInputRefusal(example: string): {
124
+ refusal: string | null;
125
+ readBadInput: (target: HTMLInputElement | null | undefined) => boolean;
126
+ };
127
+ /**
128
+ * The drawn diagnostic — objectui#6716's exact markup, in one spelling so the
129
+ * four widgets of this class cannot drift apart.
130
+ */
131
+ export declare function BadInputMessage({ refusal }: {
132
+ refusal: string | null;
133
+ }): React.JSX.Element | null;
134
+ /** The class that marks the refused control, in one spelling. */
135
+ export declare const BAD_INPUT_BORDER = "border-red-500 focus-visible:ring-red-500";