@entifix/react-controls 0.18.1 → 0.19.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 (180) hide show
  1. package/README.md +5 -5
  2. package/dist/i18n/catalog/en.d.ts.map +1 -1
  3. package/dist/i18n/catalog/en.js +13 -1
  4. package/dist/i18n/catalog/en.js.map +1 -1
  5. package/dist/i18n/catalog/es.d.ts +12 -0
  6. package/dist/i18n/catalog/es.d.ts.map +1 -1
  7. package/dist/i18n/catalog/es.js +13 -1
  8. package/dist/i18n/catalog/es.js.map +1 -1
  9. package/dist/i18n/catalog/index.d.ts +24 -0
  10. package/dist/i18n/catalog/index.d.ts.map +1 -1
  11. package/dist/index.d.ts +7 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +7 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/primitives.d.ts +7 -0
  16. package/dist/primitives.d.ts.map +1 -1
  17. package/dist/primitives.js +7 -0
  18. package/dist/primitives.js.map +1 -1
  19. package/dist/theme/theme-context.d.ts +23 -7
  20. package/dist/theme/theme-context.d.ts.map +1 -1
  21. package/dist/theme/theme-context.js +76 -14
  22. package/dist/theme/theme-context.js.map +1 -1
  23. package/dist/theme/theme-script.d.ts +4 -3
  24. package/dist/theme/theme-script.d.ts.map +1 -1
  25. package/dist/theme/theme-script.js +8 -3
  26. package/dist/theme/theme-script.js.map +1 -1
  27. package/dist/theme/types.d.ts +19 -3
  28. package/dist/theme/types.d.ts.map +1 -1
  29. package/dist/theme/types.js.map +1 -1
  30. package/dist/ui/atoms/alert/alert-dismiss.d.ts +9 -0
  31. package/dist/ui/atoms/alert/alert-dismiss.d.ts.map +1 -0
  32. package/dist/ui/atoms/alert/alert-dismiss.js +28 -0
  33. package/dist/ui/atoms/alert/alert-dismiss.js.map +1 -0
  34. package/dist/ui/atoms/alert/alert.d.ts +24 -0
  35. package/dist/ui/atoms/alert/alert.d.ts.map +1 -0
  36. package/dist/ui/atoms/alert/alert.js +50 -0
  37. package/dist/ui/atoms/alert/alert.js.map +1 -0
  38. package/dist/ui/atoms/alert/index.d.ts +3 -0
  39. package/dist/ui/atoms/alert/index.d.ts.map +1 -0
  40. package/dist/ui/atoms/alert/index.js +4 -0
  41. package/dist/ui/atoms/alert/index.js.map +1 -0
  42. package/dist/ui/atoms/alert/tone.d.ts +8 -0
  43. package/dist/ui/atoms/alert/tone.d.ts.map +1 -0
  44. package/dist/ui/atoms/alert/tone.js +11 -0
  45. package/dist/ui/atoms/alert/tone.js.map +1 -0
  46. package/dist/ui/atoms/button/button-recipe.js +1 -1
  47. package/dist/ui/atoms/button/button-recipe.js.map +1 -1
  48. package/dist/ui/atoms/button/index.d.ts +1 -0
  49. package/dist/ui/atoms/button/index.d.ts.map +1 -1
  50. package/dist/ui/atoms/button/index.js +1 -0
  51. package/dist/ui/atoms/button/index.js.map +1 -1
  52. package/dist/ui/atoms/button/submit-button.d.ts +13 -0
  53. package/dist/ui/atoms/button/submit-button.d.ts.map +1 -0
  54. package/dist/ui/atoms/button/submit-button.js +29 -0
  55. package/dist/ui/atoms/button/submit-button.js.map +1 -0
  56. package/dist/ui/atoms/cell-value/cell-value.d.ts +6 -1
  57. package/dist/ui/atoms/cell-value/cell-value.d.ts.map +1 -1
  58. package/dist/ui/atoms/cell-value/cell-value.js +20 -2
  59. package/dist/ui/atoms/cell-value/cell-value.js.map +1 -1
  60. package/dist/ui/atoms/field/checkbox.d.ts.map +1 -1
  61. package/dist/ui/atoms/field/checkbox.js +21 -1
  62. package/dist/ui/atoms/field/checkbox.js.map +1 -1
  63. package/dist/ui/atoms/field/field.js +1 -1
  64. package/dist/ui/atoms/field/field.js.map +1 -1
  65. package/dist/ui/atoms/field-control/field-control.d.ts +6 -1
  66. package/dist/ui/atoms/field-control/field-control.d.ts.map +1 -1
  67. package/dist/ui/atoms/field-control/field-control.js +23 -3
  68. package/dist/ui/atoms/field-control/field-control.js.map +1 -1
  69. package/dist/ui/atoms/money/index.d.ts +4 -0
  70. package/dist/ui/atoms/money/index.d.ts.map +1 -0
  71. package/dist/ui/atoms/money/index.js +5 -0
  72. package/dist/ui/atoms/money/index.js.map +1 -0
  73. package/dist/ui/atoms/money/money-input.d.ts +19 -0
  74. package/dist/ui/atoms/money/money-input.d.ts.map +1 -0
  75. package/dist/ui/atoms/money/money-input.js +75 -0
  76. package/dist/ui/atoms/money/money-input.js.map +1 -0
  77. package/dist/ui/atoms/money/money-math.d.ts +21 -0
  78. package/dist/ui/atoms/money/money-math.d.ts.map +1 -0
  79. package/dist/ui/atoms/money/money-math.js +36 -0
  80. package/dist/ui/atoms/money/money-math.js.map +1 -0
  81. package/dist/ui/atoms/money/money.d.ts +19 -0
  82. package/dist/ui/atoms/money/money.d.ts.map +1 -0
  83. package/dist/ui/atoms/money/money.js +26 -0
  84. package/dist/ui/atoms/money/money.js.map +1 -0
  85. package/dist/ui/atoms/number-input/index.d.ts +3 -0
  86. package/dist/ui/atoms/number-input/index.d.ts.map +1 -0
  87. package/dist/ui/atoms/number-input/index.js +4 -0
  88. package/dist/ui/atoms/number-input/index.js.map +1 -0
  89. package/dist/ui/atoms/number-input/number-input.d.ts +10 -0
  90. package/dist/ui/atoms/number-input/number-input.d.ts.map +1 -0
  91. package/dist/ui/atoms/number-input/number-input.js +23 -0
  92. package/dist/ui/atoms/number-input/number-input.js.map +1 -0
  93. package/dist/ui/atoms/number-input/quantity-input.d.ts +22 -0
  94. package/dist/ui/atoms/number-input/quantity-input.d.ts.map +1 -0
  95. package/dist/ui/atoms/number-input/quantity-input.js +99 -0
  96. package/dist/ui/atoms/number-input/quantity-input.js.map +1 -0
  97. package/dist/ui/atoms/skeleton/skeleton.js +1 -1
  98. package/dist/ui/atoms/skeleton/skeleton.js.map +1 -1
  99. package/dist/ui/atoms/status-tag/index.d.ts +2 -0
  100. package/dist/ui/atoms/status-tag/index.d.ts.map +1 -0
  101. package/dist/ui/atoms/status-tag/index.js +3 -0
  102. package/dist/ui/atoms/status-tag/index.js.map +1 -0
  103. package/dist/ui/atoms/status-tag/status-tag.d.ts +17 -0
  104. package/dist/ui/atoms/status-tag/status-tag.d.ts.map +1 -0
  105. package/dist/ui/atoms/status-tag/status-tag.js +37 -0
  106. package/dist/ui/atoms/status-tag/status-tag.js.map +1 -0
  107. package/dist/ui/atoms/switch/switch.d.ts.map +1 -1
  108. package/dist/ui/atoms/switch/switch.js +6 -3
  109. package/dist/ui/atoms/switch/switch.js.map +1 -1
  110. package/dist/ui/molecules/breadcrumbs/breadcrumbs.d.ts +11 -2
  111. package/dist/ui/molecules/breadcrumbs/breadcrumbs.d.ts.map +1 -1
  112. package/dist/ui/molecules/breadcrumbs/breadcrumbs.js +9 -7
  113. package/dist/ui/molecules/breadcrumbs/breadcrumbs.js.map +1 -1
  114. package/dist/ui/molecules/column-settings/column-settings.js +1 -1
  115. package/dist/ui/molecules/column-settings/column-settings.js.map +1 -1
  116. package/dist/ui/molecules/confirm-dialog/confirm-dialog.js +2 -2
  117. package/dist/ui/molecules/confirm-dialog/confirm-dialog.js.map +1 -1
  118. package/dist/ui/molecules/description-list/description-list.d.ts +17 -0
  119. package/dist/ui/molecules/description-list/description-list.d.ts.map +1 -0
  120. package/dist/ui/molecules/description-list/description-list.js +36 -0
  121. package/dist/ui/molecules/description-list/description-list.js.map +1 -0
  122. package/dist/ui/molecules/description-list/index.d.ts +2 -0
  123. package/dist/ui/molecules/description-list/index.d.ts.map +1 -0
  124. package/dist/ui/molecules/description-list/index.js +3 -0
  125. package/dist/ui/molecules/description-list/index.js.map +1 -0
  126. package/dist/ui/molecules/drawer/drawer.js +3 -3
  127. package/dist/ui/molecules/drawer/drawer.js.map +1 -1
  128. package/dist/ui/molecules/empty-state/empty-state.d.ts +15 -0
  129. package/dist/ui/molecules/empty-state/empty-state.d.ts.map +1 -0
  130. package/dist/ui/molecules/empty-state/empty-state.js +29 -0
  131. package/dist/ui/molecules/empty-state/empty-state.js.map +1 -0
  132. package/dist/ui/molecules/empty-state/index.d.ts +2 -0
  133. package/dist/ui/molecules/empty-state/index.d.ts.map +1 -0
  134. package/dist/ui/molecules/empty-state/index.js +3 -0
  135. package/dist/ui/molecules/empty-state/index.js.map +1 -0
  136. package/dist/ui/molecules/entity-link-input/entity-link-input.d.ts.map +1 -1
  137. package/dist/ui/molecules/entity-link-input/entity-link-input.js +7 -4
  138. package/dist/ui/molecules/entity-link-input/entity-link-input.js.map +1 -1
  139. package/dist/ui/molecules/entity-link-picker/entity-link-picker.js +2 -2
  140. package/dist/ui/molecules/entity-link-picker/entity-link-picker.js.map +1 -1
  141. package/dist/ui/molecules/media-gallery/index.d.ts +2 -0
  142. package/dist/ui/molecules/media-gallery/index.d.ts.map +1 -0
  143. package/dist/ui/molecules/media-gallery/index.js +3 -0
  144. package/dist/ui/molecules/media-gallery/index.js.map +1 -0
  145. package/dist/ui/molecules/media-gallery/media-gallery.d.ts +31 -0
  146. package/dist/ui/molecules/media-gallery/media-gallery.d.ts.map +1 -0
  147. package/dist/ui/molecules/media-gallery/media-gallery.js +64 -0
  148. package/dist/ui/molecules/media-gallery/media-gallery.js.map +1 -0
  149. package/dist/ui/molecules/menu/menu.d.ts.map +1 -1
  150. package/dist/ui/molecules/menu/menu.js +6 -4
  151. package/dist/ui/molecules/menu/menu.js.map +1 -1
  152. package/dist/ui/molecules/nav-menu/nav-menu.js +4 -4
  153. package/dist/ui/molecules/nav-menu/nav-menu.js.map +1 -1
  154. package/dist/ui/molecules/segmented-nav/segment-classes.d.ts +3 -3
  155. package/dist/ui/molecules/segmented-nav/segment-classes.d.ts.map +1 -1
  156. package/dist/ui/molecules/segmented-nav/segment-classes.js +2 -2
  157. package/dist/ui/molecules/segmented-nav/segment-classes.js.map +1 -1
  158. package/dist/ui/molecules/segmented-nav/segmented-nav.d.ts +14 -4
  159. package/dist/ui/molecules/segmented-nav/segmented-nav.d.ts.map +1 -1
  160. package/dist/ui/molecules/segmented-nav/segmented-nav.js +6 -5
  161. package/dist/ui/molecules/segmented-nav/segmented-nav.js.map +1 -1
  162. package/dist/ui/molecules/split-view/split-view.js +3 -3
  163. package/dist/ui/molecules/split-view/split-view.js.map +1 -1
  164. package/dist/ui/molecules/tab-strip/tab-strip.d.ts +51 -12
  165. package/dist/ui/molecules/tab-strip/tab-strip.d.ts.map +1 -1
  166. package/dist/ui/molecules/tab-strip/tab-strip.js +125 -34
  167. package/dist/ui/molecules/tab-strip/tab-strip.js.map +1 -1
  168. package/dist/ui/molecules/toast-region/toast-region.js +1 -1
  169. package/dist/ui/molecules/toast-region/toast-region.js.map +1 -1
  170. package/dist/ui/organisms/command-palette/command-palette.js +6 -6
  171. package/dist/ui/organisms/command-palette/command-palette.js.map +1 -1
  172. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.d.ts.map +1 -1
  173. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js +8 -2
  174. package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js.map +1 -1
  175. package/dist/ui/organisms/entity-form/entity-form.js +4 -2
  176. package/dist/ui/organisms/entity-form/entity-form.js.map +1 -1
  177. package/dist/ui/organisms/entity-table/entity-table.d.ts.map +1 -1
  178. package/dist/ui/organisms/entity-table/entity-table.js +2 -1
  179. package/dist/ui/organisms/entity-table/entity-table.js.map +1 -1
  180. package/package.json +5 -5
