@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
@@ -48,6 +48,7 @@ import { useMetadataClient, useMetadataTypes } from './useMetadata';
48
48
  import { resolveResourceConfig } from './registry';
49
49
  import { t as translate, detectLocale } from './i18n';
50
50
  import { AssignedUsersSection } from './AssignedUsersSection';
51
+ import { mergePermissionSlice, scopePermissionSet, } from './permission-slice';
51
52
  function getObjectActions(locale) {
52
53
  return [
53
54
  { key: 'allowCreate', short: 'C', tip: translate('perm.action.create', locale) },
@@ -64,7 +65,7 @@ function getObjectActions(locale) {
64
65
  /* ────────────────────────────────────────────────────────────────── */
65
66
  /* Component */
66
67
  /* ────────────────────────────────────────────────────────────────── */
67
- export function PermissionMatrixEditPage({ type, name }) {
68
+ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, publishNonce }) {
68
69
  const navigate = useNavigate();
69
70
  const client = useMetadataClient();
70
71
  const { entries } = useMetadataTypes(client);
@@ -94,20 +95,27 @@ export function PermissionMatrixEditPage({ type, name }) {
94
95
  setLoading(true);
95
96
  (async () => {
96
97
  try {
97
- const [lay, objList] = await Promise.all([
98
+ const [lay, objList, pendingDraft] = await Promise.all([
98
99
  client.layered(type, name).catch(() => null),
99
- client.list('object').catch(() => []),
100
+ // In package scope, list only the objects this package declares
101
+ // (ADR-0086 P0) — otherwise the whole environment leaks into the panel.
102
+ client.list('object', packageId ? { packageId } : {}).catch(() => []),
103
+ // ADR-0086 P2 (D6): under the package door a set is draft/published
104
+ // metadata, so surface the PENDING draft if one exists — otherwise a
105
+ // just-saved-not-yet-published edit would appear lost on reopen. Draft
106
+ // reads return the `{ type, name, item }` envelope; `null` = no draft.
107
+ packageId
108
+ ? client.getDraft(type, name, { packageId }).catch(() => null)
109
+ : Promise.resolve(null),
100
110
  ]);
101
111
  if (cancelled)
102
112
  return;
103
- const effective = (lay?.effective ??
113
+ const draftBody = pendingDraft
114
+ ? (pendingDraft.item ?? pendingDraft)
115
+ : null;
116
+ // Draft wins over the published baseline for display (D6).
117
+ const effective = (draftBody ?? lay?.effective ??
104
118
  lay?.code ?? { name, objects: {} });
105
- setDraft({
106
- ...effective,
107
- name: String(effective?.name ?? name),
108
- objects: effective?.objects ?? {},
109
- fields: effective?.fields ?? {},
110
- });
111
119
  const list = (objList ?? [])
112
120
  .map((row) => {
113
121
  const item = row?.item ?? row;
@@ -116,6 +124,22 @@ export function PermissionMatrixEditPage({ type, name }) {
116
124
  .filter((o) => !!o.name)
117
125
  .sort((a, b) => a.name.localeCompare(b.name));
118
126
  setObjects(list);
127
+ const full = {
128
+ ...effective,
129
+ name: String(effective?.name ?? name),
130
+ objects: effective?.objects ?? {},
131
+ fields: effective?.fields ?? {},
132
+ };
133
+ // Package scope: only surface this package's slice for editing; rows
134
+ // contributed by other packages stay off-screen and are re-merged on
135
+ // Save from a fresh read (see doSave).
136
+ if (packageId) {
137
+ const sliced = scopePermissionSet(full, list.map((o) => o.name));
138
+ setDraft({ ...full, objects: sliced.objects, fields: sliced.fields });
139
+ }
140
+ else {
141
+ setDraft(full);
142
+ }
119
143
  }
120
144
  catch (err) {
121
145
  setError(err?.message ?? String(err));
@@ -128,7 +152,7 @@ export function PermissionMatrixEditPage({ type, name }) {
128
152
  return () => {
129
153
  cancelled = true;
130
154
  };
131
- }, [client, type, name]);
155
+ }, [client, type, name, packageId, publishNonce]);
132
156
  /* ── Lazy-load fields when an object is expanded ─────────── */
133
157
  async function ensureFields(objectName) {
134
158
  if (fieldsByObject[objectName])
@@ -211,17 +235,52 @@ export function PermissionMatrixEditPage({ type, name }) {
211
235
  return { ...prev, fields };
212
236
  });
213
237
  }
238
+ /**
239
+ * Re-narrow a freshly-read full permission set to this package's slice for
240
+ * display. No-op at environment scope (no `packageId`).
241
+ */
242
+ function toDisplayDraft(set) {
243
+ if (!packageId)
244
+ return set;
245
+ const sliced = scopePermissionSet(set, objects.map((o) => o.name));
246
+ return { ...set, objects: sliced.objects, fields: sliced.fields };
247
+ }
214
248
  /* ── Save ────────────────────────────────────────────────── */
215
249
  async function doSave(force, pending) {
216
250
  const payload = pending ?? draft;
217
251
  setSaving(true);
218
252
  setError(null);
219
253
  try {
220
- await client.save(type, payload.name, payload, {
254
+ // Package scope: merge only this package's slice back onto a fresh read
255
+ // of the record so rows contributed by other packages survive byte-for-
256
+ // byte (ADR-0086 P0). Environment scope keeps the whole-record save.
257
+ let toSave = payload;
258
+ if (packageId) {
259
+ const scope = objects.map((o) => o.name);
260
+ const fresh = await client
261
+ .layered(type, payload.name)
262
+ .catch(() => null);
263
+ const base = (fresh?.effective ?? payload);
264
+ toSave = mergePermissionSlice(base, payload, scope);
265
+ }
266
+ // ADR-0086 P2 (D6/D7). Package door → the set is metadata: write a DRAFT
267
+ // (stamped with `packageId`) that the package's atomic Publish promotes,
268
+ // exactly like the Data/Interfaces pillars — NOT a live record write.
269
+ // Environment door (no packageId) stays live (config).
270
+ await client.save(type, payload.name, toSave, {
221
271
  force,
272
+ ...(packageId ? { mode: 'draft', packageId } : {}),
222
273
  });
223
- const lay = await client.layered(type, payload.name);
224
- setDraft((lay.effective ?? payload));
274
+ if (packageId) {
275
+ // The draft is now the pending truth for display; the published baseline
276
+ // hasn't moved. Show what we just staged and let the surface count it.
277
+ setDraft(toDisplayDraft(toSave));
278
+ onDraftSaved?.();
279
+ }
280
+ else {
281
+ const lay = await client.layered(type, payload.name);
282
+ setDraft(toDisplayDraft((lay.effective ?? toSave)));
283
+ }
225
284
  setDestructive(null);
226
285
  }
227
286
  catch (err) {
@@ -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,24 +1,3 @@
1
- /**
2
- * Metadata admin i18n bundle (Phase 3f).
3
- *
4
- * Lightweight static label table for the 27 built-in metadata types,
5
- * plus a tiny `t()` helper for engine UI strings.
6
- *
7
- * Why not i18next? The engine already consumes `label` from the
8
- * server's `/meta/types` response (which is sourced from
9
- * `DEFAULT_METADATA_TYPE_REGISTRY`). This bundle exists as a fallback
10
- * for environments without translation bundles configured, and as the
11
- * single source of truth for Chinese labels until the platform's
12
- * `setup.translation.ts` ships zh-CN coverage.
13
- *
14
- * Usage:
15
- * import { translateMetadataType, t } from './i18n';
16
- * translateMetadataType('view', 'zh-CN') // → '视图'
17
- * t('engine.directory.title', 'zh-CN') // → '元数据'
18
- *
19
- * The DirectoryPage / PageShell call these to localise headings when
20
- * the consumer hasn't wired the global i18n provider.
21
- */
22
1
  export type SupportedLocale = 'en-US' | 'zh-CN';
23
2
  export declare function translateMetadataType(type: string, locale?: SupportedLocale | string, fallback?: string): string;
24
3
  export declare function translateMetadataDomain(domain: string, locale?: SupportedLocale | string): string;
@@ -32,3 +11,15 @@ export declare function tFormat(key: string, locale: SupportedLocale | string |
32
11
  export declare function translateValidationMessage(message: string | undefined, locale?: SupportedLocale | string): string;
33
12
  /** Returns the locale string most browsers report (matches navigator.language). */
34
13
  export declare function detectLocale(): SupportedLocale;
14
+ /**
15
+ * React hook — the Studio/metadata-admin locale derived from the app's ACTIVE
16
+ * i18n language, collapsed to one of the two locales this catalog ships.
17
+ *
18
+ * Prefer this over {@link detectLocale} in components: `detectLocale()` reads
19
+ * `navigator.language`, which never changes when the user switches languages
20
+ * in-app via the LocaleSwitcher (that only moves the i18next instance). This
21
+ * follows the live `useObjectTranslation().language`, so the Studio pillars
22
+ * re-render in lock-step with the console locale — the same source the rest of
23
+ * the app (Home, forms, list views) renders from.
24
+ */
25
+ export declare function useMetadataLocale(): SupportedLocale;
@@ -1,4 +1,26 @@
1
1
  // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+ /**
3
+ * Metadata admin i18n bundle (Phase 3f).
4
+ *
5
+ * Lightweight static label table for the 27 built-in metadata types,
6
+ * plus a tiny `t()` helper for engine UI strings.
7
+ *
8
+ * Why not i18next? The engine already consumes `label` from the
9
+ * server's `/meta/types` response (which is sourced from
10
+ * `DEFAULT_METADATA_TYPE_REGISTRY`). This bundle exists as a fallback
11
+ * for environments without translation bundles configured, and as the
12
+ * single source of truth for Chinese labels until the platform's
13
+ * `setup.translation.ts` ships zh-CN coverage.
14
+ *
15
+ * Usage:
16
+ * import { translateMetadataType, t } from './i18n';
17
+ * translateMetadataType('view', 'zh-CN') // → '视图'
18
+ * t('engine.directory.title', 'zh-CN') // → '元数据'
19
+ *
20
+ * The DirectoryPage / PageShell call these to localise headings when
21
+ * the consumer hasn't wired the global i18n provider.
22
+ */
23
+ import { useObjectTranslation } from '@object-ui/i18n';
2
24
  const TYPE_LABELS_EN = {
3
25
  // Data
4
26
  object: 'Object',
@@ -547,9 +569,10 @@ const ENGINE_STRINGS_EN = {
547
569
  'engine.packages.col.version': 'Version',
548
570
  'engine.packages.col.scope': 'Scope',
549
571
  'engine.packages.col.status': 'Status',
550
- 'engine.packages.scope.project': 'Project',
572
+ 'engine.packages.scope.project': 'Read-only · code',
551
573
  'engine.packages.scope.system': 'System',
552
574
  'engine.packages.scope.cloud': 'Cloud',
575
+ 'engine.packages.scope.writable': 'Writable',
553
576
  'engine.packages.status.enabled': 'Enabled',
554
577
  'engine.packages.status.disabled': 'Disabled',
555
578
  'engine.packages.import.invalidJson': 'Selected file is not valid JSON.',
@@ -805,6 +828,157 @@ const ENGINE_STRINGS_EN = {
805
828
  // AI assistant entry points
806
829
  'designer.canvas.askAi': 'Ask AI',
807
830
  'designer.canvas.askAiGenerate': 'Generate fields with AI',
831
+ // ── StudioDesignSurface (ADR-0080 WYSIWYG design surface) ──────────────
832
+ // Shared chrome
833
+ 'engine.studio.cancel': 'Cancel',
834
+ 'engine.studio.create': 'Create',
835
+ 'engine.studio.createDraft': 'Create (save as draft)',
836
+ 'engine.studio.saveDraft': 'Save draft',
837
+ 'engine.studio.publish': 'Publish',
838
+ 'engine.studio.loading': 'Loading…',
839
+ 'engine.studio.loadFailed': 'Failed to load',
840
+ 'engine.studio.unpublishedDraft': 'Unpublished draft',
841
+ 'engine.studio.unpublished': 'Unpublished',
842
+ 'engine.studio.new': 'New',
843
+ 'engine.studio.edit': 'Edit',
844
+ 'engine.studio.done': 'Done',
845
+ 'engine.studio.close': 'Close',
846
+ 'engine.studio.deselect': 'Clear selection',
847
+ 'engine.studio.home': 'Back to home',
848
+ // Pillar tab labels
849
+ 'engine.studio.pillar.data': 'Data',
850
+ 'engine.studio.pillar.automations': 'Automations',
851
+ 'engine.studio.pillar.interfaces': 'Interfaces',
852
+ 'engine.studio.pillar.access': 'Access',
853
+ // Header · package-level publish + app bridge
854
+ 'engine.studio.changes': 'Changes',
855
+ 'engine.studio.publishTitle': 'Confirm and publish all pending drafts at once (whole package · one atomic release)',
856
+ 'engine.studio.publishNoneTitle': 'No drafts pending publish',
857
+ 'engine.studio.publishedAll': 'Published all drafts in this package (one atomic release)',
858
+ 'engine.studio.app.open': 'Open app',
859
+ 'engine.studio.app.openTitle': 'Open app “{label}” (the published front-end)',
860
+ 'engine.studio.app.create': 'Create app',
861
+ 'engine.studio.app.noneTitle': 'This package has no app (front-end) yet — create one',
862
+ 'engine.studio.app.willOpenAfterPublish': 'After publishing, this becomes “Open app”',
863
+ 'engine.studio.app.pending': 'App “{label}” pending publish',
864
+ 'engine.studio.app.namePlaceholder': 'App name (e.g. Order Center)',
865
+ 'engine.studio.app.idPlaceholder': 'Identifier (e.g. orders_app)',
866
+ 'engine.studio.app.savedDraft': 'App “{label}” saved as draft — open it after publishing',
867
+ // Package switcher
868
+ 'engine.studio.pkg.switchTitle': 'Switch / create package',
869
+ 'engine.studio.pkg.readonly': 'Read-only',
870
+ 'engine.studio.pkg.writable': 'Writable',
871
+ 'engine.studio.pkg.heading': 'Packages (apps)',
872
+ 'engine.studio.pkg.none': 'No app packages yet',
873
+ 'engine.studio.pkg.namePlaceholder': 'Name (e.g. Repair Center)',
874
+ 'engine.studio.pkg.idPlaceholder': 'Package ID (e.g. com.example.repairs)',
875
+ 'engine.studio.pkg.createWritable': 'Create writable package',
876
+ 'engine.studio.pkg.new': 'New package (writable base)',
877
+ 'engine.studio.pkg.created': 'Package {name} created (writable)',
878
+ // Nav item inspector (Interfaces pillar)
879
+ 'engine.studio.nav.selectItem': 'Select a menu item on the left.',
880
+ 'engine.studio.nav.label': 'Label',
881
+ 'engine.studio.nav.labelPlaceholder': 'e.g. Positions',
882
+ 'engine.studio.nav.linkObject': 'Link to object',
883
+ 'engine.studio.nav.chooseObject': '— Choose object —',
884
+ 'engine.studio.nav.boundHint': 'This menu item opens that object’s record list.',
885
+ 'engine.studio.nav.unboundHint': 'Choose an object; the menu item will open its record list.',
886
+ 'engine.studio.nav.noObjects': 'This package has no objects yet — create one in the Data pillar first.',
887
+ // Interfaces pillar
888
+ 'engine.studio.if.pickLeft': 'Select a menu item on the left',
889
+ 'engine.studio.if.navHeading': '{app} · Navigation',
890
+ 'engine.studio.if.editNavTitle': 'Edit navigation (drag to reorder / rename / add-remove)',
891
+ 'engine.studio.if.doneEditTitle': 'Done editing',
892
+ 'engine.studio.if.noApp': 'This package has no app yet.',
893
+ 'engine.studio.if.noNavItems': 'No nav items yet — (click “Edit” above to add)',
894
+ 'engine.studio.if.previewIsRuntime': 'Preview = runtime · same renderer',
895
+ 'engine.studio.if.noAppTitle': 'This package has no app yet',
896
+ 'engine.studio.if.noAppHint': 'Create an app to design its navigation and interfaces.',
897
+ 'engine.studio.if.readonlyPreview': '{type} shows a read-only preview for now; design support is in progress.',
898
+ 'engine.studio.if.editHint': 'Click a block → edit on the right · then “Save draft” → “Publish”',
899
+ 'engine.studio.if.objectHintPre': 'Runtime list preview · edit fields / structure in the ',
900
+ 'engine.studio.if.objectHintPost': ' pillar',
901
+ 'engine.studio.inspector.props': 'Properties',
902
+ 'engine.studio.inspector.emptyLine1': 'Click a block on the canvas,',
903
+ 'engine.studio.inspector.emptyLine2': 'and edit its properties right here.',
904
+ // Data pillar
905
+ 'engine.studio.data.newFieldLabel': 'New field',
906
+ 'engine.studio.data.nameFieldLabel': 'Name',
907
+ 'engine.studio.data.idExists': 'Identifier “{name}” already exists',
908
+ 'engine.studio.data.fieldCount': '{count} fields',
909
+ 'engine.studio.data.pickObject': 'Select an object',
910
+ 'engine.studio.data.objects': 'Objects',
911
+ 'engine.studio.data.searchObjects': 'Search objects…',
912
+ 'engine.studio.data.noObjects': 'No objects yet — create one below to start',
913
+ 'engine.studio.data.labelPlaceholder': 'Display name (e.g. Repair Ticket)',
914
+ 'engine.studio.data.idPlaceholder': 'Identifier (e.g. repair_ticket)',
915
+ 'engine.studio.data.newObject': 'New object',
916
+ 'engine.studio.data.firstObjectTitle': 'Start with your first object',
917
+ 'engine.studio.data.firstObjectHint': 'Objects are your app’s data foundation (e.g. “Orders”, “Customers”). Enter a display name and identifier at the bottom-left to create one; then design its fields, forms, and automations, and publish once at the end.',
918
+ 'engine.studio.data.tab.records': 'Records',
919
+ 'engine.studio.data.tab.form': 'Form',
920
+ 'engine.studio.data.tab.rules': 'Validations',
921
+ 'engine.studio.data.tab.settings': 'Settings',
922
+ 'engine.studio.data.badge.grid': 'Runtime list · same renderer',
923
+ 'engine.studio.data.badge.rules': 'Validation rules · draft',
924
+ 'engine.studio.data.badge.settings': 'Object settings · draft',
925
+ 'engine.studio.data.badge.formLayout': 'Form design · draft',
926
+ 'engine.studio.data.badge.formPreview': 'Runtime form · published definition',
927
+ 'engine.studio.data.addFieldTitle': 'Add a field (then set its type and properties on the right)',
928
+ 'engine.studio.data.addField': 'Add field',
929
+ 'engine.studio.data.editFieldProps': 'Edit field properties',
930
+ 'engine.studio.data.draftObjectTitle': 'Unpublished new object',
931
+ 'engine.studio.data.draftObjectHint': 'The Records grid queries real data, but this object has no table until it’s published. Design fields and groups in “Form · Layout” first, then click “Publish” in the top bar — after publishing, this becomes its live data grid.',
932
+ 'engine.studio.data.goDesignFields': 'Go to “Form · Layout” to design fields',
933
+ 'engine.studio.data.gridHint': 'Column “+” adds a field · pencil edits properties · drag a header to reorder · then “Save draft” → “Publish”',
934
+ 'engine.studio.data.form.layout': 'Layout',
935
+ 'engine.studio.data.form.preview': 'Preview',
936
+ 'engine.studio.data.form.layoutBadge': 'Layout designer · draft (includes unpublished changes)',
937
+ 'engine.studio.data.form.previewBadge': 'Runtime form · published definition',
938
+ 'engine.studio.data.form.previewWarn': 'You have unpublished changes — this preview shows the pre-publish (published) state; confirm the draft in “Layout”, and to see the post-publish result, click “Publish” in the top bar first.',
939
+ 'engine.studio.data.form.noPublishedTitle': 'No published definition yet',
940
+ 'engine.studio.data.form.noPublishedHint': '“Preview” renders the published runtime form, but this object isn’t published yet. Confirm the draft in “Layout”, then click “Publish” in the top bar to preview.',
941
+ 'engine.studio.data.formHint': 'Click any field → edit properties on the right · “Add field” adds a field · then “Save draft” → “Publish”',
942
+ 'engine.studio.data.fieldProps': 'Field properties',
943
+ // Automations pillar
944
+ 'engine.studio.auto.nodeStart': 'Start',
945
+ 'engine.studio.auto.nodeEnd': 'End',
946
+ 'engine.studio.auto.savedDraft': 'Automation “{label}” saved as draft',
947
+ 'engine.studio.auto.defaultOff': 'Off by default · review before enabling',
948
+ 'engine.studio.auto.heading': 'Automations · flow',
949
+ 'engine.studio.auto.newTitle': 'New automation',
950
+ 'engine.studio.auto.none': 'No automations yet — click “New” to start',
951
+ 'engine.studio.auto.namePlaceholder': 'Name (e.g. Offer Notice)',
952
+ 'engine.studio.auto.idPlaceholder': 'Identifier (e.g. offer_notice)',
953
+ 'engine.studio.auto.canvasHint': 'Visual orchestration · click a node to configure',
954
+ 'engine.studio.auto.pick': 'Select an automation',
955
+ 'engine.studio.auto.editHint': 'Click a node → configure on the right · then “Save draft” → “Publish”',
956
+ 'engine.studio.auto.config': 'Configuration',
957
+ 'engine.studio.auto.emptyLine1': 'Click a node on the canvas,',
958
+ 'engine.studio.auto.emptyLine2': 'and its configuration appears here.',
959
+ // Access pillar
960
+ 'engine.studio.access.created': 'Permission set “{label}” created',
961
+ 'engine.studio.access.title': 'Permission matrix',
962
+ 'engine.studio.access.subtitle': 'Objects × CRUD · field-level R/W',
963
+ 'engine.studio.access.bannerTitle': 'This matrix lists only the objects this package declares, and “Save” merges just that slice — grants contributed by other packages are preserved. Edits are saved as package drafts and go live when you Publish the package (top bar), exactly like Data and Interfaces.',
964
+ 'engine.studio.access.banner': 'This package’s objects · saved as draft',
965
+ 'engine.studio.access.heading': 'Permission sets / Profiles',
966
+ 'engine.studio.access.search': 'Search permissions…',
967
+ 'engine.studio.access.none': 'No permission sets yet — create one below',
968
+ 'engine.studio.access.labelPlaceholder': 'Display name (e.g. Sales permissions)',
969
+ 'engine.studio.access.idPlaceholder': 'Identifier (e.g. sales_perms)',
970
+ 'engine.studio.access.new': 'New permission set',
971
+ 'engine.studio.access.emptyMain': 'Create a permission set to start configuring',
972
+ 'engine.studio.access.pick': 'Select a permission set',
973
+ // ── AppNavCanvas (nav-tree editor, used by Studio + AppPreview) ─────────
974
+ 'engine.appNav.heading': 'Navigation',
975
+ 'engine.appNav.addItem': 'Add nav item',
976
+ 'engine.appNav.removeItem': 'Remove nav item',
977
+ 'engine.appNav.empty': 'Empty — click “Add nav item” to start',
978
+ 'engine.appNav.emptyReadonly': 'No top-level nav items yet',
979
+ 'engine.appNav.newItem': 'New item',
980
+ 'engine.appNav.itemOne': 'item',
981
+ 'engine.appNav.itemOther': 'items',
808
982
  };
809
983
  const ENGINE_STRINGS_ZH = {
810
984
  'engine.package.writableRequired': '请先选择或新建一个可写的基座(package)——只读的代码包中无法新建该项。',
@@ -1241,9 +1415,10 @@ const ENGINE_STRINGS_ZH = {
1241
1415
  'engine.packages.col.version': '版本',
1242
1416
  'engine.packages.col.scope': '范围',
1243
1417
  'engine.packages.col.status': '状态',
1244
- 'engine.packages.scope.project': '项目',
1418
+ 'engine.packages.scope.project': '只读 · 代码包',
1245
1419
  'engine.packages.scope.system': '系统',
1246
1420
  'engine.packages.scope.cloud': '云端',
1421
+ 'engine.packages.scope.writable': '可写',
1247
1422
  'engine.packages.status.enabled': '已启用',
1248
1423
  'engine.packages.status.disabled': '已禁用',
1249
1424
  'engine.packages.import.invalidJson': '所选文件不是有效 JSON。',
@@ -1498,6 +1673,157 @@ const ENGINE_STRINGS_ZH = {
1498
1673
  // AI assistant entry points
1499
1674
  'designer.canvas.askAi': '问 AI',
1500
1675
  'designer.canvas.askAiGenerate': '用 AI 生成字段',
1676
+ // ── StudioDesignSurface (ADR-0080 WYSIWYG design surface) ──────────────
1677
+ // Shared chrome
1678
+ 'engine.studio.cancel': '取消',
1679
+ 'engine.studio.create': '创建',
1680
+ 'engine.studio.createDraft': '创建(存为草稿)',
1681
+ 'engine.studio.saveDraft': '保存草稿',
1682
+ 'engine.studio.publish': '发布',
1683
+ 'engine.studio.loading': '加载中…',
1684
+ 'engine.studio.loadFailed': '加载失败',
1685
+ 'engine.studio.unpublishedDraft': '未发布草稿',
1686
+ 'engine.studio.unpublished': '未发布',
1687
+ 'engine.studio.new': '新建',
1688
+ 'engine.studio.edit': '编辑',
1689
+ 'engine.studio.done': '完成',
1690
+ 'engine.studio.close': '关闭',
1691
+ 'engine.studio.deselect': '取消选择',
1692
+ 'engine.studio.home': '返回主页',
1693
+ // Pillar tab labels
1694
+ 'engine.studio.pillar.data': '数据',
1695
+ 'engine.studio.pillar.automations': '自动化',
1696
+ 'engine.studio.pillar.interfaces': '界面',
1697
+ 'engine.studio.pillar.access': '权限',
1698
+ // Header · package-level publish + app bridge
1699
+ 'engine.studio.changes': '变更',
1700
+ 'engine.studio.publishTitle': '一次性确认并发布全部待发布草稿(整包 · 一次原子发布)',
1701
+ 'engine.studio.publishNoneTitle': '没有待发布的草稿',
1702
+ 'engine.studio.publishedAll': '已发布本软件包的全部草稿(一次原子发布)',
1703
+ 'engine.studio.app.open': '打开应用',
1704
+ 'engine.studio.app.openTitle': '打开应用「{label}」(发布后的前端界面)',
1705
+ 'engine.studio.app.create': '创建应用',
1706
+ 'engine.studio.app.noneTitle': '这个软件包还没有应用(前端界面)— 创建一个',
1707
+ 'engine.studio.app.willOpenAfterPublish': '发布后这里会变成「打开应用」',
1708
+ 'engine.studio.app.pending': '应用「{label}」待发布',
1709
+ 'engine.studio.app.namePlaceholder': '应用名称(如:订单中心)',
1710
+ 'engine.studio.app.idPlaceholder': '标识符(如:orders_app)',
1711
+ 'engine.studio.app.savedDraft': '应用「{label}」已存为草稿 — 发布后即可打开',
1712
+ // Package switcher
1713
+ 'engine.studio.pkg.switchTitle': '切换 / 新建软件包',
1714
+ 'engine.studio.pkg.readonly': '只读',
1715
+ 'engine.studio.pkg.writable': '可写',
1716
+ 'engine.studio.pkg.heading': '软件包(应用)',
1717
+ 'engine.studio.pkg.none': '暂无应用软件包',
1718
+ 'engine.studio.pkg.namePlaceholder': '名称(如:维修中心)',
1719
+ 'engine.studio.pkg.idPlaceholder': '包 ID(如:com.example.repairs)',
1720
+ 'engine.studio.pkg.createWritable': '创建可写软件包',
1721
+ 'engine.studio.pkg.new': '新建软件包(可写 base)',
1722
+ 'engine.studio.pkg.created': '软件包 {name} 已创建(可写)',
1723
+ // Nav item inspector (Interfaces pillar)
1724
+ 'engine.studio.nav.selectItem': '在左侧选择一个菜单项。',
1725
+ 'engine.studio.nav.label': '标签',
1726
+ 'engine.studio.nav.labelPlaceholder': '如:职位',
1727
+ 'engine.studio.nav.linkObject': '链接到对象',
1728
+ 'engine.studio.nav.chooseObject': '— 选择对象 —',
1729
+ 'engine.studio.nav.boundHint': '这个菜单项会打开该对象的记录列表。',
1730
+ 'engine.studio.nav.unboundHint': '选择一个对象,菜单项将打开它的记录列表。',
1731
+ 'engine.studio.nav.noObjects': '这个软件包还没有对象 — 先到 Data 支柱创建。',
1732
+ // Interfaces pillar
1733
+ 'engine.studio.if.pickLeft': '从左侧选择一个菜单项',
1734
+ 'engine.studio.if.navHeading': '{app} · 导航',
1735
+ 'engine.studio.if.editNavTitle': '编辑导航(拖拽排序 / 重命名 / 增删)',
1736
+ 'engine.studio.if.doneEditTitle': '完成编辑',
1737
+ 'engine.studio.if.noApp': '这个软件包还没有应用。',
1738
+ 'engine.studio.if.noNavItems': '还没有导航项 —(点上方「编辑」添加)',
1739
+ 'engine.studio.if.previewIsRuntime': '预览即运行 · 同一渲染器',
1740
+ 'engine.studio.if.noAppTitle': '这个软件包还没有应用',
1741
+ 'engine.studio.if.noAppHint': '创建一个应用来设计它的导航与界面。',
1742
+ 'engine.studio.if.readonlyPreview': '{type} 暂用只读预览,设计能力建设中。',
1743
+ 'engine.studio.if.editHint': '点选积木 → 右侧直接改 · 改完「保存草稿」→「发布」',
1744
+ 'engine.studio.if.objectHintPre': '运行态列表预览 · 改字段 / 结构请到 ',
1745
+ 'engine.studio.if.objectHintPost': ' 支柱',
1746
+ 'engine.studio.inspector.props': '属性',
1747
+ 'engine.studio.inspector.emptyLine1': '在画布里点选一个积木,',
1748
+ 'engine.studio.inspector.emptyLine2': '它的属性会在这里直接编辑。',
1749
+ // Data pillar
1750
+ 'engine.studio.data.newFieldLabel': '新字段',
1751
+ 'engine.studio.data.nameFieldLabel': '名称',
1752
+ 'engine.studio.data.idExists': '标识符 "{name}" 已存在',
1753
+ 'engine.studio.data.fieldCount': '{count} 字段',
1754
+ 'engine.studio.data.pickObject': '选择一个对象',
1755
+ 'engine.studio.data.objects': '对象',
1756
+ 'engine.studio.data.searchObjects': '搜索对象…',
1757
+ 'engine.studio.data.noObjects': '还没有对象 — 在下方新建一个开始',
1758
+ 'engine.studio.data.labelPlaceholder': '显示名(如:报修工单)',
1759
+ 'engine.studio.data.idPlaceholder': '标识符(如:repair_ticket)',
1760
+ 'engine.studio.data.newObject': '新建对象',
1761
+ 'engine.studio.data.firstObjectTitle': '从第一个对象开始',
1762
+ 'engine.studio.data.firstObjectHint': '对象是应用的数据基座(如「订单」「客户」)。在左下角输入显示名与标识符即可创建;之后为它设计字段、表单与自动化,最后一次发布。',
1763
+ 'engine.studio.data.tab.records': '记录',
1764
+ 'engine.studio.data.tab.form': '表单',
1765
+ 'engine.studio.data.tab.rules': '验证',
1766
+ 'engine.studio.data.tab.settings': '设置',
1767
+ 'engine.studio.data.badge.grid': '运行态列表 · 同一渲染器',
1768
+ 'engine.studio.data.badge.rules': '验证规则 · 草稿',
1769
+ 'engine.studio.data.badge.settings': '对象设置 · 草稿',
1770
+ 'engine.studio.data.badge.formLayout': '表单设计 · 草稿',
1771
+ 'engine.studio.data.badge.formPreview': '运行态表单 · 已发布定义',
1772
+ 'engine.studio.data.addFieldTitle': '添加一个字段(随后在右侧设置类型与属性)',
1773
+ 'engine.studio.data.addField': '添加字段',
1774
+ 'engine.studio.data.editFieldProps': '编辑字段属性',
1775
+ 'engine.studio.data.draftObjectTitle': '未发布的新对象',
1776
+ 'engine.studio.data.draftObjectHint': '「记录」网格查询真实数据,而这个对象发布前还没有数据表。请先在「表单 · 布局」里设计字段与分组,然后点顶栏「发布」— 发布后这里就是它的实时数据网格。',
1777
+ 'engine.studio.data.goDesignFields': '去「表单 · 布局」设计字段',
1778
+ 'engine.studio.data.gridHint': '列头「+」加字段 · 笔形改属性 · 拖列头重排 · 改完「保存草稿」→「发布」',
1779
+ 'engine.studio.data.form.layout': '布局',
1780
+ 'engine.studio.data.form.preview': '预览',
1781
+ 'engine.studio.data.form.layoutBadge': '布局设计器 · 草稿(含未发布改动)',
1782
+ 'engine.studio.data.form.previewBadge': '运行态表单 · 已发布定义',
1783
+ 'engine.studio.data.form.previewWarn': '有未发布改动 — 此预览为发布前(已发布)的效果;草稿确认用「布局」,看发布后效果请先点顶栏「发布」',
1784
+ 'engine.studio.data.form.noPublishedTitle': '尚无已发布定义',
1785
+ 'engine.studio.data.form.noPublishedHint': '「预览」渲染已发布的运行态表单,而这个对象还未发布。在「布局」里确认草稿,点顶栏「发布」后即可预览。',
1786
+ 'engine.studio.data.formHint': '点选任意字段 → 右侧改属性 · 「添加字段」加字段 · 改完「保存草稿」→「发布」',
1787
+ 'engine.studio.data.fieldProps': '字段属性',
1788
+ // Automations pillar
1789
+ 'engine.studio.auto.nodeStart': '开始',
1790
+ 'engine.studio.auto.nodeEnd': '结束',
1791
+ 'engine.studio.auto.savedDraft': '自动化「{label}」已存为草稿',
1792
+ 'engine.studio.auto.defaultOff': '默认 OFF · 审阅后再启用',
1793
+ 'engine.studio.auto.heading': '自动化 · flow',
1794
+ 'engine.studio.auto.newTitle': '新建自动化',
1795
+ 'engine.studio.auto.none': '还没有自动化 — 点「新建」开始',
1796
+ 'engine.studio.auto.namePlaceholder': '名称(如:录用通知)',
1797
+ 'engine.studio.auto.idPlaceholder': '标识符(如:offer_notice)',
1798
+ 'engine.studio.auto.canvasHint': '可视化编排 · 点选节点配置',
1799
+ 'engine.studio.auto.pick': '选择一个自动化',
1800
+ 'engine.studio.auto.editHint': '点选节点 → 右侧配置 · 改完「保存草稿」→「发布」',
1801
+ 'engine.studio.auto.config': '配置',
1802
+ 'engine.studio.auto.emptyLine1': '在画布里点选一个节点,',
1803
+ 'engine.studio.auto.emptyLine2': '它的配置会在这里显示。',
1804
+ // Access pillar
1805
+ 'engine.studio.access.created': '权限集「{label}」已创建',
1806
+ 'engine.studio.access.title': '权限矩阵',
1807
+ 'engine.studio.access.subtitle': '对象 × CRUD · 字段级读写',
1808
+ 'engine.studio.access.bannerTitle': '此矩阵仅列出本包声明的对象,「Save」只合并本包切片 —— 其他包贡献的授权原样保留。编辑保存为软件包草稿,点击顶栏「发布」后随整个包一起生效(与数据、界面一致)。',
1809
+ 'engine.studio.access.banner': '仅本包对象 · 保存为草稿',
1810
+ 'engine.studio.access.heading': '权限集 / Profile',
1811
+ 'engine.studio.access.search': '搜索权限…',
1812
+ 'engine.studio.access.none': '还没有权限集 — 在下方新建一个',
1813
+ 'engine.studio.access.labelPlaceholder': '显示名(如:销售权限)',
1814
+ 'engine.studio.access.idPlaceholder': '标识符(如:sales_perms)',
1815
+ 'engine.studio.access.new': '新建权限集',
1816
+ 'engine.studio.access.emptyMain': '新建一个权限集开始配置',
1817
+ 'engine.studio.access.pick': '选择一个权限集',
1818
+ // ── AppNavCanvas (nav-tree editor, used by Studio + AppPreview) ─────────
1819
+ 'engine.appNav.heading': '导航',
1820
+ 'engine.appNav.addItem': '添加导航项',
1821
+ 'engine.appNav.removeItem': '删除导航项',
1822
+ 'engine.appNav.empty': '还没有导航项 — 点「添加导航项」开始',
1823
+ 'engine.appNav.emptyReadonly': '还没有顶层导航项',
1824
+ 'engine.appNav.newItem': '新菜单项',
1825
+ 'engine.appNav.itemOne': '项',
1826
+ 'engine.appNav.itemOther': '项',
1501
1827
  };
1502
1828
  function pickTable(locale) {
1503
1829
  const lower = (locale ?? '').toLowerCase();
@@ -1569,3 +1895,18 @@ export function detectLocale() {
1569
1895
  return 'zh-CN';
1570
1896
  return 'en-US';
1571
1897
  }
1898
+ /**
1899
+ * React hook — the Studio/metadata-admin locale derived from the app's ACTIVE
1900
+ * i18n language, collapsed to one of the two locales this catalog ships.
1901
+ *
1902
+ * Prefer this over {@link detectLocale} in components: `detectLocale()` reads
1903
+ * `navigator.language`, which never changes when the user switches languages
1904
+ * in-app via the LocaleSwitcher (that only moves the i18next instance). This
1905
+ * follows the live `useObjectTranslation().language`, so the Studio pillars
1906
+ * re-render in lock-step with the console locale — the same source the rest of
1907
+ * the app (Home, forms, list views) renders from.
1908
+ */
1909
+ export function useMetadataLocale() {
1910
+ const { language } = useObjectTranslation();
1911
+ return /^zh/i.test(language) ? 'zh-CN' : 'en-US';
1912
+ }