@vantageos/mosaic 0.4.0 → 0.5.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 (150) hide show
  1. package/CHANGELOG.md +39 -15
  2. package/README.md +114 -5
  3. package/dist/PipelineBoard-FucyGArR.d.cts +759 -0
  4. package/dist/PipelineBoard-FucyGArR.d.ts +759 -0
  5. package/dist/PipelineBoard.markdown-DIcvrS-2.d.cts +16 -0
  6. package/dist/PipelineBoard.markdown-DIcvrS-2.d.ts +16 -0
  7. package/dist/{StatusBadge.schema-1tOjB4Ir.d.cts → StatusBadge.schema-BEjSH2_4.d.cts} +2 -2
  8. package/dist/{StatusBadge.schema-1tOjB4Ir.d.ts → StatusBadge.schema-BEjSH2_4.d.ts} +2 -2
  9. package/dist/{Tabs.schema-BzcEyeq8.d.cts → Tabs.schema-Dqr6vwV3.d.cts} +2 -2
  10. package/dist/{Tabs.schema-BzcEyeq8.d.ts → Tabs.schema-Dqr6vwV3.d.ts} +2 -2
  11. package/dist/Timeline-Coo_3tA_.d.cts +114 -0
  12. package/dist/Timeline-Coo_3tA_.d.ts +114 -0
  13. package/dist/Timeline.markdown-C6UPDv1H.d.cts +4 -0
  14. package/dist/Timeline.markdown-C6UPDv1H.d.ts +4 -0
  15. package/dist/TransactionPreview-BzWDn8kE.d.cts +391 -0
  16. package/dist/TransactionPreview-BzWDn8kE.d.ts +391 -0
  17. package/dist/TransactionPreview.markdown-Dpb0VYgN.d.cts +7 -0
  18. package/dist/TransactionPreview.markdown-Dpb0VYgN.d.ts +7 -0
  19. package/dist/artifacts.cjs +186 -2
  20. package/dist/artifacts.cjs.map +1 -1
  21. package/dist/artifacts.js +186 -2
  22. package/dist/artifacts.js.map +1 -1
  23. package/dist/confirmation.cjs +724 -2
  24. package/dist/confirmation.cjs.map +1 -1
  25. package/dist/confirmation.d.cts +2 -1
  26. package/dist/confirmation.d.ts +2 -1
  27. package/dist/confirmation.js +722 -4
  28. package/dist/confirmation.js.map +1 -1
  29. package/dist/display.cjs +1551 -203
  30. package/dist/display.cjs.map +1 -1
  31. package/dist/display.d.cts +6 -229
  32. package/dist/display.d.ts +6 -229
  33. package/dist/display.js +1535 -206
  34. package/dist/display.js.map +1 -1
  35. package/dist/forms.cjs +139 -46
  36. package/dist/forms.cjs.map +1 -1
  37. package/dist/forms.d.cts +2 -2
  38. package/dist/forms.d.ts +2 -2
  39. package/dist/forms.js +139 -46
  40. package/dist/forms.js.map +1 -1
  41. package/dist/host.cjs +822 -0
  42. package/dist/host.cjs.map +1 -0
  43. package/dist/host.d.cts +287 -0
  44. package/dist/host.d.ts +287 -0
  45. package/dist/host.js +794 -0
  46. package/dist/host.js.map +1 -0
  47. package/dist/{index-CwXVgtGb.d.cts → index-BadRcCbm.d.cts} +28 -11
  48. package/dist/{index-CwXVgtGb.d.ts → index-BadRcCbm.d.ts} +28 -11
  49. package/dist/index.cjs +2198 -247
  50. package/dist/index.cjs.map +1 -1
  51. package/dist/index.d.cts +42 -8
  52. package/dist/index.d.ts +42 -8
  53. package/dist/index.js +2163 -251
  54. package/dist/index.js.map +1 -1
  55. package/dist/input.d.cts +2 -2
  56. package/dist/input.d.ts +2 -2
  57. package/dist/media.cjs +186 -2
  58. package/dist/media.cjs.map +1 -1
  59. package/dist/media.d.cts +1 -1
  60. package/dist/media.d.ts +1 -1
  61. package/dist/media.js +186 -2
  62. package/dist/media.js.map +1 -1
  63. package/dist/preact/artifacts.js +186 -2
  64. package/dist/preact/artifacts.js.map +1 -1
  65. package/dist/preact/confirmation.d.ts +321 -1
  66. package/dist/preact/confirmation.js +625 -4
  67. package/dist/preact/confirmation.js.map +1 -1
  68. package/dist/preact/display.d.ts +544 -47
  69. package/dist/preact/display.js +1190 -126
  70. package/dist/preact/display.js.map +1 -1
  71. package/dist/preact/forms.d.ts +32 -15
  72. package/dist/preact/forms.js +139 -46
  73. package/dist/preact/forms.js.map +1 -1
  74. package/dist/preact/host.d.ts +307 -0
  75. package/dist/preact/host.js +875 -0
  76. package/dist/preact/host.js.map +1 -0
  77. package/dist/preact/media.d.ts +37 -1
  78. package/dist/preact/media.js +423 -0
  79. package/dist/preact/media.js.map +1 -1
  80. package/dist/preact/progress.d.ts +92 -1
  81. package/dist/preact/progress.js +465 -3
  82. package/dist/preact/progress.js.map +1 -1
  83. package/dist/progress.cjs +521 -2
  84. package/dist/progress.cjs.map +1 -1
  85. package/dist/progress.d.cts +2 -1
  86. package/dist/progress.d.ts +2 -1
  87. package/dist/progress.js +518 -3
  88. package/dist/progress.js.map +1 -1
  89. package/dist/react/artifacts.cjs +186 -2
  90. package/dist/react/artifacts.cjs.map +1 -1
  91. package/dist/react/artifacts.js +186 -2
  92. package/dist/react/artifacts.js.map +1 -1
  93. package/dist/react/confirmation.cjs +626 -2
  94. package/dist/react/confirmation.cjs.map +1 -1
  95. package/dist/react/confirmation.d.cts +3 -3
  96. package/dist/react/confirmation.d.ts +3 -3
  97. package/dist/react/confirmation.js +625 -4
  98. package/dist/react/confirmation.js.map +1 -1
  99. package/dist/react/display.cjs +1076 -123
  100. package/dist/react/display.cjs.map +1 -1
  101. package/dist/react/display.d.cts +13 -27
  102. package/dist/react/display.d.ts +13 -27
  103. package/dist/react/display.js +1064 -126
  104. package/dist/react/display.js.map +1 -1
  105. package/dist/react/forms.cjs +139 -46
  106. package/dist/react/forms.cjs.map +1 -1
  107. package/dist/react/forms.d.cts +1 -1
  108. package/dist/react/forms.d.ts +1 -1
  109. package/dist/react/forms.js +139 -46
  110. package/dist/react/forms.js.map +1 -1
  111. package/dist/react/host.cjs +910 -0
  112. package/dist/react/host.cjs.map +1 -0
  113. package/dist/react/host.d.cts +24 -0
  114. package/dist/react/host.d.ts +24 -0
  115. package/dist/react/host.js +875 -0
  116. package/dist/react/host.js.map +1 -0
  117. package/dist/react/input.d.cts +1 -1
  118. package/dist/react/input.d.ts +1 -1
  119. package/dist/react/media.cjs +426 -0
  120. package/dist/react/media.cjs.map +1 -1
  121. package/dist/react/media.d.cts +19 -1
  122. package/dist/react/media.d.ts +19 -1
  123. package/dist/react/media.js +423 -0
  124. package/dist/react/media.js.map +1 -1
  125. package/dist/react/progress.cjs +467 -2
  126. package/dist/react/progress.cjs.map +1 -1
  127. package/dist/react/progress.d.cts +1 -1
  128. package/dist/react/progress.d.ts +1 -1
  129. package/dist/react/progress.js +465 -3
  130. package/dist/react/progress.js.map +1 -1
  131. package/dist/server/resource.cjs +151 -0
  132. package/dist/server/resource.cjs.map +1 -0
  133. package/dist/server/resource.d.cts +90 -0
  134. package/dist/server/resource.d.ts +90 -0
  135. package/dist/server/resource.js +147 -0
  136. package/dist/server/resource.js.map +1 -0
  137. package/dist/server.cjs +1100 -25
  138. package/dist/server.cjs.map +1 -1
  139. package/dist/server.d.cts +84 -5
  140. package/dist/server.d.ts +84 -5
  141. package/dist/server.js +1087 -27
  142. package/dist/server.js.map +1 -1
  143. package/package.json +250 -233
  144. package/registry.yaml +44 -2
  145. package/dist/Alert.schema-LPJE5gFP.d.cts +0 -71
  146. package/dist/Alert.schema-LPJE5gFP.d.ts +0 -71
  147. package/dist/ProgressBar.schema-CdVYx0Wq.d.cts +0 -22
  148. package/dist/ProgressBar.schema-CdVYx0Wq.d.ts +0 -22
  149. package/dist/{ConfirmDialog-B3CQQGmJ.d.cts → ConfirmDialog-BCtOcmHe.d.cts} +1 -1
  150. package/dist/{ConfirmDialog-B3CQQGmJ.d.ts → ConfirmDialog-BCtOcmHe.d.ts} +1 -1
package/dist/display.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { useVirtualizer } from '@tanstack/react-virtual';
2
- import { useState, useEffect, useRef, useCallback } from 'react';
2
+ import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
3
3
  import { z } from 'zod';
4
- import { jsx, jsxs } from 'react/jsx-runtime';
4
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
 
6
6
  // src/components/display/TableView.tsx
7
7
 
@@ -98,7 +98,99 @@ var EN = {
98
98
  "Select.search.placeholder": "Search\u2026",
99
99
  "Select.empty": "No options available",
100
100
  "Select.error.required": "Select an option",
101
- "Select.error.invalidProps": "Select: invalid props"
101
+ "Select.error.invalidProps": "Select: invalid props",
102
+ "ModelContext.selection.one": "1 row selected",
103
+ "ModelContext.selection.many": "{n} rows selected",
104
+ "ModelContext.selection.none": "No rows selected",
105
+ "TableView.sort.by": "Sort by",
106
+ "TableView.select.row": "Select row",
107
+ "TableView.select.all": "Select all rows",
108
+ "TableView.loading": "Loading table",
109
+ "StatCard.title": "Statistic",
110
+ "StatCard.trend.up": "Up",
111
+ "StatCard.trend.down": "Down",
112
+ "StatCard.trend.flat": "Flat",
113
+ "StatCard.empty.message": "No statistic to display",
114
+ "StatCard.loading": "Loading statistic",
115
+ "StatCard.error.invalidProps": "StatCard: invalid props",
116
+ "BalanceCard.title": "Balance",
117
+ "BalanceCard.network": "Network",
118
+ "BalanceCard.testnet": "Testnet",
119
+ "BalanceCard.address": "Address",
120
+ "BalanceCard.contract": "Token contract",
121
+ "BalanceCard.value": "Value",
122
+ "BalanceCard.copy": "Copy address",
123
+ "BalanceCard.copied": "Address copied",
124
+ "BalanceCard.empty.message": "No balance to display",
125
+ "BalanceCard.loading": "Loading balance",
126
+ "BalanceCard.error.invalidProps": "BalanceCard: invalid props",
127
+ "Timeline.title": "Timeline",
128
+ "Timeline.status.done": "Done",
129
+ "Timeline.status.current": "In progress",
130
+ "Timeline.status.pending": "Pending",
131
+ "Timeline.status.blocked": "Blocked",
132
+ "Timeline.completed": "{n} completed",
133
+ "Timeline.more": "{n} more",
134
+ "Timeline.empty.message": "No steps to display",
135
+ "Timeline.loading": "Loading timeline",
136
+ "Timeline.error.invalidProps": "Timeline: invalid props",
137
+ "MessageFeed.title": "Messages",
138
+ "MessageFeed.unread": "Unread",
139
+ "MessageFeed.attachment": "Attachment",
140
+ "MessageFeed.showing": "Showing the latest {n} of {total}",
141
+ "MessageFeed.empty.message": "No messages",
142
+ "MessageFeed.loading": "Loading messages",
143
+ "MessageFeed.error.invalidProps": "MessageFeed: invalid props",
144
+ "PipelineBoard.title": "Pipeline",
145
+ "PipelineBoard.deals.one": "1 deal",
146
+ "PipelineBoard.deals.many": "{n} deals",
147
+ "PipelineBoard.total": "Total",
148
+ "PipelineBoard.stage.empty": "No deals",
149
+ "PipelineBoard.filter.label": "Show stages",
150
+ "PipelineBoard.summary.open": "Open full board",
151
+ "PipelineBoard.more": "{n} more stages",
152
+ "PipelineBoard.empty.message": "No pipeline to display",
153
+ "PipelineBoard.loading": "Loading pipeline",
154
+ "PipelineBoard.error.invalidProps": "PipelineBoard: invalid props",
155
+ "PipelineBoard.col.stage": "Stage",
156
+ "PipelineBoard.col.deals": "Deals",
157
+ "TransactionPreview.title": "Transaction preview",
158
+ "TransactionPreview.notice": "NOT SIGNED - preparation only",
159
+ "TransactionPreview.kind.transfer": "Transfer",
160
+ "TransactionPreview.kind.swap": "Swap",
161
+ "TransactionPreview.testnet": "Testnet",
162
+ "TransactionPreview.field.network": "Network",
163
+ "TransactionPreview.field.from": "From",
164
+ "TransactionPreview.field.to": "To",
165
+ "TransactionPreview.field.amount": "Amount",
166
+ "TransactionPreview.field.fee": "Network fee",
167
+ "TransactionPreview.field.sell": "Sell",
168
+ "TransactionPreview.field.buy": "Buy",
169
+ "TransactionPreview.field.minReceived": "Minimum received",
170
+ "TransactionPreview.field.route": "Route",
171
+ "TransactionPreview.field.priceImpact": "Price impact",
172
+ "TransactionPreview.field.expires": "Quote expires",
173
+ "TransactionPreview.priceImpact.high": "High price impact",
174
+ "TransactionPreview.simulation.success": "Simulation succeeded",
175
+ "TransactionPreview.simulation.failed": "Simulation failed",
176
+ "TransactionPreview.simulation.unavailable": "Simulation unavailable",
177
+ "TransactionPreview.warnings": "Warnings",
178
+ "TransactionPreview.handoff": "Open in wallet",
179
+ "TransactionPreview.empty.message": "No transaction to preview",
180
+ "TransactionPreview.loading": "Loading preview",
181
+ "TransactionPreview.error.invalidProps": "TransactionPreview: invalid props",
182
+ "ProgressBar.title": "Progress",
183
+ "ConfirmDialog.title": "Confirmation",
184
+ "ConfirmDialog.options": "Answers",
185
+ "TableView.title": "Data table",
186
+ "TableView.markdown.more": "{n} more rows",
187
+ "MarkdownRenderer.title": "Markdown content",
188
+ "TokenDisplayOnceModal.title": "Token (shown once)",
189
+ "StatusBadge.title": "Status",
190
+ "TokenDisplayOnceModal.notice.once": "The value is shown once, in the app view only.",
191
+ "TokenDisplayOnceModal.field.scope": "Scope",
192
+ "TokenDisplayOnceModal.field.expires": "Expires",
193
+ "TokenDisplayOnceModal.field.fingerprint": "Fingerprint"
102
194
  };
