@agent-native/core 0.123.0 → 0.123.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 (263) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +16 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/templates-meta.ts +12 -0
  5. package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +115 -0
  6. package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +177 -0
  7. package/corpus/templates/crm/.agents/skills/crm/SKILL.md +205 -0
  8. package/corpus/templates/crm/.agents/skills/customizing-agent-native/SKILL.md +220 -0
  9. package/corpus/templates/crm/.agents/skills/feature-flags/SKILL.md +169 -0
  10. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +228 -0
  11. package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +178 -0
  12. package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +122 -0
  13. package/corpus/templates/crm/.env.example +5 -0
  14. package/corpus/templates/crm/AGENTS.md +82 -0
  15. package/corpus/templates/crm/CHANGELOG.md +25 -0
  16. package/corpus/templates/crm/DEVELOPING.md +66 -0
  17. package/corpus/templates/crm/README.md +122 -0
  18. package/corpus/templates/crm/_gitignore +12 -0
  19. package/corpus/templates/crm/actions/_crm-action-utils.ts +98 -0
  20. package/corpus/templates/crm/actions/_crm-attribute-utils.ts +219 -0
  21. package/corpus/templates/crm/actions/_crm-dashboard.ts +69 -0
  22. package/corpus/templates/crm/actions/_crm-data-program-actions.ts +15 -0
  23. package/corpus/templates/crm/actions/_crm-list-utils.ts +775 -0
  24. package/corpus/templates/crm/actions/_crm-signal-utils.ts +79 -0
  25. package/corpus/templates/crm/actions/add-crm-record-to-list.ts +164 -0
  26. package/corpus/templates/crm/actions/apply-crm-proposals.ts +291 -0
  27. package/corpus/templates/crm/actions/archive-crm-attribute.ts +69 -0
  28. package/corpus/templates/crm/actions/attach-call-evidence.ts +170 -0
  29. package/corpus/templates/crm/actions/configure-crm-connection.ts +126 -0
  30. package/corpus/templates/crm/actions/configure-native-crm.ts +44 -0
  31. package/corpus/templates/crm/actions/create-crm-attribute.ts +350 -0
  32. package/corpus/templates/crm/actions/create-crm-list.ts +194 -0
  33. package/corpus/templates/crm/actions/create-crm-record.ts +148 -0
  34. package/corpus/templates/crm/actions/create-crm-signal-tracker.ts +58 -0
  35. package/corpus/templates/crm/actions/delete-crm-saved-view.ts +68 -0
  36. package/corpus/templates/crm/actions/delete-staged-dataset.ts +14 -0
  37. package/corpus/templates/crm/actions/estimate-crm-enrichment.ts +229 -0
  38. package/corpus/templates/crm/actions/find-crm-duplicates.ts +87 -0
  39. package/corpus/templates/crm/actions/get-crm-automation-recipe.ts +36 -0
  40. package/corpus/templates/crm/actions/get-crm-dashboard-panel.ts +35 -0
  41. package/corpus/templates/crm/actions/get-crm-dashboard.ts +14 -0
  42. package/corpus/templates/crm/actions/get-crm-overview.ts +13 -0
  43. package/corpus/templates/crm/actions/get-crm-pipeline-data.ts +61 -0
  44. package/corpus/templates/crm/actions/get-crm-record-page.ts +287 -0
  45. package/corpus/templates/crm/actions/get-crm-record.ts +75 -0
  46. package/corpus/templates/crm/actions/get-crm-workspace.ts +364 -0
  47. package/corpus/templates/crm/actions/install-crm-pipeline-dashboard.ts +81 -0
  48. package/corpus/templates/crm/actions/list-crm-attributes.ts +71 -0
  49. package/corpus/templates/crm/actions/list-crm-connections.ts +68 -0
  50. package/corpus/templates/crm/actions/list-crm-dashboard-revisions.ts +15 -0
  51. package/corpus/templates/crm/actions/list-crm-dashboards.ts +15 -0
  52. package/corpus/templates/crm/actions/list-crm-enrichment-slots.ts +28 -0
  53. package/corpus/templates/crm/actions/list-crm-list-entries.ts +192 -0
  54. package/corpus/templates/crm/actions/list-crm-lists.ts +80 -0
  55. package/corpus/templates/crm/actions/list-crm-proposals.ts +55 -0
  56. package/corpus/templates/crm/actions/list-crm-record-field-history.ts +201 -0
  57. package/corpus/templates/crm/actions/list-crm-record-values.ts +200 -0
  58. package/corpus/templates/crm/actions/list-crm-records.ts +103 -0
  59. package/corpus/templates/crm/actions/list-crm-saved-views.ts +66 -0
  60. package/corpus/templates/crm/actions/list-crm-signal-hits.ts +43 -0
  61. package/corpus/templates/crm/actions/list-crm-signal-trackers.ts +32 -0
  62. package/corpus/templates/crm/actions/list-crm-tasks.ts +22 -0
  63. package/corpus/templates/crm/actions/list-staged-datasets.ts +14 -0
  64. package/corpus/templates/crm/actions/list-workspace-connections.ts +47 -0
  65. package/corpus/templates/crm/actions/manage-crm-attribute-option.ts +272 -0
  66. package/corpus/templates/crm/actions/manage-crm-signal-tracker.ts +87 -0
  67. package/corpus/templates/crm/actions/manage-crm-task.ts +132 -0
  68. package/corpus/templates/crm/actions/merge-crm-records.ts +691 -0
  69. package/corpus/templates/crm/actions/navigate.ts +54 -0
  70. package/corpus/templates/crm/actions/provider-api-catalog.ts +22 -0
  71. package/corpus/templates/crm/actions/provider-api-docs.ts +54 -0
  72. package/corpus/templates/crm/actions/provider-api-request.ts +92 -0
  73. package/corpus/templates/crm/actions/query-staged-dataset.ts +44 -0
  74. package/corpus/templates/crm/actions/record-crm-call-insight.ts +160 -0
  75. package/corpus/templates/crm/actions/record-crm-smart-signal.ts +150 -0
  76. package/corpus/templates/crm/actions/remove-crm-list-entry.ts +79 -0
  77. package/corpus/templates/crm/actions/restore-crm-dashboard-revision.ts +23 -0
  78. package/corpus/templates/crm/actions/review-crm-signal.ts +31 -0
  79. package/corpus/templates/crm/actions/run-crm-attribute-fill.ts +443 -0
  80. package/corpus/templates/crm/actions/run-crm-enrichment.ts +647 -0
  81. package/corpus/templates/crm/actions/run-crm-saved-view-program.ts +103 -0
  82. package/corpus/templates/crm/actions/run-crm-signal-trackers.ts +265 -0
  83. package/corpus/templates/crm/actions/run.ts +7 -0
  84. package/corpus/templates/crm/actions/save-crm-dashboard.ts +51 -0
  85. package/corpus/templates/crm/actions/save-crm-saved-view.ts +296 -0
  86. package/corpus/templates/crm/actions/sync-crm.ts +146 -0
  87. package/corpus/templates/crm/actions/update-crm-attribute.ts +111 -0
  88. package/corpus/templates/crm/actions/update-crm-list-entry.ts +102 -0
  89. package/corpus/templates/crm/actions/update-crm-list.ts +97 -0
  90. package/corpus/templates/crm/actions/update-crm-record.ts +543 -0
  91. package/corpus/templates/crm/actions/view-screen.ts +324 -0
  92. package/corpus/templates/crm/agent-native.app-skill.json +69 -0
  93. package/corpus/templates/crm/app/components/crm/CreateCrmRecordDialog.tsx +158 -0
  94. package/corpus/templates/crm/app/components/crm/CrmDashboardPanel.tsx +78 -0
  95. package/corpus/templates/crm/app/components/crm/CrmSignalsPanel.tsx +220 -0
  96. package/corpus/templates/crm/app/components/crm/IntelligenceSettings.tsx +414 -0
  97. package/corpus/templates/crm/app/components/crm/RecordActions.tsx +648 -0
  98. package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +488 -0
  99. package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +232 -0
  100. package/corpus/templates/crm/app/components/crm/SavedViewDataProgram.tsx +93 -0
  101. package/corpus/templates/crm/app/components/crm/Surface.tsx +97 -0
  102. package/corpus/templates/crm/app/components/crm/WorkOverview.tsx +180 -0
  103. package/corpus/templates/crm/app/components/crm/board/CrmBoard.tsx +988 -0
  104. package/corpus/templates/crm/app/components/crm/board/board-model.ts +399 -0
  105. package/corpus/templates/crm/app/components/crm/board/entry-filter.ts +150 -0
  106. package/corpus/templates/crm/app/components/crm/board/view-draft.ts +227 -0
  107. package/corpus/templates/crm/app/components/crm/grid/CrmGrid.tsx +1033 -0
  108. package/corpus/templates/crm/app/components/crm/grid/GridCell.tsx +620 -0
  109. package/corpus/templates/crm/app/components/crm/grid/clipboard.ts +84 -0
  110. package/corpus/templates/crm/app/components/crm/grid/model.ts +304 -0
  111. package/corpus/templates/crm/app/components/crm/grid/navigation.ts +220 -0
  112. package/corpus/templates/crm/app/components/crm/grid/query.ts +214 -0
  113. package/corpus/templates/crm/app/components/crm/record/AttributePanel.tsx +192 -0
  114. package/corpus/templates/crm/app/components/crm/record/FieldHistory.tsx +179 -0
  115. package/corpus/templates/crm/app/components/crm/record/RecordHeader.tsx +103 -0
  116. package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +235 -0
  117. package/corpus/templates/crm/app/components/crm/record/RecordTabs.tsx +206 -0
  118. package/corpus/templates/crm/app/components/crm/record/field-editors.tsx +245 -0
  119. package/corpus/templates/crm/app/components/crm/record/record-data.ts +603 -0
  120. package/corpus/templates/crm/app/components/crm/settings/AdvancedSettings.tsx +49 -0
  121. package/corpus/templates/crm/app/components/crm/settings/AttributeOptionsEditor.tsx +441 -0
  122. package/corpus/templates/crm/app/components/crm/settings/ConnectionSettings.tsx +169 -0
  123. package/corpus/templates/crm/app/components/crm/settings/FieldsSettings.tsx +935 -0
  124. package/corpus/templates/crm/app/components/crm/settings/ListsSettings.tsx +339 -0
  125. package/corpus/templates/crm/app/components/crm/settings/settings-admin.ts +377 -0
  126. package/corpus/templates/crm/app/components/crm/shared/AttributeValueParts.tsx +69 -0
  127. package/corpus/templates/crm/app/components/crm/shared/attribute-value.ts +591 -0
  128. package/corpus/templates/crm/app/components/layout/CrmCommandMenu.tsx +556 -0
  129. package/corpus/templates/crm/app/components/layout/CrmLayout.tsx +130 -0
  130. package/corpus/templates/crm/app/components/layout/CrmSidebar.tsx +407 -0
  131. package/corpus/templates/crm/app/components/layout/command-palette-search.ts +126 -0
  132. package/corpus/templates/crm/app/components/layout/crm-ui-intents.ts +41 -0
  133. package/corpus/templates/crm/app/components/layout/sidebar-collapse.ts +40 -0
  134. package/corpus/templates/crm/app/components/layout/sidebar-lists.ts +109 -0
  135. package/corpus/templates/crm/app/components/ui/alert-dialog.tsx +1 -0
  136. package/corpus/templates/crm/app/components/ui/badge.tsx +1 -0
  137. package/corpus/templates/crm/app/components/ui/button.tsx +1 -0
  138. package/corpus/templates/crm/app/components/ui/card.tsx +1 -0
  139. package/corpus/templates/crm/app/components/ui/checkbox.tsx +1 -0
  140. package/corpus/templates/crm/app/components/ui/dialog.tsx +1 -0
  141. package/corpus/templates/crm/app/components/ui/dropdown-menu.tsx +1 -0
  142. package/corpus/templates/crm/app/components/ui/hover-card.tsx +1 -0
  143. package/corpus/templates/crm/app/components/ui/input.tsx +1 -0
  144. package/corpus/templates/crm/app/components/ui/label.tsx +1 -0
  145. package/corpus/templates/crm/app/components/ui/popover.tsx +1 -0
  146. package/corpus/templates/crm/app/components/ui/select.tsx +1 -0
  147. package/corpus/templates/crm/app/components/ui/sheet.tsx +1 -0
  148. package/corpus/templates/crm/app/components/ui/skeleton.tsx +1 -0
  149. package/corpus/templates/crm/app/components/ui/switch.tsx +1 -0
  150. package/corpus/templates/crm/app/components/ui/table.tsx +1 -0
  151. package/corpus/templates/crm/app/components/ui/tabs.tsx +1 -0
  152. package/corpus/templates/crm/app/components/ui/textarea.tsx +1 -0
  153. package/corpus/templates/crm/app/components/ui/toolkit-provider.tsx +17 -0
  154. package/corpus/templates/crm/app/components/ui/tooltip.tsx +1 -0
  155. package/corpus/templates/crm/app/design-system.ts +3 -0
  156. package/corpus/templates/crm/app/entry.client.tsx +19 -0
  157. package/corpus/templates/crm/app/entry.server.tsx +10 -0
  158. package/corpus/templates/crm/app/global.css +167 -0
  159. package/corpus/templates/crm/app/hooks/use-keyboard-shortcuts.ts +206 -0
  160. package/corpus/templates/crm/app/hooks/use-navigation-state.ts +48 -0
  161. package/corpus/templates/crm/app/i18n/en-US.ts +684 -0
  162. package/corpus/templates/crm/app/i18n/index.ts +34 -0
  163. package/corpus/templates/crm/app/lib/dashboard.ts +13 -0
  164. package/corpus/templates/crm/app/lib/navigation.ts +17 -0
  165. package/corpus/templates/crm/app/lib/tab-id.ts +1 -0
  166. package/corpus/templates/crm/app/lib/types.ts +172 -0
  167. package/corpus/templates/crm/app/lib/utils.ts +1 -0
  168. package/corpus/templates/crm/app/root.tsx +93 -0
  169. package/corpus/templates/crm/app/routes/_index.tsx +26 -0
  170. package/corpus/templates/crm/app/routes/accounts.tsx +41 -0
  171. package/corpus/templates/crm/app/routes/agent.tsx +5 -0
  172. package/corpus/templates/crm/app/routes/ask.tsx +53 -0
  173. package/corpus/templates/crm/app/routes/dashboard.tsx +118 -0
  174. package/corpus/templates/crm/app/routes/lists.tsx +228 -0
  175. package/corpus/templates/crm/app/routes/opportunities.tsx +41 -0
  176. package/corpus/templates/crm/app/routes/people.tsx +41 -0
  177. package/corpus/templates/crm/app/routes/proposals.tsx +388 -0
  178. package/corpus/templates/crm/app/routes/records.$recordId.tsx +53 -0
  179. package/corpus/templates/crm/app/routes/records.tsx +64 -0
  180. package/corpus/templates/crm/app/routes/settings.tsx +118 -0
  181. package/corpus/templates/crm/app/routes/setup.tsx +308 -0
  182. package/corpus/templates/crm/app/routes/tasks.tsx +252 -0
  183. package/corpus/templates/crm/app/routes/views.tsx +683 -0
  184. package/corpus/templates/crm/app/routes.ts +19 -0
  185. package/corpus/templates/crm/app/vite-env.d.ts +6 -0
  186. package/corpus/templates/crm/changelog/2026-07-26-any-list-or-view-can-be-shown-as-a-board-grouped-by-stage-so.md +6 -0
  187. package/corpus/templates/crm/changelog/2026-07-26-crm-now-finds-likely-duplicate-records-and-tells-you-exactly.md +6 -0
  188. package/corpus/templates/crm/changelog/2026-07-26-enrichment-now-shows-what-a-run-will-cost-before-it-starts-g.md +6 -0
  189. package/corpus/templates/crm/changelog/2026-07-26-lists-turn-any-set-of-records-into-a-pipeline-with-its-own-s.md +6 -0
  190. package/corpus/templates/crm/changelog/2026-07-26-moving-a-record-or-list-entry-into-a-stage-that-was-retired-.md +6 -0
  191. package/corpus/templates/crm/changelog/2026-07-26-opening-a-record-now-shows-one-page-with-its-fields-the-list.md +6 -0
  192. package/corpus/templates/crm/changelog/2026-07-26-press-cmd-k-to-jump-to-any-record-list-view-or-setting-and-u.md +6 -0
  193. package/corpus/templates/crm/changelog/2026-07-26-provider-record-edits-now-show-an-exact-before-after-diff-an.md +6 -0
  194. package/corpus/templates/crm/changelog/2026-07-26-record-page-attribute-labels-now-read-in-full-instead-of-bei.md +6 -0
  195. package/corpus/templates/crm/changelog/2026-07-26-records-now-use-typed-fields-currency-dates-ratings-status-s.md +6 -0
  196. package/corpus/templates/crm/changelog/2026-07-26-retyping-a-value-on-the-record-page-now-replaces-it-instead-.md +6 -0
  197. package/corpus/templates/crm/changelog/2026-07-26-settings-now-has-fields-and-lists-tabs-for-adding-fields-edi.md +6 -0
  198. package/corpus/templates/crm/changelog/2026-07-26-the-proposal-review-now-shows-the-field-that-actually-change.md +6 -0
  199. package/corpus/templates/crm/changelog/2026-07-26-the-record-grid-is-now-a-real-spreadsheet-filter-sort-and-pa.md +6 -0
  200. package/corpus/templates/crm/components.json +20 -0
  201. package/corpus/templates/crm/docs/architecture/crm-contract.md +214 -0
  202. package/corpus/templates/crm/docs/features/README.md +55 -0
  203. package/corpus/templates/crm/docs/features/f1-records-and-fields.mdx +52 -0
  204. package/corpus/templates/crm/docs/features/f2-lists-pipelines-and-views.mdx +48 -0
  205. package/corpus/templates/crm/docs/features/f3-work-and-record-quality.mdx +40 -0
  206. package/corpus/templates/crm/docs/features/f4-evidence-and-chat.mdx +47 -0
  207. package/corpus/templates/crm/learnings.defaults.md +5 -0
  208. package/corpus/templates/crm/netlify.toml +11 -0
  209. package/corpus/templates/crm/package.json +61 -0
  210. package/corpus/templates/crm/public/favicon.svg +1 -0
  211. package/corpus/templates/crm/public/icon-180.svg +1 -0
  212. package/corpus/templates/crm/public/manifest.json +11 -0
  213. package/corpus/templates/crm/react-router.config.ts +7 -0
  214. package/corpus/templates/crm/server/crm/adapter.ts +32 -0
  215. package/corpus/templates/crm/server/crm/crm-field-firewall.ts +64 -0
  216. package/corpus/templates/crm/server/crm/crm-mirror.ts +721 -0
  217. package/corpus/templates/crm/server/crm/field-policy-attributes.ts +48 -0
  218. package/corpus/templates/crm/server/crm/hubspot-adapter.ts +1146 -0
  219. package/corpus/templates/crm/server/crm/native-adapter.ts +1810 -0
  220. package/corpus/templates/crm/server/crm/provider-record-link.ts +264 -0
  221. package/corpus/templates/crm/server/crm/read-through.ts +207 -0
  222. package/corpus/templates/crm/server/crm/salesforce-adapter.ts +1255 -0
  223. package/corpus/templates/crm/server/db/crm-store.ts +1112 -0
  224. package/corpus/templates/crm/server/db/index.ts +191 -0
  225. package/corpus/templates/crm/server/db/schema.ts +647 -0
  226. package/corpus/templates/crm/server/lib/crm-query.ts +1558 -0
  227. package/corpus/templates/crm/server/lib/dedupe.ts +714 -0
  228. package/corpus/templates/crm/server/lib/enrichment-cost.ts +381 -0
  229. package/corpus/templates/crm/server/lib/enrichment-slots.ts +519 -0
  230. package/corpus/templates/crm/server/lib/intelligence/default-detectors.ts +65 -0
  231. package/corpus/templates/crm/server/lib/intelligence/evidence.ts +105 -0
  232. package/corpus/templates/crm/server/lib/intelligence/keyword-detector.ts +94 -0
  233. package/corpus/templates/crm/server/lib/intelligence/smart-detector.ts +39 -0
  234. package/corpus/templates/crm/server/lib/intelligence/summary.ts +19 -0
  235. package/corpus/templates/crm/server/lib/lifecycle.ts +634 -0
  236. package/corpus/templates/crm/server/lib/provider-api.ts +64 -0
  237. package/corpus/templates/crm/server/middleware/auth.ts +4 -0
  238. package/corpus/templates/crm/server/plugins/agent-chat.ts +65 -0
  239. package/corpus/templates/crm/server/plugins/auth.ts +14 -0
  240. package/corpus/templates/crm/server/plugins/core-routes.ts +17 -0
  241. package/corpus/templates/crm/server/plugins/db.ts +738 -0
  242. package/corpus/templates/crm/server/routes/[...page].get.ts +5 -0
  243. package/corpus/templates/crm/shared/crm-attributes.ts +469 -0
  244. package/corpus/templates/crm/shared/crm-automation-recipes.ts +132 -0
  245. package/corpus/templates/crm/shared/crm-contract.ts +417 -0
  246. package/corpus/templates/crm/shared/crm-navigation.ts +190 -0
  247. package/corpus/templates/crm/shared/crm-sales-config.ts +63 -0
  248. package/corpus/templates/crm/ssr-entry.ts +11 -0
  249. package/corpus/templates/crm/tsconfig.json +24 -0
  250. package/corpus/templates/crm/vite.config.ts +15 -0
  251. package/corpus/templates/crm/vitest.config.ts +5 -0
  252. package/corpus/templates/design/app/i18n-data.ts +12 -12
  253. package/corpus/templates/design/changelog/2026-07-26-apply-design-updates-from-a-centered-top-toolbar-without-cov.md +6 -0
  254. package/dist/cli/templates-meta.d.ts.map +1 -1
  255. package/dist/cli/templates-meta.js +12 -0
  256. package/dist/cli/templates-meta.js.map +1 -1
  257. package/dist/collab/routes.d.ts +1 -1
  258. package/dist/observability/routes.d.ts +3 -3
  259. package/dist/resources/handlers.d.ts +1 -1
  260. package/dist/server/realtime-token.d.ts +1 -1
  261. package/dist/server/transcribe-voice.d.ts +1 -1
  262. package/package.json +1 -1
  263. package/src/cli/templates-meta.ts +12 -0