@@ -2,11 +2,16 @@ import { type EntityFieldDescriptor } from '@entifix/core';
2
2
  export interface CellValueProps {
3
3
  value: unknown;
4
4
  descriptor: EntityFieldDescriptor;
5
+ /**
6
+ * The record the value belongs to. A money member whose currency is another
7
+ * member of the record (`money.currencyMember`) reads it from here.
8
+ */
9
+ record?: Readonly<Record<string, unknown>>;
5
10
  }
6
11
  /**
7
12
  * Renders one entity member according to its metadata type. Centralizing this
8
13
  * is what lets a table stay generic: a link, a date and a number all arrive as
9
14
  * "the value of an accessor", and only the descriptor decides how they read.
10
15
  */
11
- export declare function CellValue({ value, descriptor }: CellValueProps): import("react").JSX.Element;
16
+ export declare function CellValue({ value, descriptor, record }: CellValueProps): import("react").JSX.Element;
12
17
  //# sourceMappingURL=cell-value.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cell-value.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/cell-value/cell-value.tsx"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,qBAAqB,EAK3B,MAAM,eAAe,CAAC;AAsIvB,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,qBAAqB,CAAC;CACnC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,cAAc,+BA+B9D"}
1
+ {"version":3,"file":"cell-value.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/cell-value/cell-value.tsx"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,qBAAqB,EAK3B,MAAM,eAAe,CAAC;AAwIvB,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,qBAAqB,CAAC;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC5C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,cAAc,+BAkDtE"}
@@ -2,6 +2,8 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { EntityCollectionLink, EntityLink, isLocalizedText, localize } from '@entifix/core';
4
4
  import { useEnumLabel, useFormatters, useLocale, useT } from '../../../i18n/index.js';
5
+ import { Money } from '../money/money.js';
6
+ import { currencyOf } from '../money/money-math.js';
5
7
  /** Stand-in for an absent value, so an empty cell still reads as intentional. */ const EMPTY = '—';
6
8
  /**
7
9
  * A link reads as its target's label when the target is loaded (embedded or
@@ -73,18 +75,34 @@ function formatByType(value, type, linkLabelProperty, formatting) {
73
75
  * Renders one entity member according to its metadata type. Centralizing this
74
76
  * is what lets a table stay generic: a link, a date and a number all arrive as
75
77
  * "the value of an accessor", and only the descriptor decides how they read.
76
- */ export function CellValue({ value, descriptor }) {
78
+ */ export function CellValue({ value, descriptor, record }) {
77
79
  var _NUMERIC_CLASS_descriptor_type;
78
80
  const t = useT();
79
81
  const formatters = useFormatters();
80
82
  const enumLabel = useEnumLabel();
81
83
  const locale = useLocale();
82
- if (value === undefined || value === null) {
84
+ if (value === undefined || value === null || value === '') {
83
85
  return /*#__PURE__*/ _jsx("span", {
84
86
  className: "text-content-muted",
85
87
  children: EMPTY
86
88
  });
87
89
  }
90
+ // An amount reads as money in the reader's locale, never as the stored
91
+ // number. A draft holds it as a string, so a numeric string counts too. A
92
+ // currency the record does not carry leaves it a plain number rather than
93
+ // guessing one.
94
+ if (descriptor.money !== undefined) {
95
+ const amount = typeof value === 'number' ? value : Number(value);
96
+ const currency = currencyOf(descriptor.money, record);
97
+ if (currency !== undefined && Number.isFinite(amount)) {
98
+ return /*#__PURE__*/ _jsx(Money, {
99
+ amount: amount,
100
+ currency: currency,
101
+ unit: descriptor.money.unit,
102
+ locale: locale
103
+ });
104
+ }
105
+ }
88
106
  const text = formatByType(value, descriptor.type, descriptor.linkLabelProperty, {
89
107
  formatters,
90
108
  yes: t('value.yes'),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/atoms/cell-value/cell-value.tsx"],"sourcesContent":["'use client';\n\nimport type { Formatters, Locale } from '@entifix/core';\nimport {\n type Entity,\n EntityCollectionLink,\n type EntityFieldDescriptor,\n EntityLink,\n isLocalizedText,\n localize,\n type MetaAccessorType,\n} from '@entifix/core';\n\nimport {\n useEnumLabel,\n useFormatters,\n useLocale,\n useT,\n} from '../../../i18n/index.js';\n\n/** Stand-in for an absent value, so an empty cell still reads as intentional. */\nconst EMPTY = '—';\n\n/**\n * A link reads as its target's label when the target is loaded (embedded or\n * resolved), and as the bare foreign key when it is not — the same cell markup\n * covers both shapes a relation can arrive in.\n */\nfunction entityLabel(\n target: Entity | undefined,\n linkLabelProperty: string,\n locale: Locale,\n): string | undefined {\n const label = (target as Record<string, unknown> | undefined)?.[\n linkLabelProperty\n ];\n if (label == null) return undefined;\n // A target labelled by a localized member reads in the reader's locale.\n return isLocalizedText(label) ? localize(label, locale) : String(label);\n}\n\nfunction formatLink(\n link: EntityLink<Entity>,\n linkLabelProperty: string,\n locale: Locale,\n): string {\n return (\n entityLabel(link.value, linkLabelProperty, locale) ??\n (link.id == null ? EMPTY : String(link.id))\n );\n}\n\nfunction formatCollectionLink(\n link: EntityCollectionLink<Entity>,\n linkLabelProperty: string,\n locale: Locale,\n): string {\n const loaded = link.values;\n if (loaded !== undefined) {\n return loaded\n .map(\n (target, index) =>\n entityLabel(target, linkLabelProperty, locale) ??\n String(target.id ?? link.ids[index] ?? ''),\n )\n .join(', ');\n }\n return link.ids.map(String).join(', ');\n}\n\n/**\n * Everything locale-dependent, resolved once by the component and handed down.\n * The formatters carry an explicit locale, which is what keeps a date rendered\n * on the server byte-identical to the same date rendered in the browser.\n */\ninterface CellFormatting {\n readonly formatters: Formatters;\n readonly yes: string;\n readonly no: string;\n readonly enumLabel: (value: string) => string;\n readonly rowCount: (count: number) => string;\n readonly locale: Locale;\n}\n\nfunction formatByType(\n value: unknown,\n type: MetaAccessorType,\n linkLabelProperty: string,\n formatting: CellFormatting,\n): string {\n switch (type) {\n case 'date':\n return formatting.formatters.date(\n value instanceof Date ? value : String(value),\n );\n case 'number':\n return typeof value === 'number'\n ? formatting.formatters.number(value)\n : String(value);\n case 'boolean':\n return value ? formatting.yes : formatting.no;\n case 'enum':\n return formatting.enumLabel(String(value));\n case 'link':\n return value instanceof EntityLink\n ? formatLink(value, linkLabelProperty, formatting.locale)\n : String(value);\n case 'linkCollection':\n return value instanceof EntityCollectionLink\n ? formatCollectionLink(value, linkLabelProperty, formatting.locale)\n : String(value);\n case 'scalarCollection':\n // The same comma list the draft holds, spaced for reading. Not `String`:\n // an array's own `toString` joins on a bare comma, which reads as one\n // run-on token in a narrow column.\n return Array.isArray(value) ? value.join(', ') : String(value);\n case 'composition':\n // Owned rows have no useful one-line form — a receipt's lines are not a\n // sentence — and the master's own screen is where they are read and\n // edited. So the cell reports the count and stops.\n return Array.isArray(value)\n ? formatting.rowCount(value.length)\n : String(value);\n case 'localized':\n // The reader's locale, falling back to the host's default; a value\n // that is not a text per locale has no reading worth guessing at.\n return isLocalizedText(value)\n ? (localize(value, formatting.locale) ?? '')\n : '';\n default:\n return String(value);\n }\n}\n\n/**\n * Tabular figures, applied once, here — not on `--font-sans` and not per call\n * site. A column of numbers or dates must align on the digit; a paragraph must\n * not, because proportional figures are what prose is set in. The descriptor\n * already knows which is which, so the cell is the only place that has to.\n */\nconst NUMERIC_CLASS: Partial<Record<MetaAccessorType, string>> = {\n number: 'tabular-nums',\n date: 'tabular-nums',\n};\n\nexport interface CellValueProps {\n value: unknown;\n descriptor: EntityFieldDescriptor;\n}\n\n/**\n * Renders one entity member according to its metadata type. Centralizing this\n * is what lets a table stay generic: a link, a date and a number all arrive as\n * \"the value of an accessor\", and only the descriptor decides how they read.\n */\nexport function CellValue({ value, descriptor }: CellValueProps) {\n const t = useT();\n const formatters = useFormatters();\n const enumLabel = useEnumLabel();\n const locale = useLocale();\n\n if (value === undefined || value === null) {\n return <span className=\"text-content-muted\">{EMPTY}</span>;\n }\n\n const text = formatByType(\n value,\n descriptor.type,\n descriptor.linkLabelProperty,\n {\n formatters,\n yes: t('value.yes'),\n no: t('value.no'),\n enumLabel: raw => enumLabel(descriptor, raw),\n rowCount: count => t('value.rowCount', { count }),\n locale,\n },\n );\n\n if (text === '') {\n return <span className=\"text-content-muted\">{EMPTY}</span>;\n }\n\n return (\n <span className={NUMERIC_CLASS[descriptor.type] ?? undefined}>{text}</span>\n );\n}\n"],"names":["EntityCollectionLink","EntityLink","isLocalizedText","localize","useEnumLabel","useFormatters","useLocale","useT","EMPTY","entityLabel","target","linkLabelProperty","locale","label","undefined","String","formatLink","link","value","id","formatCollectionLink","loaded","values","map","index","ids","join","formatByType","type","formatting","formatters","date","Date","number","yes","no","enumLabel","Array","isArray","rowCount","length","NUMERIC_CLASS","CellValue","descriptor","t","span","className","text","raw","count"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../src/ui/atoms/cell-value/cell-value.tsx"],"sourcesContent":["'use client';\n\nimport type { Formatters, Locale } from '@entifix/core';\nimport {\n type Entity,\n EntityCollectionLink,\n type EntityFieldDescriptor,\n EntityLink,\n isLocalizedText,\n localize,\n type MetaAccessorType,\n} from '@entifix/core';\n\nimport {\n useEnumLabel,\n useFormatters,\n useLocale,\n useT,\n} from '../../../i18n/index.js';\nimport { Money } from '../money/money.js';\nimport { currencyOf } from '../money/money-math.js';\n\n/** Stand-in for an absent value, so an empty cell still reads as intentional. */\nconst EMPTY = '—';\n\n/**\n * A link reads as its target's label when the target is loaded (embedded or\n * resolved), and as the bare foreign key when it is not — the same cell markup\n * covers both shapes a relation can arrive in.\n */\nfunction entityLabel(\n target: Entity | undefined,\n linkLabelProperty: string,\n locale: Locale,\n): string | undefined {\n const label = (target as Record<string, unknown> | undefined)?.[\n linkLabelProperty\n ];\n if (label == null) return undefined;\n // A target labelled by a localized member reads in the reader's locale.\n return isLocalizedText(label) ? localize(label, locale) : String(label);\n}\n\nfunction formatLink(\n link: EntityLink<Entity>,\n linkLabelProperty: string,\n locale: Locale,\n): string {\n return (\n entityLabel(link.value, linkLabelProperty, locale) ??\n (link.id == null ? EMPTY : String(link.id))\n );\n}\n\nfunction formatCollectionLink(\n link: EntityCollectionLink<Entity>,\n linkLabelProperty: string,\n locale: Locale,\n): string {\n const loaded = link.values;\n if (loaded !== undefined) {\n return loaded\n .map(\n (target, index) =>\n entityLabel(target, linkLabelProperty, locale) ??\n String(target.id ?? link.ids[index] ?? ''),\n )\n .join(', ');\n }\n return link.ids.map(String).join(', ');\n}\n\n/**\n * Everything locale-dependent, resolved once by the component and handed down.\n * The formatters carry an explicit locale, which is what keeps a date rendered\n * on the server byte-identical to the same date rendered in the browser.\n */\ninterface CellFormatting {\n readonly formatters: Formatters;\n readonly yes: string;\n readonly no: string;\n readonly enumLabel: (value: string) => string;\n readonly rowCount: (count: number) => string;\n readonly locale: Locale;\n}\n\nfunction formatByType(\n value: unknown,\n type: MetaAccessorType,\n linkLabelProperty: string,\n formatting: CellFormatting,\n): string {\n switch (type) {\n case 'date':\n return formatting.formatters.date(\n value instanceof Date ? value : String(value),\n );\n case 'number':\n return typeof value === 'number'\n ? formatting.formatters.number(value)\n : String(value);\n case 'boolean':\n return value ? formatting.yes : formatting.no;\n case 'enum':\n return formatting.enumLabel(String(value));\n case 'link':\n return value instanceof EntityLink\n ? formatLink(value, linkLabelProperty, formatting.locale)\n : String(value);\n case 'linkCollection':\n return value instanceof EntityCollectionLink\n ? formatCollectionLink(value, linkLabelProperty, formatting.locale)\n : String(value);\n case 'scalarCollection':\n // The same comma list the draft holds, spaced for reading. Not `String`:\n // an array's own `toString` joins on a bare comma, which reads as one\n // run-on token in a narrow column.\n return Array.isArray(value) ? value.join(', ') : String(value);\n case 'composition':\n // Owned rows have no useful one-line form — a receipt's lines are not a\n // sentence — and the master's own screen is where they are read and\n // edited. So the cell reports the count and stops.\n return Array.isArray(value)\n ? formatting.rowCount(value.length)\n : String(value);\n case 'localized':\n // The reader's locale, falling back to the host's default; a value\n // that is not a text per locale has no reading worth guessing at.\n return isLocalizedText(value)\n ? (localize(value, formatting.locale) ?? '')\n : '';\n default:\n return String(value);\n }\n}\n\n/**\n * Tabular figures, applied once, here — not on `--font-sans` and not per call\n * site. A column of numbers or dates must align on the digit; a paragraph must\n * not, because proportional figures are what prose is set in. The descriptor\n * already knows which is which, so the cell is the only place that has to.\n */\nconst NUMERIC_CLASS: Partial<Record<MetaAccessorType, string>> = {\n number: 'tabular-nums',\n date: 'tabular-nums',\n};\n\nexport interface CellValueProps {\n value: unknown;\n descriptor: EntityFieldDescriptor;\n /**\n * The record the value belongs to. A money member whose currency is another\n * member of the record (`money.currencyMember`) reads it from here.\n */\n record?: Readonly<Record<string, unknown>>;\n}\n\n/**\n * Renders one entity member according to its metadata type. Centralizing this\n * is what lets a table stay generic: a link, a date and a number all arrive as\n * \"the value of an accessor\", and only the descriptor decides how they read.\n */\nexport function CellValue({ value, descriptor, record }: CellValueProps) {\n const t = useT();\n const formatters = useFormatters();\n const enumLabel = useEnumLabel();\n const locale = useLocale();\n\n if (value === undefined || value === null || value === '') {\n return <span className=\"text-content-muted\">{EMPTY}</span>;\n }\n\n // An amount reads as money in the reader's locale, never as the stored\n // number. A draft holds it as a string, so a numeric string counts too. A\n // currency the record does not carry leaves it a plain number rather than\n // guessing one.\n if (descriptor.money !== undefined) {\n const amount = typeof value === 'number' ? value : Number(value);\n const currency = currencyOf(descriptor.money, record);\n if (currency !== undefined && Number.isFinite(amount)) {\n return (\n <Money\n amount={amount}\n currency={currency}\n unit={descriptor.money.unit}\n locale={locale}\n />\n );\n }\n }\n\n const text = formatByType(\n value,\n descriptor.type,\n descriptor.linkLabelProperty,\n {\n formatters,\n yes: t('value.yes'),\n no: t('value.no'),\n enumLabel: raw => enumLabel(descriptor, raw),\n rowCount: count => t('value.rowCount', { count }),\n locale,\n },\n );\n\n if (text === '') {\n return <span className=\"text-content-muted\">{EMPTY}</span>;\n }\n\n return (\n <span className={NUMERIC_CLASS[descriptor.type] ?? undefined}>{text}</span>\n );\n}\n"],"names":["EntityCollectionLink","EntityLink","isLocalizedText","localize","useEnumLabel","useFormatters","useLocale","useT","Money","currencyOf","EMPTY","entityLabel","target","linkLabelProperty","locale","label","undefined","String","formatLink","link","value","id","formatCollectionLink","loaded","values","map","index","ids","join","formatByType","type","formatting","formatters","date","Date","number","yes","no","enumLabel","Array","isArray","rowCount","length","NUMERIC_CLASS","CellValue","descriptor","record","t","span","className","money","amount","Number","currency","isFinite","unit","text","raw","count"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"checkbox.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/field/checkbox.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,wBAAwB,EAAqB,MAAM,OAAO,CAAC;AAIzE,MAAM,WAAW,aAAc,SAAQ,IAAI,CACzC,wBAAwB,CAAC,OAAO,CAAC,EACjC,MAAM,CACP;IACC;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,EACvB,KAAK,EACL,SAAS,EACT,aAAqB,EACrB,GAAG,KAAK,EACT,EAAE,aAAa,+BAiCf"}
1
+ {"version":3,"file":"checkbox.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/field/checkbox.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,wBAAwB,EAAqB,MAAM,OAAO,CAAC;AAIzE,MAAM,WAAW,aAAc,SAAQ,IAAI,CACzC,wBAAwB,CAAC,OAAO,CAAC,EACjC,MAAM,CACP;IACC;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAwBD;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,EACvB,KAAK,EACL,SAAS,EACT,aAAqB,EACrB,GAAG,KAAK,EACT,EAAE,aAAa,+BAiCf"}
@@ -4,6 +4,26 @@ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_wi
4
4
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  import { useEffect, useRef } from 'react';
6
6
  import { cn } from '../../utils/cn.js';
7
+ /**
8
+ * The box is painted at 1rem and HIT at `--target-min` (WCAG 2.5.8, ADR 0025).
9
+ * The input itself is the target-sized element — an input renders no
10
+ * pseudo-element of its own until `appearance: none` hands its painting over,
11
+ * which is what this does — and a negative margin gives the extra back, so the
12
+ * layout still sees a 1rem box at any floor and compact density stays compact. The square,
13
+ * its tick and the indeterminate bar are drawn by `::before` and `::after`
14
+ * with `border-control` and the primary colour; the tick and the bar are
15
+ * borders, so forced colours keep them.
16
+ */ const BOX = [
17
+ 'relative size-(--target-min) shrink-0 cursor-pointer appearance-none rounded-sm focus-ring',
18
+ 'disabled:cursor-not-allowed disabled:opacity-50',
19
+ // The painted square: 1rem, centred in the target whatever its floor.
20
+ 'before:absolute before:inset-[calc((var(--target-min)-(--spacing(4)))/2)] before:rounded-sm before:border before:border-border-control before:bg-surface-elevated',
21
+ 'checked:before:border-primary checked:before:bg-primary indeterminate:before:border-primary indeterminate:before:bg-primary',
22
+ // The tick: two borders of a rotated box.
23
+ 'checked:after:absolute checked:after:inset-0 checked:after:m-auto checked:after:h-2.5 checked:after:w-1.5 checked:after:-translate-y-px checked:after:rotate-45 checked:after:border-r-2 checked:after:border-b-2 checked:after:border-primary-content',
24
+ // The bar: one border.
25
+ 'indeterminate:after:absolute indeterminate:after:top-1/2 indeterminate:after:left-1/2 indeterminate:after:w-2 indeterminate:after:-translate-1/2 indeterminate:after:border-t-2 indeterminate:after:border-primary-content'
26
+ ].join(' ');
7
27
  /**
8
28
  * A checkbox, with its caption when it has one.
9
29
  *
@@ -31,7 +51,7 @@ import { cn } from '../../utils/cn.js';
31
51
  const box = /*#__PURE__*/ _jsx("input", _extends({
32
52
  ref: ref,
33
53
  type: "checkbox",
34
- className: cn('size-4 accent-primary', label ? undefined : className)
54
+ className: cn(BOX, label ? undefined : className)
35
55
  }, props));
36
56
  if (label === undefined) return box;
37
57
  return /*#__PURE__*/ _jsxs("label", {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/atoms/field/checkbox.tsx"],"sourcesContent":["'use client';\n\nimport { type ComponentPropsWithoutRef, useEffect, useRef } from 'react';\n\nimport { cn } from '../../utils/cn.js';\n\nexport interface CheckboxProps extends Omit<\n ComponentPropsWithoutRef<'input'>,\n 'type'\n> {\n /**\n * The visible caption. Omit it for a box whose meaning comes from where it\n * sits — a row's selection box, where the column header is the caption and\n * the row itself is the subject — and pass `aria-label` instead, so the\n * control still announces itself to a screen reader. A box with neither is\n * unlabelled.\n */\n label?: string;\n /**\n * The third state: some of the rows below are selected, but not all.\n *\n * It is a DOM **property**, not an attribute, so React cannot set it from\n * JSX — `<input indeterminate>` is silently dropped — and it has to be\n * written through a ref after render. That is the whole reason this component\n * is separate from its siblings in `field.tsx`, and the reason it carries\n * `'use client'`: those two are hook-free and stay server-renderable, because\n * the storefront's search page is a Server Component that imports `TextInput`\n * and must not pull a client boundary into a prerendered route.\n */\n indeterminate?: boolean;\n}\n\n/**\n * A checkbox, with its caption when it has one.\n *\n * Two shapes, and the difference is not cosmetic. With `label` it renders the\n * caption inside a `<label>` and the whole pair is the click target — the form\n * control every other field is. Without one it renders a bare box that takes\n * its meaning from its position, which is what a selection column needs: a\n * caption per row would repeat the column header on every line, and the row's\n * own cells are already the subject.\n */\nexport function Checkbox({\n label,\n className,\n indeterminate = false,\n ...props\n}: CheckboxProps) {\n const ref = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n // The effect only runs once the input is mounted, so the ref is always\n // filled here; the guard is what keeps a future refactor that renders the\n // box conditionally from throwing rather than no-op'ing.\n /* v8 ignore next */\n if (ref.current) ref.current.indeterminate = indeterminate;\n }, [indeterminate]);\n\n const box = (\n <input\n ref={ref}\n type=\"checkbox\"\n className={cn('size-4 accent-primary', label ? undefined : className)}\n {...props}\n />\n );\n\n if (label === undefined) return box;\n\n return (\n <label\n className={cn(\n 'flex cursor-pointer items-center gap-2xs text-step-sm text-content',\n className,\n )}\n >\n {box}\n {label}\n </label>\n );\n}\n"],"names":["useEffect","useRef","cn","Checkbox","label","className","indeterminate","props","ref","current","box","input","type","undefined"],"mappings":"AAAA;;;;AAEA,SAAwCA,SAAS,EAAEC,MAAM,QAAQ,QAAQ;AAEzE,SAASC,EAAE,QAAQ,oBAAoB;AA4BvC;;;;;;;;;CASC,GACD,OAAO,SAASC,SAAS,EAKT;QALS,EACvBC,KAAK,EACLC,SAAS,EACTC,gBAAgB,KAAK,EAEP,OADXC;;;;;IAEH,MAAMC,MAAMP,OAAyB;IAErCD,UAAU;QACR,uEAAuE;QACvE,0EAA0E;QAC1E,yDAAyD;QACzD,kBAAkB,GAClB,IAAIQ,IAAIC,OAAO,EAAED,IAAIC,OAAO,CAACH,aAAa,GAAGA;IAC/C,GAAG;QAACA;KAAc;IAElB,MAAMI,oBACJ,KAACC;QACCH,KAAKA;QACLI,MAAK;QACLP,WAAWH,GAAG,yBAAyBE,QAAQS,YAAYR;OACvDE;IAIR,IAAIH,UAAUS,WAAW,OAAOH;IAEhC,qBACE,MAACN;QACCC,WAAWH,GACT,sEACAG;;YAGDK;YACAN;;;AAGP"}
1
+ {"version":3,"sources":["../../../../src/ui/atoms/field/checkbox.tsx"],"sourcesContent":["'use client';\n\nimport { type ComponentPropsWithoutRef, useEffect, useRef } from 'react';\n\nimport { cn } from '../../utils/cn.js';\n\nexport interface CheckboxProps extends Omit<\n ComponentPropsWithoutRef<'input'>,\n 'type'\n> {\n /**\n * The visible caption. Omit it for a box whose meaning comes from where it\n * sits — a row's selection box, where the column header is the caption and\n * the row itself is the subject — and pass `aria-label` instead, so the\n * control still announces itself to a screen reader. A box with neither is\n * unlabelled.\n */\n label?: string;\n /**\n * The third state: some of the rows below are selected, but not all.\n *\n * It is a DOM **property**, not an attribute, so React cannot set it from\n * JSX — `<input indeterminate>` is silently dropped — and it has to be\n * written through a ref after render. That is the whole reason this component\n * is separate from its siblings in `field.tsx`, and the reason it carries\n * `'use client'`: those two are hook-free and stay server-renderable, because\n * the storefront's search page is a Server Component that imports `TextInput`\n * and must not pull a client boundary into a prerendered route.\n */\n indeterminate?: boolean;\n}\n\n/**\n * The box is painted at 1rem and HIT at `--target-min` (WCAG 2.5.8, ADR 0025).\n * The input itself is the target-sized element — an input renders no\n * pseudo-element of its own until `appearance: none` hands its painting over,\n * which is what this does — and a negative margin gives the extra back, so the\n * layout still sees a 1rem box at any floor and compact density stays compact. The square,\n * its tick and the indeterminate bar are drawn by `::before` and `::after`\n * with `border-control` and the primary colour; the tick and the bar are\n * borders, so forced colours keep them.\n */\nconst BOX = [\n 'relative size-(--target-min) shrink-0 cursor-pointer appearance-none rounded-sm focus-ring',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n // The painted square: 1rem, centred in the target whatever its floor.\n 'before:absolute before:inset-[calc((var(--target-min)-(--spacing(4)))/2)] before:rounded-sm before:border before:border-border-control before:bg-surface-elevated',\n 'checked:before:border-primary checked:before:bg-primary indeterminate:before:border-primary indeterminate:before:bg-primary',\n // The tick: two borders of a rotated box.\n 'checked:after:absolute checked:after:inset-0 checked:after:m-auto checked:after:h-2.5 checked:after:w-1.5 checked:after:-translate-y-px checked:after:rotate-45 checked:after:border-r-2 checked:after:border-b-2 checked:after:border-primary-content',\n // The bar: one border.\n 'indeterminate:after:absolute indeterminate:after:top-1/2 indeterminate:after:left-1/2 indeterminate:after:w-2 indeterminate:after:-translate-1/2 indeterminate:after:border-t-2 indeterminate:after:border-primary-content',\n].join(' ');\n\n/**\n * A checkbox, with its caption when it has one.\n *\n * Two shapes, and the difference is not cosmetic. With `label` it renders the\n * caption inside a `<label>` and the whole pair is the click target — the form\n * control every other field is. Without one it renders a bare box that takes\n * its meaning from its position, which is what a selection column needs: a\n * caption per row would repeat the column header on every line, and the row's\n * own cells are already the subject.\n */\nexport function Checkbox({\n label,\n className,\n indeterminate = false,\n ...props\n}: CheckboxProps) {\n const ref = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n // The effect only runs once the input is mounted, so the ref is always\n // filled here; the guard is what keeps a future refactor that renders the\n // box conditionally from throwing rather than no-op'ing.\n /* v8 ignore next */\n if (ref.current) ref.current.indeterminate = indeterminate;\n }, [indeterminate]);\n\n const box = (\n <input\n ref={ref}\n type=\"checkbox\"\n className={cn(BOX, label ? undefined : className)}\n {...props}\n />\n );\n\n if (label === undefined) return box;\n\n return (\n <label\n className={cn(\n 'flex cursor-pointer items-center gap-2xs text-step-sm text-content',\n className,\n )}\n >\n {box}\n {label}\n </label>\n );\n}\n"],"names":["useEffect","useRef","cn","BOX","join","Checkbox","label","className","indeterminate","props","ref","current","box","input","type","undefined"],"mappings":"AAAA;;;;AAEA,SAAwCA,SAAS,EAAEC,MAAM,QAAQ,QAAQ;AAEzE,SAASC,EAAE,QAAQ,oBAAoB;AA4BvC;;;;;;;;;CASC,GACD,MAAMC,MAAM;IACV;IACA;IACA,sEAAsE;IACtE;IACA;IACA,0CAA0C;IAC1C;IACA,uBAAuB;IACvB;CACD,CAACC,IAAI,CAAC;AAEP;;;;;;;;;CASC,GACD,OAAO,SAASC,SAAS,EAKT;QALS,EACvBC,KAAK,EACLC,SAAS,EACTC,gBAAgB,KAAK,EAEP,OADXC;;;;;IAEH,MAAMC,MAAMT,OAAyB;IAErCD,UAAU;QACR,uEAAuE;QACvE,0EAA0E;QAC1E,yDAAyD;QACzD,kBAAkB,GAClB,IAAIU,IAAIC,OAAO,EAAED,IAAIC,OAAO,CAACH,aAAa,GAAGA;IAC/C,GAAG;QAACA;KAAc;IAElB,MAAMI,oBACJ,KAACC;QACCH,KAAKA;QACLI,MAAK;QACLP,WAAWL,GAAGC,KAAKG,QAAQS,YAAYR;OACnCE;IAIR,IAAIH,UAAUS,WAAW,OAAOH;IAEhC,qBACE,MAACN;QACCC,WAAWL,GACT,sEACAK;;YAGDK;YACAN;;;AAGP"}
@@ -3,7 +3,7 @@ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_wi
3
3
  import { jsx as _jsx } from "react/jsx-runtime";
4
4
  import { cn } from '../../utils/cn.js';
5
5
  /** Shared shell for the interactive form controls below. */ const CONTROL_CLASS = [
6
- 'rounded-lg border border-border bg-surface-elevated px-2xs py-3xs',
6
+ 'min-h-(--target-min) rounded-lg border border-border-control bg-surface-elevated px-2xs py-3xs',
7
7
  'text-step-sm text-content',
8
8
  'transition-colors duration-200 ease-smooth',
9
9
  'focus-ring',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/atoms/field/field.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef } from 'react';\n\nimport { cn } from '../../utils/cn.js';\n\n/** Shared shell for the interactive form controls below. */\nconst CONTROL_CLASS = [\n 'rounded-lg border border-border bg-surface-elevated px-2xs py-3xs',\n 'text-step-sm text-content',\n 'transition-colors duration-200 ease-smooth',\n 'focus-ring',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n].join(' ');\n\nexport function TextInput({\n className,\n ...props\n}: ComponentPropsWithoutRef<'input'>) {\n return <input className={cn(CONTROL_CLASS, className)} {...props} />;\n}\n\nexport function Select({\n className,\n ...props\n}: ComponentPropsWithoutRef<'select'>) {\n return <select className={cn(CONTROL_CLASS, className)} {...props} />;\n}\n"],"names":["cn","CONTROL_CLASS","join","TextInput","className","props","input","Select","select"],"mappings":";;;AAEA,SAASA,EAAE,QAAQ,oBAAoB;AAEvC,0DAA0D,GAC1D,MAAMC,gBAAgB;IACpB;IACA;IACA;IACA;IACA;CACD,CAACC,IAAI,CAAC;AAEP,OAAO,SAASC,UAAU,EAGU;QAHV,EACxBC,SAAS,EAEyB,OAD/BC;;;IAEH,qBAAO,KAACC;QAAMF,WAAWJ,GAAGC,eAAeG;OAAgBC;AAC7D;AAEA,OAAO,SAASE,OAAO,EAGc;QAHd,EACrBH,SAAS,EAE0B,OADhCC;;;IAEH,qBAAO,KAACG;QAAOJ,WAAWJ,GAAGC,eAAeG;OAAgBC;AAC9D"}
1
+ {"version":3,"sources":["../../../../src/ui/atoms/field/field.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef } from 'react';\n\nimport { cn } from '../../utils/cn.js';\n\n/** Shared shell for the interactive form controls below. */\nconst CONTROL_CLASS = [\n 'min-h-(--target-min) rounded-lg border border-border-control bg-surface-elevated px-2xs py-3xs',\n 'text-step-sm text-content',\n 'transition-colors duration-200 ease-smooth',\n 'focus-ring',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n].join(' ');\n\nexport function TextInput({\n className,\n ...props\n}: ComponentPropsWithoutRef<'input'>) {\n return <input className={cn(CONTROL_CLASS, className)} {...props} />;\n}\n\nexport function Select({\n className,\n ...props\n}: ComponentPropsWithoutRef<'select'>) {\n return <select className={cn(CONTROL_CLASS, className)} {...props} />;\n}\n"],"names":["cn","CONTROL_CLASS","join","TextInput","className","props","input","Select","select"],"mappings":";;;AAEA,SAASA,EAAE,QAAQ,oBAAoB;AAEvC,0DAA0D,GAC1D,MAAMC,gBAAgB;IACpB;IACA;IACA;IACA;IACA;CACD,CAACC,IAAI,CAAC;AAEP,OAAO,SAASC,UAAU,EAGU;QAHV,EACxBC,SAAS,EAEyB,OAD/BC;;;IAEH,qBAAO,KAACC;QAAMF,WAAWJ,GAAGC,eAAeG;OAAgBC;AAC7D;AAEA,OAAO,SAASE,OAAO,EAGc;QAHd,EACrBH,SAAS,EAE0B,OADhCC;;;IAEH,qBAAO,KAACG;QAAOJ,WAAWJ,GAAGC,eAAeG;OAAgBC;AAC9D"}
@@ -23,6 +23,11 @@ export interface FieldControlProps {
23
23
  value: string;
24
24
  onChange: (value: string) => void;
25
25
  id?: string;
26
+ /**
27
+ * The record being edited, as its draft. A money member whose currency is
28
+ * another member (`money.currencyMember`) reads it from here.
29
+ */
30
+ record?: Readonly<Record<string, unknown>>;
26
31
  }
27
32
  /**
28
33
  * The edit-mode inverse of {@link CellValue}: it turns one metadata descriptor
@@ -46,5 +51,5 @@ export interface FieldControlProps {
46
51
  * so the draft it round-trips through *is* editable text. Splitting on the
47
52
  * comma is `reconstructEntity`'s half of that same contract.
48
53
  */
49
- export declare function FieldControl({ descriptor, value, onChange, id, ...aria }: FieldControlProps): import("react").JSX.Element;
54
+ export declare function FieldControl({ descriptor, value, onChange, id, record, ...aria }: FieldControlProps): import("react").JSX.Element;
50
55
  //# sourceMappingURL=field-control.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"field-control.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/field-control/field-control.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAoB,MAAM,eAAe,CAAC;AAK7E,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,qBAAqB,CAAC;IAClC;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAQD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,EAC3B,UAAU,EACV,KAAK,EACL,QAAQ,EACR,EAAE,EACF,GAAG,IAAI,EACR,EAAE,iBAAiB,+BAkDnB"}
1
+ {"version":3,"file":"field-control.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/field-control/field-control.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAoB,MAAM,eAAe,CAAC;AAO7E,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,qBAAqB,CAAC;IAClC;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC5C;AAQD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,EAC3B,UAAU,EACV,KAAK,EACL,QAAQ,EACR,EAAE,EACF,MAAM,EACN,GAAG,IAAI,EACR,EAAE,iBAAiB,+BAyEnB"}
@@ -2,8 +2,10 @@
2
2
  import { _ as _extends } from "@swc/helpers/_/_extends";
3
3
  import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
4
4
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
- import { useEnumLabel } from '../../../i18n/index.js';
5
+ import { useEnumLabel, useLocale } from '../../../i18n/index.js';
6
6
  import { Checkbox, Select, TextInput } from '../field/index.js';
7
+ import { MoneyInput } from '../money/money-input.js';
8
+ import { currencyOf } from '../money/money-math.js';
7
9
  /** Native input type for the text-like controls; text is the default. */ const INPUT_TYPE = {
8
10
  number: 'number',
9
11
  date: 'date'
@@ -30,14 +32,16 @@ import { Checkbox, Select, TextInput } from '../field/index.js';
30
32
  * so the draft it round-trips through *is* editable text. Splitting on the
31
33
  * comma is `reconstructEntity`'s half of that same contract.
32
34
  */ export function FieldControl(_0) {
33
- let { descriptor, value, onChange, id } = _0, aria = _object_without_properties_loose(_0, [
35
+ let { descriptor, value, onChange, id, record } = _0, aria = _object_without_properties_loose(_0, [
34
36
  "descriptor",
35
37
  "value",
36
38
  "onChange",
37
- "id"
39
+ "id",
40
+ "record"
38
41
  ]);
39
42
  var _INPUT_TYPE_descriptor_type;
40
43
  const enumLabel = useEnumLabel();
44
+ const locale = useLocale();
41
45
  const disabled = descriptor.readonly || descriptor.type === 'link' || descriptor.type === 'linkCollection' || descriptor.type === 'composition';
42
46
  if (descriptor.type === 'boolean') {
43
47
  return /*#__PURE__*/ _jsx(Checkbox, _extends({
@@ -69,6 +73,22 @@ import { Checkbox, Select, TextInput } from '../field/index.js';
69
73
  ]
70
74
  }));
71
75
  }
76
+ // An amount is edited in major units, in the reader's decimal separator,
77
+ // and written back in the unit it is stored in. Without a currency to know
78
+ // the minor units by, it stays a plain number field.
79
+ const currency = descriptor.money === undefined ? undefined : currencyOf(descriptor.money, record);
80
+ if (descriptor.money !== undefined && currency !== undefined) {
81
+ return /*#__PURE__*/ _jsx(MoneyInput, _extends({
82
+ id: id
83
+ }, aria, {
84
+ value: value,
85
+ currency: currency,
86
+ unit: descriptor.money.unit,
87
+ locale: locale,
88
+ disabled: disabled,
89
+ onChange: onChange
90
+ }));
91
+ }
72
92
  return /*#__PURE__*/ _jsx(TextInput, _extends({
73
93
  id: id
74
94
  }, aria, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/ui/atoms/field-control/field-control.tsx"],"sourcesContent":["'use client';\n\nimport type { EntityFieldDescriptor, MetaAccessorType } from '@entifix/core';\n\nimport { useEnumLabel } from '../../../i18n/index.js';\nimport { Checkbox, Select, TextInput } from '../field/index.js';\n\nexport interface FieldControlProps {\n descriptor: EntityFieldDescriptor;\n /**\n * ARIA wiring the caller owns, because only the caller knows the shape it is\n * rendered into.\n *\n * A form labels its control with a `<label for>` and needs none of this; a\n * detail grid's cell has no label of its own — the column header is the\n * label, and the cell's message is the description — so it points at both by\n * id. Passing them through here rather than duplicating the type→input map\n * in the grid is what keeps one control deciding what a `date` member looks\n * like.\n */\n 'aria-labelledby'?: string;\n 'aria-describedby'?: string;\n 'aria-invalid'?: boolean;\n /**\n * The draft value as a string — the shape a native input round-trips. A form\n * holds its whole draft as `Record<string, string>` and reconstructs typed\n * values only at submit, so the control never sees a domain value.\n */\n value: string;\n onChange: (value: string) => void;\n id?: string;\n}\n\n/** Native input type for the text-like controls; text is the default. */\nconst INPUT_TYPE: Partial<Record<MetaAccessorType, string>> = {\n number: 'number',\n date: 'date',\n};\n\n/**\n * The edit-mode inverse of {@link CellValue}: it turns one metadata descriptor\n * into the input that edits it. Centralizing the type→control choice is what\n * lets a form stay generic, the same way `CellValue` keeps a table generic.\n *\n * A relation (`link`/`linkCollection`) is rendered read-only here, deliberately:\n * editing one needs a picker that can search and page the target, which is a\n * different control with different dependencies. So this atom stays a pure\n * type→input map and a relation never falls back to a broken text box.\n *\n * For a to-one `link` that is only half the story — `EntityForm` wires the\n * picker in one level up from its `linkSources`, so the member does get an\n * editor, just not from here. A `linkCollection` has no editor anywhere yet\n * (#26), and read-only is all it gets. A `composition` is the same: its rows\n * need a detail grid, and a text box holding `[object Object]` would be\n * strictly worse than an honest disabled field.\n *\n * A `scalarCollection` is the exception among the collections and falls through\n * to the plain text input on purpose: a `string[]` has a lossless comma form,\n * so the draft it round-trips through *is* editable text. Splitting on the\n * comma is `reconstructEntity`'s half of that same contract.\n */\nexport function FieldControl({\n descriptor,\n value,\n onChange,\n id,\n ...aria\n}: FieldControlProps) {\n const enumLabel = useEnumLabel();\n const disabled =\n descriptor.readonly ||\n descriptor.type === 'link' ||\n descriptor.type === 'linkCollection' ||\n descriptor.type === 'composition';\n\n if (descriptor.type === 'boolean') {\n return (\n <Checkbox\n id={id}\n {...aria}\n label={descriptor.label}\n checked={value === 'true'}\n disabled={disabled}\n onChange={event => onChange(String(event.currentTarget.checked))}\n />\n );\n }\n\n if (descriptor.type === 'enum') {\n return (\n <Select\n id={id}\n {...aria}\n value={value}\n disabled={disabled}\n onChange={event => onChange(event.currentTarget.value)}\n >\n <option value=\"\">—</option>\n {(descriptor.enumValues ?? []).map(option => (\n <option key={option} value={option}>\n {enumLabel(descriptor, option)}\n </option>\n ))}\n </Select>\n );\n }\n\n return (\n <TextInput\n id={id}\n {...aria}\n type={INPUT_TYPE[descriptor.type] ?? 'text'}\n value={value}\n disabled={disabled}\n onChange={event => onChange(event.currentTarget.value)}\n />\n );\n}\n"],"names":["useEnumLabel","Checkbox","Select","TextInput","INPUT_TYPE","number","date","FieldControl","descriptor","value","onChange","id","aria","enumLabel","disabled","readonly","type","label","checked","event","String","currentTarget","option","enumValues","map"],"mappings":"AAAA;;;;AAIA,SAASA,YAAY,QAAQ,yBAAyB;AACtD,SAASC,QAAQ,EAAEC,MAAM,EAAEC,SAAS,QAAQ,oBAAoB;AA4BhE,uEAAuE,GACvE,MAAMC,aAAwD;IAC5DC,QAAQ;IACRC,MAAM;AACR;AAEA;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,SAASC,aAAa,EAMT;QANS,EAC3BC,UAAU,EACVC,KAAK,EACLC,QAAQ,EACRC,EAAE,EAEgB,OADfC;;;;;;QA6COR;IA3CV,MAAMS,YAAYb;IAClB,MAAMc,WACJN,WAAWO,QAAQ,IACnBP,WAAWQ,IAAI,KAAK,UACpBR,WAAWQ,IAAI,KAAK,oBACpBR,WAAWQ,IAAI,KAAK;IAEtB,IAAIR,WAAWQ,IAAI,KAAK,WAAW;QACjC,qBACE,KAACf;YACCU,IAAIA;WACAC;YACJK,OAAOT,WAAWS,KAAK;YACvBC,SAAST,UAAU;YACnBK,UAAUA;YACVJ,UAAUS,CAAAA,QAAST,SAASU,OAAOD,MAAME,aAAa,CAACH,OAAO;;IAGpE;IAEA,IAAIV,WAAWQ,IAAI,KAAK,QAAQ;YAUxBR;QATN,qBACE,MAACN;YACCS,IAAIA;WACAC;YACJH,OAAOA;YACPK,UAAUA;YACVJ,UAAUS,CAAAA,QAAST,SAASS,MAAME,aAAa,CAACZ,KAAK;;8BAErD,KAACa;oBAAOb,OAAM;8BAAG;;gBAChB,EAACD,yBAAAA,WAAWe,UAAU,YAArBf,yBAAyB,EAAE,EAAEgB,GAAG,CAACF,CAAAA,uBACjC,KAACA;wBAAoBb,OAAOa;kCACzBT,UAAUL,YAAYc;uBADZA;;;IAMrB;IAEA,qBACE,KAACnB;QACCQ,IAAIA;OACAC;QACJI,IAAI,GAAEZ,8BAAAA,UAAU,CAACI,WAAWQ,IAAI,CAAC,YAA3BZ,8BAA+B;QACrCK,OAAOA;QACPK,UAAUA;QACVJ,UAAUS,CAAAA,QAAST,SAASS,MAAME,aAAa,CAACZ,KAAK;;AAG3D"}
1
+ {"version":3,"sources":["../../../../src/ui/atoms/field-control/field-control.tsx"],"sourcesContent":["'use client';\n\nimport type { EntityFieldDescriptor, MetaAccessorType } from '@entifix/core';\n\nimport { useEnumLabel, useLocale } from '../../../i18n/index.js';\nimport { Checkbox, Select, TextInput } from '../field/index.js';\nimport { MoneyInput } from '../money/money-input.js';\nimport { currencyOf } from '../money/money-math.js';\n\nexport interface FieldControlProps {\n descriptor: EntityFieldDescriptor;\n /**\n * ARIA wiring the caller owns, because only the caller knows the shape it is\n * rendered into.\n *\n * A form labels its control with a `<label for>` and needs none of this; a\n * detail grid's cell has no label of its own — the column header is the\n * label, and the cell's message is the description — so it points at both by\n * id. Passing them through here rather than duplicating the type→input map\n * in the grid is what keeps one control deciding what a `date` member looks\n * like.\n */\n 'aria-labelledby'?: string;\n 'aria-describedby'?: string;\n 'aria-invalid'?: boolean;\n /**\n * The draft value as a string — the shape a native input round-trips. A form\n * holds its whole draft as `Record<string, string>` and reconstructs typed\n * values only at submit, so the control never sees a domain value.\n */\n value: string;\n onChange: (value: string) => void;\n id?: string;\n /**\n * The record being edited, as its draft. A money member whose currency is\n * another member (`money.currencyMember`) reads it from here.\n */\n record?: Readonly<Record<string, unknown>>;\n}\n\n/** Native input type for the text-like controls; text is the default. */\nconst INPUT_TYPE: Partial<Record<MetaAccessorType, string>> = {\n number: 'number',\n date: 'date',\n};\n\n/**\n * The edit-mode inverse of {@link CellValue}: it turns one metadata descriptor\n * into the input that edits it. Centralizing the type→control choice is what\n * lets a form stay generic, the same way `CellValue` keeps a table generic.\n *\n * A relation (`link`/`linkCollection`) is rendered read-only here, deliberately:\n * editing one needs a picker that can search and page the target, which is a\n * different control with different dependencies. So this atom stays a pure\n * type→input map and a relation never falls back to a broken text box.\n *\n * For a to-one `link` that is only half the story — `EntityForm` wires the\n * picker in one level up from its `linkSources`, so the member does get an\n * editor, just not from here. A `linkCollection` has no editor anywhere yet\n * (#26), and read-only is all it gets. A `composition` is the same: its rows\n * need a detail grid, and a text box holding `[object Object]` would be\n * strictly worse than an honest disabled field.\n *\n * A `scalarCollection` is the exception among the collections and falls through\n * to the plain text input on purpose: a `string[]` has a lossless comma form,\n * so the draft it round-trips through *is* editable text. Splitting on the\n * comma is `reconstructEntity`'s half of that same contract.\n */\nexport function FieldControl({\n descriptor,\n value,\n onChange,\n id,\n record,\n ...aria\n}: FieldControlProps) {\n const enumLabel = useEnumLabel();\n const locale = useLocale();\n const disabled =\n descriptor.readonly ||\n descriptor.type === 'link' ||\n descriptor.type === 'linkCollection' ||\n descriptor.type === 'composition';\n\n if (descriptor.type === 'boolean') {\n return (\n <Checkbox\n id={id}\n {...aria}\n label={descriptor.label}\n checked={value === 'true'}\n disabled={disabled}\n onChange={event => onChange(String(event.currentTarget.checked))}\n />\n );\n }\n\n if (descriptor.type === 'enum') {\n return (\n <Select\n id={id}\n {...aria}\n value={value}\n disabled={disabled}\n onChange={event => onChange(event.currentTarget.value)}\n >\n <option value=\"\">—</option>\n {(descriptor.enumValues ?? []).map(option => (\n <option key={option} value={option}>\n {enumLabel(descriptor, option)}\n </option>\n ))}\n </Select>\n );\n }\n\n // An amount is edited in major units, in the reader's decimal separator,\n // and written back in the unit it is stored in. Without a currency to know\n // the minor units by, it stays a plain number field.\n const currency =\n descriptor.money === undefined\n ? undefined\n : currencyOf(descriptor.money, record);\n if (descriptor.money !== undefined && currency !== undefined) {\n return (\n <MoneyInput\n id={id}\n {...aria}\n value={value}\n currency={currency}\n unit={descriptor.money.unit}\n locale={locale}\n disabled={disabled}\n onChange={onChange}\n />\n );\n }\n\n return (\n <TextInput\n id={id}\n {...aria}\n type={INPUT_TYPE[descriptor.type] ?? 'text'}\n value={value}\n disabled={disabled}\n onChange={event => onChange(event.currentTarget.value)}\n />\n );\n}\n"],"names":["useEnumLabel","useLocale","Checkbox","Select","TextInput","MoneyInput","currencyOf","INPUT_TYPE","number","date","FieldControl","descriptor","value","onChange","id","record","aria","enumLabel","locale","disabled","readonly","type","label","checked","event","String","currentTarget","option","enumValues","map","currency","money","undefined","unit"],"mappings":"AAAA;;;;AAIA,SAASA,YAAY,EAAEC,SAAS,QAAQ,yBAAyB;AACjE,SAASC,QAAQ,EAAEC,MAAM,EAAEC,SAAS,QAAQ,oBAAoB;AAChE,SAASC,UAAU,QAAQ,0BAA0B;AACrD,SAASC,UAAU,QAAQ,yBAAyB;AAiCpD,uEAAuE,GACvE,MAAMC,aAAwD;IAC5DC,QAAQ;IACRC,MAAM;AACR;AAEA;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,SAASC,aAAa,EAOT;QAPS,EAC3BC,UAAU,EACVC,KAAK,EACLC,QAAQ,EACRC,EAAE,EACFC,MAAM,EAEY,OADfC;;;;;;;QAoEOT;IAlEV,MAAMU,YAAYjB;IAClB,MAAMkB,SAASjB;IACf,MAAMkB,WACJR,WAAWS,QAAQ,IACnBT,WAAWU,IAAI,KAAK,UACpBV,WAAWU,IAAI,KAAK,oBACpBV,WAAWU,IAAI,KAAK;IAEtB,IAAIV,WAAWU,IAAI,KAAK,WAAW;QACjC,qBACE,KAACnB;YACCY,IAAIA;WACAE;YACJM,OAAOX,WAAWW,KAAK;YACvBC,SAASX,UAAU;YACnBO,UAAUA;YACVN,UAAUW,CAAAA,QAASX,SAASY,OAAOD,MAAME,aAAa,CAACH,OAAO;;IAGpE;IAEA,IAAIZ,WAAWU,IAAI,KAAK,QAAQ;YAUxBV;QATN,qBACE,MAACR;YACCW,IAAIA;WACAE;YACJJ,OAAOA;YACPO,UAAUA;YACVN,UAAUW,CAAAA,QAASX,SAASW,MAAME,aAAa,CAACd,KAAK;;8BAErD,KAACe;oBAAOf,OAAM;8BAAG;;gBAChB,EAACD,yBAAAA,WAAWiB,UAAU,YAArBjB,yBAAyB,EAAE,EAAEkB,GAAG,CAACF,CAAAA,uBACjC,KAACA;wBAAoBf,OAAOe;kCACzBV,UAAUN,YAAYgB;uBADZA;;;IAMrB;IAEA,yEAAyE;IACzE,2EAA2E;IAC3E,qDAAqD;IACrD,MAAMG,WACJnB,WAAWoB,KAAK,KAAKC,YACjBA,YACA1B,WAAWK,WAAWoB,KAAK,EAAEhB;IACnC,IAAIJ,WAAWoB,KAAK,KAAKC,aAAaF,aAAaE,WAAW;QAC5D,qBACE,KAAC3B;YACCS,IAAIA;WACAE;YACJJ,OAAOA;YACPkB,UAAUA;YACVG,MAAMtB,WAAWoB,KAAK,CAACE,IAAI;YAC3Bf,QAAQA;YACRC,UAAUA;YACVN,UAAUA;;IAGhB;IAEA,qBACE,KAACT;QACCU,IAAIA;OACAE;QACJK,IAAI,GAAEd,8BAAAA,UAAU,CAACI,WAAWU,IAAI,CAAC,YAA3Bd,8BAA+B;QACrCK,OAAOA;QACPO,UAAUA;QACVN,UAAUW,CAAAA,QAASX,SAASW,MAAME,aAAa,CAACd,KAAK;;AAG3D"}
@@ -0,0 +1,4 @@
1
+ export * from './money.js';
2
+ export * from './money-input.js';
3
+ export { currencyOf, formatMoney, fractionDigits, fromMajorUnits, type MoneyUnit, toMajorUnits, } from './money-math.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/money/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,OAAO,EACL,UAAU,EACV,WAAW,EACX,cAAc,EACd,cAAc,EACd,KAAK,SAAS,EACd,YAAY,GACb,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,5 @@
1
+ export * from './money.js';
2
+ export * from './money-input.js';
3
+ export { currencyOf, formatMoney, fractionDigits, fromMajorUnits, toMajorUnits } from './money-math.js';
4
+
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/ui/atoms/money/index.ts"],"sourcesContent":["export * from './money.js';\nexport * from './money-input.js';\nexport {\n currencyOf,\n formatMoney,\n fractionDigits,\n fromMajorUnits,\n type MoneyUnit,\n toMajorUnits,\n} from './money-math.js';\n"],"names":["currencyOf","formatMoney","fractionDigits","fromMajorUnits","toMajorUnits"],"mappings":"AAAA,cAAc,aAAa;AAC3B,cAAc,mBAAmB;AACjC,SACEA,UAAU,EACVC,WAAW,EACXC,cAAc,EACdC,cAAc,EAEdC,YAAY,QACP,kBAAkB"}
@@ -0,0 +1,19 @@
1
+ import { type NumberInputProps } from '../number-input/number-input.js';
2
+ import { type MoneyUnit } from './money-math.js';
3
+ export interface MoneyInputProps extends Omit<NumberInputProps, 'value' | 'onChange' | 'inputMode'> {
4
+ /** The stored amount, as a draft string (`'6499'`), or `''` for none. */
5
+ value: string;
6
+ /** Called with the stored amount as a draft string. */
7
+ onChange: (value: string) => void;
8
+ currency: string;
9
+ unit: MoneyUnit;
10
+ locale: string;
11
+ }
12
+ /**
13
+ * Edits an amount in major units — what a person means by a price — and writes
14
+ * back the stored unit. What was typed is kept as typed (a trailing `64,` is
15
+ * not reformatted under the cursor), and the field follows the stored value
16
+ * only when it changes from outside.
17
+ */
18
+ export declare function MoneyInput({ value, onChange, currency, unit, locale, ...props }: MoneyInputProps): import("react").JSX.Element;
19
+ //# sourceMappingURL=money-input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"money-input.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/money/money-input.tsx"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAGL,KAAK,SAAS,EAEf,MAAM,iBAAiB,CAAC;AAEzB,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAC3C,gBAAgB,EAChB,OAAO,GAAG,UAAU,GAAG,WAAW,CACnC;IACC,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAkBD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,IAAI,EACJ,MAAM,EACN,GAAG,KAAK,EACT,EAAE,eAAe,+BA+CjB"}
@@ -0,0 +1,75 @@
1
+ 'use client';
2
+ import { _ as _extends } from "@swc/helpers/_/_extends";
3
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
4
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
+ import { useEffect, useState } from 'react';
6
+ import { NumberInput } from '../number-input/number-input.js';
7
+ import { fractionDigits, fromMajorUnits, toMajorUnits } from './money-math.js';
8
+ const parseMajor = (text)=>{
9
+ // Accept either decimal separator: the last `,` or `.` is the decimal one.
10
+ const normalised = text.trim().replace(/[\s']/g, '');
11
+ if (normalised === '') return undefined;
12
+ const last = Math.max(normalised.lastIndexOf(','), normalised.lastIndexOf('.'));
13
+ const plain = last === -1 ? normalised : `${normalised.slice(0, last).replace(/[.,]/g, '')}.${normalised.slice(last + 1)}`;
14
+ const major = Number(plain);
15
+ return Number.isFinite(major) ? major : undefined;
16
+ };
17
+ /**
18
+ * Edits an amount in major units — what a person means by a price — and writes
19
+ * back the stored unit. What was typed is kept as typed (a trailing `64,` is
20
+ * not reformatted under the cursor), and the field follows the stored value
21
+ * only when it changes from outside.
22
+ */ export function MoneyInput(_0) {
23
+ let { value, onChange, currency, unit, locale } = _0, props = _object_without_properties_loose(_0, [
24
+ "value",
25
+ "onChange",
26
+ "currency",
27
+ "unit",
28
+ "locale"
29
+ ]);
30
+ const digits = fractionDigits(currency);
31
+ const display = (stored)=>stored === '' || !Number.isFinite(Number(stored)) ? stored : new Intl.NumberFormat(locale, {
32
+ minimumFractionDigits: digits,
33
+ maximumFractionDigits: digits,
34
+ useGrouping: false
35
+ }).format(toMajorUnits(Number(stored), currency, unit));
36
+ const [text, setText] = useState(()=>display(value));
37
+ const [emitted, setEmitted] = useState(value);
38
+ useEffect(()=>{
39
+ if (value === emitted) return;
40
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- an outside write (a reset, a load) replaces what was typed
41
+ setText(display(value));
42
+ setEmitted(value);
43
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- `display` is derived from props already listed
44
+ }, [
45
+ value,
46
+ emitted,
47
+ currency,
48
+ unit,
49
+ locale
50
+ ]);
51
+ return /*#__PURE__*/ _jsxs("span", {
52
+ className: "inline-flex items-center gap-2xs",
53
+ children: [
54
+ /*#__PURE__*/ _jsx(NumberInput, _extends({}, props, {
55
+ inputMode: digits === 0 ? 'numeric' : 'decimal',
56
+ value: text,
57
+ onChange: (event)=>{
58
+ const next = event.currentTarget.value;
59
+ setText(next);
60
+ const major = parseMajor(next);
61
+ const stored = major === undefined ? next.trim() === '' ? '' : next : String(fromMajorUnits(major, currency, unit));
62
+ setEmitted(stored);
63
+ onChange(stored);
64
+ }
65
+ })),
66
+ /*#__PURE__*/ _jsx("span", {
67
+ className: "text-step-sm text-content-muted",
68
+ "aria-hidden": "true",
69
+ children: currency
70
+ })
71
+ ]
72
+ });
73
+ }
74
+
75
+ //# sourceMappingURL=money-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/ui/atoms/money/money-input.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useState } from 'react';\n\nimport {\n NumberInput,\n type NumberInputProps,\n} from '../number-input/number-input.js';\nimport {\n fractionDigits,\n fromMajorUnits,\n type MoneyUnit,\n toMajorUnits,\n} from './money-math.js';\n\nexport interface MoneyInputProps extends Omit<\n NumberInputProps,\n 'value' | 'onChange' | 'inputMode'\n> {\n /** The stored amount, as a draft string (`'6499'`), or `''` for none. */\n value: string;\n /** Called with the stored amount as a draft string. */\n onChange: (value: string) => void;\n currency: string;\n unit: MoneyUnit;\n locale: string;\n}\n\nconst parseMajor = (text: string): number | undefined => {\n // Accept either decimal separator: the last `,` or `.` is the decimal one.\n const normalised = text.trim().replace(/[\\s']/g, '');\n if (normalised === '') return undefined;\n const last = Math.max(\n normalised.lastIndexOf(','),\n normalised.lastIndexOf('.'),\n );\n const plain =\n last === -1\n ? normalised\n : `${normalised.slice(0, last).replace(/[.,]/g, '')}.${normalised.slice(last + 1)}`;\n const major = Number(plain);\n return Number.isFinite(major) ? major : undefined;\n};\n\n/**\n * Edits an amount in major units — what a person means by a price — and writes\n * back the stored unit. What was typed is kept as typed (a trailing `64,` is\n * not reformatted under the cursor), and the field follows the stored value\n * only when it changes from outside.\n */\nexport function MoneyInput({\n value,\n onChange,\n currency,\n unit,\n locale,\n ...props\n}: MoneyInputProps) {\n const digits = fractionDigits(currency);\n const display = (stored: string) =>\n stored === '' || !Number.isFinite(Number(stored))\n ? stored\n : new Intl.NumberFormat(locale, {\n minimumFractionDigits: digits,\n maximumFractionDigits: digits,\n useGrouping: false,\n }).format(toMajorUnits(Number(stored), currency, unit));\n\n const [text, setText] = useState(() => display(value));\n const [emitted, setEmitted] = useState(value);\n\n useEffect(() => {\n if (value === emitted) return;\n // eslint-disable-next-line react-hooks/set-state-in-effect -- an outside write (a reset, a load) replaces what was typed\n setText(display(value));\n setEmitted(value);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- `display` is derived from props already listed\n }, [value, emitted, currency, unit, locale]);\n\n return (\n <span className=\"inline-flex items-center gap-2xs\">\n <NumberInput\n {...props}\n inputMode={digits === 0 ? 'numeric' : 'decimal'}\n value={text}\n onChange={event => {\n const next = event.currentTarget.value;\n setText(next);\n const major = parseMajor(next);\n const stored =\n major === undefined\n ? next.trim() === ''\n ? ''\n : next\n : String(fromMajorUnits(major, currency, unit));\n setEmitted(stored);\n onChange(stored);\n }}\n />\n <span className=\"text-step-sm text-content-muted\" aria-hidden=\"true\">\n {currency}\n </span>\n </span>\n );\n}\n"],"names":["useEffect","useState","NumberInput","fractionDigits","fromMajorUnits","toMajorUnits","parseMajor","text","normalised","trim","replace","undefined","last","Math","max","lastIndexOf","plain","slice","major","Number","isFinite","MoneyInput","value","onChange","currency","unit","locale","props","digits","display","stored","Intl","NumberFormat","minimumFractionDigits","maximumFractionDigits","useGrouping","format","setText","emitted","setEmitted","span","className","inputMode","event","next","currentTarget","String","aria-hidden"],"mappings":"AAAA;;;;AAEA,SAASA,SAAS,EAAEC,QAAQ,QAAQ,QAAQ;AAE5C,SACEC,WAAW,QAEN,kCAAkC;AACzC,SACEC,cAAc,EACdC,cAAc,EAEdC,YAAY,QACP,kBAAkB;AAezB,MAAMC,aAAa,CAACC;IAClB,2EAA2E;IAC3E,MAAMC,aAAaD,KAAKE,IAAI,GAAGC,OAAO,CAAC,UAAU;IACjD,IAAIF,eAAe,IAAI,OAAOG;IAC9B,MAAMC,OAAOC,KAAKC,GAAG,CACnBN,WAAWO,WAAW,CAAC,MACvBP,WAAWO,WAAW,CAAC;IAEzB,MAAMC,QACJJ,SAAS,CAAC,IACNJ,aACA,GAAGA,WAAWS,KAAK,CAAC,GAAGL,MAAMF,OAAO,CAAC,SAAS,IAAI,CAAC,EAAEF,WAAWS,KAAK,CAACL,OAAO,IAAI;IACvF,MAAMM,QAAQC,OAAOH;IACrB,OAAOG,OAAOC,QAAQ,CAACF,SAASA,QAAQP;AAC1C;AAEA;;;;;CAKC,GACD,OAAO,SAASU,WAAW,EAOT;QAPS,EACzBC,KAAK,EACLC,QAAQ,EACRC,QAAQ,EACRC,IAAI,EACJC,MAAM,EAEU,OADbC;;;;;;;IAEH,MAAMC,SAASzB,eAAeqB;IAC9B,MAAMK,UAAU,CAACC,SACfA,WAAW,MAAM,CAACX,OAAOC,QAAQ,CAACD,OAAOW,WACrCA,SACA,IAAIC,KAAKC,YAAY,CAACN,QAAQ;YAC5BO,uBAAuBL;YACvBM,uBAAuBN;YACvBO,aAAa;QACf,GAAGC,MAAM,CAAC/B,aAAac,OAAOW,SAASN,UAAUC;IAEvD,MAAM,CAAClB,MAAM8B,QAAQ,GAAGpC,SAAS,IAAM4B,QAAQP;IAC/C,MAAM,CAACgB,SAASC,WAAW,GAAGtC,SAASqB;IAEvCtB,UAAU;QACR,IAAIsB,UAAUgB,SAAS;QACvB,yHAAyH;QACzHD,QAAQR,QAAQP;QAChBiB,WAAWjB;IACX,yGAAyG;IAC3G,GAAG;QAACA;QAAOgB;QAASd;QAAUC;QAAMC;KAAO;IAE3C,qBACE,MAACc;QAAKC,WAAU;;0BACd,KAACvC,0BACKyB;gBACJe,WAAWd,WAAW,IAAI,YAAY;gBACtCN,OAAOf;gBACPgB,UAAUoB,CAAAA;oBACR,MAAMC,OAAOD,MAAME,aAAa,CAACvB,KAAK;oBACtCe,QAAQO;oBACR,MAAM1B,QAAQZ,WAAWsC;oBACzB,MAAMd,SACJZ,UAAUP,YACNiC,KAAKnC,IAAI,OAAO,KACd,KACAmC,OACFE,OAAO1C,eAAec,OAAOM,UAAUC;oBAC7Cc,WAAWT;oBACXP,SAASO;gBACX;;0BAEF,KAACU;gBAAKC,WAAU;gBAAkCM,eAAY;0BAC3DvB;;;;AAIT"}
@@ -0,0 +1,21 @@
1
+ import type { MoneyFormat } from '@entifix/core';
2
+ export type MoneyUnit = MoneyFormat['unit'];
3
+ /**
4
+ * How many minor units a currency has, as `Intl` resolves it: 2 for GTQ and
5
+ * USD, 0 for JPY, 3 for KWD. Read from the platform rather than a table, so a
6
+ * currency nobody here thought of is still right.
7
+ */
8
+ export declare function fractionDigits(currency: string): number;
9
+ /** The amount in major units (64.99), whichever unit it is stored in. */
10
+ export declare function toMajorUnits(amount: number, currency: string, unit: MoneyUnit): number;
11
+ /** A major-unit amount back in the unit it is stored in, rounded to it. */
12
+ export declare function fromMajorUnits(major: number, currency: string, unit: MoneyUnit): number;
13
+ /**
14
+ * The amount as the reader's locale writes money: `Q64.99` in `en`, `64,99 GTQ`
15
+ * or similar in `es`. Never the bare stored number — a till once printed
16
+ * 6499 GTQ for 64,99.
17
+ */
18
+ export declare function formatMoney(amount: number, currency: string, unit: MoneyUnit, locale: string): string;
19
+ /** The currency a money member reads in: fixed, or the record's own member. */
20
+ export declare function currencyOf(money: MoneyFormat, record: Readonly<Record<string, unknown>> | undefined): string | undefined;
21
+ //# sourceMappingURL=money-math.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"money-math.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/money/money-math.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,MAAM,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;AAE5C;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CASvD;AAED,yEAAyE;AACzE,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,SAAS,GACd,MAAM,CAER;AAED,2EAA2E;AAC3E,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,SAAS,GACd,MAAM,CAKR;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,MAAM,GACb,MAAM,CAIR;AAED,+EAA+E;AAC/E,wBAAgB,UAAU,CACxB,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,GACpD,MAAM,GAAG,SAAS,CAIpB"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * How many minor units a currency has, as `Intl` resolves it: 2 for GTQ and
3
+ * USD, 0 for JPY, 3 for KWD. Read from the platform rather than a table, so a
4
+ * currency nobody here thought of is still right.
5
+ */ export function fractionDigits(currency) {
6
+ var _resolvedOptions_maximumFractionDigits;
7
+ // Always set for `style: 'currency'`; the fallback only satisfies the type.
8
+ /* v8 ignore next */ return (_resolvedOptions_maximumFractionDigits = new Intl.NumberFormat('en', {
9
+ style: 'currency',
10
+ currency
11
+ }).resolvedOptions().maximumFractionDigits) != null ? _resolvedOptions_maximumFractionDigits : 2;
12
+ }
13
+ /** The amount in major units (64.99), whichever unit it is stored in. */ export function toMajorUnits(amount, currency, unit) {
14
+ return unit === 'major' ? amount : amount / 10 ** fractionDigits(currency);
15
+ }
16
+ /** A major-unit amount back in the unit it is stored in, rounded to it. */ export function fromMajorUnits(major, currency, unit) {
17
+ const digits = fractionDigits(currency);
18
+ return unit === 'major' ? Number(major.toFixed(digits)) : Math.round(major * 10 ** digits);
19
+ }
20
+ /**
21
+ * The amount as the reader's locale writes money: `Q64.99` in `en`, `64,99 GTQ`
22
+ * or similar in `es`. Never the bare stored number — a till once printed
23
+ * 6499 GTQ for 64,99.
24
+ */ export function formatMoney(amount, currency, unit, locale) {
25
+ return new Intl.NumberFormat(locale, {
26
+ style: 'currency',
27
+ currency
28
+ }).format(toMajorUnits(amount, currency, unit));
29
+ }
30
+ /** The currency a money member reads in: fixed, or the record's own member. */ export function currencyOf(money, record) {
31
+ if (money.currency !== undefined) return money.currency;
32
+ const value = record == null ? void 0 : record[money.currencyMember];
33
+ return typeof value === 'string' && value !== '' ? value : undefined;
34
+ }
35
+
36
+ //# sourceMappingURL=money-math.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/ui/atoms/money/money-math.ts"],"sourcesContent":["import type { MoneyFormat } from '@entifix/core';\n\nexport type MoneyUnit = MoneyFormat['unit'];\n\n/**\n * How many minor units a currency has, as `Intl` resolves it: 2 for GTQ and\n * USD, 0 for JPY, 3 for KWD. Read from the platform rather than a table, so a\n * currency nobody here thought of is still right.\n */\nexport function fractionDigits(currency: string): number {\n // Always set for `style: 'currency'`; the fallback only satisfies the type.\n /* v8 ignore next */\n return (\n new Intl.NumberFormat('en', {\n style: 'currency',\n currency,\n }).resolvedOptions().maximumFractionDigits ?? 2\n );\n}\n\n/** The amount in major units (64.99), whichever unit it is stored in. */\nexport function toMajorUnits(\n amount: number,\n currency: string,\n unit: MoneyUnit,\n): number {\n return unit === 'major' ? amount : amount / 10 ** fractionDigits(currency);\n}\n\n/** A major-unit amount back in the unit it is stored in, rounded to it. */\nexport function fromMajorUnits(\n major: number,\n currency: string,\n unit: MoneyUnit,\n): number {\n const digits = fractionDigits(currency);\n return unit === 'major'\n ? Number(major.toFixed(digits))\n : Math.round(major * 10 ** digits);\n}\n\n/**\n * The amount as the reader's locale writes money: `Q64.99` in `en`, `64,99 GTQ`\n * or similar in `es`. Never the bare stored number — a till once printed\n * 6499 GTQ for 64,99.\n */\nexport function formatMoney(\n amount: number,\n currency: string,\n unit: MoneyUnit,\n locale: string,\n): string {\n return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(\n toMajorUnits(amount, currency, unit),\n );\n}\n\n/** The currency a money member reads in: fixed, or the record's own member. */\nexport function currencyOf(\n money: MoneyFormat,\n record: Readonly<Record<string, unknown>> | undefined,\n): string | undefined {\n if (money.currency !== undefined) return money.currency;\n const value = record?.[money.currencyMember];\n return typeof value === 'string' && value !== '' ? value : undefined;\n}\n"],"names":["fractionDigits","currency","Intl","NumberFormat","style","resolvedOptions","maximumFractionDigits","toMajorUnits","amount","unit","fromMajorUnits","major","digits","Number","toFixed","Math","round","formatMoney","locale","format","currencyOf","money","record","undefined","value","currencyMember"],"mappings":"AAIA;;;;CAIC,GACD,OAAO,SAASA,eAAeC,QAAgB;QAI3C;IAHF,4EAA4E;IAC5E,kBAAkB,GAClB,QACE,yCAAA,IAAIC,KAAKC,YAAY,CAAC,MAAM;QAC1BC,OAAO;QACPH;IACF,GAAGI,eAAe,GAAGC,qBAAqB,YAH1C,yCAG8C;AAElD;AAEA,uEAAuE,GACvE,OAAO,SAASC,aACdC,MAAc,EACdP,QAAgB,EAChBQ,IAAe;IAEf,OAAOA,SAAS,UAAUD,SAASA,SAAS,MAAMR,eAAeC;AACnE;AAEA,yEAAyE,GACzE,OAAO,SAASS,eACdC,KAAa,EACbV,QAAgB,EAChBQ,IAAe;IAEf,MAAMG,SAASZ,eAAeC;IAC9B,OAAOQ,SAAS,UACZI,OAAOF,MAAMG,OAAO,CAACF,WACrBG,KAAKC,KAAK,CAACL,QAAQ,MAAMC;AAC/B;AAEA;;;;CAIC,GACD,OAAO,SAASK,YACdT,MAAc,EACdP,QAAgB,EAChBQ,IAAe,EACfS,MAAc;IAEd,OAAO,IAAIhB,KAAKC,YAAY,CAACe,QAAQ;QAAEd,OAAO;QAAYH;IAAS,GAAGkB,MAAM,CAC1EZ,aAAaC,QAAQP,UAAUQ;AAEnC;AAEA,6EAA6E,GAC7E,OAAO,SAASW,WACdC,KAAkB,EAClBC,MAAqD;IAErD,IAAID,MAAMpB,QAAQ,KAAKsB,WAAW,OAAOF,MAAMpB,QAAQ;IACvD,MAAMuB,QAAQF,0BAAAA,MAAQ,CAACD,MAAMI,cAAc,CAAC;IAC5C,OAAO,OAAOD,UAAU,YAAYA,UAAU,KAAKA,QAAQD;AAC7D"}
@@ -0,0 +1,19 @@
1
+ import type { ComponentPropsWithoutRef } from 'react';
2
+ import { type MoneyUnit } from './money-math.js';
3
+ export interface MoneyProps extends Omit<ComponentPropsWithoutRef<'span'>, 'children'> {
4
+ amount: number;
5
+ /** ISO 4217, e.g. `GTQ`. */
6
+ currency: string;
7
+ /** Whether `amount` counts minor units (cents) or major ones. */
8
+ unit: MoneyUnit;
9
+ /** The reader's locale; a client caller reads it with the `useLocale` hook. */
10
+ locale: string;
11
+ }
12
+ /**
13
+ * An amount of money, formatted in the reader's locale with its currency.
14
+ * `unit: 'minor'` divides by the currency's own minor-unit count, so JPY and
15
+ * KWD are right too. Tabular figures, so a column of prices aligns on the
16
+ * digit. Server-safe.
17
+ */
18
+ export declare function Money({ amount, currency, unit, locale, className, ...props }: MoneyProps): import("react").JSX.Element;
19
+ //# sourceMappingURL=money.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"money.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/money/money.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,OAAO,CAAC;AAGtD,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE9D,MAAM,WAAW,UAAW,SAAQ,IAAI,CACtC,wBAAwB,CAAC,MAAM,CAAC,EAChC,UAAU,CACX;IACC,MAAM,EAAE,MAAM,CAAC;IACf,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,IAAI,EAAE,SAAS,CAAC;IAChB,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,EACpB,MAAM,EACN,QAAQ,EACR,IAAI,EACJ,MAAM,EACN,SAAS,EACT,GAAG,KAAK,EACT,EAAE,UAAU,+BASZ"}
@@ -0,0 +1,26 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
3
+ import { jsx as _jsx } from "react/jsx-runtime";
4
+ import { cn } from '../../utils/cn.js';
5
+ import { formatMoney } from './money-math.js';
6
+ /**
7
+ * An amount of money, formatted in the reader's locale with its currency.
8
+ * `unit: 'minor'` divides by the currency's own minor-unit count, so JPY and
9
+ * KWD are right too. Tabular figures, so a column of prices aligns on the
10
+ * digit. Server-safe.
11
+ */ export function Money(_0) {
12
+ let { amount, currency, unit, locale, className } = _0, props = _object_without_properties_loose(_0, [
13
+ "amount",
14
+ "currency",
15
+ "unit",
16
+ "locale",
17
+ "className"
18
+ ]);
19
+ return /*#__PURE__*/ _jsx("span", _extends({
20
+ className: cn('whitespace-nowrap tabular-nums', className)
21
+ }, props, {
22
+ children: formatMoney(amount, currency, unit, locale)
23
+ }));
24
+ }
25
+
26
+ //# sourceMappingURL=money.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/ui/atoms/money/money.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef } from 'react';\n\nimport { cn } from '../../utils/cn.js';\nimport { formatMoney, type MoneyUnit } from './money-math.js';\n\nexport interface MoneyProps extends Omit<\n ComponentPropsWithoutRef<'span'>,\n 'children'\n> {\n amount: number;\n /** ISO 4217, e.g. `GTQ`. */\n currency: string;\n /** Whether `amount` counts minor units (cents) or major ones. */\n unit: MoneyUnit;\n /** The reader's locale; a client caller reads it with the `useLocale` hook. */\n locale: string;\n}\n\n/**\n * An amount of money, formatted in the reader's locale with its currency.\n * `unit: 'minor'` divides by the currency's own minor-unit count, so JPY and\n * KWD are right too. Tabular figures, so a column of prices aligns on the\n * digit. Server-safe.\n */\nexport function Money({\n amount,\n currency,\n unit,\n locale,\n className,\n ...props\n}: MoneyProps) {\n return (\n <span\n className={cn('whitespace-nowrap tabular-nums', className)}\n {...props}\n >\n {formatMoney(amount, currency, unit, locale)}\n </span>\n );\n}\n"],"names":["cn","formatMoney","Money","amount","currency","unit","locale","className","props","span"],"mappings":";;;AAEA,SAASA,EAAE,QAAQ,oBAAoB;AACvC,SAASC,WAAW,QAAwB,kBAAkB;AAe9D;;;;;CAKC,GACD,OAAO,SAASC,MAAM,EAOT;QAPS,EACpBC,MAAM,EACNC,QAAQ,EACRC,IAAI,EACJC,MAAM,EACNC,SAAS,EAEE,OADRC;;;;;;;IAEH,qBACE,KAACC;QACCF,WAAWP,GAAG,kCAAkCO;OAC5CC;kBAEHP,YAAYE,QAAQC,UAAUC,MAAMC;;AAG3C"}
@@ -0,0 +1,3 @@
1
+ export * from './number-input.js';
2
+ export * from './quantity-input.js';
3
+ //# sourceMappingURL=index.d.ts.map