@_linked/shape-ui 2.0.0 → 2.3.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 (207) hide show
  1. package/lib/esm/columns.d.ts +58 -0
  2. package/lib/esm/columns.js +173 -0
  3. package/lib/esm/columns.js.map +1 -0
  4. package/lib/esm/components/AddInstanceForms.d.ts +38 -0
  5. package/lib/esm/components/AddInstanceForms.js +143 -0
  6. package/lib/esm/components/AddInstanceForms.js.map +1 -0
  7. package/lib/esm/components/AddInstanceForms.module.css +12 -0
  8. package/lib/esm/components/BatchEditDrawer.d.ts +13 -0
  9. package/lib/esm/components/BatchEditDrawer.js +122 -0
  10. package/lib/esm/components/BatchEditDrawer.js.map +1 -0
  11. package/lib/esm/components/BatchEditDrawer.module.css +193 -0
  12. package/lib/esm/components/CustomMultiSelect.d.ts +26 -0
  13. package/lib/esm/components/CustomMultiSelect.js +344 -0
  14. package/lib/esm/components/CustomMultiSelect.js.map +1 -0
  15. package/lib/esm/components/CustomMultiSelect.module.css +291 -0
  16. package/lib/esm/components/DynamicFormField.d.ts +21 -0
  17. package/lib/esm/components/DynamicFormField.js +149 -0
  18. package/lib/esm/components/DynamicFormField.js.map +1 -0
  19. package/lib/esm/components/DynamicFormField.module.css +169 -0
  20. package/lib/esm/components/EditInstanceForms.d.ts +15 -0
  21. package/lib/esm/components/EditInstanceForms.js +118 -0
  22. package/lib/esm/components/EditInstanceForms.js.map +1 -0
  23. package/lib/esm/components/FilterBadge.d.ts +20 -0
  24. package/lib/esm/components/FilterBadge.js +26 -0
  25. package/lib/esm/components/FilterBadge.js.map +1 -0
  26. package/lib/esm/components/FilterBadge.module.css +25 -0
  27. package/lib/esm/components/FormField.d.ts +18 -0
  28. package/lib/esm/components/FormField.js +9 -0
  29. package/lib/esm/components/FormField.js.map +1 -0
  30. package/lib/esm/components/FormField.module.css +57 -0
  31. package/lib/esm/components/InstanceDeletionDialog.d.ts +17 -0
  32. package/lib/esm/components/InstanceDeletionDialog.js +83 -0
  33. package/lib/esm/components/InstanceDeletionDialog.js.map +1 -0
  34. package/lib/esm/components/InstanceDeletionDialog.module.css +59 -0
  35. package/lib/esm/components/InstanceOverview.d.ts +43 -0
  36. package/lib/esm/components/InstanceOverview.js +236 -0
  37. package/lib/esm/components/InstanceOverview.js.map +1 -0
  38. package/lib/esm/components/InstanceOverview.module.css +35 -0
  39. package/lib/esm/components/InstanceView.d.ts +29 -0
  40. package/lib/esm/components/InstanceView.js +97 -0
  41. package/lib/esm/components/InstanceView.js.map +1 -0
  42. package/lib/esm/components/InstanceView.module.css +157 -0
  43. package/lib/esm/components/NodeBadge.d.ts +25 -0
  44. package/lib/esm/components/NodeBadge.js +29 -0
  45. package/lib/esm/components/NodeBadge.js.map +1 -0
  46. package/lib/esm/components/NodeValuesEditor.d.ts +15 -0
  47. package/lib/esm/components/NodeValuesEditor.js +29 -0
  48. package/lib/esm/components/NodeValuesEditor.js.map +1 -0
  49. package/lib/esm/components/NodeValuesEditor.module.css +4 -0
  50. package/lib/esm/components/ReactTable.d.ts +59 -0
  51. package/lib/esm/components/ReactTable.js +378 -0
  52. package/lib/esm/components/ReactTable.js.map +1 -0
  53. package/lib/esm/components/ReactTable.module.css +950 -0
  54. package/lib/esm/components/TextfieldEditor.d.ts +1 -2
  55. package/lib/esm/components/TextfieldEditor.js +2 -2
  56. package/lib/esm/components/TextfieldEditor.js.map +1 -1
  57. package/lib/esm/components/forms.module.css +96 -0
  58. package/lib/esm/host.d.ts +196 -0
  59. package/lib/esm/host.js +25 -0
  60. package/lib/esm/host.js.map +1 -0
  61. package/lib/esm/hostContext.d.ts +34 -0
  62. package/lib/esm/hostContext.js +95 -0
  63. package/lib/esm/hostContext.js.map +1 -0
  64. package/lib/esm/index.d.ts +60 -10
  65. package/lib/esm/index.js +60 -14
  66. package/lib/esm/index.js.map +1 -1
  67. package/lib/esm/read.d.ts +48 -0
  68. package/lib/esm/read.js +96 -0
  69. package/lib/esm/read.js.map +1 -0
  70. package/lib/esm/search.d.ts +32 -0
  71. package/lib/esm/search.js +87 -0
  72. package/lib/esm/search.js.map +1 -0
  73. package/lib/esm/shape/contracts.d.ts +58 -0
  74. package/lib/esm/shape/contracts.js +15 -0
  75. package/lib/esm/shape/contracts.js.map +1 -0
  76. package/lib/esm/shape/naming.d.ts +30 -0
  77. package/lib/esm/shape/naming.js +52 -0
  78. package/lib/esm/shape/naming.js.map +1 -0
  79. package/lib/esm/shape/nodeDisplay.d.ts +36 -0
  80. package/lib/esm/shape/nodeDisplay.js +117 -0
  81. package/lib/esm/shape/nodeDisplay.js.map +1 -0
  82. package/lib/esm/shape/propertyTypes.d.ts +12 -0
  83. package/lib/esm/shape/propertyTypes.js +60 -0
  84. package/lib/esm/shape/propertyTypes.js.map +1 -0
  85. package/lib/esm/shape/propertyVisibility.d.ts +56 -0
  86. package/lib/esm/shape/propertyVisibility.js +112 -0
  87. package/lib/esm/shape/propertyVisibility.js.map +1 -0
  88. package/lib/esm/shape/shapeDependencies.d.ts +41 -0
  89. package/lib/esm/shape/shapeDependencies.js +141 -0
  90. package/lib/esm/shape/shapeDependencies.js.map +1 -0
  91. package/lib/esm/shape/tableFilters.d.ts +35 -0
  92. package/lib/esm/shape/tableFilters.js +171 -0
  93. package/lib/esm/shape/tableFilters.js.map +1 -0
  94. package/lib/esm/shape/useFormDependencies.d.ts +38 -0
  95. package/lib/esm/shape/useFormDependencies.js +178 -0
  96. package/lib/esm/shape/useFormDependencies.js.map +1 -0
  97. package/lib/esm/shape/validation.d.ts +15 -0
  98. package/lib/esm/shape/validation.js +106 -0
  99. package/lib/esm/shape/validation.js.map +1 -0
  100. package/lib/esm/tokens.css +25 -0
  101. package/lib/esm/types.d.ts +22 -0
  102. package/lib/esm/types.js +1 -0
  103. package/lib/esm/types.js.map +1 -0
  104. package/lib/esm/write.d.ts +51 -0
  105. package/lib/esm/write.js +99 -0
  106. package/lib/esm/write.js.map +1 -0
  107. package/package.json +44 -18
  108. package/src/backend.ts +1 -0
  109. package/src/columns.regression.test.ts +143 -0
  110. package/src/columns.test.ts +200 -0
  111. package/src/columns.ts +228 -0
  112. package/src/components/AddInstanceForms.module.css +12 -0
  113. package/src/components/AddInstanceForms.tsx +321 -0
  114. package/src/components/AvatarEditor.tsx +34 -0
  115. package/src/components/BatchEditDrawer.module.css +193 -0
  116. package/src/components/BatchEditDrawer.tsx +295 -0
  117. package/src/components/CheckboxEditor.module.css +6 -0
  118. package/src/components/CheckboxEditor.tsx +86 -0
  119. package/src/components/CustomMultiSelect.module.css +291 -0
  120. package/src/components/CustomMultiSelect.tsx +527 -0
  121. package/src/components/DateEditor.tsx +50 -0
  122. package/src/components/DynamicFormField.module.css +169 -0
  123. package/src/components/DynamicFormField.tsx +357 -0
  124. package/src/components/EditInstanceForms.tsx +260 -0
  125. package/src/components/FilterBadge.module.css +25 -0
  126. package/src/components/FilterBadge.tsx +70 -0
  127. package/src/components/FormField.module.css +57 -0
  128. package/src/components/FormField.tsx +59 -0
  129. package/src/components/ImageValueEditor.module.css +3 -0
  130. package/src/components/ImageValueEditor.tsx +82 -0
  131. package/src/components/InstanceDeletionDialog.module.css +59 -0
  132. package/src/components/InstanceDeletionDialog.tsx +175 -0
  133. package/src/components/InstanceOverview.module.css +35 -0
  134. package/src/components/InstanceOverview.tsx +385 -0
  135. package/src/components/InstanceView.module.css +157 -0
  136. package/src/components/InstanceView.tsx +212 -0
  137. package/src/components/NodeBadge.tsx +60 -0
  138. package/src/components/NodeValuesEditor.module.css +4 -0
  139. package/src/components/NodeValuesEditor.tsx +64 -0
  140. package/src/components/RadioButtonEditor.module.css +22 -0
  141. package/src/components/RadioButtonEditor.tsx +96 -0
  142. package/src/components/ReactTable.module.css +950 -0
  143. package/src/components/ReactTable.tsx +1013 -0
  144. package/src/components/SelectEditor.tsx +85 -0
  145. package/src/components/ShapeTable.module.css +394 -0
  146. package/src/components/SwitchEditor.tsx +60 -0
  147. package/src/components/TextareaEditor.tsx +55 -0
  148. package/src/components/TextfieldEditor.tsx +67 -0
  149. package/src/components/ToggleEditor.tsx +63 -0
  150. package/src/components/forms.module.css +96 -0
  151. package/src/host.ts +226 -0
  152. package/src/hostContext.tsx +114 -0
  153. package/src/index.ts +93 -0
  154. package/src/package.ts +21 -0
  155. package/src/read.ts +148 -0
  156. package/src/search.test.ts +174 -0
  157. package/src/search.ts +108 -0
  158. package/src/shape/contracts.ts +66 -0
  159. package/src/shape/naming.ts +52 -0
  160. package/src/shape/nodeDisplay.ts +114 -0
  161. package/src/shape/propertyTypes.ts +86 -0
  162. package/src/shape/propertyVisibility.ts +180 -0
  163. package/src/shape/shapeDependencies.ts +183 -0
  164. package/src/shape/tableFilters.ts +223 -0
  165. package/src/shape/types.ts +1158 -0
  166. package/src/shape/useFormDependencies.ts +257 -0
  167. package/src/shape/validation.ts +121 -0
  168. package/src/tokens.css +25 -0
  169. package/src/types.ts +20 -0
  170. package/src/utils/helper.ts +35 -0
  171. package/src/write.test.ts +174 -0
  172. package/src/write.ts +114 -0
  173. package/.changeset/README.md +0 -20
  174. package/.changeset/config.json +0 -14
  175. package/.github/workflows/changeset-check.yml +0 -44
  176. package/.github/workflows/ci.yml +0 -43
  177. package/.github/workflows/publish.yml +0 -158
  178. package/CHANGELOG.md +0 -30
  179. package/lib/esm/components/AvatarEditor.d.ts +0 -20
  180. package/lib/esm/components/AvatarEditor.js +0 -7
  181. package/lib/esm/components/AvatarEditor.js.map +0 -1
  182. package/lib/esm/components/CheckboxEditor.d.ts +0 -21
  183. package/lib/esm/components/CheckboxEditor.js +0 -44
  184. package/lib/esm/components/CheckboxEditor.js.map +0 -1
  185. package/lib/esm/components/DateEditor.d.ts +0 -20
  186. package/lib/esm/components/DateEditor.js +0 -22
  187. package/lib/esm/components/DateEditor.js.map +0 -1
  188. package/lib/esm/components/RadioButtonEditor.d.ts +0 -23
  189. package/lib/esm/components/RadioButtonEditor.js +0 -41
  190. package/lib/esm/components/RadioButtonEditor.js.map +0 -1
  191. package/lib/esm/components/SelectEditor.d.ts +0 -21
  192. package/lib/esm/components/SelectEditor.js +0 -30
  193. package/lib/esm/components/SelectEditor.js.map +0 -1
  194. package/lib/esm/components/SwitchEditor.d.ts +0 -21
  195. package/lib/esm/components/SwitchEditor.js +0 -23
  196. package/lib/esm/components/SwitchEditor.js.map +0 -1
  197. package/lib/esm/components/TextareaEditor.d.ts +0 -23
  198. package/lib/esm/components/TextareaEditor.js +0 -15
  199. package/lib/esm/components/TextareaEditor.js.map +0 -1
  200. package/lib/esm/components/ToggleEditor.d.ts +0 -22
  201. package/lib/esm/components/ToggleEditor.js +0 -23
  202. package/lib/esm/components/ToggleEditor.js.map +0 -1
  203. package/lib/esm/ontologies/lincd-ui.d.ts +0 -6
  204. package/lib/esm/ontologies/lincd-ui.js +0 -18
  205. package/lib/esm/ontologies/lincd-ui.js.map +0 -1
  206. package/tsconfig-cjs.json +0 -8
  207. package/tsconfig-esm.json +0 -9
