@object-ui/app-shell 6.1.0 → 6.2.1

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 (87) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/README.md +10 -1
  3. package/dist/console/AppContent.js +53 -2
  4. package/dist/console/ai/AiChatPage.d.ts +8 -0
  5. package/dist/console/ai/AiChatPage.js +188 -0
  6. package/dist/console/ai/ConversationsSidebar.d.ts +7 -0
  7. package/dist/console/ai/ConversationsSidebar.js +111 -0
  8. package/dist/console/auth/LoginPage.js +19 -2
  9. package/dist/console/auth/RegisterPage.js +30 -1
  10. package/dist/console/marketplace/MarketplaceAccessDenied.js +3 -1
  11. package/dist/console/marketplace/MarketplaceInstalledPage.js +11 -3
  12. package/dist/console/marketplace/MarketplacePackagePage.js +57 -19
  13. package/dist/console/marketplace/MarketplacePage.js +55 -18
  14. package/dist/console/marketplace/marketplaceApi.d.ts +20 -0
  15. package/dist/console/marketplace/usePackageL10n.d.ts +38 -0
  16. package/dist/console/marketplace/usePackageL10n.js +110 -0
  17. package/dist/console/organizations/CreateWorkspaceDialog.js +29 -1
  18. package/dist/console/organizations/OrganizationsPage.js +24 -3
  19. package/dist/context/FavoritesProvider.d.ts +40 -2
  20. package/dist/context/FavoritesProvider.js +201 -20
  21. package/dist/hooks/index.d.ts +1 -0
  22. package/dist/hooks/index.js +1 -0
  23. package/dist/hooks/useChatConversation.d.ts +7 -0
  24. package/dist/hooks/useChatConversation.js +37 -5
  25. package/dist/hooks/useConversationList.d.ts +25 -0
  26. package/dist/hooks/useConversationList.js +131 -0
  27. package/dist/hooks/useNavPins.d.ts +11 -4
  28. package/dist/hooks/useNavPins.js +104 -53
  29. package/dist/index.d.ts +7 -0
  30. package/dist/index.js +14 -0
  31. package/dist/layout/AppHeader.js +2 -2
  32. package/dist/layout/AppSidebar.js +20 -1
  33. package/dist/layout/UnifiedSidebar.js +1 -1
  34. package/dist/providers/ExpressionProvider.d.ts +11 -1
  35. package/dist/providers/ExpressionProvider.js +11 -6
  36. package/dist/services/builtinComponents.d.ts +1 -0
  37. package/dist/services/builtinComponents.js +169 -0
  38. package/dist/services/componentRegistry.d.ts +63 -0
  39. package/dist/services/componentRegistry.js +36 -0
  40. package/dist/views/ComponentNavView.d.ts +6 -0
  41. package/dist/views/ComponentNavView.js +26 -0
  42. package/dist/views/RecordDetailView.js +66 -6
  43. package/dist/views/RecordFormPage.js +15 -3
  44. package/dist/views/SearchResultsPage.js +4 -0
  45. package/dist/views/metadata-admin/DesignerEditorWrapper.d.ts +68 -0
  46. package/dist/views/metadata-admin/DesignerEditorWrapper.js +158 -0
  47. package/dist/views/metadata-admin/DirectoryPage.d.ts +1 -0
  48. package/dist/views/metadata-admin/DirectoryPage.js +135 -0
  49. package/dist/views/metadata-admin/LayeredDiff.d.ts +6 -0
  50. package/dist/views/metadata-admin/LayeredDiff.js +26 -0
  51. package/dist/views/metadata-admin/MetadataDetailDrawer.d.ts +13 -0
  52. package/dist/views/metadata-admin/MetadataDetailDrawer.js +52 -0
  53. package/dist/views/metadata-admin/PageShell.d.ts +34 -0
  54. package/dist/views/metadata-admin/PageShell.js +40 -0
  55. package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +5 -0
  56. package/dist/views/metadata-admin/PermissionMatrixEditor.js +288 -0
  57. package/dist/views/metadata-admin/QuickFind.d.ts +5 -0
  58. package/dist/views/metadata-admin/QuickFind.js +152 -0
  59. package/dist/views/metadata-admin/RelatedPanel.d.ts +33 -0
  60. package/dist/views/metadata-admin/RelatedPanel.js +171 -0
  61. package/dist/views/metadata-admin/ResourceEditPage.d.ts +13 -0
  62. package/dist/views/metadata-admin/ResourceEditPage.js +302 -0
  63. package/dist/views/metadata-admin/ResourceHistoryPage.d.ts +5 -0
  64. package/dist/views/metadata-admin/ResourceHistoryPage.js +100 -0
  65. package/dist/views/metadata-admin/ResourceListPage.d.ts +4 -0
  66. package/dist/views/metadata-admin/ResourceListPage.js +146 -0
  67. package/dist/views/metadata-admin/ResourceRouter.d.ts +10 -0
  68. package/dist/views/metadata-admin/ResourceRouter.js +47 -0
  69. package/dist/views/metadata-admin/SchemaForm.d.ts +99 -0
  70. package/dist/views/metadata-admin/SchemaForm.js +565 -0
  71. package/dist/views/metadata-admin/anchors.d.ts +1 -0
  72. package/dist/views/metadata-admin/anchors.js +229 -0
  73. package/dist/views/metadata-admin/default-schemas.d.ts +6 -0
  74. package/dist/views/metadata-admin/default-schemas.js +207 -0
  75. package/dist/views/metadata-admin/i18n.d.ts +33 -0
  76. package/dist/views/metadata-admin/i18n.js +303 -0
  77. package/dist/views/metadata-admin/index.d.ts +33 -0
  78. package/dist/views/metadata-admin/index.js +39 -0
  79. package/dist/views/metadata-admin/predicate.d.ts +31 -0
  80. package/dist/views/metadata-admin/predicate.js +150 -0
  81. package/dist/views/metadata-admin/registry.d.ts +232 -0
  82. package/dist/views/metadata-admin/registry.js +106 -0
  83. package/dist/views/metadata-admin/useMetadata.d.ts +37 -0
  84. package/dist/views/metadata-admin/useMetadata.js +96 -0
  85. package/dist/views/metadata-admin/widgets.d.ts +68 -0
  86. package/dist/views/metadata-admin/widgets.js +287 -0
  87. package/package.json +27 -26
