@object-ui/app-shell 5.2.1 → 5.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # @object-ui/app-shell — Changelog
2
2
 
3
+ ## 5.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - efb4c00: feat(observability): Sentry integration + bundle splitting for production launch
8
+
9
+ **Sentry (opt-in via `VITE_SENTRY_DSN`)**
10
+ - New `initSentry()` / `captureError()` / `setSentryUser()` / `getSentry()`
11
+ helpers exported from `@object-ui/app-shell`.
12
+ - Dynamic-import design: when `VITE_SENTRY_DSN` is unset, `@sentry/react`
13
+ is **never fetched** — zero bundle cost for self-hosted users.
14
+ - `ErrorBoundary.componentDidCatch` now best-effort reports to Sentry.
15
+ - Console app calls `initSentry()` before React mount; never blocks first
16
+ paint.
17
+ - Configurable via:
18
+ - `VITE_SENTRY_DSN` — required to enable
19
+ - `VITE_SENTRY_ENVIRONMENT` — defaults to `MODE`
20
+ - `VITE_SENTRY_RELEASE` — defaults to `VITE_APP_VERSION`
21
+ - `VITE_SENTRY_TRACES_SAMPLE_RATE` — defaults to `0.1`
22
+ - `VITE_SENTRY_REPLAY=true` — opt-in to 10% on-error replay
23
+ - Sensitive URL params (`token`, `access_token`, `apiKey`, etc.) are
24
+ stripped from breadcrumb URLs before send.
25
+
26
+ **Bundle splitting**
27
+ - `plugin-dashboard` (8 component types) now lazy-registered via
28
+ `ComponentRegistry.registerLazy()` — only loads on dashboard pages.
29
+ - `plugin-dashboard` and `plugin-report` each get their own chunk
30
+ (previously merged into `plugins-views`).
31
+ - Net first-paint JS reduction: **~200 KB** when the user never visits a
32
+ dashboard or report page.
33
+ - New chunks: `plugin-dashboard` (119 K), `plugin-report` (92 K),
34
+ `vendor-sentry` (346 K raw / 97 K brotli, lazy).
35
+ - `plugins-views` shrinks 387 K → 180 K (now `plugin-list` + `plugin-detail` only).
36
+
37
+ ### Patch Changes
38
+
39
+ - @object-ui/types@5.3.0
40
+ - @object-ui/core@5.3.0
41
+ - @object-ui/i18n@5.3.0
42
+ - @object-ui/react@5.3.0
43
+ - @object-ui/components@5.3.0
44
+ - @object-ui/fields@5.3.0
45
+ - @object-ui/layout@5.3.0
46
+ - @object-ui/data-objectstack@5.3.0
47
+ - @object-ui/auth@5.3.0
48
+ - @object-ui/permissions@5.3.0
49
+ - @object-ui/collaboration@5.3.0
50
+ - @object-ui/providers@5.3.0
51
+
3
52
  ## 5.2.1
4
53
 
5
54
  ### Patch Changes
