@vaam-apps/ui 0.1.0 → 0.1.1

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 (283) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/components/data/code.d.ts +0 -1
  3. package/dist/components/data/code.js +0 -1
  4. package/dist/components/data/copy-button.d.ts +0 -1
  5. package/dist/components/data/copy-button.js +0 -1
  6. package/dist/components/data/detail-row.d.ts +0 -1
  7. package/dist/components/data/detail-row.js +0 -1
  8. package/dist/components/data/id-display.d.ts +0 -1
  9. package/dist/components/data/id-display.js +0 -1
  10. package/dist/components/data/masked-value.d.ts +0 -1
  11. package/dist/components/data/masked-value.js +0 -1
  12. package/dist/components/data/money.d.ts +0 -1
  13. package/dist/components/data/money.js +0 -1
  14. package/dist/components/data/phone-display.d.ts +0 -1
  15. package/dist/components/data/phone-display.js +0 -1
  16. package/dist/components/data/stat-tile.d.ts +0 -1
  17. package/dist/components/data/stat-tile.js +0 -1
  18. package/dist/components/data/timestamp-display.d.ts +0 -1
  19. package/dist/components/data/timestamp-display.js +0 -1
  20. package/dist/components/patterns/inline-banner.d.ts +0 -1
  21. package/dist/components/patterns/inline-banner.js +0 -1
  22. package/dist/components/patterns/inline-empty-state.d.ts +0 -1
  23. package/dist/components/patterns/inline-empty-state.js +0 -1
  24. package/dist/components/patterns/live-row.d.ts +0 -1
  25. package/dist/components/patterns/live-row.js +0 -1
  26. package/dist/components/patterns/payload-inspector.d.ts +0 -1
  27. package/dist/components/patterns/payload-inspector.js +0 -1
  28. package/dist/components/patterns/route-skeleton.d.ts +0 -1
  29. package/dist/components/patterns/route-skeleton.js +0 -1
  30. package/dist/components/patterns/screen-layout.d.ts +0 -1
  31. package/dist/components/patterns/screen-layout.js +0 -1
  32. package/dist/components/patterns/stale-write-banner.d.ts +0 -1
  33. package/dist/components/patterns/stale-write-banner.js +0 -1
  34. package/dist/components/patterns/state-timeline.d.ts +0 -1
  35. package/dist/components/patterns/state-timeline.js +0 -1
  36. package/dist/components/primitives/badge.d.ts +0 -1
  37. package/dist/components/primitives/badge.js +0 -1
  38. package/dist/components/primitives/button.d.ts +0 -1
  39. package/dist/components/primitives/button.js +0 -1
  40. package/dist/components/primitives/calendar.d.ts +0 -1
  41. package/dist/components/primitives/calendar.js +0 -1
  42. package/dist/components/primitives/card.d.ts +0 -1
  43. package/dist/components/primitives/card.js +0 -1
  44. package/dist/components/primitives/checkbox.d.ts +0 -1
  45. package/dist/components/primitives/checkbox.js +0 -1
  46. package/dist/components/primitives/chip-select.d.ts +0 -1
  47. package/dist/components/primitives/chip-select.js +0 -1
  48. package/dist/components/primitives/command-menu.d.ts +0 -1
  49. package/dist/components/primitives/command-menu.js +0 -1
  50. package/dist/components/primitives/confirm-dialog.d.ts +0 -1
  51. package/dist/components/primitives/confirm-dialog.js +0 -1
  52. package/dist/components/primitives/date-picker.d.ts +0 -1
  53. package/dist/components/primitives/date-picker.js +0 -1
  54. package/dist/components/primitives/dialog.d.ts +0 -1
  55. package/dist/components/primitives/dialog.js +0 -1
  56. package/dist/components/primitives/drawer.d.ts +0 -1
  57. package/dist/components/primitives/drawer.js +0 -1
  58. package/dist/components/primitives/dropdown-menu.d.ts +0 -1
  59. package/dist/components/primitives/dropdown-menu.js +0 -1
  60. package/dist/components/primitives/form-field.d.ts +0 -1
  61. package/dist/components/primitives/form-field.js +0 -1
  62. package/dist/components/primitives/inline-confirm.d.ts +0 -1
  63. package/dist/components/primitives/inline-confirm.js +0 -1
  64. package/dist/components/primitives/input.d.ts +0 -1
  65. package/dist/components/primitives/input.js +0 -1
  66. package/dist/components/primitives/label.d.ts +0 -1
  67. package/dist/components/primitives/label.js +0 -1
  68. package/dist/components/primitives/pagination.d.ts +0 -1
  69. package/dist/components/primitives/pagination.js +0 -1
  70. package/dist/components/primitives/popover.d.ts +0 -1
  71. package/dist/components/primitives/popover.js +0 -1
  72. package/dist/components/primitives/progress.d.ts +0 -1
  73. package/dist/components/primitives/progress.js +0 -1
  74. package/dist/components/primitives/radio-group.d.ts +0 -1
  75. package/dist/components/primitives/radio-group.js +0 -1
  76. package/dist/components/primitives/select.d.ts +0 -1
  77. package/dist/components/primitives/select.js +0 -1
  78. package/dist/components/primitives/separator.d.ts +0 -1
  79. package/dist/components/primitives/separator.js +0 -1
  80. package/dist/components/primitives/side-nav.d.ts +0 -1
  81. package/dist/components/primitives/side-nav.js +0 -1
  82. package/dist/components/primitives/skeleton.d.ts +0 -1
  83. package/dist/components/primitives/skeleton.js +0 -1
  84. package/dist/components/primitives/spinner.d.ts +0 -1
  85. package/dist/components/primitives/spinner.js +0 -1
  86. package/dist/components/primitives/switch.d.ts +0 -1
  87. package/dist/components/primitives/switch.js +0 -1
  88. package/dist/components/primitives/table.d.ts +0 -1
  89. package/dist/components/primitives/table.js +0 -1
  90. package/dist/components/primitives/tabs.d.ts +0 -1
  91. package/dist/components/primitives/tabs.js +0 -1
  92. package/dist/components/primitives/textarea.d.ts +0 -1
  93. package/dist/components/primitives/textarea.js +0 -1
  94. package/dist/components/primitives/toast.d.ts +0 -1
  95. package/dist/components/primitives/toast.js +0 -1
  96. package/dist/components/primitives/tooltip.d.ts +0 -1
  97. package/dist/components/primitives/tooltip.js +0 -1
  98. package/dist/components/status/state-chip.d.ts +0 -1
  99. package/dist/components/status/state-chip.js +0 -1
  100. package/dist/components/status/state-mark.d.ts +0 -1
  101. package/dist/components/status/state-mark.js +0 -1
  102. package/dist/components/status/status-pill.d.ts +0 -1
  103. package/dist/components/status/status-pill.js +0 -1
  104. package/dist/components/status/status-tokens.d.ts +0 -1
  105. package/dist/components/status/status-tokens.js +0 -1
  106. package/dist/index.d.ts +0 -1
  107. package/dist/index.js +0 -1
  108. package/dist/lib/cn.d.ts +0 -1
  109. package/dist/lib/cn.js +0 -1
  110. package/dist/lib/money.d.ts +0 -1
  111. package/dist/lib/money.js +0 -1
  112. package/dist/lib/omit-undefined.d.ts +0 -1
  113. package/dist/lib/omit-undefined.js +0 -1
  114. package/package.json +3 -7
  115. package/dist/components/data/code.d.ts.map +0 -1
  116. package/dist/components/data/code.js.map +0 -1
  117. package/dist/components/data/copy-button.d.ts.map +0 -1
  118. package/dist/components/data/copy-button.js.map +0 -1
  119. package/dist/components/data/detail-row.d.ts.map +0 -1
  120. package/dist/components/data/detail-row.js.map +0 -1
  121. package/dist/components/data/id-display.d.ts.map +0 -1
  122. package/dist/components/data/id-display.js.map +0 -1
  123. package/dist/components/data/masked-value.d.ts.map +0 -1
  124. package/dist/components/data/masked-value.js.map +0 -1
  125. package/dist/components/data/money.d.ts.map +0 -1
  126. package/dist/components/data/money.js.map +0 -1
  127. package/dist/components/data/phone-display.d.ts.map +0 -1
  128. package/dist/components/data/phone-display.js.map +0 -1
  129. package/dist/components/data/stat-tile.d.ts.map +0 -1
  130. package/dist/components/data/stat-tile.js.map +0 -1
  131. package/dist/components/data/timestamp-display.d.ts.map +0 -1
  132. package/dist/components/data/timestamp-display.js.map +0 -1
  133. package/dist/components/patterns/inline-banner.d.ts.map +0 -1
  134. package/dist/components/patterns/inline-banner.js.map +0 -1
  135. package/dist/components/patterns/inline-empty-state.d.ts.map +0 -1
  136. package/dist/components/patterns/inline-empty-state.js.map +0 -1
  137. package/dist/components/patterns/live-row.d.ts.map +0 -1
  138. package/dist/components/patterns/live-row.js.map +0 -1
  139. package/dist/components/patterns/payload-inspector.d.ts.map +0 -1
  140. package/dist/components/patterns/payload-inspector.js.map +0 -1
  141. package/dist/components/patterns/route-skeleton.d.ts.map +0 -1
  142. package/dist/components/patterns/route-skeleton.js.map +0 -1
  143. package/dist/components/patterns/screen-layout.d.ts.map +0 -1
  144. package/dist/components/patterns/screen-layout.js.map +0 -1
  145. package/dist/components/patterns/stale-write-banner.d.ts.map +0 -1
  146. package/dist/components/patterns/stale-write-banner.js.map +0 -1
  147. package/dist/components/patterns/state-timeline.d.ts.map +0 -1
  148. package/dist/components/patterns/state-timeline.js.map +0 -1
  149. package/dist/components/primitives/badge.d.ts.map +0 -1
  150. package/dist/components/primitives/badge.js.map +0 -1
  151. package/dist/components/primitives/button.d.ts.map +0 -1
  152. package/dist/components/primitives/button.js.map +0 -1
  153. package/dist/components/primitives/calendar.d.ts.map +0 -1
  154. package/dist/components/primitives/calendar.js.map +0 -1
  155. package/dist/components/primitives/card.d.ts.map +0 -1
  156. package/dist/components/primitives/card.js.map +0 -1
  157. package/dist/components/primitives/checkbox.d.ts.map +0 -1
  158. package/dist/components/primitives/checkbox.js.map +0 -1
  159. package/dist/components/primitives/chip-select.d.ts.map +0 -1
  160. package/dist/components/primitives/chip-select.js.map +0 -1
  161. package/dist/components/primitives/command-menu.d.ts.map +0 -1
  162. package/dist/components/primitives/command-menu.js.map +0 -1
  163. package/dist/components/primitives/confirm-dialog.d.ts.map +0 -1
  164. package/dist/components/primitives/confirm-dialog.js.map +0 -1
  165. package/dist/components/primitives/date-picker.d.ts.map +0 -1
  166. package/dist/components/primitives/date-picker.js.map +0 -1
  167. package/dist/components/primitives/dialog.d.ts.map +0 -1
  168. package/dist/components/primitives/dialog.js.map +0 -1
  169. package/dist/components/primitives/drawer.d.ts.map +0 -1
  170. package/dist/components/primitives/drawer.js.map +0 -1
  171. package/dist/components/primitives/dropdown-menu.d.ts.map +0 -1
  172. package/dist/components/primitives/dropdown-menu.js.map +0 -1
  173. package/dist/components/primitives/form-field.d.ts.map +0 -1
  174. package/dist/components/primitives/form-field.js.map +0 -1
  175. package/dist/components/primitives/inline-confirm.d.ts.map +0 -1
  176. package/dist/components/primitives/inline-confirm.js.map +0 -1
  177. package/dist/components/primitives/input.d.ts.map +0 -1
  178. package/dist/components/primitives/input.js.map +0 -1
  179. package/dist/components/primitives/label.d.ts.map +0 -1
  180. package/dist/components/primitives/label.js.map +0 -1
  181. package/dist/components/primitives/pagination.d.ts.map +0 -1
  182. package/dist/components/primitives/pagination.js.map +0 -1
  183. package/dist/components/primitives/popover.d.ts.map +0 -1
  184. package/dist/components/primitives/popover.js.map +0 -1
  185. package/dist/components/primitives/progress.d.ts.map +0 -1
  186. package/dist/components/primitives/progress.js.map +0 -1
  187. package/dist/components/primitives/radio-group.d.ts.map +0 -1
  188. package/dist/components/primitives/radio-group.js.map +0 -1
  189. package/dist/components/primitives/select.d.ts.map +0 -1
  190. package/dist/components/primitives/select.js.map +0 -1
  191. package/dist/components/primitives/separator.d.ts.map +0 -1
  192. package/dist/components/primitives/separator.js.map +0 -1
  193. package/dist/components/primitives/side-nav.d.ts.map +0 -1
  194. package/dist/components/primitives/side-nav.js.map +0 -1
  195. package/dist/components/primitives/skeleton.d.ts.map +0 -1
  196. package/dist/components/primitives/skeleton.js.map +0 -1
  197. package/dist/components/primitives/spinner.d.ts.map +0 -1
  198. package/dist/components/primitives/spinner.js.map +0 -1
  199. package/dist/components/primitives/switch.d.ts.map +0 -1
  200. package/dist/components/primitives/switch.js.map +0 -1
  201. package/dist/components/primitives/table.d.ts.map +0 -1
  202. package/dist/components/primitives/table.js.map +0 -1
  203. package/dist/components/primitives/tabs.d.ts.map +0 -1
  204. package/dist/components/primitives/tabs.js.map +0 -1
  205. package/dist/components/primitives/textarea.d.ts.map +0 -1
  206. package/dist/components/primitives/textarea.js.map +0 -1
  207. package/dist/components/primitives/toast.d.ts.map +0 -1
  208. package/dist/components/primitives/toast.js.map +0 -1
  209. package/dist/components/primitives/tooltip.d.ts.map +0 -1
  210. package/dist/components/primitives/tooltip.js.map +0 -1
  211. package/dist/components/status/state-chip.d.ts.map +0 -1
  212. package/dist/components/status/state-chip.js.map +0 -1
  213. package/dist/components/status/state-mark.d.ts.map +0 -1
  214. package/dist/components/status/state-mark.js.map +0 -1
  215. package/dist/components/status/status-pill.d.ts.map +0 -1
  216. package/dist/components/status/status-pill.js.map +0 -1
  217. package/dist/components/status/status-tokens.d.ts.map +0 -1
  218. package/dist/components/status/status-tokens.js.map +0 -1
  219. package/dist/index.d.ts.map +0 -1
  220. package/dist/index.js.map +0 -1
  221. package/dist/lib/cn.d.ts.map +0 -1
  222. package/dist/lib/cn.js.map +0 -1
  223. package/dist/lib/money.d.ts.map +0 -1
  224. package/dist/lib/money.js.map +0 -1
  225. package/dist/lib/omit-undefined.d.ts.map +0 -1
  226. package/dist/lib/omit-undefined.js.map +0 -1
  227. package/src/components/data/code.tsx +0 -43
  228. package/src/components/data/copy-button.tsx +0 -97
  229. package/src/components/data/detail-row.tsx +0 -113
  230. package/src/components/data/id-display.tsx +0 -49
  231. package/src/components/data/masked-value.tsx +0 -96
  232. package/src/components/data/money.tsx +0 -46
  233. package/src/components/data/phone-display.tsx +0 -60
  234. package/src/components/data/stat-tile.tsx +0 -68
  235. package/src/components/data/timestamp-display.tsx +0 -106
  236. package/src/components/patterns/inline-banner.tsx +0 -60
  237. package/src/components/patterns/inline-empty-state.tsx +0 -45
  238. package/src/components/patterns/live-row.tsx +0 -75
  239. package/src/components/patterns/payload-inspector.tsx +0 -157
  240. package/src/components/patterns/route-skeleton.tsx +0 -77
  241. package/src/components/patterns/screen-layout.tsx +0 -44
  242. package/src/components/patterns/stale-write-banner.tsx +0 -44
  243. package/src/components/patterns/state-timeline.tsx +0 -229
  244. package/src/components/primitives/badge.tsx +0 -40
  245. package/src/components/primitives/button.tsx +0 -54
  246. package/src/components/primitives/calendar.tsx +0 -136
  247. package/src/components/primitives/card.tsx +0 -70
  248. package/src/components/primitives/checkbox.tsx +0 -101
  249. package/src/components/primitives/chip-select.tsx +0 -105
  250. package/src/components/primitives/command-menu.tsx +0 -92
  251. package/src/components/primitives/confirm-dialog.tsx +0 -109
  252. package/src/components/primitives/date-picker.tsx +0 -271
  253. package/src/components/primitives/dialog.tsx +0 -195
  254. package/src/components/primitives/drawer.tsx +0 -326
  255. package/src/components/primitives/dropdown-menu.tsx +0 -128
  256. package/src/components/primitives/form-field.tsx +0 -136
  257. package/src/components/primitives/inline-confirm.tsx +0 -118
  258. package/src/components/primitives/input.tsx +0 -21
  259. package/src/components/primitives/label.tsx +0 -43
  260. package/src/components/primitives/pagination.tsx +0 -110
  261. package/src/components/primitives/popover.tsx +0 -41
  262. package/src/components/primitives/progress.tsx +0 -66
  263. package/src/components/primitives/radio-group.tsx +0 -90
  264. package/src/components/primitives/select.tsx +0 -254
  265. package/src/components/primitives/separator.tsx +0 -49
  266. package/src/components/primitives/side-nav.tsx +0 -253
  267. package/src/components/primitives/skeleton.tsx +0 -25
  268. package/src/components/primitives/spinner.tsx +0 -39
  269. package/src/components/primitives/switch.tsx +0 -74
  270. package/src/components/primitives/table.tsx +0 -157
  271. package/src/components/primitives/tabs.tsx +0 -163
  272. package/src/components/primitives/textarea.tsx +0 -21
  273. package/src/components/primitives/toast.tsx +0 -117
  274. package/src/components/primitives/tooltip.tsx +0 -38
  275. package/src/components/status/state-chip.tsx +0 -70
  276. package/src/components/status/state-mark.tsx +0 -154
  277. package/src/components/status/status-pill.tsx +0 -153
  278. package/src/components/status/status-tokens.ts +0 -173
  279. package/src/index.ts +0 -96
  280. package/src/lib/cn.ts +0 -168
  281. package/src/lib/money.ts +0 -164
  282. package/src/lib/omit-undefined.ts +0 -25
  283. /package/{src → dist}/styles/theme.css +0 -0