@@ -0,0 +1,158 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3
+ /**
4
+ * DesignerEditorWrapper — Phase 3d.
5
+ *
6
+ * Generic "load metadata → hand to a controlled designer component →
7
+ * save on commit" wrapper. Lets us plug existing bespoke designers
8
+ * (ObjectViewConfigurator, DashboardEditor, PageDesigner, …) into the
9
+ * unified Setup-app shell without rewriting them.
10
+ *
11
+ * Each designer takes a different prop shape, so we accept a
12
+ * `renderDesigner` callback that gets `(value, onChange, readOnly)`
13
+ * and returns whatever the designer needs.
14
+ *
15
+ * Wiring is dead simple — see `builtinComponents.tsx`:
16
+ *
17
+ * registerMetadataResource({
18
+ * type: 'view',
19
+ * EditPage: (props) => (
20
+ * <DesignerEditorWrapper
21
+ * {...props}
22
+ * renderDesigner={(value, onChange, readOnly) => (
23
+ * <ObjectViewConfigurator config={value} onChange={onChange} readOnly={readOnly} />
24
+ * )}
25
+ * />
26
+ * ),
27
+ * });
28
+ *
29
+ * The wrapper handles:
30
+ * • Initial fetch via `client.layered()` (so admins see overlay vs code).
31
+ * • Local edit state with Save / Revert.
32
+ * • Destructive-change confirmation (409 → dialog → retry with force).
33
+ * • Reset overlay button (DELETE).
34
+ * • Read-only fallback when the type isn't allowed to override at runtime
35
+ * (driven by /meta/types#allowOrgOverride).
36
+ *
37
+ * Validation errors (422) are surfaced as an error banner; bespoke
38
+ * designers usually handle their own field-level UX.
39
+ */
40
+ import * as React from 'react';
41
+ import { useNavigate } from 'react-router-dom';
42
+ import { Save, Loader2, RotateCcw, History as HistoryIcon, AlertTriangle, RefreshCw, } from 'lucide-react';
43
+ import { Button } from '@object-ui/components';
44
+ import { Badge } from '@object-ui/components';
45
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@object-ui/components';
46
+ import { PageShell } from './PageShell';
47
+ import { useMetadataClient, useMetadataTypes } from './useMetadata';
48
+ import { resolveResourceConfig } from './registry';
49
+ import { t, detectLocale } from './i18n';
50
+ export function DesignerEditorWrapper(props) {
51
+ return _jsx(DesignerEditorBody, { ...props, withChrome: true });
52
+ }
53
+ /**
54
+ * Embedded variant — same state machine, but no surrounding `PageShell`.
55
+ * Used by `ResourceEditPage` to host the designer inside a tab alongside
56
+ * the generic Form / Layers / References tabs. The action toolbar (Save /
57
+ * Reset / Refresh) is rendered inline at the top of the panel so the tab
58
+ * remains self-sufficient.
59
+ */
60
+ export function DesignerEditorBody({ type, name, renderDesigner, fromMetadata, toMetadata, withChrome = false, }) {
61
+ const navigate = useNavigate();
62
+ const client = useMetadataClient();
63
+ const { entries } = useMetadataTypes(client);
64
+ const entry = entries.find((t) => t.type === type);
65
+ const resolved = resolveResourceConfig(type, entry);
66
+ const writable = !!resolved.allowOrgOverride;
67
+ const locale = detectLocale();
68
+ const [value, setValue] = React.useState(null);
69
+ const [original, setOriginal] = React.useState(null);
70
+ const [loading, setLoading] = React.useState(true);
71
+ const [saving, setSaving] = React.useState(false);
72
+ const [error, setError] = React.useState(null);
73
+ const [destructive, setDestructive] = React.useState(null);
74
+ const dirty = React.useMemo(() => {
75
+ try {
76
+ return JSON.stringify(value) !== JSON.stringify(original);
77
+ }
78
+ catch {
79
+ return true;
80
+ }
81
+ }, [value, original]);
82
+ const load = React.useCallback(async () => {
83
+ setLoading(true);
84
+ setError(null);
85
+ try {
86
+ const lay = await client.layered(type, name);
87
+ const raw = (lay.effective ?? lay.code ?? {});
88
+ const v = (fromMetadata ? fromMetadata(raw) : raw);
89
+ setValue(v);
90
+ setOriginal(v);
91
+ }
92
+ catch (err) {
93
+ setError(err?.message ?? String(err));
94
+ }
95
+ finally {
96
+ setLoading(false);
97
+ }
98
+ }, [client, type, name, fromMetadata]);
99
+ React.useEffect(() => {
100
+ void load();
101
+ }, [load]);
102
+ async function doSave(force, pending) {
103
+ const payload = pending ?? value;
104
+ if (payload == null)
105
+ return;
106
+ setSaving(true);
107
+ setError(null);
108
+ try {
109
+ const body = toMetadata ? toMetadata(payload) : payload;
110
+ // Ensure `name` is set — designers may not carry it.
111
+ const finalBody = body && typeof body === 'object'
112
+ ? { ...body, name: body.name ?? name }
113
+ : body;
114
+ await client.save(type, name, finalBody, { force });
115
+ await load();
116
+ setDestructive(null);
117
+ }
118
+ catch (err) {
119
+ if (err?.status === 409 && err?.code === 'destructive_change') {
120
+ const issues = err?.body?.issues ?? [];
121
+ setDestructive({ issues: Array.isArray(issues) ? issues : [], pending: payload });
122
+ }
123
+ else {
124
+ setError(err?.message ?? String(err));
125
+ }
126
+ }
127
+ finally {
128
+ setSaving(false);
129
+ }
130
+ }
131
+ async function doReset() {
132
+ if (!confirm(t('engine.edit.resetConfirm', locale).replace('{type}', type).replace('{name}', name)))
133
+ return;
134
+ setSaving(true);
135
+ try {
136
+ await client.reset(type, name);
137
+ await load();
138
+ }
139
+ catch (err) {
140
+ setError(err?.message ?? String(err));
141
+ }
142
+ finally {
143
+ setSaving(false);
144
+ }
145
+ }
146
+ if (loading || value == null) {
147
+ const loadingBody = (_jsxs("div", { className: "p-6 text-sm text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), " ", t('engine.edit.loading', locale), " ", type, "/", name, "\u2026"] }));
148
+ return withChrome ? (_jsx(PageShell, { entry: entry, itemName: name, children: loadingBody })) : (loadingBody);
149
+ }
150
+ const toolbarButtons = (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: load, disabled: saving, children: [_jsx(RefreshCw, { className: "h-4 w-4 mr-1" }), " ", t('engine.list.refresh', locale)] }), withChrome && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`./history`), children: [_jsx(HistoryIcon, { className: "h-4 w-4 mr-1" }), " ", t('engine.edit.history', locale)] })), writable && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: doReset, disabled: saving, children: [_jsx(RotateCcw, { className: "h-4 w-4 mr-1" }), " ", t('engine.edit.reset', locale)] })), writable && (_jsxs(Button, { size: "sm", onClick: () => doSave(false), disabled: saving || !dirty, children: [saving ? (_jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" })) : (_jsx(Save, { className: "h-4 w-4 mr-1" })), t('engine.edit.save', locale)] }))] }));
151
+ const banners = (_jsxs(_Fragment, { children: [!writable && (_jsx("div", { className: "mx-6 mt-4 rounded-md border bg-muted/40 px-3 py-2 text-xs text-muted-foreground", children: t('engine.edit.readOnlyHint', locale) })), error && (_jsxs("div", { className: "mx-6 mt-4 rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive flex items-start gap-2", children: [_jsx(AlertTriangle, { className: "h-4 w-4 mt-0.5 shrink-0" }), _jsx("span", { children: error })] })), dirty && writable && (_jsxs("div", { className: "mx-6 mt-3 flex items-center gap-2 rounded-md border border-amber-300/50 bg-amber-50 dark:bg-amber-950/30 px-3 py-1.5 text-xs", children: [_jsx(Badge, { variant: "outline", children: t('engine.edit.unsaved', locale) }), _jsx("span", { children: t('engine.edit.unsavedHint', locale) })] }))] }));
152
+ const designerArea = (_jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: renderDesigner(value, (next) => setValue(next), !writable) }));
153
+ const destructiveDialog = (_jsx(Dialog, { open: !!destructive, onOpenChange: (open) => !open && setDestructive(null), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(AlertTriangle, { className: "h-4 w-4 text-amber-500" }), " ", t('engine.edit.destructive', locale)] }), _jsx(DialogDescription, { children: t('engine.edit.destructiveHint', locale) })] }), _jsx("ul", { className: "text-sm space-y-1 max-h-64 overflow-auto", children: destructive?.issues.map((i, idx) => (_jsxs("li", { className: "border-l-2 border-amber-500 pl-2", children: [i.kind && _jsx(Badge, { variant: "outline", className: "mr-2", children: i.kind }), i.message ?? JSON.stringify(i)] }, idx))) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", onClick: () => setDestructive(null), children: t('engine.cancel', locale) }), _jsxs(Button, { variant: "destructive", onClick: () => destructive && doSave(true, destructive.pending), disabled: saving, children: [saving && _jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" }), t('engine.edit.forceSave', locale)] })] })] }) }));
154
+ if (!withChrome) {
155
+ return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden", children: [_jsx("div", { className: "flex items-center justify-end gap-1 px-6 pt-3", children: toolbarButtons }), banners, designerArea, destructiveDialog] }));
156
+ }
157
+ return (_jsxs(PageShell, { entry: entry ?? { type, label: type }, itemName: name, subtitle: t('engine.edit.bespokeDesigner', locale), actions: toolbarButtons, children: [_jsxs("div", { className: "flex flex-col h-full overflow-hidden", children: [banners, designerArea] }), destructiveDialog] }));
158
+ }
@@ -0,0 +1 @@
1
+ export declare function MetadataDirectoryPage(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,135 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3
+ /**
4
+ * MetadataDirectoryPage — registry-driven landing page (Phase 3c).
5
+ *
6
+ * Replaces the Phase 3b placeholder. Improvements:
7
+ * • Search across type id + label + description.
8
+ * • Domain filter chips with counts.
9
+ * • "Writable only" toggle for admins triaging permissions work.
10
+ * • Tiles show: label, machine name, item count (lazy fetched per
11
+ * type when in view), badges (writable / overlay), description.
12
+ *
13
+ * Item counts are lazy — we only fetch when the user lands on a
14
+ * domain that has it. For MVP we just show "—" until the user clicks
15
+ * into a type. Pre-fetching all 27 counts on page load is wasteful.
16
+ */
17
+ import * as React from 'react';
18
+ import { Link } from 'react-router-dom';
19
+ import { Search, Database, Layers, Workflow, Sparkles, Settings, ShieldCheck, Box } from 'lucide-react';
20
+ import { Input } from '@object-ui/components';
21
+ import { Button } from '@object-ui/components';
22
+ import { Badge } from '@object-ui/components';
23
+ import { Kbd } from '@object-ui/components';
24
+ import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
25
+ import { useMetadataClient, useMetadataTypes, } from './useMetadata';
26
+ import { MetadataQuickFind } from './QuickFind';
27
+ import { translateMetadataType, translateMetadataDomain, t, tFormat, detectLocale, } from './i18n';
28
+ const DOMAIN_ICONS = {
29
+ data: Database,
30
+ ui: Layers,
31
+ automation: Workflow,
32
+ ai: Sparkles,
33
+ system: Settings,
34
+ platform: Settings,
35
+ identity: ShieldCheck,
36
+ security: ShieldCheck,
37
+ };
38
+ const DOMAIN_ORDER = [
39
+ 'data',
40
+ 'ui',
41
+ 'automation',
42
+ 'ai',
43
+ 'identity',
44
+ 'security',
45
+ 'system',
46
+ 'platform',
47
+ 'other',
48
+ ];
49
+ /**
50
+ * Types intentionally hidden from the directory + list views.
51
+ *
52
+ * `field` is managed in-context via the parent object's edit form
53
+ * (master-detail widget). A flat global list of every field across
54
+ * every object is rarely useful and clutters the admin surface.
55
+ */
56
+ const HIDDEN_TYPES = new Set(['field']);
57
+ export function MetadataDirectoryPage() {
58
+ const client = useMetadataClient();
59
+ const { loading, error, entries } = useMetadataTypes(client);
60
+ const locale = React.useMemo(() => detectLocale(), []);
61
+ const [query, setQuery] = React.useState('');
62
+ const [domainFilter, setDomainFilter] = React.useState('all');
63
+ const [writableOnly, setWritableOnly] = React.useState(false);
64
+ // Counts per domain for the filter chip bar.
65
+ const domainCounts = React.useMemo(() => {
66
+ const visible = entries.filter((e) => !HIDDEN_TYPES.has(e.type));
67
+ const c = { all: visible.length };
68
+ for (const e of visible) {
69
+ const d = e.domain ?? 'other';
70
+ c[d] = (c[d] ?? 0) + 1;
71
+ }
72
+ return c;
73
+ }, [entries]);
74
+ const writableCount = entries.filter((e) => !HIDDEN_TYPES.has(e.type) && e.allowOrgOverride).length;
75
+ const filtered = entries.filter((e) => {
76
+ if (HIDDEN_TYPES.has(e.type))
77
+ return false;
78
+ if (writableOnly && !e.allowOrgOverride)
79
+ return false;
80
+ if (domainFilter !== 'all' && (e.domain ?? 'other') !== domainFilter)
81
+ return false;
82
+ if (query) {
83
+ const q = query.toLowerCase();
84
+ const hit = e.type.toLowerCase().includes(q) ||
85
+ (e.label ?? '').toLowerCase().includes(q) ||
86
+ (e.description ?? '').toLowerCase().includes(q);
87
+ if (!hit)
88
+ return false;
89
+ }
90
+ return true;
91
+ });
92
+ // Group filtered by domain for display.
93
+ const grouped = React.useMemo(() => {
94
+ var _a;
95
+ const map = {};
96
+ for (const e of filtered)
97
+ (map[_a = e.domain ?? 'other'] ?? (map[_a] = [])).push(e);
98
+ return Object.entries(map).sort(([a], [b]) => {
99
+ const ai = DOMAIN_ORDER.indexOf(a);
100
+ const bi = DOMAIN_ORDER.indexOf(b);
101
+ return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
102
+ });
103
+ }, [filtered]);
104
+ if (loading) {
105
+ return _jsx("div", { className: "p-6 text-sm text-muted-foreground", children: t('engine.directory.loading', locale) });
106
+ }
107
+ if (error) {
108
+ return (_jsxs("div", { className: "p-6 text-sm text-destructive", children: [t('engine.directory.loadFailed', locale), ": ", error] }));
109
+ }
110
+ return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden", children: [_jsxs("div", { className: "px-6 pt-5 pb-4 border-b bg-background", children: [_jsx("h1", { className: "text-xl font-semibold", children: t('engine.directory.title', locale) }), _jsx("p", { className: "text-sm text-muted-foreground mt-1 max-w-3xl", dangerouslySetInnerHTML: {
111
+ __html: tFormat('engine.directory.description', locale, {
112
+ count: `<strong class="text-foreground">${entries.filter((e) => !HIDDEN_TYPES.has(e.type)).length}</strong>`,
113
+ writable: writableCount,
114
+ }),
115
+ } }), _jsxs("div", { className: "flex items-center gap-3 mt-4 flex-wrap", children: [_jsxs("div", { className: "relative flex-1 min-w-[240px] max-w-lg", children: [_jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" }), _jsx(Input, { className: "pl-8", placeholder: t('engine.directory.search', locale), value: query, onChange: (e) => setQuery(e.target.value) })] }), _jsxs(Button, { variant: writableOnly ? 'default' : 'outline', size: "sm", onClick: () => setWritableOnly((w) => !w), children: [t('engine.directory.writableOnly', locale), " (", writableCount, ")"] }), _jsxs("div", { className: "text-xs text-muted-foreground flex items-center gap-1.5", children: [t('engine.directory.quickFind', locale), " ", _jsx(Kbd, { children: "\u2318" }), _jsx(Kbd, { children: "\u21E7" }), _jsx(Kbd, { children: "M" })] })] }), _jsxs("div", { className: "flex items-center gap-1.5 mt-3 flex-wrap", children: [_jsx(DomainChip, { domain: "all", label: t('engine.directory.all', locale), active: domainFilter === 'all', count: domainCounts.all ?? 0, onClick: () => setDomainFilter('all') }), DOMAIN_ORDER.filter((d) => domainCounts[d]).map((d) => (_jsx(DomainChip, { domain: d, label: translateMetadataDomain(d, locale), active: domainFilter === d, count: domainCounts[d] ?? 0, onClick: () => setDomainFilter(d) }, d)))] })] }), _jsxs("div", { className: "flex-1 overflow-auto p-6 space-y-6", children: [filtered.length === 0 && (_jsxs(Empty, { children: [_jsx(EmptyTitle, { children: t('engine.directory.noMatches', locale) }), _jsx(EmptyDescription, { children: t('engine.directory.noMatchesHint', locale) })] })), grouped.map(([domain, group]) => {
116
+ const Icon = DOMAIN_ICONS[domain] ?? Box;
117
+ return (_jsxs("section", { className: "space-y-2", children: [_jsxs("h2", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5", children: [_jsx(Icon, { className: "h-3.5 w-3.5" }), translateMetadataDomain(domain, locale), " (", group.length, ")"] }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3", children: group.map((e) => (_jsx(TypeTile, { entry: e, locale: locale }, e.type))) })] }, domain));
118
+ })] }), _jsx(MetadataQuickFind, {})] }));
119
+ }
120
+ function DomainChip({ domain, label, active, count, onClick, }) {
121
+ return (_jsxs("button", { type: "button", onClick: onClick, className: 'px-2.5 py-1 rounded-full text-xs border transition-colors ' +
122
+ (active
123
+ ? 'bg-primary text-primary-foreground border-primary'
124
+ : 'bg-background hover:bg-accent border-border text-muted-foreground'), children: [label ?? domain, " ", _jsxs("span", { className: "opacity-70 ml-0.5", children: ["(", count, ")"] })] }));
125
+ }
126
+ function TypeTile({ entry, locale }) {
127
+ // Prefer the locale-table translation; fall back to server's `label` (typically English).
128
+ const label = translateMetadataType(entry.type, locale, entry.label);
129
+ return (_jsxs(Link, { to: `./${encodeURIComponent(entry.type)}`, className: "block p-4 border rounded-lg hover:border-primary hover:bg-accent transition-colors", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "font-medium truncate", children: label }), _jsx("code", { className: "text-xs text-muted-foreground font-mono", children: entry.type })] }), entry.allowOrgOverride ? (_jsx(Badge, { className: 'text-[10px] shrink-0 ' +
130
+ (entry.overrideSource === 'env'
131
+ ? 'bg-amber-100 text-amber-800 hover:bg-amber-100'
132
+ : 'bg-emerald-100 text-emerald-800 hover:bg-emerald-100'), title: entry.overrideSource === 'env'
133
+ ? 'Writable via OBJECTSTACK_METADATA_WRITABLE env var'
134
+ : 'Writable per ADR-0005 overlay opt-in', children: "writable" })) : (_jsx(Badge, { variant: "outline", className: "text-[10px] shrink-0 text-muted-foreground", children: "read-only" }))] }), entry.description && (_jsx("div", { className: "text-xs text-muted-foreground mt-2 line-clamp-2", children: entry.description }))] }));
135
+ }
@@ -0,0 +1,6 @@
1
+ import type { MetadataLayered } from '@object-ui/data-objectstack';
2
+ export interface LayeredDiffProps {
3
+ layered: MetadataLayered<Record<string, unknown>> | null;
4
+ loading?: boolean;
5
+ }
6
+ export declare function LayeredDiff({ layered, loading }: LayeredDiffProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Tabs, TabsContent, TabsList, TabsTrigger, } from '@object-ui/components';
3
+ import { Badge } from '@object-ui/components';
4
+ export function LayeredDiff({ layered, loading }) {
5
+ if (loading) {
6
+ return (_jsx("div", { className: "p-4 text-sm text-muted-foreground", children: "Loading layers\u2026" }));
7
+ }
8
+ if (!layered) {
9
+ return (_jsx("div", { className: "p-4 text-sm text-muted-foreground", children: "Layered view unavailable for this item." }));
10
+ }
11
+ const hasOverlay = layered.overlay != null;
12
+ return (_jsxs(Tabs, { defaultValue: "effective", className: "w-full", children: [_jsxs(TabsList, { className: "grid grid-cols-3 w-fit", children: [_jsxs(TabsTrigger, { value: "code", children: ["Code", _jsx(Badge, { variant: "outline", className: "ml-1.5 text-[10px]", children: "artifact" })] }), _jsxs(TabsTrigger, { value: "overlay", children: ["Overlay", hasOverlay ? (_jsx(Badge, { className: "ml-1.5 text-[10px] bg-emerald-600 text-emerald-50", children: layered.overlayScope ?? 'set' })) : (_jsx(Badge, { variant: "outline", className: "ml-1.5 text-[10px] text-muted-foreground", children: "none" }))] }), _jsxs(TabsTrigger, { value: "effective", children: ["Effective", _jsx(Badge, { variant: "outline", className: "ml-1.5 text-[10px]", children: "merged" })] })] }), _jsx(TabsContent, { value: "code", className: "mt-3", children: _jsx(LayerPanel, { payload: layered.code, emptyHint: "No code-level artifact for this item. It may have been created at runtime as a pure overlay." }) }), _jsx(TabsContent, { value: "overlay", className: "mt-3", children: _jsx(LayerPanel, { payload: layered.overlay, emptyHint: "No overlay set. The runtime is using the code-level value as-is." }) }), _jsx(TabsContent, { value: "effective", className: "mt-3", children: _jsx(LayerPanel, { payload: layered.effective, emptyHint: "No effective value resolved." }) })] }));
13
+ }
14
+ function LayerPanel({ payload, emptyHint, }) {
15
+ if (payload == null) {
16
+ return (_jsx("div", { className: "rounded border bg-muted/30 p-4 text-xs text-muted-foreground", children: emptyHint }));
17
+ }
18
+ let pretty;
19
+ try {
20
+ pretty = JSON.stringify(payload, null, 2);
21
+ }
22
+ catch {
23
+ pretty = String(payload);
24
+ }
25
+ return (_jsx("pre", { className: "rounded border bg-muted/30 p-3 text-xs font-mono overflow-auto max-h-[420px]", children: pretty }));
26
+ }
@@ -0,0 +1,13 @@
1
+ import type { RelatedTarget } from './RelatedPanel';
2
+ export interface MetadataDetailDrawerProps {
3
+ /** When non-null, drawer is open and shows this target. */
4
+ target: RelatedTarget | null;
5
+ /** Called when the drawer requests close (overlay click, esc, close btn). */
6
+ onClose: () => void;
7
+ /** Optional context: parent's type / name, shown in the title. */
8
+ parentContext?: {
9
+ type: string;
10
+ name: string;
11
+ };
12
+ }
13
+ export declare function MetadataDetailDrawer({ target, onClose, parentContext, }: MetadataDetailDrawerProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3
+ /**
4
+ * MetadataDetailDrawer — slide-over editor for a related metadata item.
5
+ *
6
+ * Opens from the parent's Related tab without taking the user away
7
+ * from the parent's context. Internally we mount the same
8
+ * `MetadataResourceEditPage` used by the full-page route, so all the
9
+ * Save / Reset / Validate behaviour is shared. The drawer just frames
10
+ * it and adds a "Open full page ↗" affordance.
11
+ *
12
+ * Width is wide enough for forms (max 1100px) but capped at 92vw to
13
+ * leave a thin strip of the parent visible behind, reinforcing the
14
+ * "still in the same object" feel.
15
+ */
16
+ import * as React from 'react';
17
+ import { useNavigate } from 'react-router-dom';
18
+ import { ExternalLink } from 'lucide-react';
19
+ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, Button, Badge, cn, } from '@object-ui/components';
20
+ import { MetadataResourceEditPage } from './ResourceEditPage';
21
+ export function MetadataDetailDrawer({ target, onClose, parentContext, }) {
22
+ const navigate = useNavigate();
23
+ const isMetadata = target?.kind === 'metadata';
24
+ const isEmbedded = target?.kind === 'embedded';
25
+ const headerType = isMetadata
26
+ ? target.type
27
+ : isEmbedded
28
+ ? target.groupLabel
29
+ : '';
30
+ const headerName = isMetadata
31
+ ? target.name
32
+ : isEmbedded
33
+ ? target.itemName
34
+ : '';
35
+ return (_jsx(Sheet, { open: target !== null, onOpenChange: (open) => {
36
+ if (!open)
37
+ onClose();
38
+ }, children: _jsxs(SheetContent, { side: "right", className: cn('w-[92vw] sm:max-w-[1100px] p-0 flex flex-col gap-0'), children: [_jsxs(SheetHeader, { className: "px-4 py-3 border-b space-y-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Badge, { variant: "outline", className: "font-mono text-[10px]", children: headerType }), _jsx(SheetTitle, { className: "font-mono text-base truncate", children: headerName }), _jsx("div", { className: "ml-auto flex items-center gap-1", children: isMetadata && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => {
39
+ navigate(`../../${encodeURIComponent(target.type)}/${encodeURIComponent(target.name)}`);
40
+ onClose();
41
+ }, title: "Open in full page", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5 mr-1" }), "Open full page"] })) })] }), parentContext && (_jsxs(SheetDescription, { className: "text-xs", children: [isEmbedded ? 'Embedded in ' : 'Related to ', _jsxs("span", { className: "font-mono", children: [parentContext.type, "/", parentContext.name] })] }))] }), _jsxs("div", { className: "flex-1 min-h-0 overflow-auto", children: [isMetadata && (_jsx(MetadataResourceEditPage, { type: target.type, name: target.name, embedded: true }, `${target.type}/${target.name}`)), isEmbedded && _jsx(EmbeddedItemView, { raw: target.raw })] })] }) }));
42
+ }
43
+ /**
44
+ * Read-only JSON preview for embedded items (fields, indexes, embedded
45
+ * validations). Editing happens via the parent's Form tab; jumping
46
+ * straight to that field is a future enhancement — for now the user
47
+ * can inspect the spec here and click "Edit in Form tab" in the panel.
48
+ */
49
+ function EmbeddedItemView({ raw }) {
50
+ const json = React.useMemo(() => JSON.stringify(raw, null, 2), [raw]);
51
+ return (_jsxs("div", { className: "p-4 space-y-3", children: [_jsxs("div", { className: "text-xs text-muted-foreground", children: ["This item lives inside its parent's body. Edit it in the parent's", ' ', _jsx("span", { className: "font-medium", children: "Form" }), " tab."] }), _jsx("pre", { className: "text-xs font-mono bg-muted/40 border rounded p-3 overflow-auto whitespace-pre-wrap break-all", children: json })] }));
52
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * PageShell — common hero / breadcrumb / action bar for every metadata
3
+ * admin page (Phase 3c).
4
+ *
5
+ * Layout (top → bottom):
6
+ * 1. Breadcrumb: All Metadata Types › <type> [ › <item> ]
7
+ * 2. Hero: icon + label + writable badge + count chip
8
+ * 3. Description (one line)
9
+ * 4. Action toolbar (Create / Refresh / Reset / Delete)
10
+ * 5. Child content (list / form / history)
11
+ *
12
+ * Keeping the chrome here means every page looks consistent and
13
+ * page bodies stay focused on their domain logic.
14
+ */
15
+ import * as React from 'react';
16
+ import type { RichMetadataTypeEntry } from './useMetadata';
17
+ export interface PageShellProps {
18
+ /** The type entry from `/meta/types` (or a synthesized stub). */
19
+ entry: RichMetadataTypeEntry | undefined;
20
+ /** Optional item name (shown in breadcrumb on edit/history). */
21
+ itemName?: string;
22
+ /** Sub-label below the title — e.g. "Edit overlay" / "Version history". */
23
+ subtitle?: string;
24
+ /** Right-side stat chips. */
25
+ stats?: Array<{
26
+ label: string;
27
+ value: React.ReactNode;
28
+ }>;
29
+ /** Right-side action buttons. */
30
+ actions?: React.ReactNode;
31
+ /** Page body. */
32
+ children: React.ReactNode;
33
+ }
34
+ export declare function PageShell({ entry, itemName, subtitle, stats, actions, children, }: PageShellProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3
+ /**
4
+ * PageShell — common hero / breadcrumb / action bar for every metadata
5
+ * admin page (Phase 3c).
6
+ *
7
+ * Layout (top → bottom):
8
+ * 1. Breadcrumb: All Metadata Types › <type> [ › <item> ]
9
+ * 2. Hero: icon + label + writable badge + count chip
10
+ * 3. Description (one line)
11
+ * 4. Action toolbar (Create / Refresh / Reset / Delete)
12
+ * 5. Child content (list / form / history)
13
+ *
14
+ * Keeping the chrome here means every page looks consistent and
15
+ * page bodies stay focused on their domain logic.
16
+ */
17
+ import * as React from 'react';
18
+ import { Link, useLocation } from 'react-router-dom';
19
+ import { Badge } from '@object-ui/components';
20
+ import { ChevronRight } from 'lucide-react';
21
+ import { detectLocale, t, translateMetadataType } from './i18n';
22
+ export function PageShell({ entry, itemName, subtitle, stats, actions, children, }) {
23
+ const type = entry?.type ?? '';
24
+ const locale = React.useMemo(() => detectLocale(), []);
25
+ // Prefer locale-table translation over server's English label.
26
+ const label = translateMetadataType(type, locale, entry?.label);
27
+ // Compute base path up to /metadata so breadcrumb links work regardless
28
+ // of how deep the current route is (list, edit, history, …).
29
+ const { pathname } = useLocation();
30
+ const metadataBase = React.useMemo(() => {
31
+ const idx = pathname.indexOf('/metadata');
32
+ return idx >= 0 ? pathname.slice(0, idx + '/metadata'.length) : '/metadata';
33
+ }, [pathname]);
34
+ return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden", children: [_jsxs("div", { className: "px-6 pt-5 pb-4 border-b bg-background", children: [_jsxs("nav", { className: "flex items-center gap-1.5 text-xs text-muted-foreground mb-2", children: [_jsx(Link, { to: metadataBase, className: "hover:text-foreground", children: t('engine.breadcrumb.allTypes', locale) }), _jsx(ChevronRight, { className: "h-3 w-3" }), _jsx(Link, { to: `${metadataBase}/${encodeURIComponent(type)}`, className: "hover:text-foreground", children: label }), itemName && (_jsxs(_Fragment, { children: [_jsx(ChevronRight, { className: "h-3 w-3" }), _jsx("span", { className: "text-foreground font-medium font-mono", children: itemName })] }))] }), _jsxs("div", { className: "flex items-start justify-between gap-3 flex-wrap", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("h1", { className: "text-xl font-semibold truncate", children: label }), _jsx("code", { className: "text-xs font-mono text-muted-foreground", children: type }), entry?.domain && (_jsx(Badge, { variant: "outline", className: "text-[10px] uppercase tracking-wider", children: entry.domain })), entry?.allowOrgOverride ? (_jsx(Badge, { className: 'text-[10px] ' +
35
+ (entry.overrideSource === 'env'
36
+ ? 'bg-amber-100 text-amber-800 hover:bg-amber-100'
37
+ : 'bg-emerald-100 text-emerald-800 hover:bg-emerald-100'), title: entry.overrideSource === 'env'
38
+ ? 'Writable via OBJECTSTACK_METADATA_WRITABLE env var'
39
+ : 'Writable per ADR-0005 overlay opt-in', children: "writable" })) : (_jsx(Badge, { variant: "outline", className: "text-[10px] text-muted-foreground", children: "read-only" }))] }), subtitle && (_jsx("div", { className: "text-sm text-muted-foreground mt-1", children: subtitle })), !subtitle && entry?.description && (_jsx("div", { className: "text-sm text-muted-foreground mt-1 max-w-3xl", children: entry.description }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [stats?.map((s, i) => (_jsxs("div", { className: "flex flex-col items-end px-3 py-1 rounded border bg-muted/30 min-w-[64px]", children: [_jsx("span", { className: "text-[10px] uppercase tracking-wider text-muted-foreground", children: s.label }), _jsx("span", { className: "text-sm font-semibold tabular-nums", children: s.value })] }, i))), actions] })] })] }), _jsx("div", { className: "flex-1 overflow-auto", children: children })] }));
40
+ }
@@ -0,0 +1,5 @@
1
+ export interface PermissionMatrixEditPageProps {
2
+ type: string;
3
+ name: string;
4
+ }
5
+ export declare function PermissionMatrixEditPage({ type, name }: PermissionMatrixEditPageProps): import("react/jsx-runtime").JSX.Element;