@77-zhike/app-sdk 0.2.0-d7b1.6 → 0.2.0-d7b1.8

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 (40) hide show
  1. package/README.md +192 -0
  2. package/package.json +9 -3
  3. package/src/cli/project-init.js +1 -0
  4. package/src/client/destinations.d.ts +25 -0
  5. package/src/client/destinations.js +70 -0
  6. package/src/client/external-url.js +9 -0
  7. package/src/client/index.d.ts +186 -3
  8. package/src/client/index.js +45 -3
  9. package/src/client/interactions.d.ts +7 -3
  10. package/src/client/interactions.js +19 -2
  11. package/src/client/points.generated.js +48 -0
  12. package/src/forms/array-contract.d.ts +32 -0
  13. package/src/forms/array-contract.js +138 -0
  14. package/src/forms/components.generated.js +2 -2
  15. package/src/forms/factory.js +21 -7
  16. package/src/forms/field-array.d.ts +16 -0
  17. package/src/forms/field-array.js +52 -0
  18. package/src/forms/index.d.ts +67 -3
  19. package/src/forms/index.js +2 -0
  20. package/src/forms/options.d.ts +7 -2
  21. package/src/forms/options.js +8 -1
  22. package/src/forms/record-options.d.ts +28 -0
  23. package/src/forms/record-options.js +92 -0
  24. package/src/forms/schema.d.ts +73 -9
  25. package/src/forms/schema.js +128 -0
  26. package/src/forms/state.d.ts +30 -0
  27. package/src/forms/state.js +82 -0
  28. package/src/forms/validation.d.ts +9 -0
  29. package/src/forms/validation.js +81 -0
  30. package/src/forms/values.d.ts +5 -0
  31. package/src/forms/values.js +38 -0
  32. package/src/server/index.d.ts +15 -0
  33. package/src/server/index.js +20 -0
  34. package/src/toolchain/real-build.js +29 -3
  35. package/src/toolchain/source-discovery.js +202 -16
  36. package/src/ui/components.generated.js +3 -1
  37. package/src/ui/factory.js +50 -3
  38. package/src/ui/index.d.ts +157 -8
  39. package/src/ui/index.js +1 -1
  40. package/src/ui/validation.js +38 -0
@@ -1,3 +1,4 @@
1
+ import { componentTypes } from '../ui/components.generated.js';
1
2
  import { createRequire } from "node:module";
2
3
  import { createHash } from "node:crypto";
3
4
  import { mkdir, rm, writeFile } from "node:fs/promises";
