@nitrogenbuilder/connector-payload 0.1.34 → 0.1.36

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 (38) hide show
  1. package/dist/collections/NitrogenComponentCatalog.d.ts +2 -0
  2. package/dist/collections/NitrogenComponentCatalog.js +44 -0
  3. package/dist/collections/NitrogenComponentUsage.d.ts +2 -0
  4. package/dist/collections/NitrogenComponentUsage.js +81 -0
  5. package/dist/components/LockedJsonField.d.ts +2 -9
  6. package/dist/components/LockedJsonField.js +7 -6
  7. package/dist/components/LockedJsonFieldRuntime.d.ts +3 -7
  8. package/dist/components/LockedJsonFieldRuntime.js +21 -22
  9. package/dist/components/NitrogenComponentInventoryClient.d.ts +13 -0
  10. package/dist/components/NitrogenComponentInventoryClient.js +309 -0
  11. package/dist/components/NitrogenComponentInventoryField.d.ts +3 -0
  12. package/dist/components/NitrogenComponentInventoryField.js +26 -0
  13. package/dist/components/NitrogenComponentInventoryNavLink.d.ts +1 -0
  14. package/dist/components/NitrogenComponentInventoryNavLink.js +75 -0
  15. package/dist/components/NitrogenComponentInventoryReindexButton.d.ts +1 -0
  16. package/dist/components/NitrogenComponentInventoryReindexButton.js +91 -0
  17. package/dist/components/NitrogenComponentInventoryView.d.ts +2 -0
  18. package/dist/components/NitrogenComponentInventoryView.js +28 -0
  19. package/dist/components/NitrogenDataViewer.d.ts +2 -2
  20. package/dist/components/NitrogenDataViewer.js +7 -6
  21. package/dist/components/NitrogenDataViewerRuntime.d.ts +7 -2
  22. package/dist/components/NitrogenDataViewerRuntime.js +171 -93
  23. package/dist/components/NitrogenNavGroup.d.ts +1 -0
  24. package/dist/components/NitrogenNavGroup.js +74 -0
  25. package/dist/editor/NitrogenEditorPage.d.ts +1 -2
  26. package/dist/endpoints/component-inventory.d.ts +6 -0
  27. package/dist/endpoints/component-inventory.js +20 -0
  28. package/dist/frontend/NitrogenWrapper.d.ts +1 -2
  29. package/dist/globals/NitrogenComponents.d.ts +2 -0
  30. package/dist/globals/NitrogenComponents.js +22 -0
  31. package/dist/index.d.ts +12 -2
  32. package/dist/index.js +94 -3
  33. package/dist/inventory/indexing.d.ts +31 -0
  34. package/dist/inventory/indexing.js +230 -0
  35. package/dist/inventory/manifest.d.ts +3 -0
  36. package/dist/inventory/manifest.js +22 -0
  37. package/dist/types.d.ts +35 -1
  38. package/package.json +7 -2
