@manta-eu/react 0.2.0 → 0.2.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 (235) hide show
  1. package/dist/components/channel-icon/index.d.ts +1 -0
  2. package/dist/components/channel-icon/index.d.ts.map +1 -1
  3. package/dist/components/channel-icon/index.js +1 -1
  4. package/dist/components/channel-icon/index.js.map +1 -1
  5. package/dist/components/planning/ActioningView.d.ts +1 -2
  6. package/dist/components/planning/ActioningView.d.ts.map +1 -1
  7. package/dist/components/planning/ActioningView.js +19 -44
  8. package/dist/components/planning/ActioningView.js.map +1 -1
  9. package/dist/components/planning/ManualActionCard.d.ts +4 -0
  10. package/dist/components/planning/ManualActionCard.d.ts.map +1 -1
  11. package/dist/components/planning/ManualActionCard.js +7 -5
  12. package/dist/components/planning/ManualActionCard.js.map +1 -1
  13. package/dist/components/planning/ManualActionList.d.ts +4 -0
  14. package/dist/components/planning/ManualActionList.d.ts.map +1 -1
  15. package/dist/components/planning/ManualActionList.js +8 -2
  16. package/dist/components/planning/ManualActionList.js.map +1 -1
  17. package/dist/components/planning/actioning-view-utils.d.ts +9 -0
  18. package/dist/components/planning/actioning-view-utils.d.ts.map +1 -1
  19. package/dist/components/planning/actioning-view-utils.js +19 -1
  20. package/dist/components/planning/actioning-view-utils.js.map +1 -1
  21. package/dist/components/tooltip/index.d.ts +7 -0
  22. package/dist/components/tooltip/index.d.ts.map +1 -1
  23. package/dist/components/tooltip/index.js +10 -1
  24. package/dist/components/tooltip/index.js.map +1 -1
  25. package/dist/globals.css +19 -6
  26. package/dist/graph/entity-link.d.ts +14 -0
  27. package/dist/graph/entity-link.d.ts.map +1 -0
  28. package/dist/graph/entity-link.js +12 -0
  29. package/dist/graph/entity-link.js.map +1 -0
  30. package/dist/graph/graph-list-utils.d.ts +1 -0
  31. package/dist/graph/graph-list-utils.d.ts.map +1 -1
  32. package/dist/graph/graph-list-utils.js +1 -0
  33. package/dist/graph/graph-list-utils.js.map +1 -1
  34. package/dist/graph/graph-model.d.ts.map +1 -1
  35. package/dist/graph/graph-model.js +14 -5
  36. package/dist/graph/graph-model.js.map +1 -1
  37. package/dist/graph/nodes/BrandAssetAnalysisView.d.ts.map +1 -1
  38. package/dist/graph/nodes/BrandAssetAnalysisView.js +2 -1
  39. package/dist/graph/nodes/BrandAssetAnalysisView.js.map +1 -1
  40. package/dist/graph/nodes/BrandFontSuggestionView.d.ts.map +1 -1
  41. package/dist/graph/nodes/BrandFontSuggestionView.js +2 -1
  42. package/dist/graph/nodes/BrandFontSuggestionView.js.map +1 -1
  43. package/dist/graph/nodes/BrandGuidelinesView.js +1 -1
  44. package/dist/graph/nodes/BrandGuidelinesView.js.map +1 -1
  45. package/dist/graph/nodes/NodePropertyTable.d.ts +4 -2
  46. package/dist/graph/nodes/NodePropertyTable.d.ts.map +1 -1
  47. package/dist/graph/nodes/NodePropertyTable.js +12 -59
  48. package/dist/graph/nodes/NodePropertyTable.js.map +1 -1
  49. package/dist/graph/nodes/node-property-row.d.ts +30 -0
  50. package/dist/graph/nodes/node-property-row.d.ts.map +1 -0
  51. package/dist/graph/nodes/node-property-row.js +35 -0
  52. package/dist/graph/nodes/node-property-row.js.map +1 -0
  53. package/dist/graph/property-formatters.d.ts +1 -0
  54. package/dist/graph/property-formatters.d.ts.map +1 -1
  55. package/dist/graph/property-formatters.js +4 -1
  56. package/dist/graph/property-formatters.js.map +1 -1
  57. package/dist/graph/special-nodes/AttributeList.d.ts +27 -0
  58. package/dist/graph/special-nodes/AttributeList.d.ts.map +1 -0
  59. package/dist/graph/special-nodes/AttributeList.js +57 -0
  60. package/dist/graph/special-nodes/AttributeList.js.map +1 -0
  61. package/dist/graph/special-nodes/EntityPreview.d.ts +5 -0
  62. package/dist/graph/special-nodes/EntityPreview.d.ts.map +1 -0
  63. package/dist/graph/special-nodes/EntityPreview.js +19 -0
  64. package/dist/graph/special-nodes/EntityPreview.js.map +1 -0
  65. package/dist/graph/special-nodes/EntityValueChip.d.ts +11 -0
  66. package/dist/graph/special-nodes/EntityValueChip.d.ts.map +1 -0
  67. package/dist/graph/special-nodes/EntityValueChip.js +40 -0
  68. package/dist/graph/special-nodes/EntityValueChip.js.map +1 -0
  69. package/dist/graph/special-nodes/GenericView.d.ts +2 -1
  70. package/dist/graph/special-nodes/GenericView.d.ts.map +1 -1
  71. package/dist/graph/special-nodes/GenericView.js +23 -69
  72. package/dist/graph/special-nodes/GenericView.js.map +1 -1
  73. package/dist/graph/special-nodes/card-facts.d.ts +52 -0
  74. package/dist/graph/special-nodes/card-facts.d.ts.map +1 -0
  75. package/dist/graph/special-nodes/card-facts.js +189 -0
  76. package/dist/graph/special-nodes/card-facts.js.map +1 -0
  77. package/dist/graph/special-nodes/property-icons.d.ts +9 -0
  78. package/dist/graph/special-nodes/property-icons.d.ts.map +1 -0
  79. package/dist/graph/special-nodes/property-icons.js +39 -0
  80. package/dist/graph/special-nodes/property-icons.js.map +1 -0
  81. package/dist/graph/status-utils.d.ts +16 -0
  82. package/dist/graph/status-utils.d.ts.map +1 -1
  83. package/dist/graph/status-utils.js +35 -0
  84. package/dist/graph/status-utils.js.map +1 -1
  85. package/dist/graph/types.d.ts +5 -0
  86. package/dist/graph/types.d.ts.map +1 -1
  87. package/dist/hooks/use-approve-anyway.d.ts +12 -0
  88. package/dist/hooks/use-approve-anyway.d.ts.map +1 -0
  89. package/dist/hooks/use-approve-anyway.js +81 -0
  90. package/dist/hooks/use-approve-anyway.js.map +1 -0
  91. package/dist/hooks/use-connection-problems.d.ts.map +1 -1
  92. package/dist/hooks/useValidateActionPlan.d.ts.map +1 -1
  93. package/dist/ldo-ui.d.ts +2 -1
  94. package/dist/ldo-ui.d.ts.map +1 -1
  95. package/dist/ldo-ui.js +1 -0
  96. package/dist/ldo-ui.js.map +1 -1
  97. package/dist/lib/manual-actions.d.ts +2 -1
  98. package/dist/lib/manual-actions.d.ts.map +1 -1
  99. package/dist/lib/manual-actions.js +3 -2
  100. package/dist/lib/manual-actions.js.map +1 -1
  101. package/dist/providers/ActiveChatProvider.js +3 -1
  102. package/dist/providers/ActiveChatProvider.js.map +1 -1
  103. package/dist/providers/ChatManagerProvider.d.ts.map +1 -1
  104. package/dist/providers/ChatManagerProvider.js +3 -1
  105. package/dist/providers/ChatManagerProvider.js.map +1 -1
  106. package/dist/providers/chat-socket-auth.d.ts +7 -2
  107. package/dist/providers/chat-socket-auth.d.ts.map +1 -1
  108. package/dist/providers/chat-socket-auth.js +87 -16
  109. package/dist/providers/chat-socket-auth.js.map +1 -1
  110. package/dist/providers/index.d.ts.map +1 -1
  111. package/dist/providers/index.js +8 -1
  112. package/dist/providers/index.js.map +1 -1
  113. package/dist/routes/activity/detail/ActivityChanges.d.ts +2 -0
  114. package/dist/routes/activity/detail/ActivityChanges.d.ts.map +1 -1
  115. package/dist/routes/activity/detail/ActivityChanges.js +8 -7
  116. package/dist/routes/activity/detail/ActivityChanges.js.map +1 -1
  117. package/dist/routes/activity/detail/ActivityDetail.d.ts +1 -0
  118. package/dist/routes/activity/detail/ActivityDetail.d.ts.map +1 -1
  119. package/dist/routes/activity/detail/ActivityDetail.js +5 -4
  120. package/dist/routes/activity/detail/ActivityDetail.js.map +1 -1
  121. package/dist/routes/activity/detail/ActivityStepsRail.d.ts +1 -0
  122. package/dist/routes/activity/detail/ActivityStepsRail.d.ts.map +1 -1
  123. package/dist/routes/activity/detail/ActivityStepsRail.js +3 -2
  124. package/dist/routes/activity/detail/ActivityStepsRail.js.map +1 -1
  125. package/dist/routes/activity/detail/ActivityTimeline.js +2 -2
  126. package/dist/routes/activity/detail/ActivityTimeline.js.map +1 -1
  127. package/dist/routes/activity/detail/NoChangesMessage.d.ts +6 -0
  128. package/dist/routes/activity/detail/NoChangesMessage.d.ts.map +1 -0
  129. package/dist/routes/activity/detail/NoChangesMessage.js +10 -0
  130. package/dist/routes/activity/detail/NoChangesMessage.js.map +1 -0
  131. package/dist/routes/data-explorer/components/RawTriples.js +1 -1
  132. package/dist/routes/data-explorer/components/RawTriples.js.map +1 -1
  133. package/dist/routes/data-explorer/components/RecentTypes.js +1 -1
  134. package/dist/routes/data-explorer/components/RecentTypes.js.map +1 -1
  135. package/dist/routes/data-explorer/components/class-detail/Instances.js +1 -1
  136. package/dist/routes/data-explorer/components/class-detail/Instances.js.map +1 -1
  137. package/dist/routes/data-explorer/components/namespace-detail/NamespaceClassRow.js +1 -1
  138. package/dist/routes/data-explorer/components/namespace-detail/NamespaceClassRow.js.map +1 -1
  139. package/dist/routes/data-explorer/components/ontology-coverage/CoverageTermColumns.js +1 -1
  140. package/dist/routes/data-explorer/components/ontology-coverage/CoverageTermColumns.js.map +1 -1
  141. package/dist/routes/data-explorer/components/ontology-coverage/UnobservedRuleVocabulary.js +1 -1
  142. package/dist/routes/data-explorer/components/ontology-coverage/UnobservedRuleVocabulary.js.map +1 -1
  143. package/dist/routes/data-explorer/components/record-detail/ChatPreview.js +2 -2
  144. package/dist/routes/data-explorer/components/record-detail/ChatPreview.js.map +1 -1
  145. package/dist/routes/integrations/components/connect-forms/DataConnectionConnectForm.d.ts.map +1 -1
  146. package/dist/routes/integrations/components/connect-forms/DataConnectionConnectForm.js +13 -6
  147. package/dist/routes/integrations/components/connect-forms/DataConnectionConnectForm.js.map +1 -1
  148. package/dist/routes/integrations/components/connect-forms/data-connection-fields.d.ts +3 -0
  149. package/dist/routes/integrations/components/connect-forms/data-connection-fields.d.ts.map +1 -1
  150. package/dist/routes/integrations/components/connect-forms/data-connection-fields.js +9 -2
  151. package/dist/routes/integrations/components/connect-forms/data-connection-fields.js.map +1 -1
  152. package/dist/routes/plans/components/ActionPlanPage.d.ts.map +1 -1
  153. package/dist/routes/plans/components/ActionPlanPage.js +4 -7
  154. package/dist/routes/plans/components/ActionPlanPage.js.map +1 -1
  155. package/dist/routes/plans/components/PlanDetailRouteContent.d.ts.map +1 -1
  156. package/dist/routes/plans/components/PlanDetailRouteContent.js +2 -1
  157. package/dist/routes/plans/components/PlanDetailRouteContent.js.map +1 -1
  158. package/package.json +12 -7
  159. package/src/app-subpaths.json +1 -0
  160. package/src/components/channel-icon/index.tsx +1 -1
  161. package/src/components/planning/ActioningView.tsx +62 -70
  162. package/src/components/planning/ManualActionCard.tsx +66 -14
  163. package/src/components/planning/ManualActionList.tsx +30 -11
  164. package/src/components/planning/actioning-view-utils.ts +27 -1
  165. package/src/components/tooltip/index.tsx +35 -2
  166. package/src/graph/entity-link.tsx +20 -0
  167. package/src/graph/graph-list-utils.ts +2 -0
  168. package/src/graph/graph-model.ts +15 -6
  169. package/src/graph/nodes/BrandAssetAnalysisView.tsx +3 -1
  170. package/src/graph/nodes/BrandFontSuggestionView.tsx +7 -1
  171. package/src/graph/nodes/BrandGuidelinesView.tsx +1 -1
  172. package/src/graph/nodes/NodePropertyTable.tsx +32 -143
  173. package/src/graph/nodes/node-property-row.tsx +154 -0
  174. package/src/graph/property-formatters.ts +5 -1
  175. package/src/graph/special-nodes/AttributeList.tsx +177 -0
  176. package/src/graph/special-nodes/EntityPreview.tsx +27 -0
  177. package/src/graph/special-nodes/EntityValueChip.tsx +85 -0
  178. package/src/graph/special-nodes/GenericView.tsx +49 -139
  179. package/src/graph/special-nodes/card-facts.ts +278 -0
  180. package/src/graph/special-nodes/property-icons.ts +59 -0
  181. package/src/graph/status-utils.ts +40 -0
  182. package/src/graph/types.ts +4 -0
  183. package/src/hooks/use-approve-anyway.ts +100 -0
  184. package/src/ldo-ui.ts +8 -0
  185. package/src/lib/manual-actions.ts +3 -2
  186. package/src/providers/ActiveChatProvider.tsx +3 -1
  187. package/src/providers/ChatManagerProvider.tsx +3 -1
  188. package/src/providers/chat-socket-auth.ts +95 -16
  189. package/src/providers/index.tsx +16 -1
  190. package/src/routes/activity/detail/ActivityChanges.tsx +21 -5
  191. package/src/routes/activity/detail/ActivityDetail.tsx +24 -5
  192. package/src/routes/activity/detail/ActivityStepsRail.tsx +4 -2
  193. package/src/routes/activity/detail/ActivityTimeline.tsx +2 -2
  194. package/src/routes/activity/detail/NoChangesMessage.tsx +20 -0
  195. package/src/routes/data-explorer/components/RawTriples.tsx +1 -1
  196. package/src/routes/data-explorer/components/RecentTypes.tsx +1 -1
  197. package/src/routes/data-explorer/components/class-detail/Instances.tsx +1 -1
  198. package/src/routes/data-explorer/components/namespace-detail/NamespaceClassRow.tsx +1 -1
  199. package/src/routes/data-explorer/components/ontology-coverage/CoverageTermColumns.tsx +1 -1
  200. package/src/routes/data-explorer/components/ontology-coverage/UnobservedRuleVocabulary.tsx +1 -1
  201. package/src/routes/data-explorer/components/record-detail/ChatPreview.tsx +2 -2
  202. package/src/routes/integrations/components/connect-forms/DataConnectionConnectForm.tsx +38 -12
  203. package/src/routes/integrations/components/connect-forms/data-connection-fields.ts +11 -2
  204. package/src/routes/plans/components/ActionPlanPage.tsx +9 -8
  205. package/src/routes/plans/components/PlanDetailRouteContent.tsx +4 -3
  206. package/dist/components/planning/ExecutionActivityRow.d.ts +0 -9
  207. package/dist/components/planning/ExecutionActivityRow.d.ts.map +0 -1
  208. package/dist/components/planning/ExecutionActivityRow.js +0 -53
  209. package/dist/components/planning/ExecutionActivityRow.js.map +0 -1
  210. package/dist/components/planning/ExecutionSectionItem.d.ts +0 -20
  211. package/dist/components/planning/ExecutionSectionItem.d.ts.map +0 -1
  212. package/dist/components/planning/ExecutionSectionItem.js +0 -66
  213. package/dist/components/planning/ExecutionSectionItem.js.map +0 -1
  214. package/dist/components/planning/ExecutionSections.d.ts +0 -16
  215. package/dist/components/planning/ExecutionSections.d.ts.map +0 -1
  216. package/dist/components/planning/ExecutionSections.js +0 -32
  217. package/dist/components/planning/ExecutionSections.js.map +0 -1
  218. package/dist/components/planning/action-plan-icons.d.ts +0 -3
  219. package/dist/components/planning/action-plan-icons.d.ts.map +0 -1
  220. package/dist/components/planning/action-plan-icons.js +0 -25
  221. package/dist/components/planning/action-plan-icons.js.map +0 -1
  222. package/dist/components/planning/execution-section-model.d.ts +0 -24
  223. package/dist/components/planning/execution-section-model.d.ts.map +0 -1
  224. package/dist/components/planning/execution-section-model.js +0 -150
  225. package/dist/components/planning/execution-section-model.js.map +0 -1
  226. package/dist/routes/data-explorer/components/TruncateTooltip.d.ts +0 -7
  227. package/dist/routes/data-explorer/components/TruncateTooltip.d.ts.map +0 -1
  228. package/dist/routes/data-explorer/components/TruncateTooltip.js +0 -9
  229. package/dist/routes/data-explorer/components/TruncateTooltip.js.map +0 -1
  230. package/src/components/planning/ExecutionActivityRow.tsx +0 -89
  231. package/src/components/planning/ExecutionSectionItem.tsx +0 -184
  232. package/src/components/planning/ExecutionSections.tsx +0 -97
  233. package/src/components/planning/action-plan-icons.tsx +0 -27
  234. package/src/components/planning/execution-section-model.ts +0 -190
  235. package/src/routes/data-explorer/components/TruncateTooltip.tsx +0 -25
