@sincpro/web-ui 0.1.0 → 1.0.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 (198) hide show
  1. package/README.md +40 -13
  2. package/dist/components/activity/Activity.d.ts +69 -0
  3. package/dist/components/activity/Activity.js +279 -0
  4. package/dist/components/activity/ActivityBadge.d.ts +16 -0
  5. package/dist/components/activity/ActivityBadge.js +29 -0
  6. package/dist/components/{chatter → activity}/Messages.d.ts +2 -2
  7. package/dist/components/{chatter → activity}/Messages.js +9 -6
  8. package/dist/components/activity/index.d.ts +4 -0
  9. package/dist/components/activity/index.js +5 -0
  10. package/dist/components/assist/AssistMark.d.ts +3 -3
  11. package/dist/components/assist/AssistMark.js +1 -1
  12. package/dist/components/assist/assist.d.ts +1 -1
  13. package/dist/components/assist/assist.js +1 -1
  14. package/dist/components/calendar/RecordCalendar.d.ts +48 -0
  15. package/dist/components/calendar/RecordCalendar.js +101 -0
  16. package/dist/components/calendar/index.d.ts +1 -0
  17. package/dist/components/calendar/index.js +1 -0
  18. package/dist/components/form/Sheet.d.ts +4 -0
  19. package/dist/components/form/Sheet.js +16 -5
  20. package/dist/components/gantt/RecordGantt.d.ts +44 -0
  21. package/dist/components/gantt/RecordGantt.js +110 -0
  22. package/dist/components/gantt/index.d.ts +1 -0
  23. package/dist/components/gantt/index.js +1 -0
  24. package/dist/components/index.d.ts +4 -1
  25. package/dist/components/index.js +4 -1
  26. package/dist/components/lines/LineItems.d.ts +45 -5
  27. package/dist/components/lines/LineItems.js +106 -18
  28. package/dist/components/listing/BoardColumns.d.ts +39 -0
  29. package/dist/components/listing/BoardColumns.js +138 -0
  30. package/dist/components/listing/Listing.js +1 -1
  31. package/dist/components/listing/ListingBar.d.ts +46 -2
  32. package/dist/components/listing/ListingBar.js +47 -5
  33. package/dist/components/listing/PivotExplorer.d.ts +34 -0
  34. package/dist/components/listing/PivotExplorer.js +86 -0
  35. package/dist/components/listing/RecordCard.d.ts +6 -8
  36. package/dist/components/listing/RecordCard.js +13 -7
  37. package/dist/components/listing/Row.d.ts +25 -2
  38. package/dist/components/listing/Row.js +30 -13
  39. package/dist/components/listing/index.d.ts +2 -0
  40. package/dist/components/listing/index.js +2 -0
  41. package/dist/components/listing/set.d.ts +1 -1
  42. package/dist/components/navigation/index.d.ts +0 -2
  43. package/dist/components/navigation/index.js +2 -3
  44. package/dist/components/record/RecordAudit.d.ts +17 -0
  45. package/dist/components/record/RecordAudit.js +27 -0
  46. package/dist/components/record/RecordHeader.d.ts +27 -17
  47. package/dist/components/record/RecordHeader.js +64 -20
  48. package/dist/components/record/RecordNotices.d.ts +37 -0
  49. package/dist/components/record/RecordNotices.js +75 -0
  50. package/dist/components/record/RecordRibbon.d.ts +13 -0
  51. package/dist/components/record/RecordRibbon.js +22 -0
  52. package/dist/components/record/ThumbBar.js +1 -1
  53. package/dist/components/record/index.d.ts +2 -0
  54. package/dist/components/record/index.js +2 -0
  55. package/dist/components/search/CriteriaChips.d.ts +6 -1
  56. package/dist/components/search/CriteriaChips.js +71 -20
  57. package/dist/components/search/SearchBar.d.ts +4 -1
  58. package/dist/components/search/SearchBar.js +15 -7
  59. package/dist/components/timeline/RecordTimeline.d.ts +60 -0
  60. package/dist/components/timeline/RecordTimeline.js +214 -0
  61. package/dist/components/timeline/TimelineToolbar.d.ts +30 -0
  62. package/dist/components/timeline/TimelineToolbar.js +31 -0
  63. package/dist/components/timeline/index.d.ts +3 -0
  64. package/dist/components/timeline/index.js +3 -0
  65. package/dist/components/timeline/time.d.ts +43 -0
  66. package/dist/components/timeline/time.js +135 -0
  67. package/dist/foundations/copy.d.ts +194 -18
  68. package/dist/foundations/copy.es.js +142 -15
  69. package/dist/foundations/copy.js +133 -11
  70. package/dist/foundations/index.d.ts +1 -1
  71. package/dist/foundations/match.d.ts +23 -0
  72. package/dist/foundations/match.js +63 -0
  73. package/dist/foundations/slots.d.ts +1 -1
  74. package/dist/foundations/slots.js +8 -3
  75. package/dist/foundations/types.d.ts +7 -0
  76. package/dist/foundations/types.js +0 -36
  77. package/dist/primitives/block/index.d.ts +1 -1
  78. package/dist/primitives/block/index.js +1 -1
  79. package/dist/primitives/cell/CellStack.d.ts +7 -1
  80. package/dist/primitives/cell/CellStack.js +5 -2
  81. package/dist/primitives/choice/ComboBox.d.ts +3 -0
  82. package/dist/primitives/choice/ComboBox.js +10 -6
  83. package/dist/primitives/choice/KanbanState.d.ts +14 -0
  84. package/dist/primitives/choice/KanbanState.js +37 -0
  85. package/dist/primitives/choice/Priority.d.ts +20 -0
  86. package/dist/primitives/choice/Priority.js +41 -0
  87. package/dist/primitives/choice/SearchField.d.ts +10 -1
  88. package/dist/primitives/choice/SearchField.js +21 -2
  89. package/dist/primitives/choice/Slider.d.ts +6 -1
  90. package/dist/primitives/choice/Slider.js +2 -2
  91. package/dist/primitives/choice/index.d.ts +2 -0
  92. package/dist/primitives/choice/index.js +2 -0
  93. package/dist/primitives/date/Calendar.js +30 -6
  94. package/dist/primitives/date/DatePicker.d.ts +11 -3
  95. package/dist/primitives/date/DatePicker.js +12 -8
  96. package/dist/primitives/date/DateTime.d.ts +35 -0
  97. package/dist/primitives/date/DateTime.js +100 -0
  98. package/dist/primitives/date/index.d.ts +1 -0
  99. package/dist/primitives/date/index.js +4 -2
  100. package/dist/primitives/dialog/Dialog.d.ts +9 -3
  101. package/dist/primitives/dialog/Dialog.js +41 -5
  102. package/dist/primitives/field/DropZone.js +1 -1
  103. package/dist/primitives/field/Field.d.ts +14 -2
  104. package/dist/primitives/field/Field.js +25 -14
  105. package/dist/primitives/field/copy.d.ts +16 -0
  106. package/dist/primitives/field/copy.js +69 -0
  107. package/dist/primitives/field/index.d.ts +2 -0
  108. package/dist/primitives/field/index.js +4 -0
  109. package/dist/primitives/field/note.d.ts +55 -0
  110. package/dist/primitives/field/note.js +117 -0
  111. package/dist/primitives/index.d.ts +1 -0
  112. package/dist/primitives/index.js +1 -0
  113. package/dist/primitives/menu/Menu.d.ts +28 -0
  114. package/dist/primitives/menu/Menu.js +47 -17
  115. package/dist/primitives/notice/Callout.js +1 -1
  116. package/dist/primitives/notice/Flag.d.ts +13 -0
  117. package/dist/primitives/notice/Flag.js +31 -0
  118. package/dist/primitives/notice/index.d.ts +2 -1
  119. package/dist/primitives/notice/index.js +2 -1
  120. package/dist/primitives/overlay/Preview.d.ts +19 -0
  121. package/dist/primitives/overlay/Preview.js +69 -0
  122. package/dist/primitives/overlay/index.d.ts +1 -0
  123. package/dist/primitives/overlay/index.js +3 -1
  124. package/dist/primitives/picker/MatchedText.d.ts +10 -0
  125. package/dist/primitives/picker/MatchedText.js +12 -0
  126. package/dist/primitives/picker/Picker.d.ts +67 -4
  127. package/dist/primitives/picker/Picker.js +142 -23
  128. package/dist/primitives/table/CellCopy.d.ts +20 -0
  129. package/dist/primitives/table/CellCopy.js +23 -0
  130. package/dist/primitives/table/DataTable.d.ts +55 -1
  131. package/dist/primitives/table/DataTable.js +126 -40
  132. package/dist/primitives/table/EditableTable.d.ts +102 -2
  133. package/dist/primitives/table/EditableTable.js +287 -32
  134. package/dist/primitives/table/ReportTable.js +2 -1
  135. package/dist/primitives/table/RowActions.js +7 -2
  136. package/dist/primitives/table/SheetTable.d.ts +4 -1
  137. package/dist/primitives/table/SheetTable.js +38 -5
  138. package/dist/primitives/table/Table.d.ts +5 -0
  139. package/dist/primitives/table/Table.js +17 -8
  140. package/dist/primitives/table/index.d.ts +2 -2
  141. package/dist/primitives/table/index.js +1 -1
  142. package/dist/primitives/tabs/Tabs.d.ts +2 -2
  143. package/dist/primitives/tabs/Tabs.js +1 -1
  144. package/dist/primitives/tag/Tags.d.ts +2 -0
  145. package/dist/primitives/tag/Tags.js +2 -1
  146. package/dist/primitives/tag/TagsField.d.ts +13 -0
  147. package/dist/primitives/tag/TagsField.js +39 -10
  148. package/dist/primitives/tag/choices.d.ts +2 -0
  149. package/dist/primitives/tree/Tree.d.ts +56 -0
  150. package/dist/primitives/tree/Tree.js +74 -0
  151. package/dist/primitives/tree/index.d.ts +1 -0
  152. package/dist/primitives/tree/index.js +3 -0
  153. package/dist/primitives/value/Avatar.d.ts +16 -0
  154. package/dist/primitives/value/Avatar.js +91 -0
  155. package/dist/primitives/value/Copyable.d.ts +11 -0
  156. package/dist/primitives/value/Copyable.js +27 -6
  157. package/dist/primitives/value/Fecha.js +10 -1
  158. package/dist/primitives/value/Person.d.ts +6 -51
  159. package/dist/primitives/value/Person.js +9 -148
  160. package/dist/primitives/value/ProgressBar.d.ts +31 -0
  161. package/dist/primitives/value/ProgressBar.js +53 -0
  162. package/dist/primitives/value/Relation.d.ts +22 -5
  163. package/dist/primitives/value/Relation.js +42 -14
  164. package/dist/primitives/value/Totals.d.ts +102 -7
  165. package/dist/primitives/value/Totals.js +52 -2
  166. package/dist/primitives/value/Value.d.ts +0 -37
  167. package/dist/primitives/value/Value.js +0 -39
  168. package/dist/primitives/value/contact.d.ts +30 -0
  169. package/dist/primitives/value/contact.js +51 -0
  170. package/dist/primitives/value/index.d.ts +5 -4
  171. package/dist/primitives/value/index.js +5 -4
  172. package/dist/tokens.css +179 -7
  173. package/dist/views/list/List.d.ts +32 -3
  174. package/dist/views/list/List.js +51 -7
  175. package/dist/views/list/ListView.d.ts +1 -1
  176. package/dist/views/list/ListView.js +70 -13
  177. package/dist/views/list/columns.js +8 -1
  178. package/dist/views/list/derive.d.ts +12 -1
  179. package/dist/views/list/derive.js +29 -3
  180. package/dist/views/list/presentations.d.ts +5 -2
  181. package/dist/views/list/presentations.js +35 -3
  182. package/dist/views/list/types.d.ts +57 -1
  183. package/dist/views/list/useNarrowList.d.ts +10 -0
  184. package/dist/views/list/useNarrowList.js +20 -6
  185. package/dist/views/record/Record.d.ts +41 -8
  186. package/dist/views/record/Record.js +113 -28
  187. package/dist/views/record/index.d.ts +1 -1
  188. package/package.json +15 -4
  189. package/dist/components/chatter/Chatter.d.ts +0 -161
  190. package/dist/components/chatter/Chatter.js +0 -232
  191. package/dist/components/chatter/index.d.ts +0 -3
  192. package/dist/components/chatter/index.js +0 -4
  193. package/dist/components/navigation/CommandPalette.d.ts +0 -40
  194. package/dist/components/navigation/CommandPalette.js +0 -114
  195. package/dist/components/navigation/commands.d.ts +0 -30
  196. package/dist/components/navigation/commands.js +0 -30
  197. /package/dist/components/{chatter → activity}/Timeline.d.ts +0 -0
  198. /package/dist/components/{chatter → activity}/Timeline.js +0 -0