@@ -1,96 +0,0 @@
1
- "use client";
2
-
3
- import { Eye, EyeOff } from "lucide-react";
4
- import { useState } from "react";
5
- import { cn } from "../../lib/cn";
6
- import { CopyButton } from "./copy-button";
7
-
8
- export interface MaskedValueProps {
9
- /** The full value. Present in the DOM only while revealed. */
10
- value: string;
11
- /** Trailing characters left visible while masked, so the value stays
12
- * recognisable without being readable. `0` masks everything. */
13
- reveal?: number;
14
- /** Leading characters left visible — for a prefixed credential like
15
- * `whsec_…` or `sk_live_…`, where the prefix says what kind of thing it
16
- * is and is not itself secret. */
17
- prefix?: number;
18
- /** Allow revealing at all. `false` gives a permanently masked value with
19
- * no toggle — for something displayed for recognition only. */
20
- revealable?: boolean;
21
- /** Offer a copy button. Copies the full value whether or not it is
22
- * currently revealed — the point of masking is the screen, not the
23
- * clipboard. */
24
- copyable?: boolean;
25
- /** Names the value in the toggle's and copy button's accessible names,
26
- * e.g. `"signing secret"`. */
27
- label?: string;
28
- className?: string | undefined;
29
- }
30
-
31
- const DOT = "•";
32
-
33
- function mask(value: string, prefix: number, reveal: number): string {
34
- const head = value.slice(0, prefix);
35
- const tail = reveal > 0 ? value.slice(-reveal) : "";
36
- const hiddenCount = Math.max(value.length - head.length - tail.length, 0);
37
- // A fixed run of dots, not one per character: the exact length of a
38
- // secret is information, and padding it out to the real length leaks it
39
- // to anyone looking over a shoulder.
40
- const dots = DOT.repeat(hiddenCount === 0 ? 0 : Math.min(Math.max(hiddenCount, 6), 12));
41
- return `${head}${dots}${tail}`;
42
- }
43
-
44
- /**
45
- * A secret or sensitive value, masked by default with an explicit reveal.
46
- *
47
- * # What this is and is not
48
- *
49
- * It is a **shoulder-surfing and screen-share control**, not a security
50
- * boundary. Whoever renders this already received the value over the
51
- * wire; anyone with the session and dev tools can read it regardless.
52
- * Withholding data that has already crossed the wire to an authorised,
53
- * authenticated session would be theatre. What masking buys is real but
54
- * narrow: the value does not end up in a screenshot, a recording, or the
55
- * eyeline of the person sitting behind the operator.
56
- *
57
- * If a value must genuinely never reach the client, that is an API
58
- * decision — do not send it — and no component can substitute for it.
59
- *
60
- * # Why it is shared
61
- *
62
- * Both applications this package serves had hand-rolled one: a console
63
- * masking a webhook signing secret as `whsec_••••••••••<last 4>`, and a
64
- * payments dashboard rendering a masked payer reference and a TOTP
65
- * enrolment secret as plain text because no such component existed.
66
- */
67
- export function MaskedValue({
68
- value,
69
- reveal = 4,
70
- prefix = 0,
71
- revealable = true,
72
- copyable = true,
73
- label = "value",
74
- className,
75
- }: MaskedValueProps) {
76
- const [revealed, setRevealed] = useState(false);
77
- const shown = revealed ? value : mask(value, prefix, reveal);
78
-
79
- return (
80
- <span className={cn("group inline-flex items-center gap-1.5", className)}>
81
- <span className={cn("font-mono", revealed && "select-all break-all")}>{shown}</span>
82
- {revealable && (
83
- <button
84
- type="button"
85
- onClick={() => setRevealed((previous) => !previous)}
86
- aria-label={revealed ? `Hide ${label}` : `Reveal ${label}`}
87
- aria-pressed={revealed}
88
- className="shrink-0 text-subtle-foreground transition-colors hover:text-foreground"
89
- >
90
- {revealed ? <EyeOff size={12} strokeWidth={1.5} /> : <Eye size={12} strokeWidth={1.5} />}
91
- </button>
92
- )}
93
- {copyable && <CopyButton value={value} label={`Copy ${label}`} />}
94
- </span>
95
- );
96
- }
@@ -1,46 +0,0 @@
1
- import { cn } from "../../lib/cn";
2
- import { type FormatMoneyOptions, formatMoney, type MinorUnits } from "../../lib/money";
3
-
4
- export interface MoneyProps extends FormatMoneyOptions {
5
- /** An integer count of the currency's smallest unit. */
6
- amount: MinorUnits;
7
- /** ISO 4217 code, e.g. `"XAF"`. */
8
- currency: string;
9
- /** Tint by sign: credits green, debits red. Off by default — in a
10
- * ledger where most rows are one direction, colouring every row is
11
- * noise, and colour alone is not an accessible way to convey sign. The
12
- * minus sign is always rendered regardless. */
13
- tone?: "none" | "signed";
14
- className?: string | undefined;
15
- }
16
-
17
- /**
18
- * An amount of money, right-alignable and vertically comparable.
19
- *
20
- * `tabular-nums` is the whole reason this is a component and not a call to
21
- * [`formatMoney`] in a `<span>`: without fixed-width digits, a column of
22
- * amounts does not line up at the decimal point, and scanning it for an
23
- * outlier — the actual job — stops working.
24
- *
25
- * `<data value>` rather than `<span>`: the machine-readable amount stays
26
- * in the DOM in minor units, so a copy-paste or a scraper gets the exact
27
- * integer rather than a locale-formatted string it would have to parse
28
- * back.
29
- */
30
- export function Money({ amount, currency, tone = "none", className, ...options }: MoneyProps) {
31
- const formatted = formatMoney(amount, currency, options);
32
- const negative = String(amount).trim().startsWith("-");
33
-
34
- return (
35
- <data
36
- value={`${String(amount)} ${currency}`}
37
- className={cn(
38
- "font-mono tabular-nums whitespace-nowrap",
39
- tone === "signed" && (negative ? "text-state-danger-fg" : "text-state-success-fg"),
40
- className,
41
- )}
42
- >
43
- {formatted}
44
- </data>
45
- );
46
- }
@@ -1,60 +0,0 @@
1
- import { cn } from "../../lib/cn";
2
- import { CopyButton } from "./copy-button";
3
-
4
- export interface PhoneDisplayProps {
5
- /** E.164, e.g. `+237677123456`. Stored form, and what gets copied. */
6
- value: string;
7
- /**
8
- * Regroups the E.164 digits for reading. Return `null` — or omit the
9
- * prop — to render the value verbatim.
10
- *
11
- * Grouping is a per-country convention (`+237 6 77 12 34 56` in
12
- * Cameroon, `+1 (415) 555-0132` in the NANP) and getting it wrong is
13
- * worse than not grouping at all, because a mis-grouped number reads as
14
- * a different number. This component therefore ships no default
15
- * grouping and no built-in country table: a caller that knows its
16
- * market passes a formatter, and everyone else gets the raw E.164,
17
- * which is always correct if less pretty.
18
- */
19
- format?: ((e164: string) => string | null) | undefined;
20
- /**
21
- * A short mono tag after the number — a carrier, a line type, a region.
22
- * Renders an em dash when absent, so a column of numbers stays aligned
23
- * whether or not each row could be classified.
24
- */
25
- tag?: string | undefined;
26
- /** Hover text for the tag. Say where it came from and how much to trust
27
- * it: a carrier inferred from a number prefix is a guess, and number
28
- * portability means it is wrong for a real fraction of subscribers. */
29
- tagTitle?: string | undefined;
30
- className?: string | undefined;
31
- }
32
-
33
- /**
34
- * A phone number, never truncated.
35
- *
36
- * Truncation is banned rather than discouraged: a half-shown phone number
37
- * is worse than useless in an investigation, and unlike an opaque id
38
- * (where a prefix is enough to match rows by eye) there is no prefix of a
39
- * phone number that identifies it.
40
- *
41
- * Copy always copies the raw E.164 — the form that pastes into a query —
42
- * not the grouped form on screen.
43
- *
44
- * Previously this was `MsisdnDisplay`, with Cameroon's `+237` grouping and
45
- * a four-entry Cameroonian carrier table compiled in. Both are now the
46
- * caller's: an application knows its market, a component library does not.
47
- */
48
- export function PhoneDisplay({ value, format, tag, tagTitle, className }: PhoneDisplayProps) {
49
- const formatted = format?.(value) ?? value;
50
-
51
- return (
52
- <span className={cn("group inline-flex items-center gap-1.5 whitespace-nowrap", className)}>
53
- <span className="font-mono tabular-nums">{formatted}</span>
54
- <span className="font-mono text-micro text-subtle-foreground" title={tagTitle}>
55
- {tag ?? "—"}
56
- </span>
57
- <CopyButton value={value} revealOnGroupHover />
58
- </span>
59
- );
60
- }
@@ -1,68 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import { cn } from "../../lib/cn";
3
- import type { StatusHue } from "../status/status-tokens";
4
- import { HUE_CLASSES } from "../status/status-tokens";
5
-
6
- export interface StatTileProps {
7
- label: ReactNode;
8
- /** The number itself. Pass a `Money` or a formatted string — this
9
- * component does no formatting, because a tile has no way to know
10
- * whether it is showing a count, a currency or a rate. */
11
- value: ReactNode;
12
- /** One line under the value: the comparison, the window, the caveat.
13
- * A bare number with no denominator is the most common way a dashboard
14
- * misleads, so this slot exists to make the denominator easy. */
15
- caption?: ReactNode;
16
- /** Tints the value. Leave unset unless the number's own colour carries
17
- * meaning — a wall of coloured tiles makes the one that matters harder
18
- * to find, which is the opposite of the point. */
19
- tone?: StatusHue | undefined;
20
- /** Top-right slot — a sparkline, a `StateChip`, a refresh button. */
21
- action?: ReactNode;
22
- className?: string | undefined;
23
- }
24
-
25
- /**
26
- * One headline number with its label and context.
27
- *
28
- * Borders, not shadows, and no card chrome of its own beyond a hairline:
29
- * tiles appear in rows of three to six, and six shadowed boxes is visual
30
- * noise where six outlined ones read as one instrument panel.
31
- *
32
- * `<dl>`/`<dt>`/`<dd>` rather than divs — a tile *is* a term and its
33
- * definition, and the semantics make a row of them navigable rather than
34
- * an undifferentiated wall of text to a screen reader.
35
- */
36
- export function StatTile({ label, value, caption, tone, action, className }: StatTileProps) {
37
- return (
38
- <div
39
- className={cn(
40
- "relative flex flex-col rounded-box border border-edge bg-base-300 p-4",
41
- className,
42
- )}
43
- >
44
- {/* Outside the `<dl>`, and absolutely positioned rather than a flex
45
- sibling of the `<dt>`. A `<dl>` may contain only `<dt>`, `<dd>`
46
- and `<div>`s that wrap dt/dd groups — an action button among
47
- them is invalid markup, which axe reports as a serious
48
- `definition-list` violation. Found by the a11y addon, after an
49
- earlier version nested the action in a header row inside the
50
- list. */}
51
- {action != null && <div className="absolute top-4 right-4">{action}</div>}
52
- <dl className="flex flex-col gap-1">
53
- <dt className={cn("text-caption text-muted-foreground", action != null && "pr-20")}>
54
- {label}
55
- </dt>
56
- <dd
57
- className={cn(
58
- "font-mono text-metric tabular-nums",
59
- tone === undefined ? "text-foreground" : HUE_CLASSES[tone].fg,
60
- )}
61
- >
62
- {value}
63
- </dd>
64
- {caption != null && <dd className="text-caption text-subtle-foreground">{caption}</dd>}
65
- </dl>
66
- </div>
67
- );
68
- }
@@ -1,106 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect, useState, useSyncExternalStore } from "react";
4
- import { cn } from "../../lib/cn";
5
-
6
- // A single shared 30s interval drives every `TimestampDisplay` instance's
7
- // relative-time re-render (design doc §7.2: "a table of 200 rows must not
8
- // run 200 timers"). Lazily started on the first mounted instance, torn
9
- // down when the last one unmounts.
10
-
11
- let tick = 0;
12
- const listeners = new Set<() => void>();
13
- let timer: ReturnType<typeof setInterval> | null = null;
14
-
15
- function subscribe(listener: () => void): () => void {
16
- listeners.add(listener);
17
- timer ??= setInterval(() => {
18
- tick += 1;
19
- for (const l of listeners) l();
20
- }, 30_000);
21
- return () => {
22
- listeners.delete(listener);
23
- if (listeners.size === 0 && timer !== null) {
24
- clearInterval(timer);
25
- timer = null;
26
- }
27
- };
28
- }
29
-
30
- function useSharedTick(): number {
31
- return useSyncExternalStore(
32
- subscribe,
33
- () => tick,
34
- () => 0,
35
- );
36
- }
37
-
38
- function pad(n: number): string {
39
- return String(n).padStart(2, "0");
40
- }
41
-
42
- /** `2026-08-08 14:03:07` + a zone suffix — ISO-ordered so it sorts and
43
- * compares visually (design doc §7.2). Only UTC is implemented (`Z`
44
- * suffix, always): the Africa/Douala toggle lives on the top bar, which
45
- * this screen doesn't build. "A bare local time with no zone label never
46
- * appears anywhere in this product" still holds — the suffix is always
47
- * present, just not yet switchable. */
48
- function formatAbsolute(date: Date): string {
49
- const y = date.getUTCFullYear();
50
- const mo = pad(date.getUTCMonth() + 1);
51
- const d = pad(date.getUTCDate());
52
- const h = pad(date.getUTCHours());
53
- const mi = pad(date.getUTCMinutes());
54
- const s = pad(date.getUTCSeconds());
55
- return `${y}-${mo}-${d} ${h}:${mi}:${s}Z`;
56
- }
57
-
58
- function formatRelative(diffMs: number): string {
59
- const minutes = Math.floor(diffMs / 60_000);
60
- if (minutes < 1) return "just now";
61
- if (minutes < 60) return `${minutes}m`;
62
- const hours = Math.floor(minutes / 60);
63
- return `${hours}h`;
64
- }
65
-
66
- export interface TimestampDisplayProps {
67
- /** ISO-8601. */
68
- value: string;
69
- className?: string;
70
- }
71
-
72
- /**
73
- * Relative for anything under 24h (`2m`, `47m`, `6h`), absolute otherwise
74
- * — design doc §7.2. Renders the absolute value on the server and on
75
- * first client render (so server/client markup matches and Next doesn't
76
- * warn/flash), then upgrades to relative once mounted, driven by the
77
- * shared interval above rather than its own timer.
78
- */
79
- export function TimestampDisplay({ value, className }: TimestampDisplayProps) {
80
- useSharedTick();
81
- const [hydrated, setHydrated] = useState(false);
82
- useEffect(() => setHydrated(true), []);
83
-
84
- const date = new Date(value);
85
- const absolute = formatAbsolute(date);
86
-
87
- if (!hydrated) {
88
- return (
89
- <span className={cn("font-mono text-subtle-foreground tabular-nums", className)}>
90
- {absolute}
91
- </span>
92
- );
93
- }
94
-
95
- const diffMs = Date.now() - date.getTime();
96
- const display = diffMs >= 0 && diffMs < 24 * 3600 * 1000 ? formatRelative(diffMs) : absolute;
97
-
98
- return (
99
- <span
100
- title={absolute}
101
- className={cn("font-mono text-subtle-foreground tabular-nums", className)}
102
- >
103
- {display}
104
- </span>
105
- );
106
- }
@@ -1,60 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import { cn } from "../../lib/cn";
3
-
4
- // R6 (AGENTS.md): the same three banner treatments — a neutral scope/notice
5
- // box, a danger error box, and a plain caption-only note — were hand-rolled
6
- // inline in every screen that needed one (`rounded-sm border border-edge
7
- // bg-surface-2 px-3 py-2 text-caption text-muted-foreground` for "neutral",
8
- // `border-state-danger-border bg-state-danger-bg ... text-state-danger-fg`
9
- // for "danger"). Classes moved here verbatim, parameterised by variant, not
10
- // redesigned. `InlineEmptyState` already set the precedent for this shape
11
- // of extraction (a status treatment repeated per-screen, given one home) —
12
- // this is the same move applied to a different repeated block.
13
- //
14
- // Not to be confused with `InlineEmptyState`: that one is for "there is
15
- // nothing to show here" inside a list; this one is for a standing notice or
16
- // an error above/around content that *is* showing.
17
-
18
- export interface InlineBannerProps {
19
- /** `'neutral'` — a standing notice (scope, caveat). `'danger'` — an
20
- * error. `'warning'` — recoverable, needs attention (a stale write).
21
- * `'success'` — a positive confirmation (a verified audit chain).
22
- * `'uncertain'` — degraded or unknown, neither failure nor success (a
23
- * stalled live feed, a UCS-2 volume spike). `'plain'` — a caption-only
24
- * note with no border or fill, for something that doesn't need the
25
- * weight of a bordered box.
26
- *
27
- * The last three were added after the R6 factorization, not with it:
28
- * four route groups had hand-rolled boxes in exactly these tones because
29
- * the variant they needed did not exist, and the agents doing the
30
- * factorization were deliberately barred from editing this package. Two
31
- * of them flagged the gap rather than inventing a competing component,
32
- * which is the behaviour the constraint was for. */
33
- variant?: "neutral" | "danger" | "warning" | "success" | "uncertain" | "plain";
34
- children: ReactNode;
35
- className?: string;
36
- }
37
-
38
- export function InlineBanner({ variant = "neutral", children, className }: InlineBannerProps) {
39
- return (
40
- <div
41
- className={cn(
42
- "text-caption",
43
- variant === "neutral" &&
44
- "rounded-sm border border-edge bg-surface-2 px-3 py-2 text-muted-foreground",
45
- variant === "danger" &&
46
- "rounded-sm border border-state-danger-border bg-state-danger-bg px-3 py-2 text-state-danger-fg",
47
- variant === "warning" &&
48
- "rounded-sm border border-state-warning-border bg-state-warning-bg px-3 py-2 text-state-warning-fg",
49
- variant === "success" &&
50
- "rounded-sm border border-state-success-border bg-state-success-bg px-3 py-2 text-state-success-fg",
51
- variant === "uncertain" &&
52
- "rounded-sm border border-state-uncertain-border bg-state-uncertain-bg px-3 py-2 text-state-uncertain-fg",
53
- variant === "plain" && "text-subtle-foreground",
54
- className,
55
- )}
56
- >
57
- {children}
58
- </div>
59
- );
60
- }
@@ -1,45 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import { cn } from "../../lib/cn";
3
-
4
- export interface InlineEmptyStateProps {
5
- message: ReactNode;
6
- action?: { label: string; onClick: () => void };
7
- /** `'standalone'` is the one exception (§5.3): a screen with nothing else
8
- * to do may centre a single line plus one primary action. Still no
9
- * illustration, still no card. */
10
- variant?: "inline" | "standalone";
11
- className?: string;
12
- }
13
-
14
- /**
15
- * Enforces the binding rule: empty states are inline status lines, not
16
- * centred placards. Use inside a table body (spanning all columns) or in
17
- * the panel where the missing list would be.
18
- */
19
- export function InlineEmptyState({
20
- message,
21
- action,
22
- variant = "inline",
23
- className,
24
- }: InlineEmptyStateProps) {
25
- return (
26
- <div
27
- className={cn(
28
- "flex items-center gap-3 text-body text-muted-foreground",
29
- variant === "standalone" ? "justify-center py-16 text-center" : "py-3 text-left",
30
- className,
31
- )}
32
- >
33
- <span>{message}</span>
34
- {action != null && (
35
- <button
36
- type="button"
37
- onClick={action.onClick}
38
- className="text-foreground underline decoration-edge-strong underline-offset-2 hover:decoration-foreground"
39
- >
40
- {action.label}
41
- </button>
42
- )}
43
- </div>
44
- );
45
- }
@@ -1,75 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect, useRef, useState } from "react";
4
- import { cn } from "../../lib/cn";
5
- import { TableRow, type TableRowProps } from "../primitives/table";
6
- import type { StatusHue } from "../status/status-tokens";
7
-
8
- export interface LiveRowProps extends TableRowProps {
9
- /** Any value that changes to trigger a wash — pass the row's `@version`, an exact change key immune to clock skew. */
10
- washTrigger: string | number;
11
- /** The destination state's hue, so the wash tints toward where the row landed. */
12
- washHue?: StatusHue;
13
- }
14
-
15
- const WASH_BG_CLASS: Record<StatusHue, string> = {
16
- neutral: "bg-state-neutral-fg/10",
17
- success: "bg-state-success-fg/10",
18
- warning: "bg-state-warning-fg/10",
19
- danger: "bg-state-danger-fg/10",
20
- uncertain: "bg-state-uncertain-fg/10",
21
- expired: "bg-state-expired-fg/10",
22
- parked: "bg-state-parked-fg/10",
23
- };
24
-
25
- /**
26
- * The row-level half of the design doc's `LiveTable`/`LiveRow` contract
27
- * (§5.3, §6.5 rule 4): "in-place status change never moves a row" — this
28
- * wraps `TableRow` with the 240ms wash-then-decay on a status change,
29
- * nothing else moves or resizes. The full `LiveTable` (scroll-position-
30
- * dependent buffered insertion, the sticky "N new" pill, sort-mode
31
- * switching) is a later, screen-level task — this is the reusable unit it
32
- * will be built on, shipped now so the wash behaviour isn't retrofitted
33
- * per-screen later.
34
- *
35
- * Reduced motion (§3.8 rule 3): the wash becomes a static hold instead of
36
- * a timed decay — the signal must survive, only the animation may not.
37
- */
38
- export function LiveRow({
39
- washTrigger,
40
- washHue = "neutral",
41
- className,
42
- children,
43
- ...props
44
- }: LiveRowProps) {
45
- const [washing, setWashing] = useState(false);
46
- const previousTrigger = useRef(washTrigger);
47
- const reducedMotion =
48
- typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
49
-
50
- useEffect(() => {
51
- if (previousTrigger.current === washTrigger) return;
52
- previousTrigger.current = washTrigger;
53
- setWashing(true);
54
- const holdMs = reducedMotion ? 1200 : 400 + 240 + 600;
55
- const timeout = setTimeout(() => setWashing(false), holdMs);
56
- return () => clearTimeout(timeout);
57
- }, [washTrigger, reducedMotion]);
58
-
59
- return (
60
- <TableRow
61
- className={cn(
62
- washing && [
63
- WASH_BG_CLASS[washHue],
64
- reducedMotion
65
- ? "transition-none"
66
- : "transition-colors duration-[var(--dur-state)] ease-out",
67
- ],
68
- className,
69
- )}
70
- {...props}
71
- >
72
- {children}
73
- </TableRow>
74
- );
75
- }