@object-ui/fields 4.0.7 → 4.0.8

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.
@@ -25,6 +25,12 @@ export declare function formatCurrency(value: number, currency?: string): string
25
25
  * E.g., $150,000 → $150K, $1,200,000 → $1.2M
26
26
  */
27
27
  export declare function formatCompactCurrency(value: number, currency?: string): string;
28
+ /**
29
+ * Format a plain number with thousands separators, no currency symbol.
30
+ * Used as a safe fallback when a currency-typed field has no `currency`
31
+ * configured — we'd rather render `1,234.50` than silently assume USD.
32
+ */
33
+ export declare function formatNumber(value: number, decimals?: number): string;
28
34
  /**
29
35
  * Format percent value
30
36
  * Handles both decimal (0.8 = 80%) and whole number (80 = 80%) inputs.
@@ -43,15 +49,15 @@ export declare function humanizeLabel(value: string): string;
43
49
  /**
44
50
  * Format date as relative time (e.g., "2 days ago", "Today", "Overdue 3d")
45
51
  */
46
- export declare function formatRelativeDate(value: string | Date): string;
52
+ export declare function formatRelativeDate(value: string | Date | number): string;
47
53
  /**
48
54
  * Format date value
49
55
  */
50
- export declare function formatDate(value: string | Date, style?: string): string;
56
+ export declare function formatDate(value: string | Date | number, style?: string): string;
51
57
  /**
52
58
  * Format datetime value
53
59
  */
54
- export declare function formatDateTime(value: string | Date): string;
60
+ export declare function formatDateTime(value: string | Date | number): string;
55
61
  /**
56
62
  * Text field cell renderer
57
63
  */
@@ -124,6 +130,16 @@ export declare function UserCellRenderer({ value }: CellRendererProps): React.Re
124
130
  * @param renderer React component to render the field
125
131
  */
126
132
  export declare function registerFieldRenderer(type: string, renderer: React.FC<CellRendererProps>): void;
133
+ /**
134
+ * Resolve the canonical cell-renderer key for a field. Accepts either a
135
+ * raw type string (back-compat) or a field-metadata object so that
136
+ * format hints (`format: 'phone'` etc.) can promote a plain `text`
137
+ * field to its richer renderer counterpart.
138
+ */
139
+ export declare function resolveCellRendererType(fieldOrType: string | {
140
+ type?: string;
141
+ format?: string;
142
+ } | null | undefined): string;
127
143
  /**
128
144
  * Get the appropriate cell renderer for a field type
129
145
  */