@@ -0,0 +1,16 @@
1
+ export declare const SIZE: {
2
+ readonly sm: "size-5 text-micro";
3
+ readonly md: "size-6 text-micro";
4
+ readonly lg: "size-8 text-meta";
5
+ readonly xl: "size-12 text-body";
6
+ };
7
+ export declare const OVERLAP: Record<AvatarSize, string>;
8
+ export type AvatarSize = keyof typeof SIZE;
9
+ export declare function Avatar({ name, size,
10
+ /** The photo, if there is one. Without it the initials go, which is the normal
11
+ * case here. */
12
+ src, }: {
13
+ name: string;
14
+ size?: AvatarSize;
15
+ src?: string;
16
+ }): import("react").JSX.Element;
@@ -0,0 +1,91 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useSurface } from "../../foundations/surface.js";
4
+ /* ==================================================================== *
5
+ * WHO — the person as a datum, not as a string of text.
6
+ *
7
+ * «andres.g@sincpro.com.bo» in a cell is a correct and unreadable datum: twelve
8
+ * rows of emails get read letter by letter. An initial in a circle is
9
+ * recognized without reading, which is what is needed when the question is «who
10
+ * does this belong to?» and not «what is their email?».
11
+ *
12
+ * ── The color comes out of the name and nobody picks it ──────────────
13
+ *
14
+ * A hash of the name against the tone palette. It is deterministic —the same
15
+ * person always has the same color, on every screen— and that is why the color
16
+ * gets to mean something. If the consumer picked it, the same person would be
17
+ * green in one list and blue in another, and recognition without reading is lost
18
+ * entirely.
19
+ *
20
+ * The color here is NOT a state: it is identity. That is why it uses the tone
21
+ * palette but never `bad` nor `warn` — a person is not «in error».
22
+ * ==================================================================== */
23
+ const PALETTE = [
24
+ "bg-info/15 text-info",
25
+ "bg-good/15 text-good",
26
+ "bg-accent/25 text-ink",
27
+ "bg-primary/12 text-primary",
28
+ "bg-ink/10 text-ink",
29
+ ];
30
+ /** The initials: two, and from the first two WORDS. «Ana María Gutiérrez» gives
31
+ * `AG` and not `AM` — the surname distinguishes more than the middle name. */
32
+ function initials(name) {
33
+ const parts = name
34
+ .trim()
35
+ .split(/\s+/)
36
+ .filter((p) => p.length > 0);
37
+ const first = parts[0]?.[0] ?? "?";
38
+ const last = parts.length > 1 ? parts[parts.length - 1]?.[0] : undefined;
39
+ return `${first}${last ?? ""}`.toUpperCase();
40
+ }
41
+ /* The same five identities on the dark chrome. The canvas palette there composed its tints
42
+ over the navy and left ink letters on a dark disc — 1.61:1, unreadable (measured on the
43
+ application bar). Same index, so a person keeps their color across the two surfaces. */
44
+ const CHROME_PALETTE = [
45
+ "bg-chrome-link/20 text-chrome-link",
46
+ "bg-chrome-good/20 text-chrome-good",
47
+ "bg-chrome-accent/12 text-chrome-accent",
48
+ "bg-chrome-link/14 text-chrome-ink",
49
+ "bg-chrome-ink/14 text-chrome-ink",
50
+ ];
51
+ function toneOf(name, surface = "canvas") {
52
+ let h = 0;
53
+ for (const ch of name)
54
+ h = (h * 31 + ch.codePointAt(0)) % 100000;
55
+ const palette = surface === "chrome" ? CHROME_PALETTE : PALETTE;
56
+ return palette[h % palette.length];
57
+ }
58
+ /* The text doesn't go below `micro`: two initials at 10 px can't be read, and an
59
+ unreadable initial is a colored circle — which is exactly what we don't want.
60
+ What changes between sizes is the circle. */
61
+ export const SIZE = {
62
+ sm: "size-5 text-micro",
63
+ md: "size-6 text-micro",
64
+ lg: "size-8 text-meta",
65
+ /* The head of a contact card: the face is what the card is about. */
66
+ xl: "size-12 text-body",
67
+ };
68
+ /* Two initials, so the overlap stays short of the second letter. The ring is
69
+ 1px: a 2px ring of the surface color reads as a gap between people, which
70
+ is the thing a stack is not. */
71
+ export const OVERLAP = {
72
+ sm: "-ml-1.5",
73
+ md: "-ml-2",
74
+ lg: "-ml-2.5",
75
+ xl: "-ml-3.5",
76
+ };
77
+ export function Avatar({ name, size = "md",
78
+ /** The photo, if there is one. Without it the initials go, which is the normal
79
+ * case here. */
80
+ src, }) {
81
+ const surface = useSurface();
82
+ if (src !== undefined) {
83
+ return (
84
+ /* The photo lies over the initials, drawn invisible: an image has no text baseline, so
85
+ a chip with a photo sat on the image's bottom edge in a row aligned by baseline (a
86
+ `DataField`), higher than the label next to it. The hidden initials give the photo
87
+ the same baseline the initials disc has. */
88
+ _jsxs("span", { title: name, className: `relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-pill ${SIZE[size]}`, children: [_jsx("span", { "aria-hidden": true, className: "invisible", children: initials(name) }), _jsx("img", { src: src, alt: name, className: "absolute inset-0 size-full object-cover" })] }));
89
+ }
90
+ return (_jsx("span", { title: name, "aria-hidden": true, className: `inline-flex shrink-0 items-center justify-center rounded-pill font-medium select-none ${SIZE[size]} ${toneOf(name, surface)}`, children: initials(name) }));
91
+ }
@@ -34,4 +34,15 @@ export type CopyableProps = {
34
34
  * el valor ya está dibujado. */
35
35
  bare?: boolean;
36
36
  };