@@ -0,0 +1,75 @@
1
+ 'use client';
2
+ import { usePathname } from 'next/navigation';
3
+ import { useEffect } from 'react';
4
+ const groupSelector = '#nav-group-Nitrogen .nav-group__content';
5
+ export default function NitrogenComponentInventoryNavLink() {
6
+ const pathname = usePathname();
7
+ const isActive = pathname === '/admin/nitrogen/components' || pathname?.startsWith('/admin/nitrogen/components/');
8
+ useEffect(() => {
9
+ const linkId = 'nitrogen-components-nav-link';
10
+ const renderLink = () => {
11
+ const target = document.querySelector(groupSelector);
12
+ if (!target) {
13
+ return;
14
+ }
15
+ const existing = document.getElementById(linkId);
16
+ if (existing) {
17
+ existing.remove();
18
+ }
19
+ const link = document.createElement('a');
20
+ link.id = linkId;
21
+ link.href = '/admin/nitrogen/components';
22
+ link.textContent = 'Nitrogen Components';
23
+ link.style.alignItems = 'center';
24
+ link.style.color = 'inherit';
25
+ link.style.display = 'flex';
26
+ link.style.fontSize = '18px';
27
+ link.style.fontWeight = isActive ? '700' : '500';
28
+ link.style.gap = '12px';
29
+ link.style.minHeight = '40px';
30
+ link.style.opacity = isActive ? '1' : '0.92';
31
+ link.style.padding = '6px 0';
32
+ link.style.textDecoration = 'none';
33
+ const icon = document.createElement('span');
34
+ icon.ariaHidden = 'true';
35
+ icon.style.color = '#6b7280';
36
+ icon.style.display = 'inline-flex';
37
+ icon.style.fontSize = '16px';
38
+ icon.style.justifyContent = 'center';
39
+ icon.style.width = '20px';
40
+ icon.innerHTML = `
41
+ <svg fill="none" height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg">
42
+ <path
43
+ d="M4 5.5C4 4.67157 4.67157 4 5.5 4H14.5C15.3284 4 16 4.67157 16 5.5V14.5C16 15.3284 15.3284 16 14.5 16H5.5C4.67157 16 4 15.3284 4 14.5V5.5Z"
44
+ stroke="currentColor"
45
+ stroke-width="1.5"
46
+ />
47
+ <path d="M7 8H13" stroke="currentColor" stroke-linecap="round" stroke-width="1.5" />
48
+ <path d="M7 10.5H13" stroke="currentColor" stroke-linecap="round" stroke-width="1.5" />
49
+ <path d="M7 13H10.5" stroke="currentColor" stroke-linecap="round" stroke-width="1.5" />
50
+ </svg>
51
+ `;
52
+ const label = document.createElement('span');
53
+ label.textContent = 'Nitrogen Components';
54
+ link.textContent = '';
55
+ link.append(icon, label);
56
+ if (isActive) {
57
+ link.setAttribute('aria-current', 'page');
58
+ }
59
+ target.append(link);
60
+ };
61
+ renderLink();
62
+ const observer = new MutationObserver(() => {
63
+ renderLink();
64
+ });
65
+ observer.observe(document.body, {
66
+ childList: true,
67
+ subtree: true,
68
+ });
69
+ return () => {
70
+ observer.disconnect();
71
+ document.getElementById(linkId)?.remove();
72
+ };
73
+ }, [isActive]);
74
+ return null;
75
+ }
@@ -0,0 +1 @@
1
+ export default function NitrogenComponentInventoryReindexButton(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,91 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from 'react';
4
+ const statusStorageKey = 'nitrogen-component-inventory:last-reindex-status';
5
+ export default function NitrogenComponentInventoryReindexButton() {
6
+ const [loading, setLoading] = useState(false);
7
+ const [status, setStatus] = useState(null);
8
+ useEffect(() => {
9
+ const rawValue = window.sessionStorage.getItem(statusStorageKey);
10
+ if (!rawValue) {
11
+ return;
12
+ }
13
+ try {
14
+ setStatus(JSON.parse(rawValue));
15
+ }
16
+ catch {
17
+ window.sessionStorage.removeItem(statusStorageKey);
18
+ }
19
+ }, []);
20
+ function persistStatus(nextStatus) {
21
+ setStatus(nextStatus);
22
+ window.sessionStorage.setItem(statusStorageKey, JSON.stringify(nextStatus));
23
+ }
24
+ async function handleClick() {
25
+ try {
26
+ setLoading(true);
27
+ const response = await fetch('/api/nitrogen/v1/component-inventory/reindex', {
28
+ method: 'POST',
29
+ });
30
+ const data = await response.json();
31
+ if (!response.ok) {
32
+ persistStatus({
33
+ kind: 'error',
34
+ message: data?.error || 'Failed to reindex component inventory.',
35
+ timestamp: new Date().toISOString(),
36
+ });
37
+ return;
38
+ }
39
+ persistStatus({
40
+ kind: 'success',
41
+ message: `Reindexed ${data.documentCount} documents and ${data.usageCount} component usages.`,
42
+ timestamp: new Date().toISOString(),
43
+ });
44
+ window.location.reload();
45
+ }
46
+ catch (error) {
47
+ console.error(error);
48
+ persistStatus({
49
+ kind: 'error',
50
+ message: 'Failed to reindex component inventory.',
51
+ timestamp: new Date().toISOString(),
52
+ });
53
+ }
54
+ finally {
55
+ setLoading(false);
56
+ }
57
+ }
58
+ function clearStatus() {
59
+ setStatus(null);
60
+ window.sessionStorage.removeItem(statusStorageKey);
61
+ }
62
+ return (_jsxs("div", { style: { display: 'grid', gap: 12 }, children: [_jsx("div", { style: { display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }, children: _jsx("button", { type: "button", onClick: handleClick, disabled: loading, style: {
63
+ border: 'none',
64
+ borderRadius: 6,
65
+ background: '#171717',
66
+ color: '#fff',
67
+ cursor: loading ? 'wait' : 'pointer',
68
+ fontSize: 14,
69
+ fontWeight: 600,
70
+ padding: '10px 14px',
71
+ }, children: loading ? 'Reindexing...' : 'Reindex Components' }) }), status ? (_jsxs("div", { style: {
72
+ alignItems: 'start',
73
+ background: status.kind === 'success' ? '#ecfdf5' : '#fef2f2',
74
+ border: `1px solid ${status.kind === 'success' ? '#a7f3d0' : '#fecaca'}`,
75
+ borderRadius: 10,
76
+ color: status.kind === 'success' ? '#065f46' : '#991b1b',
77
+ display: 'grid',
78
+ gap: 6,
79
+ gridTemplateColumns: '1fr auto',
80
+ padding: '12px 14px',
81
+ }, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 13, fontWeight: 700 }, children: status.kind === 'success' ? 'Last Reindex Completed' : 'Reindex Failed' }), _jsx("div", { style: { fontSize: 13, marginTop: 2 }, children: status.message }), _jsx("div", { style: { fontSize: 12, marginTop: 4, opacity: 0.75 }, children: new Date(status.timestamp).toLocaleString() })] }), _jsx("button", { type: "button", onClick: clearStatus, style: {
82
+ appearance: 'none',
83
+ background: 'transparent',
84
+ border: 'none',
85
+ color: 'inherit',
86
+ cursor: 'pointer',
87
+ fontSize: 12,
88
+ fontWeight: 700,
89
+ padding: 0,
90
+ }, children: "Dismiss" })] })) : null] }));
91
+ }
@@ -0,0 +1,2 @@
1
+ import type { AdminViewServerProps } from 'payload';
2
+ export default function NitrogenComponentInventoryView({ clientConfig, initPageResult, i18n, locale, params, searchParams, viewActions, viewType, }: AdminViewServerProps): Promise<import("react/jsx-runtime").JSX.Element>;
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // @ts-expect-error The consuming Payload app provides @payloadcms/next at runtime.
3
+ import { DefaultTemplate } from '@payloadcms/next/templates';
4
+ import { findAllDocs, NITROGEN_COMPONENT_CATALOG_COLLECTION, NITROGEN_COMPONENT_USAGE_COLLECTION, } from '../inventory/indexing';
5
+ import NitrogenComponentInventoryClient from './NitrogenComponentInventoryClient';
6
+ import NitrogenComponentInventoryReindexButton from './NitrogenComponentInventoryReindexButton';
7
+ export default async function NitrogenComponentInventoryView({ clientConfig, initPageResult, i18n, locale, params, searchParams, viewActions, viewType, }) {
8
+ const payload = initPageResult.req.payload;
9
+ const [catalogDocs, usageDocs] = await Promise.all([
10
+ findAllDocs(payload, NITROGEN_COMPONENT_CATALOG_COLLECTION),
11
+ findAllDocs(payload, NITROGEN_COMPONENT_USAGE_COLLECTION),
12
+ ]);
13
+ const catalogByName = new Map(catalogDocs.map((doc) => [doc.componentName, doc.definition]));
14
+ const usageByName = new Map();
15
+ for (const usageDoc of usageDocs) {
16
+ const docs = usageByName.get(usageDoc.componentName) || [];
17
+ docs.push(usageDoc);
18
+ usageByName.set(usageDoc.componentName, docs);
19
+ }
20
+ const componentNames = Array.from(new Set([...catalogByName.keys(), ...usageByName.keys()])).sort((a, b) => a.localeCompare(b));
21
+ const components = componentNames.map((componentName) => ({
22
+ componentName,
23
+ definition: catalogByName.get(componentName),
24
+ isUnknown: !catalogByName.get(componentName),
25
+ usages: usageByName.get(componentName) || [],
26
+ }));
27
+ return (_jsx(DefaultTemplate, { i18n: i18n, locale: locale, params: params, payload: payload, req: initPageResult.req, searchParams: searchParams, user: initPageResult.req.user, viewActions: viewActions, viewType: viewType, visibleEntities: initPageResult.visibleEntities, children: _jsxs("div", { style: { padding: 24 }, children: [_jsxs("div", { style: { display: 'grid', gap: 12, marginBottom: 24 }, children: [_jsx("h1", { style: { margin: 0, fontSize: 28 }, children: "Nitrogen Components" }), _jsx("p", { style: { margin: 0, color: '#4b5563', maxWidth: 920 }, children: "Inventory view for the registered Nitrogen component manifest and all indexed usages across Nitrogen-enabled documents and templates." }), _jsx(NitrogenComponentInventoryReindexButton, {})] }), _jsx(NitrogenComponentInventoryClient, { adminRoute: clientConfig.routes.admin, components: components })] }) }));
28
+ }
@@ -1,3 +1,3 @@
1
- import type { JSONFieldClientComponent } from 'payload';
2
- declare const NitrogenDataViewer: JSONFieldClientComponent;
1
+ import type { JSONFieldServerComponent } from 'payload';
2
+ declare const NitrogenDataViewer: JSONFieldServerComponent;
3
3
  export default NitrogenDataViewer;