@@ -1,9 +1,35 @@
1
1
  import { GQLActionPlanState } from "@manta-eu/client";
2
+ import { isPresent } from "@manta-eu/lib/present";
2
3
  import { match } from "ts-pattern";
3
- import { parseTimestampMs } from "./execution-section-model";
4
+
5
+ export function parseTimestampMs(timestamp: string | null | undefined): number | null {
6
+ if (!timestamp) return null;
7
+ const value = new Date(timestamp).getTime();
8
+ return Number.isFinite(value) ? value : null;
9
+ }
4
10
 
5
11
  export type ExecutionStatus = "idle" | "running" | "completed" | "failed" | "needs-approval";
6
12
 
13
+ export type RunSurface = "completed-changes" | "run" | "starting-run" | "loading" | "none";
14
+
15
+ export type DeriveRunSurfaceArgs = {
16
+ executionStatus: ExecutionStatus;
17
+ runIri: string | null;
18
+ hasCompletedChangesSlot: boolean;
19
+ isLoading: boolean;
20
+ };
21
+
22
+ export function deriveRunSurface(args: DeriveRunSurfaceArgs): RunSurface {
23
+ const { executionStatus, runIri, hasCompletedChangesSlot, isLoading } = args;
24
+
25
+ if (executionStatus === "completed" && hasCompletedChangesSlot) return "completed-changes";
26
+ if (isPresent(runIri)) return "run";
27
+ if (executionStatus === "running" || executionStatus === "needs-approval") return "starting-run";
28
+ if (isLoading) return "loading";
29
+
30
+ return "none";
31
+ }
32
+
7
33
  export type DeriveExecutionStatusArgs = {
8
34
  provenance: {
9
35
  planErrorMessage: string | null;
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
 
3
3
  import * as TooltipPrimitive from "@radix-ui/react-tooltip";
4
- import type { ComponentPropsWithoutRef, ElementRef } from "react";
5
- import { forwardRef } from "react";
4
+ import type { ComponentPropsWithoutRef, ElementRef, ReactNode } from "react";
5
+ import { forwardRef, useState } from "react";
6
6
  import { cn } from "#style";
7
7
 
8
8
  export const TooltipProvider = TooltipPrimitive.Provider;
@@ -43,3 +43,36 @@ export const TooltipContent = forwardRef<ElementRef<typeof TooltipPrimitive.Cont
43
43
  ),
44
44
  );