@@ -0,0 +1,620 @@
1
+ import { useActionQuery } from "@agent-native/core/client/hooks";
2
+ import { useT } from "@agent-native/core/client/i18n";
3
+ import {
4
+ IconAlertTriangle,
5
+ IconCheck,
6
+ IconClockExclamation,
7
+ IconCopy,
8
+ IconExternalLink,
9
+ IconStar,
10
+ IconStarFilled,
11
+ } from "@tabler/icons-react";
12
+ import { useEffect, useRef, useState } from "react";
13
+ import { toast } from "sonner";
14
+
15
+ import { Checkbox } from "@/components/ui/checkbox";
16
+ import {
17
+ HoverCard,
18
+ HoverCardContent,
19
+ HoverCardTrigger,
20
+ } from "@/components/ui/hover-card";
21
+ import {
22
+ Popover,
23
+ PopoverContent,
24
+ PopoverTrigger,
25
+ } from "@/components/ui/popover";
26
+ import { cn } from "@/lib/utils";
27
+
28
+ import {
29
+ activeOptions,
30
+ attributeInputValue,
31
+ RATING_MAX,
32
+ valueTokens,
33
+ } from "../shared/attribute-value";
34
+ import {
35
+ AttributeOptionChip,
36
+ AttributeRating,
37
+ } from "../shared/AttributeValueParts";
38
+ import {
39
+ cellSpecFor,
40
+ formatCell,
41
+ statusOverrunDays,
42
+ type CrmCellProvenance,
43
+ type CrmCellValue,
44
+ type CrmGridAttribute,
45
+ } from "./model";
46
+ import { resolveGridKey, type GridDirection } from "./navigation";
47
+
48
+ type Translate = ReturnType<typeof useT>;
49
+
50
+ const EMPTY = "—";
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Provenance
54
+ // ---------------------------------------------------------------------------
55
+
56
+ const PROVENANCE_TINT: Record<CrmCellProvenance["actorType"], string> = {
57
+ user: "",
58
+ agent: "bg-violet-500/70",
59
+ automation: "bg-amber-500/70",
60
+ provider: "bg-sky-500/70",
61
+ system: "bg-muted-foreground/40",
62
+ };
63
+
64
+ /**
65
+ * A 5px corner wedge, not a badge. Provenance has to be legible at a glance
66
+ * across a whole screen of cells without competing with the values themselves.
67
+ */
68
+ export function ProvenanceMarker({
69
+ provenance,
70
+ attributeLabel,
71
+ }: {
72
+ provenance: CrmCellProvenance | undefined;
73
+ attributeLabel: string;
74
+ }) {
75
+ const t = useT();
76
+ if (!provenance) return null;
77
+ if (provenance.readable && provenance.actorType === "user") return null;
78
+ const tint = provenance.readable
79
+ ? PROVENANCE_TINT[provenance.actorType]
80
+ : "bg-destructive/70";
81
+ if (!tint) return null;
82
+ return (
83
+ <HoverCard openDelay={220}>
84
+ <HoverCardTrigger asChild>
85
+ <span
86
+ className="absolute bottom-0 right-0 size-0 cursor-help border-b-[5px] border-l-[5px] border-l-transparent"
87
+ style={{ borderBottomColor: "transparent" }}
88
+ aria-label={t("grid.provenanceOf", { attribute: attributeLabel })}
89
+ >
90
+ <span
91
+ className={cn("absolute -bottom-[5px] right-0 size-[5px]", tint)}
92
+ style={{ clipPath: "polygon(100% 0, 100% 100%, 0 100%)" }}
93
+ />
94
+ </span>
95
+ </HoverCardTrigger>
96
+ <HoverCardContent align="end" className="w-72 text-xs">
97
+ {provenance.readable ? (
98
+ <dl className="grid gap-1.5">
99
+ <div className="flex items-center justify-between gap-3">
100
+ <dt className="text-muted-foreground">{t("grid.provSetBy")}</dt>
101
+ <dd className="font-medium">
102
+ {t(`grid.actor.${provenance.actorType}`)}
103
+ </dd>
104
+ </div>
105
+ {provenance.source ? (
106
+ <div className="flex items-center justify-between gap-3">
107
+ <dt className="text-muted-foreground">
108
+ {t("grid.provSource")}
109
+ </dt>
110
+ <dd className="truncate font-medium">{provenance.source}</dd>
111
+ </div>
112
+ ) : null}
113
+ {provenance.observedAt ? (
114
+ <div className="flex items-center justify-between gap-3">
115
+ <dt className="text-muted-foreground">{t("grid.provWhen")}</dt>
116
+ <dd>{new Date(provenance.observedAt).toLocaleString()}</dd>
117
+ </div>
118
+ ) : null}
119
+ {typeof provenance.confidence === "number" ? (
120
+ <div className="flex items-center justify-between gap-3">
121
+ <dt className="text-muted-foreground">
122
+ {t("grid.provConfidence")}
123
+ </dt>
124
+ <dd>{Math.round(provenance.confidence * 100)}%</dd>
125
+ </div>
126
+ ) : null}
127
+ {provenance.reasoning ? (
128
+ <div className="mt-1 border-t border-border/60 pt-1.5">
129
+ <dt className="sr-only">{t("grid.provReasoning")}</dt>
130
+ <dd className="leading-5 text-muted-foreground">
131
+ {provenance.reasoning}
132
+ </dd>
133
+ </div>
134
+ ) : null}
135
+ {provenance.sourceUrl ? (
136
+ <a
137
+ href={provenance.sourceUrl}
138
+ target="_blank"
139
+ rel="noreferrer"
140
+ className="mt-1 inline-flex items-center gap-1 font-medium text-foreground underline-offset-2 hover:underline"
141
+ >
142
+ {t("grid.provOpenSource")}
143
+ <IconExternalLink className="size-3" />
144
+ </a>
145
+ ) : null}
146
+ </dl>
147
+ ) : (
148
+ <p className="flex items-start gap-2 leading-5 text-muted-foreground">
149
+ <IconAlertTriangle className="mt-0.5 size-3.5 shrink-0 text-destructive" />
150
+ {t("grid.provUnreadable")}
151
+ </p>
152
+ )}
153
+ </HoverCardContent>
154
+ </HoverCard>
155
+ );
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Display
160
+ // ---------------------------------------------------------------------------
161
+
162
+ function copyToClipboard(text: string, t: Translate) {
163
+ void navigator.clipboard
164
+ .writeText(text)
165
+ .then(() => toast.success(t("grid.copied")))
166
+ .catch(() => toast.error(t("grid.copyFailed")));
167
+ }
168
+
169
+ function Chip({
170
+ children,
171
+ onCopy,
172
+ href,
173
+ }: {
174
+ children: React.ReactNode;
175
+ onCopy?: () => void;
176
+ href?: string;
177
+ }) {
178
+ return (
179
+ <span className="inline-flex max-w-full items-center gap-1 rounded-full border border-border/70 bg-muted/40 py-0.5 pl-1.5 pr-2 text-xs">
180
+ <span className="truncate">{children}</span>
181
+ {href ? (
182
+ <a
183
+ href={href}
184
+ target="_blank"
185
+ rel="noreferrer"
186
+ className="shrink-0 text-muted-foreground hover:text-foreground"
187
+ onClick={(event) => event.stopPropagation()}
188
+ >
189
+ <IconExternalLink className="size-3" />
190
+ </a>
191
+ ) : null}
192
+ {onCopy ? (
193
+ <button
194
+ type="button"
195
+ className="shrink-0 cursor-pointer text-muted-foreground hover:text-foreground"
196
+ onClick={(event) => {
197
+ event.stopPropagation();
198
+ onCopy();
199
+ }}
200
+ >
201
+ <IconCopy className="size-3" />
202
+ </button>
203
+ ) : null}
204
+ </span>
205
+ );
206
+ }
207
+
208
+ function StatusPill({
209
+ attribute,
210
+ value,
211
+ overrun,
212
+ }: {
213
+ attribute: CrmGridAttribute;
214
+ value: CrmCellValue;
215
+ overrun: number | null;
216
+ }) {
217
+ const t = useT();
218
+ const token = valueTokens(attribute, value)[0];
219
+ // A status whose option was deleted still shows its stored value: "—" would
220
+ // claim the record has no stage at all.
221
+ if (!token) return <span className="text-muted-foreground">{EMPTY}</span>;
222
+ return (
223
+ <span className="inline-flex items-center gap-1.5">
224
+ <AttributeOptionChip token={token} />
225
+ {overrun !== null ? (
226
+ <IconClockExclamation
227
+ className="size-3.5 shrink-0 text-amber-600 dark:text-amber-500"
228
+ aria-label={t("grid.slaOverrun", { days: overrun })}
229
+ />
230
+ ) : null}
231
+ </span>
232
+ );
233
+ }
234
+
235
+ export function CellDisplay({
236
+ attribute,
237
+ value,
238
+ since,
239
+ now,
240
+ }: {
241
+ attribute: CrmGridAttribute;
242
+ value: CrmCellValue;
243
+ since?: string;
244
+ now?: Date;
245
+ }) {
246
+ const t = useT();
247
+ const type = attribute.attributeType;
248
+
249
+ if (type === "checkbox") {
250
+ return (
251
+ <Checkbox
252
+ checked={value === true}
253
+ disabled
254
+ aria-label={attribute.label}
255
+ className="pointer-events-none"
256
+ />
257
+ );
258
+ }
259
+ if (type === "rating") return <AttributeRating value={value} />;
260
+ if (type === "status") {
261
+ return (
262
+ <StatusPill
263
+ attribute={attribute}
264
+ value={value}
265
+ overrun={statusOverrunDays({
266
+ attribute,
267
+ value,
268
+ since,
269
+ ...(now ? { now } : {}),
270
+ })}
271
+ />
272
+ );
273
+ }
274
+ if (type === "select") {
275
+ const tokens = valueTokens(attribute, value);
276
+ if (!tokens.length) {
277
+ return <span className="text-muted-foreground">{EMPTY}</span>;
278
+ }
279
+ return (
280
+ <span className="flex flex-wrap items-center gap-1">
281
+ {tokens.map((token, index) => (
282
+ <AttributeOptionChip key={`${token.label}:${index}`} token={token} />
283
+ ))}
284
+ </span>
285
+ );
286
+ }
287
+ if (type === "email-address" || type === "domain") {
288
+ const values = Array.isArray(value) ? value : value === null ? [] : [value];
289
+ if (!values.length) {
290
+ return <span className="text-muted-foreground">{EMPTY}</span>;
291
+ }
292
+ return (
293
+ <span className="flex flex-wrap items-center gap-1">
294
+ {values.map((entry) => {
295
+ const text = String(entry);
296
+ return (
297
+ <Chip
298
+ key={text}
299
+ onCopy={() => copyToClipboard(text, t)}
300
+ {...(type === "domain"
301
+ ? { href: text.startsWith("http") ? text : `https://${text}` }
302
+ : {})}
303
+ >
304
+ {text}
305
+ </Chip>
306
+ );
307
+ })}
308
+ </span>
309
+ );
310
+ }
311
+ if (type === "record-reference" || type === "actor-reference") {
312
+ const text = formatCell(attribute, value);
313
+ if (!text) return <span className="text-muted-foreground">{EMPTY}</span>;
314
+ return <Chip>{text}</Chip>;
315
+ }
316
+
317
+ const text = formatCell(attribute, value);
318
+ if (!text) return <span className="text-muted-foreground">{EMPTY}</span>;
319
+ return <span className="block truncate">{text}</span>;
320
+ }
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // Editors
324
+ // ---------------------------------------------------------------------------
325
+
326
+ const INPUT_CLASS =
327
+ "h-full w-full border-0 bg-transparent px-2 text-sm outline-none ring-0 placeholder:text-muted-foreground/60";
328
+
329
+ function useAutoFocus<T extends HTMLElement>() {
330
+ const ref = useRef<T>(null);
331
+ useEffect(() => {
332
+ ref.current?.focus();
333
+ if (ref.current instanceof HTMLInputElement) ref.current.select();
334
+ }, []);
335
+ return ref;
336
+ }
337
+
338
+ function ReferencePicker({
339
+ attribute,
340
+ onPick,
341
+ onCancel,
342
+ }: {
343
+ attribute: CrmGridAttribute;
344
+ onPick: (value: string) => void;
345
+ onCancel: () => void;
346
+ }) {
347
+ const t = useT();
348
+ const [search, setSearch] = useState("");
349
+ const inputRef = useAutoFocus<HTMLInputElement>();
350
+ const results = useActionQuery<{
351
+ records?: Array<{ id: string; displayName: string; subtitle?: string }>;
352
+ }>(
353
+ "list-crm-records" as never,
354
+ { query: search.trim() || undefined, limit: 8 } as never,
355
+ { enabled: search.trim().length > 1 } as never,
356
+ );
357
+ const records = results.data?.records ?? [];
358
+ return (
359
+ <div className="w-64">
360
+ <input
361
+ ref={inputRef}
362
+ value={search}
363
+ onChange={(event) => setSearch(event.target.value)}
364
+ onKeyDown={(event) => {
365
+ if (event.key === "Escape") onCancel();
366
+ }}
367
+ placeholder={t("grid.searchRecords")}
368
+ className="w-full border-b border-border/70 bg-transparent px-3 py-2 text-sm outline-none"
369
+ />
370
+ <div className="max-h-56 overflow-y-auto py-1">
371
+ {search.trim().length <= 1 ? (
372
+ <p className="px-3 py-2 text-xs text-muted-foreground">
373
+ {t("grid.searchToLink")}
374
+ </p>
375
+ ) : results.isError ? (
376
+ <p className="px-3 py-2 text-xs text-destructive">
377
+ {t("grid.searchFailed")}
378
+ </p>
379
+ ) : records.length === 0 ? (
380
+ <p className="px-3 py-2 text-xs text-muted-foreground">
381
+ {results.isLoading ? t("grid.searching") : t("grid.noMatches")}
382
+ </p>
383
+ ) : (
384
+ records.map((record) => (
385
+ <button
386
+ key={record.id}
387
+ type="button"
388
+ className="flex w-full cursor-pointer flex-col items-start px-3 py-1.5 text-left text-sm hover:bg-muted"
389
+ onClick={() => onPick(record.displayName)}
390
+ >
391
+ <span className="truncate font-medium">{record.displayName}</span>
392
+ {record.subtitle ? (
393
+ <span className="truncate text-xs text-muted-foreground">
394
+ {record.subtitle}
395
+ </span>
396
+ ) : null}
397
+ </button>
398
+ ))
399
+ )}
400
+ </div>
401
+ <p className="border-t border-border/60 px-3 py-1.5 text-[11px] text-muted-foreground">
402
+ {t("grid.referenceHint", { attribute: attribute.label })}
403
+ </p>
404
+ </div>
405
+ );
406
+ }
407
+
408
+ function OptionPicker({
409
+ attribute,
410
+ value,
411
+ onPick,
412
+ }: {
413
+ attribute: CrmGridAttribute;
414
+ value: CrmCellValue;
415
+ onPick: (value: CrmCellValue) => void;
416
+ }) {
417
+ const t = useT();
418
+ const selected = new Set(
419
+ (Array.isArray(value) ? value : value === null ? [] : [value]).map(String),
420
+ );
421
+ const options = activeOptions(attribute);
422
+ if (!options.length) {
423
+ return (
424
+ <p className="w-56 px-3 py-2 text-xs text-muted-foreground">
425
+ {t("grid.noOptions")}
426
+ </p>
427
+ );
428
+ }
429
+ function toggle(optionValue: string) {
430
+ if (!attribute.multi) {
431
+ onPick(selected.has(optionValue) ? null : optionValue);
432
+ return;
433
+ }
434
+ const next = new Set(selected);
435
+ if (next.has(optionValue)) next.delete(optionValue);
436
+ else next.add(optionValue);
437
+ onPick(next.size ? [...next] : null);
438
+ }
439
+ return (
440
+ <div className="max-h-64 w-56 overflow-y-auto py-1">
441
+ {options.map((option) => (
442
+ <button
443
+ key={option.id}
444
+ type="button"
445
+ className="flex w-full cursor-pointer items-center gap-2 px-3 py-1.5 text-left text-sm hover:bg-muted"
446
+ onClick={() => toggle(option.value)}
447
+ >
448
+ <span
449
+ className="size-2 shrink-0 rounded-full"
450
+ style={{
451
+ backgroundColor: option.color ?? "var(--muted-foreground)",
452
+ }}
453
+ />
454
+ <span className="flex-1 truncate">{option.title}</span>
455
+ {selected.has(option.value) ? (
456
+ <IconCheck className="size-3.5 shrink-0" />
457
+ ) : null}
458
+ </button>
459
+ ))}
460
+ </div>
461
+ );
462
+ }
463
+
464
+ /**
465
+ * The editor for one cell. `seed` is the character that started the edit, so
466
+ * typing over a cell replaces its value the way a spreadsheet does.
467
+ */
468
+ export function CellEditor({
469
+ attribute,
470
+ value,
471
+ seed,
472
+ onCommit,
473
+ onCancel,
474
+ }: {
475
+ attribute: CrmGridAttribute;
476
+ value: CrmCellValue;
477
+ seed?: string;
478
+ onCommit: (
479
+ raw: string | CrmCellValue,
480
+ isRawText: boolean,
481
+ direction?: GridDirection,
482
+ ) => void;
483
+ onCancel: () => void;
484
+ }) {
485
+ const spec = cellSpecFor(attribute);
486
+ // The raw value, never the formatted one: a currency cell seeded with
487
+ // "$1,200.00" parses back as not-a-number the moment the user presses Enter.
488
+ const [text, setText] = useState(() =>
489
+ seed !== undefined ? seed : attributeInputValue(attribute, value),
490
+ );
491
+ const inputRef = useAutoFocus<HTMLInputElement>();
492
+ // Escape and Enter both remove the input from the tree; without this the
493
+ // blur that follows would commit a value the user just cancelled.
494
+ const handled = useRef(false);
495
+
496
+ if (spec.editor === "checkbox") {
497
+ return (
498
+ <span className="flex h-full items-center justify-center">
499
+ <Checkbox
500
+ autoFocus
501
+ checked={value === true}
502
+ onCheckedChange={(next) => onCommit(next === true, false)}
503
+ aria-label={attribute.label}
504
+ />
505
+ </span>
506
+ );
507
+ }
508
+
509
+ if (spec.editor === "rating") {
510
+ const current = typeof value === "number" ? value : 0;
511
+ return (
512
+ <span className="flex h-full items-center gap-0.5 px-2">
513
+ {Array.from({ length: RATING_MAX }, (_, index) => index + 1).map(
514
+ (step) => (
515
+ <button
516
+ key={step}
517
+ type="button"
518
+ className="cursor-pointer"
519
+ onClick={() => onCommit(step === current ? null : step, false)}
520
+ >
521
+ {step <= current ? (
522
+ <IconStarFilled className="size-4 text-amber-500" />
523
+ ) : (
524
+ <IconStar className="size-4 text-muted-foreground/50 hover:text-amber-500" />
525
+ )}
526
+ </button>
527
+ ),
528
+ )}
529
+ </span>
530
+ );
531
+ }
532
+
533
+ if (spec.editor === "options") {
534
+ return (
535
+ <Popover
536
+ defaultOpen
537
+ onOpenChange={(open) => {
538
+ if (!open) onCancel();
539
+ }}
540
+ >
541
+ <PopoverTrigger asChild>
542
+ <span className="flex h-full items-center px-2 text-sm">
543
+ <CellDisplay attribute={attribute} value={value} />
544
+ </span>
545
+ </PopoverTrigger>
546
+ <PopoverContent align="start" className="p-0">
547
+ <OptionPicker
548
+ attribute={attribute}
549
+ value={value}
550
+ onPick={(next) => onCommit(next, false)}
551
+ />
552
+ </PopoverContent>
553
+ </Popover>
554
+ );
555
+ }
556
+
557
+ if (spec.editor === "reference") {
558
+ return (
559
+ <Popover
560
+ defaultOpen
561
+ onOpenChange={(open) => {
562
+ if (!open) onCancel();
563
+ }}
564
+ >
565
+ <PopoverTrigger asChild>
566
+ <span className="flex h-full items-center px-2 text-sm">
567
+ <CellDisplay attribute={attribute} value={value} />
568
+ </span>
569
+ </PopoverTrigger>
570
+ <PopoverContent align="start" className="p-0">
571
+ <ReferencePicker
572
+ attribute={attribute}
573
+ onPick={(next) => onCommit(next, true)}
574
+ onCancel={onCancel}
575
+ />
576
+ </PopoverContent>
577
+ </Popover>
578
+ );
579
+ }
580
+
581
+ // The grid overrides the registry's `number` input type: spinner arrows in a
582
+ // 34px cell steal the scroll wheel while the sheet is being scrolled.
583
+ const inputType = spec.inputType === "number" ? "text" : spec.inputType;
584
+
585
+ return (
586
+ <input
587
+ ref={inputRef}
588
+ type={inputType}
589
+ defaultValue={text}
590
+ inputMode={spec.editor === "number" ? "decimal" : undefined}
591
+ onChange={(event) => setText(event.target.value)}
592
+ onKeyDown={(event) => {
593
+ const intent = resolveGridKey(event, { editing: true });
594
+ if (!intent) return;
595
+ // The editor owns Enter/Tab/Escape while it is open; letting them reach
596
+ // the grid would move the active cell and commit twice.
597
+ event.preventDefault();
598
+ event.stopPropagation();
599
+ handled.current = true;
600
+ if (intent.type === "cancel") {
601
+ onCancel();
602
+ return;
603
+ }
604
+ if (intent.type === "commit") {
605
+ onCommit(
606
+ event.currentTarget.value,
607
+ true,
608
+ intent.direction ?? undefined,
609
+ );
610
+ }
611
+ }}
612
+ onBlur={(event) => {
613
+ if (handled.current) return;
614
+ onCommit(event.target.value, true);
615
+ }}
616
+ aria-label={attribute.label}
617
+ className={cn(INPUT_CLASS, spec.align === "right" && "text-right")}
618
+ />
619
+ );
620
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * TSV that survives a round trip through Sheets and Excel.
3
+ *
4
+ * Both quote a cell containing a tab, newline, or quote character and double
5
+ * embedded quotes inside it. Splitting on `\t` and `\n` without honouring that
6
+ * silently shreds one pasted cell into several — which lands wrong values in
7
+ * the wrong columns and looks like a successful paste.
8
+ */
9
+
10
+ const NEEDS_QUOTING = /[\t\n\r"]/;
11
+
12
+ export function encodeTsvCell(value: string): string {
13
+ if (!NEEDS_QUOTING.test(value)) return value;
14
+ return `"${value.replace(/"/g, '""')}"`;
15
+ }
16
+
17
+ export function encodeTsv(rows: string[][]): string {
18
+ return rows.map((row) => row.map(encodeTsvCell).join("\t")).join("\n");
19
+ }
20
+
21
+ /**
22
+ * Parse TSV into a rectangle of raw cell strings. Ragged input stays ragged —
23
+ * padding here would invent empty values the source never had.
24
+ */
25
+ export function decodeTsv(text: string): string[][] {
26
+ const rows: string[][] = [];
27
+ let row: string[] = [];
28
+ let cell = "";
29
+ let quoted = false;
30
+ let index = 0;
31
+
32
+ const endCell = () => {
33
+ row.push(cell);
34
+ cell = "";
35
+ };
36
+ const endRow = () => {
37
+ endCell();
38
+ rows.push(row);
39
+ row = [];
40
+ };
41
+
42
+ while (index < text.length) {
43
+ const char = text[index]!;
44
+ if (quoted) {
45
+ if (char === '"') {
46
+ if (text[index + 1] === '"') {
47
+ cell += '"';
48
+ index += 2;
49
+ continue;
50
+ }
51
+ quoted = false;
52
+ index += 1;
53
+ continue;
54
+ }
55
+ cell += char;
56
+ index += 1;
57
+ continue;
58
+ }
59
+ if (char === '"' && cell === "") {
60
+ quoted = true;
61
+ index += 1;
62
+ continue;
63
+ }
64
+ if (char === "\t") {
65
+ endCell();
66
+ index += 1;
67
+ continue;
68
+ }
69
+ if (char === "\r") {
70
+ index += 1;
71
+ continue;
72
+ }
73
+ if (char === "\n") {
74
+ endRow();
75
+ index += 1;
76
+ continue;
77
+ }
78
+ cell += char;
79
+ index += 1;
80
+ }
81
+ // A trailing newline ends the last row; anything else leaves a pending cell.
82
+ if (cell !== "" || row.length > 0 || rows.length === 0) endRow();
83
+ return rows;
84
+ }