@@ -91,6 +91,14 @@ export interface RecordPickerDialogProps {
91
91
  columns?: Array<string | LookupColumnDef>;
92
92
  /** Primary display field (default: 'name') */
93
93
  displayField?: string;
94
+ /**
95
+ * Optional `titleFormat` template (e.g. `"{full_name}"` or
96
+ * `"{case_number} - {subject}"`). When set and the displayField column is
97
+ * auto-inferred, the column renders via the template instead of reading
98
+ * a possibly-missing field. Mirrors how DetailView/ObjectCalendar resolve
99
+ * record titles.
100
+ */
101
+ titleFormat?: string | null;
94
102
  /** Record id field (default: 'id') */
95
103
  idField?: string;
96
104
  /** Page size (default: 10) */
@@ -163,4 +171,4 @@ export interface RecordPickerDialogProps {
163
171
  * states, and single/multi-select. Responsive: mobile-friendly width
164
172
  * via Tailwind breakpoints.
165
173
  */
166
- export declare function RecordPickerDialog({ open, onOpenChange, title, multiple, dataSource, objectName, columns: columnsProp, displayField, idField, pageSize, value, onSelect, onSelectRecords, lookupFilters, cellRenderer, filterColumns, renderFilterBar, renderGrid, }: RecordPickerDialogProps): import("react/jsx-runtime").JSX.Element;
174
+ export declare function RecordPickerDialog({ open, onOpenChange, title, multiple, dataSource, objectName, columns: columnsProp, displayField, titleFormat, idField, pageSize, value, onSelect, onSelectRecords, lookupFilters, cellRenderer, filterColumns, renderFilterBar, renderGrid, }: RecordPickerDialogProps): import("react/jsx-runtime").JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/fields",
3
- "version": "4.0.7",
3
+ "version": "4.0.8",
4
4
  "description": "Field renderers and registry for Object UI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -26,13 +26,13 @@
26
26
  "dependencies": {
27
27
  "clsx": "^2.1.1",
28
28
  "lucide-react": "^1.14.0",
29
- "tailwind-merge": "^3.5.0",
30
- "@object-ui/components": "4.0.7",
31
- "@object-ui/core": "4.0.7",
32
- "@object-ui/i18n": "4.0.7",
33
- "@object-ui/providers": "4.0.7",
34
- "@object-ui/react": "4.0.7",
35
- "@object-ui/types": "4.0.7"
29
+ "tailwind-merge": "^3.6.0",
30
+ "@object-ui/components": "4.0.8",
31
+ "@object-ui/core": "4.0.8",
32
+ "@object-ui/i18n": "4.0.8",
33
+ "@object-ui/providers": "4.0.8",
34
+ "@object-ui/react": "4.0.8",
35
+ "@object-ui/types": "4.0.8"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "react": "^18.0.0 || ^19.0.0",
@@ -43,7 +43,7 @@
43
43
  "@types/react-dom": "19.2.3",
44
44
  "@vitejs/plugin-react": "^6.0.1",
45
45
  "typescript": "^6.0.3",
46
- "vite": "^8.0.10",
46
+ "vite": "^8.0.12",
47
47
  "vite-plugin-dts": "^5.0.0"
48
48
  },
49
49
  "publishConfig": {
package/src/index.tsx CHANGED
@@ -11,6 +11,23 @@ import type { FieldMetadata, SelectOptionMetadata } from '@object-ui/types';
11
11
  import { ComponentRegistry } from '@object-ui/core';
12
12
  import { Badge, Avatar, AvatarFallback, Button, Checkbox, EmptyValue } from '@object-ui/components';
13
13
  import { Check, X, Copy, Phone as PhoneIcon } from 'lucide-react';
14
+ import { useObjectTranslation } from '@object-ui/react';
15
+
16
+ /**
17
+ * Safe label resolver for cell-level UI strings. Falls back to the English
18
+ * default when no I18nProvider is available or when the key is missing.
19
+ */
20
+ function useFieldLabel() {
21
+ try {
22
+ const { t } = useObjectTranslation();
23
+ return (key: string, fallback: string) => {
24
+ const v = t(key);
25
+ return !v || v === key ? fallback : v;
26
+ };
27
+ } catch {
28
+ return (_k: string, fallback: string) => fallback;
29
+ }
30
+ }
14
31
 
15
32
  import { TextField } from './widgets/TextField';
16
33
  import { NumberField } from './widgets/NumberField';
@@ -127,6 +144,22 @@ export function formatCompactCurrency(value: number, currency: string = 'USD'):
127
144
  }
128
145
  }
129
146
 
147
+ /**
148
+ * Format a plain number with thousands separators, no currency symbol.
149
+ * Used as a safe fallback when a currency-typed field has no `currency`
150
+ * configured — we'd rather render `1,234.50` than silently assume USD.
151
+ */
152
+ export function formatNumber(value: number, decimals: number = 2): string {
153
+ try {
154
+ return new Intl.NumberFormat('en-US', {
155
+ minimumFractionDigits: decimals,
156
+ maximumFractionDigits: decimals,
157
+ }).format(value);
158
+ } catch {
159
+ return value.toFixed(decimals);
160
+ }
161
+ }
162
+
130
163
  /**
131
164
  * Format percent value
132
165
  * Handles both decimal (0.8 = 80%) and whole number (80 = 80%) inputs.
@@ -153,10 +186,10 @@ export function humanizeLabel(value: string): string {
153
186
  /**
154
187
  * Format date as relative time (e.g., "2 days ago", "Today", "Overdue 3d")
155
188
  */
156
- export function formatRelativeDate(value: string | Date): string {
157
- if (!value) return '—';
158
- const date = typeof value === 'string' ? new Date(value) : value;
159
- if (isNaN(date.getTime())) return '—';
189
+ export function formatRelativeDate(value: string | Date | number): string {
190
+ if (value === null || value === undefined || value === '') return '—';
191
+ const date = value instanceof Date ? value : new Date(value as any);
192
+ if (!(date instanceof Date) || isNaN(date.getTime())) return '—';
160
193
 
161
194
  const now = new Date();
162
195
  const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
@@ -179,10 +212,10 @@ export function formatRelativeDate(value: string | Date): string {
179
212
  /**
180
213
  * Format date value
181
214
  */
182
- export function formatDate(value: string | Date, style?: string): string {
183
- if (!value) return '—';
184
- const date = typeof value === 'string' ? new Date(value) : value;
185
- if (isNaN(date.getTime())) return '—';
215
+ export function formatDate(value: string | Date | number, style?: string): string {
216
+ if (value === null || value === undefined || value === '') return '—';
217
+ const date = value instanceof Date ? value : new Date(value as any);
218
+ if (!(date instanceof Date) || isNaN(date.getTime())) return '—';
186
219
 
187
220
  if (style === 'short') {
188
221
  // Compact format for mobile: "Jan 15, '24"
@@ -207,10 +240,10 @@ export function formatDate(value: string | Date, style?: string): string {
207
240
  /**
208
241
  * Format datetime value
209
242
  */
210
- export function formatDateTime(value: string | Date): string {
211
- if (!value) return '—';
212
- const date = typeof value === 'string' ? new Date(value) : value;
213
- if (isNaN(date.getTime())) return '—';
243
+ export function formatDateTime(value: string | Date | number): string {
244
+ if (value === null || value === undefined || value === '') return '—';
245
+ const date = value instanceof Date ? value : new Date(value as any);
246
+ if (!(date instanceof Date) || isNaN(date.getTime())) return '—';
214
247
 
215
248
  return date.toLocaleDateString(undefined, {
216
249
  year: 'numeric',
@@ -255,9 +288,14 @@ export function CurrencyCellRenderer({ value, field }: CellRendererProps): React
255
288
 
256
289
  const safe = coerceToSafeValue(value);
257
290
  const currencyField = field as any;
258
- const currency = currencyField.currency || 'USD';
291
+ // Honor both `currency` (legacy/grid configs) and `defaultCurrency`
292
+ // (the canonical key from `@objectstack/spec` Field.currency()).
293
+ const currency: string | undefined = currencyField.currency || currencyField.defaultCurrency;
259
294
  const num = Number(safe);
260
- const formatted = !isNaN(num) ? formatCurrency(num, currency) : String(safe);
295
+ const decimals = currencyField.precision ?? currencyField.scale ?? currencyField.decimals ?? 2;
296
+ const formatted = !isNaN(num)
297
+ ? (currency ? formatCurrency(num, currency) : formatNumber(num, decimals))
298
+ : String(safe);
261
299
 
262
300
  return <span className="tabular-nums font-medium whitespace-nowrap">{formatted}</span>;
263
301
  }
@@ -351,7 +389,7 @@ export function BooleanCellRenderer({ value, field }: CellRendererProps): React.
351
389
  }
352
390
 
353
391
  return (
354
- <div className="flex items-center justify-center">
392
+ <div className="flex items-center justify-start">
355
393
  <Checkbox checked={!!value} disabled className="pointer-events-none" />
356
394
  </div>
357
395
  );
@@ -370,16 +408,16 @@ export function DateCellRenderer({ value, field }: CellRendererProps): React.Rea
370
408
  // Determine if date is overdue (in the past) — but only color it red when the
371
409
  // field is *semantically* a due/deadline. A plain "start_date" or "created_at"
372
410
  // in the past should not render in red.
373
- const date = typeof safe === 'string' ? new Date(safe) : safe;
374
- const isValidDate = date instanceof Date && !isNaN(date.getTime());
411
+ const date = safe != null ? new Date(safe as string | number) : null;
412
+ const isValidDate = date !== null && !isNaN(date.getTime());
375
413
  const startOfToday = new Date();
376
414
  startOfToday.setHours(0, 0, 0, 0);
377
415
  const fieldName = String(dateField?.name || dateField?.accessorKey || dateField?.key || '').toLowerCase();
378
416
  const dueLike =
379
417
  dateField?.dueLike === true ||
380
418
  /(^|_)(due|deadline|expires?|expiry|expiration|expected_close|target_close|sla|return_by|renewal|next_action)(_|$)/.test(fieldName);
381
- const isOverdue = dueLike && isValidDate && date < startOfToday;
382
- const isoString = isValidDate ? date.toISOString() : String(safe);
419
+ const isOverdue = dueLike && isValidDate && date! < startOfToday;
420
+ const isoString = isValidDate ? date!.toISOString() : String(safe);
383
421
 
384
422
  return (
385
423
  <span
@@ -397,8 +435,8 @@ export function DateCellRenderer({ value, field }: CellRendererProps): React.Rea
397
435
  export function DateTimeCellRenderer({ value }: CellRendererProps): React.ReactElement {
398
436
  if (!value) return <EmptyValue />;
399
437
  const safe = coerceToSafeValue(value);
400
- const date = typeof safe === 'string' ? new Date(safe) : safe;
401
- if (!(date instanceof Date) || isNaN(date.getTime())) return <EmptyValue />;
438
+ const date = safe != null ? new Date(safe as string | number) : null;
439
+ if (date === null || isNaN(date.getTime())) return <EmptyValue />;
402
440
 
403
441
  const datePart = date.toLocaleDateString(undefined, {
404
442
  month: 'numeric',
@@ -421,8 +459,9 @@ export function DateTimeCellRenderer({ value }: CellRendererProps): React.ReactE
421
459
 
422
460
  // Semantic color mapping (auto-detect from value text for priority & status fields)
423
461
  // Keys use underscore notation; lookup normalizes spaces/hyphens to underscores automatically.
462
+ // Chinese keys are stored as-is and matched directly (no normalization side-effects).
424
463
  const SEMANTIC_COLOR_MAP: Record<string, string> = {
425
- // Priority values
464
+ // Priority values (en)
426
465
  critical: 'red',
427
466
  urgent: 'red',
428
467
  high: 'orange',
@@ -430,7 +469,7 @@ const SEMANTIC_COLOR_MAP: Record<string, string> = {
430
469
  normal: 'blue',
431
470
  low: 'gray',
432
471
  none: 'gray',
433
- // Status values
472
+ // Status values (en)
434
473
  paid: 'green',
435
474
  completed: 'green',
436
475
  done: 'green',
@@ -455,6 +494,52 @@ const SEMANTIC_COLOR_MAP: Record<string, string> = {
455
494
  overdue: 'red',
456
495
  delivered: 'purple',
457
496
  archived: 'indigo',
497
+ // CRM lifecycle values (en)
498
+ contacted: 'blue',
499
+ qualified: 'purple',
500
+ converted: 'green',
501
+ won: 'green',
502
+ lost: 'red',
503
+ // Priority values (zh)
504
+ 紧急: 'red',
505
+ 严重: 'red',
506
+ 高: 'orange',
507
+ 中: 'yellow',
508
+ 普通: 'blue',
509
+ 低: 'gray',
510
+ 无: 'gray',
511
+ // Status values (zh)
512
+ 新建: 'gray',
513
+ 草稿: 'gray',
514
+ 待处理: 'yellow',
515
+ 待审核: 'yellow',
516
+ 待联系: 'yellow',
517
+ 挂起: 'yellow',
518
+ 进行中: 'blue',
519
+ 处理中: 'blue',
520
+ 已联系: 'blue',
521
+ 跟进中: 'blue',
522
+ 已发货: 'blue',
523
+ 打开: 'blue',
524
+ 已确认: 'green',
525
+ 已审核: 'green',
526
+ 已通过: 'green',
527
+ 已完成: 'green',
528
+ 已支付: 'green',
529
+ 已签收: 'green',
530
+ 已转化: 'green',
531
+ 成单: 'green',
532
+ 赢得: 'green',
533
+ 已签约: 'green',
534
+ 已交付: 'purple',
535
+ 已归档: 'indigo',
536
+ 已关闭: 'gray',
537
+ 已取消: 'red',
538
+ 已拒绝: 'red',
539
+ 失败: 'red',
540
+ 逾期: 'red',
541
+ 流失: 'red',
542
+ 丢失: 'red',
458
543
  };
459
544
 
460
545
  // Color to Tailwind class mapping for custom Badge styling
@@ -511,18 +596,29 @@ function getBadgeColorClasses(color?: string, val?: unknown): string {
511
596
  export function SelectCellRenderer({ value, field }: CellRendererProps): React.ReactElement {
512
597
  const selectField = field as any;
513
598
  const options: SelectOptionMetadata[] = selectField.options || [];
514
-
599
+
515
600
  if (value == null || value === '') return <EmptyValue />;
516
-
601
+
602
+ // Match a stored value to a configured option, falling back to a
603
+ // case-insensitive comparison so seed data with mixed case
604
+ // (e.g. "Referral" stored, "referral" defined) still resolves to the
605
+ // localized option label.
606
+ const findOption = (val: any): SelectOptionMetadata | undefined => {
607
+ const exact = options.find(opt => opt.value === val);
608
+ if (exact) return exact;
609
+ const norm = String(val).toLowerCase();
610
+ return options.find(opt => String(opt.value).toLowerCase() === norm);
611
+ };
612
+
517
613
  // Handle multiple values
518
614
  if (Array.isArray(value)) {
519
615
  return (
520
616
  <div className="flex flex-wrap gap-1">
521
617
  {value.map((val, idx) => {
522
- const option = options.find(opt => opt.value === val);
618
+ const option = findOption(val);
523
619
  const label = option?.label || humanizeLabel(String(val));
524
620
  const colorClasses = getBadgeColorClasses(option?.color, val);
525
-
621
+
526
622
  return (
527
623
  <Badge
528
624
  key={idx}
@@ -536,9 +632,9 @@ export function SelectCellRenderer({ value, field }: CellRendererProps): React.R
536
632
  </div>
537
633
  );
538
634
  }
539
-
635
+
540
636
  // Handle single value
541
- const option = options.find(opt => opt.value === value);
637
+ const option = findOption(value);
542
638
  const label = option?.label || humanizeLabel(String(value));
543
639
  const colorClasses = getBadgeColorClasses(option?.color, value);
544
640
 
@@ -557,7 +653,8 @@ export function SelectCellRenderer({ value, field }: CellRendererProps): React.R
557
653
  */
558
654
  export function EmailCellRenderer({ value }: CellRendererProps): React.ReactElement {
559
655
  if (!value) return <EmptyValue />;
560
-
656
+
657
+ const label = useFieldLabel();
561
658
  const safe = String(coerceToSafeValue(value) ?? '');
562
659
  const [copied, setCopied] = React.useState(false);
563
660
 
@@ -588,7 +685,7 @@ export function EmailCellRenderer({ value }: CellRendererProps): React.ReactElem
588
685
  type="button"
589
686
  className="opacity-0 group-hover/email:opacity-100 transition-opacity p-0.5 rounded hover:bg-muted"
590
687
  onClick={handleCopy}
591
- aria-label="Copy email"
688
+ aria-label={label('detail.copyEmail', 'Copy email')}
592
689
  >
593
690
  {copied ? (
594
691
  <Check className="h-3 w-3 text-green-600" />
@@ -630,7 +727,8 @@ export function UrlCellRenderer({ value }: CellRendererProps): React.ReactElemen
630
727
  */
631
728
  export function PhoneCellRenderer({ value }: CellRendererProps): React.ReactElement {
632
729
  if (!value) return <EmptyValue />;
633
-
730
+
731
+ const label = useFieldLabel();
634
732
  const safe = String(coerceToSafeValue(value) ?? '');
635
733
  const [copied, setCopied] = React.useState(false);
636
734
 
@@ -657,7 +755,7 @@ export function PhoneCellRenderer({ value }: CellRendererProps): React.ReactElem
657
755
  type="button"
658
756
  className="opacity-0 group-hover/phone:opacity-100 transition-opacity p-0.5 rounded hover:bg-muted"
659
757
  onClick={handleCopy}
660
- aria-label="Copy phone number"
758
+ aria-label={label('detail.copyPhone', 'Copy phone number')}
661
759
  >
662
760
  {copied ? (
663
761
  <Check className="h-3 w-3 text-green-600" />
@@ -868,6 +966,51 @@ export function registerFieldRenderer(type: string, renderer: React.FC<CellRende
868
966
  fieldRegistry.set(type, renderer);
869
967
  }
870
968
 
969
+ /**
970
+ * Format hints (e.g. `{ type: 'text', format: 'phone' }`) that should
971
+ * map to a richer cell renderer than the bare type would imply. The
972
+ * canonical ObjectStack pattern uses `Field.text({ format: 'phone' })`
973
+ * for plain text columns that should still render with a tel: link,
974
+ * `Field.text({ format: 'url' })` for clickable links, etc.
975
+ *
976
+ * Only applies when the field's base type is a generic text type;
977
+ * explicit types like 'phone'/'email'/'currency' already win without
978
+ * any format hint.
979
+ */
980
+ const FORMAT_TO_RENDERER: Record<string, string> = {
981
+ phone: 'phone',
982
+ tel: 'phone',
983
+ telephone: 'phone',
984
+ email: 'email',
985
+ url: 'url',
986
+ uri: 'url',
987
+ link: 'url',
988
+ currency: 'currency',
989
+ money: 'currency',
990
+ percent: 'percent',
991
+ percentage: 'percent',
992
+ };
993
+
994
+ const TEXTUAL_BASE_TYPES = new Set(['text', 'textarea', 'string', 'longtext', '']);
995
+
996
+ /**
997
+ * Resolve the canonical cell-renderer key for a field. Accepts either a
998
+ * raw type string (back-compat) or a field-metadata object so that
999
+ * format hints (`format: 'phone'` etc.) can promote a plain `text`
1000
+ * field to its richer renderer counterpart.
1001
+ */
1002
+ export function resolveCellRendererType(fieldOrType: string | { type?: string; format?: string } | null | undefined): string {
1003
+ if (!fieldOrType) return 'text';
1004
+ if (typeof fieldOrType === 'string') return fieldOrType;
1005
+ const baseType = (fieldOrType.type || '').toLowerCase();
1006
+ const formatRaw = fieldOrType.format;
1007
+ const format = typeof formatRaw === 'string' ? formatRaw.toLowerCase() : '';
1008
+ if (format && FORMAT_TO_RENDERER[format] && TEXTUAL_BASE_TYPES.has(baseType)) {
1009
+ return FORMAT_TO_RENDERER[format];
1010
+ }
1011
+ return fieldOrType.type || 'text';
1012
+ }
1013
+
871
1014
  /**
872
1015
  * Get the appropriate cell renderer for a field type
873
1016
  */
@@ -3,29 +3,47 @@ import { Input, EmptyValue } from '@object-ui/components';
3
3
  import { FieldWidgetProps } from './types';
4
4
 
5
5
  /**
6
- * Format currency value for display
6
+ * Format currency value for display. When `currency` is undefined the value
7
+ * is rendered as a plain number with thousands separators (no symbol),
8
+ * because silently assuming USD is misleading for non-USD businesses.
7
9
  */
8
- function formatCurrency(value: number, currency: string = 'USD'): string {
10
+ function formatAmount(value: number, currency: string | undefined, precision: number): string {
11
+ if (currency) {
12
+ try {
13
+ return new Intl.NumberFormat('en-US', {
14
+ style: 'currency',
15
+ currency,
16
+ minimumFractionDigits: precision,
17
+ maximumFractionDigits: precision,
18
+ }).format(value);
19
+ } catch {
20
+ return `${currency} ${value.toFixed(precision)}`;
21
+ }
22
+ }
9
23
  try {
10
24
  return new Intl.NumberFormat('en-US', {
11
- style: 'currency',
12
- currency,
25
+ minimumFractionDigits: precision,
26
+ maximumFractionDigits: precision,
13
27
  }).format(value);
14
28
  } catch {
15
- return `${currency} ${value.toFixed(2)}`;
29
+ return value.toFixed(precision);
16
30
  }
17
31
  }
18
32
 
33
+ const CURRENCY_SYMBOLS: Record<string, string> = {
34
+ USD: '$',
35
+ };
36
+
19
37
  export function CurrencyField({ value, onChange, field, readonly, errorMessage, className, ...props }: FieldWidgetProps<number>) {
20
38
  const currencyField = (field || (props as any).schema) as any;
21
- const currency = currencyField?.currency || 'USD';
39
+ const currency: string | undefined = currencyField?.currency;
22
40
  const precision = currencyField?.precision ?? 2;
23
41
 
24
42
  if (readonly) {
25
43
  if (value == null) return <EmptyValue />;
26
44
  return (
27
45
  <span className="text-sm font-medium tabular-nums">
28
- {formatCurrency(Number(value), currency)}
46
+ {formatAmount(Number(value), currency, precision)}
29
47
  </span>
30
48
  );
31
49
  }
@@ -38,11 +56,15 @@ export function CurrencyField({ value, onChange, field, readonly, errorMessage,
38
56
  }
39
57
  };
40
58
 
59
+ const symbol = currency ? (currency === 'USD' ? '$' : currency) : '';
60
+
41
61
  return (
42
62
  <div className="relative">
43
- <span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm text-gray-500">
44
- {currency === 'USD' ? '$' : currency}
45
- </span>
63
+ {symbol && (
64
+ <span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm text-gray-500">
65
+ {symbol}
66
+ </span>
67
+ )}
46
68
  <Input
47
69
  {...props}
48
70
  type="number"
@@ -54,7 +76,7 @@ export function CurrencyField({ value, onChange, field, readonly, errorMessage,
54
76
  onBlur={handleBlur}
55
77
  placeholder={currencyField?.placeholder || '0.00'}
56
78
  disabled={readonly || props.disabled}
57
- className={`pl-8 ${className || ''}`}
79
+ className={`${symbol ? 'pl-8' : ''} ${className || ''}`}
58
80
  step={Math.pow(10, -precision).toFixed(precision)}
59
81
  aria-invalid={!!errorMessage}
60
82
  />