45
45
  TooltipContent.displayName = "TooltipContent";
46
+
47
+ export type TruncateTooltipProps = { text: string; className: string; children?: ReactNode };
48
+
49
+ export function TruncateTooltip(props: TruncateTooltipProps) {
50
+ const { text, className, children } = props;
51
+
52
+ const [isTruncated, setIsTruncated] = useState(false);
53
+
54
+ return (
55
+ <Tooltip>
56
+ <TooltipTrigger asChild>
57
+ <span
58
+ className={className}
59
+ onMouseEnter={(event) => {
60
+ const element = event.currentTarget;
61
+ // Line height can round a single line 1px taller than its box, so only a real clamp counts.
62
+ setIsTruncated(
63
+ element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight + 1,
64
+ );
65
+ }}
66
+ >
67
+ {children ?? text}
68
+ </span>
69
+ </TooltipTrigger>
70
+
71
+ {isTruncated && (
72
+ <TooltipContent showArrow={false} className="max-h-64 max-w-sm overflow-y-auto break-words text-left">
73
+ <p className="typography-tiny">{text}</p>
74
+ </TooltipContent>
75
+ )}
76
+ </Tooltip>
77
+ );
78
+ }
@@ -0,0 +1,20 @@
1
+ import { type ComponentType, createContext, type ReactNode, useContext } from "react";
2
+
3
+ export type EntityLinkProps = { iri: string; className?: string; children: ReactNode };
4
+
5
+ export type EntityLinkComponent = ComponentType<EntityLinkProps>;
6
+
7
+ // The host app supplies how an entity opens (its router and record route); without one, entities render unlinked.
8
+ const EntityLinkContext = createContext<EntityLinkComponent | undefined>(undefined);
9
+
10
+ export type EntityLinkProviderProps = { component: EntityLinkComponent; children: ReactNode };
11
+
12
+ export function EntityLinkProvider(props: EntityLinkProviderProps) {
13
+ const { component, children } = props;
14
+
15
+ return <EntityLinkContext.Provider value={component}>{children}</EntityLinkContext.Provider>;
16
+ }
17
+
18
+ export function useEntityLink(): EntityLinkComponent | undefined {
19
+ return useContext(EntityLinkContext);
20
+ }
@@ -87,6 +87,7 @@ export type NodePropertyChange = {
87
87
 
88
88
  export type NodePropertyPreview = {
89
89
  id: string;
90
+ predicate?: string;
90
91
  label: string;
91
92
  value: string;
92
93
  changeType?: PropertyChangeType;
@@ -162,6 +163,7 @@ export function selectPriorityPreviewFields(
162
163
 
163
164
  return {
164
165
  id: `${property.key}-${index}`,
166
+ predicate: property.iri,
165
167
  label: property.label ?? propertyLabelFromKey(property.key),
166
168
  value,
167
169
  rawValue: String(property.value),
@@ -1,3 +1,4 @@
1
+ import { isFullIri } from "@manta-eu/client/shared/rdf-namespaces";
1
2
  import { labelPredicateSuffixes } from "@manta-eu/rdf-lib/detail-view-types";
2
3
  import { expandPrefix, wellKnownPredicates } from "@manta-eu/rdf-lib/namespaces";
3
4
  import type { RdfGraph } from "../lib/rdf-helpers";
@@ -10,7 +11,7 @@ import {
10
11
  import { humanizePredicateName, type NodesAndEdges, type RDFEdge, shortenUri } from "./graph-utils";
11
12
  import { readVocabValue } from "./nodes/typed-node-helpers";
12
13
  import { mantaLightTheme } from "./themes";
13
- import type { LayoutDirection, NodeProperty, RDFNode } from "./types";
14
+ import type { LayoutDirection, NodeProperty, NodeRelation, RDFNode } from "./types";
14
15
 
15
16
  const labelPredicate = wellKnownPredicates.label;
16
17
  const rdfType = wellKnownPredicates.type;
@@ -44,6 +45,7 @@ type NodeAccumulator = {
44
45
  label?: string;
45
46
  type?: string;
46
47
  data: Record<string, unknown>;
48
+ relations: NodeRelation[];
47
49
  };
48
50
 
49
51
  type GraphEdgeRecord = {
@@ -60,7 +62,7 @@ function createOrGetNodeAccumulator(
60
62
  ): NodeAccumulator {
61
63
  let node = nodeData.get(subjectId);
62
64
  if (!node) {
63
- node = { data: {} };
65
+ node = { data: {}, relations: [] };
64
66
  nodeData.set(subjectId, node);
65
67
  connectionCounts.set(subjectId, 0);
66
68
  }
@@ -159,7 +161,7 @@ function buildNodesAndEdges(
159
161
  const allNodeIds = new Set(nodeData.keys());
160
162
  const hubThreshold = computeHubThreshold(connectionCounts);
161
163
 
162
- const graphNodes: RDFNode[] = Array.from(nodeData).map(([id, { label, type, data }]) => {
164
+ const graphNodes: RDFNode[] = Array.from(nodeData).map(([id, { label, type, data, relations }]) => {
163
165
  const childIds = (parentChildMap.get(id) || []).filter((childId) => allNodeIds.has(childId));
164
166
  const parentId = childParentMap.get(id);
165
167
  const depth = nodeDepths.get(id) || 0;
@@ -193,6 +195,7 @@ function buildNodesAndEdges(
193
195
  isHub: connCount >= hubThreshold,
194
196
  jsonLd: { "@id": id, "@type": type, ...data },
195
197
  properties,
198
+ relations,
196
199
  childIds,
197
200
  parentId,
198
201
  isCollapsed: false,
@@ -334,11 +337,15 @@ export function rdfGraphToNodesAndEdges(
334
337
  const longNumericPattern = /\d{10,}/;
335
338
  const leafValueSchemes = ["http://", "https://", "s3://"];
336
339
 
340
+ // A stored file or web address is a value to render, not a neighbor (without `s3:`, a long digit run reads as a ref).
341
+ function isLeafValue(targetId: string): boolean {
342
+ const isNamespacedIri = targetId.includes("#") || targetId.includes("vocab");
343
+ return !isNamespacedIri && leafValueSchemes.some((scheme) => targetId.startsWith(scheme));
344
+ }
345
+
337
346
  function isEnumLikeRef(targetId: string): boolean {
338
347
  if (definedSubjects.has(targetId)) return false;
339
- const isNamespacedIri = targetId.includes("#") || targetId.includes("vocab");
340
- // A stored file is a value to render, not a neighbor: without `s3:` a key with a long digit run reads as a ref.
341
- if (!isNamespacedIri && leafValueSchemes.some((scheme) => targetId.startsWith(scheme))) return true;
348
+ if (isLeafValue(targetId)) return true;
342
349
  const localName = targetId.split(/[#/]/).pop() ?? targetId;
343
350
  if (uuidPattern.test(localName) || longNumericPattern.test(localName)) return false;
344
351
  return true;
@@ -379,10 +386,12 @@ export function rdfGraphToNodesAndEdges(
379
386
 
380
387
  if (isEnumLikeRef(targetId)) {
381
388
  mergeNodeValue(node.data, predicate, targetId);
389
+ if (!isLeafValue(targetId) && isFullIri(targetId)) node.relations.push({ predicate, target: targetId });
382
390
  continue;
383
391
  }
384
392
 
385
393
  createOrGetNodeAccumulator(nodeData, connectionCounts, targetId);
394
+ if (isFullIri(targetId)) node.relations.push({ predicate, target: targetId });
386
395
  connectionCounts.set(subjectId, (connectionCounts.get(subjectId) || 0) + 1);
387
396
  connectionCounts.set(targetId, (connectionCounts.get(targetId) || 0) + 1);
388
397
 
@@ -13,6 +13,7 @@ import { useRedirectEndpoint } from "#hooks/use-redirect-endpoint";
13
13
  const filePredicate = `${namespaces.brand}file`;
14
14
  const mimePredicate = `${namespaces.nmo}contentMimeType`;
15
15
  const descriptionPredicate = `${namespaces.brand}assetDescription`;
16
+ const listedElsewhere = [filePredicate, mimePredicate, descriptionPredicate];
16
17
 
17
18
  // The generated schema requires `m:file`, which only the planned request carries, so a lens read never matches.
18
19
  export const BrandAssetAnalysisView = memo(function BrandAssetAnalysisView(props: EntityViewProps) {
@@ -46,8 +47,9 @@ export const BrandAssetAnalysisView = memo(function BrandAssetAnalysisView(props
46
47
 
47
48
  <NodePropertyTable
48
49
  properties={data.properties}
50
+ relations={data.relations}
49
51
  jsonLd={data.jsonLd}
50
- omitPredicates={[filePredicate, mimePredicate, descriptionPredicate]}
52
+ omitPredicates={listedElsewhere}
51
53
  />
52
54
  </BrandNodeBody>
53
55
  </EntityCardShell>
@@ -23,6 +23,7 @@ import { useFontFace } from "#hooks/use-font-face";
23
23
  import { useRedirectEndpoint } from "#hooks/use-redirect-endpoint";
24
24
 
25
25
  const weightPredicate = `${namespaces.brand}hasSuggestedWeight`;
26
+ const listedElsewhere = [weightPredicate];
26
27
  const specimenText = "Aa Bb Cc Qq Rr 0123456789 — The quick brown fox jumps over the lazy dog";
27
28
 
28
29
  function useSuggestedWeights(iris: string[]): SuggestedWeight[] {
@@ -118,7 +119,12 @@ export const BrandFontSuggestionView = memo(function BrandFontSuggestionView(pro
118
119
  </ul>
119
120
  )}
120
121
 
121
- <NodePropertyTable properties={data.properties} jsonLd={data.jsonLd} omitPredicates={[weightPredicate]} />
122
+ <NodePropertyTable
123
+ properties={data.properties}
124
+ jsonLd={data.jsonLd}
125
+ relations={data.relations}
126
+ omitPredicates={listedElsewhere}
127
+ />
122
128
  </BrandNodeBody>
123
129
  </EntityCardShell>
124
130
  );
@@ -18,7 +18,7 @@ export const BrandGuidelinesView = memo(function BrandGuidelinesView(props: Enti
18
18
  onContextMenu={onContextMenu}
19
19
  >
20
20
  <BrandNodeBody>
21
- <NodePropertyTable properties={data.properties} jsonLd={data.jsonLd} />
21
+ <NodePropertyTable properties={data.properties} jsonLd={data.jsonLd} relations={data.relations} />
22
22
  </BrandNodeBody>
23
23
  </EntityCardShell>
24
24
  );
@@ -1,15 +1,16 @@
1
- import { isPresent } from "@manta-eu/lib/present";
2
- import { Check, ChevronDown, ChevronRight, X } from "lucide-react";
3
- import { useMemo, useState } from "react";
4
- import { match, P } from "ts-pattern";
1
+ import { useCallback, useMemo } from "react";
5
2
  import { shouldHidePreviewField } from "../graph-list-utils";
6
- import { ontologyTermIrisForProperties, withOntologyLabels } from "../ontology-labels";
7
- import { formatDisplayValue } from "../property-formatters";
8
- import type { NodeProperty } from "../types";
9
- import { classifyPropertyDisplay, type PropertyDisplay } from "./node-property-display";
10
- import { useOntologyTerms } from "#hooks/use-ontology-terms";
11
- import { formatPropertyLabel } from "#lib/iri-labels";
12
- import { cn } from "#style";
3
+ import { AttributeList, useAttributeFacts } from "../special-nodes/AttributeList";
4
+ import type { NodeProperty, NodeRelation } from "../types";
5
+
6
+ export {
7
+ NodePropertyExpander,
8
+ NodePropertyList,
9
+ NodePropertyRow,
10
+ PropertyValue,
11
+ type NodePropertyExpanderProps,
12
+ type NodePropertyRowProps,
13
+ } from "./node-property-row";
13
14
 
14
15
  export type NodePropertyTableProps = {
15
16
  properties: NodeProperty[];
@@ -17,145 +18,33 @@ export type NodePropertyTableProps = {
17
18
  jsonLd?: Record<string, unknown>;
18
19
  // Rendered above the table by the card itself, so the table skips them.
19
20
  omitPredicates?: readonly string[];
21
+ relations?: NodeRelation[];
20
22
  };
21
23
 
22
- function Tag(props: { children: string }) {
23
- return (
24
- <span className="rounded bg-faint px-1.5 py-[1px] typography-tiny text-default-foreground">{props.children}</span>
25
- );
26
- }
27
-
28
- function PropertyValue(props: { display: PropertyDisplay }) {
29
- return match(props.display)
30
- .with({ kind: "tags" }, ({ values }) => (
31
- <div className="flex flex-wrap gap-1">
32
- {values.map((value) => (
33
- <Tag key={value}>{value}</Tag>
34
- ))}
35
- </div>
36
- ))
37
- .with({ kind: "color" }, ({ hex }) => (
38
- <span className="flex items-center gap-1.5">
39
- <span
40
- aria-hidden
41
- className="size-3.5 shrink-0 rounded-sm border border-default-border"
42
- style={{ backgroundColor: hex }}
43
- />
44
- <span className="tabular-nums">{hex}</span>
45
- </span>
46
- ))
47
- .with({ kind: "ratio" }, ({ fraction, text }) => (
48
- <span className="flex items-center gap-1.5">
49
- <span aria-hidden className="h-1 w-12 shrink-0 overflow-hidden rounded-full bg-faint">
50
- <span className="block h-full rounded-full bg-accent-foreground" style={{ width: `${fraction * 100}%` }} />
51
- </span>
52
- <span className="tabular-nums">{text}</span>
53
- </span>
54
- ))
55
- .with({ kind: "number" }, ({ text }) => <span className="tabular-nums">{text}</span>)
56
- .with({ kind: "boolean" }, ({ value }) =>
57
- value ? (
58
- <span className="flex items-center gap-1">
59
- <Check aria-hidden className="size-3.5 shrink-0 text-success-foreground" />
60
- true
61
- </span>
62
- ) : (
63
- <span className="flex items-center gap-1">
64
- <X aria-hidden className="size-3.5 shrink-0 text-muted-foreground" />
65
- false
66
- </span>
67
- ),
68
- )
69
- .with(P.union({ kind: "prose" }, { kind: "text" }), ({ text }) => <span>{text}</span>)
70
- .exhaustive();
71
- }
72
-
73
- type Row = { key: string; label: string; display: PropertyDisplay };
74
-
75
- function PropertyRow(props: { row: Row }) {
76
- const { row } = props;
77
-
78
- return (
79
- <div className="-mx-1 grid grid-cols-[minmax(0,7rem)_minmax(0,1fr)] items-baseline gap-x-3 gap-y-1 rounded-md border-t border-muted-border/50 px-1 py-1.5 transition-colors first:border-t-0 hover:bg-muted/30">
80
- <span className="typography-tiny min-w-0 break-words text-muted-foreground first-letter:uppercase">
81
- {row.label}
82
- </span>
83
- <div
84
- className={cn(
85
- "typography-tiny min-w-0 break-words text-default-foreground",
86
- // A long description reads better as its own full-width block under the label.
87
- row.display.kind === "prose" && "col-span-2",
88
- )}
89
- >
90
- <PropertyValue display={row.display} />
91
- </div>
92
- </div>
93
- );
94
- }
24
+ const noJsonLd: Record<string, unknown> = {};
95
25
 
96
- // Every property the node carries, laid out like the record-detail field list. Rule-derived
26
+ // Every property the node carries, in the same attribute list the fallback card uses. Rule-derived
97
27
  // properties are included: they are real on the instance even though no shape declares them.
98
- // Short values keep the compact two-column rhythm; prose paragraphs collapse behind the same
99
- // "Show more" expander AttioNodeBody uses, so the card stays a summary until asked.
100
28
  export function NodePropertyTable(props: NodePropertyTableProps) {
101
- const { properties, jsonLd, omitPredicates = [] } = props;
29
+ const { properties, jsonLd = noJsonLd, omitPredicates, relations } = props;
102
30
 
103
- const [expanded, setExpanded] = useState(false);
104
-
105
- const terms = useOntologyTerms(useMemo(() => ontologyTermIrisForProperties(properties), [properties]));
106
-
107
- const rows = useMemo(() => {
108
- const omitted = new Set(omitPredicates);
109
-
110
- return withOntologyLabels(properties, terms)
111
- .filter(
112
- (property) =>
113
- !omitted.has(property.iri ?? "") && !shouldHidePreviewField(property.label ?? "", property.iri ?? ""),
114
- )
115
- .map((property): Row => {
116
- const label = property.label || formatPropertyLabel(property.iri ?? property.key);
117
- const formatted = formatDisplayValue(property.value, {
118
- predicate: property.iri,
119
- predicateLabel: property.label,
120
- valueScale: property.valueScale,
121
- });
122
-
123
- return {
124
- key: property.iri ?? property.key,
125
- label,
126
- display: classifyPropertyDisplay({ label, formatted, raw: jsonLd?.[property.iri ?? ""] ?? property.value }),
127
- };
128
- });
129
- }, [properties, terms, jsonLd, omitPredicates]);
130
-
131
- const compactRows = rows.filter((row) => row.display.kind !== "prose");
132
- const proseRows = rows.filter((row) => row.display.kind === "prose");
31
+ const include = useCallback(
32
+ (property: NodeProperty) =>
33
+ !(omitPredicates ?? []).includes(property.iri ?? "") &&
34
+ !shouldHidePreviewField(property.label ?? "", property.iri ?? ""),
35
+ [omitPredicates],
36
+ );
133
37
 
134
- if (!isPresent(rows)) return null;
38
+ const visibleRelations = useMemo(
39
+ () =>
40
+ relations?.filter(
41
+ (relation) =>
42
+ !(omitPredicates ?? []).includes(relation.predicate) && !shouldHidePreviewField("", relation.predicate),
43
+ ),
44
+ [relations, omitPredicates],
45
+ );
135
46
 
136
- return (
137
- <div className="flex flex-col">
138
- {compactRows.map((row) => (
139
- <PropertyRow key={row.key} row={row} />
140
- ))}
47
+ const { facts, previewCount } = useAttributeFacts({ properties, relations: visibleRelations, jsonLd, include });
141
48
 
142
- {isPresent(proseRows) && (
143
- <>
144
- {expanded && proseRows.map((row) => <PropertyRow key={row.key} row={row} />)}
145
- <button
146
- type="button"
147
- onClick={() => setExpanded((value) => !value)}
148
- className="nodrag flex items-center gap-1 pt-1.5 typography-tiny-medium text-muted-foreground hover:text-default-foreground"
149
- >
150
- {expanded ? (
151
- <ChevronDown size={11} className="shrink-0" />
152
- ) : (
153
- <ChevronRight size={11} className="shrink-0" />
154
- )}
155
- {expanded ? "Show less" : `Show ${proseRows.length} more`}
156
- </button>
157
- </>
158
- )}
159
- </div>
160
- );
49
+ return <AttributeList facts={facts} previewCount={previewCount} showAll />;
161
50
  }
@@ -0,0 +1,154 @@
1
+ import { Check, ChevronDown, ChevronRight, type LucideIcon, X } from "lucide-react";
2
+ import type { ReactNode } from "react";
3
+ import { match, P } from "ts-pattern";
4
+ import type { PropertyDisplay } from "./node-property-display";
5
+ import { TruncateTooltip } from "#components/tooltip/index";
6
+ import { cn } from "#style";
7
+
8
+ function Tag(props: { children: string }) {
9
+ return (
10
+ <span className="rounded bg-faint px-1.5 py-[1px] typography-tiny text-default-foreground">{props.children}</span>
11
+ );
12
+ }
13
+
14
+ export function PropertyValue(props: { display: PropertyDisplay }) {
15
+ return match(props.display)
16
+ .with({ kind: "tags" }, ({ values }) => (
17
+ <div className="flex flex-wrap gap-1">
18
+ {values.map((value) => (
19
+ <Tag key={value}>{value}</Tag>
20
+ ))}
21
+ </div>
22
+ ))
23
+ .with({ kind: "color" }, ({ hex }) => (
24
+ <span className="flex items-center gap-1.5">
25
+ <span
26
+ aria-hidden
27
+ className="size-3.5 shrink-0 rounded-sm border border-default-border"
28
+ style={{ backgroundColor: hex }}
29
+ />
30
+ <span className="tabular-nums">{hex}</span>
31
+ </span>
32
+ ))
33
+ .with({ kind: "ratio" }, ({ fraction, text }) => (
34
+ <span className="flex items-center gap-1.5">
35
+ <span aria-hidden className="h-1 w-12 shrink-0 overflow-hidden rounded-full bg-faint">
36
+ <span className="block h-full rounded-full bg-accent-foreground" style={{ width: `${fraction * 100}%` }} />
37
+ </span>
38
+ <span className="tabular-nums">{text}</span>
39
+ </span>
40
+ ))
41
+ .with({ kind: "number" }, ({ text }) => <span className="tabular-nums">{text}</span>)
42
+ .with({ kind: "boolean" }, ({ value }) =>
43
+ value ? (
44
+ <span className="flex items-center gap-1">
45
+ <Check aria-hidden className="size-3.5 shrink-0 text-success-foreground" />
46
+ true
47
+ </span>
48
+ ) : (
49
+ <span className="flex items-center gap-1">
50
+ <X aria-hidden className="size-3.5 shrink-0 text-muted-foreground" />
51
+ false
52
+ </span>
53
+ ),
54
+ )
55
+ .with(P.union({ kind: "prose" }, { kind: "text" }), ({ text }) => <span>{text}</span>)
56
+ .exhaustive();
57
+ }
58
+
59
+ export function NodePropertyList(props: { children: ReactNode }) {
60
+ return <dl className="flex flex-col">{props.children}</dl>;
61
+ }
62
+
63
+ export type NodePropertyRowProps = {
64
+ label: string;
65
+ icon?: LucideIcon;
66
+ labelPrefix?: ReactNode;
67
+ children: ReactNode;
68
+ fullWidth?: boolean;
69
+ overflow?: "wrap" | "truncate" | "clamp";
70
+ valueTitle?: string;
71
+ divided?: boolean;
72
+ wideLabel?: boolean;
73
+ emphasized?: boolean;
74
+ valueClassName?: string;
75
+ };
76
+
77
+ // Render inside NodePropertyList so the dt/dd pairs sit in a dl.
78
+ export function NodePropertyRow(props: NodePropertyRowProps) {
79
+ const {
80
+ label,
81
+ icon: Icon,
82
+ labelPrefix,
83
+ children,
84
+ fullWidth = false,
85
+ overflow = "wrap",
86
+ valueTitle,
87
+ divided = true,
88
+ wideLabel = false,
89
+ emphasized = false,
90
+ valueClassName,
91
+ } = props;
92
+
93
+ const clipped = overflow !== "wrap";
94
+ const spansRow = fullWidth || overflow === "clamp";
95
+
96
+ return (
97
+ <div
98
+ className={cn(
99
+ "grid items-baseline gap-x-3 gap-y-1",
100
+ // The wide label column grows with the card, never with the rows shown, so Show all never shifts it.
101
+ wideLabel ? "grid-cols-[minmax(8rem,40%)_minmax(0,1fr)]" : "grid-cols-[minmax(0,7rem)_minmax(0,1fr)]",
102
+ divided ? "border-t border-muted-border/50 py-1.5 first:border-t-0" : "py-1",
103
+ )}
104
+ >
105
+ <dt className={cn("flex min-w-0 items-baseline gap-1.5 text-muted-foreground", spansRow && "col-span-2")}>
106
+ {Icon && <Icon aria-hidden className="size-3 shrink-0 self-center" />}
107
+ {labelPrefix}
108
+ {overflow === "truncate" ? (
109
+ <TruncateTooltip text={label} className="typography-tiny min-w-0 truncate first-letter:uppercase" />
110
+ ) : (
111
+ <span className="typography-tiny min-w-0 break-words first-letter:uppercase">{label}</span>
112
+ )}
113
+ </dt>
114
+ <dd
115
+ className={cn(
116
+ emphasized ? "typography-tiny-medium" : "typography-tiny",
117
+ "min-w-0 text-default-foreground",
118
+ overflow === "wrap" && "break-words",
119
+ spansRow && "col-span-2",
120
+ valueClassName,
121
+ )}
122
+ >
123
+ {clipped && valueTitle !== undefined ? (
124
+ <TruncateTooltip
125
+ text={valueTitle}
126
+ className={cn("block min-w-0", overflow === "truncate" ? "truncate" : "line-clamp-4 break-words")}
127
+ >
128
+ {children}
129
+ </TruncateTooltip>
130
+ ) : (
131
+ children
132
+ )}
133
+ </dd>
134
+ </div>
135
+ );
136
+ }
137
+
138
+ export type NodePropertyExpanderProps = { expanded: boolean; collapsedLabel: string; onToggle: () => void };
139
+
140
+ export function NodePropertyExpander(props: NodePropertyExpanderProps) {
141
+ const { expanded, collapsedLabel, onToggle } = props;
142
+
143
+ return (
144
+ <button
145
+ type="button"
146
+ onClick={onToggle}
147
+ aria-expanded={expanded}
148
+ className="nodrag flex items-center gap-1 pt-1.5 typography-tiny-medium text-muted-foreground hover:text-default-foreground"
149
+ >
150
+ {expanded ? <ChevronDown size={11} className="shrink-0" /> : <ChevronRight size={11} className="shrink-0" />}
151
+ {expanded ? "Show less" : collapsedLabel}
152
+ </button>
153
+ );
154
+ }
@@ -135,6 +135,10 @@ function humanizeToken(value: string): string {
135
135
  return value;
136
136
  }
137
137
 
138
+ export function isIsoDateTime(value: string): boolean {
139
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value);
140
+ }
141
+
138
142
  export function formatDisplayValue(
139
143
  value: string,
140
144
  options?: {
@@ -162,7 +166,7 @@ export function formatDisplayValue(
162
166
 
163
167
  if (combinedLabel.includes("currency")) return value;
164
168
 
165
- if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)) {
169
+ if (isIsoDateTime(value)) {
166
170
  const date = parseISO(value);
167
171
 
168
172
  if (isValid(date)) return format(date, "MMM d, yyyy");