103
195
  var FR = {
104
196
  // ─── Non-forms components ──────────────────────────────────────────────────
@@ -192,7 +284,99 @@ var FR = {
192
284
  "Select.search.placeholder": "Rechercher\u2026",
193
285
  "Select.empty": "Aucune option disponible",
194
286
  "Select.error.required": "S\xE9lectionnez une option",
195
- "Select.error.invalidProps": "Select : propri\xE9t\xE9s invalides"
287
+ "Select.error.invalidProps": "Select : propri\xE9t\xE9s invalides",
288
+ "ModelContext.selection.one": "1 ligne s\xE9lectionn\xE9e",
289
+ "ModelContext.selection.many": "{n} lignes s\xE9lectionn\xE9es",
290
+ "ModelContext.selection.none": "Aucune ligne s\xE9lectionn\xE9e",
291
+ "TableView.sort.by": "Trier par",
292
+ "TableView.select.row": "S\xE9lectionner la ligne",
293
+ "TableView.select.all": "Tout s\xE9lectionner",
294
+ "TableView.loading": "Chargement du tableau",
295
+ "StatCard.title": "Statistique",
296
+ "StatCard.trend.up": "En hausse",
297
+ "StatCard.trend.down": "En baisse",
298
+ "StatCard.trend.flat": "Stable",
299
+ "StatCard.empty.message": "Aucune statistique \xE0 afficher",
300
+ "StatCard.loading": "Chargement de la statistique",
301
+ "StatCard.error.invalidProps": "StatCard : propri\xE9t\xE9s invalides",
302
+ "BalanceCard.title": "Solde",
303
+ "BalanceCard.network": "R\xE9seau",
304
+ "BalanceCard.testnet": "R\xE9seau de test",
305
+ "BalanceCard.address": "Adresse",
306
+ "BalanceCard.contract": "Contrat du jeton",
307
+ "BalanceCard.value": "Valeur",
308
+ "BalanceCard.copy": "Copier l'adresse",
309
+ "BalanceCard.copied": "Adresse copi\xE9e",
310
+ "BalanceCard.empty.message": "Aucun solde \xE0 afficher",
311
+ "BalanceCard.loading": "Chargement du solde",
312
+ "BalanceCard.error.invalidProps": "BalanceCard : propri\xE9t\xE9s invalides",
313
+ "Timeline.title": "Chronologie",
314
+ "Timeline.status.done": "Termin\xE9",
315
+ "Timeline.status.current": "En cours",
316
+ "Timeline.status.pending": "\xC0 venir",
317
+ "Timeline.status.blocked": "Bloqu\xE9",
318
+ "Timeline.completed": "{n} termin\xE9(s)",
319
+ "Timeline.more": "{n} de plus",
320
+ "Timeline.empty.message": "Aucune \xE9tape \xE0 afficher",
321
+ "Timeline.loading": "Chargement de la chronologie",
322
+ "Timeline.error.invalidProps": "Timeline : propri\xE9t\xE9s invalides",
323
+ "MessageFeed.title": "Messages",
324
+ "MessageFeed.unread": "Non lu",
325
+ "MessageFeed.attachment": "Pi\xE8ce jointe",
326
+ "MessageFeed.showing": "Affichage des {n} derniers sur {total}",
327
+ "MessageFeed.empty.message": "Aucun message",
328
+ "MessageFeed.loading": "Chargement des messages",
329
+ "MessageFeed.error.invalidProps": "MessageFeed : propri\xE9t\xE9s invalides",
330
+ "PipelineBoard.title": "Pipeline",
331
+ "PipelineBoard.deals.one": "1 affaire",
332
+ "PipelineBoard.deals.many": "{n} affaires",
333
+ "PipelineBoard.total": "Total",
334
+ "PipelineBoard.stage.empty": "Aucune affaire",
335
+ "PipelineBoard.filter.label": "Afficher les \xE9tapes",
336
+ "PipelineBoard.summary.open": "Ouvrir le tableau complet",
337
+ "PipelineBoard.more": "{n} \xE9tapes de plus",
338
+ "PipelineBoard.empty.message": "Aucun pipeline \xE0 afficher",
339
+ "PipelineBoard.loading": "Chargement du pipeline",
340
+ "PipelineBoard.error.invalidProps": "PipelineBoard : propri\xE9t\xE9s invalides",
341
+ "PipelineBoard.col.stage": "\xC9tape",
342
+ "PipelineBoard.col.deals": "Affaires",
343
+ "TransactionPreview.title": "Aper\xE7u de transaction",
344
+ "TransactionPreview.notice": "NON SIGN\xC9 - pr\xE9paration uniquement",
345
+ "TransactionPreview.kind.transfer": "Transfert",
346
+ "TransactionPreview.kind.swap": "\xC9change",
347
+ "TransactionPreview.testnet": "R\xE9seau de test",
348
+ "TransactionPreview.field.network": "R\xE9seau",
349
+ "TransactionPreview.field.from": "De",
350
+ "TransactionPreview.field.to": "\xC0",
351
+ "TransactionPreview.field.amount": "Montant",
352
+ "TransactionPreview.field.fee": "Frais de r\xE9seau",
353
+ "TransactionPreview.field.sell": "Vendre",
354
+ "TransactionPreview.field.buy": "Acheter",
355
+ "TransactionPreview.field.minReceived": "Minimum re\xE7u",
356
+ "TransactionPreview.field.route": "Itin\xE9raire",
357
+ "TransactionPreview.field.priceImpact": "Impact sur le prix",
358
+ "TransactionPreview.field.expires": "Expiration du devis",
359
+ "TransactionPreview.priceImpact.high": "Impact sur le prix \xE9lev\xE9",
360
+ "TransactionPreview.simulation.success": "Simulation r\xE9ussie",
361
+ "TransactionPreview.simulation.failed": "Simulation \xE9chou\xE9e",
362
+ "TransactionPreview.simulation.unavailable": "Simulation indisponible",
363
+ "TransactionPreview.warnings": "Avertissements",
364
+ "TransactionPreview.handoff": "Ouvrir dans le portefeuille",
365
+ "TransactionPreview.empty.message": "Aucune transaction \xE0 pr\xE9visualiser",
366
+ "TransactionPreview.loading": "Chargement de l'aper\xE7u",
367
+ "TransactionPreview.error.invalidProps": "TransactionPreview : propri\xE9t\xE9s invalides",
368
+ "ProgressBar.title": "Progression",
369
+ "ConfirmDialog.title": "Confirmation",
370
+ "ConfirmDialog.options": "R\xE9ponses",
371
+ "TableView.title": "Tableau de donn\xE9es",
372
+ "TableView.markdown.more": "{n} lignes de plus",
373
+ "MarkdownRenderer.title": "Contenu Markdown",
374
+ "TokenDisplayOnceModal.title": "Jeton (affich\xE9 une fois)",
375
+ "StatusBadge.title": "Statut",
376
+ "TokenDisplayOnceModal.notice.once": "La valeur n'est affich\xE9e qu'une fois, dans la vue de l'application uniquement.",
377
+ "TokenDisplayOnceModal.field.scope": "Port\xE9e",
378
+ "TokenDisplayOnceModal.field.expires": "Expire le",
379
+ "TokenDisplayOnceModal.field.fingerprint": "Empreinte"
196
380
  };
197
381
  var TABLE = {
198
382
  en: EN,
@@ -201,92 +385,279 @@ var TABLE = {
201
385
  function t(key, locale = "en") {
202
386
  return TABLE[locale]?.[key] ?? TABLE.en[key];
203
387
  }
388
+
389
+ // src/components/display/Skeleton.logic.ts
390
+ function getAnimationClass(animation) {
391
+ switch (animation) {
392
+ case "pulse":
393
+ return "animate-pulse";
394
+ case "wave":
395
+ return "skeleton-wave";
396
+ case "none":
397
+ return "";
398
+ }
399
+ }
400
+ function getVariantClass(variant) {
401
+ switch (variant) {
402
+ case "rect":
403
+ return "rounded-md";
404
+ case "circle":
405
+ return "rounded-full";
406
+ case "text":
407
+ return "rounded";
408
+ }
409
+ }
410
+ var SKELETON_BG_CLASS = "bg-gray-200 dark:bg-gray-700";
411
+ function textLineKey(index) {
412
+ return `skeleton-line-${index}`;
413
+ }
414
+ function lastLineWidth(index, total, baseWidth) {
415
+ if (total > 1 && index === total - 1) {
416
+ return "75%";
417
+ }
418
+ return baseWidth;
419
+ }
420
+ var SkeletonPropsSchema = z.object({
421
+ variant: z.enum(["rect", "circle", "text"]).default("rect"),
422
+ width: z.union([z.string().min(1), z.number().positive()]).optional(),
423
+ height: z.union([z.string().min(1), z.number().positive()]).optional(),
424
+ count: z.number().int().min(1).max(20).default(1),
425
+ animation: z.enum(["pulse", "wave", "none"]).default("pulse"),
426
+ locale: z.enum(["en", "fr"]).default("en")
427
+ });
428
+ function validateSkeletonProps(raw) {
429
+ return SkeletonPropsSchema.parse(raw);
430
+ }
431
+ function resolveSkeletonDimensions(variant, width, height) {
432
+ const toCss = (v, fallback) => {
433
+ if (v === void 0) return fallback;
434
+ if (typeof v === "number") return `${v}px`;
435
+ return v;
436
+ };
437
+ if (variant === "circle") {
438
+ const size = width !== void 0 ? toCss(width, "40px") : height !== void 0 ? toCss(height, "40px") : "40px";
439
+ return { widthCss: size, heightCss: size };
440
+ }
441
+ if (variant === "text") {
442
+ return {
443
+ widthCss: toCss(width, "100%"),
444
+ heightCss: toCss(height, "16px")
445
+ };
446
+ }
447
+ return {
448
+ widthCss: toCss(width, "100%"),
449
+ heightCss: toCss(height, "100px")
450
+ };
451
+ }
452
+ function SkeletonInner({ variant, width, height, count, animation, locale }) {
453
+ const { widthCss, heightCss } = resolveSkeletonDimensions(variant, width, height);
454
+ const animClass = getAnimationClass(animation);
455
+ const variantClass = getVariantClass(variant);
456
+ const ariaLabel = t("Skeleton.aria.label", locale);
457
+ const baseClass = `${SKELETON_BG_CLASS} ${variantClass} ${animClass}`.trim();
458
+ if (variant === "text") {
459
+ return (
460
+ // biome-ignore lint/a11y/useSemanticElements: role="status" is the correct ARIA pattern for loading indicators; <output> conveys form output semantics which is incorrect here
461
+ /* @__PURE__ */ jsx("div", { role: "status", "aria-label": ariaLabel, "aria-busy": "true", className: "flex flex-col gap-2", children: Array.from({ length: count }, (_, i) => /* @__PURE__ */ jsx(
462
+ "span",
463
+ {
464
+ className: `block ${baseClass}`,
465
+ style: {
466
+ width: lastLineWidth(i, count, widthCss),
467
+ height: heightCss
468
+ },
469
+ "aria-hidden": "true"
470
+ },
471
+ textLineKey(i)
472
+ )) })
473
+ );
474
+ }
475
+ return /* @__PURE__ */ jsx(
476
+ "div",
477
+ {
478
+ role: "status",
479
+ "aria-label": ariaLabel,
480
+ "aria-busy": "true",
481
+ className: `block ${baseClass}`,
482
+ style: { width: widthCss, height: heightCss }
483
+ }
484
+ );
485
+ }
486
+ function Skeleton(raw) {
487
+ try {
488
+ const props = validateSkeletonProps(raw);
489
+ return /* @__PURE__ */ jsx(SkeletonInner, { ...props });
490
+ } catch {
491
+ const locale = raw.locale === "fr" ? "fr" : "en";
492
+ return /* @__PURE__ */ jsx("span", { role: "alert", className: "text-red-600 text-sm", children: t("Skeleton.error.invalidProps", locale) });
493
+ }
494
+ }
495
+
496
+ // src/components/display/TableView.logic.ts
497
+ function isMissing(v) {
498
+ return v === null || v === void 0 || v === "";
499
+ }
500
+ var DECIMAL = /^(-)?(\d+)(?:\.(\d+))?$/;
501
+ function parseDecimal(s) {
502
+ const m = DECIMAL.exec(s.trim());
503
+ if (!m) return void 0;
504
+ const int = (m[2] ?? "0").replace(/^0+(?=\d)/, "");
505
+ const frac = (m[3] ?? "").replace(/0+$/, "");
506
+ const zero = int === "0" && frac === "";
507
+ return { neg: m[1] === "-" && !zero, int, frac };
508
+ }
509
+ function compareDecimalStrings(a, b) {
510
+ const x = parseDecimal(a);
511
+ const y = parseDecimal(b);
512
+ if (!x || !y) return 0;
513
+ if (x.neg !== y.neg) return x.neg ? -1 : 1;
514
+ const sign = x.neg ? -1 : 1;
515
+ if (x.int.length !== y.int.length) return sign * (x.int.length < y.int.length ? -1 : 1);
516
+ if (x.int !== y.int) return sign * (x.int < y.int ? -1 : 1);
517
+ const len = Math.max(x.frac.length, y.frac.length);
518
+ const fx = x.frac.padEnd(len, "0");
519
+ const fy = y.frac.padEnd(len, "0");
520
+ if (fx === fy) return 0;
521
+ return sign * (fx < fy ? -1 : 1);
522
+ }
523
+ var collators = /* @__PURE__ */ new Map();
524
+ function collator(locale) {
525
+ let c = collators.get(locale);
526
+ if (!c) {
527
+ c = new Intl.Collator(locale, { numeric: true, sensitivity: "base" });
528
+ collators.set(locale, c);
529
+ }
530
+ return c;
531
+ }
532
+ function compareCells(a, b, kind, locale) {
533
+ if (kind === "amount") return compareDecimalStrings(String(a), String(b));
534
+ if (kind === "number") {
535
+ const x = Number(a);
536
+ const y = Number(b);
537
+ if (Number.isNaN(x) || Number.isNaN(y)) return collator(locale).compare(String(a), String(b));
538
+ return x === y ? 0 : x < y ? -1 : 1;
539
+ }
540
+ return collator(locale).compare(String(a), String(b));
541
+ }
542
+ function sortItems(items, getValue, kind, direction, locale) {
543
+ const sign = direction === "asc" ? 1 : -1;
544
+ return items.map((item, index) => ({ item, index, value: getValue(item) })).sort((p, q) => {
545
+ const pm = isMissing(p.value);
546
+ const qm = isMissing(q.value);
547
+ if (pm || qm) return pm && qm ? p.index - q.index : pm ? 1 : -1;
548
+ const c = compareCells(p.value, q.value, kind, locale);
549
+ return c !== 0 ? sign * c : p.index - q.index;
550
+ }).map((e) => e.item);
551
+ }
552
+ function nextSort(current, key) {
553
+ if (!current || current.key !== key) return { key, direction: "asc" };
554
+ return current.direction === "asc" ? { key, direction: "desc" } : void 0;
555
+ }
556
+ function toggleSelection(current, key, mode) {
557
+ if (mode === "none") return new Set(current);
558
+ if (mode === "single") return current.has(key) ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([key]);
559
+ const next = new Set(current);
560
+ if (!next.delete(key)) next.add(key);
561
+ return next;
562
+ }
563
+ function toggleAll(current, allKeys) {
564
+ return allKeys.length > 0 && allKeys.every((k) => current.has(k)) ? /* @__PURE__ */ new Set() : new Set(allKeys);
565
+ }
566
+ var ColumnKindSchema = z.enum(["text", "number", "amount", "icon"]);
567
+ var AlignSchema = z.enum(["start", "center", "end"]);
204
568
  var ColumnDefSchema = z.object({
205
569
  key: z.string().min(1),
206
- header: z.string().min(1)
570
+ header: z.string().min(1),
571
+ /** Cell kind: drives comparison and rendering. `amount` = decimal STRING, never a float. */
572
+ type: ColumnKindSchema.default("text"),
573
+ /** Defaults to `end` for `number` / `amount` columns, `start` otherwise. */
574
+ align: AlignSchema.optional(),
575
+ /** CSS width: a number is pixels. */
576
+ width: z.union([z.string().min(1), z.number().positive()]).optional(),
577
+ sortable: z.boolean().default(false),
578
+ /**
579
+ * Values of this column are masked in the A-1 markdown fallback and kept out of the HTML
580
+ * props of a tool result (delivered via the result `_meta`), never written to `content[]`.
581
+ */
582
+ sensitive: z.boolean().default(false)
207
583
  // render is a runtime function — excluded from Zod, typed separately
208
584
  });
585
+ var SortSchema = z.object({ key: z.string().min(1), direction: z.enum(["asc", "desc"]) });
209
586
  var TableViewPropsSchema = z.object({
210
587
  columns: z.array(ColumnDefSchema).min(1, "TableView requires at least one column"),
588
+ /**
589
+ * Serialisable rows, for views built through `createMosaicResource` (the React props carry
590
+ * `rows` separately).
591
+ */
592
+ rows: z.array(z.record(z.unknown())).default([]),
211
593
  virtualizeThreshold: z.number().int().positive().default(100),
212
594
  ariaLabel: z.string().min(1),
213
- locale: z.enum(["en", "fr"]).default("en")
595
+ locale: z.enum(["en", "fr"]).default("en"),
596
+ selectionMode: z.enum(["none", "single", "multiple"]).default("none"),
597
+ /** Initial sort. */
598
+ sort: SortSchema.optional(),
599
+ stickyHeader: z.boolean().default(false),
600
+ /** Scroll region height when `stickyHeader` is on (pixels). */
601
+ maxHeight: z.number().int().positive().default(400),
602
+ density: z.enum(["comfortable", "compact"]).default("comfortable"),
603
+ loading: z.boolean().default(false)
214
604
  });
215
605
  function validateTableViewProps(raw) {
216
606
  return TableViewPropsSchema.parse(raw);
217
607
  }
218
608
  var OVERSCAN = 5;
219
- function TableView(props) {
220
- let validatedThreshold = 100;
609
+ var SKELETON_ROWS = ["head", "r1", "r2", "r3", "r4", "r5"];
610
+ function resolveLocale(l) {
611
+ return l === "fr" ? "fr" : "en";
612
+ }
613
+ function validate(props) {
221
614
  try {
222
615
  const validated = validateTableViewProps({
223
- columns: props.columns.map(({ key, header }) => ({ key, header })),
616
+ columns: props.columns.map(({ key, header, type, align, width, sortable }) => ({
617
+ key,
618
+ header,
619
+ type,
620
+ align,
621
+ width,
622
+ sortable
623
+ })),
224
624
  virtualizeThreshold: props.virtualizeThreshold,
225
625
  ariaLabel: props.ariaLabel,
226
- locale: props.locale
626
+ locale: props.locale,
627
+ selectionMode: props.selectionMode,
628
+ density: props.density
227
629
  });
228
- validatedThreshold = validated.virtualizeThreshold;
630
+ return validated.virtualizeThreshold;
229
631
  } catch {
230
- const locale = props.locale === "fr" ? "fr" : "en";
231
- return /* @__PURE__ */ jsx("div", { role: "alert", children: t("TableView.error.invalidProps", locale) });
632
+ return void 0;
232
633
  }
233
- const isVirtual = props.rows.length > validatedThreshold;
234
- const resolvedLocale = props.locale === "fr" ? "fr" : "en";
235
- if (isVirtual) {
236
- return /* @__PURE__ */ jsx(
237
- VirtualTable,
238
- {
239
- rows: props.rows,
240
- columns: props.columns,
241
- ariaLabel: props.ariaLabel,
242
- locale: resolvedLocale,
243
- onRowClick: props.onRowClick
244
- }
245
- );
634
+ }
635
+ function TableView(props) {
636
+ const threshold = validate(props);
637
+ if (threshold === void 0) {
638
+ return /* @__PURE__ */ jsx("div", { role: "alert", children: t("TableView.error.invalidProps", resolveLocale(props.locale)) });
246
639
  }
247
640
  return /* @__PURE__ */ jsx(
248
- StaticTable,
641
+ TableCore,
249
642
  {
643
+ ...props,
250
644
  rows: props.rows,
251
- columns: props.columns,
252
- ariaLabel: props.ariaLabel,
253
- locale: resolvedLocale,
254
- onRowClick: props.onRowClick
645
+ virtualizeThreshold: threshold,
646
+ locale: resolveLocale(props.locale)
255
647
  }
256
648
  );
257
649
  }
258
650
  function StreamingTableView(props) {
259
- let validatedThreshold = 100;
260
- try {
261
- const validated = validateTableViewProps({
262
- columns: props.columns.map(({ key, header }) => ({ key, header })),
263
- virtualizeThreshold: props.virtualizeThreshold,
264
- ariaLabel: props.ariaLabel,
265
- locale: props.locale
266
- });
267
- validatedThreshold = validated.virtualizeThreshold;
268
- } catch {
269
- const locale = props.locale === "fr" ? "fr" : "en";
270
- return /* @__PURE__ */ jsx("div", { role: "alert", children: t("TableView.error.invalidProps", locale) });
651
+ const threshold = validate(props);
652
+ if (threshold === void 0) {
653
+ return /* @__PURE__ */ jsx("div", { role: "alert", children: t("TableView.error.invalidProps", resolveLocale(props.locale)) });
271
654
  }
272
- return /* @__PURE__ */ jsx(
273
- StreamingTableViewInner,
274
- {
275
- ...props,
276
- virtualizeThreshold: validatedThreshold,
277
- onRowClick: props.onRowClick
278
- }
279
- );
655
+ return /* @__PURE__ */ jsx(StreamingTableViewInner, { ...props, virtualizeThreshold: threshold });
280
656
  }
281
657
  function StreamingTableViewInner({
282
- columns,
283
658
  rows$,
284
- virtualizeThreshold = 100,
285
- ariaLabel,
286
- locale,
287
- onRowClick
659
+ ...rest
288
660
  }) {
289
- const resolvedLocale = locale === "fr" ? "fr" : "en";
290
661
  const [rows, setRows] = useState([]);
291
662
  useEffect(() => {
292
663
  setRows([]);
@@ -297,87 +668,282 @@ function StreamingTableViewInner({
297
668
  sub.unsubscribe();
298
669
  };
299
670
  }, [rows$]);
300
- const isVirtual = rows.length > virtualizeThreshold;
301
- if (isVirtual) {
671
+ return /* @__PURE__ */ jsx(TableCore, { ...rest, rows, locale: resolveLocale(rest.locale) });
672
+ }
673
+ function kindOf(col) {
674
+ return col.type ?? "text";
675
+ }
676
+ function alignOf(col) {
677
+ return col.align ?? (col.type === "amount" || col.type === "number" ? "end" : "start");
678
+ }
679
+ function cellText(row, key) {
680
+ return String(row[key] ?? "");
681
+ }
682
+ function safeIconSrc(v) {
683
+ if (typeof v !== "string") return void 0;
684
+ return /^(https?:\/\/|data:image\/)/i.test(v) ? v : void 0;
685
+ }
686
+ function renderCell(col, row) {
687
+ if (col.render) {
688
+ return col.render(row);
689
+ }
690
+ const value = row[col.key];
691
+ if (col.type === "icon") {
692
+ const src = safeIconSrc(value);
693
+ return src ? /* @__PURE__ */ jsx("img", { src, alt: "", width: 20, height: 20, style: { verticalAlign: "middle" } }) : null;
694
+ }
695
+ if (col.type === "amount" || col.type === "number") {
696
+ return /* @__PURE__ */ jsx("span", { style: { fontVariantNumeric: "tabular-nums" }, children: String(value ?? "") });
697
+ }
698
+ return String(value ?? "");
699
+ }
700
+ function TableCore(props) {
701
+ const {
702
+ columns,
703
+ rows,
704
+ ariaLabel,
705
+ locale,
706
+ selectionMode = "none",
707
+ stickyHeader = false,
708
+ density = "comfortable"
709
+ } = props;
710
+ const [innerSort, setInnerSort] = useState(props.defaultSort);
711
+ const sort = props.sort === void 0 ? innerSort : props.sort ?? void 0;
712
+ const onSort = (key) => {
713
+ const next = nextSort(sort, key);
714
+ if (props.sort === void 0) setInnerSort(next);
715
+ props.onSortChange?.(next);
716
+ };
717
+ const items = useMemo(
718
+ () => rows.map((row, i) => {
719
+ const id = row.id;
720
+ return { row, key: id != null ? String(id) : `row-${i}` };
721
+ }),
722
+ [rows]
723
+ );
724
+ const sorted = useMemo(() => {
725
+ const col = sort ? columns.find((c) => c.key === sort.key) : void 0;
726
+ if (!sort || !col) return items;
727
+ return sortItems(
728
+ items,
729
+ (it) => it.row[col.key],
730
+ kindOf(col),
731
+ sort.direction,
732
+ locale
733
+ );
734
+ }, [items, sort, columns, locale]);
735
+ const [innerSelected, setInnerSelected] = useState(
736
+ () => new Set(props.defaultSelectedKeys ?? [])
737
+ );
738
+ const selected = props.selectedKeys ? new Set(props.selectedKeys) : innerSelected;
739
+ const commit = (next) => {
740
+ if (!props.selectedKeys) setInnerSelected(next);
741
+ const chosen = sorted.filter((it) => next.has(it.key));
742
+ props.onSelectionChange?.(
743
+ chosen.map((it) => it.row),
744
+ chosen.map((it) => it.key)
745
+ );
746
+ };
747
+ const selection = selectionMode === "none" ? void 0 : {
748
+ mode: selectionMode,
749
+ selected,
750
+ toggle: (key) => commit(toggleSelection(selected, key, selectionMode)),
751
+ toggleAll: () => commit(
752
+ toggleAll(
753
+ selected,
754
+ sorted.map((it) => it.key)
755
+ )
756
+ )
757
+ };
758
+ if (props.loading) {
759
+ return /* @__PURE__ */ jsx("div", { "aria-busy": "true", "aria-label": t("TableView.loading", locale), lang: locale, children: SKELETON_ROWS.map((_, i) => /* @__PURE__ */ jsx(
760
+ Skeleton,
761
+ {
762
+ variant: "rect",
763
+ height: i === 0 ? 24 : 36,
764
+ locale
765
+ },
766
+ SKELETON_ROWS[i]
767
+ )) });
768
+ }
769
+ const content = {
770
+ items: sorted,
771
+ columns,
772
+ ariaLabel,
773
+ locale,
774
+ onRowClick: props.onRowClick,
775
+ sort,
776
+ onSort,
777
+ selection,
778
+ sticky: stickyHeader,
779
+ density
780
+ };
781
+ const isVirtual = rows.length > props.virtualizeThreshold;
782
+ if (isVirtual) return /* @__PURE__ */ jsx(VirtualTable, { ...content });
783
+ if (stickyHeader) {
302
784
  return /* @__PURE__ */ jsx(
303
- VirtualTable,
785
+ "div",
304
786
  {
305
- rows,
306
- columns,
307
- ariaLabel,
308
- locale: resolvedLocale,
309
- onRowClick
787
+ tabIndex: 0,
788
+ role: "region",
789
+ "aria-label": ariaLabel,
790
+ style: { maxHeight: props.maxHeight ?? 400, overflow: "auto" },
791
+ children: /* @__PURE__ */ jsx(StaticTable, { ...content })
310
792
  }
311
793
  );
312
794
  }
795
+ return /* @__PURE__ */ jsx(StaticTable, { ...content });
796
+ }
797
+ var CELL_PADDING = { comfortable: void 0, compact: "2px 6px" };
798
+ function HeaderRow({
799
+ columns,
800
+ locale,
801
+ sort,
802
+ onSort,
803
+ selection,
804
+ sticky,
805
+ density,
806
+ items
807
+ }) {
808
+ const padding = CELL_PADDING[density];
809
+ const stickyStyle = sticky ? { position: "sticky", top: 0, background: "var(--mosaic-color-background, Canvas)" } : {};
810
+ const allSelected = items.length > 0 && items.every((it) => selection?.selected.has(it.key));
811
+ const someSelected = items.some((it) => selection?.selected.has(it.key));
812
+ return /* @__PURE__ */ jsxs("tr", { children: [
813
+ selection ? /* @__PURE__ */ jsx("th", { scope: "col", style: { ...stickyStyle, padding, width: 32 }, children: selection.mode === "multiple" ? /* @__PURE__ */ jsx(
814
+ "input",
815
+ {
816
+ type: "checkbox",
817
+ "aria-label": t("TableView.select.all", locale),
818
+ checked: allSelected,
819
+ ref: (el) => {
820
+ if (el) el.indeterminate = someSelected && !allSelected;
821
+ },
822
+ onChange: selection.toggleAll
823
+ }
824
+ ) : null }) : null,
825
+ columns.map((col) => {
826
+ const dir = sort?.key === col.key ? sort.direction : void 0;
827
+ return /* @__PURE__ */ jsx(
828
+ "th",
829
+ {
830
+ scope: "col",
831
+ "aria-sort": col.sortable ? dir === "asc" ? "ascending" : dir === "desc" ? "descending" : "none" : void 0,
832
+ style: {
833
+ ...stickyStyle,
834
+ padding,
835
+ textAlign: alignOf(col),
836
+ ...col.width !== void 0 ? { width: col.width } : {}
837
+ },
838
+ children: col.sortable ? /* @__PURE__ */ jsxs(
839
+ "button",
840
+ {
841
+ type: "button",
842
+ "aria-label": `${t("TableView.sort.by", locale)} ${col.header}`,
843
+ onClick: () => onSort(col.key),
844
+ style: { all: "inherit", cursor: "pointer", padding: 0 },
845
+ children: [
846
+ col.header,
847
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: dir === "asc" ? " \u25B2" : dir === "desc" ? " \u25BC" : "" })
848
+ ]
849
+ }
850
+ ) : col.header
851
+ },
852
+ col.key
853
+ );
854
+ })
855
+ ] });
856
+ }
857
+ function SelectCell({
858
+ item,
859
+ columns,
860
+ selection,
861
+ locale,
862
+ padding
863
+ }) {
864
+ const first = columns[0];
865
+ const label = `${t("TableView.select.row", locale)}: ${first ? cellText(item.row, first.key) : item.key}`;
313
866
  return /* @__PURE__ */ jsx(
314
- StaticTable,
867
+ "td",
315
868
  {
316
- rows,
317
- columns,
318
- ariaLabel,
319
- locale: resolvedLocale,
320
- onRowClick
869
+ style: { padding, width: 32 },
870
+ onClick: (e) => e.stopPropagation(),
871
+ onKeyDown: (e) => e.stopPropagation(),
872
+ children: /* @__PURE__ */ jsx(
873
+ "input",
874
+ {
875
+ type: selection.mode === "multiple" ? "checkbox" : "radio",
876
+ name: selection.mode === "single" ? "mosaic-table-selection" : void 0,
877
+ "aria-label": label,
878
+ checked: selection.selected.has(item.key),
879
+ onChange: () => selection.toggle(item.key),
880
+ onKeyDown: (e) => e.stopPropagation()
881
+ }
882
+ )
321
883
  }
322
884
  );
323
885
  }
324
- function renderCell(col, row) {
325
- if (col.render) {
326
- return col.render(row);
327
- }
328
- return String(row[col.key] ?? "");
886
+ function rowActivation(onRowClick, row, index) {
887
+ if (!onRowClick) return {};
888
+ return {
889
+ role: "button",
890
+ tabIndex: 0,
891
+ onClick: () => onRowClick(row, index),
892
+ onKeyDown: (e) => {
893
+ if (e.key === "Enter" || e.key === " ") {
894
+ e.preventDefault();
895
+ onRowClick(row, index);
896
+ }
897
+ }
898
+ };
329
899
  }
330
- function StaticTable({
331
- rows,
332
- columns,
333
- ariaLabel,
334
- locale,
335
- onRowClick
336
- }) {
900
+ function StaticTable(props) {
901
+ const { items, columns, ariaLabel, locale, onRowClick, selection, density } = props;
337
902
  const emptyMsg = t("TableView.empty.message", locale);
903
+ const padding = CELL_PADDING[density];
904
+ const colSpan = columns.length + (selection ? 1 : 0);
338
905
  return /* @__PURE__ */ jsxs(
339
906
  "table",
340
907
  {
341
908
  "aria-label": ariaLabel,
342
- "aria-rowcount": rows.length,
909
+ "aria-rowcount": items.length,
910
+ "data-density": density,
343
911
  style: { width: "100%", borderCollapse: "collapse" },
344
912
  children: [
345
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: columns.map((col) => /* @__PURE__ */ jsx("th", { scope: "col", children: col.header }, col.key)) }) }),
346
- /* @__PURE__ */ jsx("tbody", { children: rows.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { colSpan: columns.length, "aria-label": emptyMsg, children: emptyMsg }) }) : rows.map((row, rowIndex) => /* @__PURE__ */ jsx(
913
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx(HeaderRow, { ...props }) }),
914
+ /* @__PURE__ */ jsx("tbody", { children: items.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { colSpan, "aria-label": emptyMsg, children: emptyMsg }) }) : items.map((item, rowIndex) => /* @__PURE__ */ jsxs(
347
915
  "tr",
348
916
  {
349
917
  "aria-rowindex": rowIndex + 2,
350
- ...onRowClick ? {
351
- role: "button",
352
- tabIndex: 0,
353
- style: { cursor: "pointer" },
354
- onClick: () => onRowClick(row, rowIndex),
355
- onKeyDown: (e) => {
356
- if (e.key === "Enter" || e.key === " ") {
357
- e.preventDefault();
358
- onRowClick(row, rowIndex);
918
+ "data-selected": selection?.selected.has(item.key) ? "true" : void 0,
919
+ ...onRowClick ? { style: { cursor: "pointer" } } : {},
920
+ ...rowActivation(onRowClick, item.row, rowIndex),
921
+ children: [
922
+ selection ? /* @__PURE__ */ jsx(
923
+ SelectCell,
924
+ {
925
+ item,
926
+ columns,
927
+ selection,
928
+ locale,
929
+ padding
359
930
  }
360
- }
361
- } : {},
362
- children: columns.map((col) => /* @__PURE__ */ jsx("td", { children: renderCell(col, row) }, col.key))
931
+ ) : null,
932
+ columns.map((col) => /* @__PURE__ */ jsx("td", { style: { padding, textAlign: alignOf(col) }, children: renderCell(col, item.row) }, col.key))
933
+ ]
363
934
  },
364
- row.id != null ? String(row.id) : `row-${rowIndex}`
935
+ item.key
365
936
  )) })
366
937
  ]
367
938
  }
368
939
  );
369
940
  }
370
- function VirtualTable({
371
- rows,
372
- columns,
373
- ariaLabel,
374
- // locale reserved for future virtual-mode empty/pagination strings
375
- locale: _locale,
376
- onRowClick
377
- }) {
941
+ function VirtualTable(props) {
942
+ const { items, columns, ariaLabel, locale, onRowClick, selection, density } = props;
378
943
  const scrollRef = useRef(null);
944
+ const padding = CELL_PADDING[density];
379
945
  const rowVirtualizer = useVirtualizer({
380
- count: rows.length,
946
+ count: items.length,
381
947
  /* v8 ignore next 1 */
382
948
  getScrollElement: () => scrollRef.current,
383
949
  estimateSize: useCallback(() => 40, []),
@@ -390,18 +956,19 @@ function VirtualTable({
390
956
  {
391
957
  "data-virtual": "true",
392
958
  ref: scrollRef,
393
- tabIndex: 0,
394
959
  role: "region",
960
+ tabIndex: 0,
395
961
  "aria-label": ariaLabel,
396
962
  style: { height: 400, overflow: "auto", position: "relative" },
397
963
  children: /* @__PURE__ */ jsxs(
398
964
  "table",
399
965
  {
400
966
  "aria-label": ariaLabel,
401
- "aria-rowcount": rows.length,
967
+ "aria-rowcount": items.length,
968
+ "data-density": density,
402
969
  style: { width: "100%", borderCollapse: "collapse" },
403
970
  children: [
404
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: columns.map((col) => /* @__PURE__ */ jsx("th", { scope: "col", children: col.header }, col.key)) }) }),
971
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx(HeaderRow, { ...props, sticky: true }) }),
405
972
  /* @__PURE__ */ jsx(
406
973
  "tbody",
407
974
  {
@@ -411,11 +978,12 @@ function VirtualTable({
411
978
  display: "block"
412
979
  },
413
980
  children: virtualItems.map((virtualRow) => {
414
- const row = rows[virtualRow.index] ?? {};
415
- return /* @__PURE__ */ jsx(
981
+ const item = items[virtualRow.index] ?? { row: {}, key: "" };
982
+ return /* @__PURE__ */ jsxs(
416
983
  "tr",
417
984
  {
418
985
  "aria-rowindex": virtualRow.index + 2,
986
+ "data-selected": selection?.selected.has(item.key) ? "true" : void 0,
419
987
  style: {
420
988
  position: "absolute",
421
989
  top: 0,
@@ -425,20 +993,22 @@ function VirtualTable({
425
993
  display: "flex",
426
994
  ...onRowClick ? { cursor: "pointer" } : {}
427
995
  },
428
- ...onRowClick ? {
429
- role: "button",
430
- tabIndex: 0,
431
- onClick: () => onRowClick(row, virtualRow.index),
432
- onKeyDown: (e) => {
433
- if (e.key === "Enter" || e.key === " ") {
434
- e.preventDefault();
435
- onRowClick(row, virtualRow.index);
996
+ ...rowActivation(onRowClick, item.row, virtualRow.index),
997
+ children: [
998
+ selection ? /* @__PURE__ */ jsx(
999
+ SelectCell,
1000
+ {
1001
+ item,
1002
+ columns,
1003
+ selection,
1004
+ locale,
1005
+ padding
436
1006
  }
437
- }
438
- } : {},
439
- children: columns.map((col) => /* @__PURE__ */ jsx("td", { style: { flex: 1 }, children: renderCell(col, row) }, col.key))
1007
+ ) : null,
1008
+ columns.map((col) => /* @__PURE__ */ jsx("td", { style: { flex: 1, padding, textAlign: alignOf(col) }, children: renderCell(col, item.row) }, col.key))
1009
+ ]
440
1010
  },
441
- row.id != null ? String(row.id) : `vrow-${virtualRow.index}`
1011
+ item.key || `vrow-${virtualRow.index}`
442
1012
  );
443
1013
  })
444
1014
  }
@@ -462,113 +1032,872 @@ function validateEmptyStateProps(raw) {
462
1032
  return EmptyStatePropsSchema.parse(raw);
463
1033
  }
464
1034
 
465
- // src/components/display/Skeleton.logic.ts
466
- function getAnimationClass(animation) {
467
- switch (animation) {
468
- case "pulse":
469
- return "animate-pulse";
470
- case "wave":
471
- return "skeleton-wave";
472
- case "none":
473
- return "";
1035
+ // src/components/display/EmptyState.logic.ts
1036
+ var ACTION_VARIANT_CLASSES = {
1037
+ primary: "inline-flex items-center justify-center rounded-md bg-slate-900 px-4 py-2 text-sm font-medium text-white shadow-sm transition-colors hover:bg-slate-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-900 disabled:pointer-events-none disabled:opacity-50",
1038
+ secondary: "inline-flex items-center justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm transition-colors hover:bg-slate-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400 disabled:pointer-events-none disabled:opacity-50"
1039
+ };
1040
+ function EmptyStateInner({
1041
+ icon,
1042
+ title,
1043
+ description,
1044
+ action,
1045
+ locale: _locale
1046
+ }) {
1047
+ const variant = action?.variant ?? "primary";
1048
+ return /* @__PURE__ */ jsxs(
1049
+ "section",
1050
+ {
1051
+ "aria-label": title,
1052
+ className: "flex flex-col items-center justify-center gap-4 px-6 py-12 text-center",
1053
+ children: [
1054
+ icon && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-slate-400", children: icon }),
1055
+ /* @__PURE__ */ jsx("h2", { className: "text-base font-semibold text-slate-900", children: title }),
1056
+ description && /* @__PURE__ */ jsx("p", { className: "max-w-sm text-sm leading-relaxed text-slate-500", children: description }),
1057
+ action && /* @__PURE__ */ jsx("button", { type: "button", onClick: action.onClick, className: ACTION_VARIANT_CLASSES[variant], children: action.label })
1058
+ ]
1059
+ }
1060
+ );
1061
+ }
1062
+ function EmptyState(raw) {
1063
+ try {
1064
+ const parsed = validateEmptyStateProps(raw);
1065
+ return /* @__PURE__ */ jsx(
1066
+ EmptyStateInner,
1067
+ {
1068
+ ...parsed,
1069
+ icon: raw.icon,
1070
+ action: parsed.action && raw.action?.onClick ? { ...parsed.action, onClick: raw.action.onClick } : void 0
1071
+ }
1072
+ );
1073
+ } catch (err) {
1074
+ console.error("[EmptyState] Invalid props \u2014 component will not render.", err);
1075
+ const locale = raw.locale === "fr" ? "fr" : "en";
1076
+ return /* @__PURE__ */ jsx("span", { role: "alert", children: t("EmptyState.error.invalidProps", locale) });
474
1077
  }
475
1078
  }
476
- function getVariantClass(variant) {
477
- switch (variant) {
478
- case "rect":
479
- return "rounded-md";
480
- case "circle":
481
- return "rounded-full";
482
- case "text":
483
- return "rounded";
1079
+ var StatCardPropsSchema = z.object({
1080
+ label: z.string().default(""),
1081
+ value: z.string().default(""),
1082
+ unit: z.string().optional(),
1083
+ trend: z.enum(["up", "down", "flat"]).optional(),
1084
+ /** Pre-formatted change, e.g. "+4.2%". */
1085
+ change: z.string().optional(),
1086
+ tone: z.enum(["neutral", "success", "warning", "danger"]).default("neutral"),
1087
+ description: z.string().optional(),
1088
+ loading: z.boolean().default(false),
1089
+ locale: z.enum(["en", "fr"]).default("en")
1090
+ });
1091
+ function validateStatCardProps(raw) {
1092
+ return StatCardPropsSchema.parse(raw);
1093
+ }
1094
+ var TONE_CLASSES = {
1095
+ neutral: "text-slate-900",
1096
+ success: "text-green-700",
1097
+ warning: "text-amber-700",
1098
+ danger: "text-red-700"
1099
+ };
1100
+ var TREND_ARROW = { up: "\u25B2", down: "\u25BC", flat: "\u25AC" };
1101
+ function StatCard(raw) {
1102
+ const parsed = StatCardPropsSchema.safeParse(raw);
1103
+ const locale = raw?.locale === "fr" ? "fr" : "en";
1104
+ if (!parsed.success) {
1105
+ return /* @__PURE__ */ jsx("div", { role: "alert", children: t("StatCard.error.invalidProps", locale) });
1106
+ }
1107
+ const p = parsed.data;
1108
+ if (p.loading) {
1109
+ return /* @__PURE__ */ jsx("div", { "aria-busy": "true", "aria-label": t("StatCard.loading", p.locale), children: /* @__PURE__ */ jsx(Skeleton, { variant: "rect", height: 72, locale: p.locale }) });
1110
+ }
1111
+ if (p.label === "" && p.value === "") {
1112
+ return /* @__PURE__ */ jsx(EmptyState, { title: t("StatCard.empty.message", p.locale), locale: p.locale });
484
1113
  }
1114
+ return /* @__PURE__ */ jsxs(
1115
+ "section",
1116
+ {
1117
+ "aria-label": `${t("StatCard.title", p.locale)}: ${p.label}`,
1118
+ lang: p.locale,
1119
+ className: "flex flex-col gap-1 rounded-lg border border-slate-200 bg-white p-4",
1120
+ children: [
1121
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-slate-500", children: p.label }),
1122
+ /* @__PURE__ */ jsxs("span", { className: `text-2xl font-semibold tabular-nums ${TONE_CLASSES[p.tone]}`, children: [
1123
+ p.value,
1124
+ p.unit ? /* @__PURE__ */ jsx("span", { className: "ml-1 text-base font-normal text-slate-500", children: p.unit }) : null
1125
+ ] }),
1126
+ p.trend || p.change ? /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 text-sm text-slate-600", children: [
1127
+ p.trend ? /* @__PURE__ */ jsxs(Fragment, { children: [
1128
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: TREND_ARROW[p.trend] }),
1129
+ /* @__PURE__ */ jsx("span", { children: t(`StatCard.trend.${p.trend}`, p.locale) })
1130
+ ] }) : null,
1131
+ p.change ? /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: p.change }) : null
1132
+ ] }) : null,
1133
+ p.description ? /* @__PURE__ */ jsx("span", { className: "text-sm text-slate-500", children: p.description }) : null
1134
+ ]
1135
+ }
1136
+ );
485
1137
  }
486
- var SKELETON_BG_CLASS = "bg-gray-200 dark:bg-gray-700";
487
- function textLineKey(index) {
488
- return `skeleton-line-${index}`;
1138
+
1139
+ // src/components/shared/markdown.ts
1140
+ var SPECIAL = /([\\`*_[\]()<>&!|~])/g;
1141
+ var BREAKS = /* @__PURE__ */ new Set([10, 11, 12, 13, 133, 8232, 8233]);
1142
+ function flatten(text) {
1143
+ let out = "";
1144
+ let gap = false;
1145
+ for (const c of text) {
1146
+ const n = c.codePointAt(0) ?? 0;
1147
+ if (BREAKS.has(n)) {
1148
+ gap = true;
1149
+ continue;
1150
+ }
1151
+ if (n < 32 && n !== 9) continue;
1152
+ if (n === 127) continue;
1153
+ if (gap) out += " ";
1154
+ gap = false;
1155
+ out += c;
1156
+ }
1157
+ return gap ? `${out} ` : out;
489
1158
  }
490
- function lastLineWidth(index, total, baseWidth) {
491
- if (total > 1 && index === total - 1) {
492
- return "75%";
1159
+ function mdEscape(text) {
1160
+ const flat = flatten(text).replace(SPECIAL, "\\$1").replace(/\b(https?|ftp):\/\//gi, "$1:\\/\\/").replace(/\bwww\./gi, (m) => `${m.slice(0, 3)}\\.`);
1161
+ return flat.replace(/^(\s*)([#>+-])/, "$1\\$2").replace(/^(\s*\d+)([.)])(?=\s|$)/, "$1\\$2");
1162
+ }
1163
+ function mdUrl(href) {
1164
+ return href.replace(
1165
+ /[()[\]\\<>`\s]/g,
1166
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`
1167
+ );
1168
+ }
1169
+ function mdCode(text) {
1170
+ const flat = flatten(text);
1171
+ const longest = Math.max(0, ...(flat.match(/`+/g) ?? []).map((r) => r.length));
1172
+ const fence = "`".repeat(longest + 1);
1173
+ return `${fence} ${flat} ${fence}`;
1174
+ }
1175
+ function mdTable(headers, rows) {
1176
+ const line = (cells) => `| ${cells.join(" | ")} |`;
1177
+ return [line(headers), line(headers.map(() => "---")), ...rows.map(line)].join("\n");
1178
+ }
1179
+ function mdHeading(text, level = 1) {
1180
+ return `${"#".repeat(level)} ${text}`;
1181
+ }
1182
+
1183
+ // src/components/display/StatCard.markdown.ts
1184
+ function statCardToMarkdown(props, locale = "en") {
1185
+ const parsed = StatCardPropsSchema.safeParse(props);
1186
+ const title = mdHeading(t("StatCard.title", locale));
1187
+ if (!parsed.success || parsed.data.label === "" && parsed.data.value === "") {
1188
+ return `${title}
1189
+
1190
+ ${t("StatCard.empty.message", locale)}
1191
+ `;
493
1192
  }
494
- return baseWidth;
1193
+ const { label, value, unit, trend, change, description } = parsed.data;
1194
+ const amount = unit ? `${value} ${unit}` : value;
1195
+ const movement = [trend ? t(`StatCard.trend.${trend}`, locale) : "", change ?? ""].filter(Boolean).join(" ");
1196
+ const line = `- **${mdEscape(label)}**: ${mdEscape(amount)}${movement ? ` (${mdEscape(movement)})` : ""}`;
1197
+ return `${title}
1198
+
1199
+ ${line}
1200
+ ${description ? `
1201
+ ${mdEscape(description)}
1202
+ ` : ""}`;
495
1203
  }
496
- var SkeletonPropsSchema = z.object({
497
- variant: z.enum(["rect", "circle", "text"]).default("rect"),
498
- width: z.union([z.string().min(1), z.number().positive()]).optional(),
499
- height: z.union([z.string().min(1), z.number().positive()]).optional(),
500
- count: z.number().int().min(1).max(20).default(1),
501
- animation: z.enum(["pulse", "wave", "none"]).default("pulse"),
1204
+ var BadgePropsSchema = z.object({
1205
+ label: z.string(),
1206
+ variant: z.enum(["default", "primary", "success", "warning", "danger", "info", "neutral"]).default("default"),
1207
+ dot: z.boolean().optional(),
1208
+ count: z.number().int().nonnegative().optional(),
502
1209
  locale: z.enum(["en", "fr"]).default("en")
503
1210
  });
504
- function validateSkeletonProps(raw) {
505
- return SkeletonPropsSchema.parse(raw);
1211
+ function validateBadgeProps(raw) {
1212
+ return BadgePropsSchema.parse(raw);
506
1213
  }
507
- function resolveSkeletonDimensions(variant, width, height) {
508
- const toCss = (v, fallback) => {
509
- if (v === void 0) return fallback;
510
- if (typeof v === "number") return `${v}px`;
511
- return v;
512
- };
513
- if (variant === "circle") {
514
- const size = width !== void 0 ? toCss(width, "40px") : height !== void 0 ? toCss(height, "40px") : "40px";
515
- return { widthCss: size, heightCss: size };
1214
+ var variantClasses = {
1215
+ default: "bg-slate-100 text-slate-700 ring-1 ring-slate-200",
1216
+ primary: "bg-indigo-100 text-indigo-800 ring-1 ring-indigo-300",
1217
+ success: "bg-green-100 text-green-800 ring-1 ring-green-300",
1218
+ warning: "bg-amber-100 text-amber-800 ring-1 ring-amber-300",
1219
+ danger: "bg-red-100 text-red-800 ring-1 ring-red-300",
1220
+ info: "bg-blue-100 text-blue-800 ring-1 ring-blue-300",
1221
+ neutral: "bg-slate-100 text-slate-800 ring-1 ring-slate-300"
1222
+ };
1223
+ var dotClasses = {
1224
+ default: "bg-slate-400",
1225
+ primary: "bg-indigo-500",
1226
+ success: "bg-green-500",
1227
+ warning: "bg-amber-500",
1228
+ danger: "bg-red-500",
1229
+ info: "bg-blue-500",
1230
+ neutral: "bg-slate-400"
1231
+ };
1232
+ function BadgeInner({ label, variant, dot, count, locale }) {
1233
+ const ariaLabel = t("Badge.aria.label", locale);
1234
+ const displayCount = count !== void 0 ? ` (${count})` : "";
1235
+ return /* @__PURE__ */ jsxs(
1236
+ "span",
1237
+ {
1238
+ "aria-label": `${ariaLabel}: ${label}${displayCount}`,
1239
+ lang: locale,
1240
+ className: `inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium ${variantClasses[variant]}`,
1241
+ children: [
1242
+ dot && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: `h-1.5 w-1.5 rounded-full ${dotClasses[variant]}` }),
1243
+ label,
1244
+ count !== void 0 && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "font-semibold tabular-nums", children: count })
1245
+ ]
1246
+ }
1247
+ );
1248
+ }
1249
+ function Badge(raw) {
1250
+ try {
1251
+ const props = validateBadgeProps(raw);
1252
+ return /* @__PURE__ */ jsx(BadgeInner, { ...props });
1253
+ } catch {
1254
+ const locale = raw.locale === "fr" ? "fr" : "en";
1255
+ return /* @__PURE__ */ jsx("span", { role: "alert", children: t("Badge.error.invalidProps", locale) });
516
1256
  }
517
- if (variant === "text") {
518
- return {
519
- widthCss: toCss(width, "100%"),
520
- heightCss: toCss(height, "16px")
521
- };
1257
+ }
1258
+
1259
+ // src/components/shared/decimal.ts
1260
+ var DECIMAL2 = /^(-)?(\d+)(?:\.(\d+))?$/;
1261
+ function sumDecimalStrings(values) {
1262
+ let scale = 0;
1263
+ const parsed = [];
1264
+ for (const v of values) {
1265
+ const m = DECIMAL2.exec(v);
1266
+ if (!m) return void 0;
1267
+ const frac2 = m[3] ?? "";
1268
+ scale = Math.max(scale, frac2.length);
1269
+ parsed.push({ neg: m[1] === "-", int: m[2] ?? "0", frac: frac2 });
522
1270
  }
523
- return {
524
- widthCss: toCss(width, "100%"),
525
- heightCss: toCss(height, "100px")
1271
+ let total = 0n;
1272
+ for (const p of parsed) {
1273
+ const n = BigInt(p.int + p.frac.padEnd(scale, "0"));
1274
+ total += p.neg ? -n : n;
1275
+ }
1276
+ const neg = total < 0n;
1277
+ const digits = (neg ? -total : total).toString().padStart(scale + 1, "0");
1278
+ const int = digits.slice(0, digits.length - scale);
1279
+ const frac = digits.slice(digits.length - scale).replace(/0+$/, "");
1280
+ return `${neg ? "-" : ""}${int}${frac ? `.${frac}` : ""}`;
1281
+ }
1282
+ function formatDecimalString(value, locale, currency) {
1283
+ if (!DECIMAL2.test(value)) return value;
1284
+ try {
1285
+ const nf = new Intl.NumberFormat(
1286
+ locale,
1287
+ currency ? { style: "currency", currency } : { maximumFractionDigits: 20 }
1288
+ );
1289
+ return nf.format(value);
1290
+ } catch {
1291
+ return value;
1292
+ }
1293
+ }
1294
+ function truncateAddress(address, head = 6, tail = 4) {
1295
+ return address.length <= head + tail + 1 ? address : `${address.slice(0, head)}\u2026${address.slice(address.length - tail)}`;
1296
+ }
1297
+ var BalanceCardPropsSchema = z.object({
1298
+ token: z.string().default(""),
1299
+ balance: z.string().default(""),
1300
+ address: z.string().default(""),
1301
+ tokenAddress: z.string().optional(),
1302
+ decimals: z.number().int().nonnegative().optional(),
1303
+ network: z.string().optional(),
1304
+ testnet: z.boolean().default(false),
1305
+ /** Decimal string in `currency`. */
1306
+ usdValue: z.string().optional(),
1307
+ currency: z.string().default("USD"),
1308
+ loading: z.boolean().default(false),
1309
+ locale: z.enum(["en", "fr"]).default("en")
1310
+ });
1311
+ function validateBalanceCardProps(raw) {
1312
+ return BalanceCardPropsSchema.parse(raw);
1313
+ }
1314
+ function BalanceCard(raw) {
1315
+ const parsed = BalanceCardPropsSchema.safeParse(raw);
1316
+ const locale = raw?.locale === "fr" ? "fr" : "en";
1317
+ const [copied, setCopied] = useState(false);
1318
+ if (!parsed.success) {
1319
+ return /* @__PURE__ */ jsx("div", { role: "alert", children: t("BalanceCard.error.invalidProps", locale) });
1320
+ }
1321
+ const p = parsed.data;
1322
+ if (p.loading) {
1323
+ return /* @__PURE__ */ jsx("div", { "aria-busy": "true", "aria-label": t("BalanceCard.loading", p.locale), children: /* @__PURE__ */ jsx(Skeleton, { variant: "rect", height: 120, locale: p.locale }) });
1324
+ }
1325
+ if (p.token === "" && p.balance === "") {
1326
+ return /* @__PURE__ */ jsx(EmptyState, { title: t("BalanceCard.empty.message", p.locale), locale: p.locale });
1327
+ }
1328
+ const canCopy = typeof navigator !== "undefined" && typeof navigator.clipboard?.writeText === "function";
1329
+ const copy = async () => {
1330
+ try {
1331
+ await navigator.clipboard.writeText(p.address);
1332
+ setCopied(true);
1333
+ } catch {
1334
+ }
526
1335
  };
1336
+ return /* @__PURE__ */ jsxs(
1337
+ "section",
1338
+ {
1339
+ "aria-label": `${t("BalanceCard.title", p.locale)}: ${p.token}`,
1340
+ lang: p.locale,
1341
+ className: "flex flex-col gap-2",
1342
+ children: [
1343
+ /* @__PURE__ */ jsx(
1344
+ StatCard,
1345
+ {
1346
+ label: t("BalanceCard.title", p.locale),
1347
+ value: p.balance,
1348
+ unit: p.token,
1349
+ locale: p.locale
1350
+ }
1351
+ ),
1352
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
1353
+ p.network ? /* @__PURE__ */ jsx(Badge, { label: p.network, variant: "info", locale: p.locale }) : null,
1354
+ p.testnet ? /* @__PURE__ */ jsx(Badge, { label: t("BalanceCard.testnet", p.locale), variant: "warning", locale: p.locale }) : null
1355
+ ] }),
1356
+ p.usdValue ? /* @__PURE__ */ jsxs("span", { className: "text-sm text-slate-600", children: [
1357
+ t("BalanceCard.value", p.locale),
1358
+ ":",
1359
+ " ",
1360
+ formatDecimalString(p.usdValue, p.locale, p.currency)
1361
+ ] }) : null,
1362
+ p.address ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-slate-600", children: [
1363
+ /* @__PURE__ */ jsx("span", { className: "font-mono", title: p.address, children: truncateAddress(p.address) }),
1364
+ canCopy ? /* @__PURE__ */ jsx(
1365
+ "button",
1366
+ {
1367
+ type: "button",
1368
+ onClick: copy,
1369
+ className: "rounded border border-slate-300 px-2 py-0.5 text-xs hover:bg-slate-50",
1370
+ children: t("BalanceCard.copy", p.locale)
1371
+ }
1372
+ ) : null,
1373
+ copied ? /* @__PURE__ */ jsx("output", { children: t("BalanceCard.copied", p.locale) }) : null
1374
+ ] }) : null
1375
+ ]
1376
+ }
1377
+ );
527
1378
  }
528
- function SkeletonInner({ variant, width, height, count, animation, locale }) {
529
- const { widthCss, heightCss } = resolveSkeletonDimensions(variant, width, height);
530
- const animClass = getAnimationClass(animation);
531
- const variantClass = getVariantClass(variant);
532
- const ariaLabel = t("Skeleton.aria.label", locale);
533
- const baseClass = `${SKELETON_BG_CLASS} ${variantClass} ${animClass}`.trim();
534
- if (variant === "text") {
535
- return (
536
- // biome-ignore lint/a11y/useSemanticElements: role="status" is the correct ARIA pattern for loading indicators; <output> conveys form output semantics which is incorrect here
537
- /* @__PURE__ */ jsx("div", { role: "status", "aria-label": ariaLabel, "aria-busy": "true", className: "flex flex-col gap-2", children: Array.from({ length: count }, (_, i) => /* @__PURE__ */ jsx(
538
- "span",
539
- {
540
- className: `block ${baseClass}`,
541
- style: {
542
- width: lastLineWidth(i, count, widthCss),
543
- height: heightCss
544
- },
545
- "aria-hidden": "true"
546
- },
547
- textLineKey(i)
548
- )) })
1379
+
1380
+ // src/components/display/BalanceCard.markdown.ts
1381
+ function balanceCardToMarkdown(props, locale = "en") {
1382
+ const parsed = BalanceCardPropsSchema.safeParse(props);
1383
+ const title = mdHeading(t("BalanceCard.title", locale));
1384
+ if (!parsed.success || parsed.data.token === "" && parsed.data.balance === "") {
1385
+ return `${title}
1386
+
1387
+ ${t("BalanceCard.empty.message", locale)}
1388
+ `;
1389
+ }
1390
+ const p = parsed.data;
1391
+ const where = [p.network, p.testnet ? t("BalanceCard.testnet", locale) : ""].filter(Boolean);
1392
+ const lines = [
1393
+ `- ${mdEscape(p.token)}: ${mdEscape(p.balance)}${where.length ? ` (${mdEscape(where.join(", "))})` : ""}`
1394
+ ];
1395
+ if (p.usdValue) {
1396
+ lines.push(
1397
+ `- ${t("BalanceCard.value", locale)}: ${mdEscape(formatDecimalString(p.usdValue, locale, p.currency))}`
549
1398
  );
550
1399
  }
1400
+ if (p.address) lines.push(`- ${t("BalanceCard.address", locale)}: ${mdCode(p.address)}`);
1401
+ if (p.tokenAddress)
1402
+ lines.push(`- ${t("BalanceCard.contract", locale)}: ${mdCode(p.tokenAddress)}`);
1403
+ return `${title}
1404
+
1405
+ ${lines.join("\n")}
1406
+ `;
1407
+ }
1408
+ var VirtualListPropsSchema = z.object({
1409
+ /** Array of items to render — required, can be empty */
1410
+ items: z.array(z.unknown()),
1411
+ /** Fixed row height in px — mutually exclusive with estimateSize */
1412
+ itemHeight: z.number().positive().optional(),
1413
+ /** Variable row height estimator — mutually exclusive with itemHeight */
1414
+ estimateSize: z.function().args(z.number()).returns(z.number()).optional(),
1415
+ /** Row renderer — required (runtime function, excluded from strict Zod parse but validated for presence) */
1416
+ renderItem: z.function().args(z.unknown(), z.number()).returns(z.unknown()).optional(),
1417
+ /** Rows to render outside visible area (default: 5) */
1418
+ overscan: z.number().int().min(0).optional(),
1419
+ /** CSS class applied to the scroll container */
1420
+ className: z.string().optional(),
1421
+ /** Locale for built-in empty state + error messages */
1422
+ locale: z.enum(["en", "fr"]).optional(),
1423
+ /** a11y: when provided, rows become keyboard-activable (role=button + tabIndex=0 + Enter/Space) */
1424
+ onRowClick: z.function().args(z.unknown(), z.number()).returns(z.void()).optional()
1425
+ });
1426
+ function validateVirtualListProps(raw) {
1427
+ return VirtualListPropsSchema.parse(raw);
1428
+ }
1429
+ var DEFAULT_OVERSCAN = 5;
1430
+ var DEFAULT_ITEM_HEIGHT = 40;
1431
+ function VirtualList(props) {
1432
+ try {
1433
+ validateVirtualListProps({
1434
+ items: props.items,
1435
+ itemHeight: props.itemHeight,
1436
+ overscan: props.overscan,
1437
+ className: props.className,
1438
+ locale: props.locale
1439
+ });
1440
+ } catch {
1441
+ const locale2 = props.locale === "fr" ? "fr" : "en";
1442
+ return /* @__PURE__ */ jsx("div", { role: "alert", children: t("VirtualList.error.invalidProps", locale2) });
1443
+ }
1444
+ const locale = props.locale === "fr" ? "fr" : "en";
1445
+ if (props.items.length === 0) {
1446
+ return /* @__PURE__ */ jsx("output", { "aria-live": "polite", children: t("VirtualList.empty.message", locale) });
1447
+ }
1448
+ return /* @__PURE__ */ jsx(
1449
+ VirtualListInner,
1450
+ {
1451
+ items: props.items,
1452
+ itemHeight: props.itemHeight,
1453
+ estimateSize: props.estimateSize,
1454
+ renderItem: props.renderItem,
1455
+ onRowClick: props.onRowClick,
1456
+ overscan: props.overscan ?? DEFAULT_OVERSCAN,
1457
+ className: props.className,
1458
+ locale
1459
+ }
1460
+ );
1461
+ }
1462
+ function VirtualListInner({
1463
+ items,
1464
+ itemHeight,
1465
+ estimateSize,
1466
+ renderItem,
1467
+ onRowClick,
1468
+ overscan,
1469
+ className
1470
+ }) {
1471
+ const scrollRef = useRef(null);
1472
+ const resolvedEstimateSize = useCallback(
1473
+ (index) => {
1474
+ if (estimateSize) return estimateSize(index);
1475
+ return itemHeight ?? DEFAULT_ITEM_HEIGHT;
1476
+ },
1477
+ [estimateSize, itemHeight]
1478
+ );
1479
+ const rowVirtualizer = useVirtualizer({
1480
+ count: items.length,
1481
+ /* v8 ignore next 1 */
1482
+ getScrollElement: () => scrollRef.current,
1483
+ estimateSize: resolvedEstimateSize,
1484
+ overscan
1485
+ });
1486
+ const virtualItems = rowVirtualizer.getVirtualItems();
1487
+ const totalSize = rowVirtualizer.getTotalSize();
551
1488
  return /* @__PURE__ */ jsx(
552
1489
  "div",
553
1490
  {
554
- role: "status",
555
- "aria-label": ariaLabel,
556
- "aria-busy": "true",
557
- className: `block ${baseClass}`,
558
- style: { width: widthCss, height: heightCss }
1491
+ "data-virtual": "true",
1492
+ ref: scrollRef,
1493
+ className,
1494
+ style: { height: 400, overflow: "auto", position: "relative" },
1495
+ children: /* @__PURE__ */ jsx("div", { style: { height: `${totalSize}px`, position: "relative" }, children: virtualItems.map((virtualRow) => {
1496
+ const item = items[virtualRow.index];
1497
+ const index = virtualRow.index;
1498
+ const rowProps = onRowClick ? {
1499
+ role: "button",
1500
+ tabIndex: 0,
1501
+ "data-list-row": true,
1502
+ style: {
1503
+ position: "absolute",
1504
+ top: 0,
1505
+ left: 0,
1506
+ width: "100%",
1507
+ transform: `translateY(${virtualRow.start}px)`,
1508
+ cursor: "pointer"
1509
+ },
1510
+ onClick: () => onRowClick(item, index),
1511
+ onKeyDown: (e) => {
1512
+ if (e.key === "Enter" || e.key === " ") {
1513
+ e.preventDefault();
1514
+ onRowClick(item, index);
1515
+ }
1516
+ }
1517
+ } : {
1518
+ "data-list-row": true,
1519
+ style: {
1520
+ position: "absolute",
1521
+ top: 0,
1522
+ left: 0,
1523
+ width: "100%",
1524
+ transform: `translateY(${virtualRow.start}px)`
1525
+ }
1526
+ };
1527
+ return /* @__PURE__ */ jsx("div", { ...rowProps, children: renderItem(item, index) }, virtualRow.key);
1528
+ }) })
559
1529
  }
560
1530
  );
561
1531
  }
562
- function Skeleton(raw) {
1532
+
1533
+ // src/components/shared/datetime.ts
1534
+ function formatTimestamp(iso, locale, timeZone) {
1535
+ const d = new Date(iso);
1536
+ if (Number.isNaN(d.getTime())) return iso;
563
1537
  try {
564
- const props = validateSkeletonProps(raw);
565
- return /* @__PURE__ */ jsx(SkeletonInner, { ...props });
1538
+ return new Intl.DateTimeFormat(locale, {
1539
+ dateStyle: "medium",
1540
+ timeStyle: "short",
1541
+ ...timeZone ? { timeZone } : {}
1542
+ }).format(d);
566
1543
  } catch {
567
- const locale = raw.locale === "fr" ? "fr" : "en";
568
- return /* @__PURE__ */ jsx("span", { role: "alert", className: "text-red-600 text-sm", children: t("Skeleton.error.invalidProps", locale) });
1544
+ return iso;
1545
+ }
1546
+ }
1547
+
1548
+ // src/components/shared/url.ts
1549
+ function normalizeHttpUrl(value) {
1550
+ if (typeof value !== "string") return void 0;
1551
+ try {
1552
+ const u = new URL(value);
1553
+ return u.protocol === "https:" || u.protocol === "http:" ? u.href : void 0;
1554
+ } catch {
1555
+ return void 0;
1556
+ }
1557
+ }
1558
+ var FeedMessageSchema = z.object({
1559
+ id: z.string().optional(),
1560
+ sender: z.string().min(1),
1561
+ channel: z.string().optional(),
1562
+ /** Plain text. Rendered as text, never as HTML. */
1563
+ content: z.string(),
1564
+ /** ISO 8601. */
1565
+ timestamp: z.string(),
1566
+ attachmentUrl: z.string().optional(),
1567
+ attachmentLabel: z.string().optional(),
1568
+ unread: z.boolean().optional()
1569
+ });
1570
+ var MessageFeedPropsSchema = z.object({
1571
+ messages: z.array(FeedMessageSchema).default([]),
1572
+ maxItems: z.number().int().positive().optional(),
1573
+ /** IANA timezone for timestamps (e.g. `hostContext.timeZone`). */
1574
+ timeZone: z.string().optional(),
1575
+ /** Above this many shown messages the feed is virtualised through VirtualList. */
1576
+ virtualizeThreshold: z.number().int().positive().default(50),
1577
+ loading: z.boolean().default(false),
1578
+ locale: z.enum(["en", "fr"]).default("en")
1579
+ });
1580
+ function validateMessageFeedProps(raw) {
1581
+ return MessageFeedPropsSchema.parse(raw);
1582
+ }
1583
+ function latestMessages(messages, maxItems) {
1584
+ const shown = maxItems === void 0 ? [...messages] : messages.slice(-maxItems);
1585
+ return { shown, total: messages.length };
1586
+ }
1587
+ function Attachment({
1588
+ m,
1589
+ locale,
1590
+ onOpenLink
1591
+ }) {
1592
+ if (!m.attachmentUrl) return null;
1593
+ const label = m.attachmentLabel ?? t("MessageFeed.attachment", locale);
1594
+ const url = normalizeHttpUrl(m.attachmentUrl);
1595
+ if (!url) return /* @__PURE__ */ jsx("span", { className: "text-sm text-slate-500", children: label });
1596
+ return onOpenLink ? /* @__PURE__ */ jsx(
1597
+ "button",
1598
+ {
1599
+ type: "button",
1600
+ onClick: () => onOpenLink(url),
1601
+ className: "text-left text-sm text-blue-700 underline",
1602
+ children: label
1603
+ }
1604
+ ) : /* @__PURE__ */ jsx(
1605
+ "a",
1606
+ {
1607
+ href: url,
1608
+ rel: "noopener noreferrer",
1609
+ target: "_blank",
1610
+ className: "text-sm text-blue-700 underline",
1611
+ children: label
1612
+ }
1613
+ );
1614
+ }
1615
+ function Message({
1616
+ m,
1617
+ locale,
1618
+ timeZone,
1619
+ onOpenLink
1620
+ }) {
1621
+ return /* @__PURE__ */ jsxs("article", { className: "flex flex-col gap-1 border-b border-slate-100 py-2", children: [
1622
+ /* @__PURE__ */ jsxs("header", { className: "flex flex-wrap items-center gap-2 text-sm", children: [
1623
+ /* @__PURE__ */ jsx("strong", { className: "text-slate-900", children: m.sender }),
1624
+ m.channel ? /* @__PURE__ */ jsx(Badge, { label: m.channel, variant: "neutral", locale }) : null,
1625
+ /* @__PURE__ */ jsx("time", { dateTime: m.timestamp, className: "text-xs text-slate-500", children: formatTimestamp(m.timestamp, locale, timeZone) }),
1626
+ m.unread ? /* @__PURE__ */ jsx("span", { className: "text-xs font-semibold text-blue-700", children: t("MessageFeed.unread", locale) }) : null
1627
+ ] }),
1628
+ /* @__PURE__ */ jsx("p", { className: "m-0 whitespace-pre-wrap text-sm text-slate-800", children: m.content }),
1629
+ /* @__PURE__ */ jsx(Attachment, { m, locale, onOpenLink })
1630
+ ] });
1631
+ }
1632
+ function MessageFeed(raw = {}) {
1633
+ const parsed = MessageFeedPropsSchema.safeParse(raw);
1634
+ const locale = raw?.locale === "fr" ? "fr" : "en";
1635
+ if (!parsed.success) {
1636
+ return /* @__PURE__ */ jsx("div", { role: "alert", children: t("MessageFeed.error.invalidProps", locale) });
1637
+ }
1638
+ const p = parsed.data;
1639
+ if (p.loading) {
1640
+ return /* @__PURE__ */ jsx("div", { "aria-busy": "true", "aria-label": t("MessageFeed.loading", p.locale), children: /* @__PURE__ */ jsx(Skeleton, { variant: "text", count: 5, locale: p.locale }) });
1641
+ }
1642
+ if (p.messages.length === 0) {
1643
+ return /* @__PURE__ */ jsx(EmptyState, { title: t("MessageFeed.empty.message", p.locale), locale: p.locale });
1644
+ }
1645
+ const { shown, total } = latestMessages(p.messages, p.maxItems);
1646
+ const virtual = shown.length > p.virtualizeThreshold;
1647
+ return /* @__PURE__ */ jsxs("section", { "aria-label": t("MessageFeed.title", p.locale), lang: p.locale, children: [
1648
+ shown.length < total ? /* @__PURE__ */ jsx("p", { className: "m-0 pb-1 text-xs text-slate-500", children: t("MessageFeed.showing", p.locale).replace("{n}", String(shown.length)).replace("{total}", String(total)) }) : null,
1649
+ virtual ? /* @__PURE__ */ jsx(
1650
+ VirtualList,
1651
+ {
1652
+ items: shown,
1653
+ itemHeight: 96,
1654
+ locale: p.locale,
1655
+ renderItem: (m) => /* @__PURE__ */ jsx(Message, { m, locale: p.locale, timeZone: p.timeZone, onOpenLink: raw.onOpenLink })
1656
+ }
1657
+ ) : /* @__PURE__ */ jsx("ol", { className: "m-0 list-none p-0", children: shown.map((m, i) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Message, { m, locale: p.locale, timeZone: p.timeZone, onOpenLink: raw.onOpenLink }) }, m.id ?? `msg-${i}`)) })
1658
+ ] });
1659
+ }
1660
+
1661
+ // src/components/display/MessageFeed.markdown.ts
1662
+ function messageFeedToMarkdown(props, locale = "en") {
1663
+ const parsed = MessageFeedPropsSchema.safeParse(props);
1664
+ const title = mdHeading(t("MessageFeed.title", locale));
1665
+ if (!parsed.success || parsed.data.messages.length === 0) {
1666
+ return `${title}
1667
+
1668
+ ${t("MessageFeed.empty.message", locale)}
1669
+ `;
1670
+ }
1671
+ const { messages, maxItems, timeZone } = parsed.data;
1672
+ const { shown, total } = latestMessages(messages, maxItems);
1673
+ const lines = shown.map((m) => {
1674
+ const when = [formatTimestamp(m.timestamp, locale, timeZone), m.channel ?? ""].filter(Boolean).join(", ");
1675
+ const href = normalizeHttpUrl(m.attachmentUrl);
1676
+ const label = mdEscape(m.attachmentLabel ?? t("MessageFeed.attachment", locale));
1677
+ const attachment = m.attachmentUrl ? href ? ` [${label}](${mdUrl(href)})` : ` ${label}` : "";
1678
+ return `> **${mdEscape(m.sender)}** (${mdEscape(when)}): ${mdEscape(m.content)}${attachment}`;
1679
+ });
1680
+ const cut = shown.length < total ? `${t("MessageFeed.showing", locale).replace("{n}", String(shown.length)).replace("{total}", String(total))}
1681
+
1682
+ ` : "";
1683
+ return `${title}
1684
+
1685
+ ${cut}${lines.join("\n>\n")}
1686
+ `;
1687
+ }
1688
+
1689
+ // src/components/forms/RadioGroup.logic.ts
1690
+ function getOptionLabelClasses(appearance, isSelected) {
1691
+ const base = "mosaic-forms-radiogroup-option-label inline-flex items-center px-3 py-1 text-sm";
1692
+ const shape = "rounded-full border" ;
1693
+ const tone = isSelected ? "bg-slate-900 text-white border-slate-900" : "bg-white text-slate-700 border-slate-300 hover:bg-slate-50";
1694
+ return [base, shape, tone].filter(Boolean).join(" ");
1695
+ }
1696
+
1697
+ // src/components/display/PipelineBoard.logic.ts
1698
+ function stageSummary(deals) {
1699
+ const amounts = deals.flatMap((d) => d.amount === void 0 ? [] : [d.amount]);
1700
+ return { count: deals.length, total: sumDecimalStrings(amounts) };
1701
+ }
1702
+ var SUMMARY_STAGE_LIMIT = 5;
1703
+ var PipelineDealSchema = z.object({
1704
+ id: z.string().min(1),
1705
+ title: z.string().min(1),
1706
+ /** Decimal STRING (never a float), in the board currency. */
1707
+ amount: z.string().optional(),
1708
+ contact: z.string().optional(),
1709
+ url: z.string().optional()
1710
+ });
1711
+ var PipelineStageSchema = z.object({
1712
+ id: z.string().min(1),
1713
+ name: z.string().min(1),
1714
+ deals: z.array(PipelineDealSchema).default([])
1715
+ });
1716
+ var PipelineBoardPropsSchema = z.object({
1717
+ stages: z.array(PipelineStageSchema).default([]),
1718
+ /** ISO 4217 code used to format amounts. */
1719
+ currency: z.string().default("EUR"),
1720
+ layout: z.enum(["board", "summary"]).default("board"),
1721
+ loading: z.boolean().default(false),
1722
+ locale: z.enum(["en", "fr"]).default("en")
1723
+ });
1724
+ function validatePipelineBoardProps(raw) {
1725
+ return PipelineBoardPropsSchema.parse(raw);
1726
+ }
1727
+ function dealsLabel(n, locale) {
1728
+ return n === 1 ? t("PipelineBoard.deals.one", locale) : t("PipelineBoard.deals.many", locale).replace("{n}", String(n));
1729
+ }
1730
+ function Totals({
1731
+ stage,
1732
+ currency,
1733
+ locale
1734
+ }) {
1735
+ const { count, total } = stageSummary(stage.deals);
1736
+ return /* @__PURE__ */ jsxs("span", { className: "flex flex-col text-sm text-slate-600", children: [
1737
+ /* @__PURE__ */ jsx("span", { children: dealsLabel(count, locale) }),
1738
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: total === void 0 ? "\u2014" : formatDecimalString(total, locale, currency) })
1739
+ ] });
1740
+ }
1741
+ function PipelineBoard(raw = {}) {
1742
+ const parsed = PipelineBoardPropsSchema.safeParse(raw);
1743
+ const locale = raw?.locale === "fr" ? "fr" : "en";
1744
+ const [hidden, setHidden] = useState(/* @__PURE__ */ new Set());
1745
+ if (!parsed.success) {
1746
+ return /* @__PURE__ */ jsx("div", { role: "alert", children: t("PipelineBoard.error.invalidProps", locale) });
569
1747
  }
1748
+ const p = parsed.data;
1749
+ if (p.loading) {
1750
+ return /* @__PURE__ */ jsx("div", { "aria-busy": "true", "aria-label": t("PipelineBoard.loading", p.locale), children: /* @__PURE__ */ jsx(Skeleton, { variant: "rect", height: 160, locale: p.locale }) });
1751
+ }
1752
+ if (p.stages.length === 0) {
1753
+ return /* @__PURE__ */ jsx(EmptyState, { title: t("PipelineBoard.empty.message", p.locale), locale: p.locale });
1754
+ }
1755
+ if (p.layout === "summary") {
1756
+ const shown = p.stages.slice(0, SUMMARY_STAGE_LIMIT);
1757
+ const rest = p.stages.length - shown.length;
1758
+ return /* @__PURE__ */ jsxs(
1759
+ "section",
1760
+ {
1761
+ "aria-label": t("PipelineBoard.title", p.locale),
1762
+ lang: p.locale,
1763
+ className: "flex flex-col gap-2",
1764
+ children: [
1765
+ /* @__PURE__ */ jsx("ul", { className: "m-0 flex list-none flex-col gap-2 p-0", children: shown.map((s) => /* @__PURE__ */ jsxs("li", { className: "flex items-center justify-between gap-2", children: [
1766
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-slate-900", children: s.name }),
1767
+ /* @__PURE__ */ jsx(Totals, { stage: s, currency: p.currency, locale: p.locale })
1768
+ ] }, s.id)) }),
1769
+ rest > 0 ? /* @__PURE__ */ jsx("span", { className: "text-sm text-slate-500", children: t("PipelineBoard.more", p.locale).replace("{n}", String(rest)) }) : null,
1770
+ raw.onRequestFullscreen ? /* @__PURE__ */ jsx(
1771
+ "button",
1772
+ {
1773
+ type: "button",
1774
+ onClick: raw.onRequestFullscreen,
1775
+ className: "self-start rounded border border-slate-300 px-3 py-1 text-sm hover:bg-slate-50",
1776
+ children: t("PipelineBoard.summary.open", p.locale)
1777
+ }
1778
+ ) : null
1779
+ ]
1780
+ }
1781
+ );
1782
+ }
1783
+ const visible = p.stages.filter((s) => !hidden.has(s.id));
1784
+ const toggle = (id) => {
1785
+ const next = new Set(hidden);
1786
+ if (next.has(id)) next.delete(id);
1787
+ else if (visible.length > 1) next.add(id);
1788
+ setHidden(next);
1789
+ };
1790
+ return /* @__PURE__ */ jsxs(
1791
+ "section",
1792
+ {
1793
+ "aria-label": t("PipelineBoard.title", p.locale),
1794
+ lang: p.locale,
1795
+ className: "flex flex-col gap-3",
1796
+ children: [
1797
+ /* @__PURE__ */ jsxs("fieldset", { className: "m-0 flex min-w-0 flex-wrap gap-2 border-0 p-0", children: [
1798
+ /* @__PURE__ */ jsx("legend", { className: "sr-only", children: t("PipelineBoard.filter.label", p.locale) }),
1799
+ p.stages.map((s) => {
1800
+ const on = !hidden.has(s.id);
1801
+ return /* @__PURE__ */ jsx(
1802
+ "button",
1803
+ {
1804
+ type: "button",
1805
+ "aria-pressed": on,
1806
+ onClick: () => toggle(s.id),
1807
+ className: getOptionLabelClasses("chips", on),
1808
+ children: s.name
1809
+ },
1810
+ s.id
1811
+ );
1812
+ })
1813
+ ] }),
1814
+ /* @__PURE__ */ jsx(
1815
+ "div",
1816
+ {
1817
+ className: "flex flex-row gap-3 overflow-x-auto",
1818
+ style: {
1819
+ scrollPaddingLeft: "var(--mosaic-safe-area-left, 0px)",
1820
+ scrollPaddingRight: "var(--mosaic-safe-area-right, 0px)"
1821
+ },
1822
+ children: visible.map((s) => /* @__PURE__ */ jsxs(
1823
+ "section",
1824
+ {
1825
+ "aria-label": s.name,
1826
+ className: "flex w-64 shrink-0 flex-col gap-2 rounded-lg border border-slate-200 bg-slate-50 p-3",
1827
+ children: [
1828
+ /* @__PURE__ */ jsxs("header", { className: "flex items-start justify-between gap-2", children: [
1829
+ /* @__PURE__ */ jsx("strong", { className: "text-slate-900", children: s.name }),
1830
+ /* @__PURE__ */ jsx(Totals, { stage: s, currency: p.currency, locale: p.locale })
1831
+ ] }),
1832
+ s.deals.length === 0 ? /* @__PURE__ */ jsx("span", { className: "text-sm text-slate-500", children: t("PipelineBoard.stage.empty", p.locale) }) : /* @__PURE__ */ jsx("ul", { className: "m-0 flex list-none flex-col gap-2 p-0", children: s.deals.map((d) => /* @__PURE__ */ jsxs("li", { className: "rounded border border-slate-200 bg-white p-2", children: [
1833
+ raw.onOpenLink && normalizeHttpUrl(d.url) ? /* @__PURE__ */ jsx(
1834
+ "button",
1835
+ {
1836
+ type: "button",
1837
+ onClick: () => raw.onOpenLink?.(normalizeHttpUrl(d.url)),
1838
+ className: "text-left font-medium text-blue-700 underline",
1839
+ children: d.title
1840
+ }
1841
+ ) : /* @__PURE__ */ jsx("span", { className: "font-medium text-slate-900", children: d.title }),
1842
+ d.amount !== void 0 ? /* @__PURE__ */ jsx("span", { className: "block text-sm tabular-nums text-slate-700", children: formatDecimalString(d.amount, p.locale, p.currency) }) : null,
1843
+ d.contact ? /* @__PURE__ */ jsx(Badge, { label: d.contact, variant: "neutral", locale: p.locale }) : null
1844
+ ] }, d.id)) })
1845
+ ]
1846
+ },
1847
+ s.id
1848
+ ))
1849
+ }
1850
+ )
1851
+ ]
1852
+ }
1853
+ );
1854
+ }
1855
+
1856
+ // src/components/display/PipelineBoard.markdown.ts
1857
+ function pipelineBoardToMarkdown(props, locale = "en") {
1858
+ const parsed = PipelineBoardPropsSchema.safeParse(props);
1859
+ const title = mdHeading(t("PipelineBoard.title", locale));
1860
+ if (!parsed.success || parsed.data.stages.length === 0) {
1861
+ return `${title}
1862
+
1863
+ ${t("PipelineBoard.empty.message", locale)}
1864
+ `;
1865
+ }
1866
+ const { stages, currency } = parsed.data;
1867
+ const money = (v) => mdEscape(formatDecimalString(v, locale, currency));
1868
+ const table = mdTable(
1869
+ [
1870
+ t("PipelineBoard.col.stage", locale),
1871
+ t("PipelineBoard.col.deals", locale),
1872
+ t("PipelineBoard.total", locale)
1873
+ ],
1874
+ stages.map((s) => {
1875
+ const sum = stageSummary(s.deals);
1876
+ return [
1877
+ mdEscape(s.name),
1878
+ String(sum.count),
1879
+ sum.total === void 0 ? "-" : money(sum.total)
1880
+ ];
1881
+ })
1882
+ );
1883
+ const sections = stages.filter((s) => s.deals.length > 0).map((s) => {
1884
+ const lines = s.deals.map((d) => {
1885
+ const amount = d.amount === void 0 ? "" : ` (${money(d.amount)})`;
1886
+ const contact = d.contact ? ` - ${mdEscape(d.contact)}` : "";
1887
+ return `- ${mdEscape(d.title)}${amount}${contact}`;
1888
+ });
1889
+ return `${mdHeading(mdEscape(s.name), 2)}
1890
+
1891
+ ${lines.join("\n")}`;
1892
+ });
1893
+ return `${title}
1894
+
1895
+ ${table}
1896
+ ${sections.length ? `
1897
+ ${sections.join("\n\n")}
1898
+ ` : ""}`;
570
1899
  }
571
1900
 
572
- export { EmptyStatePropsSchema, Skeleton, SkeletonPropsSchema, StreamingTableView, TableView, TableViewPropsSchema, resolveSkeletonDimensions, validateEmptyStateProps, validateSkeletonProps, validateTableViewProps };
1901
+ export { BalanceCard, BalanceCardPropsSchema, EmptyStatePropsSchema, MessageFeed, MessageFeedPropsSchema, PipelineBoard, PipelineBoardPropsSchema, Skeleton, SkeletonPropsSchema, StatCard, StatCardPropsSchema, StreamingTableView, TableView, TableViewPropsSchema, balanceCardToMarkdown, compareDecimalStrings, messageFeedToMarkdown, nextSort, pipelineBoardToMarkdown, resolveSkeletonDimensions, sortItems, statCardToMarkdown, validateBalanceCardProps, validateEmptyStateProps, validateMessageFeedProps, validatePipelineBoardProps, validateSkeletonProps, validateStatCardProps, validateTableViewProps };
573
1902
  //# sourceMappingURL=display.js.map
574
1903
  //# sourceMappingURL=display.js.map