@@ -19,6 +19,7 @@ import { Component } from 'react';
19
19
  import { Button, Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
20
20
  import { AlertTriangle, RotateCcw, Home } from 'lucide-react';
21
21
  import { useObjectTranslation } from '@object-ui/i18n';
22
+ import { captureError } from '../observability';
22
23
  /** Inner fallback component that uses the i18n hook */
23
24
  function DefaultErrorFallback({ error, onReset }) {
24
25
  const { t } = useObjectTranslation();
@@ -42,6 +43,8 @@ export class ErrorBoundary extends Component {
42
43
  }
43
44
  componentDidCatch(error, errorInfo) {
44
45
  console.error('[ErrorBoundary] Caught error:', error, errorInfo);
46
+ // Best-effort: report to Sentry if initialized. No-op when DSN absent.
47
+ captureError(error, { componentStack: errorInfo.componentStack });
45
48
  this.props.onError?.(error, errorInfo);
46
49
  }
47
50
  render() {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Tiny CommonMark-subset renderer for marketplace READMEs.
3
+ *
4
+ * Covers what app authors actually write in their README field:
5
+ * • `#`, `##`, `###` headings
6
+ * • Blank-line-separated paragraphs
7
+ * • `- ` / `* ` bulleted lists (single level)
8
+ * • `1. ` numbered lists (single level)
9
+ * • `**bold**`, `*italic*`, `` `code` ``, `[text](url)` inline
10
+ * • Triple-backtick fenced code blocks
11
+ *
12
+ * Deliberately NOT a full Markdown engine — we want zero new deps and
13
+ * predictable output. If a README really needs tables or images, the
14
+ * publisher can host their own docs and link via `homepage_url`.
15
+ */
16
+ export declare function MarkdownText({ source, className }: {
17
+ source: string;
18
+ className?: string;
19
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,141 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Tiny CommonMark-subset renderer for marketplace READMEs.
4
+ *
5
+ * Covers what app authors actually write in their README field:
6
+ * • `#`, `##`, `###` headings
7
+ * • Blank-line-separated paragraphs
8
+ * • `- ` / `* ` bulleted lists (single level)
9
+ * • `1. ` numbered lists (single level)
10
+ * • `**bold**`, `*italic*`, `` `code` ``, `[text](url)` inline
11
+ * • Triple-backtick fenced code blocks
12
+ *
13
+ * Deliberately NOT a full Markdown engine — we want zero new deps and
14
+ * predictable output. If a README really needs tables or images, the
15
+ * publisher can host their own docs and link via `homepage_url`.
16
+ */
17
+ import { Fragment } from 'react';
18
+ function parseBlocks(src) {
19
+ const lines = src.replace(/\r\n/g, '\n').split('\n');
20
+ const blocks = [];
21
+ let i = 0;
22
+ while (i < lines.length) {
23
+ const line = lines[i];
24
+ // Fenced code block
25
+ if (line.startsWith('```')) {
26
+ const lang = line.slice(3).trim();
27
+ i++;
28
+ const buf = [];
29
+ while (i < lines.length && !lines[i].startsWith('```')) {
30
+ buf.push(lines[i]);
31
+ i++;
32
+ }
33
+ i++; // closing fence
34
+ blocks.push({ type: 'code', text: buf.join('\n'), lang });
35
+ continue;
36
+ }
37
+ // Headings
38
+ const h = /^(#{1,3})\s+(.*)$/.exec(line);
39
+ if (h) {
40
+ const level = h[1].length;
41
+ blocks.push({ type: `h${level}`, text: h[2].trim() });
42
+ i++;
43
+ continue;
44
+ }
45
+ // Bullet list
46
+ if (/^[-*]\s+/.test(line)) {
47
+ const items = [];
48
+ while (i < lines.length && /^[-*]\s+/.test(lines[i])) {
49
+ items.push(lines[i].replace(/^[-*]\s+/, ''));
50
+ i++;
51
+ }
52
+ blocks.push({ type: 'ul', items });
53
+ continue;
54
+ }
55
+ // Numbered list
56
+ if (/^\d+\.\s+/.test(line)) {
57
+ const items = [];
58
+ while (i < lines.length && /^\d+\.\s+/.test(lines[i])) {
59
+ items.push(lines[i].replace(/^\d+\.\s+/, ''));
60
+ i++;
61
+ }
62
+ blocks.push({ type: 'ol', items });
63
+ continue;
64
+ }
65
+ // Blank line → flush
66
+ if (!line.trim()) {
67
+ i++;
68
+ continue;
69
+ }
70
+ // Paragraph (collect until blank / heading / list)
71
+ const buf = [];
72
+ while (i < lines.length &&
73
+ lines[i].trim() &&
74
+ !/^#{1,3}\s+/.test(lines[i]) &&
75
+ !/^[-*]\s+/.test(lines[i]) &&
76
+ !/^\d+\.\s+/.test(lines[i]) &&
77
+ !lines[i].startsWith('```')) {
78
+ buf.push(lines[i]);
79
+ i++;
80
+ }
81
+ blocks.push({ type: 'p', text: buf.join(' ') });
82
+ }
83
+ return blocks;
84
+ }
85
+ // Inline pass: **bold**, *italic*, `code`, [text](url). Order matters
86
+ // so we tokenize once and replace by position.
87
+ function renderInline(input) {
88
+ const out = [];
89
+ let remaining = input;
90
+ let key = 0;
91
+ const patterns = [
92
+ { re: /`([^`]+)`/, render: (m) => _jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-[0.85em]", children: m[1] }, `k${key++}`) },
93
+ { re: /\*\*([^*]+)\*\*/, render: (m) => _jsx("strong", { className: "font-semibold text-foreground", children: m[1] }, `k${key++}`) },
94
+ { re: /\*([^*]+)\*/, render: (m) => _jsx("em", { children: m[1] }, `k${key++}`) },
95
+ { re: /\[([^\]]+)\]\(([^)]+)\)/, render: (m) => (_jsx("a", { href: m[2], target: "_blank", rel: "noopener noreferrer", className: "text-violet-600 underline-offset-2 hover:underline dark:text-violet-400", children: m[1] }, `k${key++}`)) },
96
+ ];
97
+ while (remaining) {
98
+ let bestIdx = -1;
99
+ let bestMatch = null;
100
+ let bestPattern = null;
101
+ for (const p of patterns) {
102
+ const m = p.re.exec(remaining);
103
+ if (m && (bestIdx === -1 || m.index < bestIdx)) {
104
+ bestIdx = m.index;
105
+ bestMatch = m;
106
+ bestPattern = p;
107
+ }
108
+ }
109
+ if (!bestMatch || !bestPattern) {
110
+ out.push(remaining);
111
+ break;
112
+ }
113
+ if (bestIdx > 0)
114
+ out.push(remaining.slice(0, bestIdx));
115
+ out.push(bestPattern.render(bestMatch));
116
+ remaining = remaining.slice(bestIdx + bestMatch[0].length);
117
+ }
118
+ return _jsx(_Fragment, { children: out.map((n, i) => _jsx(Fragment, { children: n }, i)) });
119
+ }
120
+ export function MarkdownText({ source, className }) {
121
+ const blocks = parseBlocks(source);
122
+ return (_jsx("div", { className: `flex flex-col gap-4 text-sm leading-relaxed text-foreground/90 ${className ?? ''}`, children: blocks.map((b, i) => {
123
+ switch (b.type) {
124
+ case 'h1':
125
+ return _jsx("h2", { className: "mt-2 text-xl font-bold tracking-tight", children: renderInline(b.text ?? '') }, i);
126
+ case 'h2':
127
+ return _jsx("h3", { className: "mt-3 text-base font-semibold tracking-tight", children: renderInline(b.text ?? '') }, i);
128
+ case 'h3':
129
+ return _jsx("h4", { className: "mt-2 text-sm font-semibold tracking-tight text-foreground/80", children: renderInline(b.text ?? '') }, i);
130
+ case 'p':
131
+ return _jsx("p", { children: renderInline(b.text ?? '') }, i);
132
+ case 'ul':
133
+ return (_jsx("ul", { className: "ml-1 flex list-none flex-col gap-1.5", children: (b.items ?? []).map((it, j) => (_jsxs("li", { className: "flex gap-2", children: [_jsx("span", { "aria-hidden": "true", className: "mt-1.5 inline-block h-1.5 w-1.5 shrink-0 rounded-full bg-violet-500/70" }), _jsx("span", { children: renderInline(it) })] }, j))) }, i));
134
+ case 'ol':
135
+ return (_jsx("ol", { className: "ml-5 flex list-decimal flex-col gap-1.5 marker:text-muted-foreground", children: (b.items ?? []).map((it, j) => (_jsx("li", { className: "pl-1", children: renderInline(it) }, j))) }, i));
136
+ case 'code':
137
+ return (_jsx("pre", { className: "overflow-x-auto rounded-lg border bg-muted/40 p-3 text-xs leading-relaxed", children: _jsx("code", { className: "font-mono", children: b.text }) }, i));
138
+ }
139
+ return null;
140
+ }) }));
141
+ }
@@ -10,6 +10,7 @@ import { useNavigate, useParams } from 'react-router-dom';
10
10
  import { Button, Badge, Card, CardContent, CardHeader, CardTitle, Skeleton, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Label, Checkbox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@object-ui/components';
11
11
  import { ArrowLeft, ExternalLink, Download, AlertCircle, Package, Trash2 } from 'lucide-react';
12
12
  import { PackageIcon } from './PackageIcon';
13
+ import { MarkdownText } from './MarkdownText';
13
14
  import { getMarketplacePackage, installPackage, installLocal, uninstallLocal, listLocalInstalls, listCloudEnvironments, listInstallableOrgIds, cloudInstallDeepLink, } from './marketplaceApi';
14
15
  export function MarketplacePackagePage() {
15
16
  const navigate = useNavigate();
@@ -190,10 +191,10 @@ export function MarketplacePackagePage() {
190
191
  const pkg = data.package;
191
192
  const latestVersion = pkg.latest_version?.version ?? data.versions[0]?.version ?? null;
192
193
  const localInstall = localInstalls.find((i) => i.manifestId === pkg.manifest_id) ?? null;
193
- return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6 max-w-5xl", children: [_jsxs(Button, { variant: "ghost", size: "sm", className: "self-start", onClick: () => navigate(`${basePath}/system/marketplace`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Back to marketplace"] }), _jsxs("div", { className: "flex items-start gap-4 flex-wrap", children: [_jsx(PackageIcon, { iconUrl: pkg.icon_url, displayName: pkg.display_name, manifestId: pkg.manifest_id, className: "h-16 w-16 rounded-xl", initialClassName: "text-2xl font-bold" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight truncate", children: pkg.display_name || pkg.manifest_id }), _jsxs("div", { className: "text-sm text-muted-foreground mt-1 flex flex-wrap items-center gap-2", children: [_jsx("code", { className: "font-mono", children: pkg.manifest_id }), latestVersion && (_jsxs(Badge, { variant: "outline", children: ["v", latestVersion] })), pkg.publisher && pkg.publisher !== 'private' && (_jsx(Badge, { variant: pkg.publisher === 'objectstack' ? 'default' : 'secondary', children: pkg.publisher })), pkg.category && _jsx(Badge, { variant: "outline", children: pkg.category }), pkg.license && _jsxs("span", { className: "text-xs", children: ["License: ", pkg.license] }), localInstall && (_jsxs(Badge, { variant: "default", className: "bg-green-600 hover:bg-green-600", children: ["Installed locally \u00B7 v", localInstall.version] }))] }), pkg.description && (_jsx("p", { className: "text-sm text-muted-foreground mt-3 max-w-2xl", children: pkg.description }))] }), _jsxs("div", { className: "flex flex-col gap-2 shrink-0 min-w-[14rem]", children: [_jsxs(Button, { onClick: doInstallLocal, disabled: !latestVersion || installingLocal, children: [_jsx(Download, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), installingLocal
194
+ return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6 max-w-5xl", children: [_jsxs(Button, { variant: "ghost", size: "sm", className: "self-start", onClick: () => navigate(`${basePath}/system/marketplace`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Back to marketplace"] }), _jsxs("div", { className: "flex items-start gap-5 flex-wrap rounded-2xl border bg-gradient-to-br from-primary/5 via-background to-background p-6", children: [_jsx(PackageIcon, { iconUrl: pkg.icon_url, displayName: pkg.display_name, manifestId: pkg.manifest_id, className: "h-20 w-20 rounded-2xl shadow-sm ring-1 ring-border", initialClassName: "text-3xl font-bold" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("h1", { className: "text-3xl font-bold tracking-tight truncate", children: pkg.display_name || pkg.manifest_id }), _jsxs("div", { className: "text-sm text-muted-foreground mt-2 flex flex-wrap items-center gap-2", children: [_jsx("code", { className: "font-mono text-xs px-1.5 py-0.5 rounded bg-muted", children: pkg.manifest_id }), latestVersion && (_jsxs(Badge, { variant: "outline", children: ["v", latestVersion] })), pkg.publisher && pkg.publisher !== 'private' && (_jsx(Badge, { variant: pkg.publisher === 'objectstack' ? 'default' : 'secondary', children: pkg.publisher })), pkg.category && _jsx(Badge, { variant: "outline", children: pkg.category }), pkg.license && _jsx(Badge, { variant: "outline", className: "font-normal", children: pkg.license }), localInstall && (_jsxs(Badge, { variant: "default", className: "bg-green-600 hover:bg-green-600", children: ["Installed \u00B7 v", localInstall.version] }))] }), pkg.description && (_jsx("p", { className: "text-base text-foreground/80 mt-4 max-w-2xl leading-relaxed", children: pkg.description }))] }), _jsxs("div", { className: "flex flex-col gap-2 shrink-0 min-w-[14rem]", children: [_jsxs(Button, { onClick: doInstallLocal, disabled: !latestVersion || installingLocal, size: "lg", children: [_jsx(Download, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), installingLocal
194
195
  ? 'Working…'
195
196
  : localInstall
196
197
  ? `Reinstall to this runtime`
197
- : 'Install to this runtime'] }), localInstall && (_jsxs(Button, { variant: "outline", onClick: doUninstallLocal, disabled: installingLocal, children: [_jsx(Trash2, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Uninstall from this runtime"] })), _jsxs(Button, { variant: "outline", onClick: openInstall, disabled: !latestVersion, children: [_jsx(Download, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Install to cloud environment\u2026"] }), pkg.homepage_url && (_jsx("a", { href: pkg.homepage_url, target: "_blank", rel: "noopener noreferrer", children: _jsxs(Button, { variant: "ghost", className: "w-full", children: [_jsx(ExternalLink, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Homepage"] }) })), localResult && (_jsx("div", { className: `rounded-md border p-2 text-xs whitespace-pre-wrap ${localResult.ok ? 'border-green-500/30 bg-green-500/5 text-green-700 dark:text-green-400' : 'border-destructive/30 bg-destructive/5 text-destructive'}`, children: localResult.message }))] })] }), _jsxs("div", { className: "grid gap-4 lg:grid-cols-3", children: [_jsx("div", { className: "lg:col-span-2 space-y-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "About" }) }), _jsx(CardContent, { children: pkg.readme ? (_jsx("pre", { className: "whitespace-pre-wrap text-sm font-sans", children: pkg.readme })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: "No readme provided." })) })] }) }), _jsx("div", { className: "space-y-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Versions" }) }), _jsx(CardContent, { children: data.versions.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "No approved versions." })) : (_jsx("ul", { className: "space-y-2", children: data.versions.map((v) => (_jsxs("li", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Package, { className: "h-3.5 w-3.5 text-muted-foreground", "aria-hidden": "true" }), _jsxs("code", { className: "font-mono", children: ["v", v.version] }), v.is_prerelease && _jsx(Badge, { variant: "outline", className: "text-xs", children: "pre" })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: v.published_at ? new Date(v.published_at).toLocaleDateString() : '—' })] }, v.id))) })) })] }) })] }), _jsx(Dialog, { open: installOpen, onOpenChange: (o) => { setInstallOpen(o); if (!o)
198
+ : 'Install to this runtime'] }), localInstall && (_jsxs(Button, { variant: "outline", onClick: doUninstallLocal, disabled: installingLocal, children: [_jsx(Trash2, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Uninstall from this runtime"] })), _jsxs(Button, { variant: "ghost", onClick: openInstall, disabled: !latestVersion, size: "sm", children: [_jsx(Download, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Install to cloud environment\u2026"] }), pkg.homepage_url && (_jsx("a", { href: pkg.homepage_url, target: "_blank", rel: "noopener noreferrer", children: _jsxs(Button, { variant: "ghost", size: "sm", className: "w-full", children: [_jsx(ExternalLink, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Homepage"] }) })), localResult && (_jsx("div", { className: `rounded-md border p-2 text-xs whitespace-pre-wrap ${localResult.ok ? 'border-green-500/30 bg-green-500/5 text-green-700 dark:text-green-400' : 'border-destructive/30 bg-destructive/5 text-destructive'}`, children: localResult.message }))] })] }), _jsxs("div", { className: "grid gap-4 lg:grid-cols-3", children: [_jsx("div", { className: "lg:col-span-2 space-y-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "About" }) }), _jsx(CardContent, { children: pkg.readme ? (_jsx(MarkdownText, { source: pkg.readme })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: "No readme provided." })) })] }) }), _jsx("div", { className: "space-y-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Versions" }) }), _jsx(CardContent, { children: data.versions.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "No approved versions." })) : (_jsx("ul", { className: "space-y-2", children: data.versions.map((v) => (_jsxs("li", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Package, { className: "h-3.5 w-3.5 text-muted-foreground", "aria-hidden": "true" }), _jsxs("code", { className: "font-mono", children: ["v", v.version] }), v.is_prerelease && _jsx(Badge, { variant: "outline", className: "text-xs", children: "pre" })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: v.published_at ? new Date(v.published_at).toLocaleDateString() : '—' })] }, v.id))) })) })] }) })] }), _jsx(Dialog, { open: installOpen, onOpenChange: (o) => { setInstallOpen(o); if (!o)
198
199
  setInstallResult(null); }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Install ", pkg.display_name || pkg.manifest_id] }), _jsx(DialogDescription, { children: "Choose an environment to install this app into. You need to be signed into ObjectStack Cloud." })] }), envsLoading ? (_jsx(Skeleton, { className: "h-10 w-full" })) : envsError ? (_jsxs("div", { className: "rounded-md border border-amber-500/30 bg-amber-500/5 p-3 text-sm space-y-2", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-amber-600", "aria-hidden": "true" }), _jsx("div", { className: "flex-1", children: envsError })] }), _jsx("a", { href: cloudInstallDeepLink(pkg.id), target: "_blank", rel: "noopener noreferrer", children: _jsxs(Button, { variant: "outline", size: "sm", className: "w-full", children: [_jsx(ExternalLink, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Open on cloud"] }) })] })) : envs.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "No environments found in your active organization." })) : (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "env-select", children: "Environment" }), _jsxs(Select, { value: selectedEnv, onValueChange: setSelectedEnv, children: [_jsx(SelectTrigger, { id: "env-select", children: _jsx(SelectValue, { placeholder: "Pick an environment" }) }), _jsx(SelectContent, { children: envs.map((e) => (_jsxs(SelectItem, { value: e.id, children: [e.display_name || e.hostname || e.id, e.plan && _jsxs("span", { className: "text-muted-foreground", children: [" \u00B7 ", e.plan] })] }, e.id))) })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "seed", checked: seedSampleData, onCheckedChange: (c) => setSeedSampleData(c === true) }), _jsx(Label, { htmlFor: "seed", className: "text-sm font-normal cursor-pointer", children: "Include sample data" })] })] })), installResult && (_jsx("div", { className: `rounded-md border p-3 text-sm ${installResult.ok ? 'border-green-500/30 bg-green-500/5 text-green-700' : 'border-destructive/30 bg-destructive/5 text-destructive'}`, children: installResult.message })), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setInstallOpen(false), children: "Close" }), !envsError && (_jsx(Button, { onClick: doInstall, disabled: !selectedEnv || installing || installResult?.ok === true, children: installing ? 'Installing…' : 'Install' }))] })] }) })] }));
199
200
  }
@@ -82,7 +82,7 @@ export function MarketplacePage() {
82
82
  return hay.includes(q);
83
83
  });
84
84
  }, [items, query, category]);
85
- return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Store, { className: "h-6 w-6 text-primary", "aria-hidden": "true" }), _jsx("h1", { className: "text-xl sm:text-2xl font-bold tracking-tight", children: "App Marketplace" })] }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Browse approved apps published to the ObjectStack catalog. Click an app to view details and install it into one of your environments." })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => navigate(`${basePath}/system/marketplace/installed`), children: [_jsx(Settings, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Installed", installed.length > 0 ? ` (${installed.length})` : ''] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void load(), disabled: loading, children: [_jsx(RefreshCcw, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Refresh"] })] })] }), _jsxs("div", { className: "flex flex-col sm:flex-row gap-3", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground", "aria-hidden": "true" }), _jsx(Input, { value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search apps by name or manifest ID\u2026", className: "pl-9", "aria-label": "Search marketplace apps" })] }), categories.length > 0 && (_jsxs("div", { className: "flex flex-wrap gap-1.5", children: [_jsx(Button, { size: "sm", variant: category === '' ? 'default' : 'outline', onClick: () => setCategory(''), children: "All" }), categories.map((cat) => (_jsx(Button, { size: "sm", variant: category === cat ? 'default' : 'outline', onClick: () => setCategory(cat), children: cat }, cat)))] }))] }), error && (_jsxs("div", { className: "flex items-start gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-destructive", "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("div", { className: "font-medium text-destructive", children: "Failed to load marketplace" }), _jsx("div", { className: "text-muted-foreground mt-1", children: error }), _jsxs("div", { className: "text-xs text-muted-foreground mt-2", children: ["Check that this runtime is configured with ", _jsx("code", { className: "font-mono", children: "OS_CLOUD_URL" }), " pointing at a reachable ObjectStack Cloud."] })] })] })), loading && items.length === 0 ? (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-2", children: [_jsx(Skeleton, { className: "h-10 w-10 rounded-lg mb-2" }), _jsx(Skeleton, { className: "h-5 w-3/4" }), _jsx(Skeleton, { className: "h-4 w-1/2 mt-1" })] }), _jsx(CardContent, { children: _jsx(Skeleton, { className: "h-12 w-full" }) })] }, i))) })) : filtered.length === 0 ? (_jsx("div", { className: "text-center py-12 text-sm text-muted-foreground", children: items.length === 0 ? 'No apps have been approved for the marketplace yet.' : 'No apps match your filters.' })) : (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: filtered.map((pkg) => {
85
+ return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Store, { className: "h-6 w-6 text-primary", "aria-hidden": "true" }), _jsx("h1", { className: "text-xl sm:text-2xl font-bold tracking-tight", children: "App Marketplace" })] }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Browse approved apps published to the ObjectStack catalog. Click an app to view details and install it into one of your environments." })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => navigate(`${basePath}/system/marketplace/installed`), children: [_jsx(Settings, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Installed", installed.length > 0 ? ` (${installed.length})` : ''] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void load(), disabled: loading, children: [_jsx(RefreshCcw, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Refresh"] })] })] }), _jsxs("div", { className: "flex flex-col sm:flex-row gap-3", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground", "aria-hidden": "true" }), _jsx(Input, { value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search apps by name or manifest ID\u2026", className: "pl-9", "aria-label": "Search marketplace apps" })] }), categories.length > 0 && (_jsxs("div", { className: "flex flex-wrap gap-1.5", children: [_jsx(Button, { size: "sm", variant: category === '' ? 'default' : 'outline', onClick: () => setCategory(''), children: "All" }), categories.map((cat) => (_jsx(Button, { size: "sm", variant: category === cat ? 'default' : 'outline', onClick: () => setCategory(cat), children: cat }, cat)))] }))] }), error && (_jsxs("div", { className: "flex items-start gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-destructive", "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("div", { className: "font-medium text-destructive", children: "Failed to load marketplace" }), _jsx("div", { className: "text-muted-foreground mt-1", children: error }), _jsxs("div", { className: "text-xs text-muted-foreground mt-2", children: ["By default this runtime points at the public ObjectStack cloud. Check the runtime is online, or override ", _jsx("code", { className: "font-mono", children: "OS_CLOUD_URL" }), " to point at a self-hosted control plane."] })] })] })), loading && items.length === 0 ? (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-2", children: [_jsx(Skeleton, { className: "h-10 w-10 rounded-lg mb-2" }), _jsx(Skeleton, { className: "h-5 w-3/4" }), _jsx(Skeleton, { className: "h-4 w-1/2 mt-1" })] }), _jsx(CardContent, { children: _jsx(Skeleton, { className: "h-12 w-full" }) })] }, i))) })) : filtered.length === 0 ? (_jsx("div", { className: "text-center py-12 text-sm text-muted-foreground", children: items.length === 0 ? 'No apps have been approved for the marketplace yet.' : 'No apps match your filters.' })) : (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: filtered.map((pkg) => {
86
86
  const localEntry = installedByManifestId.get(pkg.manifest_id);
87
87
  return (_jsxs(Card, { className: "cursor-pointer transition-colors hover:bg-accent/50 flex flex-col", onClick: () => navigate(`${basePath}/system/marketplace/${pkg.id}`), role: "link", tabIndex: 0, onKeyDown: (e) => {
88
88
  if (e.key === 'Enter' || e.key === ' ') {
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ export { ConsoleShell, ConnectedShell, RequireOrganization, AuthenticatedRoute,
18
18
  export { ConsoleLayout, AppHeader, AppSidebar, UnifiedSidebar, AppSwitcher, ConnectionStatus, ActivityFeed, LocaleSwitcher, ModeToggle, AuthPageLayout, } from './layout';
19
19
  export type { ActivityItem } from './layout';
20
20
  export { CommandPalette, KeyboardShortcutsDialog, OnboardingWalkthrough, ConditionalAuthWrapper, ConsoleToaster, RouteFader, toastWithUndo, type ToastWithUndoOptions, ErrorBoundary, LoadingScreen, ThemeProvider, useTheme, } from './chrome';
21
+ export { initSentry, captureError, setSentryUser, getSentry } from './observability';
21
22
  export { ObjectView, RecordDetailView, RecordFormPage, DashboardView, PageView, ReportView, SearchResultsPage, ViewConfigPanel, } from './views';
22
23
  export type { RecordFormPageProps } from './views';
23
24
  export { useFavorites, useMetadataService, useNavPins, useNavigationSync, NavigationSyncEffect, addNavigationItem, removeNavigationItems, renameNavigationItems, navigationEqual, generateNavId, useResponsiveSidebar, } from './hooks';
package/dist/index.js CHANGED
@@ -20,6 +20,8 @@ export { ConsoleShell, ConnectedShell, RequireOrganization, AuthenticatedRoute,
20
20
  export { ConsoleLayout, AppHeader, AppSidebar, UnifiedSidebar, AppSwitcher, ConnectionStatus, ActivityFeed, LocaleSwitcher, ModeToggle, AuthPageLayout, } from './layout';
21
21
  // Top-level chrome (dialogs, providers, error boundaries)
22
22
  export { CommandPalette, KeyboardShortcutsDialog, OnboardingWalkthrough, ConditionalAuthWrapper, ConsoleToaster, RouteFader, toastWithUndo, ErrorBoundary, LoadingScreen, ThemeProvider, useTheme, } from './chrome';
23
+ // Observability — Sentry integration, opt-in via VITE_SENTRY_DSN
24
+ export { initSentry, captureError, setSentryUser, getSentry } from './observability';
23
25
  // Standard inner-SPA views
24
26
  export { ObjectView, RecordDetailView, RecordFormPage, DashboardView, PageView, ReportView, SearchResultsPage, ViewConfigPanel, } from './views';
25
27
  // Hooks
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Observability primitives — Sentry integration.
3
+ *
4
+ * All exports are no-op safe when no DSN is configured. See sentry.ts for
5
+ * configuration via `VITE_SENTRY_*` envvars.
6
+ *
7
+ * @module
8
+ */
9
+ export { initSentry, captureError, setSentryUser, getSentry } from './sentry';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Observability primitives — Sentry integration.
3
+ *
4
+ * All exports are no-op safe when no DSN is configured. See sentry.ts for
5
+ * configuration via `VITE_SENTRY_*` envvars.
6
+ *
7
+ * @module
8
+ */
9
+ export { initSentry, captureError, setSentryUser, getSentry } from './sentry';
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Sentry integration — opt-in via `VITE_SENTRY_DSN`.
3
+ *
4
+ * Design goals:
5
+ * - **Zero cost when disabled.** `@sentry/react` is dynamically imported only
6
+ * when a DSN is configured, so apps without Sentry pay zero bundle bytes.
7
+ * - **Graceful degradation.** If init fails (network, CSP, etc.) we log a
8
+ * warning and continue — the host app must still render.
9
+ * - **Sensible defaults.** 10% transaction sampling, no session replay,
10
+ * `release` + `environment` pulled from Vite envvars.
11
+ *
12
+ * Env vars consumed (all optional):
13
+ * - `VITE_SENTRY_DSN` — DSN; absent disables the integration entirely
14
+ * - `VITE_SENTRY_ENVIRONMENT` — defaults to `MODE` (production/development)
15
+ * - `VITE_SENTRY_RELEASE` — defaults to `VITE_APP_VERSION` or `unknown`
16
+ * - `VITE_SENTRY_TRACES_SAMPLE_RATE` — defaults to `0.1`
17
+ *
18
+ * @module
19
+ */
20
+ type SentryModule = typeof import('@sentry/react');
21
+ /**
22
+ * Returns the loaded Sentry module, or `null` if Sentry was never initialized
23
+ * (e.g. DSN missing). Callers must handle the null case.
24
+ */
25
+ export declare function getSentry(): SentryModule | null;
26
+ /**
27
+ * Initializes Sentry if `VITE_SENTRY_DSN` is configured. Safe to call multiple
28
+ * times — only the first invocation runs.
29
+ *
30
+ * @returns `true` if Sentry was initialized, `false` if disabled or failed.
31
+ */
32
+ export declare function initSentry(): Promise<boolean>;
33
+ /**
34
+ * Reports an error to Sentry if initialized; otherwise no-op. Use this from
35
+ * ErrorBoundary or any catch block where you want best-effort reporting.
36
+ */
37
+ export declare function captureError(error: unknown, context?: Record<string, unknown>): void;
38
+ /**
39
+ * Sets the active user context for subsequent events. Pass `null` on logout.
40
+ */
41
+ export declare function setSentryUser(user: {
42
+ id?: string;
43
+ email?: string;
44
+ username?: string;
45
+ } | null): void;
46
+ export {};
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Sentry integration — opt-in via `VITE_SENTRY_DSN`.
3
+ *
4
+ * Design goals:
5
+ * - **Zero cost when disabled.** `@sentry/react` is dynamically imported only
6
+ * when a DSN is configured, so apps without Sentry pay zero bundle bytes.
7
+ * - **Graceful degradation.** If init fails (network, CSP, etc.) we log a
8
+ * warning and continue — the host app must still render.
9
+ * - **Sensible defaults.** 10% transaction sampling, no session replay,
10
+ * `release` + `environment` pulled from Vite envvars.
11
+ *
12
+ * Env vars consumed (all optional):
13
+ * - `VITE_SENTRY_DSN` — DSN; absent disables the integration entirely
14
+ * - `VITE_SENTRY_ENVIRONMENT` — defaults to `MODE` (production/development)
15
+ * - `VITE_SENTRY_RELEASE` — defaults to `VITE_APP_VERSION` or `unknown`
16
+ * - `VITE_SENTRY_TRACES_SAMPLE_RATE` — defaults to `0.1`
17
+ *
18
+ * @module
19
+ */
20
+ let sentryModule = null;
21
+ let initPromise = null;
22
+ /**
23
+ * Returns the loaded Sentry module, or `null` if Sentry was never initialized
24
+ * (e.g. DSN missing). Callers must handle the null case.
25
+ */
26
+ export function getSentry() {
27
+ return sentryModule;
28
+ }
29
+ /**
30
+ * Initializes Sentry if `VITE_SENTRY_DSN` is configured. Safe to call multiple
31
+ * times — only the first invocation runs.
32
+ *
33
+ * @returns `true` if Sentry was initialized, `false` if disabled or failed.
34
+ */
35
+ export function initSentry() {
36
+ if (initPromise)
37
+ return initPromise;
38
+ initPromise = (async () => {
39
+ const env = import.meta.env ?? {};
40
+ const dsn = env.VITE_SENTRY_DSN;
41
+ if (!dsn)
42
+ return false;
43
+ try {
44
+ const Sentry = (await import('@sentry/react'));
45
+ const tracesSampleRate = Number(env.VITE_SENTRY_TRACES_SAMPLE_RATE ?? '0.1');
46
+ Sentry.init({
47
+ dsn,
48
+ environment: env.VITE_SENTRY_ENVIRONMENT || env.MODE || 'production',
49
+ release: env.VITE_SENTRY_RELEASE || env.VITE_APP_VERSION || 'unknown',
50
+ tracesSampleRate: Number.isFinite(tracesSampleRate) ? tracesSampleRate : 0.1,
51
+ // Send IP address + user agent on events. Sentry's recommended default
52
+ // for production. Disable by setting VITE_SENTRY_SEND_DEFAULT_PII=false.
53
+ sendDefaultPii: env.VITE_SENTRY_SEND_DEFAULT_PII !== 'false',
54
+ // Replay is opt-in via VITE_SENTRY_REPLAY=true to keep payload small.
55
+ // When enabled, only 10% of error sessions are recorded.
56
+ replaysSessionSampleRate: 0,
57
+ replaysOnErrorSampleRate: env.VITE_SENTRY_REPLAY === 'true' ? 0.1 : 0,
58
+ // Browser tracing — captures pageloads + navigation transactions.
59
+ integrations: [Sentry.browserTracingIntegration()],
60
+ // Strip query strings + Authorization from breadcrumbs before send.
61
+ beforeBreadcrumb(breadcrumb) {
62
+ if (breadcrumb.category === 'fetch' || breadcrumb.category === 'xhr') {
63
+ if (breadcrumb.data?.url && typeof breadcrumb.data.url === 'string') {
64
+ breadcrumb.data.url = stripSensitive(breadcrumb.data.url);
65
+ }
66
+ }
67
+ return breadcrumb;
68
+ },
69
+ });
70
+ sentryModule = Sentry;
71
+ return true;
72
+ }
73
+ catch (err) {
74
+ console.warn('[sentry] init failed; continuing without observability:', err);
75
+ return false;
76
+ }
77
+ })();
78
+ return initPromise;
79
+ }
80
+ /**
81
+ * Reports an error to Sentry if initialized; otherwise no-op. Use this from
82
+ * ErrorBoundary or any catch block where you want best-effort reporting.
83
+ */
84
+ export function captureError(error, context) {
85
+ if (!sentryModule)
86
+ return;
87
+ try {
88
+ sentryModule.captureException(error, context ? { extra: context } : undefined);
89
+ }
90
+ catch {
91
+ // never let observability break the host app
92
+ }
93
+ }
94
+ /**
95
+ * Sets the active user context for subsequent events. Pass `null` on logout.
96
+ */
97
+ export function setSentryUser(user) {
98
+ if (!sentryModule)
99
+ return;
100
+ try {
101
+ sentryModule.setUser(user);
102
+ }
103
+ catch {
104
+ /* swallow */
105
+ }
106
+ }
107
+ function stripSensitive(url) {
108
+ try {
109
+ const u = new URL(url, 'http://localhost');
110
+ // Drop common token-shaped query params before sending to Sentry.
111
+ for (const key of ['token', 'access_token', 'id_token', 'apiKey', 'api_key', 'password']) {
112
+ if (u.searchParams.has(key))
113
+ u.searchParams.set(key, '[redacted]');
114
+ }
115
+ return u.pathname + (u.searchParams.toString() ? '?' + u.searchParams.toString() : '');
116
+ }
117
+ catch {
118
+ return url;
119
+ }
120
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/app-shell",
3
- "version": "5.2.1",
3
+ "version": "5.3.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
@@ -25,37 +25,38 @@
25
25
  "./styles.css": "./src/styles.css"
26
26
  },
27
27
  "dependencies": {
28
+ "@sentry/react": "^8.55.2",
28
29
  "lucide-react": "^1.16.0",
29
30
  "sonner": "^2.0.7",
30
- "@object-ui/auth": "5.2.1",
31
- "@object-ui/collaboration": "5.2.1",
32
- "@object-ui/components": "5.2.1",
33
- "@object-ui/core": "5.2.1",
34
- "@object-ui/data-objectstack": "5.2.1",
35
- "@object-ui/fields": "5.2.1",
36
- "@object-ui/i18n": "5.2.1",
37
- "@object-ui/layout": "5.2.1",
38
- "@object-ui/permissions": "5.2.1",
39
- "@object-ui/providers": "5.2.1",
40
- "@object-ui/react": "5.2.1",
41
- "@object-ui/types": "5.2.1"
31
+ "@object-ui/auth": "5.3.0",
32
+ "@object-ui/collaboration": "5.3.0",
33
+ "@object-ui/components": "5.3.0",
34
+ "@object-ui/core": "5.3.0",
35
+ "@object-ui/data-objectstack": "5.3.0",
36
+ "@object-ui/fields": "5.3.0",
37
+ "@object-ui/i18n": "5.3.0",
38
+ "@object-ui/layout": "5.3.0",
39
+ "@object-ui/permissions": "5.3.0",
40
+ "@object-ui/providers": "5.3.0",
41
+ "@object-ui/react": "5.3.0",
42
+ "@object-ui/types": "5.3.0"
42
43
  },
43
44
  "peerDependencies": {
44
45
  "react": "^18.0.0 || ^19.0.0",
45
46
  "react-dom": "^18.0.0 || ^19.0.0",
46
47
  "react-router-dom": "^6.0.0 || ^7.0.0",
47
- "@object-ui/plugin-calendar": "^5.2.1",
48
- "@object-ui/plugin-charts": "^5.2.1",
49
- "@object-ui/plugin-chatbot": "^5.2.1",
50
- "@object-ui/plugin-dashboard": "^5.2.1",
51
- "@object-ui/plugin-designer": "^5.2.1",
52
- "@object-ui/plugin-detail": "^5.2.1",
53
- "@object-ui/plugin-form": "^5.2.1",
54
- "@object-ui/plugin-grid": "^5.2.1",
55
- "@object-ui/plugin-kanban": "^5.2.1",
56
- "@object-ui/plugin-list": "^5.2.1",
57
- "@object-ui/plugin-report": "^5.2.1",
58
- "@object-ui/plugin-view": "^5.2.1"
48
+ "@object-ui/plugin-calendar": "^5.3.0",
49
+ "@object-ui/plugin-charts": "^5.3.0",
50
+ "@object-ui/plugin-chatbot": "^5.3.0",
51
+ "@object-ui/plugin-dashboard": "^5.3.0",
52
+ "@object-ui/plugin-designer": "^5.3.0",
53
+ "@object-ui/plugin-detail": "^5.3.0",
54
+ "@object-ui/plugin-form": "^5.3.0",
55
+ "@object-ui/plugin-grid": "^5.3.0",
56
+ "@object-ui/plugin-kanban": "^5.3.0",
57
+ "@object-ui/plugin-list": "^5.3.0",
58
+ "@object-ui/plugin-report": "^5.3.0",
59
+ "@object-ui/plugin-view": "^5.3.0"
59
60
  },
60
61
  "devDependencies": {
61
62
  "@types/node": "^25.9.0",