@@ -245,6 +246,24 @@ function createSdkBuildPlugin(projectDirectory, sources, target) {
245
246
  return {
246
247
  name: "zhike-app-sdk-runtime",
247
248
  setup(buildApi) {
249
+ if (target === "remote-client") {
250
+ // 新构建固化 authoring 默认值;Guest 仍为不可变旧 Bundle 提供兼容模块。
251
+ buildApi.onResolve({ filter: /^@77-zhike\/app-sdk\/ui$/ }, args => args.namespace === "zhike-ui-authoring"
252
+ ? { path: args.path, external: true }
253
+ : { path: args.path, namespace: "zhike-ui-authoring" });
254
+ buildApi.onLoad({ filter: /.*/, namespace: "zhike-ui-authoring" }, () => ({ loader: "js", contents: `
255
+ import * as ui from "@77-zhike/app-sdk/ui";
256
+ import { createElement } from "react";
257
+ export const TextBlock = props => createElement(ui.TextBlock, { ...props, align: props.align ?? "left" });
258
+ export const Badge = props => {
259
+ if (props.tone !== undefined) throw new Error("Badge.tone 已移除,请使用 StatusBadge");
260
+ return createElement(ui.Badge, props);
261
+ };
262
+ export const TableCell = Object.defineProperties(props => createElement(ui.TableCell, props), { Avatar: { get: () => ui.Avatar }, Badge: { value: Badge } });
263
+ export const Table = Object.defineProperties(props => createElement(ui.Table, props), { Header: { get: () => ui.Table.Header }, HeaderCell: { get: () => ui.Table.HeaderCell }, Body: { get: () => ui.Table.Body }, Row: { get: () => ui.Table.Row }, Cell: { value: TableCell } });
264
+ export const { ${[...componentTypes.filter(type => !["TextBlock", "Badge", "Table", "TableCell"].includes(type)), "Typography", "Widget"].join(", ")} } = ui;
265
+ ` }));
266
+ }
248
267
  if (target !== "remote-client") buildApi.onResolve({ filter: /^@77-zhike\/app-sdk\/(client|server)$/ }, (args) => ({
249
268
  external: target === "server" && args.path.endsWith("/server"),
250
269
  path: args.path,
@@ -254,7 +273,10 @@ function createSdkBuildPlugin(projectDirectory, sources, target) {
254
273
  }));
255
274
  buildApi.onLoad({ filter: /.*/, namespace: "zhike-sdk-runtime" }, (args) => {
256
275
  if (args.path.endsWith("/client")) {
257
- return { contents: "export const defineFrontendExtension = (definition) => definition;", loader: "js" };
276
+ return {
277
+ contents: "export const defineFrontendExtension = (definition) => definition; export const definePage = (definition) => ({ ...definition, type: 'page' });",
278
+ loader: "js",
279
+ };
258
280
  }
259
281
  return { contents: "export const defineLifecycleEvent = (definition) => definition; export const defineServerFunction = (handler) => handler; export const defineWebhookHandler = (definition) => definition; export const createWebhookHandler = async () => { throw new Error('webhook_handler_runtime_unavailable'); }; export const listWebhookHandlers = async () => { throw new Error('webhook_handler_runtime_unavailable'); }; export const deleteWebhookHandler = async () => { throw new Error('webhook_handler_runtime_unavailable'); };", loader: "js" };
260
282
  });
@@ -293,7 +315,7 @@ function createManifest({ clientBytes, serverBytes, sources }) {
293
315
  sdkAbiVersion: "2",
294
316
  toolchainVersion: TOOLING_VERSION,
295
317
  ...(sources.frontendExtensions.length > 0
296
- ? { clientRuntime: { runtimeVersion: "remote-react.v2" } } : {}),
318
+ ? { clientRuntime: { runtimeVersion: "remote-react.v3" } } : {}),
297
319
  artifacts: {
298
320
  ...(clientBytes
299
321
  ? {
@@ -316,7 +338,7 @@ function createManifest({ clientBytes, serverBytes, sources }) {
316
338
  }
317
339
  : {}),
318
340
  },
319
- frontendExtensions: sources.frontendExtensions.map(({ key, type, entry, label, icon, objects }) => ({
341
+ frontendExtensions: sources.frontendExtensions.map(({ key, type, entry, label, icon, objects, name, order, variant, showInNavigation }) => ({
320
342
  artifact: "client",
321
343
  entry,
322
344
  key,
@@ -324,6 +346,10 @@ function createManifest({ clientBytes, serverBytes, sources }) {
324
346
  ...(label === undefined ? {} : { label }),
325
347
  ...(icon === undefined ? {} : { icon }),
326
348
  ...(objects === undefined ? {} : { objects }),
349
+ ...(name === undefined ? {} : { name }),
350
+ ...(order === undefined ? {} : { order }),
351
+ ...(variant === undefined ? {} : { variant }),
352
+ ...(showInNavigation === undefined ? {} : { showInNavigation }),
327
353
  })),
328
354
  lifecycleEvents: sources.lifecycleEvents.map(({ event, key }) => ({
329
355
  artifact: "server",
@@ -8,8 +8,13 @@ import { ZhikeCliError } from "../errors.js";
8
8
 
9
9
  const KEY_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
10
10
  const RECORD_OBJECT_PATTERN = /^[a-z][a-z0-9_]{0,106}$/;
11
+ const RECORD_EXTENSION_OBJECT_PATTERN = /^(?:accounts|contacts|opportunities|custom_[a-z0-9_]{1,100})$/;
11
12
  const RECORD_ACTION_LABEL_MAX_LENGTH = 80;
12
13
  const RECORD_ACTION_OBJECTS_MAX_LENGTH = 32;
14
+ const RECORD_ACTION_TYPES = new Set(["record-action", "object-action", "bulk-record-action"]);
15
+ const RECORD_COMPONENT_TYPES = new Set(["record-widget", "record-section", "record-tab"]);
16
+ const PAGE_NAME_MAX_LENGTH = 80;
17
+ const PAGE_VARIANTS = new Set(["full", "centered"]);
13
18
  const SDK_ICON_NAMES = new Set(sdkIconNames);
14
19
  const SERVER_MODULE_PATTERN = /^server\/functions\/[a-z0-9][a-z0-9/-]*$/;
15
20
  const LIFECYCLE_EVENTS = new Set([
@@ -86,6 +91,9 @@ export async function discoverAppSources(projectDirectory) {
86
91
  const extensionMatch = file.relativePath.match(
87
92
  /^app\/extensions\/([a-z][a-z0-9-]{0,63})\/extension\.tsx$/,
88
93
  );
94
+ const pageMatch = file.relativePath.match(
95
+ /^app\/pages\/([a-z][a-z0-9-]{0,63})\/page\.tsx$/,
96
+ );
89
97
  const lifecycleMatch = file.relativePath.match(
90
98
  /^app\/events\/(app-installed|app-uninstalled|connection-added|connection-removed)\.event\.ts$/,
91
99
  );
@@ -103,6 +111,14 @@ export async function discoverAppSources(projectDirectory) {
103
111
  );
104
112
  registerUniqueKey(globalKeys, definition.key, file.relativePath);
105
113
  lifecycleEvents.push(definition);
114
+ } else if (pageMatch) {
115
+ const definition = parseWorkspacePage(
116
+ sourceFile,
117
+ file.relativePath,
118
+ pageMatch[1],
119
+ );
120
+ registerUniqueKey(globalKeys, definition.key, file.relativePath);
121
+ frontendExtensions.push(definition);
106
122
  } else if (extensionMatch) {
107
123
  const definition = parseFrontendExtension(
108
124
  sourceFile,
@@ -221,16 +237,21 @@ function parseFrontendExtension(sourceFile, relativePath, directoryKey) {
221
237
  const key = requireStringProperty(object, "key", relativePath);
222
238
  const type = requireStringProperty(object, "type", relativePath);
223
239
  const point = frontendPoint(type);
224
- if (!point) throw toolchainError("zhike_frontend_extension_type_unsupported", `${relativePath}: 扩展类型尚未开放`);
225
- const handlerName = point.kind === "view" ? "component" : (point.callbackName || "handler");
240
+ if (!point || type === "page") throw toolchainError("zhike_frontend_extension_type_unsupported", `${relativePath}: 扩展类型尚未开放`);
241
+ const handlerName = point.kind === "view" ? (point.componentName || "component") : (point.callbackName || "handler");
226
242
  const forbiddenNames = point.kind === "view"
227
- ? ["handler", "onTrigger"]
228
- : ["component", ...(handlerName === "handler" ? ["onTrigger"] : ["handler"])];
243
+ ? ["component", "Widget", "Section", "Tab", "Page", "handler", "onTrigger"].filter(name => name !== handlerName)
244
+ : ["component", "Widget", "Section", "Tab", "Page", ...(handlerName === "handler" ? ["onTrigger"] : ["handler"])];
229
245
  if (forbiddenNames.some(name => findProperty(object, name))) {
230
246
  throw toolchainError("zhike_frontend_definition_mixed", `${relativePath}: component 与 handler 不得混填`);
231
247
  }
232
248
  const property = findProperty(object, handlerName);
233
- if (point.kind === "view" && property && ts.isPropertyAssignment(property) && ts.isIdentifier(property.initializer)) {
249
+ if (
250
+ point.kind === "view" &&
251
+ property &&
252
+ ((ts.isPropertyAssignment(property) && ts.isIdentifier(property.initializer)) ||
253
+ ts.isShorthandPropertyAssignment(property))
254
+ ) {
234
255
  // 引用的组件由 TypeScript 检查可调用性;控制面不执行组件。
235
256
  } else {
236
257
  requireFunctionProperty(object, handlerName, relativePath);
@@ -241,7 +262,7 @@ function parseFrontendExtension(sourceFile, relativePath, directoryKey) {
241
262
  `${relativePath}: key 必须与扩展目录一致`,
242
263
  );
243
264
  }
244
- if (type === "record-action") {
265
+ if (RECORD_ACTION_TYPES.has(type)) {
245
266
  return Object.freeze({
246
267
  key,
247
268
  type,
@@ -250,9 +271,90 @@ function parseFrontendExtension(sourceFile, relativePath, directoryKey) {
250
271
  ...parseRecordActionMetadata(object, relativePath),
251
272
  });
252
273
  }
274
+ if (RECORD_COMPONENT_TYPES.has(type)) {
275
+ return Object.freeze({
276
+ key,
277
+ type,
278
+ entry: key,
279
+ relativePath,
280
+ ...parseRecordComponentMetadata(object, relativePath, handlerName),
281
+ });
282
+ }
253
283
  return Object.freeze({ key, type, entry: key, relativePath });
254
284
  }
255
285
 
286
+ function parseWorkspacePage(sourceFile, relativePath, slug) {
287
+ requireSdkNamedImport(
288
+ sourceFile,
289
+ relativePath,
290
+ "@77-zhike/app-sdk/client",
291
+ "definePage",
292
+ );
293
+ const call = requireDefaultExportCall(sourceFile, relativePath, "definePage");
294
+ const object = requireObjectLiteral(call.arguments[0], relativePath);
295
+ assertOnlyProperties(
296
+ object,
297
+ ["name", "order", "variant", "Page", "showInNavigation"],
298
+ relativePath,
299
+ "zhike_page_definition_invalid",
300
+ "Page definition 包含不支持的字段",
301
+ );
302
+
303
+ const rawName = requireStringProperty(object, "name", relativePath);
304
+ const name = rawName.trim();
305
+ if (
306
+ [...name].length === 0 ||
307
+ [...name].length > PAGE_NAME_MAX_LENGTH ||
308
+ /\p{Cc}/u.test(name)
309
+ ) {
310
+ throw toolchainError(
311
+ "zhike_page_name_invalid",
312
+ `${relativePath}: name trim 后必须是 1~${PAGE_NAME_MAX_LENGTH} 个非控制字符`,
313
+ );
314
+ }
315
+ const variant = requireStringProperty(object, "variant", relativePath);
316
+ if (!PAGE_VARIANTS.has(variant)) {
317
+ throw toolchainError(
318
+ "zhike_page_variant_invalid",
319
+ `${relativePath}: variant 必须是 full 或 centered`,
320
+ );
321
+ }
322
+ const orderProperty = findProperty(object, "order");
323
+ const order = orderProperty === undefined
324
+ ? undefined
325
+ : requireSafeIntegerProperty(orderProperty, relativePath);
326
+ const visibilityProperty = findProperty(object, "showInNavigation");
327
+ let showInNavigation;
328
+ if (visibilityProperty !== undefined) {
329
+ if (!ts.isPropertyAssignment(visibilityProperty)
330
+ || ![ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword].includes(visibilityProperty.initializer.kind)) {
331
+ throw toolchainError("zhike_page_visibility_invalid", `${relativePath}: showInNavigation 必须是静态布尔值`);
332
+ }
333
+ showInNavigation = visibilityProperty.initializer.kind === ts.SyntaxKind.TrueKeyword;
334
+ }
335
+ const pageProperty = findProperty(object, "Page");
336
+ if (
337
+ !pageProperty ||
338
+ !ts.isPropertyAssignment(pageProperty) ||
339
+ !(ts.isIdentifier(pageProperty.initializer) || isFunctionLike(pageProperty.initializer))
340
+ ) {
341
+ throw toolchainError(
342
+ "zhike_page_component_invalid",
343
+ `${relativePath}: Page 必须是 React component 标识符或函数`,
344
+ );
345
+ }
346
+ return Object.freeze({
347
+ key: slug,
348
+ type: "page",
349
+ entry: slug,
350
+ relativePath,
351
+ name,
352
+ ...(order === undefined ? {} : { order }),
353
+ ...(showInNavigation === undefined ? {} : { showInNavigation }),
354
+ variant,
355
+ });
356
+ }
357
+
256
358
  function parseRecordActionMetadata(object, relativePath) {
257
359
  assertOnlyProperties(object, ["key", "type", "label", "icon", "objects", "onTrigger"], relativePath);
258
360
  const label = requireStringProperty(object, "label", relativePath).trim();
@@ -283,9 +385,48 @@ function parseRecordActionMetadata(object, relativePath) {
283
385
  };
284
386
  }
285
387
 
286
- function requireRecordObjects(property, relativePath) {
388
+ function parseRecordComponentMetadata(object, relativePath, componentName) {
389
+ assertOnlyProperties(
390
+ object,
391
+ ["key", "type", "label", "objects", componentName],
392
+ relativePath,
393
+ "zhike_record_component_definition_invalid",
394
+ "Record Page component definition 包含不支持的字段",
395
+ );
396
+ const label = requireStringProperty(object, "label", relativePath).trim();
397
+ if (
398
+ [...label].length === 0 ||
399
+ [...label].length > RECORD_ACTION_LABEL_MAX_LENGTH ||
400
+ /\p{Cc}/u.test(label)
401
+ ) {
402
+ throw toolchainError(
403
+ "zhike_record_component_label_invalid",
404
+ `${relativePath}: label 必须是 1~${RECORD_ACTION_LABEL_MAX_LENGTH} 个非控制字符`,
405
+ );
406
+ }
407
+ const objectsProperty = findProperty(object, "objects");
408
+ const objects = objectsProperty
409
+ ? requireRecordObjects(
410
+ objectsProperty,
411
+ relativePath,
412
+ "zhike_record_component_objects_invalid",
413
+ true,
414
+ )
415
+ : null;
416
+ return {
417
+ label,
418
+ ...(objects === null ? {} : { objects }),
419
+ };
420
+ }
421
+
422
+ function requireRecordObjects(
423
+ property,
424
+ relativePath,
425
+ errorCode = "zhike_record_action_objects_invalid",
426
+ rejectDuplicates = false,
427
+ ) {
287
428
  if (!ts.isPropertyAssignment(property)) {
288
- throw toolchainError("zhike_record_action_objects_invalid", `${relativePath}: objects 必须是字符串字面量或字符串字面量数组`);
429
+ throw toolchainError(errorCode, `${relativePath}: objects 必须是字符串字面量或字符串字面量数组`);
289
430
  }
290
431
  const initializer = property.initializer;
291
432
  const values = ts.isStringLiteral(initializer)
@@ -293,7 +434,7 @@ function requireRecordObjects(property, relativePath) {
293
434
  : ts.isArrayLiteralExpression(initializer)
294
435
  ? initializer.elements.map(item => {
295
436
  if (!ts.isStringLiteral(item)) {
296
- throw toolchainError("zhike_record_action_objects_invalid", `${relativePath}: objects 必须是字符串字面量数组`);
437
+ throw toolchainError(errorCode, `${relativePath}: objects 必须是字符串字面量数组`);
297
438
  }
298
439
  return item.text;
299
440
  })
@@ -302,17 +443,24 @@ function requireRecordObjects(property, relativePath) {
302
443
  values === null ||
303
444
  values.length === 0 ||
304
445
  values.length > RECORD_ACTION_OBJECTS_MAX_LENGTH ||
305
- values.some(value => !RECORD_OBJECT_PATTERN.test(value))
446
+ (rejectDuplicates && new Set(values).size !== values.length) ||
447
+ values.some(value => !RECORD_OBJECT_PATTERN.test(value) || !RECORD_EXTENSION_OBJECT_PATTERN.test(value))
306
448
  ) {
307
449
  throw toolchainError(
308
- "zhike_record_action_objects_invalid",
309
- `${relativePath}: objects 必须包含 1~${RECORD_ACTION_OBJECTS_MAX_LENGTH} 个合法公开对象标识`,
450
+ errorCode,
451
+ `${relativePath}: objects 只支持 accounts、contacts、opportunities 或 custom_*,且必须包含 1~${RECORD_ACTION_OBJECTS_MAX_LENGTH} 个合法公开对象标识`,
310
452
  );
311
453
  }
312
- return Object.freeze([...new Set(values)]);
454
+ return Object.freeze(rejectDuplicates ? values : [...new Set(values)]);
313
455
  }
314
456
 
315
- function assertOnlyProperties(object, allowed, relativePath) {
457
+ function assertOnlyProperties(
458
+ object,
459
+ allowed,
460
+ relativePath,
461
+ code = "zhike_frontend_definition_unknown_property",
462
+ message = "Frontend Extension 包含不支持的字段",
463
+ ) {
316
464
  const allowedNames = new Set(allowed);
317
465
  for (const property of object.properties) {
318
466
  const name = property.name && (
@@ -320,8 +468,8 @@ function assertOnlyProperties(object, allowed, relativePath) {
320
468
  ) ? property.name.text : null;
321
469
  if (!name || !allowedNames.has(name)) {
322
470
  throw toolchainError(
323
- "zhike_frontend_definition_unknown_property",
324
- `${relativePath}: Frontend Extension 包含不支持的字段`,
471
+ code,
472
+ `${relativePath}: ${message}`,
325
473
  );
326
474
  }
327
475
  }
@@ -571,6 +719,20 @@ function rejectUnsupportedPaths(files) {
571
719
  `${path}: Frontend Extension 必须使用约定目录`,
572
720
  );
573
721
  }
722
+ if (/^app\/pages\/[^/]+\/page\.tsx$/.test(path)) {
723
+ const slug = path.split("/")[2];
724
+ if (!KEY_PATTERN.test(slug)) {
725
+ throw toolchainError(
726
+ "zhike_page_slug_invalid",
727
+ `${path}: Page slug 必须是最多 64 字符的 lowercase kebab-case`,
728
+ );
729
+ }
730
+ } else if (path.startsWith("app/pages/") && /\/page\.(?:ts|tsx)$/.test(path)) {
731
+ throw toolchainError(
732
+ "zhike_page_location_invalid",
733
+ `${path}: Page 必须位于 src/app/pages/<slug>/page.tsx 一级目录`,
734
+ );
735
+ }
574
736
  if (
575
737
  path.startsWith("app/webhooks/") &&
576
738
  path.endsWith(".ts") &&
@@ -624,6 +786,30 @@ function requireStringArrayProperty(object, name, relativePath) {
624
786
  });
625
787
  }
626
788
 
789
+ function requireSafeIntegerProperty(property, relativePath) {
790
+ if (!ts.isPropertyAssignment(property)) {
791
+ throw toolchainError(
792
+ "zhike_page_order_invalid",
793
+ `${relativePath}: order 必须是安全整数字面量`,
794
+ );
795
+ }
796
+ const initializer = property.initializer;
797
+ const value = ts.isNumericLiteral(initializer)
798
+ ? Number(initializer.text)
799
+ : ts.isPrefixUnaryExpression(initializer) &&
800
+ initializer.operator === ts.SyntaxKind.MinusToken &&
801
+ ts.isNumericLiteral(initializer.operand)
802
+ ? -Number(initializer.operand.text)
803
+ : Number.NaN;
804
+ if (!Number.isSafeInteger(value)) {
805
+ throw toolchainError(
806
+ "zhike_page_order_invalid",
807
+ `${relativePath}: order 必须是安全整数字面量`,
808
+ );
809
+ }
810
+ return value;
811
+ }
812
+
627
813
  function requireFunctionProperty(object, name, relativePath, { requireAsync = false } = {}) {
628
814
  const property = findProperty(object, name);
629
815
  const functionNode =
@@ -1,2 +1,4 @@
1
1
  // 此文件由 scripts/generate-components.mjs 生成,请修改产品能力声明。
2
- export const componentTypes = Object.freeze(["Text","Icon","Stack","Section","Banner","Badge","LoadingState","Button","LookupCell","Tabs","TabsList","TabsTrigger","TabsContent","Table","TableHeader","TableBody","TableRow","TableCell"]);
2
+ export const componentTypes = Object.freeze(["Icon","Stack","SplitLayout","SplitLayoutMain","SplitLayoutAside","SplitLayoutActions","Section","Banner","Badge","LoadingState","Card","Grid","EmptyState","Link","Button","Switch","LookupCell","Tabs","TabsList","TabsTrigger","TabsContent","Table","TableHeader","TableBody","TableRow","TableCell","Avatar","DescriptionList","DescriptionListItem","DescriptionListMenu","DescriptionListMenuItem","Divider","Json","ExternalLink","StatusBadge","TextBlock","TypographyTitle","TypographyBody","TypographyCaption","TableHeaderCell","WidgetTextWidget","WidgetTitle","WidgetTextPrimary","WidgetTextSecondary","WidgetLoading","WidgetDecoration"]);
3
+ export const legacyComponentTypes = Object.freeze(["Text","Icon","Stack","SplitLayout","SplitLayoutMain","SplitLayoutAside","SplitLayoutActions","Section","Banner","Badge","LoadingState","Card","Grid","EmptyState","Link","Button","Switch","LookupCell","Tabs","TabsList","TabsTrigger","TabsContent","Table","TableHeader","TableBody","TableRow","TableCell","Avatar","DescriptionList","DescriptionListItem","DescriptionListMenu","DescriptionListMenuItem","Divider","Json","ExternalLink","StatusBadge","TextBlock","TypographyTitle","TypographyBody","TypographyCaption","TableHeaderCell","WidgetTextWidget","WidgetTitle","WidgetTextPrimary","WidgetTextSecondary","WidgetLoading","WidgetDecoration"]);
4
+ export const badgeColors = Object.freeze(["amber","blue","cyan","green","grey","lavender","lime","orange","pink","purple","red","yellow"]);
package/src/ui/factory.js CHANGED
@@ -1,9 +1,42 @@
1
- import { componentTypes } from './components.generated.js';
1
+ import { componentTypes, legacyComponentTypes } from './components.generated.js';
2
2
  import { useNormalizedOptions } from '../forms/normalize-options.js';
3
+ import { assertExternalUrl } from '../client/external-url.js';
4
+ import { assertBannerActions, assertJsonValue } from './validation.js';
3
5
 
4
- export function createUiComponents(React) {
6
+ export function createUiComponents(React, { legacy = false, useOwner = () => null, openExternal = () => Promise.reject(new Error('runtime_host_interaction_unavailable')) } = {}) {
5
7
  const TabValue = React.createContext(null);
6
- return Object.fromEntries(componentTypes.map(type => [type, function RemoteComponent(props) {
8
+ const components = Object.fromEntries((legacy ? legacyComponentTypes : componentTypes).map(type => [type, function RemoteComponent(props) {
9
+ if (!legacy && type === 'TextBlock') return React.createElement(type, { ...props, align: props.align ?? 'left' });
10
+ if (!legacy && type === 'Badge' && props.tone !== undefined) throw new Error('Badge.tone 已移除,请使用 StatusBadge');
11
+ if (type === 'Avatar') {
12
+ const { src, ...rest } = props;
13
+ return React.createElement(type, { ...rest, ...(src == null ? {} : { src: assertExternalUrl(src) }) });
14
+ }
15
+ if (type === 'Json') {
16
+ assertJsonValue(props.data);
17
+ return React.createElement(type, props);
18
+ }
19
+ if (type === 'Banner') {
20
+ const { actions, onAction, ...rest } = props;
21
+ if (onAction !== undefined || (actions !== undefined && !Array.isArray(actions))) throw new Error('runtime_ui_contract_invalid');
22
+ if (!actions?.length) return React.createElement(type, rest);
23
+ // 显式复制 metadata 和 handler;调用期间 App 原地修改数组不能重定向旧动作。
24
+ const data = actions.map(action => ({ label: action.label, variant: action.variant }));
25
+ const handlers = actions.map(action => action.onTrigger);
26
+ assertBannerActions(data);
27
+ if (handlers.some(handler => typeof handler !== 'function')) throw new Error('runtime_ui_contract_invalid');
28
+ return React.createElement(type, { ...rest, actions: data, onAction(index) {
29
+ if (!Number.isSafeInteger(index) || index < 0 || index >= handlers.length) throw new Error('runtime_ui_contract_invalid');
30
+ return handlers[index]();
31
+ } });
32
+ }
33
+ if (type === 'ExternalLink') {
34
+ const owner = useOwner();
35
+ const { href: input, onOpen, ...rest } = props;
36
+ if (onOpen !== undefined) throw new Error('runtime_ui_contract_invalid');
37
+ const href = assertExternalUrl(input);
38
+ return React.createElement(type, { ...rest, href, onOpen: () => openExternal(owner, href) });
39
+ }
7
40
  if (type === 'Tabs') return React.createElement(TabValue.Provider, { value: props.value }, React.createElement(type, props));
8
41
  if (type === 'TabsContent') {
9
42
  const value = React.useContext(TabValue);
@@ -16,4 +49,18 @@ export function createUiComponents(React) {
16
49
  }
17
50
  return React.createElement(type, props);
18
51
  }]));
52
+ Object.assign(components.DescriptionList, { Item: components.DescriptionListItem, Menu: components.DescriptionListMenu });
53
+ components.DescriptionListMenu.Item = components.DescriptionListMenuItem;
54
+ Object.assign(components.Table, { Header: components.TableHeader, HeaderCell: components.TableHeaderCell, Body: components.TableBody, Row: components.TableRow, Cell: components.TableCell });
55
+ Object.assign(components.TableCell, { Avatar: components.Avatar, Badge: components.Badge });
56
+ Object.assign(components.SplitLayout, { Main: components.SplitLayoutMain, Aside: components.SplitLayoutAside, Actions: components.SplitLayoutActions });
57
+ components.Typography = Object.freeze({ Title: components.TypographyTitle, Body: components.TypographyBody, Caption: components.TypographyCaption });
58
+ components.Widget = Object.freeze({
59
+ TextWidget: components.WidgetTextWidget,
60
+ Title: components.WidgetTitle,
61
+ Text: Object.freeze({ Primary: components.WidgetTextPrimary, Secondary: components.WidgetTextSecondary }),
62
+ Loading: components.WidgetLoading,
63
+ Decoration: components.WidgetDecoration,
64
+ });
65
+ return components;
19
66
  }