@object-ui/app-shell 11.2.0 → 11.4.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +562 -0
  2. package/README.md +23 -0
  3. package/dist/console/ConsoleShell.js +17 -2
  4. package/dist/console/home/CloudOnboardingNext.d.ts +9 -0
  5. package/dist/console/home/CloudOnboardingNext.js +14 -4
  6. package/dist/console/home/HomePage.js +34 -7
  7. package/dist/console/organizations/CreateWorkspaceDialog.js +33 -3
  8. package/dist/console/organizations/OrganizationsPage.js +16 -7
  9. package/dist/hooks/useConsoleActionRuntime.js +32 -3
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +6 -0
  12. package/dist/preview/DraftChangesPanel.d.ts +3 -1
  13. package/dist/preview/DraftChangesPanel.js +6 -5
  14. package/dist/utils/deriveRelatedLists.d.ts +20 -5
  15. package/dist/utils/deriveRelatedLists.js +31 -13
  16. package/dist/utils/index.d.ts +2 -24
  17. package/dist/utils/index.js +14 -101
  18. package/dist/utils/resolveViewId.d.ts +23 -0
  19. package/dist/utils/resolveViewId.js +37 -0
  20. package/dist/utils/warnSuppressedListNav.d.ts +10 -0
  21. package/dist/utils/warnSuppressedListNav.js +40 -0
  22. package/dist/views/DashboardView.js +2 -3
  23. package/dist/views/InterfaceListPage.js +10 -5
  24. package/dist/views/ObjectView.js +65 -12
  25. package/dist/views/PageView.js +2 -3
  26. package/dist/views/RecordDetailView.js +131 -104
  27. package/dist/views/RecordFormPage.js +7 -1
  28. package/dist/views/RelatedRecordActionsBridge.d.ts +24 -0
  29. package/dist/views/RelatedRecordActionsBridge.js +114 -0
  30. package/dist/views/ReportView.js +2 -3
  31. package/dist/views/metadata-admin/PackagesPage.js +18 -7
  32. package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +18 -1
  33. package/dist/views/metadata-admin/PermissionMatrixEditor.js +73 -14
  34. package/dist/views/metadata-admin/clientValidation.js +8 -2
  35. package/dist/views/metadata-admin/color-variant-field.d.ts +1 -12
  36. package/dist/views/metadata-admin/color-variant-field.js +11 -0
  37. package/dist/views/metadata-admin/i18n.d.ts +12 -21
  38. package/dist/views/metadata-admin/i18n.js +343 -2
  39. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +25 -11
  40. package/dist/views/metadata-admin/permission-slice.d.ts +66 -0
  41. package/dist/views/metadata-admin/permission-slice.js +70 -0
  42. package/dist/views/metadata-admin/previews/AppNavCanvas.js +11 -7
  43. package/dist/views/metadata-admin/previews/FlowRunsPanel.d.ts +16 -7
  44. package/dist/views/metadata-admin/previews/FlowRunsPanel.js +18 -2
  45. package/dist/views/metadata-admin/previews/OutlineStrip.d.ts +1 -13
  46. package/dist/views/metadata-admin/previews/OutlineStrip.js +12 -0
  47. package/dist/views/metadata-admin/previews/PagePreview.js +9 -0
  48. package/dist/views/metadata-admin/previews/SourcePageEditor.d.ts +28 -0
  49. package/dist/views/metadata-admin/previews/SourcePageEditor.js +83 -0
  50. package/dist/views/studio-design/BuilderLanding.d.ts +15 -0
  51. package/dist/views/studio-design/BuilderLanding.js +133 -0
  52. package/dist/views/studio-design/ObjectFormDesigner.d.ts +31 -0
  53. package/dist/views/studio-design/ObjectFormDesigner.js +226 -0
  54. package/dist/views/studio-design/ObjectSettingsPanel.d.ts +30 -0
  55. package/dist/views/studio-design/ObjectSettingsPanel.js +45 -0
  56. package/dist/views/studio-design/ObjectValidationsPanel.d.ts +30 -0
  57. package/dist/views/studio-design/ObjectValidationsPanel.js +78 -0
  58. package/dist/views/studio-design/StudioDesignSurface.d.ts +20 -0
  59. package/dist/views/studio-design/StudioDesignSurface.js +1306 -0
  60. package/dist/views/studio-design/metadataError.d.ts +23 -0
  61. package/dist/views/studio-design/metadataError.js +44 -0
  62. package/dist/views/studio-design/packages-io.d.ts +27 -0
  63. package/dist/views/studio-design/packages-io.js +61 -0
  64. package/package.json +46 -43