@@ -0,0 +1,157 @@
1
+ /*
2
+ * Lifted from ViewInstance.module.css alongside the InstanceView organism.
3
+ *
4
+ * Only the rules that render an instance's VALUES. The page keeps its layout, header,
5
+ * actions and skeleton rules — those belong to the page, not to the value list.
6
+ */
7
+
8
+ .rows {
9
+ padding: var(--surface-gap) var(--space-2xl) var(--space-lg) var(--space-2xl);
10
+ }
11
+
12
+ .form {
13
+ display: flex;
14
+ min-height: 3.75rem;
15
+ align-items: center;
16
+ border-top: 1px solid var(--table-row-border);
17
+ justify-content: flex-start;
18
+ padding: var(--space-sm) 0;
19
+ }
20
+
21
+ .form:first-child {
22
+ border-top: none;
23
+ }
24
+
25
+ .form.stacked {
26
+ flex-direction: column;
27
+ align-items: flex-start;
28
+ gap: var(--space-sm);
29
+ }
30
+
31
+ .form.stacked .label {
32
+ width: 100%;
33
+ }
34
+
35
+ .form.stacked .value {
36
+ width: 100%;
37
+ }
38
+
39
+ .label {
40
+ display: flex;
41
+ flex-direction: column;
42
+ width: 30%;
43
+ min-width: 120px;
44
+ height: 100%;
45
+ justify-content: center;
46
+ padding-right: var(--space-md);
47
+ flex-shrink: 0;
48
+ }
49
+
50
+ .label label {
51
+ color: var(--instance-view-gray-500, var(--color-gray-500));
52
+ font-size: var(--typography-font-size);
53
+ }
54
+
55
+ .value {
56
+ display: flex;
57
+ flex-wrap: wrap;
58
+ gap: var(--space-sm-plus);
59
+ width: 70%;
60
+ max-height: 8.75rem;
61
+ overflow-y: auto;
62
+ }
63
+
64
+ .value p {
65
+ color: var(--text-primary);
66
+ font-size: var(--typography-font-size);
67
+ margin: 0;
68
+ }
69
+
70
+ .badgeContainer {
71
+ display: flex;
72
+ flex-wrap: wrap;
73
+ gap: var(--space-2xs) 0;
74
+ overflow-y: auto;
75
+ }
76
+
77
+ .expandedContainer {
78
+ display: flex;
79
+ flex-wrap: wrap;
80
+ gap: var(--space-sm);
81
+ overflow-y: auto;
82
+ margin-top: var(--space-sm);
83
+ padding: var(--space-sm);
84
+ border: 1px solid var(--surface-border);
85
+ border-radius: var(--radius-field);
86
+ background: var(--section-bg);
87
+ }
88
+
89
+ .imageValue {
90
+ display: inline-flex;
91
+ align-items: center;
92
+ gap: var(--space-sm);
93
+ }
94
+
95
+ .imageLabel {
96
+ color: var(--instance-view-gray-600, var(--color-gray-600));
97
+ font-size: var(--typography-small-size);
98
+ word-break: break-all;
99
+ }
100
+
101
+ [data-theme='dark'] .imageLabel {
102
+ color: var(--instance-view-gray-400, var(--color-gray-400));
103
+ }
104
+
105
+ [data-theme='dark'] .label label {
106
+ color: var(--instance-view-gray-400, var(--color-gray-400));
107
+ }
108
+
109
+ .rows {
110
+ padding: var(--space-sm) var(--space-md);
111
+ }
112
+
113
+ .form {
114
+ padding: var(--space-md) 0;
115
+ }
116
+
117
+ .label {
118
+ width: 35%;
119
+ min-width: 100px;
120
+ padding-right: var(--space-sm);
121
+ }
122
+
123
+ .value {
124
+ width: 65%;
125
+ }
126
+
127
+ .rows {
128
+ padding: var(--space-xs) var(--space-sm);
129
+ }
130
+
131
+ .form {
132
+ flex-direction: column;
133
+ align-items: flex-start;
134
+ gap: var(--space-2xs);
135
+ padding: var(--space-sm) 0;
136
+ }
137
+
138
+ .label {
139
+ width: 100%;
140
+ min-width: unset;
141
+ padding-right: 0;
142
+ margin-bottom: 0;
143
+ }
144
+
145
+ .label label {
146
+ font-size: var(--typography-small-size);
147
+ }
148
+
149
+ .value {
150
+ width: 100%;
151
+ max-height: none;
152
+ }
153
+
154
+ .value p {
155
+ font-size: var(--typography-small-size);
156
+ }
157
+
@@ -0,0 +1,212 @@
1
+ import {useState} from 'react';
2
+ import {motion} from 'framer-motion';
3
+ import {cl} from '@_linked/react/utils/ClassNames';
4
+ import type {
5
+ PropertyShapeWire,
6
+ NodeShapeWire,
7
+ } from '@_linked/core/shapes/nodeShapeWire';
8
+ import {NodeBadge} from './NodeBadge.js';
9
+ import ImageThumb from '@_linked/primitives/components/ImageThumb';
10
+ import {getNodeDisplay} from '../shape/nodeDisplay.js';
11
+ import {
12
+ getTransition,
13
+ staggerContainer,
14
+ staggerItem,
15
+ useReducedMotion,
16
+ } from '@_linked/primitives/motion';
17
+ import style from './InstanceView.module.css';
18
+
19
+ /**
20
+ * Read-only rendering of one instance's values.
21
+ *
22
+ * Lifted out of `ViewInstance` (436 lines), which rendered all of this inline. That made the
23
+ * single-instance view the one CRUD surface with no organism at all — nothing to extract,
24
+ * nothing to test, and nothing that could move into the package. The page keeps what a page
25
+ * should: routing, breadcrumbs, layout chrome, actions, the deletion dialog.
26
+ *
27
+ * Navigation is a callback, not a router call. Clicking a related node means "show me that
28
+ * one", and where that goes is the host's business.
29
+ */
30
+
31
+ export interface InstanceViewProps {
32
+ shape?: NodeShapeWire;
33
+ /** Property metadata by label — used to resolve which shape a related node belongs to. */
34
+ properties: Record<string, PropertyShapeWire>;
35
+ /** The instance's values, keyed by property label. */
36
+ subject: Record<string, any> | null;
37
+ /**
38
+ * A related node was clicked. Given the node and the property it came from, so the caller
39
+ * can resolve the target shape from `properties[propertyName].valueShape`.
40
+ */
41
+ onNodeClick?: (node: {id: string; label?: string}, propertyName: string) => void;
42
+ }
43
+
44
+ /** Above this many values, the rest collapse behind a "(n…)" badge. */
45
+ const MAX_VALUES = 7;
46
+
47
+ export function InstanceView({
48
+ shape,
49
+ properties,
50
+ subject,
51
+ onNodeClick,
52
+ }: InstanceViewProps) {
53
+ const [expandedArrays, setExpandedArrays] = useState<Set<string>>(new Set());
54
+ const reducedMotion = useReducedMotion();
55
+
56
+ const expand = (propertyName: string) =>
57
+ setExpandedArrays((prev) => new Set(prev).add(propertyName));
58
+
59
+ const handleNodeClick = (node: any, propertyName: string) => {
60
+ if (node?.id) onNodeClick?.(node, propertyName);
61
+ };
62
+
63
+ /** An image source, whether the value carries one directly or via a nested ImageObject. */
64
+ const imageSourceOf = (value: any): string | undefined => {
65
+ const images = value?.image;
66
+ const first = Array.isArray(images) ? images[0] : images;
67
+ if (!first) return undefined;
68
+ return typeof first === 'object' && first.contentUrl
69
+ ? String(first.contentUrl)
70
+ : String(first);
71
+ };
72
+
73
+ const renderArrayItem = (item: any, key: number, propertyName: string) => {
74
+ if (typeof item === 'object' && item?.contentUrl) {
75
+ return (
76
+ <div key={key}>
77
+ <ImageThumb src={item.contentUrl} size={40} alt={item.label || propertyName} />
78
+ </div>
79
+ );
80
+ }
81
+ if (typeof item === 'object' && item?.id) {
82
+ const imgSrc = imageSourceOf(item);
83
+ if (imgSrc) {
84
+ return (
85
+ <div key={key}>
86
+ <ImageThumb src={imgSrc} size={40} alt={item.label || propertyName} />
87
+ </div>
88
+ );
89
+ }
90
+ return (
91
+ <div key={key}>
92
+ <NodeBadge
93
+ text={item.label || item.id}
94
+ onClick={() => handleNodeClick(item, propertyName)}
95
+ clickable
96
+ />
97
+ </div>
98
+ );
99
+ }
100
+ return (
101
+ <div key={key}>
102
+ <NodeBadge text={String(item)} />
103
+ </div>
104
+ );
105
+ };
106
+
107
+ const renderValue = (value: any, propertyName: string) => {
108
+ if (value === null || value === undefined) return <p>-</p>;
109
+ if (typeof value === 'boolean') return <p>{value ? 'Yes' : 'No'}</p>;
110
+
111
+ // An ImageObject: the value IS the image.
112
+ if (typeof value === 'object' && value.contentUrl) {
113
+ return (
114
+ <div className={style.imageValue}>
115
+ <ImageThumb src={value.contentUrl} size={64} alt={propertyName} />
116
+ <p className={style.imageLabel}>{value.label || value.contentUrl}</p>
117
+ </div>
118
+ );
119
+ }
120
+
121
+ // A related node: show its image if it has one, else a clickable badge.
122
+ if (typeof value === 'object' && value.id) {
123
+ const imgSrc = imageSourceOf(value);
124
+ if (imgSrc) {
125
+ return (
126
+ <div className={style.imageValue}>
127
+ <ImageThumb src={imgSrc} size={48} alt={value.label || propertyName} />
128
+ {value.label && <span className={style.imageLabel}>{value.label}</span>}
129
+ </div>
130
+ );
131
+ }
132
+ return (
133
+ <NodeBadge
134
+ text={getNodeDisplay(value)}
135
+ onClick={() => handleNodeClick(value, propertyName)}
136
+ clickable
137
+ />
138
+ );
139
+ }
140
+
141
+ if (Array.isArray(value)) {
142
+ if (value.length === 0) return <p>-</p>;
143
+ const isExpanded = expandedArrays.has(propertyName);
144
+ const visible = isExpanded ? value : value.slice(0, MAX_VALUES);
145
+ const leftOver = value.length - MAX_VALUES;
146
+ return (
147
+ <div className={style.badgeContainer}>
148
+ {visible.map((item, index) => renderArrayItem(item, index, propertyName))}
149
+ {!isExpanded && leftOver > 0 && (
150
+ <div>
151
+ <NodeBadge
152
+ text={`(${leftOver}...)`}
153
+ onClick={() => expand(propertyName)}
154
+ clickable
155
+ />
156
+ </div>
157
+ )}
158
+ </div>
159
+ );
160
+ }
161
+
162
+ return <p>{String(value)}</p>;
163
+ };
164
+
165
+ if (!subject) return null;
166
+
167
+ // Only properties that actually have a value, and never the identifier — it names the
168
+ // record rather than describing it.
169
+ const populated = (shape?.propertyShapes ?? []).filter((prop) => {
170
+ if (prop.label === 'id') return false;
171
+ const value = subject[prop.label];
172
+ return value !== undefined && value !== null && value !== '';
173
+ });
174
+
175
+ return (
176
+ <motion.div
177
+ className={style.rows}
178
+ variants={staggerContainer}
179
+ initial="initial"
180
+ animate="animate"
181
+ >
182
+ {populated.map((prop) => {
183
+ const value = subject[prop.label];
184
+ const formattedLabel =
185
+ prop.label.charAt(0).toUpperCase() + prop.label.slice(1);
186
+ const isStacked = shouldStack(value);
187
+ return (
188
+ <motion.div
189
+ key={prop.label}
190
+ className={cl(style.form, isStacked && style.stacked)}
191
+ variants={staggerItem}
192
+ transition={getTransition(reducedMotion)}
193
+ >
194
+ <div className={style.label}>
195
+ <label>{formattedLabel}</label>
196
+ </div>
197
+ <div className={style.value}>{renderValue(value, prop.label)}</div>
198
+ </motion.div>
199
+ );
200
+ })}
201
+ </motion.div>
202
+ );
203
+ }
204
+
205
+ /** A value wide enough that label-beside-value stops reading well. */
206
+ function shouldStack(value: unknown): boolean {
207
+ if (Array.isArray(value)) return value.length > 3;
208
+ if (typeof value === 'string') return value.length > 120;
209
+ return false;
210
+ }
211
+
212
+ export default InstanceView;
@@ -0,0 +1,60 @@
1
+ import * as React from 'react';
2
+ import {Badge} from '@_linked/primitives/components/Badge';
3
+
4
+ export interface NodeBadgeProps {
5
+ /** What identifies the node — a label where it has one, otherwise its IRI. */
6
+ text: string;
7
+ onClick?: () => void;
8
+ /** Whether the badge acts on a click. A read-only host may not navigate. */
9
+ clickable?: boolean;
10
+ title?: string;
11
+ className?: string;
12
+ }
13
+
14
+ /**
15
+ * A chip standing for one node.
16
+ *
17
+ * The only badge this package needs, and it is a thin composition over the presentational
18
+ * primitive. It exists as its own component rather than as a bare `<Badge>` for one reason:
19
+ * "a related instance" is a shape-domain concept, and a table cell, a form field and a
20
+ * read-only view must render it identically. A caller reaching for the primitive directly
21
+ * would have to remember which variant and colour meant "node", and eventually two of them
22
+ * would disagree.
23
+ *
24
+ * `clickable` is separate from `onClick` deliberately. A host that cannot navigate still
25
+ * passes a handler in some code paths, and a chip that looks interactive and does nothing is
26
+ * worse than one that looks inert.
27
+ */
28
+ export function NodeBadge({
29
+ text,
30
+ onClick,
31
+ clickable = false,
32
+ title,
33
+ className,
34
+ }: NodeBadgeProps) {
35
+ const interactive = clickable && !!onClick;
36
+
37
+ if (!interactive) {
38
+ return (
39
+ <Badge variant="soft" color="neutral" title={title} className={className}>
40
+ {text}
41
+ </Badge>
42
+ );
43
+ }
44
+
45
+ return (
46
+ <Badge variant="soft" color="neutral" title={title} className={className} asChild>
47
+ <button
48
+ type="button"
49
+ onClick={(event) => {
50
+ // A node chip frequently sits inside a clickable row. Without this, choosing the
51
+ // chip also triggers the row.
52
+ event.stopPropagation();
53
+ onClick?.();
54
+ }}
55
+ >
56
+ {text}
57
+ </button>
58
+ </Badge>
59
+ );
60
+ }
@@ -0,0 +1,4 @@
1
+ .Root {
2
+ display: block;
3
+ width: 100%;
4
+ }
@@ -0,0 +1,64 @@
1
+ import type { PropertyShapeWire, NodeShapeWire } from '@_linked/core/shapes/nodeShapeWire';
2
+ import CustomMultiSelect from './CustomMultiSelect.js';
3
+ import style from './NodeValuesEditor.module.css';
4
+
5
+ //TODO: create new NodeValuesEditor component, which takes property & of props
6
+ //TODO: NodeValuesEditor will look at the property shape and determine potential Values by itself
7
+ //TODO: it also checks if the property has `inList` and then uses inList.getContents() as potential values
8
+ //TODO: it also updates the value by using of[property.label]
9
+ // it renders the MultiSelect component and provides value & multiselect props
10
+ // it also provides potentialValues to MultiSelect, which will be the instances of property.valueShape.getLocalInstances()
11
+ // it renders <MultiSelect />
12
+ interface NodeValuesEditorProps {
13
+ property: PropertyShapeWire;
14
+ of;
15
+ /**
16
+ * The shape being edited. Passed down rather than read from the route: the picker needs
17
+ * it to say which shape the answer belongs to, and the form above already knows.
18
+ */
19
+ sourceShape?: NodeShapeWire;
20
+ onBeforeNavigate?: () => Promise<string | void> | string | void;
21
+ narrowedIds?: string[] | null;
22
+ isNarrowing?: boolean;
23
+ }
24
+ const NodeValuesEditor = ({ property, of, sourceShape, onBeforeNavigate, narrowedIds, isNarrowing }: NodeValuesEditorProps) => {
25
+ let valueShape = property.valueShape;
26
+ // let shapeClass = getShapeClass(of.targetClass);
27
+
28
+ if (!valueShape) {
29
+ // valueShape = Thing;
30
+ } else {
31
+ }
32
+ // let instanceValues = (
33
+ // getShapeClass(valueShape.namedNode) as any
34
+ // ).getLocalInstances();
35
+
36
+ const values = of[property.label];
37
+ // let potentialValues = property.inList;
38
+
39
+
40
+ return (
41
+ <div className={style.Root}>
42
+ <CustomMultiSelect
43
+ values={values}
44
+ property={property}
45
+ sourceShape={sourceShape}
46
+ onBeforeNavigate={onBeforeNavigate}
47
+ narrowedIds={narrowedIds}
48
+ isNarrowing={isNarrowing}
49
+ onChange={(selected) => {
50
+ // Write back to form data object (same mutation pattern as DynamicFormField)
51
+ // Include label so draft restore can display friendly names
52
+ of[property.label] =
53
+ selected.length === 0
54
+ ? undefined
55
+ : property.maxCount === 1
56
+ ? { id: selected[0].id, label: selected[0].label }
57
+ : selected.map((v) => ({ id: v.id, label: v.label }));
58
+ }}
59
+ />
60
+ </div>
61
+ );
62
+ };
63
+
64
+ export default NodeValuesEditor;
@@ -0,0 +1,22 @@
1
+ .Root {
2
+ display: flex;
3
+ flex-direction: row;
4
+ box-sizing: border-box;
5
+ gap: 1rem;
6
+
7
+ &.columnBased {
8
+ flex-direction: column;
9
+ padding: 10px 0;
10
+ }
11
+
12
+ .radioButtonItem {
13
+ display: flex;
14
+ gap: 0.5rem;
15
+ align-items: center;
16
+ }
17
+
18
+ .radioButton {
19
+ width: 25px;
20
+ min-width: 25px;
21
+ }
22
+ }
@@ -0,0 +1,96 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import type { PropertyShapeWire } from '@_linked/core/shapes/nodeShapeWire';
3
+ import { Root, Item, RadioGroup } from '@_linked/primitives/components/RadioGroup';
4
+ import styles from './RadioButtonEditor.module.css';
5
+ import { cl } from '@_linked/react/utils/ClassNames';
6
+
7
+ export interface RadioButtonProps {
8
+ onValueChange?: (newValue) => void;
9
+ of?: Record<string, any>;
10
+ property?: PropertyShapeWire;
11
+ children?: React.ReactNode;
12
+ className?: string;
13
+ /**
14
+ * Converts the value returned by the shape, to the radio input string for the radio input
15
+ * @param shapePropertyValue
16
+ * @returns
17
+ */
18
+ preConvertValueFn?: (shapePropertyValue: any) => string;
19
+ /**
20
+ * Converts the value of the radio input to a value that can be provided to the set method of the shape
21
+ * @param radioInputValue
22
+ * @returns
23
+ */
24
+ postConvertValueFn?: (radioInputValue: string) => any;
25
+ }
26
+
27
+ export const RadioButtonEditor: React.FC<RadioButtonProps> = ({
28
+ onValueChange,
29
+ of,
30
+ property,
31
+ preConvertValueFn,
32
+ postConvertValueFn,
33
+ children,
34
+ className,
35
+ ...props
36
+ }) => {
37
+ const [labelHasLongString, setLabelHasLongString] = useState(false);
38
+
39
+ const onChangeHandler = (newValue) => {
40
+ if (!of) return;
41
+ of[property.label] = newValue;
42
+
43
+ if (onValueChange) {
44
+ onValueChange(of[property.label]);
45
+ // onValueChange(
46
+ // options.map((option) => {
47
+ // console.log(option, 'option');
48
+ // of[property.label] = newValue;
49
+ // }),
50
+ // );
51
+ }
52
+ };
53
+
54
+ useEffect(() => {
55
+ //check if radio button label has a long string
56
+ if (property.in) {
57
+ const containsLongString = property.in.some((list) => {
58
+ throw Error('TODO: property.in for RadioButtonEditor');
59
+ // const wordCount = list.value.split(' ').length;
60
+ // return wordCount > 5;
61
+ });
62
+ setLabelHasLongString(containsLongString);
63
+ }
64
+ }, [property.in]);
65
+
66
+ // function cl(radioButtonField: any, arg1: any): string {
67
+ // throw new Error('Function not implemented.');
68
+ // }
69
+
70
+ console.log('of.label', of[property.label]);
71
+
72
+ return (
73
+ <>
74
+ <RadioGroup.Root
75
+ defaultValue="default"
76
+ aria-label="View density"
77
+ className={cl(styles.Root, labelHasLongString && styles.columnBased)}
78
+ >
79
+ {property.in ? (
80
+ property.in.map((item) => (
81
+ <label className={styles.radioButtonItem}>
82
+ <RadioGroup.Item
83
+ value={item.toString()}
84
+ className={styles.radioButton}
85
+ />
86
+ TODO: item from property.in
87
+ {/* {item.value.charAt(0).toUpperCase() + item.value.slice(1)} */}
88
+ </label>
89
+ ))
90
+ ) : (
91
+ <p>unimplemented single radio button</p>
92
+ )}
93
+ </RadioGroup.Root>
94
+ </>
95
+ );
96
+ };