@nuasite/collections-admin 0.43.0-beta.3 → 0.43.0-beta.4

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.
@@ -0,0 +1,240 @@
1
+ /**
2
+ * collections-admin SPA — read-only milestone (cms-headless F3.1).
3
+ *
4
+ * Host-agnostic: driven only by an `apiBase` prop, with internal view-state
5
+ * navigation (list → entries → detail) via React state — never the host router.
6
+ * That keeps the same component usable as a webmaster tab today and at
7
+ * `/_nua/admin` for local dev later (F7).
8
+ *
9
+ * Read-only: browse collections, list entries (sparse projection + cursor
10
+ * pagination), and view a single entry's frontmatter + markdown body. Mutations
11
+ * (editor/media/conflict) arrive in F3.2.
12
+ */
13
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
14
+ import { CmsClientError, createClient } from './client';
15
+ import { FieldRow } from './field-view';
16
+ import './styles.css';
17
+ function useAsync(load, deps) {
18
+ const [state, setState] = useState({ data: null, error: null, loading: true });
19
+ const [nonce, setNonce] = useState(0);
20
+ const loadRef = useRef(load);
21
+ loadRef.current = load;
22
+ // `load` is read through a ref; re-runs are driven by the explicit `deps` and
23
+ // the reload `nonce` so the effect deps stay stable and lint-clean.
24
+ const effectDeps = [...deps, nonce];
25
+ useEffect(() => {
26
+ let active = true;
27
+ setState({ data: null, error: null, loading: true });
28
+ loadRef.current().then((data) => {
29
+ if (active)
30
+ setState({ data, error: null, loading: false });
31
+ }, (error) => {
32
+ if (active)
33
+ setState({ data: null, error: error instanceof Error ? error : new Error(String(error)), loading: false });
34
+ });
35
+ return () => {
36
+ active = false;
37
+ };
38
+ }, effectDeps);
39
+ const reload = useCallback(() => setNonce((n) => n + 1), []);
40
+ return { ...state, reload };
41
+ }
42
+ // ============================================================================
43
+ // Presentational primitives
44
+ // ============================================================================
45
+ function Spinner({ label }) {
46
+ return (<div className="nua-cadmin-state">
47
+ <div className="nua-cadmin-spinner"/>
48
+ <div>{label}</div>
49
+ </div>);
50
+ }
51
+ function ErrorState({ error, onRetry }) {
52
+ const title = error instanceof CmsClientError && error.isUnauthorized
53
+ ? 'Session expired'
54
+ : error instanceof CmsClientError && error.isForbidden
55
+ ? 'No access'
56
+ : 'Something went wrong';
57
+ return (<div className="nua-cadmin-error">
58
+ <div className="nua-cadmin-error-title">{title}</div>
59
+ <div>{error.message}</div>
60
+ {onRetry ? <button type="button" className="nua-cadmin-retry" onClick={onRetry}>Try again</button> : null}
61
+ </div>);
62
+ }
63
+ function EmptyState({ label }) {
64
+ return <div className="nua-cadmin-state">{label}</div>;
65
+ }
66
+ // ============================================================================
67
+ // Collection list
68
+ // ============================================================================
69
+ function CollectionList({ client, onOpen }) {
70
+ const { data, error, loading, reload } = useAsync(() => client.getCollections(), [client]);
71
+ if (loading)
72
+ return <Spinner label="Loading collections…"/>;
73
+ if (error)
74
+ return <ErrorState error={error} onRetry={reload}/>;
75
+ if (!data || data.length === 0)
76
+ return <EmptyState label="No collections found in this project."/>;
77
+ return (<div className="nua-cadmin-list">
78
+ {data.map((collection) => (<button key={collection.name} type="button" className="nua-cadmin-card" onClick={() => onOpen(collection.name)}>
79
+ <span className="nua-cadmin-card-main">
80
+ <span className="nua-cadmin-card-label">{collection.label || collection.name}</span>
81
+ <span className="nua-cadmin-card-sub">
82
+ {collection.name}
83
+ {collection.type ? ` · ${collection.type}` : ''}
84
+ {` · ${collection.fileExtension}`}
85
+ </span>
86
+ </span>
87
+ <span className="nua-cadmin-badge">{collection.entryCount} {collection.entryCount === 1 ? 'entry' : 'entries'}</span>
88
+ </button>))}
89
+ </div>);
90
+ }
91
+ // ============================================================================
92
+ // Entries table (sparse projection + cursor pagination)
93
+ // ============================================================================
94
+ const ENTRIES_PAGE_SIZE = 50;
95
+ const ENTRIES_FIELDS = 'slug,title,draft,pathname';
96
+ function EntriesTable({ client, collection, onOpen }) {
97
+ const [rows, setRows] = useState([]);
98
+ const [cursor, setCursor] = useState(undefined);
99
+ const [hasMore, setHasMore] = useState(false);
100
+ const [error, setError] = useState(null);
101
+ const [loading, setLoading] = useState(true);
102
+ const [loadingMore, setLoadingMore] = useState(false);
103
+ const loadPage = useCallback(async (nextCursor, append) => {
104
+ if (append)
105
+ setLoadingMore(true);
106
+ else
107
+ setLoading(true);
108
+ setError(null);
109
+ try {
110
+ const result = await client.getEntries(collection, {
111
+ fields: ENTRIES_FIELDS,
112
+ draft: 'all',
113
+ limit: ENTRIES_PAGE_SIZE,
114
+ cursor: nextCursor,
115
+ });
116
+ setRows((prev) => (append ? [...prev, ...result.entries] : result.entries));
117
+ setCursor(result.cursor);
118
+ setHasMore(result.hasMore);
119
+ }
120
+ catch (e) {
121
+ setError(e instanceof Error ? e : new Error(String(e)));
122
+ }
123
+ finally {
124
+ setLoading(false);
125
+ setLoadingMore(false);
126
+ }
127
+ }, [client, collection]);
128
+ useEffect(() => {
129
+ setRows([]);
130
+ setCursor(undefined);
131
+ setHasMore(false);
132
+ void loadPage(undefined, false);
133
+ }, [loadPage]);
134
+ if (loading)
135
+ return <Spinner label="Loading entries…"/>;
136
+ if (error)
137
+ return <ErrorState error={error} onRetry={() => void loadPage(undefined, false)}/>;
138
+ if (rows.length === 0)
139
+ return <EmptyState label="This collection has no entries."/>;
140
+ return (<div>
141
+ <table className="nua-cadmin-table">
142
+ <thead>
143
+ <tr>
144
+ <th>Slug</th>
145
+ <th>Title</th>
146
+ <th>Draft</th>
147
+ <th>Pathname</th>
148
+ </tr>
149
+ </thead>
150
+ <tbody>
151
+ {rows.map((entry) => (<tr key={entry.slug} className="nua-cadmin-row" onClick={() => onOpen(entry.slug)}>
152
+ <td className="nua-cadmin-cell-mono">{entry.slug}</td>
153
+ <td>{entry.title ?? <span className="nua-cadmin-field-empty">—</span>}</td>
154
+ <td>{entry.draft ? <span className="nua-cadmin-badge nua-cadmin-badge-draft">draft</span> : ''}</td>
155
+ <td className="nua-cadmin-cell-mono">{entry.pathname ?? '—'}</td>
156
+ </tr>))}
157
+ </tbody>
158
+ </table>
159
+ {hasMore ? (<button type="button" className="nua-cadmin-load-more" disabled={loadingMore} onClick={() => void loadPage(cursor, true)}>
160
+ {loadingMore ? 'Loading…' : 'Load more'}
161
+ </button>) : null}
162
+ </div>);
163
+ }
164
+ // ============================================================================
165
+ // Entry detail
166
+ // ============================================================================
167
+ /**
168
+ * Order the collection's fields for display: `publish-toggle`/`publish-date`
169
+ * roles and `sidebar`/`header` positioned fields first, then the rest in schema
170
+ * order. Hidden fields are dropped.
171
+ */
172
+ function orderFields(fields) {
173
+ const visible = fields.filter((f) => !f.hidden);
174
+ const pinned = visible.filter((f) => f.role !== undefined || f.position !== undefined);
175
+ const rest = visible.filter((f) => f.role === undefined && f.position === undefined);
176
+ return [...pinned, ...rest];
177
+ }
178
+ function EntryDetail({ client, collections, collection, slug }) {
179
+ const { data, error, loading, reload } = useAsync(() => client.getEntry(collection, slug), [client, collection, slug]);
180
+ const def = useMemo(() => collections.find((c) => c.name === collection), [collections, collection]);
181
+ if (loading)
182
+ return <Spinner label="Loading entry…"/>;
183
+ if (error)
184
+ return <ErrorState error={error} onRetry={reload}/>;
185
+ if (!data)
186
+ return <EmptyState label="Entry not found."/>;
187
+ const fieldDefs = def ? orderFields(def.fields) : [];
188
+ const renderedNames = new Set(fieldDefs.map((f) => f.name));
189
+ // Frontmatter keys present on the entry but absent from the inferred schema.
190
+ const extraKeys = Object.keys(data.frontmatter).filter((k) => !renderedNames.has(k));
191
+ return (<div>
192
+ <div className="nua-cadmin-fields">
193
+ {fieldDefs.length === 0 && extraKeys.length === 0 ? <EmptyState label="No frontmatter fields."/> : null}
194
+ {fieldDefs.map((field) => (<FieldRow key={field.name} field={field} raw={data.frontmatter[field.name]?.value}/>))}
195
+ {extraKeys.map((key) => (<FieldRow key={key} field={{ name: key, type: 'text', required: false }} raw={data.frontmatter[key]?.value}/>))}
196
+ </div>
197
+
198
+ <h3 className="nua-cadmin-section-title">Body</h3>
199
+ {data.body.trim() === ''
200
+ ? <EmptyState label="This entry has no markdown body."/>
201
+ : <pre className="nua-cadmin-body-content">{data.body}</pre>}
202
+ </div>);
203
+ }
204
+ export function CollectionsAdminApp({ apiBase, onClose }) {
205
+ const client = useMemo(() => createClient(apiBase), [apiBase]);
206
+ const [state, setState] = useState({ view: 'list' });
207
+ // The collection definitions are needed by the detail view to drive field
208
+ // rendering; load them once at the root and pass down.
209
+ const collectionsState = useAsync(() => client.getCollections(), [client]);
210
+ const collections = collectionsState.data ?? [];
211
+ const goList = useCallback(() => setState({ view: 'list' }), []);
212
+ const goEntries = useCallback((collection) => setState({ view: 'entries', collection }), []);
213
+ const goDetail = useCallback((collection, slug) => setState({ view: 'detail', collection, slug }), []);
214
+ const activeCollection = state.view !== 'list'
215
+ ? collections.find((c) => c.name === state.collection)
216
+ : undefined;
217
+ const collectionLabel = activeCollection ? (activeCollection.label || activeCollection.name) : (state.view !== 'list' ? state.collection : '');
218
+ return (<div className="nua-cadmin">
219
+ <div className="nua-cadmin-header">
220
+ {state.view === 'entries' ? (<button type="button" className="nua-cadmin-back" onClick={goList}>← Collections</button>) : null}
221
+ {state.view === 'detail' ? (<button type="button" className="nua-cadmin-back" onClick={() => goEntries(state.collection)}>← {collectionLabel}</button>) : null}
222
+
223
+ {state.view === 'list' ? <h2 className="nua-cadmin-title">Collections</h2> : null}
224
+ {state.view === 'entries' ? <h2 className="nua-cadmin-title">{collectionLabel}</h2> : null}
225
+ {state.view === 'detail' ? (<h2 className="nua-cadmin-title">
226
+ {collectionLabel}
227
+ <span className="nua-cadmin-crumb"> / {state.slug}</span>
228
+ </h2>) : null}
229
+
230
+ <span className="nua-cadmin-spacer"/>
231
+ {onClose ? <button type="button" className="nua-cadmin-close" aria-label="Close" onClick={onClose}>×</button> : null}
232
+ </div>
233
+
234
+ <div className="nua-cadmin-body">
235
+ {state.view === 'list' ? <CollectionList client={client} onOpen={goEntries}/> : null}
236
+ {state.view === 'entries' ? <EntriesTable client={client} collection={state.collection} onOpen={(slug) => goDetail(state.collection, slug)}/> : null}
237
+ {state.view === 'detail' ? <EntryDetail client={client} collections={collections} collection={state.collection} slug={state.slug}/> : null}
238
+ </div>
239
+ </div>);
240
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Typed read-only client over the cms-sidecar `/cms/v1` HTTP contract.
3
+ *
4
+ * The host (webmaster BFF, or a local dev proxy in F7) mounts the sidecar under
5
+ * an `apiBase` and adds the `/cms/v1` prefix itself — so this client requests
6
+ * `${apiBase}/project`, `${apiBase}/collections`, etc. (never `/cms/v1/...`).
7
+ *
8
+ * The structural model (collections/entries/fields) is reused 1:1 from
9
+ * `@nuasite/cms-types`. The thin HTTP envelope (project model, sparse entries
10
+ * list, error codes) mirrors the sidecar's wire types; it is declared here
11
+ * because those types are not part of the `@nuasite/cms-types` contract surface.
12
+ */
13
+ // ============================================================================
14
+ // Client error
15
+ // ============================================================================
16
+ /**
17
+ * Thrown for any non-2xx response. Carries the parsed sidecar error code so the
18
+ * UI can distinguish auth failures (`unauthorized`/`forbidden`) from a missing
19
+ * collection/entry (`not_found`) or a generic failure.
20
+ */
21
+ export class CmsClientError extends Error {
22
+ status;
23
+ code;
24
+ constructor(status, code, message) {
25
+ super(message);
26
+ this.status = status;
27
+ this.code = code;
28
+ this.name = 'CmsClientError';
29
+ }
30
+ /** Session cookie missing/expired upstream — the user must re-authenticate. */
31
+ get isUnauthorized() {
32
+ return this.code === 'unauthorized' || this.status === 401;
33
+ }
34
+ /** Authenticated but lacks access to this project. */
35
+ get isForbidden() {
36
+ return this.code === 'forbidden' || this.status === 403;
37
+ }
38
+ get isNotFound() {
39
+ return this.code === 'not_found' || this.status === 404;
40
+ }
41
+ }
42
+ // ============================================================================
43
+ // Client
44
+ // ============================================================================
45
+ function isApiError(value) {
46
+ return typeof value === 'object'
47
+ && value !== null
48
+ && 'error' in value
49
+ && typeof value.error === 'string'
50
+ && 'code' in value
51
+ && typeof value.code === 'string';
52
+ }
53
+ const KNOWN_ERROR_CODES = [
54
+ 'not_found',
55
+ 'conflict',
56
+ 'validation',
57
+ 'parse_error',
58
+ 'io_error',
59
+ 'unsupported',
60
+ 'unauthorized',
61
+ ];
62
+ function isErrorCode(value) {
63
+ return KNOWN_ERROR_CODES.includes(value);
64
+ }
65
+ export function createClient(apiBase) {
66
+ // Normalise: drop a trailing slash so `${base}${path}` joins cleanly.
67
+ const base = apiBase.endsWith('/') ? apiBase.slice(0, -1) : apiBase;
68
+ async function request(path) {
69
+ const response = await fetch(`${base}${path}`, {
70
+ method: 'GET',
71
+ credentials: 'include',
72
+ headers: { accept: 'application/json' },
73
+ });
74
+ if (!response.ok) {
75
+ throw await toError(response);
76
+ }
77
+ // Successful responses are always JSON in the read-only surface.
78
+ const value = await response.json();
79
+ return value;
80
+ }
81
+ async function toError(response) {
82
+ // 403 is produced by the BFF (project scope), not the sidecar, so it has no
83
+ // sidecar `code`; surface it as a distinct `forbidden`.
84
+ if (response.status === 403) {
85
+ const message = await readErrorMessage(response, 'You do not have access to this project.');
86
+ return new CmsClientError(403, 'forbidden', message);
87
+ }
88
+ const body = await response.json().catch(() => null);
89
+ if (isApiError(body) && isErrorCode(body.code)) {
90
+ return new CmsClientError(response.status, body.code, body.error);
91
+ }
92
+ if (response.status === 401) {
93
+ return new CmsClientError(401, 'unauthorized', 'Your session has expired. Please reload.');
94
+ }
95
+ return new CmsClientError(response.status, 'unknown', `Request failed (${response.status})`);
96
+ }
97
+ async function readErrorMessage(response, fallback) {
98
+ const body = await response.json().catch(() => null);
99
+ if (isApiError(body))
100
+ return body.error;
101
+ if (typeof body === 'object' && body !== null && 'error' in body) {
102
+ const err = body.error;
103
+ if (typeof err === 'object' && err !== null && 'message' in err && typeof err.message === 'string') {
104
+ return err.message;
105
+ }
106
+ }
107
+ return fallback;
108
+ }
109
+ return {
110
+ getProject() {
111
+ return request('/project');
112
+ },
113
+ getCollections() {
114
+ return request('/collections');
115
+ },
116
+ getEntries(collection, options = {}) {
117
+ const params = new URLSearchParams();
118
+ if (options.fields !== undefined)
119
+ params.set('fields', options.fields);
120
+ if (options.draft !== undefined)
121
+ params.set('draft', options.draft);
122
+ if (options.cursor !== undefined)
123
+ params.set('cursor', options.cursor);
124
+ if (options.limit !== undefined)
125
+ params.set('limit', String(options.limit));
126
+ const query = params.toString();
127
+ const suffix = query === '' ? '' : `?${query}`;
128
+ return request(`/collections/${encodeURIComponent(collection)}/entries${suffix}`);
129
+ },
130
+ getEntry(collection, slug) {
131
+ return request(`/collections/${encodeURIComponent(collection)}/entries/${encodeURIComponent(slug)}`);
132
+ },
133
+ };
134
+ }
@@ -7,7 +7,7 @@
7
7
  * native draft model as the editor.
8
8
  */
9
9
  import type { CollectionDefinition } from '@nuasite/cms-types';
10
- import { type CmsClient } from './client';
10
+ import { type CmsClient } from '@nuasite/cms-client';
11
11
  export declare function EntryCreate({ client, definition, collection, onCreated, onCancel }: {
12
12
  client: CmsClient;
13
13
  definition: CollectionDefinition | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"entry-create.d.ts","sourceRoot":"","sources":["../../src/entry-create.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAmB,MAAM,oBAAoB,CAAA;AAE/E,OAAO,EAAE,KAAK,SAAS,EAAkB,MAAM,UAAU,CAAA;AAQzD,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE;IACpF,MAAM,EAAE,SAAS,CAAA;IACjB,UAAU,EAAE,oBAAoB,GAAG,SAAS,CAAA;IAC5C,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,QAAQ,EAAE,MAAM,IAAI,CAAA;CACpB,+BA0FA"}
1
+ {"version":3,"file":"entry-create.d.ts","sourceRoot":"","sources":["../../src/entry-create.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAmB,MAAM,oBAAoB,CAAA;AAE/E,OAAO,EAAE,KAAK,SAAS,EAAkE,MAAM,qBAAqB,CAAA;AAOpH,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE;IACpF,MAAM,EAAE,SAAS,CAAA;IACjB,UAAU,EAAE,oBAAoB,GAAG,SAAS,CAAA;IAC5C,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,QAAQ,EAAE,MAAM,IAAI,CAAA;CACpB,+BA0FA"}
@@ -18,7 +18,7 @@
18
18
  * + `serverHash`; "use ours" re-PATCHes with `baseHash = serverHash` (force-over).
19
19
  */
20
20
  import type { CollectionDefinition } from '@nuasite/cms-types';
21
- import type { CmsClient } from './client';
21
+ import { type CmsClient } from '@nuasite/cms-client';
22
22
  export declare function EntryEditor({ client, definition, collection, slug, onDeleted, onRenamed }: {
23
23
  client: CmsClient;
24
24
  definition: CollectionDefinition | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"entry-editor.d.ts","sourceRoot":"","sources":["../../src/entry-editor.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAmB,MAAM,oBAAoB,CAAA;AAE/E,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,UAAU,CAAA;AAgHtD,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE;IAC3F,MAAM,EAAE,SAAS,CAAA;IACjB,UAAU,EAAE,oBAAoB,GAAG,SAAS,CAAA;IAC5C,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC,+BA4MA"}
1
+ {"version":3,"file":"entry-editor.d.ts","sourceRoot":"","sources":["../../src/entry-editor.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAmB,MAAM,oBAAoB,CAAA;AAE/E,OAAO,EAAE,KAAK,SAAS,EAAgH,MAAM,qBAAqB,CAAA;AA8GlK,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE;IAC3F,MAAM,EAAE,SAAS,CAAA;IACjB,UAAU,EAAE,oBAAoB,GAAG,SAAS,CAAA;IAC5C,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC,+BA4MA"}
@@ -8,7 +8,7 @@
8
8
  * widgets reach the sidecar through the injected `EditorContext`.
9
9
  */
10
10
  import type { FieldDefinition } from '@nuasite/cms-types';
11
- import type { CmsClient } from './client';
11
+ import { type CmsClient } from '@nuasite/cms-client';
12
12
  /** Cross-cutting services a widget may need (media uploads, reference lookups). */
13
13
  export interface EditorContext {
14
14
  client: CmsClient;
@@ -1 +1 @@
1
- {"version":3,"file":"field-editor.d.ts","sourceRoot":"","sources":["../../src/field-editor.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAa,MAAM,oBAAoB,CAAA;AAEpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAIzC,mFAAmF;AACnF,MAAM,WAAW,aAAa;IAC7B,MAAM,EAAE,SAAS,CAAA;IACjB,4EAA4E;IAC5E,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;CACb;AAED,UAAU,gBAAgB;IACzB,KAAK,EAAE,eAAe,CAAA;IACtB,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAClC,GAAG,EAAE,aAAa,CAAA;CAClB;AAwQD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,gBAAgB,+BA2C5E"}
1
+ {"version":3,"file":"field-editor.d.ts","sourceRoot":"","sources":["../../src/field-editor.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAa,MAAM,oBAAoB,CAAA;AAEpE,OAAO,EAAE,KAAK,SAAS,EAAsF,MAAM,qBAAqB,CAAA;AAGxI,mFAAmF;AACnF,MAAM,WAAW,aAAa;IAC7B,MAAM,EAAE,SAAS,CAAA;IACjB,4EAA4E;IAC5E,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;CACb;AAED,UAAU,gBAAgB;IACzB,KAAK,EAAE,eAAe,CAAA;IACtB,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAClC,GAAG,EAAE,aAAa,CAAA;CAClB;AAwQD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,gBAAgB,+BA2C5E"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Read-only field rendering, driven by a collection's `FieldDefinition`.
3
+ *
4
+ * Scalar types (text/number/boolean/date/select/image/url/email/tel/color) show
5
+ * their value directly; structural types (array/object/reference) show their
6
+ * structure. Nothing here mutates — editing arrives in F3.2.
7
+ */
8
+ import type { FieldDefinition } from '@nuasite/cms-types';
9
+ export declare function FieldValueView({ field, raw }: {
10
+ field: FieldDefinition;
11
+ raw: string | undefined;
12
+ }): import("react").JSX.Element;
13
+ export declare function FieldRow({ field, raw }: {
14
+ field: FieldDefinition;
15
+ raw: string | undefined;
16
+ }): import("react").JSX.Element;
17
+ //# sourceMappingURL=field-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-view.d.ts","sourceRoot":"","sources":["../../src/field-view.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAa,MAAM,oBAAoB,CAAA;AAgDpE,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;IAAE,KAAK,EAAE,eAAe,CAAC;IAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,+BAmBjG;AAED,wBAAgB,QAAQ,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;IAAE,KAAK,EAAE,eAAe,CAAC;IAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,+BAU3F"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Read-only field rendering, driven by a collection's `FieldDefinition`.
3
+ *
4
+ * Scalar types (text/number/boolean/date/select/image/url/email/tel/color) show
5
+ * their value directly; structural types (array/object/reference) show their
6
+ * structure. Nothing here mutates — editing arrives in F3.2.
7
+ */
8
+ /**
9
+ * Frontmatter values arrive from the sidecar already stringified
10
+ * (`{ value: string; line: number }`). For structural types the string is a
11
+ * JSON payload; we parse it best-effort for a structured render and fall back to
12
+ * the raw string when it is not JSON.
13
+ */
14
+ function parseStructured(raw) {
15
+ const trimmed = raw.trim();
16
+ if (trimmed === '')
17
+ return undefined;
18
+ if (!(trimmed.startsWith('{') || trimmed.startsWith('[')))
19
+ return raw;
20
+ try {
21
+ return JSON.parse(trimmed);
22
+ }
23
+ catch {
24
+ return raw;
25
+ }
26
+ }
27
+ function StructuredValue({ raw }) {
28
+ const parsed = parseStructured(raw);
29
+ if (parsed === undefined) {
30
+ return <span className="nua-cadmin-field-empty">—</span>;
31
+ }
32
+ if (typeof parsed === 'string') {
33
+ return <span>{parsed}</span>;
34
+ }
35
+ return <pre className="nua-cadmin-field-structured">{JSON.stringify(parsed, null, 2)}</pre>;
36
+ }
37
+ function BooleanValue({ raw }) {
38
+ const on = raw === 'true' || raw === '1' || raw.toLowerCase() === 'yes';
39
+ return <span className={on ? 'nua-cadmin-bool-on' : 'nua-cadmin-bool-off'}>{on ? 'Yes' : 'No'}</span>;
40
+ }
41
+ function ImageValue({ raw }) {
42
+ if (raw === '')
43
+ return <span className="nua-cadmin-field-empty">—</span>;
44
+ const looksLikeUrl = /^(https?:\/\/|\/)/.test(raw);
45
+ return (<div>
46
+ {looksLikeUrl ? <img className="nua-cadmin-img" src={raw} alt=""/> : null}
47
+ <div className="nua-cadmin-cell-mono">{raw}</div>
48
+ </div>);
49
+ }
50
+ const STRUCTURAL_TYPES = new Set(['array', 'object', 'reference']);
51
+ export function FieldValueView({ field, raw }) {
52
+ if (raw === undefined || raw === '') {
53
+ return <div className="nua-cadmin-field-value nua-cadmin-field-empty">—</div>;
54
+ }
55
+ let content;
56
+ switch (field.type) {
57
+ case 'boolean':
58
+ content = <BooleanValue raw={raw}/>;
59
+ break;
60
+ case 'image':
61
+ case 'file':
62
+ content = <ImageValue raw={raw}/>;
63
+ break;
64
+ default:
65
+ content = STRUCTURAL_TYPES.has(field.type) ? <StructuredValue raw={raw}/> : <span>{raw}</span>;
66
+ }
67
+ return <div className="nua-cadmin-field-value">{content}</div>;
68
+ }
69
+ export function FieldRow({ field, raw }) {
70
+ return (<div className="nua-cadmin-field">
71
+ <div className="nua-cadmin-field-label">
72
+ <span>{field.name}</span>
73
+ <span className="nua-cadmin-field-type">{field.type}{field.required ? " · required" : ""}</span>
74
+ </div>
75
+ <FieldValueView field={field} raw={raw}/>
76
+ </div>);
77
+ }
@@ -7,6 +7,5 @@
7
7
  */
8
8
  export { CollectionsAdminApp, type CollectionsAdminAppProps } from './app';
9
9
  export { FIELD_TYPES, isFieldType } from '@nuasite/cms-types';
10
- export { type CmsApiError, type CmsCapabilities, type CmsClient, CmsClientError, type CmsConflict, type CmsEntriesListResult, type CmsErrorCode, type CmsPageEntry, type CmsProjectModel, createClient, type CreateEntryInput, type GetEntriesOptions, isMediaUnavailable, type MediaContext, type UpdateEntryInput, type UpdateEntryResult, } from './client';
11
- export type { EntryDraft } from './form-model';
10
+ export { type CmsApiError, type CmsCapabilities, type CmsClient, CmsClientError, type CmsConflict, type CmsEntriesListResult, type CmsErrorCode, type CmsPageEntry, type CmsProjectModel, createClient, type CreateEntryInput, type EntryDraft, type GetEntriesOptions, isMediaUnavailable, type MediaContext, type UpdateEntryInput, type UpdateEntryResult, } from '@nuasite/cms-client';
12
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,mBAAmB,EAAE,KAAK,wBAAwB,EAAE,MAAM,OAAO,CAAA;AAI1E,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAC7D,OAAO,EACN,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,cAAc,EACd,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,YAAY,EACZ,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,kBAAkB,EAClB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,GACtB,MAAM,UAAU,CAAA;AACjB,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,mBAAmB,EAAE,KAAK,wBAAwB,EAAE,MAAM,OAAO,CAAA;AAI1E,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAG7D,OAAO,EACN,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,cAAc,EACd,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,YAAY,EACZ,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,kBAAkB,EAClB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,GACtB,MAAM,qBAAqB,CAAA"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `@nuasite/collections-admin` — read-only collections SPA over the cms-sidecar
3
+ * `/cms/v1` HTTP contract (cms-headless F3.1). Host-agnostic: mount
4
+ * `<CollectionsAdminApp apiBase={…} />` and it drives its own internal view-state
5
+ * navigation. Self-contained styles ship at `./styles.css` (imported by the app).
6
+ */
7
+ export { CollectionsAdminApp } from './app';
8
+ export { CmsClientError, createClient, } from './client';
@@ -9,7 +9,7 @@
9
9
  * `listMedia`; on `unsupported`/`501` it disables upload and shows a hint while
10
10
  * keeping the manual URL field fully usable — the editor is never blocked on media.
11
11
  */
12
- import { type CmsClient } from './client';
12
+ import { type CmsClient } from '@nuasite/cms-client';
13
13
  interface MediaPickerProps {
14
14
  client: CmsClient;
15
15
  value: string;
@@ -1 +1 @@
1
- {"version":3,"file":"media-picker.d.ts","sourceRoot":"","sources":["../../src/media-picker.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,KAAK,SAAS,EAAsB,MAAM,UAAU,CAAA;AAE7D,UAAU,gBAAgB;IACzB,MAAM,EAAE,SAAS,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAC/B;AASD,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,gBAAgB,+BAiG1G"}
1
+ {"version":3,"file":"media-picker.d.ts","sourceRoot":"","sources":["../../src/media-picker.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,KAAK,SAAS,EAAsB,MAAM,qBAAqB,CAAA;AAExE,UAAU,gBAAgB;IACzB,MAAM,EAAE,SAAS,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAC/B;AASD,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,gBAAgB,+BAiG1G"}