@open-mercato/ui 0.4.2-canary-c02407ff85

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 (319) hide show
  1. package/build.mjs +62 -0
  2. package/dist/backend/AppShell.js +902 -0
  3. package/dist/backend/AppShell.js.map +7 -0
  4. package/dist/backend/ConfirmDialog.js +17 -0
  5. package/dist/backend/ConfirmDialog.js.map +7 -0
  6. package/dist/backend/ContextHelp.js +31 -0
  7. package/dist/backend/ContextHelp.js.map +7 -0
  8. package/dist/backend/CrudForm.js +2028 -0
  9. package/dist/backend/CrudForm.js.map +7 -0
  10. package/dist/backend/DataTable.js +1363 -0
  11. package/dist/backend/DataTable.js.map +7 -0
  12. package/dist/backend/EmptyState.js +52 -0
  13. package/dist/backend/EmptyState.js.map +7 -0
  14. package/dist/backend/FilterBar.js +140 -0
  15. package/dist/backend/FilterBar.js.map +7 -0
  16. package/dist/backend/FilterOverlay.js +279 -0
  17. package/dist/backend/FilterOverlay.js.map +7 -0
  18. package/dist/backend/FlashMessages.js +66 -0
  19. package/dist/backend/FlashMessages.js.map +7 -0
  20. package/dist/backend/JsonBuilder.js +322 -0
  21. package/dist/backend/JsonBuilder.js.map +7 -0
  22. package/dist/backend/JsonDisplay.js +203 -0
  23. package/dist/backend/JsonDisplay.js.map +7 -0
  24. package/dist/backend/Page.js +27 -0
  25. package/dist/backend/Page.js.map +7 -0
  26. package/dist/backend/PerspectiveSidebar.js +282 -0
  27. package/dist/backend/PerspectiveSidebar.js.map +7 -0
  28. package/dist/backend/RowActions.js +148 -0
  29. package/dist/backend/RowActions.js.map +7 -0
  30. package/dist/backend/TruncatedCell.js +92 -0
  31. package/dist/backend/TruncatedCell.js.map +7 -0
  32. package/dist/backend/UserMenu.js +107 -0
  33. package/dist/backend/UserMenu.js.map +7 -0
  34. package/dist/backend/ValueIcons.js +34 -0
  35. package/dist/backend/ValueIcons.js.map +7 -0
  36. package/dist/backend/custom-fields/FieldDefinitionsEditor.js +1264 -0
  37. package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +7 -0
  38. package/dist/backend/custom-fields/FieldDefinitionsManager.js +332 -0
  39. package/dist/backend/custom-fields/FieldDefinitionsManager.js.map +7 -0
  40. package/dist/backend/dashboard/DashboardScreen.js +578 -0
  41. package/dist/backend/dashboard/DashboardScreen.js.map +7 -0
  42. package/dist/backend/dashboard/index.js +5 -0
  43. package/dist/backend/dashboard/index.js.map +7 -0
  44. package/dist/backend/dashboard/widgetRegistry.js +55 -0
  45. package/dist/backend/dashboard/widgetRegistry.js.map +7 -0
  46. package/dist/backend/detail/ActivitiesSection.js +962 -0
  47. package/dist/backend/detail/ActivitiesSection.js.map +7 -0
  48. package/dist/backend/detail/AddressEditor.js +413 -0
  49. package/dist/backend/detail/AddressEditor.js.map +7 -0
  50. package/dist/backend/detail/AddressTiles.js +437 -0
  51. package/dist/backend/detail/AddressTiles.js.map +7 -0
  52. package/dist/backend/detail/AddressesSection.js +264 -0
  53. package/dist/backend/detail/AddressesSection.js.map +7 -0
  54. package/dist/backend/detail/AttachmentDeleteDialog.js +41 -0
  55. package/dist/backend/detail/AttachmentDeleteDialog.js.map +7 -0
  56. package/dist/backend/detail/AttachmentMetadataDialog.js +517 -0
  57. package/dist/backend/detail/AttachmentMetadataDialog.js.map +7 -0
  58. package/dist/backend/detail/AttachmentsSection.js +367 -0
  59. package/dist/backend/detail/AttachmentsSection.js.map +7 -0
  60. package/dist/backend/detail/CustomDataSection.js +433 -0
  61. package/dist/backend/detail/CustomDataSection.js.map +7 -0
  62. package/dist/backend/detail/DetailFieldsSection.js +75 -0
  63. package/dist/backend/detail/DetailFieldsSection.js.map +7 -0
  64. package/dist/backend/detail/ErrorMessage.js +28 -0
  65. package/dist/backend/detail/ErrorMessage.js.map +7 -0
  66. package/dist/backend/detail/InlineEditors.js +681 -0
  67. package/dist/backend/detail/InlineEditors.js.map +7 -0
  68. package/dist/backend/detail/LoadingMessage.js +14 -0
  69. package/dist/backend/detail/LoadingMessage.js.map +7 -0
  70. package/dist/backend/detail/NotesSection.js +1032 -0
  71. package/dist/backend/detail/NotesSection.js.map +7 -0
  72. package/dist/backend/detail/TabEmptyState.js +25 -0
  73. package/dist/backend/detail/TabEmptyState.js.map +7 -0
  74. package/dist/backend/detail/TagsSection.js +254 -0
  75. package/dist/backend/detail/TagsSection.js.map +7 -0
  76. package/dist/backend/detail/addressFormat.js +77 -0
  77. package/dist/backend/detail/addressFormat.js.map +7 -0
  78. package/dist/backend/detail/index.js +34 -0
  79. package/dist/backend/detail/index.js.map +7 -0
  80. package/dist/backend/fields/registry.generated.js +8 -0
  81. package/dist/backend/fields/registry.generated.js.map +7 -0
  82. package/dist/backend/fields/registry.js +29 -0
  83. package/dist/backend/fields/registry.js.map +7 -0
  84. package/dist/backend/indexes/PartialIndexBanner.js +58 -0
  85. package/dist/backend/indexes/PartialIndexBanner.js.map +7 -0
  86. package/dist/backend/indexes/store.js +62 -0
  87. package/dist/backend/indexes/store.js.map +7 -0
  88. package/dist/backend/injection/InjectionSpot.js +179 -0
  89. package/dist/backend/injection/InjectionSpot.js.map +7 -0
  90. package/dist/backend/injection/PageInjectionBoundary.js +26 -0
  91. package/dist/backend/injection/PageInjectionBoundary.js.map +7 -0
  92. package/dist/backend/injection/helpers.js +26 -0
  93. package/dist/backend/injection/helpers.js.map +7 -0
  94. package/dist/backend/injection/widgetRegistry.js +55 -0
  95. package/dist/backend/injection/widgetRegistry.js.map +7 -0
  96. package/dist/backend/inputs/ComboboxInput.js +225 -0
  97. package/dist/backend/inputs/ComboboxInput.js.map +7 -0
  98. package/dist/backend/inputs/LookupSelect.js +191 -0
  99. package/dist/backend/inputs/LookupSelect.js.map +7 -0
  100. package/dist/backend/inputs/PhoneNumberField.js +100 -0
  101. package/dist/backend/inputs/PhoneNumberField.js.map +7 -0
  102. package/dist/backend/inputs/SwitchableMarkdownInput.js +92 -0
  103. package/dist/backend/inputs/SwitchableMarkdownInput.js.map +7 -0
  104. package/dist/backend/inputs/TagsInput.js +222 -0
  105. package/dist/backend/inputs/TagsInput.js.map +7 -0
  106. package/dist/backend/inputs/index.js +6 -0
  107. package/dist/backend/inputs/index.js.map +7 -0
  108. package/dist/backend/operations/LastOperationBanner.js +80 -0
  109. package/dist/backend/operations/LastOperationBanner.js.map +7 -0
  110. package/dist/backend/operations/store.js +183 -0
  111. package/dist/backend/operations/store.js.map +7 -0
  112. package/dist/backend/schedule/ScheduleAgenda.js +107 -0
  113. package/dist/backend/schedule/ScheduleAgenda.js.map +7 -0
  114. package/dist/backend/schedule/ScheduleGrid.js +107 -0
  115. package/dist/backend/schedule/ScheduleGrid.js.map +7 -0
  116. package/dist/backend/schedule/ScheduleToolbar.js +166 -0
  117. package/dist/backend/schedule/ScheduleToolbar.js.map +7 -0
  118. package/dist/backend/schedule/ScheduleView.js +165 -0
  119. package/dist/backend/schedule/ScheduleView.js.map +7 -0
  120. package/dist/backend/schedule/index.js +6 -0
  121. package/dist/backend/schedule/index.js.map +7 -0
  122. package/dist/backend/schedule/recurrence.js +83 -0
  123. package/dist/backend/schedule/recurrence.js.map +7 -0
  124. package/dist/backend/schedule/types.js +1 -0
  125. package/dist/backend/schedule/types.js.map +7 -0
  126. package/dist/backend/upgrades/UpgradeActionBanner.js +91 -0
  127. package/dist/backend/upgrades/UpgradeActionBanner.js.map +7 -0
  128. package/dist/backend/utils/api.js +127 -0
  129. package/dist/backend/utils/api.js.map +7 -0
  130. package/dist/backend/utils/apiCall.js +48 -0
  131. package/dist/backend/utils/apiCall.js.map +7 -0
  132. package/dist/backend/utils/crud.js +126 -0
  133. package/dist/backend/utils/crud.js.map +7 -0
  134. package/dist/backend/utils/customFieldColumns.js +56 -0
  135. package/dist/backend/utils/customFieldColumns.js.map +7 -0
  136. package/dist/backend/utils/customFieldDefs.js +143 -0
  137. package/dist/backend/utils/customFieldDefs.js.map +7 -0
  138. package/dist/backend/utils/customFieldFilters.js +126 -0
  139. package/dist/backend/utils/customFieldFilters.js.map +7 -0
  140. package/dist/backend/utils/customFieldForms.js +162 -0
  141. package/dist/backend/utils/customFieldForms.js.map +7 -0
  142. package/dist/backend/utils/customFieldValues.js +26 -0
  143. package/dist/backend/utils/customFieldValues.js.map +7 -0
  144. package/dist/backend/utils/flash.js +16 -0
  145. package/dist/backend/utils/flash.js.map +7 -0
  146. package/dist/backend/utils/nav.js +185 -0
  147. package/dist/backend/utils/nav.js.map +7 -0
  148. package/dist/backend/utils/serverErrors.js +230 -0
  149. package/dist/backend/utils/serverErrors.js.map +7 -0
  150. package/dist/frontend/AuthFooter.js +23 -0
  151. package/dist/frontend/AuthFooter.js.map +7 -0
  152. package/dist/frontend/LanguageSwitcher.js +57 -0
  153. package/dist/frontend/LanguageSwitcher.js.map +7 -0
  154. package/dist/frontend/Layout.js +14 -0
  155. package/dist/frontend/Layout.js.map +7 -0
  156. package/dist/index.js +32 -0
  157. package/dist/index.js.map +7 -0
  158. package/dist/primitives/DataLoader.js +67 -0
  159. package/dist/primitives/DataLoader.js.map +7 -0
  160. package/dist/primitives/ErrorNotice.js +20 -0
  161. package/dist/primitives/ErrorNotice.js.map +7 -0
  162. package/dist/primitives/alert.js +38 -0
  163. package/dist/primitives/alert.js.map +7 -0
  164. package/dist/primitives/badge.js +28 -0
  165. package/dist/primitives/badge.js.map +7 -0
  166. package/dist/primitives/button.js +44 -0
  167. package/dist/primitives/button.js.map +7 -0
  168. package/dist/primitives/card.js +91 -0
  169. package/dist/primitives/card.js.map +7 -0
  170. package/dist/primitives/checkbox.js +28 -0
  171. package/dist/primitives/checkbox.js.map +7 -0
  172. package/dist/primitives/dialog.js +90 -0
  173. package/dist/primitives/dialog.js.map +7 -0
  174. package/dist/primitives/input.js +22 -0
  175. package/dist/primitives/input.js.map +7 -0
  176. package/dist/primitives/label.js +21 -0
  177. package/dist/primitives/label.js.map +7 -0
  178. package/dist/primitives/separator.js +9 -0
  179. package/dist/primitives/separator.js.map +7 -0
  180. package/dist/primitives/spinner.js +24 -0
  181. package/dist/primitives/spinner.js.map +7 -0
  182. package/dist/primitives/switch.js +80 -0
  183. package/dist/primitives/switch.js.map +7 -0
  184. package/dist/primitives/table.js +29 -0
  185. package/dist/primitives/table.js.map +7 -0
  186. package/dist/primitives/tabs.js +87 -0
  187. package/dist/primitives/tabs.js.map +7 -0
  188. package/dist/primitives/textarea.js +21 -0
  189. package/dist/primitives/textarea.js.map +7 -0
  190. package/dist/primitives/tooltip.js +60 -0
  191. package/dist/primitives/tooltip.js.map +7 -0
  192. package/dist/theme/QueryProvider.js +44 -0
  193. package/dist/theme/QueryProvider.js.map +7 -0
  194. package/dist/theme/ThemeProvider.js +95 -0
  195. package/dist/theme/ThemeProvider.js.map +7 -0
  196. package/dist/theme/ThemeToggle.js +88 -0
  197. package/dist/theme/ThemeToggle.js.map +7 -0
  198. package/dist/theme/index.js +10 -0
  199. package/dist/theme/index.js.map +7 -0
  200. package/dist/types/react-big-calendar.d.js +1 -0
  201. package/dist/types/react-big-calendar.d.js.map +7 -0
  202. package/jest.config.cjs +23 -0
  203. package/jest.setup.ts +55 -0
  204. package/package.json +105 -0
  205. package/src/backend/AppShell.tsx +1096 -0
  206. package/src/backend/ConfirmDialog.tsx +19 -0
  207. package/src/backend/ContextHelp.tsx +38 -0
  208. package/src/backend/CrudForm.tsx +2503 -0
  209. package/src/backend/DataTable.tsx +1730 -0
  210. package/src/backend/EmptyState.tsx +65 -0
  211. package/src/backend/FilterBar.tsx +161 -0
  212. package/src/backend/FilterOverlay.tsx +328 -0
  213. package/src/backend/FlashMessages.tsx +82 -0
  214. package/src/backend/JsonBuilder.tsx +362 -0
  215. package/src/backend/JsonDisplay.tsx +254 -0
  216. package/src/backend/Page.tsx +30 -0
  217. package/src/backend/PerspectiveSidebar.tsx +337 -0
  218. package/src/backend/RowActions.tsx +151 -0
  219. package/src/backend/TruncatedCell.tsx +133 -0
  220. package/src/backend/UserMenu.tsx +118 -0
  221. package/src/backend/ValueIcons.tsx +48 -0
  222. package/src/backend/__tests__/AppShell.test.tsx +115 -0
  223. package/src/backend/__tests__/CrudForm.render.test.tsx +30 -0
  224. package/src/backend/__tests__/DataTable.render.test.tsx +48 -0
  225. package/src/backend/__tests__/custom-field-filters.test.ts +72 -0
  226. package/src/backend/__tests__/custom-field-forms.test.ts +54 -0
  227. package/src/backend/__tests__/serverErrors.test.ts +83 -0
  228. package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +1292 -0
  229. package/src/backend/custom-fields/FieldDefinitionsManager.tsx +381 -0
  230. package/src/backend/dashboard/DashboardScreen.tsx +684 -0
  231. package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +112 -0
  232. package/src/backend/dashboard/index.ts +1 -0
  233. package/src/backend/dashboard/widgetRegistry.ts +68 -0
  234. package/src/backend/detail/ActivitiesSection.tsx +1284 -0
  235. package/src/backend/detail/AddressEditor.tsx +472 -0
  236. package/src/backend/detail/AddressTiles.tsx +587 -0
  237. package/src/backend/detail/AddressesSection.tsx +346 -0
  238. package/src/backend/detail/AttachmentDeleteDialog.tsx +56 -0
  239. package/src/backend/detail/AttachmentMetadataDialog.tsx +672 -0
  240. package/src/backend/detail/AttachmentsSection.tsx +414 -0
  241. package/src/backend/detail/CustomDataSection.tsx +530 -0
  242. package/src/backend/detail/DetailFieldsSection.tsx +147 -0
  243. package/src/backend/detail/ErrorMessage.tsx +32 -0
  244. package/src/backend/detail/InlineEditors.tsx +877 -0
  245. package/src/backend/detail/LoadingMessage.tsx +14 -0
  246. package/src/backend/detail/NotesSection.tsx +1275 -0
  247. package/src/backend/detail/TabEmptyState.tsx +48 -0
  248. package/src/backend/detail/TagsSection.tsx +314 -0
  249. package/src/backend/detail/addressFormat.tsx +121 -0
  250. package/src/backend/detail/index.ts +44 -0
  251. package/src/backend/fields/registry.generated.ts +8 -0
  252. package/src/backend/fields/registry.ts +38 -0
  253. package/src/backend/indexes/PartialIndexBanner.tsx +68 -0
  254. package/src/backend/indexes/store.ts +88 -0
  255. package/src/backend/injection/InjectionSpot.tsx +236 -0
  256. package/src/backend/injection/PageInjectionBoundary.tsx +31 -0
  257. package/src/backend/injection/helpers.ts +35 -0
  258. package/src/backend/injection/widgetRegistry.ts +68 -0
  259. package/src/backend/inputs/ComboboxInput.tsx +269 -0
  260. package/src/backend/inputs/LookupSelect.tsx +247 -0
  261. package/src/backend/inputs/PhoneNumberField.tsx +129 -0
  262. package/src/backend/inputs/SwitchableMarkdownInput.tsx +128 -0
  263. package/src/backend/inputs/TagsInput.tsx +259 -0
  264. package/src/backend/inputs/index.ts +5 -0
  265. package/src/backend/operations/LastOperationBanner.tsx +85 -0
  266. package/src/backend/operations/__tests__/LastOperationBanner.test.tsx +99 -0
  267. package/src/backend/operations/store.ts +230 -0
  268. package/src/backend/schedule/ScheduleAgenda.tsx +136 -0
  269. package/src/backend/schedule/ScheduleGrid.tsx +136 -0
  270. package/src/backend/schedule/ScheduleToolbar.tsx +178 -0
  271. package/src/backend/schedule/ScheduleView.tsx +198 -0
  272. package/src/backend/schedule/index.ts +5 -0
  273. package/src/backend/schedule/recurrence.ts +99 -0
  274. package/src/backend/schedule/types.ts +26 -0
  275. package/src/backend/upgrades/UpgradeActionBanner.tsx +128 -0
  276. package/src/backend/utils/__tests__/apiCall.test.ts +109 -0
  277. package/src/backend/utils/__tests__/crud.test.ts +87 -0
  278. package/src/backend/utils/__tests__/customFieldDefs.test.ts +25 -0
  279. package/src/backend/utils/__tests__/customFieldValues.test.ts +35 -0
  280. package/src/backend/utils/api.ts +149 -0
  281. package/src/backend/utils/apiCall.ts +96 -0
  282. package/src/backend/utils/crud.ts +174 -0
  283. package/src/backend/utils/customFieldColumns.ts +71 -0
  284. package/src/backend/utils/customFieldDefs.ts +245 -0
  285. package/src/backend/utils/customFieldFilters.ts +145 -0
  286. package/src/backend/utils/customFieldForms.ts +196 -0
  287. package/src/backend/utils/customFieldValues.ts +41 -0
  288. package/src/backend/utils/flash.ts +17 -0
  289. package/src/backend/utils/nav.ts +238 -0
  290. package/src/backend/utils/serverErrors.ts +302 -0
  291. package/src/frontend/AuthFooter.tsx +29 -0
  292. package/src/frontend/LanguageSwitcher.tsx +66 -0
  293. package/src/frontend/Layout.tsx +13 -0
  294. package/src/index.ts +32 -0
  295. package/src/primitives/DataLoader.tsx +92 -0
  296. package/src/primitives/ErrorNotice.tsx +26 -0
  297. package/src/primitives/alert.tsx +52 -0
  298. package/src/primitives/badge.tsx +31 -0
  299. package/src/primitives/button.tsx +47 -0
  300. package/src/primitives/card.tsx +92 -0
  301. package/src/primitives/checkbox.tsx +28 -0
  302. package/src/primitives/dialog.tsx +110 -0
  303. package/src/primitives/input.tsx +20 -0
  304. package/src/primitives/label.tsx +18 -0
  305. package/src/primitives/separator.tsx +7 -0
  306. package/src/primitives/spinner.tsx +27 -0
  307. package/src/primitives/switch.tsx +86 -0
  308. package/src/primitives/table.tsx +27 -0
  309. package/src/primitives/tabs.tsx +128 -0
  310. package/src/primitives/textarea.tsx +20 -0
  311. package/src/primitives/tooltip.tsx +85 -0
  312. package/src/theme/QueryProvider.tsx +46 -0
  313. package/src/theme/ThemeProvider.tsx +120 -0
  314. package/src/theme/ThemeToggle.tsx +88 -0
  315. package/src/theme/index.ts +3 -0
  316. package/src/types/react-big-calendar.d.ts +16 -0
  317. package/tsconfig.build.json +11 -0
  318. package/tsconfig.json +9 -0
  319. package/watch.mjs +6 -0
