@airoom/nextmin-react 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ import type { SchemaDef } from '../../lib/types';
2
+ type Props = {
3
+ schema?: SchemaDef;
4
+ data: Record<string, any>;
5
+ };
6
+ export declare function DynamicViewer({ schema, data }: Props): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,274 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React from 'react';
4
+ import { formatCell } from '../../views/list/formatters';
5
+ import { summarizeObject } from '../../views/list/jsonSummary';
6
+ // Hide technical keys in the viewer
7
+ const HIDDEN_KEYS = new Set(['id', '_id', 'v', '__v', 'baseId', 'exId', '__childId', 'createdAt', 'updatedAt', 'deletedAt']);
8
+ // Detect password-like keys generically (no value should be visible)
9
+ function normKey(k) {
10
+ return k.toLowerCase().replace(/[^a-z]/g, '');
11
+ }
12
+ function isPasswordKeyRaw(k) {
13
+ const n = normKey(k);
14
+ // Generic detection: match common variants and substrings
15
+ if (n === 'password' || n === 'pwd')
16
+ return true;
17
+ if (n.includes('password'))
18
+ return true; // e.g., userpassword, passwordhash
19
+ if (n.includes('passwd'))
20
+ return true; // passwd
21
+ if (n.includes('passcode'))
22
+ return true; // passcode
23
+ if (n.includes('secret'))
24
+ return true; // clientsecret, secretKey
25
+ return false;
26
+ }
27
+ function isPasswordByAttr(attr) {
28
+ const a = (Array.isArray(attr) ? attr[0] : attr);
29
+ const t = String(a?.type ?? '').toLowerCase();
30
+ const f = String(a?.format ?? '').toLowerCase();
31
+ return t === 'password' || f === 'password';
32
+ }
33
+ // --- small helpers (duplicated minimal logic from table) ---
34
+ const IMG_EXT_RE = /(png|jpe?g|gif|webp|bmp|svg|avif|heic|heif)$/i;
35
+ function isUrlLike(s) {
36
+ return /^https?:\/\//i.test(s) || s.startsWith('/') || s.startsWith('data:');
37
+ }
38
+ function isLikelyImageUrl(s) {
39
+ if (!isUrlLike(s))
40
+ return false;
41
+ if (s.startsWith('data:image/'))
42
+ return true;
43
+ try {
44
+ const url = new URL(s, 'http://x');
45
+ const path = url.pathname || '';
46
+ return IMG_EXT_RE.test(path.split('.').pop() || '');
47
+ }
48
+ catch {
49
+ const path = s.split('?')[0] || '';
50
+ return IMG_EXT_RE.test(path.split('.').pop() || '');
51
+ }
52
+ }
53
+ function extractUrl(v) {
54
+ if (!v)
55
+ return null;
56
+ if (typeof v === 'string' && isLikelyImageUrl(v))
57
+ return v;
58
+ if (typeof v === 'object') {
59
+ const u = (v.url || v.src || v.value);
60
+ return u && isLikelyImageUrl(u) ? u : null;
61
+ }
62
+ return null;
63
+ }
64
+ function extractAllImageUrls(val) {
65
+ if (Array.isArray(val)) {
66
+ const list = val.map(extractUrl).filter(Boolean);
67
+ if (list.length)
68
+ return list;
69
+ return (val.filter((s) => typeof s === 'string' && isLikelyImageUrl(s)) || []);
70
+ }
71
+ const one = extractUrl(val);
72
+ return one ? [one] : [];
73
+ }
74
+ function humanizeLabel(key) {
75
+ const spaced = key.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/_/g, ' ');
76
+ return spaced.slice(0, 1).toUpperCase() + spaced.slice(1);
77
+ }
78
+ /** ---------- Date/time helpers (local timezone formatting) ---------- */
79
+ // Check if a field name is a date field
80
+ function isDateField(key) {
81
+ if (!key)
82
+ return false;
83
+ const k = key.toLowerCase().replace(/[^a-z]/g, '');
84
+ return k === 'createdat' || k === 'updatedat' || k === 'deletedat';
85
+ }
86
+ function tryParseDate(val) {
87
+ if (val == null)
88
+ return null;
89
+ // Already a Date
90
+ if (val instanceof Date)
91
+ return isNaN(val.getTime()) ? null : val;
92
+ // ONLY parse ISO-like strings, NOT numbers (to avoid phone numbers, experience years, etc.)
93
+ if (typeof val === 'string') {
94
+ const s = val.trim();
95
+ // Must look like an ISO date: YYYY-MM-DD or contain 'T' or end with 'Z'
96
+ const looksIso = /^\d{4}-\d{2}-\d{2}/.test(s) || s.includes('T') || s.endsWith('Z');
97
+ if (!looksIso)
98
+ return null;
99
+ const d = new Date(s);
100
+ return isNaN(d.getTime()) ? null : d;
101
+ }
102
+ return null;
103
+ }
104
+ function formatDateTimeLocal(d) {
105
+ // Use the runtime locale and include seconds for precision
106
+ try {
107
+ return d.toLocaleString(undefined, {
108
+ year: 'numeric',
109
+ month: 'short',
110
+ day: '2-digit',
111
+ hour: '2-digit',
112
+ minute: '2-digit',
113
+ second: '2-digit',
114
+ hour12: true,
115
+ });
116
+ }
117
+ catch {
118
+ return d.toString();
119
+ }
120
+ }
121
+ function isPrivate(attr) {
122
+ const a = (Array.isArray(attr) ? attr[0] : attr);
123
+ return !!a?.private;
124
+ }
125
+ function FieldRow({ label, children }) {
126
+ return (_jsxs("div", { className: "grid grid-cols-12 gap-3 border-b border-default-100 py-3", children: [_jsx("div", { className: "col-span-4 md:col-span-3 text-foreground/70 text-sm", children: label }), _jsx("div", { className: "col-span-8 md:col-span-9 break-words", children: children })] }));
127
+ }
128
+ function ImageThumbs({ urls }) {
129
+ return (_jsx("div", { className: "flex flex-wrap gap-2", children: urls.map((u, i) => (_jsx("a", { href: u, target: "_blank", rel: "noreferrer", children: _jsx("img", { src: u, alt: "image", width: 64, height: 64, className: "h-16 w-16 rounded-md object-cover border border-default-200 bg-default-100", loading: "lazy" }) }, `${u}-${i}`))) }));
130
+ }
131
+ /** ---------- Generic label + nested-list condensation ---------- */
132
+ const COMMON_LABEL_KEYS = ['name', 'title', 'label', 'display', 'show'];
133
+ function toLabel(v) {
134
+ if (v == null)
135
+ return null;
136
+ if (typeof v === 'string')
137
+ return v.trim() || null;
138
+ if (typeof v === 'number' || typeof v === 'boolean')
139
+ return String(v);
140
+ if (typeof v === 'object') {
141
+ // Prefer common label keys
142
+ for (const k of COMMON_LABEL_KEYS) {
143
+ const val = v[k];
144
+ if (typeof val === 'string' && val.trim())
145
+ return val;
146
+ }
147
+ // Fallbacks
148
+ if (typeof v.value === 'string' && v.value.trim())
149
+ return v.value;
150
+ if (typeof v.id === 'string' && v.id.trim())
151
+ return v.id;
152
+ }
153
+ return null;
154
+ }
155
+ function listLabels(arr) {
156
+ if (!Array.isArray(arr))
157
+ return null;
158
+ const labels = arr
159
+ .map((it) => toLabel(it))
160
+ .filter((s) => !!s && !!s.trim());
161
+ return labels.length ? labels : null;
162
+ }
163
+ function summarizeList(labels, max = 10) {
164
+ if (labels.length <= max)
165
+ return labels.join(', ');
166
+ const shown = labels.slice(0, max).join(', ');
167
+ const more = labels.length - max;
168
+ return `${shown}, +${more} more`;
169
+ }
170
+ function tryCondenseEntity(obj) {
171
+ if (!obj || typeof obj !== 'object')
172
+ return null;
173
+ // Primary label from the object itself
174
+ const primary = toLabel(obj);
175
+ if (!primary)
176
+ return null;
177
+ // Find the most informative array property that yields labels
178
+ let best = null;
179
+ for (const [k, v] of Object.entries(obj)) {
180
+ if (HIDDEN_KEYS.has(k) || isPasswordKeyRaw(k))
181
+ continue;
182
+ if (!Array.isArray(v))
183
+ continue;
184
+ const labels = listLabels(v);
185
+ if (labels && labels.length) {
186
+ if (!best || labels.length > best.length)
187
+ best = labels;
188
+ }
189
+ }
190
+ if (best && best.length) {
191
+ return `${primary} — ${summarizeList(best)}`;
192
+ }
193
+ return null;
194
+ }
195
+ function maybeParseJson(val) {
196
+ if (typeof val !== 'string')
197
+ return val;
198
+ const s = val.trim();
199
+ if (!s)
200
+ return val;
201
+ if (!(s.startsWith('{') || s.startsWith('[')))
202
+ return val;
203
+ try {
204
+ const parsed = JSON.parse(s);
205
+ if (parsed && (typeof parsed === 'object' || Array.isArray(parsed)))
206
+ return parsed;
207
+ return val;
208
+ }
209
+ catch {
210
+ return val;
211
+ }
212
+ }
213
+ function renderValue(input, key) {
214
+ const val = maybeParseJson(input);
215
+ if (val == null)
216
+ return _jsx("span", { className: "text-foreground/50", children: "\u2014" });
217
+ // Only parse dates for known date fields (createdAt, updatedAt, deletedAt)
218
+ if (isDateField(key)) {
219
+ const d = tryParseDate(val);
220
+ if (d) {
221
+ const iso = d.toISOString();
222
+ return (_jsx("time", { dateTime: iso, title: iso, className: "whitespace-pre-wrap", children: formatDateTimeLocal(d) }));
223
+ }
224
+ }
225
+ // Detect images
226
+ const urls = extractAllImageUrls(val);
227
+ if (urls.length)
228
+ return _jsx(ImageThumbs, { urls: urls });
229
+ // Primitive or simple object/array formatting via shared formatter
230
+ if (Array.isArray(val)) {
231
+ // Prefer shared summarizer for arrays
232
+ if (val.length && typeof val[0] === 'object') {
233
+ // Show each summarized object on its own line for readability
234
+ const lines = val
235
+ .map((v) => (v && typeof v === 'object' ? summarizeObject(v)?.text : null))
236
+ .filter((s) => !!s && !!s.trim());
237
+ if (lines.length) {
238
+ return (_jsx("div", { className: "grid gap-1", children: lines.map((s, i) => (_jsx("div", { className: "text-foreground/90", children: s }, i))) }));
239
+ }
240
+ }
241
+ // Fallback detailed rendering
242
+ return (_jsx("div", { className: "grid gap-2", children: val.map((v, i) => (_jsx("div", { className: "rounded-medium border border-default-100 p-2", children: typeof v === 'object' && v !== null ? (_jsx(ObjectView, { obj: v })) : (_jsx("span", { children: formatCell(v, key) })) }, i))) }));
243
+ }
244
+ if (typeof val === 'object') {
245
+ const condensed = summarizeObject(val)?.text || tryCondenseEntity(val);
246
+ if (condensed)
247
+ return _jsx("span", { children: condensed });
248
+ return _jsx(ObjectView, { obj: val });
249
+ }
250
+ return _jsx("span", { children: formatCell(val, key) });
251
+ }
252
+ function ObjectView({ obj }) {
253
+ const entries = Object.entries(obj).filter(([k]) => !HIDDEN_KEYS.has(k) && !isPasswordKeyRaw(k));
254
+ if (!entries.length)
255
+ return _jsx("span", { className: "text-foreground/50", children: '{ }' });
256
+ return (_jsx("div", { className: "grid gap-1", children: entries.map(([k, v]) => (_jsxs("div", { className: "flex gap-2 items-start", children: [_jsxs("div", { className: "min-w-24 text-foreground/60 text-sm", children: [humanizeLabel(k), ":"] }), _jsx("div", { className: "flex-1", children: renderValue(v, k) })] }, k))) }));
257
+ }
258
+ export function DynamicViewer({ schema, data }) {
259
+ // Use schema order if available, otherwise object keys order
260
+ const attributes = (schema?.attributes ?? {});
261
+ const keys = React.useMemo(() => {
262
+ const base = Object.keys(attributes).filter((k) => {
263
+ if (HIDDEN_KEYS.has(k) || isPasswordKeyRaw(k))
264
+ return false;
265
+ if (isPasswordByAttr(attributes[k]))
266
+ return false;
267
+ return !isPrivate(attributes[k]);
268
+ });
269
+ if (base.length)
270
+ return base;
271
+ return Object.keys(data || {}).filter((k) => !HIDDEN_KEYS.has(k) && !isPasswordKeyRaw(k));
272
+ }, [attributes, data]);
273
+ return (_jsx("div", { className: "grid gap-1", children: keys.map((key) => (_jsx(FieldRow, { label: humanizeLabel(key), children: renderValue(data?.[key], key) }, key))) }));
274
+ }
@@ -0,0 +1,12 @@
1
+ import type { SchemaDef } from '../../lib/types';
2
+ export type OpenViewParams = {
3
+ title?: string;
4
+ model?: string;
5
+ schema?: SchemaDef;
6
+ data: Record<string, any>;
7
+ };
8
+ export declare function useViewDrawer(): {
9
+ readonly open: (params: OpenViewParams) => void;
10
+ readonly close: () => void;
11
+ readonly ViewDrawerPortal: () => import("react/jsx-runtime").JSX.Element | null;
12
+ };
@@ -0,0 +1,24 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React from 'react';
4
+ import { Drawer, DrawerBody, DrawerContent, DrawerFooter, DrawerHeader, Button, } from '@heroui/react';
5
+ import { DynamicViewer } from './DynamicViewer';
6
+ export function useViewDrawer() {
7
+ const [state, setState] = React.useState(null);
8
+ const open = React.useCallback((params) => {
9
+ setState({ ...params, isOpen: true });
10
+ }, []);
11
+ const close = React.useCallback(() => {
12
+ setState((s) => (s ? { ...s, isOpen: false } : s));
13
+ // delay unmount slightly for animation
14
+ setTimeout(() => setState(null), 200);
15
+ }, []);
16
+ const ViewDrawerPortal = React.useCallback(() => {
17
+ if (!state)
18
+ return null;
19
+ const { isOpen, title, model, schema, data } = state;
20
+ const headerTitle = title || model || 'Details';
21
+ return (_jsx(Drawer, { isOpen: isOpen, onClose: close, placement: "right", size: "lg", children: _jsxs(DrawerContent, { className: "w-[100vw] md:max-w-[50vw]", children: [_jsx(DrawerHeader, { className: "border-b border-default-200", children: headerTitle }), _jsx(DrawerBody, { className: "px-4 py-3", children: _jsx(DynamicViewer, { schema: schema, data: data }) }), _jsx(DrawerFooter, { className: "border-t border-default-200", children: _jsx(Button, { variant: "flat", onPress: close, children: "Close" }) })] }) }));
22
+ }, [state, close]);
23
+ return { open, close, ViewDrawerPortal };
24
+ }