@object-ui/app-shell 11.2.0 → 11.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,45 @@
1
1
  # @object-ui/app-shell — Changelog
2
2
 
3
+ ## 11.3.0
4
+
5
+ ### Patch Changes
6
+
7
+ - ca4a795: fix(app-shell): restore admin design surface gated on the removed `user.role='admin'` overwrite
8
+
9
+ ADR-0068 (a3a5abff8) stopped the server `customSession` from overwriting
10
+ `user.role = 'admin'` for workspace owners/admins — canonical roles now arrive
11
+ in `user.roles[]` (`org_owner` / `org_admin`) with `user.isPlatformAdmin` as a
12
+ derived alias, and `useIsWorkspaceAdmin()` was introduced to read them. Four
13
+ runtime views were missed in that migration and still gated their admin design
14
+ tools on the now-defunct `user?.role === 'admin'`, so workspace owners/admins
15
+ silently lost:
16
+
17
+ - **ObjectView** — the list "+ New view" button plus rename/delete/pin/
18
+ set-default/config/manage-views and the view config panel.
19
+ - **PageView / DashboardView / ReportView** — the inline "Edit"/config entry
20
+ points for the shared page / dashboard / report definitions.
21
+
22
+ All four now call `useIsWorkspaceAdmin()` (same helper already adopted by
23
+ AppSidebar, UnifiedSidebar, HomePage, Marketplace…). No behavior change for
24
+ genuine platform admins; restores the surface for org owners/admins.
25
+
26
+ - Updated dependencies [d88c8ec]
27
+ - Updated dependencies [b7237bb]
28
+ - Updated dependencies [d23d6eb]
29
+ - @object-ui/components@11.3.0
30
+ - @object-ui/i18n@11.3.0
31
+ - @object-ui/core@11.3.0
32
+ - @object-ui/fields@11.3.0
33
+ - @object-ui/layout@11.3.0
34
+ - @object-ui/plugin-editor@11.3.0
35
+ - @object-ui/react@11.3.0
36
+ - @object-ui/data-objectstack@11.3.0
37
+ - @object-ui/types@11.3.0
38
+ - @object-ui/auth@11.3.0
39
+ - @object-ui/permissions@11.3.0
40
+ - @object-ui/collaboration@11.3.0
41
+ - @object-ui/providers@11.3.0
42
+
3
43
  ## 11.2.0
4
44
 
5
45
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -61,6 +61,7 @@ import './console/marketplace/InstalledListWidget';
61
61
  import './console/home/CloudOnboardingNext';
62
62
  export { MetadataDirectoryPage, MetadataResourceRouter, MetadataResourceListPage, MetadataResourceEditPage, MetadataResourceHistoryPage, MetadataDiagnosticsPage, MetadataQuickFind, MetadataPageShell, SchemaForm, LayeredDiff, registerMetadataResource, getMetadataResource, listMetadataResources, resolveResourceConfig, useMetadataClient, useMetadataTypes, useTypesIndex, useGlobalDiagnostics, matchesQuery, registerMetadataPreview, getMetadataPreview, listMetadataPreviewTypes, registerMetadataInspector, getMetadataInspector, listMetadataInspectorTypes, } from './views/metadata-admin';
63
63
  export type { MetadataResourceConfig, MetadataDomain, RichMetadataTypeEntry, MetadataPreview, MetadataPreviewProps, MetadataSelection, MetadataInspector, MetadataInspectorProps, } from './views/metadata-admin';
64
+ export { StudioDesignSurface, type StudioDesignSurfaceProps, } from './views/studio-design/StudioDesignSurface';
64
65
  export { assistantBus, useAssistant, useRegisterAssistantEditor, requestAssistantOpen, } from './assistant/assistantBus';
65
66
  export type { AssistantSnapshot, AssistantEditorContext, AssistantEditorField, } from './assistant/assistantBus';
66
67
  export { RemediationOverlay } from './console/RemediationOverlay';
package/dist/index.js CHANGED
@@ -79,6 +79,9 @@ import './console/home/CloudOnboardingNext';
79
79
  // list / edit / create components, and host apps can compose the
80
80
  // page primitives directly when needed.