@@ -1,7 +1,8 @@
1
- 'use client';
2
- import dynamic from 'next/dynamic';
3
- const NitrogenDataViewer = dynamic(() => import('./NitrogenDataViewerRuntime'), {
4
- loading: () => null,
5
- ssr: false,
6
- });
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import NitrogenDataViewerRuntime from './NitrogenDataViewerRuntime';
3
+ const NitrogenDataViewer = ({ field, path, value }) => {
4
+ const label = typeof field.label === 'string' ? field.label : field.name;
5
+ const description = typeof field.admin?.description === 'string' ? field.admin.description : undefined;
6
+ return (_jsx(NitrogenDataViewerRuntime, { description: description, initialValue: value, label: label, path: path }));
7
+ };
7
8
  export default NitrogenDataViewer;
@@ -1,3 +1,8 @@
1
- import type { JSONFieldClientComponent } from 'payload';
2
- declare const NitrogenDataViewerRuntime: JSONFieldClientComponent;
1
+ import React from 'react';
2
+ declare const NitrogenDataViewerRuntime: React.FC<{
3
+ description?: string;
4
+ initialValue?: unknown;
5
+ label: string;
6
+ path?: string;
7
+ }>;
3
8
  export default NitrogenDataViewerRuntime;
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useState, useCallback } from 'react';
4
- import { Collapsible, useField } from '@payloadcms/ui';
3
+ import { useField } from '@payloadcms/ui';
4
+ import { useCallback, useMemo, useState } from 'react';
5
5
  function updateAtPath(root, path, newValue) {
6
6
  if (path.length === 0)
7
7
  return newValue;
@@ -74,10 +74,10 @@ function renameKeyAtPath(root, path, newKey) {
74
74
  return navigate(root, parentPath);
75
75
  }
76
76
  function addChildAtPath(root, path) {
77
- function getAt(node, p) {
78
- if (p.length === 0)
77
+ function getAt(node, currentPath) {
78
+ if (currentPath.length === 0)
79
79
  return node;
80
- const [head, ...rest] = p;
80
+ const [head, ...rest] = currentPath;
81
81
  if (Array.isArray(node))
82
82
  return getAt(node[head], rest);
83
83
  if (typeof node === 'object' && node !== null) {
@@ -92,88 +92,117 @@ function addChildAtPath(root, path) {
92
92
  if (typeof target === 'object' && target !== null) {
93
93
  const obj = target;
94
94
  let key = 'newKey';
95
- let i = 1;
96
- while (key in obj)
97
- key = `newKey${i++}`;
95
+ let index = 1;
96
+ while (key in obj) {
97
+ key = `newKey${index++}`;
98
+ }
98
99
  return updateAtPath(root, path, { ...obj, [key]: '' });
99
100
  }
100
101
  return root;
101
102
  }
102
- function InlineEdit({ display, onCommit, colorClass, }) {
103
+ function coerceDisplayValue(value) {
104
+ if (typeof value === 'string') {
105
+ return value;
106
+ }
107
+ return value != null ? JSON.stringify(value, null, 2) : '[]';
108
+ }
109
+ function parseJsonValue(value) {
110
+ if (typeof value === 'string') {
111
+ try {
112
+ return JSON.parse(value);
113
+ }
114
+ catch {
115
+ return [];
116
+ }
117
+ }
118
+ return value ?? [];
119
+ }
120
+ function InlineEdit({ colorClass, display, onCommit, }) {
103
121
  const [draft, setDraft] = useState(display);
104
122
  const commit = () => onCommit(draft);
105
- return (_jsx("input", { autoFocus: true, value: draft, onChange: (e) => setDraft(e.target.value), onBlur: commit, onKeyDown: (e) => {
106
- if (e.key === 'Enter') {
107
- e.preventDefault();
123
+ return (_jsx("input", { autoFocus: true, className: colorClass, onBlur: commit, onChange: (event) => setDraft(event.target.value), onClick: (event) => event.stopPropagation(), onKeyDown: (event) => {
124
+ if (event.key === 'Enter') {
125
+ event.preventDefault();
108
126
  commit();
109
127
  }
110
- if (e.key === 'Escape')
128
+ if (event.key === 'Escape') {
111
129
  onCommit(display);
112
- }, onClick: (e) => e.stopPropagation(), className: colorClass, style: {
130
+ }
131
+ }, style: {
113
132
  background: 'transparent',
114
133
  border: 'none',
115
134
  borderBottom: '1px solid currentColor',
116
135
  color: 'inherit',
117
136
  font: 'inherit',
118
- padding: '0 2px',
119
- outline: 'none',
137
+ maxWidth: '500px',
120
138
  minWidth: '4ch',
139
+ outline: 'none',
140
+ padding: '0 2px',
121
141
  width: `${Math.max(draft.length + 2, 4)}ch`,
122
- maxWidth: '500px',
123
- } }));
142
+ }, value: draft }));
124
143
  }
125
- function ActionBtn({ label, color, onClick, }) {
126
- return (_jsx("button", { type: "button", onClick: onClick, style: {
127
- marginLeft: 6,
128
- padding: '0 5px',
129
- fontSize: '10px',
130
- lineHeight: '16px',
144
+ function ActionButton({ color, label, onClick, }) {
145
+ return (_jsx("button", { onClick: onClick, style: {
131
146
  background: color,
132
- color: '#fff',
133
147
  border: 'none',
134
- borderRadius: '3px',
148
+ borderRadius: 3,
149
+ color: '#fff',
135
150
  cursor: 'pointer',
136
151
  flexShrink: 0,
152
+ fontSize: 10,
153
+ lineHeight: '16px',
154
+ marginLeft: 6,
137
155
  opacity: 0.85,
138
- }, children: label }));
156
+ padding: '0 5px',
157
+ }, type: "button", children: label }));
139
158
  }
140
- function JsonNode({ k, value, depth, forceOpen, path, unlocked, callbacks, }) {
159
+ function JsonNode({ callbacks, depth, forceOpen, k, path, unlocked, value, }) {
141
160
  const defaultOpen = forceOpen !== null ? forceOpen : depth < 2;
142
161
  const [open, setOpen] = useState(defaultOpen);
143
- const [editingValue, setEditingValue] = useState(false);
144
162
  const [editingKey, setEditingKey] = useState(false);
145
- const { onUpdate, onDelete, onRenameKey, onAddChild } = callbacks;
146
- const del = unlocked ? (_jsx(ActionBtn, { label: "\u00D7", color: "#ef4444", onClick: (e) => { e.stopPropagation(); onDelete(path); } })) : null;
147
- const keyEl = k === undefined ? null : (_jsxs(_Fragment, { children: [_jsxs("span", { className: "njv-key", children: ["\"", unlocked && editingKey ? (_jsx(InlineEdit, { display: k, colorClass: "njv-key", onCommit: (newKey) => {
163
+ const [editingValue, setEditingValue] = useState(false);
164
+ const { onAddChild, onDelete, onRenameKey, onUpdate } = callbacks;
165
+ const deleteButton = unlocked ? (_jsx(ActionButton, { color: "#ef4444", label: "\u00D7", onClick: (event) => {
166
+ event.stopPropagation();
167
+ onDelete(path);
168
+ } })) : null;
169
+ const keyElement = k === undefined ? null : (_jsxs(_Fragment, { children: [_jsxs("span", { className: "njv-key", children: ["\"", unlocked && editingKey ? (_jsx(InlineEdit, { colorClass: "njv-key", display: k, onCommit: (newKey) => {
148
170
  setEditingKey(false);
149
- if (newKey !== k)
171
+ if (newKey !== k) {
150
172
  onRenameKey(path, newKey);
151
- } })) : (_jsx("span", { onClick: unlocked ? (e) => { e.stopPropagation(); setEditingKey(true); } : undefined, style: unlocked ? { cursor: 'text', borderBottom: '1px dotted currentColor' } : undefined, children: k })), "\""] }), _jsx("span", { className: "njv-brace", children: ": " })] }));
173
+ }
174
+ } })) : (_jsx("span", { onClick: unlocked ? (event) => { event.stopPropagation(); setEditingKey(true); } : undefined, style: unlocked ? { borderBottom: '1px dotted currentColor', cursor: 'text' } : undefined, children: k })), "\""] }), _jsx("span", { className: "njv-brace", children: ": " })] }));
152
175
  if (value === null) {
153
- return (_jsxs("div", { className: "njv-line", style: { alignItems: 'center' }, children: [_jsxs("span", { className: "njv-code", style: { paddingLeft: depth * 20 }, children: [keyEl, _jsx("span", { className: "njv-null", title: unlocked ? 'Click to convert to empty string' : undefined, onClick: unlocked ? (e) => { e.stopPropagation(); onUpdate(path, ''); } : undefined, style: unlocked ? { cursor: 'pointer', borderBottom: '1px dotted currentColor' } : undefined, children: "null" })] }), del] }));
176
+ return (_jsxs("div", { className: "njv-line", style: { alignItems: 'center' }, children: [_jsxs("span", { className: "njv-code", style: { paddingLeft: depth * 20 }, children: [keyElement, _jsx("span", { className: "njv-null", onClick: unlocked ? (event) => { event.stopPropagation(); onUpdate(path, ''); } : undefined, style: unlocked ? { borderBottom: '1px dotted currentColor', cursor: 'pointer' } : undefined, title: unlocked ? 'Click to convert to empty string' : undefined, children: "null" })] }), deleteButton] }));
154
177
  }
155
178
  if (typeof value === 'string') {
156
- return (_jsxs("div", { className: "njv-line", style: { alignItems: 'center' }, children: [_jsxs("span", { className: "njv-code", style: { paddingLeft: depth * 20 }, children: [keyEl, _jsxs("span", { className: "njv-string", children: ["\"", unlocked && editingValue ? (_jsx(InlineEdit, { display: value, colorClass: "njv-string", onCommit: (v) => {
179
+ return (_jsxs("div", { className: "njv-line", style: { alignItems: 'center' }, children: [_jsxs("span", { className: "njv-code", style: { paddingLeft: depth * 20 }, children: [keyElement, _jsxs("span", { className: "njv-string", children: ["\"", unlocked && editingValue ? (_jsx(InlineEdit, { colorClass: "njv-string", display: value, onCommit: (nextValue) => {
157
180
  setEditingValue(false);
158
- onUpdate(path, v);
159
- } })) : (_jsx("span", { onClick: unlocked ? (e) => { e.stopPropagation(); setEditingValue(true); } : undefined, style: unlocked ? { cursor: 'text', borderBottom: '1px dotted currentColor' } : undefined, children: value.length > 120 ? value.slice(0, 120) + '…' : value })), "\""] })] }), del] }));
181
+ onUpdate(path, nextValue);
182
+ } })) : (_jsx("span", { onClick: unlocked ? (event) => { event.stopPropagation(); setEditingValue(true); } : undefined, style: unlocked ? { borderBottom: '1px dotted currentColor', cursor: 'text' } : undefined, children: value.length > 120 ? `${value.slice(0, 120)}…` : value })), "\""] })] }), deleteButton] }));
160
183
  }
161
184
  if (typeof value === 'number') {
162
- return (_jsxs("div", { className: "njv-line", style: { alignItems: 'center' }, children: [_jsxs("span", { className: "njv-code", style: { paddingLeft: depth * 20 }, children: [keyEl, _jsx("span", { className: "njv-number", children: unlocked && editingValue ? (_jsx(InlineEdit, { display: String(value), colorClass: "njv-number", onCommit: (v) => {
185
+ return (_jsxs("div", { className: "njv-line", style: { alignItems: 'center' }, children: [_jsxs("span", { className: "njv-code", style: { paddingLeft: depth * 20 }, children: [keyElement, _jsx("span", { className: "njv-number", children: unlocked && editingValue ? (_jsx(InlineEdit, { colorClass: "njv-number", display: String(value), onCommit: (nextValue) => {
163
186
  setEditingValue(false);
164
- const n = Number(v);
165
- onUpdate(path, Number.isNaN(n) ? v : n);
166
- } })) : (_jsx("span", { onClick: unlocked ? (e) => { e.stopPropagation(); setEditingValue(true); } : undefined, style: unlocked ? { cursor: 'text', borderBottom: '1px dotted currentColor' } : undefined, children: value })) })] }), del] }));
187
+ const numericValue = Number(nextValue);
188
+ onUpdate(path, Number.isNaN(numericValue) ? nextValue : numericValue);
189
+ } })) : (_jsx("span", { onClick: unlocked ? (event) => { event.stopPropagation(); setEditingValue(true); } : undefined, style: unlocked ? { borderBottom: '1px dotted currentColor', cursor: 'text' } : undefined, children: value })) })] }), deleteButton] }));
167
190
  }
168
191
  if (typeof value === 'boolean') {
169
- return (_jsxs("div", { className: "njv-line", style: { alignItems: 'center' }, children: [_jsxs("span", { className: "njv-code", style: { paddingLeft: depth * 20 }, children: [keyEl, _jsx("span", { className: "njv-boolean", onClick: unlocked ? (e) => { e.stopPropagation(); onUpdate(path, !value); } : undefined, title: unlocked ? 'Click to toggle' : undefined, style: unlocked ? { cursor: 'pointer', borderBottom: '1px dotted currentColor' } : undefined, children: String(value) })] }), del] }));
192
+ return (_jsxs("div", { className: "njv-line", style: { alignItems: 'center' }, children: [_jsxs("span", { className: "njv-code", style: { paddingLeft: depth * 20 }, children: [keyElement, _jsx("span", { className: "njv-boolean", onClick: unlocked ? (event) => { event.stopPropagation(); onUpdate(path, !value); } : undefined, style: unlocked ? { borderBottom: '1px dotted currentColor', cursor: 'pointer' } : undefined, title: unlocked ? 'Click to toggle' : undefined, children: String(value) })] }), deleteButton] }));
170
193
  }
171
194
  if (Array.isArray(value)) {
172
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "njv-line", style: { alignItems: 'center' }, children: [_jsxs("span", { className: "njv-code", style: { paddingLeft: depth * 20, cursor: 'pointer', userSelect: 'none' }, onClick: () => setOpen((o) => !o), children: [_jsx("span", { className: "njv-arrow", children: open ? '▼' : '▶' }), keyEl, _jsx("span", { className: "njv-brace", children: "[" }), !open && (_jsxs(_Fragment, { children: [_jsxs("span", { className: "njv-summary", children: [" ", value.length, " items "] }), _jsx("span", { className: "njv-brace", children: "]" })] }))] }), del] }), open && (_jsxs(_Fragment, { children: [value.map((item, i) => (_jsx(JsonNode, { value: item, depth: depth + 1, forceOpen: forceOpen, path: [...path, i], unlocked: unlocked, callbacks: callbacks }, i))), _jsxs("div", { className: "njv-line", style: { alignItems: 'center' }, children: [_jsx("span", { className: "njv-code", style: { paddingLeft: (depth + 1) * 20 }, children: _jsx("span", { className: "njv-brace", children: "]" }) }), unlocked && (_jsx(ActionBtn, { label: "+ item", color: "#22c55e", onClick: (e) => { e.stopPropagation(); onAddChild(path); } }))] })] }))] }));
195
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "njv-line", style: { alignItems: 'center' }, children: [_jsxs("span", { className: "njv-code", onClick: () => setOpen((current) => !current), style: { cursor: 'pointer', paddingLeft: depth * 20, userSelect: 'none' }, children: [_jsx("span", { className: "njv-arrow", children: open ? '▼' : '▶' }), keyElement, _jsx("span", { className: "njv-brace", children: "[" }), !open ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "njv-summary", children: [" ", value.length, " items "] }), _jsx("span", { className: "njv-brace", children: "]" })] })) : null] }), deleteButton] }), open ? (_jsxs(_Fragment, { children: [value.map((item, index) => (_jsx(JsonNode, { callbacks: callbacks, depth: depth + 1, forceOpen: forceOpen, path: [...path, index], unlocked: unlocked, value: item }, index))), _jsxs("div", { className: "njv-line", style: { alignItems: 'center' }, children: [_jsx("span", { className: "njv-code", style: { paddingLeft: (depth + 1) * 20 }, children: _jsx("span", { className: "njv-brace", children: "]" }) }), unlocked ? (_jsx(ActionButton, { color: "#22c55e", label: "+ item", onClick: (event) => {
196
+ event.stopPropagation();
197
+ onAddChild(path);
198
+ } })) : null] })] })) : null] }));
173
199
  }