@@ -340,19 +340,33 @@ function useObjectOptions() {
340
340
  const [opts, setOpts] = React.useState([]);
341
341
  React.useEffect(() => {
342
342
  let cancelled = false;
343
- client
344
- .list('object')
345
- .then((items) => {
343
+ Promise.all([
344
+ client.list('object'),
345
+ // Draft objects are not yet published, so `list('object')` can't see
346
+ // them. Include them so a lookup can target a SIBLING object being
347
+ // designed in the same authoring pass (before the package's first
348
+ // publish) instead of forcing the author to type an API name blind.
349
+ client.listDrafts({ type: 'object' }).catch(() => []),
350
+ ])
351
+ .then(([published, drafts]) => {
346
352
  if (cancelled)
347
353
  return;
348
- const mapped = items
349
- .filter((i) => typeof i?.name === 'string' && i.name)
350
- .map((i) => ({
351
- value: i.name,
352
- label: i.label ? `${i.label} (${i.name})` : i.name,
353
- }))
354
- .sort((a, b) => a.value.localeCompare(b.value));
355
- setOpts(mapped);
354
+ const byName = new Map();
355
+ for (const i of published ?? []) {
356
+ if (typeof i?.name === 'string' && i.name && !byName.has(i.name)) {
357
+ byName.set(i.name, {
358
+ value: i.name,
359
+ label: i.label ? `${i.label} (${i.name})` : i.name,
360
+ });
361
+ }
362
+ }
363
+ for (const d of drafts ?? []) {
364
+ const name = d.name;
365
+ if (typeof name === 'string' && name && !byName.has(name)) {
366
+ byName.set(name, { value: name, label: `${name} (草稿)` });
367
+ }
368
+ }
369
+ setOpts([...byName.values()].sort((a, b) => a.value.localeCompare(b.value)));
356
370
  })
357
371
  .catch(() => {
358
372
  // Empty list — picker falls back to free-text. No banner needed.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Package-scoped slice merge for the permission matrix (ADR-0086 P0).
3
+ *
4
+ * A Permission Set / Profile is a single metadata record whose `objects` and
5
+ * `fields` maps accumulate authorization rows contributed by many packages.
6
+ * When the Access matrix is opened inside a package context it must:
7
+ *
8
+ * 1. show only the objects that package declares (scope), and
9
+ * 2. on Save write back ONLY that slice — leaving every row contributed by
10
+ * other packages byte-for-byte intact.
11
+ *
12
+ * Overwriting the whole record (the pre-P0 behavior) silently drops the rows
13
+ * other packages contributed. {@link mergePermissionSlice} rebuilds the record
14
+ * from a freshly-read base, keeping the set-level identity and every
15
+ * out-of-scope row, and overlaying only the in-scope rows the user edited.
16
+ */
17
+ export interface ObjectPerm {
18
+ allowCreate?: boolean;
19
+ allowRead?: boolean;
20
+ allowEdit?: boolean;
21
+ allowDelete?: boolean;
22
+ allowTransfer?: boolean;
23
+ allowRestore?: boolean;
24
+ allowPurge?: boolean;
25
+ viewAllRecords?: boolean;
26
+ modifyAllRecords?: boolean;
27
+ }
28
+ export interface FieldPerm {
29
+ readable?: boolean;
30
+ editable?: boolean;
31
+ }
32
+ export interface PermissionSetDraft {
33
+ name: string;
34
+ label?: string;
35
+ isProfile?: boolean;
36
+ objects: Record<string, ObjectPerm>;
37
+ fields?: Record<string, FieldPerm>;
38
+ systemPermissions?: string[];
39
+ tabPermissions?: Record<string, 'visible' | 'hidden' | 'default_on' | 'default_off'>;
40
+ [extra: string]: unknown;
41
+ }
42
+ /**
43
+ * Object name embedded in a `${object}.${field}` field-permission key. Object
44
+ * and field names are field-name-safe (snake_case, no dots), so the object is
45
+ * everything up to the first dot.
46
+ */
47
+ export declare function fieldKeyObject(key: string): string;
48
+ /**
49
+ * Narrow a permission set down to just the rows whose object is in `scope`.
50
+ * Used to drive the matrix display so a package panel lists only its own
51
+ * objects (and their field overrides), never the whole environment.
52
+ */
53
+ export declare function scopePermissionSet(set: Pick<PermissionSetDraft, 'objects' | 'fields'>, scope: Iterable<string>): {
54
+ objects: Record<string, ObjectPerm>;
55
+ fields: Record<string, FieldPerm>;
56
+ };
57
+ /**
58
+ * Merge the edited in-scope slice back onto a freshly-read full `base`.
59
+ *
60
+ * Out-of-scope rows (other packages' contributions) are copied verbatim from
61
+ * `base`; in-scope rows are taken entirely from `edited` (so removing a grant
62
+ * in the package panel deletes only that package's row). Set-level identity and
63
+ * any extra keys (systemPermissions, tabPermissions, …) come from `base`, with
64
+ * name / label / isProfile taking the user's edits.
65
+ */
66
+ export declare function mergePermissionSlice(base: PermissionSetDraft, edited: PermissionSetDraft, scope: Iterable<string>): PermissionSetDraft;
@@ -0,0 +1,70 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+ /**
3
+ * Object name embedded in a `${object}.${field}` field-permission key. Object
4
+ * and field names are field-name-safe (snake_case, no dots), so the object is
5
+ * everything up to the first dot.
6
+ */
7
+ export function fieldKeyObject(key) {
8
+ const dot = key.indexOf('.');
9
+ return dot === -1 ? key : key.slice(0, dot);
10
+ }
11
+ function asScopeSet(scope) {
12
+ return scope instanceof Set ? scope : new Set(scope);
13
+ }
14
+ /**
15
+ * Narrow a permission set down to just the rows whose object is in `scope`.
16
+ * Used to drive the matrix display so a package panel lists only its own
17
+ * objects (and their field overrides), never the whole environment.
18
+ */
19
+ export function scopePermissionSet(set, scope) {
20
+ const scopeSet = asScopeSet(scope);
21
+ const objects = {};
22
+ for (const [k, v] of Object.entries(set.objects ?? {})) {
23
+ if (scopeSet.has(k))
24
+ objects[k] = v;
25
+ }
26
+ const fields = {};
27
+ for (const [k, v] of Object.entries(set.fields ?? {})) {
28
+ if (scopeSet.has(fieldKeyObject(k)))
29
+ fields[k] = v;
30
+ }
31
+ return { objects, fields };
32
+ }
33
+ /**
34
+ * Merge the edited in-scope slice back onto a freshly-read full `base`.
35
+ *
36
+ * Out-of-scope rows (other packages' contributions) are copied verbatim from
37
+ * `base`; in-scope rows are taken entirely from `edited` (so removing a grant
38
+ * in the package panel deletes only that package's row). Set-level identity and
39
+ * any extra keys (systemPermissions, tabPermissions, …) come from `base`, with
40
+ * name / label / isProfile taking the user's edits.
41
+ */
42
+ export function mergePermissionSlice(base, edited, scope) {
43
+ const scopeSet = asScopeSet(scope);
44
+ const objects = {};
45
+ for (const [k, v] of Object.entries(base.objects ?? {})) {
46
+ if (!scopeSet.has(k))
47
+ objects[k] = v; // preserve other packages' rows
48
+ }
49
+ for (const [k, v] of Object.entries(edited.objects ?? {})) {
50
+ if (scopeSet.has(k))
51
+ objects[k] = v; // write this package's slice
52
+ }
53
+ const fields = {};
54
+ for (const [k, v] of Object.entries(base.fields ?? {})) {
55
+ if (!scopeSet.has(fieldKeyObject(k)))
56
+ fields[k] = v;
57
+ }
58
+ for (const [k, v] of Object.entries(edited.fields ?? {})) {
59
+ if (scopeSet.has(fieldKeyObject(k)))
60
+ fields[k] = v;
61
+ }
62
+ return {
63
+ ...base,
64
+ name: edited.name,
65
+ label: edited.label,
66
+ isProfile: edited.isProfile,
67
+ objects,
68
+ fields,
69
+ };
70
+ }
@@ -19,6 +19,7 @@ import * as React from 'react';
19
19
  import { BarChart3, Compass, Database, FileText, Folder, GripVertical, LayoutDashboard, Link as LinkIcon, Plus, Trash2, } from 'lucide-react';
20
20
  import { Badge, cn } from '@object-ui/components';
21
21
  import { appendArray, moveArray, spliceArray } from '../inspectors/_shared';
22
+ import { t, useMetadataLocale } from '../i18n';
22
23
  const DND_MIME = 'text/x-objectui-nav';
23
24
  function inferKind(it) {
24
25
  if (it.kind)
@@ -100,6 +101,7 @@ function navPath(it) {
100
101
  return typeof p === 'string' && p ? p : undefined;
101
102
  }
102
103
  export function AppNavCanvas({ draft, rootKey, onPatch, selection, onSelectionChange, }) {
104
+ const locale = useMetadataLocale();
103
105
  const items = React.useMemo(() => {
104
106
  const v = draft[rootKey];
105
107
  return Array.isArray(v) ? v : [];
@@ -114,15 +116,16 @@ export function AppNavCanvas({ draft, rootKey, onPatch, selection, onSelectionCh
114
116
  const addItem = React.useCallback(() => {
115
117
  if (!onPatch)
116
118
  return;
117
- const newItem = { label: 'New item', path: '' };
119
+ const newLabel = t('engine.appNav.newItem', locale);
120
+ const newItem = { label: newLabel, path: '' };
118
121
  const next = appendArray(items, newItem);
119
122
  setItems(next);
120
123
  onSelectionChange?.({
121
124
  kind: 'nav',
122
125
  id: `${rootKey}[${next.length - 1}]`,
123
- label: 'New item',
126
+ label: newLabel,
124
127
  });
125
- }, [onPatch, items, setItems, rootKey, onSelectionChange]);
128
+ }, [onPatch, items, setItems, rootKey, onSelectionChange, locale]);
126
129
  const removeItem = React.useCallback((index) => {
127
130
  if (!onPatch)
128
131
  return;
@@ -158,7 +161,7 @@ export function AppNavCanvas({ draft, rootKey, onPatch, selection, onSelectionCh
158
161
  label: navLabel(next[to] ?? {}, to),
159
162
  });
160
163
  }, [onPatch, items, setItems, rootKey, onSelectionChange]);
161
- return (_jsxs("div", { className: "rounded-md border bg-card/40", children: [_jsxs("div", { className: "flex items-center justify-between border-b px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs", children: [_jsx("span", { className: "font-mono uppercase tracking-wide text-muted-foreground", children: rootKey }), _jsxs(Badge, { variant: "outline", className: "text-[10px]", children: [items.length, " ", items.length === 1 ? 'item' : 'items'] })] }), onPatch && (_jsxs("button", { type: "button", className: "inline-flex items-center gap-1 rounded border border-dashed px-2 py-1 text-[11px] text-muted-foreground hover:bg-muted/30 hover:text-foreground", onClick: addItem, children: [_jsx(Plus, { className: "h-3 w-3" }), " Add nav item"] }))] }), _jsx("div", { className: "space-y-1.5 p-2", onDragOver: (e) => {
164
+ return (_jsxs("div", { className: "rounded-md border bg-card/40", children: [_jsxs("div", { className: "flex items-center justify-between border-b px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs", children: [_jsx("span", { className: "font-medium uppercase tracking-wide text-muted-foreground", children: t('engine.appNav.heading', locale) }), _jsxs(Badge, { variant: "outline", className: "text-[10px]", children: [items.length, " ", items.length === 1 ? t('engine.appNav.itemOne', locale) : t('engine.appNav.itemOther', locale)] })] }), onPatch && (_jsxs("button", { type: "button", className: "inline-flex items-center gap-1 rounded border border-dashed px-2 py-1 text-[11px] text-muted-foreground hover:bg-muted/30 hover:text-foreground", onClick: addItem, children: [_jsx(Plus, { className: "h-3 w-3" }), " ", t('engine.appNav.addItem', locale)] }))] }), _jsx("div", { className: "space-y-1.5 p-2", onDragOver: (e) => {
162
165
  if (!onPatch)
163
166
  return;
164
167
  if (!e.dataTransfer.types.includes(DND_MIME))
@@ -176,8 +179,8 @@ export function AppNavCanvas({ draft, rootKey, onPatch, selection, onSelectionCh
176
179
  moveItem(dragIndex, items.length);
177
180
  setDragIndex(null);
178
181
  }, children: items.length === 0 ? (_jsx("div", { className: "rounded border border-dashed px-3 py-4 text-center text-[11px] text-muted-foreground", children: onPatch
179
- ? 'Empty — click "Add nav item" to start'
180
- : 'No top-level nav items yet' })) : (items.map((it, i) => (_jsx(NavCardTree, { item: it, index: i, depth: 0, path: `${rootKey}[${i}]`, selectedId: selectedId, canEdit: !!onPatch, onClick: (p, lbl) => onSelectionChange?.({ kind: 'nav', id: p, label: lbl }), onRename: (lbl) => renameItem(i, lbl), onRemove: () => removeItem(i), onDragStart: () => setDragIndex(i), onDragEnd: () => setDragIndex(null), onDropBefore: () => {
182
+ ? t('engine.appNav.empty', locale)
183
+ : t('engine.appNav.emptyReadonly', locale) })) : (items.map((it, i) => (_jsx(NavCardTree, { item: it, index: i, depth: 0, path: `${rootKey}[${i}]`, selectedId: selectedId, canEdit: !!onPatch, onClick: (p, lbl) => onSelectionChange?.({ kind: 'nav', id: p, label: lbl }), onRename: (lbl) => renameItem(i, lbl), onRemove: () => removeItem(i), onDragStart: () => setDragIndex(i), onDragEnd: () => setDragIndex(null), onDropBefore: () => {
181
184
  if (dragIndex == null)
182
185
  return;
183
186
  moveItem(dragIndex, i);
@@ -192,6 +195,7 @@ function NavCardTree({ item, index, depth, path, selectedId, canEdit, onClick, o
192
195
  })] }));
193
196
  }
194
197
  function NavCard({ item, index, depth, path, isSelected, canEdit, onClick, onRename, onRemove, onDragStart, onDragEnd, onDropBefore, }) {
198
+ const locale = useMetadataLocale();
195
199
  const kind = inferKind(item);
196
200
  const Icon = kindIcon(kind);
197
201
  const tone = kindTone(kind);
@@ -256,5 +260,5 @@ function NavCard({ item, index, depth, path, isSelected, canEdit, onClick, onRen
256
260
  e.stopPropagation();
257
261
  onRemove();
258
262
  }
259
- }, className: "inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-destructive/10 hover:text-destructive", "aria-label": "Remove nav item", children: _jsx(Trash2, { className: "h-3 w-3" }) }))] })] }));
263
+ }, className: "inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:bg-destructive/10 hover:text-destructive", "aria-label": t('engine.appNav.removeItem', locale), children: _jsx(Trash2, { className: "h-3 w-3" }) }))] })] }));
260
264
  }
@@ -10,16 +10,20 @@
10
10
  * designer.
11
11
  */
12
12
  import * as React from 'react';
13
+ /** An error on a run/step. The engine sends the run-level `error` as a plain
14
+ * string (`ExecutionLog.error`) while a step-level error is a `{code,message}`
15
+ * object — the panel accepts either shape. */
16
+ type RunError = string | {
17
+ code?: string;
18
+ message?: string;
19
+ };
13
20
  /** Step entry of a run log (spec `ExecutionStepLogSchema`, fields we render). */
14
21
  interface RunStep {
15
22
  nodeId: string;
16
23
  nodeType?: string;
17
24
  status: 'success' | 'failure' | 'skipped' | string;
18
25
  durationMs?: number;
19
- error?: {
20
- code?: string;
21
- message?: string;
22
- };
26
+ error?: RunError;
23
27
  }
24
28
  /** Run log entry (spec `ExecutionLogSchema`, fields we render). */
25
29
  export interface FlowRun {
@@ -34,10 +38,15 @@ export interface FlowRun {
34
38
  object?: string;
35
39
  };
36
40
  steps?: RunStep[];
37
- error?: {
38
- message?: string;
39
- };
41
+ error?: RunError;
40
42
  }
43
+ /**
44
+ * Normalize a run/step error to its human-readable message. The engine emits a
45
+ * run-level `error` as a plain string but a step-level error as `{code,message}`
46
+ * — reading `.message` off the string case silently dropped the run failure
47
+ * reason (the whole point of the Runs panel for a failed run), so accept both.
48
+ */
49
+ export declare function errorText(e: RunError | undefined | null): string | undefined;
41
50
  /** Fetch a flow's run history. Exposed for tests. */
42
51
  export declare function fetchFlowRuns(flowName: string, signal?: AbortSignal): Promise<FlowRun[] | null>;
43
52
  export declare function FlowRunsPanel({ flowName }: {
@@ -15,6 +15,20 @@ import * as React from 'react';
15
15
  import { AlertCircle, CheckCircle2, ChevronDown, ChevronRight, Clock, Loader2, PauseCircle, RefreshCw, SkipForward } from 'lucide-react';
16
16
  import { cn } from '@object-ui/components';
17
17
  import { apiBase } from './useFlowNodePalette';
18
+ /**
19
+ * Normalize a run/step error to its human-readable message. The engine emits a
20
+ * run-level `error` as a plain string but a step-level error as `{code,message}`
21
+ * — reading `.message` off the string case silently dropped the run failure
22
+ * reason (the whole point of the Runs panel for a failed run), so accept both.
23
+ */
24
+ export function errorText(e) {
25
+ if (!e)
26
+ return undefined;
27
+ if (typeof e === 'string')
28
+ return e || undefined;
29
+ const m = e.message;
30
+ return typeof m === 'string' && m ? m : undefined;
31
+ }
18
32
  /** Fetch a flow's run history. Exposed for tests. */
19
33
  export async function fetchFlowRuns(flowName, signal) {
20
34
  try {
@@ -60,14 +74,16 @@ function StepRow({ step }) {
60
74
  : step.status === 'failure'
61
75
  ? 'text-rose-600 dark:text-rose-400'
62
76
  : 'text-muted-foreground';
63
- return (_jsxs("li", { className: "flex items-baseline gap-1.5 py-0.5", children: [_jsx("span", { className: cn('shrink-0 text-[9px] font-semibold uppercase', cls), children: step.status }), _jsx("span", { className: "truncate font-mono text-[10px]", title: step.nodeId, children: step.nodeId }), step.nodeType && _jsx("span", { className: "shrink-0 text-[9px] uppercase text-muted-foreground", children: step.nodeType }), fmtDuration(step.durationMs) && (_jsx("span", { className: "ml-auto shrink-0 text-[9px] text-muted-foreground", children: fmtDuration(step.durationMs) })), step.error?.message && (_jsx("span", { className: "min-w-0 truncate text-[9px] text-rose-600", title: step.error.message, children: step.error.message }))] }));
77
+ const stepErr = errorText(step.error);
78
+ return (_jsxs("li", { className: "flex items-baseline gap-1.5 py-0.5", children: [_jsx("span", { className: cn('shrink-0 text-[9px] font-semibold uppercase', cls), children: step.status }), _jsx("span", { className: "truncate font-mono text-[10px]", title: step.nodeId, children: step.nodeId }), step.nodeType && _jsx("span", { className: "shrink-0 text-[9px] uppercase text-muted-foreground", children: step.nodeType }), fmtDuration(step.durationMs) && (_jsx("span", { className: "ml-auto shrink-0 text-[9px] text-muted-foreground", children: fmtDuration(step.durationMs) })), stepErr && (_jsx("span", { className: "min-w-0 truncate text-[9px] text-rose-600", title: stepErr, children: stepErr }))] }));
64
79
  }
65
80
  function RunRow({ run }) {
66
81
  const [open, setOpen] = React.useState(false);
67
82
  const meta = statusMeta(run.status);
68
83
  const Icon = meta.icon;
69
84
  const steps = Array.isArray(run.steps) ? run.steps : [];
70
- return (_jsxs("li", { className: "rounded border bg-background", children: [_jsxs("button", { type: "button", onClick: () => setOpen((v) => !v), className: "flex w-full items-center gap-1.5 p-1.5 text-left", "aria-expanded": open, children: [open ? (_jsx(ChevronDown, { className: "h-3 w-3 shrink-0 text-muted-foreground" })) : (_jsx(ChevronRight, { className: "h-3 w-3 shrink-0 text-muted-foreground" })), _jsx(Icon, { className: cn('h-3.5 w-3.5 shrink-0', meta.cls, run.status === 'running' && 'animate-spin') }), _jsx("span", { className: cn('shrink-0 text-[10px] font-semibold', meta.cls), children: meta.label }), _jsx("span", { className: "min-w-0 truncate text-[10px] text-muted-foreground", title: run.id, children: fmtTime(run.startedAt) }), fmtDuration(run.durationMs) && (_jsx("span", { className: "ml-auto shrink-0 text-[10px] text-muted-foreground", children: fmtDuration(run.durationMs) }))] }), open && (_jsxs("div", { className: "border-t px-2 py-1.5", children: [_jsxs("div", { className: "pb-1 font-mono text-[9px] text-muted-foreground", title: run.id, children: ["run ", run.id, run.trigger?.type && ` · trigger ${run.trigger.type}`] }), run.error?.message && (_jsx("div", { className: "pb-1 text-[10px] text-rose-600", children: run.error.message })), steps.length === 0 ? (_jsx("div", { className: "text-[10px] italic text-muted-foreground", children: "No step log recorded." })) : (_jsx("ul", { className: "divide-y divide-border/50", children: steps.map((s, i) => (_jsx(StepRow, { step: s }, `${s.nodeId}#${i}`))) }))] }))] }));
85
+ const runErr = errorText(run.error);
86
+ return (_jsxs("li", { className: "rounded border bg-background", children: [_jsxs("button", { type: "button", onClick: () => setOpen((v) => !v), className: "flex w-full items-center gap-1.5 p-1.5 text-left", "aria-expanded": open, children: [open ? (_jsx(ChevronDown, { className: "h-3 w-3 shrink-0 text-muted-foreground" })) : (_jsx(ChevronRight, { className: "h-3 w-3 shrink-0 text-muted-foreground" })), _jsx(Icon, { className: cn('h-3.5 w-3.5 shrink-0', meta.cls, run.status === 'running' && 'animate-spin') }), _jsx("span", { className: cn('shrink-0 text-[10px] font-semibold', meta.cls), children: meta.label }), _jsx("span", { className: "min-w-0 truncate text-[10px] text-muted-foreground", title: run.id, children: fmtTime(run.startedAt) }), fmtDuration(run.durationMs) && (_jsx("span", { className: "ml-auto shrink-0 text-[10px] text-muted-foreground", children: fmtDuration(run.durationMs) }))] }), open && (_jsxs("div", { className: "border-t px-2 py-1.5", children: [_jsxs("div", { className: "pb-1 font-mono text-[9px] text-muted-foreground", title: run.id, children: ["run ", run.id, run.trigger?.type && ` · trigger ${run.trigger.type}`] }), runErr && (_jsx("div", { className: "pb-1 text-[10px] text-rose-600", children: runErr })), steps.length === 0 ? (_jsx("div", { className: "text-[10px] italic text-muted-foreground", children: "No step log recorded." })) : (_jsx("ul", { className: "divide-y divide-border/50", children: steps.map((s, i) => (_jsx(StepRow, { step: s }, `${s.nodeId}#${i}`))) }))] }))] }));
71
87
  }
72
88
  export function FlowRunsPanel({ flowName }) {
73
89
  const [runs, setRuns] = React.useState([]);
@@ -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,15 @@
1
+ /**
2
+ * BuilderLanding — the application builder's front door.
3
+ *
4
+ * The journey from login: Home → Studio app → 「应用构建」 (this page, embedded
5
+ * in the app chrome via the `studio:builder` component ref) → pick or create a
6
+ * writable base package → the full-screen pillar builder
7
+ * (`/studio/:packageId/:tab`). Also served standalone at bare `/studio` so the
8
+ * builder is bookmarkable.
9
+ *
10
+ * Writable bases (where authoring happens) lead; read-only code packages are
11
+ * listed secondary for browsing. Writability is the shared display heuristic
12
+ * from packages-io — the ADR-0070 D4 gate stays the server-side authority.
13
+ */
14
+ import * as React from 'react';
15
+ export declare function BuilderLanding(): React.ReactElement;