81
81
  export { MetadataDirectoryPage, MetadataResourceRouter, MetadataResourceListPage, MetadataResourceEditPage, MetadataResourceHistoryPage, MetadataDiagnosticsPage, MetadataQuickFind, MetadataPageShell, SchemaForm, LayeredDiff, registerMetadataResource, getMetadataResource, listMetadataResources, resolveResourceConfig, useMetadataClient, useMetadataTypes, useTypesIndex, useGlobalDiagnostics, matchesQuery, registerMetadataPreview, getMetadataPreview, listMetadataPreviewTypes, registerMetadataInspector, getMetadataInspector, listMetadataInspectorTypes, } from './views/metadata-admin';
82
+ // Studio WYSIWYG design surface (ADR-0080) — the open-source design surface.
83
+ // The left AI copilot is an injected `aiSlot`; OSS renders three zones.
84
+ export { StudioDesignSurface, } from './views/studio-design/StudioDesignSurface';
82
85
  // AI assistant bus — connects the metadata designers to the global chat.
83
86
  export { assistantBus, useAssistant, useRegisterAssistantEditor, requestAssistantOpen, } from './assistant/assistantBus';
84
87
  export { RemediationOverlay } from './console/RemediationOverlay';
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Utility functions for ObjectStack Console
3
3
  */
4
+ export { getRecordDisplayName, deriveTitleField, isTitleEligibleField, formatTitleTemplate as formatRecordTitle, } from '@object-ui/core';
5
+ export type { RecordDisplayNameOptions } from '@object-ui/core';
4
6
  export { resolveRecordFormTarget, resolveFormViewLayout, } from './recordFormNavigation';
5
7
  export type { ObjectDefinitionForNavigation, RecordFormTarget, ObjectDefinitionForFormView, FormViewDefinition, FormViewModalLayout, } from './recordFormNavigation';
6
8
  export { deriveRelatedLists } from './deriveRelatedLists';