174
200
  if (typeof value === 'object') {
175
201
  const entries = Object.entries(value);
176
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "njv-line", style: { alignItems: 'center' }, children: [_jsxs("span", { className: "njv-code", style: { paddingLeft: depth * 20, cursor: 'pointer', userSelect: 'none' }, onClick: () => setOpen((o) => !o), children: [_jsx("span", { className: "njv-arrow", children: open ? '▼' : '▶' }), keyEl, _jsx("span", { className: "njv-brace", children: '{' }), !open && (_jsxs(_Fragment, { children: [_jsxs("span", { className: "njv-summary", children: [" ", entries.length, " keys "] }), _jsx("span", { className: "njv-brace", children: '}' })] }))] }), del] }), open && (_jsxs(_Fragment, { children: [entries.map(([ek, ev]) => (_jsx(JsonNode, { k: ek, value: ev, depth: depth + 1, forceOpen: forceOpen, path: [...path, ek], unlocked: unlocked, callbacks: callbacks }, ek))), _jsxs("div", { className: "njv-line", style: { alignItems: 'center' }, children: [_jsx("span", { className: "njv-code", style: { paddingLeft: (depth + 1) * 20 }, children: _jsx("span", { className: "njv-brace", children: '}' }) }), unlocked && (_jsx(ActionBtn, { label: "+ key", color: "#22c55e", onClick: (e) => { e.stopPropagation(); onAddChild(path); } }))] })] }))] }));
202
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "njv-line", style: { alignItems: 'center' }, children: [_jsxs("span", { className: "njv-code", onClick: () => setOpen((current) => !current), style: { cursor: 'pointer', paddingLeft: depth * 20, userSelect: 'none' }, children: [_jsx("span", { className: "njv-arrow", children: open ? '▼' : '▶' }), keyElement, _jsx("span", { className: "njv-brace", children: '{' }), !open ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "njv-summary", children: [" ", entries.length, " keys "] }), _jsx("span", { className: "njv-brace", children: '}' })] })) : null] }), deleteButton] }), open ? (_jsxs(_Fragment, { children: [entries.map(([entryKey, entryValue]) => (_jsx(JsonNode, { callbacks: callbacks, depth: depth + 1, forceOpen: forceOpen, k: entryKey, path: [...path, entryKey], unlocked: unlocked, value: entryValue }, entryKey))), _jsxs("div", { className: "njv-line", style: { alignItems: 'center' }, children: [_jsx("span", { className: "njv-code", style: { paddingLeft: (depth + 1) * 20 }, children: _jsx("span", { className: "njv-brace", children: '}' }) }), unlocked ? (_jsx(ActionButton, { color: "#22c55e", label: "+ key", onClick: (event) => {
203
+ event.stopPropagation();
204
+ onAddChild(path);
205
+ } })) : null] })] })) : null] }));
177
206
  }