37
+ /**
38
+ * The button alone, for a value that is only known when it is pressed — what a field shows
39
+ * at that moment (`Field`'s copy button reads it from the screen). `Copyable` is this
40
+ * button next to its value.
41
+ */
42
+ export declare function CopyButton({ value, label, }: {
43
+ /** The text, or how to get it at the moment of the press. */
44
+ value: string | (() => string);
45
+ /** Its name at rest. Defaults to the copy table's «Copiar». */
46
+ label?: string;
47
+ }): import("react").JSX.Element;
37
48
  export declare function Copyable({ value, children, mono, bare }: CopyableProps): import("react").JSX.Element;
@@ -6,7 +6,12 @@ import { useEffect, useRef, useState } from "react";
6
6
  /** Cuánto dura el tilde. Dos segundos: alcanza para verlo sin que el botón se
7
7
  * quede en un estado que ya no es cierto. */
8
8
  const CONFIRMED_FOR = 2000;
9
- export function Copyable({ value, children, mono = false, bare = false }) {
9
+ /**
10
+ * The button alone, for a value that is only known when it is pressed — what a field shows
11
+ * at that moment (`Field`'s copy button reads it from the screen). `Copyable` is this
12
+ * button next to its value.
13
+ */
14
+ export function CopyButton({ value, label, }) {
10
15
  const copy = useUiCopy();
11
16
  const [state, setState] = useState("idle");
12
17
  const timer = useRef(null);
@@ -19,8 +24,15 @@ export function Copyable({ value, children, mono = false, bare = false }) {
19
24
  async function run() {
20
25
  if (timer.current !== null)
21
26
  clearTimeout(timer.current);
27
+ const text = typeof value === "function" ? value() : value;
28
+ if (text.trim() === "") {
29
+ /* Nothing on screen to copy: said, not a check mark over an empty clipboard. */
30
+ setState("empty");
31
+ timer.current = setTimeout(() => setState("idle"), CONFIRMED_FOR);
32
+ return;
33
+ }
22
34
  try {
23
- await navigator.clipboard.writeText(value);
35
+ await navigator.clipboard.writeText(text);
24
36
  setState("done");
25
37
  }
26
38
  catch {
@@ -30,16 +42,25 @@ export function Copyable({ value, children, mono = false, bare = false }) {
30
42
  }
31
43
  timer.current = setTimeout(() => setState("idle"), CONFIRMED_FOR);
32
44
  }
33
- const label = state === "done"
45
+ const said = state === "done"
34
46
  ? copy.copyable.done
35
47
  : state === "failed"
36
48
  ? copy.copyable.failed
37
- : copy.copyable.label;
38
- const button = (_jsx("button", { type: "button", onClick: run, "aria-label": label, title: label, className: `inline-flex shrink-0 items-center justify-center rounded-control p-0.5 transition-colors duration-(--duration-instant) ${state === "done"
49
+ : state === "empty"
50
+ ? copy.copyable.empty
51
+ : (label ?? copy.copyable.label);
52
+ return (_jsx("button", { type: "button", onClick: (e) => {
53
+ /* Inside a row that opens or a link: copying is not opening. */
54
+ e.stopPropagation();
55
+ void run();
56
+ }, "aria-label": said, title: said, className: `inline-flex shrink-0 items-center justify-center rounded-control p-0.5 transition-colors duration-(--duration-instant) ${state === "done"
39
57
  ? "text-good"
40
- : state === "failed"
58
+ : state === "failed" || state === "empty"
41
59
  ? "text-bad"
42
60
  : "text-muted hover:bg-ink/6 hover:text-ink"}`, children: state === "done" ? (_jsx(Check, { className: "size-(--size-icon-sm)", strokeWidth: 2.5 })) : (_jsx(Copy, { className: "size-(--size-icon-sm)", strokeWidth: 2 })) }));
61
+ }
62
+ export function Copyable({ value, children, mono = false, bare = false }) {
63
+ const button = _jsx(CopyButton, { value: value });
43
64
  if (bare)
44
65
  return button;
45
66
  return (
@@ -61,12 +61,21 @@ export function formatDateTime(when) {
61
61
  const locale = currentCopyLocale();
62
62
  return `${dateFormat(locale).format(when)} ${timeFormat(locale).format(when)}`;
63
63
  }
64
+ /** A day written as `YYYY-MM-DD` (what a date field stores) is that day HERE. `new Date()`
65
+ * reads it as midnight in UTC, and west of Greenwich that is the evening before: a due date
66
+ * of the 15th was drawn as the 14th in La Paz. Anything else goes through `Date` as is. */
67
+ function toDate(when) {
68
+ const day = typeof when === "string" ? /^(\d{4})-(\d{2})-(\d{2})$/.exec(when) : null;
69
+ if (day === null)
70
+ return new Date(when);
71
+ return new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3]));
72
+ }
64
73
  export function DateValue({ when, format = "relative",
65
74
  /** So it can be tested: without this, a story with «hace 2 min» changes its
66
75
  * text every minute and no snapshot works as a reference. */
67
76
  now, }) {
68
77
  const copy = useUiCopy();
69
- const d = when instanceof Date ? when : new Date(when);
78
+ const d = when instanceof Date ? when : toDate(when);
70
79
  if (Number.isNaN(d.getTime())) {
71
80
  return (_jsx("span", { className: "text-meta text-muted italic", title: copy.time.invalid, children: copy.time.noDate }));
72
81
  }
@@ -1,52 +1,8 @@
1
+ import { type AvatarSize } from "./Avatar.js";
2
+ import type { PersonContact } from "./contact.js";
1
3
  import type { ReactNode } from "react";
2
- declare const SIZE: {
3
- readonly sm: "size-5 text-micro";
4
- readonly md: "size-6 text-micro";
5
- readonly lg: "size-8 text-meta";
6
- readonly xl: "size-12 text-body";
7
- };
8
- export type AvatarSize = keyof typeof SIZE;
9
- export declare function Avatar({ name, size,
10
- /** The photo, if there is one. Without it the initials go, which is the normal
11
- * case here. */
12
- src, }: {
13
- name: string;
14
- size?: AvatarSize;
15
- src?: string;
16
- }): import("react").JSX.Element;
17
- /** The ways to reach a person. Each one that is present becomes a row of the contact card. */
18
- export type PersonContact = {
19
- email?: string;
20
- /** As people write it — «+591 7 123-4567». The `tel:` link keeps only the digits and `+`. */
21
- phone?: string;
22
- /** The WhatsApp number with its country code, written any way; `wa.me` gets the digits. */
23
- whatsapp?: string;
24
- };
25
- export type ContactCardProps = {
26
- name: string;
27
- /** The photo. Without it, the initials — in the same color the person has everywhere. */
28
- src?: string;
29
- /** What the person is here: «Vendedora · Sucursal La Paz». */
30
- role?: string;
31
- contact?: PersonContact;
32
- /** Go to the person's own record — «Ver ficha». Without it the card only reaches them. */
33
- onOpen?: () => void;
34
- };
35
- /**
36
- * Who a person is and how to reach them: the face, the name, the role, and one row per way
37
- * of contact — write, call, WhatsApp — then their record.
38
- *
39
- * Context: in an ERP the question after «who is the salesperson of this invoice?» is almost
40
- * always «how do I reach her?». Copying an email out of another screen to paste it into the
41
- * mail client is the detour this card removes. Each way of contact is a ROW and not a bare
42
- * icon: the address and the number are drawn, because often what the person wants is to read
43
- * the number aloud, not to dial it from the browser. Every row is a real link (`mailto:`,
44
- * `tel:`, `wa.me`), so middle-click, «copy address» and the phone's own dialer work.
45
- *
46
- * It is what opens from a `UserChip` with `contact`, and it is exported on its own for the
47
- * side panel of a record, where the same person is shown without a chip to open.
48
- */
49
- export declare function ContactCard({ name, src, role, contact, onOpen }: ContactCardProps): import("react").JSX.Element;
4
+ export { Avatar, type AvatarSize } from "./Avatar.js";
5
+ export type { PersonContact } from "./contact.js";
50
6
  export type UserChipProps = {
51
7
  name: string;
52
8
  size?: AvatarSize;
@@ -57,7 +13,7 @@ export type UserChipProps = {
57
13
  role?: string;
58
14
  /** The ways to reach them. Present = the chip opens a contact card. */
59
15
  contact?: PersonContact;
60
- /** Go to the person's record. Present = the chip opens a card with «Ver ficha». */
16
+ /** Go to the person's record. Present = the chip opens its card with «Abrir». */
61
17
  onOpen?: () => void;
62
18
  };
63
19
  /**
@@ -65,7 +21,7 @@ export type UserChipProps = {
65
21
  * a fact of a record.
66
22
  *
67
23
  * Context: a chip is a button ONLY when it has somewhere to go. With `contact` or `onOpen` it
68
- * opens the `ContactCard` (press, Enter or Space — React Aria's `DialogTrigger` through
24
+ * opens the person's `RelationCard` (press, Enter or Space — React Aria's `DialogTrigger` through
69
25
  * `PopoverTrigger`: focus goes into the card, Escape and a click outside close it, and focus
70
26
  * comes back to the chip). Without them it stays text: a button that does nothing is a lie a
71
27
  * keyboard user pays for with one more Tab stop on every row.
@@ -87,4 +43,3 @@ export declare function UserStack({ names, max, size, }: {
87
43
  max?: number;
88
44
  size?: AvatarSize;
89
45
  }): import("react").JSX.Element;
90
- export {};
@@ -1,154 +1,15 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useUiCopy } from "../../foundations/copy.js";
4
- import { useSurface } from "../../foundations/surface.js";
5
- import { Button } from "../button/Button.js";
6
4
  import { PopoverTrigger } from "../overlay/PopoverTrigger.js";
7
- import { ArrowRight, Mail, MessageCircle, Phone } from "lucide-react";
5
+ import { Avatar, OVERLAP, SIZE, } from "./Avatar.js";
6
+ import { RelationCard } from "./Relation.js";
8
7
  /* ==================================================================== *
9
- * WHO — the person as a datum, not as a string of text.
10
- *
11
- * «andres.g@sincpro.com.bo» in a cell is a correct and unreadable datum: twelve
12
- * rows of emails get read letter by letter. An initial in a circle is
13
- * recognized without reading, which is what is needed when the question is «who
14
- * does this belong to?» and not «what is their email?».
15
- *
16
- * ── The color comes out of the name and nobody picks it ──────────────
17
- *
18
- * A hash of the name against the tone palette. It is deterministic —the same
19
- * person always has the same color, on every screen— and that is why the color
20
- * gets to mean something. If the consumer picked it, the same person would be
21
- * green in one list and blue in another, and recognition without reading is lost
22
- * entirely.
23
- *
24
- * The color here is NOT a state: it is identity. That is why it uses the tone
25
- * palette but never `bad` nor `warn` — a person is not «in error».
8
+ * WHO — a person as a chip and as a stack. The face itself is `Avatar`; what a person's
9
+ * card says (the face, the role, the ways to reach them, «Abrir») is the `RelationCard` of
10
+ * that person: a person IS a related record, and one card answers for all of them.
26
11
  * ==================================================================== */
27
- const PALETTE = [
28
- "bg-info/15 text-info",
29
- "bg-good/15 text-good",
30
- "bg-accent/25 text-ink",
31
- "bg-primary/12 text-primary",
32
- "bg-ink/10 text-ink",
33
- ];
34
- /** The initials: two, and from the first two WORDS. «Ana María Gutiérrez» gives
35
- * `AG` and not `AM` — the surname distinguishes more than the middle name. */
36
- function initials(name) {
37
- const parts = name
38
- .trim()
39
- .split(/\s+/)
40
- .filter((p) => p.length > 0);
41
- const first = parts[0]?.[0] ?? "?";
42
- const last = parts.length > 1 ? parts[parts.length - 1]?.[0] : undefined;
43
- return `${first}${last ?? ""}`.toUpperCase();
44
- }
45
- /* The same five identities on the dark chrome. The canvas palette there composed its tints
46
- over the navy and left ink letters on a dark disc — 1.61:1, unreadable (measured on the
47
- application bar). Same index, so a person keeps their color across the two surfaces. */
48
- const CHROME_PALETTE = [
49
- "bg-chrome-link/20 text-chrome-link",
50
- "bg-chrome-good/20 text-chrome-good",
51
- "bg-chrome-accent/12 text-chrome-accent",
52
- "bg-chrome-link/14 text-chrome-ink",
53
- "bg-chrome-ink/14 text-chrome-ink",
54
- ];
55
- function toneOf(name, surface = "canvas") {
56
- let h = 0;
57
- for (const ch of name)
58
- h = (h * 31 + ch.codePointAt(0)) % 100000;
59
- const palette = surface === "chrome" ? CHROME_PALETTE : PALETTE;
60
- return palette[h % palette.length];
61
- }
62
- /* The text doesn't go below `micro`: two initials at 10 px can't be read, and an
63
- unreadable initial is a colored circle — which is exactly what we don't want.
64
- What changes between sizes is the circle. */
65
- const SIZE = {
66
- sm: "size-5 text-micro",
67
- md: "size-6 text-micro",
68
- lg: "size-8 text-meta",
69
- /* The head of a contact card: the face is what the card is about. */
70
- xl: "size-12 text-body",
71
- };
72
- /* Two initials, so the overlap stays short of the second letter. The ring is
73
- 1px: a 2px ring of the surface color reads as a gap between people, which
74
- is the thing a stack is not. */
75
- const OVERLAP = {
76
- sm: "-ml-1.5",
77
- md: "-ml-2",
78
- lg: "-ml-2.5",
79
- xl: "-ml-3.5",
80
- };
81
- export function Avatar({ name, size = "md",
82
- /** The photo, if there is one. Without it the initials go, which is the normal
83
- * case here. */
84
- src, }) {
85
- const surface = useSurface();
86
- if (src !== undefined) {
87
- return (
88
- /* The photo lies over the initials, drawn invisible: an image has no text baseline, so
89
- a chip with a photo sat on the image's bottom edge in a row aligned by baseline (a
90
- `DataField`), higher than the label next to it. The hidden initials give the photo
91
- the same baseline the initials disc has. */
92
- _jsxs("span", { title: name, className: `relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-pill ${SIZE[size]}`, children: [_jsx("span", { "aria-hidden": true, className: "invisible", children: initials(name) }), _jsx("img", { src: src, alt: name, className: "absolute inset-0 size-full object-cover" })] }));
93
- }
94
- return (_jsx("span", { title: name, "aria-hidden": true, className: `inline-flex shrink-0 items-center justify-center rounded-pill font-medium select-none ${SIZE[size]} ${toneOf(name, surface)}`, children: initials(name) }));
95
- }
96
- /** The three contact links, built from what people type. `wa.me` takes digits only — a `+`,
97
- * a space or a dash breaks it — and `tel:` takes digits and the leading `+`. */
98
- function contactRows(contact, copy) {
99
- const rows = [];
100
- if (contact.email !== undefined && contact.email !== "") {
101
- rows.push({
102
- key: "email",
103
- icon: Mail,
104
- label: copy.email,
105
- value: contact.email,
106
- href: `mailto:${contact.email}`,
107
- });
108
- }
109
- if (contact.phone !== undefined && contact.phone !== "") {
110
- rows.push({
111
- key: "call",
112
- icon: Phone,
113
- label: copy.call,
114
- value: contact.phone,
115
- href: `tel:${contact.phone.replace(/[^\d+]/g, "")}`,
116
- });
117
- }
118
- if (contact.whatsapp !== undefined && contact.whatsapp !== "") {
119
- rows.push({
120
- key: "whatsapp",
121
- icon: MessageCircle,
122
- label: copy.whatsapp,
123
- value: contact.whatsapp,
124
- href: `https://wa.me/${contact.whatsapp.replace(/\D/g, "")}`,
125
- });
126
- }
127
- return rows;
128
- }
129
- /**
130
- * Who a person is and how to reach them: the face, the name, the role, and one row per way
131
- * of contact — write, call, WhatsApp — then their record.
132
- *
133
- * Context: in an ERP the question after «who is the salesperson of this invoice?» is almost
134
- * always «how do I reach her?». Copying an email out of another screen to paste it into the
135
- * mail client is the detour this card removes. Each way of contact is a ROW and not a bare
136
- * icon: the address and the number are drawn, because often what the person wants is to read
137
- * the number aloud, not to dial it from the browser. Every row is a real link (`mailto:`,
138
- * `tel:`, `wa.me`), so middle-click, «copy address» and the phone's own dialer work.
139
- *
140
- * It is what opens from a `UserChip` with `contact`, and it is exported on its own for the
141
- * side panel of a record, where the same person is shown without a chip to open.
142
- */
143
- export function ContactCard({ name, src, role, contact = {}, onOpen }) {
144
- const copy = useUiCopy().person;
145
- const rows = contactRows(contact, copy);
146
- return (
147
- /* React events bubble through the popover's portal to the React parents of the chip —
148
- a table row that opens on click among them. A click inside the card is not a click
149
- on the row. */
150
- _jsxs("div", { className: "flex w-64 max-w-full flex-col gap-3", onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx("span", { "aria-hidden": true, className: "flex shrink-0", children: _jsx(Avatar, { name: name, size: "xl", ...(src === undefined ? {} : { src }) }) }), _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate text-lead font-semibold text-ink", title: name, children: name }), role === undefined ? null : (_jsx("span", { className: "line-clamp-2 text-meta text-muted", title: role, children: role }))] })] }), rows.length === 0 ? null : (_jsx("ul", { className: "-mx-1 flex flex-col", children: rows.map(({ key, icon: Icon, label, value, href }) => (_jsx("li", { children: _jsxs("a", { href: href, "data-contact": key, ...(key === "whatsapp" ? { target: "_blank", rel: "noreferrer" } : {}), className: "flex min-w-0 items-center gap-2.5 rounded-control px-1 py-1.5 no-underline outline-primary outline-offset-2 hover:bg-ink/6 focus-visible:outline-2 forced-colors:outline-[Highlight]", children: [_jsx(Icon, { "aria-hidden": true, className: "size-(--size-icon-md) shrink-0 text-primary", strokeWidth: 2 }), _jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "text-meta font-medium text-ink", children: label }), _jsx("span", { className: "truncate text-micro tabular-nums text-muted", children: value })] })] }) }, key))) })), onOpen === undefined ? null : (_jsx("div", { className: "flex justify-end border-t border-line pt-2", children: _jsx(Button, { role: "ghost", size: "sm", icon: ArrowRight, onClick: onOpen, children: copy.open }) }))] }));
151
- }
12
+ export { Avatar } from "./Avatar.js";
152
13
  /** The chip as the opener of its card. A native button that takes what `PopoverTrigger`
153
14
  * hands its trigger — `ref`, the press handlers, `expanded` and `haspopup` — the same
154
15
  * contract the system's buttons keep. */
@@ -164,7 +25,7 @@ function ChipButton({ expanded, haspopup, children, ref, ...native }) {
164
25
  * a fact of a record.
165
26
  *
166
27
  * Context: a chip is a button ONLY when it has somewhere to go. With `contact` or `onOpen` it
167
- * opens the `ContactCard` (press, Enter or Space — React Aria's `DialogTrigger` through
28
+ * opens the person's `RelationCard` (press, Enter or Space — React Aria's `DialogTrigger` through
168
29
  * `PopoverTrigger`: focus goes into the card, Escape and a click outside close it, and focus
169
30
  * comes back to the chip). Without them it stays text: a button that does nothing is a lie a
170
31
  * keyboard user pays for with one more Tab stop on every row.
@@ -180,7 +41,7 @@ export function UserChip({ name, size = "md", src, detail, role, contact, onOpen
180
41
  if (!reachable) {
181
42
  return _jsx("span", { className: "inline-flex min-w-0 items-center gap-2", children: face });
182
43
  }
183
- return (_jsx(PopoverTrigger, { label: name, trigger: _jsx(ChipButton, { children: face }), children: (close) => (_jsx(ContactCard, { name: name, ...(src === undefined ? {} : { src }), ...(role === undefined ? {} : { role }), ...(contact === undefined ? {} : { contact }), ...(onOpen === undefined
44
+ return (_jsx(PopoverTrigger, { label: name, trigger: _jsx(ChipButton, { children: face }), children: (close) => (_jsx(RelationCard, { name: name, media: { person: true, ...(src === undefined ? {} : { src }) }, ...(role === undefined ? {} : { secondary: role }), ...(contact === undefined ? {} : { contact }), ...(onOpen === undefined
184
45
  ? {}
185
46
  : {
186
47
  onOpen: () => {
@@ -0,0 +1,31 @@
1
+ import type { Tone } from "../../foundations/types.js";
2
+ import type { ReactNode } from "react";
3
+ export type ProgressBarProps = {
4
+ /** Where the task is, from `minValue` to `maxValue`. Absent = under way and it cannot say
5
+ * how far: the bar sweeps and draws no figure. */
6
+ value?: number;
7
+ /** What is running: «Importación de clientes», «Cierre de octubre». Always its accessible
8
+ * name; drawn above the bar only with `showLabel`. */
9
+ label: string;
10
+ minValue?: number;
11
+ /** The size of the task — 4000 for an import of 4.000 rows. 100 by default, so a plain
12
+ * percentage needs nothing else. */
13
+ maxValue?: number;
14
+ /** How the figure is written. Absent: the percentage of the task. */
15
+ formatOptions?: Intl.NumberFormatOptions;
16
+ /** The figure written by hand, when a percentage does not say it: «1.240 de 4.000 filas».
17
+ * It is also what a screen reader reads. Indeterminate, it is the only figure drawn —
18
+ * «leyendo el archivo…». */
19
+ valueLabel?: ReactNode;
20
+ /** Draws the label above the bar: the bar a screen is about (an import, the closing of
21
+ * the month). Off in a cell or a row, where the column or the line already says it. */
22
+ showLabel?: boolean;
23
+ /** Off when the figure is already beside it — a cell with its column. */
24
+ showValue?: boolean;
25
+ /** The width of the track (inline) or of the whole piece (with `showLabel`). A bar in a
26
+ * list row is shorter than the bar of an import. */
27
+ width?: string;
28
+ /** The state of the task. `brand` — the gradient — is the long bar of a live process. */
29
+ tone?: Tone | "brand";
30
+ };
31
+ export declare function ProgressBar({ value, label, minValue, maxValue, formatOptions, valueLabel, showLabel, showValue, width, tone, }: ProgressBarProps): import("react").JSX.Element;
@@ -0,0 +1,53 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { AriaLocale } from "../../foundations/aria.js";
4
+ import { cn } from "../../foundations/variants.js";
5
+ import { ProgressBar as AriaProgressBar } from "react-aria-components";
6
+ /**
7
+ * A task under way: the import of 4.000 rows, the closing of the month, a file going up. A
8
+ * real bar, never a spinner — a spinner says «wait» and does not say how much; a bar says both.
9
+ *
10
+ * Context: it looks like `Meter` and it is the opposite thing. A meter is a QUANTITY in a
11
+ * range that just is — the stock against the warehouse, the credit used — and full is often
12
+ * the alarm. A progress bar is something HAPPENING, moving toward done, and full is the good
13
+ * news. That is why their color comes from different places: the meter's is the reading of
14
+ * its value; this one's is the state of the task, said by whoever runs it — `good` finished,
15
+ * `warn` is running slow, `bad` failed — and `brand` by default, the blue→gold gradient of
16
+ * the long bar of a live import. (At 6 px and 62 % in a list row a gradient reads as a
17
+ * broken bar, so a short bar there asks for a flat tone.)
18
+ *
19
+ * ── Determinate and indeterminate ─────────────────────────────────────
20
+ *
21
+ * With a `value` it fills to it, and the figure next to it is in tabular figures so a column
22
+ * of bars keeps its digits aligned. Without one the task is under way and cannot say how far
23
+ * — the server has not counted the rows yet — and the bar SAYS so instead of inventing a
24
+ * number: a segment sweeps along the track and no figure is drawn. That sweep is the only
25
+ * loop of the system (`--animate-progress-sweep`), and it runs behind `motion-safe:`: with
26
+ * `prefers-reduced-motion` the segment stands still, the whole track faded — «busy» without
27
+ * movement, and without a fill that could be read as a percentage.
28
+ *
29
+ * React Aria writes `role="progressbar"` and the `aria-value*` — no `aria-valuenow` while it
30
+ * is indeterminate, which is how a screen reader tells the two apart — and formats the figure
31
+ * in the copy table's locale (`AriaLocale`).
32
+ *
33
+ * The track is the system's `--size-track`: at 3 px it was a line that could not be told
34
+ * from a separator; two bars of different thickness on one screen read as two things.
35
+ */
36
+ const FILL = {
37
+ neutral: "bg-muted",
38
+ info: "bg-info",
39
+ good: "bg-good",
40
+ warn: "bg-warn",
41
+ bad: "bg-bad",
42
+ accent: "bg-accent",
43
+ };
44
+ export function ProgressBar({ value, label, minValue = 0, maxValue = 100, formatOptions, valueLabel, showLabel = false, showValue = true, width = "w-24", tone = "brand", }) {
45
+ const fill = tone === "brand" ? "bar-brand" : FILL[tone];
46
+ return (_jsx(AriaLocale, { children: _jsx(AriaProgressBar, { "aria-label": label, ...(value === undefined ? {} : { value }), isIndeterminate: value === undefined, minValue: minValue, maxValue: maxValue, formatOptions: formatOptions, valueLabel: valueLabel, className: showLabel
47
+ ? `flex max-w-full flex-col gap-1.5 ${width}`
48
+ : "flex min-w-0 items-center gap-2", children: ({ percentage, valueText, isIndeterminate }) => {
49
+ const figure = showValue && valueText !== undefined ? (_jsx("span", { className: "shrink-0 text-meta tabular-nums text-muted", children: valueText })) : null;
50
+ const track = (_jsx("span", { className: cn("relative block h-(--size-track) overflow-hidden rounded-pill bg-ink/10 forced-colors:outline forced-colors:outline-[ButtonBorder]", showLabel ? "w-full" : width), children: isIndeterminate ? (_jsx("span", { "data-indeterminate": true, className: cn("absolute inset-y-0 left-0 w-2/5 rounded-pill forced-colors:bg-[Highlight]", "motion-safe:animate-progress-sweep motion-reduce:w-full motion-reduce:opacity-40", fill) })) : (_jsx("span", { className: cn("block h-full rounded-pill transition-[width] duration-(--duration-panel) ease-(--ease-out) forced-colors:bg-[Highlight]", fill), style: { width: `${percentage ?? 0}%` } })) }));
51
+ return showLabel ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "flex items-baseline justify-between gap-2", children: [_jsx("span", { className: "truncate text-meta text-ink", children: label }), figure] }), track] })) : (_jsxs(_Fragment, { children: [track, figure] }));
52
+ } }) }));
53
+ }
@@ -1,5 +1,6 @@
1
- import type { Tone } from "../../foundations/types.js";
1
+ import type { OpenTarget, Tone } from "../../foundations/types.js";
2
2
  import { type FormMode } from "../field/mode.js";
3
+ import { type PersonContact } from "./contact.js";
3
4
  import type { LucideIcon } from "lucide-react";
4
5
  import { type ReactNode } from "react";
5
6
  /** What the floating summary of the related record says. */
@@ -18,16 +19,22 @@ export type RelationSummary = {
18
19
  value: ReactNode;
19
20
  }[];
20
21
  };
22
+ /** The picture of the related record: a photo or an icon on a square tile — or, for a person
23
+ * (`person`), the round face: the photo, or the initials in the color that person has
24
+ * everywhere. */
21
25
  type RelationMedia = {
22
26
  src?: string;
23
27
  alt?: string;
24
28
  icon?: LucideIcon;
29
+ person?: true;
25
30
  };
26
31
  export type RelationCardProps = {
27
32
  name: string;
28
33
  secondary?: ReactNode;
29
34
  media?: RelationMedia;
30
35
  summary?: RelationSummary;
36
+ /** How to reach it — a person, a company: one row per way (write, call, WhatsApp). */
37
+ contact?: PersonContact;
31
38
  /** «Abrir»: go to the related record. */
32
39
  onOpen?: () => void;
33
40
  /** The record's address, when it has one: «Abrir» becomes a real link. */
@@ -39,9 +46,10 @@ export type RelationCardProps = {
39
46
  * Context: it answers the question a person has about the customer of an invoice without
40
47
  * leaving the invoice — «does it owe us?», «what is its NIT?» — and, when the answer is
41
48
  * «I need more», one press takes them to the record. It is what floats from a
42
- * `RelationValue`, and it is exported on its own for a side panel.
49
+ * `RelationValue` and what opens from a `UserChip` (a person is a related record too: round
50
+ * face, role, the ways to reach them), and it is exported on its own for a side panel.
43
51
  */
44
- export declare function RelationCard({ name, secondary, media, summary, onOpen, href, }: RelationCardProps): import("react").JSX.Element;
52
+ export declare function RelationCard({ name, secondary, media, summary, contact, onOpen, href, }: RelationCardProps): import("react").JSX.Element;
45
53
  export type RelationValueProps = {
46
54
  /** The related record's name. Empty, `null` or absent = no record: said, in muted. */
47
55
  name?: string | null;
@@ -51,10 +59,19 @@ export type RelationValueProps = {
51
59
  media?: RelationMedia;
52
60
  /** Go to the related record. The name becomes a link. */
53
61
  onOpen?: () => void;
62
+ /** Go to it in the place the person chose (`OpenTarget`): the name opens in the first of
63
+ * `openTargets` — a modal by default, the form stays behind — and «Abrir en…» beside it offers
64
+ * the rest. Takes over from `onOpen`. */
65
+ onOpenIn?: (to: OpenTarget) => void;
66
+ openTargets?: readonly OpenTarget[];
54
67
  /** The related record's address: the name becomes a real `<a>` (middle-click, copy). */
55
68
  href?: string;
56
- /** Present = an info button next to the name opens the `RelationCard`. */
69
+ /** The facts of the related record. Present = its `RelationCard` floats from the name
70
+ * (hover, focus, long press); from an info button when the name is not a link. */
57
71
  summary?: RelationSummary;
72
+ /** What floats instead of the `RelationCard` — the product of a line, a consumer's own
73
+ * card. `true` floats the card even without `summary`; `false` floats nothing. */
74
+ preview?: boolean | ReactNode;
58
75
  /** The control that EDITS this relation — a `Picker` — drawn instead in edit mode. */
59
76
  editor?: ReactNode;
60
77
  /** Forces the mode; by default the form's. Only matters with `editor`. */
@@ -65,5 +82,5 @@ export type RelationValueProps = {
65
82
  * picture, and its summary one press away. Works in a table cell, a list row, a `Record.Facts`
66
83
  * value and a form field (read mode; pass the `Picker` as `editor` for edit mode).
67
84
  */
68
- export declare function RelationValue({ name, secondary, media, onOpen, href, summary, editor, mode, }: RelationValueProps): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
85
+ export declare function RelationValue({ name, secondary, media, onOpen: onOpenOwn, onOpenIn, openTargets, href, summary, preview, editor, mode, }: RelationValueProps): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
69
86
  export {};