@@ -23,27 +25,3 @@ export declare function resolveI18nLabel(label: string | {
23
25
  * Preferred over CSS `capitalize` for i18n compatibility.
24
26
  */
25
27
  export declare function capitalizeFirst(str: string): string;
26
- /**
27
- * Format a record title using the titleFormat pattern.
28
- *
29
- * Accepts either a legacy string template or an Expression envelope
30
- * (`{ dialect: 'template', source: string }`) emitted by `@objectstack/spec`'s
31
- * normalized templates. The placeholder syntax (`{field}`) is identical in both
32
- * shapes; only the wrapping object is new.
33
- *
34
- * Empty placeholders (missing or null/empty fields) are stripped along with
35
- * any orphan separator they leave behind, so a template like
36
- * "{full_name} - {company}"
37
- * evaluated against `{ company: "Acme" }` resolves to `"Acme"` rather than
38
- * `" - Acme"`. Returns an empty string when no placeholder resolved.
39
- */
40
- export declare function formatRecordTitle(titleFormat: string | {
41
- source?: string;
42
- } | undefined, record: any): string;
43
- /**
44
- * Get display name for a record using titleFormat or fallback
45
- * @param objectDef Object definition with optional titleFormat
46
- * @param record The record data
47
- * @returns Display name for the record
48
- */
49
- export declare function getRecordDisplayName(objectDef: any, record: any): string;
@@ -1,6 +1,14 @@
1
1
  /**
2
2
  * Utility functions for ObjectStack Console
3
3
  */
4
+ // Re-export the unified record display-name resolver (ADR-0079) so existing
5
+ // importers of `@object-ui/app-shell`'s `getRecordDisplayName` /
6
+ // `formatRecordTitle` keep working unchanged. The implementation now lives in
7
+ // `@object-ui/core` (pure util, shared by every view plugin and field widget).
8
+ export { getRecordDisplayName, deriveTitleField, isTitleEligibleField,
9
+ // `formatTitleTemplate` is the new canonical name; alias it to the legacy
10
+ // `formatRecordTitle` export this module has always provided.
11
+ formatTitleTemplate as formatRecordTitle, } from '@object-ui/core';
4
12
  export { resolveRecordFormTarget, resolveFormViewLayout, } from './recordFormNavigation';
5
13
  export { deriveRelatedLists } from './deriveRelatedLists';
6
14
  export { preferLocal } from './preferLocal';
@@ -32,104 +40,9 @@ export function capitalizeFirst(str) {
32
40
  return str;
33
41
  return str.charAt(0).toUpperCase() + str.slice(1);
34
42
  }
35
- // Sentinel used to mark empty-placeholder positions inside formatRecordTitle
36
- // so adjacent separators can be stripped in a second pass.
37
- const EMPTY_TOKEN = '\u0000';
38
- // Separator characters commonly placed between {fields} in titleFormat patterns
39
- // (hyphen, em/en dashes, pipes, slashes, middle dot, comma, colon).
40
- const SEPARATOR_CLASS = '[-\\u2013\\u2014|/·,:]';
41
- /**
42
- * Format a record title using the titleFormat pattern.
43
- *
44
- * Accepts either a legacy string template or an Expression envelope
45
- * (`{ dialect: 'template', source: string }`) emitted by `@objectstack/spec`'s
46
- * normalized templates. The placeholder syntax (`{field}`) is identical in both
47
- * shapes; only the wrapping object is new.
48
- *
49
- * Empty placeholders (missing or null/empty fields) are stripped along with
50
- * any orphan separator they leave behind, so a template like
51
- * "{full_name} - {company}"
52
- * evaluated against `{ company: "Acme" }` resolves to `"Acme"` rather than
53
- * `" - Acme"`. Returns an empty string when no placeholder resolved.
54
- */
55
- export function formatRecordTitle(titleFormat, record) {
56
- // Normalize Expression envelope ({ dialect, source }) → raw template string.
57
- const template = typeof titleFormat === 'string'
58
- ? titleFormat
59
- : (titleFormat && typeof titleFormat === 'object' && typeof titleFormat.source === 'string')
60
- ? titleFormat.source
61
- : undefined;
62
- if (!template || !record) {
63
- return record?.id || record?._id || 'Record';
64
- }
65
- let anyResolved = false;
66
- let out = template.replace(/\{([^{}]+)\}/g, (_match, fieldName) => {
67
- // Support dotted paths (e.g. `{account.name}`) for $expanded lookups.
68
- const parts = String(fieldName).trim().split('.');
69
- let value = record;
70
- for (const p of parts) {
71
- if (value == null)
72
- break;
73
- value = value[p];
74
- }
75
- // Auto-extract display name from expanded reference objects, with a
76
- // Salesforce-style fallback chain.
77
- if (value && typeof value === 'object') {
78
- const o = value;
79
- let display = o.name ?? o.full_name ?? o.display_name ?? o.label ?? o.title ?? o.subject ?? null;
80
- if (display == null || (typeof display === 'string' && !display.trim())) {
81
- const composite = [o.salutation, o.first_name, o.last_name]
82
- .filter((p) => typeof p === 'string' && p.trim())
83
- .map((p) => p.trim())
84
- .join(' ');
85
- if (composite)
86
- display = composite;
87
- else if (typeof o.email === 'string' && o.email.trim())
88
- display = o.email.trim();
89
- else
90
- display = null;
91
- }
92
- value = display;
93
- }
94
- if (value === null || value === undefined || value === '') {
95
- return EMPTY_TOKEN;
96
- }
97
- anyResolved = true;
98
- return String(value);
99
- });
100
- if (!anyResolved)
101
- return '';
102
- // Drop separators on either side of an empty token, then any leftover
103
- // tokens, then collapse runs of whitespace.
104
- const sepBefore = new RegExp(`\\s*${SEPARATOR_CLASS}\\s*${EMPTY_TOKEN}`, 'g');
105
- const sepAfter = new RegExp(`${EMPTY_TOKEN}\\s*${SEPARATOR_CLASS}\\s*`, 'g');
106
- out = out
107
- .replace(sepBefore, '')
108
- .replace(sepAfter, '')
109
- .replace(new RegExp(EMPTY_TOKEN, 'g'), '')
110
- .replace(/\s+/g, ' ')
111
- .trim();
112
- return out;
113
- }
114
- /**
115
- * Get display name for a record using titleFormat or fallback
116
- * @param objectDef Object definition with optional titleFormat
117
- * @param record The record data
118
- * @returns Display name for the record
119
- */
120
- export function getRecordDisplayName(objectDef, record) {
121
- if (objectDef?.titleFormat) {
122
- const formatted = formatRecordTitle(objectDef.titleFormat, record);
123
- if (formatted)
124
- return formatted;
125
- }
126
- return (record?.name ||
127
- record?.full_name ||
128
- record?.fullName ||
129
- record?.title ||
130
- record?.label ||
131
- record?.subject ||
132
- record?.id ||
133
- record?._id ||
134
- 'Untitled');
135
- }
43
+ // NOTE (ADR-0079): `formatRecordTitle` (now canonically `formatTitleTemplate`)
44
+ // and `getRecordDisplayName` moved to `@object-ui/core` and are re-exported
45
+ // from the top of this module. The previous local copies — a titleFormat-only
46
+ // resolver that fell back to a hard-coded `name`/`title`/… list and bottomed
47
+ // out at the literal 'Untitled' are gone, so every surface now also honors
48
+ // the object's `displayNameField` + type-aware derivation + `Record #<id>`.
@@ -13,7 +13,7 @@ import { DrillNavigationProvider } from '@object-ui/react';
13
13
  import { useOpenRecordList } from './useOpenRecordList';
14
14
  import { ModalForm } from '@object-ui/plugin-form';
15
15
  import { DashboardConfigPanel } from './DashboardConfigPanel';
16
- import { useAuth } from '@object-ui/auth';
16
+ import { useIsWorkspaceAdmin } from '@object-ui/auth';
17
17
  import { toast } from 'sonner';
18
18
  import { Empty, EmptyTitle, EmptyDescription, Button, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '@object-ui/components';
19
19
  import { LayoutDashboard, Pencil, TrendingUp, BarChart3, LineChart, PieChart, Table2, LayoutGrid, Plus, } from 'lucide-react';
@@ -77,8 +77,7 @@ export function DashboardView({ dataSource }) {
77
77
  const { dashboardLabel, dashboardDescription } = useObjectLabel();
78
78
  // Editing a dashboard mutates the SHARED definition, so it is an admin-only
79
79
  // quick-edit affordance (mirrors ObjectView's view-config gate).
80
- const { user } = useAuth();
81
- const isAdmin = user?.role === 'admin';
80
+ const isAdmin = useIsWorkspaceAdmin();
82
81
  const [isLoading, setIsLoading] = useState(true);
83
82
  const [configPanelOpen, setConfigPanelOpen] = useState(false);
84
83
  const [selectedWidgetId, setSelectedWidgetId] = useState(null);
@@ -319,7 +319,10 @@ export function InterfaceListPage({ page, className, onConfigChange, reserveEdit
319
319
  showGroup: false,
320
320
  showColor: false,
321
321
  allowExport: false,
322
- inlineEdit: false,
322
+ // Inline record editing is a page-authored property: a list block opts in
323
+ // via `userActions.editInline` (default off). When on, clicking a cell
324
+ // edits it with the dedicated field widgets, same as the object views.
325
+ inlineEdit: userActions.editInline === true,
323
326
  };
324
327
  // eslint-disable-next-line react-hooks/exhaustive-deps
325
328
  }, [objectDefName, viewDefJson, cfg]);
@@ -37,7 +37,7 @@ import { resolveManagedByEmptyState } from '../utils/managedByEmptyState';
37
37
  import { useObjectActions } from '../hooks/useObjectActions';
38
38
  import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
39
39
  import { usePermissions } from '@object-ui/permissions';
40
- import { useAuth } from '@object-ui/auth';
40
+ import { useAuth, useIsWorkspaceAdmin } from '@object-ui/auth';
41
41
  import { useRealtimeSubscription, useConflictResolution } from '@object-ui/collaboration';
42
42
  import { ActionProvider, useNavigationOverlay, SchemaRenderer } from '@object-ui/react';
43
43
  import { toast } from 'sonner';
@@ -253,7 +253,7 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }) {
253
253
  const [recordCount, setRecordCount] = useState(undefined);
254
254
  // Admin users automatically get design tools (no toggle needed)
255
255
  const { user, activeOrganization } = useAuth();
256
- const isAdmin = user?.role === 'admin';
256
+ const isAdmin = useIsWorkspaceAdmin();
257
257
  const { can } = usePermissions();
258
258
  // Get Object Definition. The outer ObjectView wrapper already guards the
259
259
  // missing-object case, so this always resolves while this component is
@@ -1302,6 +1302,8 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }) {
1302
1302
  persistViewPatch(viewDef.id, viewDef, { filter });
1303
1303
  }, onHiddenFieldsChange: (hidden) => {
1304
1304
  persistViewPatch(viewDef.id, viewDef, { hiddenFields: hidden });
1305
+ }, onInlineEditChange: (next) => {
1306
+ persistViewPatch(viewDef.id, viewDef, { inlineEdit: next });
1305
1307
  }, onColumnStateChange: (state) => {
1306
1308
  persistViewPatch(viewDef.id, viewDef, { columnState: state });
1307
1309
  }, userFilterSelections: initialUfSelections, onUserFilterSelectionsChange: handleUserFilterSelectionsChange, dataSource: ds }, key));