@@ -0,0 +1,362 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Button } from '../primitives/button'
5
+ import { Plus, Trash2, ChevronRight, ChevronDown, Code, LayoutList } from 'lucide-react'
6
+
7
+ function cn(...classes: (string | undefined | null | false)[]) {
8
+ return classes.filter(Boolean).join(' ')
9
+ }
10
+
11
+ export type JsonBuilderProps = {
12
+ value: any
13
+ onChange: (value: any) => void
14
+ disabled?: boolean
15
+ error?: string
16
+ }
17
+
18
+ type JsonNodeType = 'string' | 'number' | 'boolean' | 'object' | 'array' | 'null'
19
+
20
+ function getJsonType(value: any): JsonNodeType {
21
+ if (value === null) return 'null'
22
+ if (Array.isArray(value)) return 'array'
23
+ return typeof value as JsonNodeType
24
+ }
25
+
26
+ export function JsonBuilder({
27
+ value,
28
+ onChange,
29
+ disabled,
30
+ error
31
+ }: JsonBuilderProps) {
32
+ const [mode, setMode] = React.useState<'raw' | 'builder'>('raw')
33
+ const [rawString, setRawString] = React.useState(() => {
34
+ if (value === null) return '{}'
35
+ return typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value || '{}')
36
+ })
37
+ const [parseError, setParseError] = React.useState<string | null>(null)
38
+
39
+ React.useEffect(() => {
40
+ if (value === null) {
41
+ if (!disabled) {
42
+ onChange({})
43
+ }
44
+ setRawString('{}')
45
+ setParseError(null)
46
+ return
47
+ }
48
+ if (typeof value === 'object') {
49
+ setRawString(JSON.stringify(value, null, 2))
50
+ setParseError(null)
51
+ }
52
+ }, [value, disabled, onChange])
53
+
54
+ const handleRawChange = (str: string) => {
55
+ setRawString(str)
56
+ try {
57
+ if (str.trim() === '') {
58
+ onChange({})
59
+ setParseError(null)
60
+ } else {
61
+ const parsed = JSON.parse(str)
62
+ onChange(parsed)
63
+ setParseError(null)
64
+ }
65
+ } catch (e) {
66
+ onChange(str)
67
+ setParseError("Invalid JSON")
68
+ }
69
+ }
70
+
71
+ const switchToBuilder = () => {
72
+ try {
73
+ if (typeof value === 'string') {
74
+ JSON.parse(value)
75
+ }
76
+ setMode('builder')
77
+ } catch (e) {
78
+ alert("Cannot switch to Builder mode: Invalid JSON")
79
+ }
80
+ }
81
+
82
+ return (
83
+ <div className="space-y-4 border rounded-md p-4 bg-card">
84
+ <div className="flex items-center space-x-2 border-b pb-2 mb-2">
85
+ <button
86
+ type="button"
87
+ onClick={() => setMode('raw')}
88
+ className={cn(
89
+ "flex items-center gap-2 px-3 py-1.5 text-sm font-medium rounded-md transition-colors",
90
+ mode === 'raw'
91
+ ? "bg-muted text-foreground"
92
+ : "text-muted-foreground hover:text-foreground hover:bg-muted"
93
+ )}
94
+ >
95
+ <Code className="w-4 h-4" />
96
+ Raw JSON
97
+ </button>
98
+ <button
99
+ type="button"
100
+ onClick={switchToBuilder}
101
+ className={cn(
102
+ "flex items-center gap-2 px-3 py-1.5 text-sm font-medium rounded-md transition-colors",
103
+ mode === 'builder'
104
+ ? "bg-muted text-foreground"
105
+ : "text-muted-foreground hover:text-foreground hover:bg-muted"
106
+ )}
107
+ >
108
+ <LayoutList className="w-4 h-4" />
109
+ Builder
110
+ </button>
111
+ </div>
112
+
113
+ {mode === 'raw' ? (
114
+ <div className="space-y-2">
115
+ <textarea
116
+ value={rawString}
117
+ onChange={(e) => handleRawChange(e.target.value)}
118
+ onBlur={() => {
119
+ try {
120
+ const parsed = JSON.parse(rawString)
121
+ setRawString(JSON.stringify(parsed, null, 2))
122
+ } catch { }
123
+ }}
124
+ placeholder='{"key": "value"}'
125
+ className="w-full rounded border px-3 py-2 min-h-[300px] text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500"
126
+ disabled={disabled}
127
+ />
128
+ {parseError && (
129
+ <div className="text-xs text-red-600">Invalid JSON format</div>
130
+ )}
131
+ </div>
132
+ ) : (
133
+ <div className="min-h-[300px] text-sm overflow-x-auto">
134
+ {typeof value === 'object' && value !== null ? (
135
+ <JsonNode
136
+ data={value}
137
+ onChange={onChange}
138
+ readOnly={disabled}
139
+ isRoot
140
+ />
141
+ ) : (
142
+ <div className="text-muted-foreground italic p-4 text-center">
143
+ Value is not an object or array. Switch to Raw to edit.
144
+ </div>
145
+ )}
146
+ </div>
147
+ )}
148
+
149
+ {error && <div className="text-xs text-red-600">{error}</div>}
150
+ </div>
151
+ )
152
+ }
153
+
154
+ interface JsonNodeProps {
155
+ data: any
156
+ onChange: (val: any) => void
157
+ onDelete?: () => void
158
+ readOnly?: boolean
159
+ label?: string
160
+ isRoot?: boolean
161
+ }
162
+
163
+ function JsonNode({ data, onChange, onDelete, readOnly, label, isRoot }: JsonNodeProps) {
164
+ const type = getJsonType(data)
165
+ const isContainer = type === 'object' || type === 'array'
166
+ const [collapsed, setCollapsed] = React.useState(false)
167
+
168
+ const handleTypeChange = (newType: JsonNodeType) => {
169
+ let newVal: any = ''
170
+ switch (newType) {
171
+ case 'string': newVal = ""; break;
172
+ case 'number': newVal = 0; break;
173
+ case 'boolean': newVal = false; break;
174
+ case 'object': newVal = {}; break;
175
+ case 'array': newVal = []; break;
176
+ case 'null': newVal = null; break;
177
+ }
178
+ onChange(newVal)
179
+ }
180
+
181
+ const handleAddKey = () => {
182
+ if (type === 'object') {
183
+ const newKey = `newKey_${Object.keys(data).length}`
184
+ onChange({ ...data, [newKey]: "" })
185
+ } else if (type === 'array') {
186
+ onChange([...data, ""])
187
+ }
188
+ }
189
+
190
+ const handleChildChange = (key: string | number, newVal: any) => {
191
+ if (type === 'object') {
192
+ onChange({ ...data, [key]: newVal })
193
+ } else if (type === 'array') {
194
+ const arr = [...data]
195
+ arr[Number(key)] = newVal
196
+ onChange(arr)
197
+ }
198
+ }
199
+
200
+ const handleKeyRename = (oldKey: string, newKey: string) => {
201
+ if (oldKey === newKey) return
202
+ const keys = Object.keys(data)
203
+ const newData: any = {}
204
+ keys.forEach(k => {
205
+ if (k === oldKey) {
206
+ newData[newKey] = data[k]
207
+ } else {
208
+ newData[k] = data[k]
209
+ }
210
+ })
211
+ onChange(newData)
212
+ }
213
+
214
+ const handleChildDelete = (key: string | number) => {
215
+ if (type === 'object') {
216
+ const newData = { ...data }
217
+ delete newData[key as string]
218
+ onChange(newData)
219
+ } else if (type === 'array') {
220
+ onChange(data.filter((_: any, i: number) => i !== key))
221
+ }
222
+ }
223
+
224
+ return (
225
+ <div className={cn("pl-0", !isRoot && "pl-4 border-l border-border ml-1")}>
226
+ <div className="flex items-start gap-2 py-1 group">
227
+
228
+ {isContainer && (
229
+ <button type="button" onClick={() => setCollapsed(!collapsed)} className="mt-1 text-muted-foreground hover:text-foreground">
230
+ {collapsed ? <ChevronRight className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
231
+ </button>
232
+ )}
233
+ {!isContainer && !isRoot && <div className="w-3" />} {/* Spacer */}
234
+
235
+ {label !== undefined && !isRoot && (
236
+ <div className="flex items-center gap-1">
237
+ <span className="text-xs text-muted-foreground font-mono">
238
+ {label}
239
+ </span>
240
+ <span className="text-muted-foreground text-xs">:</span>
241
+ </div>
242
+ )}
243
+
244
+ <div className="flex-1 flex gap-2 items-center flex-wrap">
245
+
246
+ {!readOnly && (
247
+ <select
248
+ value={type}
249
+ onChange={(e) => handleTypeChange(e.target.value as JsonNodeType)}
250
+ className="text-xs border rounded px-1 py-0.5 bg-muted text-foreground focus:ring-1 focus:ring-blue-500"
251
+ >
252
+ <option value="string">String</option>
253
+ <option value="number">Number</option>
254
+ <option value="boolean">Boolean</option>
255
+ <option value="object">Object</option>
256
+ <option value="array">Array</option>
257
+ <option value="null">Null</option>
258
+ </select>
259
+ )}
260
+
261
+ {type === 'string' && (
262
+ <input
263
+ className="flex-1 min-w-[120px] text-sm border rounded px-2 py-0.5 focus:outline-none focus:border-blue-500"
264
+ value={data}
265
+ onChange={e => onChange(e.target.value)}
266
+ disabled={readOnly}
267
+ />
268
+ )}
269
+ {type === 'number' && (
270
+ <input
271
+ type="number"
272
+ className="flex-1 w-[100px] text-sm border rounded px-2 py-0.5 focus:outline-none focus:border-blue-500"
273
+ value={data}
274
+ onChange={e => onChange(parseFloat(e.target.value) || 0)}
275
+ disabled={readOnly}
276
+ />
277
+ )}
278
+ {type === 'boolean' && (
279
+ <select
280
+ className="flex-1 w-[100px] text-sm border rounded px-2 py-0.5 focus:outline-none focus:border-blue-500"
281
+ value={String(data)}
282
+ onChange={e => onChange(e.target.value === 'true')}
283
+ disabled={readOnly}
284
+ >
285
+ <option value="true">true</option>
286
+ <option value="false">false</option>
287
+ </select>
288
+ )}
289
+ {type === 'null' && <span className="text-xs text-muted-foreground">null</span>}
290
+ {isContainer && (
291
+ <span className="text-xs text-muted-foreground">
292
+ {type === 'object' ? `{ ${Object.keys(data).length} items }` : `[ ${data.length} items ]`}
293
+ </span>
294
+ )}
295
+
296
+ {onDelete && !readOnly && (
297
+ <button
298
+ type="button"
299
+ onClick={onDelete}
300
+ className="text-muted-foreground hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"
301
+ title="Remove item"
302
+ >
303
+ <Trash2 className="w-3.5 h-3.5" />
304
+ </button>
305
+ )}
306
+ </div>
307
+ </div>
308
+
309
+ {isContainer && !collapsed && (
310
+ <div className="flex flex-col gap-1 w-full pl-2">
311
+ {type === 'object' && Object.entries(data).map(([key, val], idx) => (
312
+ <div key={idx} className="flex">
313
+ <div className="pt-2">
314
+ {/* Key Renamer */}
315
+ <input
316
+ className="w-[100px] text-xs font-mono border-b border-transparent hover:border-gray-300 focus:border-blue-500 bg-transparent focus:outline-none text-right pr-1"
317
+ value={key}
318
+ onChange={(e) => handleKeyRename(key, e.target.value)}
319
+ disabled={readOnly}
320
+ />
321
+ </div>
322
+ <div className="flex-1">
323
+ <JsonNode
324
+ data={val}
325
+ onChange={(v) => handleChildChange(key, v)}
326
+ onDelete={() => handleChildDelete(key)}
327
+ readOnly={readOnly}
328
+ />
329
+ </div>
330
+ </div>
331
+ ))}
332
+
333
+ {type === 'array' && (data as any[]).map((val, idx) => (
334
+ <JsonNode
335
+ key={idx}
336
+ label={String(idx)}
337
+ data={val}
338
+ onChange={(v) => handleChildChange(idx, v)}
339
+ onDelete={() => handleChildDelete(idx)}
340
+ readOnly={readOnly}
341
+ />
342
+ ))}
343
+
344
+ {!readOnly && (
345
+ <div className="pl-4 mt-1">
346
+ <Button
347
+ type="button"
348
+ variant="outline"
349
+ size="sm"
350
+ onClick={handleAddKey}
351
+ className="h-6 text-xs"
352
+ >
353
+ <Plus className="w-3 h-3 mr-1" />
354
+ Add {type === 'object' ? 'Property' : 'Item'}
355
+ </Button>
356
+ </div>
357
+ )}
358
+ </div>
359
+ )}
360
+ </div>
361
+ )
362
+ }
@@ -0,0 +1,254 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '@open-mercato/shared/lib/utils'
5
+ import { Button } from '@open-mercato/ui/primitives/button'
6
+ import { Check, Copy, ChevronRight, ChevronDown } from 'lucide-react'
7
+
8
+ type JsonDisplayProps = {
9
+ data: unknown
10
+ title?: string
11
+ className?: string
12
+ defaultExpanded?: boolean
13
+ maxInitialDepth?: number
14
+ theme?: 'light' | 'dark'
15
+ showCopy?: boolean
16
+ maxHeight?: string
17
+ }
18
+
19
+ type JsonNodeProps = {
20
+ data: unknown
21
+ depth: number
22
+ maxInitialDepth: number
23
+ theme: 'light' | 'dark'
24
+ isLast?: boolean
25
+ parentKey?: string
26
+ }
27
+
28
+ export function JsonDisplay({
29
+ data,
30
+ title,
31
+ className,
32
+ defaultExpanded = false,
33
+ maxInitialDepth = 2,
34
+ theme = 'light',
35
+ showCopy = true,
36
+ maxHeight,
37
+ }: JsonDisplayProps) {
38
+ const [copied, setCopied] = React.useState(false)
39
+
40
+ const jsonString = React.useMemo(() => {
41
+ try {
42
+ return JSON.stringify(data, null, 2)
43
+ } catch (err) {
44
+ return String(data)
45
+ }
46
+ }, [data])
47
+
48
+ const handleCopy = React.useCallback(async () => {
49
+ try {
50
+ await navigator.clipboard.writeText(jsonString)
51
+ setCopied(true)
52
+ setTimeout(() => setCopied(false), 2000)
53
+ } catch (err) {
54
+ console.error('Failed to copy to clipboard:', err)
55
+ }
56
+ }, [jsonString])
57
+
58
+ const containerClasses = cn(
59
+ 'rounded-lg border bg-card p-6',
60
+ className
61
+ )
62
+
63
+ return (
64
+ <div className={containerClasses}>
65
+ {(title || showCopy) && (
66
+ <div className="flex items-center justify-between mb-4">
67
+ {title && <h2 className="text-lg font-semibold">{title}</h2>}
68
+ {showCopy && (
69
+ <Button
70
+ type="button"
71
+ variant="ghost"
72
+ size="sm"
73
+ onClick={handleCopy}
74
+ className="h-7 px-2"
75
+ >
76
+ {copied ? (
77
+ <>
78
+ <Check className="h-3 w-3 mr-1" />
79
+ <span className="text-xs">Copied</span>
80
+ </>
81
+ ) : (
82
+ <>
83
+ <Copy className="h-3 w-3 mr-1" />
84
+ <span className="text-xs">Copy</span>
85
+ </>
86
+ )}
87
+ </Button>
88
+ )}
89
+ </div>
90
+ )}
91
+ <div
92
+ className="bg-muted p-4 rounded overflow-x-auto font-mono text-sm"
93
+ style={maxHeight ? { maxHeight, overflowY: 'auto' } : undefined}
94
+ >
95
+ <JsonNode
96
+ data={data}
97
+ depth={0}
98
+ maxInitialDepth={defaultExpanded ? Infinity : maxInitialDepth}
99
+ theme={theme}
100
+ />
101
+ </div>
102
+ </div>
103
+ )
104
+ }
105
+
106
+ const JsonNode = React.memo<JsonNodeProps>(({ data, depth, maxInitialDepth, theme, isLast = true, parentKey }) => {
107
+ const [isExpanded, setIsExpanded] = React.useState(depth < maxInitialDepth)
108
+
109
+ const indent = depth * 16
110
+
111
+ const themeClasses = {
112
+ key: theme === 'light' ? 'text-blue-600' : 'text-blue-400',
113
+ string: theme === 'light' ? 'text-green-600' : 'text-green-400',
114
+ number: theme === 'light' ? 'text-purple-600' : 'text-purple-400',
115
+ boolean: theme === 'light' ? 'text-orange-600' : 'text-orange-400',
116
+ null: 'text-muted-foreground',
117
+ punctuation: 'text-muted-foreground',
118
+ }
119
+
120
+ if (data === null) {
121
+ return (
122
+ <span className={themeClasses.null}>null</span>
123
+ )
124
+ }
125
+
126
+ if (data === undefined) {
127
+ return (
128
+ <span className={themeClasses.null}>undefined</span>
129
+ )
130
+ }
131
+
132
+ if (typeof data === 'string') {
133
+ return (
134
+ <span className={themeClasses.string}>"{data}"</span>
135
+ )
136
+ }
137
+
138
+ if (typeof data === 'number') {
139
+ return (
140
+ <span className={themeClasses.number}>{data}</span>
141
+ )
142
+ }
143
+
144
+ if (typeof data === 'boolean') {
145
+ return (
146
+ <span className={themeClasses.boolean}>{String(data)}</span>
147
+ )
148
+ }
149
+
150
+ if (Array.isArray(data)) {
151
+ if (data.length === 0) {
152
+ return <span className={themeClasses.punctuation}>[]</span>
153
+ }
154
+
155
+ return (
156
+ <span>
157
+ <button
158
+ type="button"
159
+ onClick={() => setIsExpanded(!isExpanded)}
160
+ className="inline-flex items-center hover:bg-muted-foreground/10 rounded px-1 -mx-1 transition-colors cursor-pointer"
161
+ >
162
+ {isExpanded ? (
163
+ <ChevronDown className="h-3 w-3 mr-1" />
164
+ ) : (
165
+ <ChevronRight className="h-3 w-3 mr-1" />
166
+ )}
167
+ <span className={themeClasses.punctuation}>
168
+ [{data.length}]
169
+ </span>
170
+ </button>
171
+ {isExpanded && (
172
+ <>
173
+ <br />
174
+ {data.map((item, index) => (
175
+ <div key={index} style={{ paddingLeft: `${indent + 16}px` }}>
176
+ <JsonNode
177
+ data={item}
178
+ depth={depth + 1}
179
+ maxInitialDepth={maxInitialDepth}
180
+ theme={theme}
181
+ isLast={index === data.length - 1}
182
+ />
183
+ {index < data.length - 1 && (
184
+ <span className={themeClasses.punctuation}>,</span>
185
+ )}
186
+ <br />
187
+ </div>
188
+ ))}
189
+ <span style={{ paddingLeft: `${indent}px` }} className={themeClasses.punctuation}>
190
+ ]
191
+ </span>
192
+ </>
193
+ )}
194
+ </span>
195
+ )
196
+ }
197
+
198
+ if (typeof data === 'object') {
199
+ const entries = Object.entries(data)
200
+
201
+ if (entries.length === 0) {
202
+ return <span className={themeClasses.punctuation}>{'{}'}</span>
203
+ }
204
+
205
+ return (
206
+ <span>
207
+ <button
208
+ type="button"
209
+ onClick={() => setIsExpanded(!isExpanded)}
210
+ className="inline-flex items-center hover:bg-muted-foreground/10 rounded px-1 -mx-1 transition-colors cursor-pointer"
211
+ >
212
+ {isExpanded ? (
213
+ <ChevronDown className="h-3 w-3 mr-1" />
214
+ ) : (
215
+ <ChevronRight className="h-3 w-3 mr-1" />
216
+ )}
217
+ <span className={themeClasses.punctuation}>
218
+ {'{'}{entries.length}{'}'}
219
+ </span>
220
+ </button>
221
+ {isExpanded && (
222
+ <>
223
+ <br />
224
+ {entries.map(([key, value], index) => (
225
+ <div key={key} style={{ paddingLeft: `${indent + 16}px` }}>
226
+ <span className={themeClasses.key}>"{key}"</span>
227
+ <span className={themeClasses.punctuation}>: </span>
228
+ <JsonNode
229
+ data={value}
230
+ depth={depth + 1}
231
+ maxInitialDepth={maxInitialDepth}
232
+ theme={theme}
233
+ isLast={index === entries.length - 1}
234
+ parentKey={key}
235
+ />
236
+ {index < entries.length - 1 && (
237
+ <span className={themeClasses.punctuation}>,</span>
238
+ )}
239
+ <br />
240
+ </div>
241
+ ))}
242
+ <span style={{ paddingLeft: `${indent}px` }} className={themeClasses.punctuation}>
243
+ {'}'}
244
+ </span>
245
+ </>
246
+ )}
247
+ </span>
248
+ )
249
+ }
250
+
251
+ return <span className={themeClasses.null}>{String(data)}</span>
252
+ })
253
+
254
+ JsonNode.displayName = 'JsonNode'
@@ -0,0 +1,30 @@
1
+ import * as React from 'react'
2
+ import { cn } from '@open-mercato/shared/lib/utils'
3
+
4
+ export function Page({ children, className }: { children: React.ReactNode; className?: string }) {
5
+ return <div className={cn('space-y-6', className)}>{children}</div>
6
+ }
7
+
8
+ export function PageHeader({
9
+ title,
10
+ description,
11
+ actions,
12
+ }: {
13
+ title: string
14
+ description?: string
15
+ actions?: React.ReactNode
16
+ }) {
17
+ return (
18
+ <div className="flex items-start justify-between gap-4">
19
+ <div>
20
+ <h1 className="text-2xl font-semibold leading-tight">{title}</h1>
21
+ {description ? <p className="text-sm text-muted-foreground mt-1">{description}</p> : null}
22
+ </div>
23
+ {actions ? <div className="flex items-center gap-2">{actions}</div> : null}
24
+ </div>
25
+ )
26
+ }
27
+
28
+ export function PageBody({ children, className }: { children: React.ReactNode; className?: string }) {
29
+ return <div className={cn('space-y-4', className)}>{children}</div>
30
+ }