178
207
  return null;
179
208
  }
@@ -281,73 +310,122 @@ function ensureStyles() {
281
310
  `;
282
311
  document.head.appendChild(style);
283
312
  }
284
- const NitrogenDataViewerRuntime = ({ path, field }) => {
285
- const { value, setValue } = useField({ path });
313
+ const NitrogenDataViewerRuntime = ({ description, initialValue, label, path }) => {
314
+ const field = path ? useField({ path }) : null;
315
+ const value = field?.value ?? initialValue;
316
+ const setValue = field?.setValue;
317
+ const [collapsed, setCollapsed] = useState(true);
286
318
  const [copied, setCopied] = useState(false);
287
- const [pasted, setPasted] = useState(false);
288
319
  const [forceOpen, setForceOpen] = useState(null);
320
+ const [manualPasteValue, setManualPasteValue] = useState('');
321
+ const [pasteError, setPasteError] = useState('');
322
+ const [pasted, setPasted] = useState(false);
323
+ const [showPastePanel, setShowPastePanel] = useState(false);
289
324
  const [treeKey, setTreeKey] = useState(0);
290
325
  const [unlocked, setUnlocked] = useState(false);
291
326
  ensureStyles();
292
- let parsed = [];
293
- try {
294
- parsed = typeof value === 'string' ? JSON.parse(value) : value;
295
- }
296
- catch {
297
- parsed = [];
298
- }
299
- const jsonString = value != null ? JSON.stringify(value, null, 2) : '[]';
327
+ const parsed = useMemo(() => parseJsonValue(value), [value]);
328
+ const jsonString = useMemo(() => coerceDisplayValue(value), [value]);
300
329
  const moduleCount = Array.isArray(parsed) ? parsed.length : null;
330
+ const applyValue = useCallback((nextValue) => {
331
+ if (!setValue)
332
+ return;
333
+ setValue(nextValue);
334
+ }, [setValue]);
335
+ const callbacks = useMemo(() => ({
336
+ onAddChild: (targetPath) => {
337
+ applyValue(addChildAtPath(parsed, targetPath));
338
+ },
339
+ onDelete: (targetPath) => {
340
+ applyValue(deleteAtPath(parsed, targetPath));
341
+ },
342
+ onRenameKey: (targetPath, newKey) => {
343
+ applyValue(renameKeyAtPath(parsed, targetPath, newKey));
344
+ },
345
+ onUpdate: (targetPath, nextValue) => {
346
+ applyValue(updateAtPath(parsed, targetPath, nextValue));
347
+ },
348
+ }), [applyValue, parsed]);
301
349
  const handleCopy = useCallback(() => {
302
350
  navigator.clipboard.writeText(jsonString).then(() => {
303
351
  setCopied(true);
304
352
  setTimeout(() => setCopied(false), 2000);
305
353
  });
306
354
  }, [jsonString]);
307
- const handlePaste = useCallback(() => {
355
+ const applyPastedJson = useCallback((rawValue) => {
356
+ const trimmed = rawValue.trim();
357
+ if (!trimmed) {
358
+ setPasteError('Paste some JSON first.');
359
+ return;
360
+ }
361
+ try {
362
+ const parsedValue = JSON.parse(trimmed);
363
+ applyValue(parsedValue);
364
+ setManualPasteValue('');
365
+ setPasteError('');
366
+ setPasted(true);
367
+ setShowPastePanel(false);
368
+ setTreeKey((current) => current + 1);
369
+ setTimeout(() => setPasted(false), 2000);
370
+ }
371
+ catch {
372
+ setPasteError('That is not valid JSON.');
373
+ }
374
+ }, [applyValue]);
375
+ const handlePasteFromClipboard = useCallback(() => {
308
376
  navigator.clipboard.readText().then((text) => {
309
- try {
310
- const parsedValue = JSON.parse(text);
311
- setValue(parsedValue);
312
- setPasted(true);
313
- setTimeout(() => setPasted(false), 2000);
314
- }
315
- catch {
316
- // ignore invalid JSON
317
- }
377
+ applyPastedJson(text);
378
+ }, () => {
379
+ setPasteError('Clipboard access was blocked. Paste manually below.');
380
+ setShowPastePanel(true);
318
381
  });
319
- }, [setValue]);
382
+ }, [applyPastedJson]);
320
383
  const expandAll = useCallback(() => {
321
384
  setForceOpen(true);
322
- setTreeKey((k) => k + 1);
385
+ setTreeKey((current) => current + 1);
323
386
  }, []);
324
387
  const collapseAll = useCallback(() => {
325
388
  setForceOpen(false);
326
- setTreeKey((k) => k + 1);
389
+ setTreeKey((current) => current + 1);
327
390
  }, []);
328
- const callbacks = {
329
- onUpdate: (p, v) => setValue(updateAtPath(parsed, p, v)),
330
- onDelete: (p) => setValue(deleteAtPath(parsed, p)),
331
- onRenameKey: (p, newKey) => setValue(renameKeyAtPath(parsed, p, newKey)),
332
- onAddChild: (p) => setValue(addChildAtPath(parsed, p)),
333
- };
334
- const header = (_jsxs("span", { children: [typeof field.label === 'string' ? field.label : field.name, moduleCount !== null && (_jsxs("span", { style: { opacity: 0.5, fontWeight: 400, marginLeft: 8 }, children: ["(", moduleCount, " item", moduleCount !== 1 ? 's' : '', ")"] }))] }));
335
- return (_jsxs("div", { className: "njv-root field-type", style: { marginBottom: '1.5rem' }, children: [_jsx(Collapsible, { header: header, initCollapsed: true, children: _jsxs("div", { style: { border: '1px solid var(--njv-border)', borderRadius: '0 0 4px 4px', overflow: 'hidden' }, children: [_jsxs("div", { className: "njv-toolbar", children: [_jsx("button", { type: "button", className: "btn btn--size-small btn--style-secondary", onClick: handleCopy, children: copied ? '✓ Copied' : 'Copy' }), unlocked && (_jsx("button", { type: "button", className: "btn btn--size-small btn--style-secondary", onClick: handlePaste, children: pasted ? '✓ Pasted' : 'Paste' })), _jsx("button", { type: "button", className: "btn btn--size-small btn--style-secondary", onClick: expandAll, children: "Expand All" }), _jsx("button", { type: "button", className: "btn btn--size-small btn--style-secondary", onClick: collapseAll, children: "Collapse All" }), _jsx("button", { type: "button", onClick: () => setUnlocked((u) => !u), style: {
336
- marginBlock: '12px',
337
- marginLeft: 'auto',
338
- padding: '4px 12px',
339
- fontSize: '12px',
340
- background: unlocked ? '#ef4444' : '#6b7280',
341
- color: '#fff',
342
- border: 'none',
343
- borderRadius: '4px',
344
- cursor: 'pointer',
345
- }, children: unlocked ? 'Lock' : 'Unlock Edit' })] }), _jsx("div", { className: "njv-body", style: {
346
- maxHeight: '50vh',
347
- overflow: 'auto',
348
- fontFamily: 'var(--font-mono, "SF Mono", Menlo, Consolas, monospace)',
349
- fontSize: 'var(--font-body-size, 13px)',
350
- backgroundColor: 'var(--njv-bg)',
351
- }, children: _jsx(JsonNode, { value: parsed ?? [], depth: 0, forceOpen: forceOpen, path: [], unlocked: unlocked, callbacks: callbacks }, treeKey) })] }) }), typeof field.admin?.description === 'string' && (_jsx("p", { style: { fontSize: '12px', color: '#888', marginTop: '4px' }, children: field.admin.description }))] }));
391
+ const header = (_jsxs("span", { children: [label, moduleCount !== null ? (_jsxs("span", { style: { fontWeight: 400, marginLeft: 8, opacity: 0.5 }, children: ["(", moduleCount, " item", moduleCount !== 1 ? 's' : '', ")"] })) : null] }));
392
+ return (_jsxs("div", { className: "njv-root field-type", style: { marginBottom: '1.5rem' }, children: [_jsxs("div", { style: { border: '1px solid var(--njv-border)', borderRadius: 4, overflow: 'hidden' }, children: [_jsxs("button", { onClick: () => setCollapsed((current) => !current), style: {
393
+ alignItems: 'center',
394
+ background: 'var(--theme-elevation-50, #f8f8f8)',
395
+ border: 'none',
396
+ borderBottom: collapsed ? 'none' : '1px solid var(--njv-border)',
397
+ cursor: 'pointer',
398
+ display: 'flex',
399
+ justifyContent: 'space-between',
400
+ padding: '12px 16px',
401
+ width: '100%',
402
+ }, type: "button", children: [header, _jsx("span", { style: { fontSize: 12, opacity: 0.65 }, children: collapsed ? 'Show' : 'Hide' })] }), !collapsed ? (_jsxs("div", { children: [_jsxs("div", { className: "njv-toolbar", children: [_jsx("button", { className: "btn btn--size-small btn--style-secondary", onClick: handleCopy, type: "button", children: copied ? '✓ Copied' : 'Copy' }), _jsx("button", { className: "btn btn--size-small btn--style-secondary", onClick: () => setUnlocked((current) => !current), type: "button", children: unlocked ? 'Lock' : 'Unlock' }), unlocked ? (_jsxs(_Fragment, { children: [_jsx("button", { className: "btn btn--size-small btn--style-secondary", onClick: handlePasteFromClipboard, type: "button", children: pasted ? '✓ Pasted' : 'Paste' }), _jsx("button", { className: "btn btn--size-small btn--style-secondary", onClick: () => {
403
+ setShowPastePanel((current) => !current);
404
+ setPasteError('');
405
+ }, type: "button", children: showPastePanel ? 'Hide Paste Box' : 'Paste Manually' })] })) : null, _jsx("button", { className: "btn btn--size-small btn--style-secondary", onClick: expandAll, type: "button", children: "Expand All" }), _jsx("button", { className: "btn btn--size-small btn--style-secondary", onClick: collapseAll, type: "button", children: "Collapse All" })] }), unlocked && showPastePanel ? (_jsxs("div", { style: {
406
+ background: 'var(--theme-elevation-50, #f8f8f8)',
407
+ borderBottom: '1px solid var(--njv-border)',
408
+ display: 'grid',
409
+ gap: 8,
410
+ padding: 12,
411
+ }, children: [_jsx("textarea", { onChange: (event) => setManualPasteValue(event.target.value), placeholder: "Paste Nitrogen JSON here...", style: {
412
+ border: '1px solid var(--theme-elevation-150, #ddd)',
413
+ borderRadius: 4,
414
+ fontFamily: 'var(--font-mono, \"SF Mono\", Menlo, Consolas, monospace)',
415
+ fontSize: 12,
416
+ minHeight: 160,
417
+ padding: 12,
418
+ resize: 'vertical',
419
+ width: '100%',
420
+ }, value: manualPasteValue }), _jsxs("div", { style: { display: 'flex', flexWrap: 'wrap', gap: 8 }, children: [_jsx("button", { className: "btn btn--size-small btn--style-primary", onClick: () => applyPastedJson(manualPasteValue), type: "button", children: "Apply Pasted JSON" }), _jsx("button", { className: "btn btn--size-small btn--style-secondary", onClick: () => {
421
+ setManualPasteValue('');
422
+ setPasteError('');
423
+ }, type: "button", children: "Clear" })] }), pasteError ? (_jsx("p", { style: { color: 'var(--theme-error-500, #dc2626)', fontSize: 12, margin: 0 }, children: pasteError })) : null] })) : null, _jsx("div", { className: "njv-body", style: {
424
+ backgroundColor: 'var(--njv-bg)',
425
+ fontFamily: 'var(--font-mono, "SF Mono", Menlo, Consolas, monospace)',
426
+ fontSize: 'var(--font-body-size, 13px)',
427
+ maxHeight: '50vh',
428
+ overflow: 'auto',
429
+ }, children: _jsx(JsonNode, { callbacks: callbacks, depth: 0, forceOpen: forceOpen, path: [], unlocked: unlocked, value: parsed }, treeKey) })] })) : null] }), typeof description === 'string' ? (_jsx("p", { style: { color: '#888', fontSize: 12, marginTop: 4 }, children: description })) : null] }));
352
430
  };
353
431
  export default NitrogenDataViewerRuntime;
@@ -0,0 +1 @@
1
+ export default function NitrogenNavGroup(): import("react/jsx-runtime").JSX.Element;