@@ -15,7 +15,7 @@ import { SchemaRenderer, useAdapter } from '@object-ui/react';
15
15
  import { Empty, EmptyTitle, EmptyDescription, Spinner } from '@object-ui/components';
16
16
  import { FileText, Pencil } from 'lucide-react';
17
17
  import { useObjectTranslation } from '@object-ui/i18n';
18
- import { useAuth } from '@object-ui/auth';
18
+ import { useIsWorkspaceAdmin } from '@object-ui/auth';
19
19
  import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
20
20
  import { useMetadata } from '../providers/MetadataProvider';
21
21
  import { useExpressionContext } from '../providers/ExpressionProvider';
@@ -31,8 +31,7 @@ export function PageView() {
31
31
  const location = useLocation();
32
32
  // Editing a page mutates the shared metadata definition, so the entry point
33
33
  // is admin-only (mirrors the view/report/dashboard runtime editors).
34
- const { user } = useAuth();
35
- const isAdmin = user?.role === 'admin';
34
+ const isAdmin = useIsWorkspaceAdmin();
36
35
  const { pages, objects, getTypeStatus } = useMetadata();
37
36
  // ADR-0048 Phase 2 — prefer the page owned by the current app's package so
38
37
  // two packages shipping `page/<same-name>` each resolve within their own
@@ -16,7 +16,7 @@ import { preferLocal } from '../utils/preferLocal';
16
16
  import { useAdapter } from '../providers/AdapterProvider';
17
17
  import { useMetadataClient } from './metadata-admin/useMetadata';
18
18
  import { persistRuntimeMetadata } from './runtime-metadata-persistence';
19
- import { useAuth } from '@object-ui/auth';
19
+ import { useIsWorkspaceAdmin } from '@object-ui/auth';
20
20
  import { DrillDownDrawer } from '@object-ui/plugin-dashboard';
21
21
  import { DrillNavigationProvider } from '@object-ui/react';
22
22
  import { useOpenRecordList } from './useOpenRecordList';
@@ -40,8 +40,7 @@ export function ReportView({ dataSource }) {
40
40
  const metadataClient = useMetadataClient();
41
41
  // Editing a report mutates the SHARED definition, so it is an admin-only
42
42
  // quick-edit affordance (mirrors ObjectView's view-config gate).
43
- const { user } = useAuth();
44
- const isAdmin = user?.role === 'admin';
43
+ const isAdmin = useIsWorkspaceAdmin();
45
44
  const [configPanelOpen, setConfigPanelOpen] = useState(false);
46
45
  // Version counter — incremented on save to refresh the stable config reference
47
46
  const [configVersion, setConfigVersion] = useState(0);
@@ -6,7 +6,12 @@
6
6
  // Types still falling through to server-only validation:
7
7
  // - `validation`: not a top-level metadata file; lives inside object. (DataValidationRuleSchema
8
8
  // exists but has empty shape, so it's not useful for client validation.)
9
- // - `profile`: spec ships no top-level ProfileSchema (7.1 confirmed).
9
+ // - `policy`: spec 11.2.0 (PR #2078) removed the generic `PolicySchema` (the org-wide
10
+ // password/network/session/audit policy) from `@objectstack/spec/security`, and the
11
+ // canonical metadata-type→schema registry (spec kernel/metadata-type-schemas.ts) has
12
+ // no `policy` entry — so there is no client schema. `RowLevelSecurityPolicySchema`
13
+ // remains on /security but is a different shape (a per-object RLS rule), NOT the
14
+ // `policy` metadata file, so it must not be substituted.
10
15
  // - `trigger`: no standalone TriggerSchema export at runtime (only
11
16
  // ConnectorTriggerSchema / WebhookEventSchema variants).
12
17
  // - `sharing_rule`: SharingRuleSchema is declared but has empty shape — server-only.
@@ -52,7 +57,8 @@ const LOADERS = {
52
57
  // packages/spec/src/kernel/metadata-type-schemas.ts for the canonical mapping.
53
58
  permission: async () => (await import('@objectstack/spec/security')).PermissionSetSchema,
54
59
  profile: async () => (await import('@objectstack/spec/security')).PermissionSetSchema,
55
- policy: async () => (await import('@objectstack/spec/security')).PolicySchema,
60
+ // `policy` intentionally omitted spec 11.2.0 dropped `PolicySchema` and the metadata-type
61
+ // registry has no `policy` schema; drafts fall through to server-side validation (see top).
56
62
  // identity
57
63
  role: async () => (await import('@objectstack/spec/identity')).RoleSchema,
58
64
  // api
@@ -1,14 +1,3 @@
1
- /**
2
- * Shared color-variant swatch picker.
3
- *
4
- * Metadata color fields (`colorVariant` on metrics / dashboard widgets, badge
5
- * tones, …) are a fixed SEMANTIC palette, not arbitrary hex. A row of colored
6
- * swatches is far more scannable than a text dropdown — the admin sees the
7
- * actual color, like Linear/Notion label pickers. Reused by the generic
8
- * SchemaForm `color-picker` widget and the curated inspectors so every color
9
- * field looks and behaves the same.
10
- */
11
- import * as React from 'react';
12
1
  export interface ColorVariant {
13
2
  value: string;
14
3
  label: string;
@@ -27,4 +16,4 @@ export declare function ColorVariantPicker({ value, onChange, disabled, options
27
16
  value: string;
28
17
  label?: string;
29
18
  }>;
30
- }): React.JSX.Element;
19
+ }): import("react").JSX.Element;
@@ -1,4 +1,15 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3
+ /**
4
+ * Shared color-variant swatch picker.
5
+ *
6
+ * Metadata color fields (`colorVariant` on metrics / dashboard widgets, badge
7
+ * tones, …) are a fixed SEMANTIC palette, not arbitrary hex. A row of colored
8
+ * swatches is far more scannable than a text dropdown — the admin sees the
9
+ * actual color, like Linear/Notion label pickers. Reused by the generic
10
+ * SchemaForm `color-picker` widget and the curated inspectors so every color
11
+ * field looks and behaves the same.
12
+ */
2
13
  import { cn } from '@object-ui/components';
3
14
  /** Canonical semantic palette (mirrors the renderer's colorVariant tokens). */
4
15
  export const COLOR_VARIANTS = [
@@ -1,15 +1,3 @@
1
- /**
2
- * OutlineStrip — clickable chip strip that lets users select
3
- * sub-elements inside a preview whose main canvas renders through a
4
- * sealed external renderer (SchemaRenderer, ReportRenderer, …) and
5
- * therefore can't intercept clicks directly.
6
- *
7
- * The strip sits above the canvas. Each chip emits a selection on
8
- * click; the currently-selected one gets a ring. Empty list collapses
9
- * the strip entirely so the canvas takes the full preview height
10
- * outside design mode.
11
- */
12
- import * as React from 'react';
13
1
  export interface OutlineEntry {
14
2
  /** Selection id to emit. */
15
3
  id: string;
@@ -29,4 +17,4 @@ export declare function OutlineStrip({ title, entries, selectedId, onSelect, onA
29
17
  onAdd?: () => void;
30
18
  /** Tooltip / aria-label for the Add chip. Defaults to "Add". */
31
19
  addLabel?: string;
32
- }): React.JSX.Element | null;
20
+ }): import("react").JSX.Element | null;
@@ -1,4 +1,16 @@
1
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
+ * OutlineStrip — clickable chip strip that lets users select
5
+ * sub-elements inside a preview whose main canvas renders through a
6
+ * sealed external renderer (SchemaRenderer, ReportRenderer, …) and
7
+ * therefore can't intercept clicks directly.
8
+ *
9
+ * The strip sits above the canvas. Each chip emits a selection on
10
+ * click; the currently-selected one gets a ring. Empty list collapses
11
+ * the strip entirely so the canvas takes the full preview height
12
+ * outside design mode.
13
+ */
2
14
  import { Plus } from 'lucide-react';
3
15
  import { cn } from '@object-ui/components';
4
16
  export function OutlineStrip({ title, entries, selectedId, onSelect, onAdd, addLabel, }) {
@@ -14,6 +14,7 @@ import { buildExpandFields } from '@object-ui/core';
14
14
  import { buildDefaultPageSchema } from '@object-ui/plugin-detail';
15
15
  import { PreviewShell, PreviewErrorBoundary, PreviewMessage } from './PreviewShell';
16
16
  import { OutlineStrip } from './OutlineStrip';
17
+ import { SourcePageEditor } from './SourcePageEditor';
17
18
  import { PageBlockCanvas } from './PageBlockCanvas';
18
19
  import { InterfaceListPage } from '../../InterfaceListPage';
19
20
  import { t as tr } from '../i18n';
@@ -205,6 +206,14 @@ export function PagePreview({ draft, editing, selection, onSelectionChange, onPa
205
206
  if (isInterfacePage) {
206
207
  return (_jsx(PreviewShell, { hint: "page \u00B7 interface", children: _jsx(PreviewErrorBoundary, { fallbackHint: "The interface page references a source object/view that isn't available.", children: _jsx(InterfaceListPage, { page: draft, onConfigChange: canEdit ? (patch) => onPatch({ interfaceConfig: { ...((draft.interfaceConfig) || {}), ...patch } }) : undefined }) }) }));
207
208
  }
209
+ // Source page (ADR-0080/0081) → `kind:'html'`/`'react'` pages are a `source`
210
+ // string, not a region tree. The design canvas does not apply (and would
211
+ // choke on the absent regions/children), so edit the source directly with a
212
+ // live preview. This is the fix for the editor crashing on these pages.
213
+ const pageKind = draft.kind;
214
+ if (pageKind === 'react' || pageKind === 'html') {
215
+ return (_jsx(SourcePageEditor, { draft: draft, onPatch: canEdit ? onPatch : undefined, readOnly: !canEdit }));
216
+ }
208
217
  // Empty draft → no preview; but if we're in design mode show the
209
218
  // canvas so users can author from scratch.
210
219
  if (!schema || Object.keys(schema).length <= 1) {
@@ -0,0 +1,28 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ *
8
+ * SourcePageEditor — the Studio editor surface for `kind:'html'` and
9
+ * `kind:'react'` pages (ADR-0080/0081). These pages ARE a `source` string
10
+ * (JSX/HTML or real React), not a region tree — so the structured design
11
+ * canvas does not apply (and would choke on the missing `regions`/`children`).
12
+ * Instead we present a code editor for the `source` field beside a live preview
13
+ * rendered through the runtime SchemaRenderer. Edits patch `draft.source`.
14
+ *
15
+ * Mirrors JsonSourceEditor's Monaco + textarea-fallback + theme handling, but
16
+ * edits ONE field (the source) instead of the whole-record JSON, with a
17
+ * JSX/TSX language mode.
18
+ */
19
+ import * as React from 'react';
20
+ export interface SourcePageEditorProps {
21
+ draft: Record<string, unknown>;
22
+ /** Patch the draft; undefined in read-only mode. */
23
+ onPatch?: (patch: Record<string, unknown>) => void;
24
+ readOnly?: boolean;
25
+ fallbackDelayMs?: number;
26
+ }
27
+ export declare function SourcePageEditor({ draft, onPatch, readOnly, fallbackDelayMs }: SourcePageEditorProps): React.JSX.Element;
28
+ export default SourcePageEditor;
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ObjectUI
4
+ * Copyright (c) 2024-present ObjectStack Inc.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ *
9
+ * SourcePageEditor — the Studio editor surface for `kind:'html'` and
10
+ * `kind:'react'` pages (ADR-0080/0081). These pages ARE a `source` string
11
+ * (JSX/HTML or real React), not a region tree — so the structured design
12
+ * canvas does not apply (and would choke on the missing `regions`/`children`).
13
+ * Instead we present a code editor for the `source` field beside a live preview
14
+ * rendered through the runtime SchemaRenderer. Edits patch `draft.source`.
15
+ *
16
+ * Mirrors JsonSourceEditor's Monaco + textarea-fallback + theme handling, but
17
+ * edits ONE field (the source) instead of the whole-record JSON, with a
18
+ * JSX/TSX language mode.
19
+ */
20
+ import * as React from 'react';
21
+ import { Skeleton } from '@object-ui/components';
22
+ import { SchemaRenderer } from '@object-ui/react';
23
+ import { PreviewShell, PreviewErrorBoundary } from './PreviewShell';
24
+ const LazyMonaco = React.lazy(async () => {
25
+ const mod = await import('@monaco-editor/react');
26
+ return { default: mod.default };
27
+ });
28
+ export function SourcePageEditor({ draft, onPatch, readOnly, fallbackDelayMs = 4000 }) {
29
+ const kind = draft.kind === 'react' ? 'react' : 'html';
30
+ const source = typeof draft.source === 'string' ? draft.source : '';
31
+ const [text, setText] = React.useState(source);
32
+ const lastCommittedRef = React.useRef(source);
33
+ const containerRef = React.useRef(null);
34
+ // Sync from upstream (Reset / inspector edits) without clobbering keystrokes.
35
+ React.useEffect(() => {
36
+ if (source !== lastCommittedRef.current) {
37
+ setText(source);
38
+ lastCommittedRef.current = source;
39
+ }
40
+ }, [source]);
41
+ // Monaco-unavailable fallback (headless / CSP) → plain textarea.
42
+ const [monacoUnavailable, setMonacoUnavailable] = React.useState(false);
43
+ React.useEffect(() => {
44
+ if (monacoUnavailable)
45
+ return;
46
+ const id = setTimeout(() => {
47
+ const el = containerRef.current;
48
+ if (!el || !el.querySelector('.view-line'))
49
+ setMonacoUnavailable(true);
50
+ }, fallbackDelayMs);
51
+ return () => clearTimeout(id);
52
+ }, [monacoUnavailable, fallbackDelayMs]);
53
+ const [theme, setTheme] = React.useState(() => typeof document !== 'undefined' && document.documentElement.classList.contains('dark') ? 'vs-dark' : 'light');
54
+ React.useEffect(() => {
55
+ if (typeof document === 'undefined')
56
+ return;
57
+ const root = document.documentElement;
58
+ const update = () => setTheme(root.classList.contains('dark') ? 'vs-dark' : 'light');
59
+ const obs = new MutationObserver(update);
60
+ obs.observe(root, { attributes: true, attributeFilter: ['class'] });
61
+ return () => obs.disconnect();
62
+ }, []);
63
+ const handleChange = (next) => {
64
+ const v = next ?? '';
65
+ setText(v);
66
+ lastCommittedRef.current = v;
67
+ onPatch?.({ source: v });
68
+ };
69
+ const previewSchema = React.useMemo(() => ({ ...draft, type: draft.type ?? 'page' }), [draft]);
70
+ return (_jsx(PreviewShell, { hint: `page · ${kind} source`, children: _jsxs("div", { className: "grid h-full grid-cols-1 divide-y divide-border lg:grid-cols-2 lg:divide-x lg:divide-y-0", children: [_jsx("div", { ref: containerRef, className: "h-full min-h-[260px] overflow-hidden bg-background", children: monacoUnavailable ? (_jsx("textarea", { value: text, onChange: (e) => handleChange(e.target.value), readOnly: readOnly, spellCheck: false, "aria-label": "Page source", className: "h-full w-full resize-none bg-background p-3 font-mono text-xs leading-relaxed outline-none" })) : (_jsx(React.Suspense, { fallback: _jsx(Skeleton, { className: "h-full w-full" }), children: _jsx(LazyMonaco, { value: text, language: "typescript", path: kind === 'react' ? 'page.tsx' : 'page.html.tsx', theme: theme, onChange: handleChange, options: {
71
+ readOnly,
72
+ minimap: { enabled: false },
73
+ fontSize: 12,
74
+ lineNumbers: 'on',
75
+ scrollBeyondLastLine: false,
76
+ automaticLayout: true,
77
+ folding: true,
78
+ wordWrap: 'on',
79
+ tabSize: 2,
80
+ scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10 },
81
+ } }) })) }), _jsx("div", { className: "h-full min-h-[260px] overflow-auto bg-muted/20", children: _jsx(PreviewErrorBoundary, { fallbackHint: "The page source threw while rendering \u2014 fix the code on the left.", children: _jsx(SchemaRenderer, { schema: previewSchema }) }) })] }) }));
82
+ }
83
+ export default SourcePageEditor;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * StudioDesignSurface — the open-source WYSIWYG design surface (ADR-0080).
3
+ *
4
+ * Routed as /studio/:packageId/{data|automations|interfaces} — three pillars,
5
+ * each composed AROUND existing renderers (no new editor code):
6
+ * - Interfaces: the real App navigation tree → live canvas (getMetadataPreview)
7
+ * + inspector (getMetadataInspector), edits persisting via draft → publish.
8
+ * - Data: the package's objects → fields + record grid.
9
+ * - Automations: flows → FlowPreview (default OFF / review-then-enable).
10
+ *
11
+ * Open-core boundary: the left AI copilot is NOT part of the open-source
12
+ * surface — it is an injected slot (`aiSlot`) the cloud edition fills.
13
+ */
14
+ import * as React from 'react';
15
+ export interface StudioDesignSurfaceProps {
16
+ /** Open-core slot — the cloud edition injects its AI copilot panel here. */
17
+ aiSlot?: React.ReactNode;
18
+ }
19
+ export declare function StudioDesignSurface({ aiSlot }: StudioDesignSurfaceProps): React.ReactElement;
20
+ export default StudioDesignSurface;