@open-feishu-card/renderer 0.3.6 → 0.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.
package/dist/all.css CHANGED
@@ -380,6 +380,90 @@
380
380
  font-size: 12px;
381
381
  }
382
382
 
383
+ /* ---- img_combination 多图混排(比例逆向自网页版 .universal-card-img-combination) ---- */
384
+ .universal-card-img-combination { display: flex; width: 100%; justify-content: space-between; }
385
+ .universal-card-img-combination__item { min-width: 0; overflow: hidden; }
386
+ .universal-card-img-combination__item > .universal-card-image__host { display: block; width: 100%; height: 100%; object-fit: cover; }
387
+ .universal-card-img-combination__item > .universal-card-image__placeholder { width: 100%; height: 100%; }
388
+ /* double:左窄(32.4%, 24:33) + 右宽(67.6%, 49.33:33) */
389
+ .universal-card-img-combination--double .universal-card-img-combination__item:nth-child(odd) {
390
+ width: 32.4%; aspect-ratio: 24 / 33;
391
+ }
392
+ .universal-card-img-combination--double .universal-card-img-combination__item:nth-child(2n) {
393
+ width: calc(67.6% - 4px); aspect-ratio: 49.33 / 33;
394
+ }
395
+ /* triple:左大图(66.5%, 1:1) + 右列两张小图 */
396
+ .universal-card-img-combination--triple .universal-card-img-combination__left {
397
+ width: 66.5%; aspect-ratio: 1; margin-right: 1%;
398
+ }
399
+ .universal-card-img-combination--triple .universal-card-img-combination__right {
400
+ width: 33%; display: flex; flex-direction: column; justify-content: space-between;
401
+ }
402
+ .universal-card-img-combination--triple .universal-card-img-combination__right .universal-card-img-combination__item {
403
+ width: 100%; aspect-ratio: 1;
404
+ }
405
+ .universal-card-img-combination--triple .universal-card-img-combination__right .universal-card-img-combination__item:first-child {
406
+ margin-bottom: 1.5%;
407
+ }
408
+ /* bisect / trisect:等宽方格换行 */
409
+ .universal-card-img-combination--bisect { flex-wrap: wrap; gap: 4px; }
410
+ .universal-card-img-combination--bisect .universal-card-img-combination__item {
411
+ width: calc(50% - 2px); aspect-ratio: 1;
412
+ }
413
+ .universal-card-img-combination--trisect { flex-wrap: wrap; justify-content: flex-start; gap: 4px; }
414
+ .universal-card-img-combination--trisect .universal-card-img-combination__item {
415
+ width: calc(33.33% - 2.67px); aspect-ratio: 1;
416
+ }
417
+
418
+ /* ---- select_img 多图选择(对齐网页版 .universal-card-select-img) ---- */
419
+ .universal-card-select-img {
420
+ width: 100%; max-width: 100%; display: grid; grid-gap: 4px; box-sizing: border-box;
421
+ }
422
+ .universal-card-select-img--stretch { grid-template-columns: 1fr; }
423
+ .universal-card-select-img--bisect { grid-template-columns: 1fr 1fr; }
424
+ .universal-card-select-img--trisect { grid-template-columns: 1fr 1fr 1fr; }
425
+ .universal-card-select-img__item-wrapper { position: relative; border-radius: 6px; }
426
+ .universal-card-select-img__item-wrapper::after {
427
+ content: " "; position: absolute; inset: 0; box-sizing: border-box;
428
+ border: 1px solid var(--N300); border-radius: 6px; pointer-events: none; z-index: 1;
429
+ }
430
+ .universal-card-select-img__item-wrapper--selected::after { border: 2px solid var(--B500); }
431
+ .universal-card-select-img__item {
432
+ position: relative; overflow: hidden; box-sizing: border-box; border-radius: 6px;
433
+ cursor: pointer; width: 100%;
434
+ }
435
+ .universal-card-select-img__item--disabled { cursor: not-allowed; }
436
+ .universal-card-select-img__item--disabled::after {
437
+ content: " "; position: absolute; inset: 0; z-index: 1;
438
+ background-color: rgba(255, 255, 255, .5);
439
+ }
440
+ .universal-card-select-img__item > .universal-card-image__host {
441
+ display: block; width: 100%; height: 100%; object-fit: cover;
442
+ }
443
+ .universal-card-select-img__item > .universal-card-image__placeholder {
444
+ width: 100%; height: 100%;
445
+ }
446
+ /* 表单内复选/单选标记(真机仅 in-form 显示;表单外用选中描边表达状态) */
447
+ .universal-card-select-img__checkbox {
448
+ position: absolute; top: 0; right: 0; z-index: 2; padding: 5px 8px; cursor: pointer;
449
+ }
450
+ .universal-card-select-img__checkbox--disabled { cursor: not-allowed; }
451
+ .universal-card-select-img__checkbox-box {
452
+ display: inline-flex; align-items: center; justify-content: center;
453
+ width: 16px; height: 16px; box-sizing: border-box; border-radius: 50%;
454
+ background-color: rgba(0, 0, 0, .15); border: 1px solid #eff0f1;
455
+ box-shadow: 0 0 2px rgba(0, 0, 0, .1);
456
+ }
457
+ .universal-card-select-img__checkbox-box--square { border-radius: 4px; }
458
+ .universal-card-select-img__checkbox-box--checked {
459
+ background-color: var(--B500); border-color: var(--B500);
460
+ }
461
+ .universal-card-select-img__checkbox-box--checked::after {
462
+ content: ""; width: 3.5px; height: 7px;
463
+ border: solid #fff; border-width: 0 2px 2px 0;
464
+ transform: rotate(45deg) translateY(-1px);
465
+ }
466
+
383
467
  /* ---- 按钮 ---- */
384
468
  /* 飞书按钮文本是纯文本、单行:min-height 容纳偶发换行,文字超长省略号收尾。
385
469
  样式全集逆向自网页版 ud__button:三大族——outlined(描边)/ text(纯文字)/ filled(实心),
package/dist/index.cjs CHANGED
@@ -850,8 +850,13 @@ function resolveCornerRadius(property) {
850
850
  }
851
851
  if (direct != null) return sizeToPx(direct);
852
852
  const echo = property.cornerRadiusValue;
853
- if (echo && typeof echo.value === "number") return echo.value + "px";
854
- if (echo && typeof echo.value === "string" && echo.value.trim()) return sizeToPx(echo.value);
853
+ if (typeof echo === "number") return echo + "px";
854
+ if (typeof echo === "string" && echo.trim()) return sizeToPx(echo);
855
+ if (echo && typeof echo === "object") {
856
+ const value = echo.value;
857
+ if (typeof value === "number") return value + "px";
858
+ if (typeof value === "string" && value.trim()) return sizeToPx(value);
859
+ }
855
860
  return void 0;
856
861
  }
857
862
  function boxToStyle(box) {
@@ -1352,6 +1357,90 @@ var BASE_STYLES_CSS = `/**
1352
1357
  font-size: 12px;
1353
1358
  }
1354
1359
 
1360
+ /* ---- img_combination \u591A\u56FE\u6DF7\u6392\uFF08\u6BD4\u4F8B\u9006\u5411\u81EA\u7F51\u9875\u7248 .universal-card-img-combination\uFF09 ---- */
1361
+ .universal-card-img-combination { display: flex; width: 100%; justify-content: space-between; }
1362
+ .universal-card-img-combination__item { min-width: 0; overflow: hidden; }
1363
+ .universal-card-img-combination__item > .universal-card-image__host { display: block; width: 100%; height: 100%; object-fit: cover; }
1364
+ .universal-card-img-combination__item > .universal-card-image__placeholder { width: 100%; height: 100%; }
1365
+ /* double\uFF1A\u5DE6\u7A84(32.4%, 24:33) + \u53F3\u5BBD(67.6%, 49.33:33) */
1366
+ .universal-card-img-combination--double .universal-card-img-combination__item:nth-child(odd) {
1367
+ width: 32.4%; aspect-ratio: 24 / 33;
1368
+ }
1369
+ .universal-card-img-combination--double .universal-card-img-combination__item:nth-child(2n) {
1370
+ width: calc(67.6% - 4px); aspect-ratio: 49.33 / 33;
1371
+ }
1372
+ /* triple\uFF1A\u5DE6\u5927\u56FE(66.5%, 1:1) + \u53F3\u5217\u4E24\u5F20\u5C0F\u56FE */
1373
+ .universal-card-img-combination--triple .universal-card-img-combination__left {
1374
+ width: 66.5%; aspect-ratio: 1; margin-right: 1%;
1375
+ }
1376
+ .universal-card-img-combination--triple .universal-card-img-combination__right {
1377
+ width: 33%; display: flex; flex-direction: column; justify-content: space-between;
1378
+ }
1379
+ .universal-card-img-combination--triple .universal-card-img-combination__right .universal-card-img-combination__item {
1380
+ width: 100%; aspect-ratio: 1;
1381
+ }
1382
+ .universal-card-img-combination--triple .universal-card-img-combination__right .universal-card-img-combination__item:first-child {
1383
+ margin-bottom: 1.5%;
1384
+ }
1385
+ /* bisect / trisect\uFF1A\u7B49\u5BBD\u65B9\u683C\u6362\u884C */
1386
+ .universal-card-img-combination--bisect { flex-wrap: wrap; gap: 4px; }
1387
+ .universal-card-img-combination--bisect .universal-card-img-combination__item {
1388
+ width: calc(50% - 2px); aspect-ratio: 1;
1389
+ }
1390
+ .universal-card-img-combination--trisect { flex-wrap: wrap; justify-content: flex-start; gap: 4px; }
1391
+ .universal-card-img-combination--trisect .universal-card-img-combination__item {
1392
+ width: calc(33.33% - 2.67px); aspect-ratio: 1;
1393
+ }
1394
+
1395
+ /* ---- select_img \u591A\u56FE\u9009\u62E9\uFF08\u5BF9\u9F50\u7F51\u9875\u7248 .universal-card-select-img\uFF09 ---- */
1396
+ .universal-card-select-img {
1397
+ width: 100%; max-width: 100%; display: grid; grid-gap: 4px; box-sizing: border-box;
1398
+ }
1399
+ .universal-card-select-img--stretch { grid-template-columns: 1fr; }
1400
+ .universal-card-select-img--bisect { grid-template-columns: 1fr 1fr; }
1401
+ .universal-card-select-img--trisect { grid-template-columns: 1fr 1fr 1fr; }
1402
+ .universal-card-select-img__item-wrapper { position: relative; border-radius: 6px; }
1403
+ .universal-card-select-img__item-wrapper::after {
1404
+ content: " "; position: absolute; inset: 0; box-sizing: border-box;
1405
+ border: 1px solid var(--N300); border-radius: 6px; pointer-events: none; z-index: 1;
1406
+ }
1407
+ .universal-card-select-img__item-wrapper--selected::after { border: 2px solid var(--B500); }
1408
+ .universal-card-select-img__item {
1409
+ position: relative; overflow: hidden; box-sizing: border-box; border-radius: 6px;
1410
+ cursor: pointer; width: 100%;
1411
+ }
1412
+ .universal-card-select-img__item--disabled { cursor: not-allowed; }
1413
+ .universal-card-select-img__item--disabled::after {
1414
+ content: " "; position: absolute; inset: 0; z-index: 1;
1415
+ background-color: rgba(255, 255, 255, .5);
1416
+ }
1417
+ .universal-card-select-img__item > .universal-card-image__host {
1418
+ display: block; width: 100%; height: 100%; object-fit: cover;
1419
+ }
1420
+ .universal-card-select-img__item > .universal-card-image__placeholder {
1421
+ width: 100%; height: 100%;
1422
+ }
1423
+ /* \u8868\u5355\u5185\u590D\u9009/\u5355\u9009\u6807\u8BB0\uFF08\u771F\u673A\u4EC5 in-form \u663E\u793A\uFF1B\u8868\u5355\u5916\u7528\u9009\u4E2D\u63CF\u8FB9\u8868\u8FBE\u72B6\u6001\uFF09 */
1424
+ .universal-card-select-img__checkbox {
1425
+ position: absolute; top: 0; right: 0; z-index: 2; padding: 5px 8px; cursor: pointer;
1426
+ }
1427
+ .universal-card-select-img__checkbox--disabled { cursor: not-allowed; }
1428
+ .universal-card-select-img__checkbox-box {
1429
+ display: inline-flex; align-items: center; justify-content: center;
1430
+ width: 16px; height: 16px; box-sizing: border-box; border-radius: 50%;
1431
+ background-color: rgba(0, 0, 0, .15); border: 1px solid #eff0f1;
1432
+ box-shadow: 0 0 2px rgba(0, 0, 0, .1);
1433
+ }
1434
+ .universal-card-select-img__checkbox-box--square { border-radius: 4px; }
1435
+ .universal-card-select-img__checkbox-box--checked {
1436
+ background-color: var(--B500); border-color: var(--B500);
1437
+ }
1438
+ .universal-card-select-img__checkbox-box--checked::after {
1439
+ content: ""; width: 3.5px; height: 7px;
1440
+ border: solid #fff; border-width: 0 2px 2px 0;
1441
+ transform: rotate(45deg) translateY(-1px);
1442
+ }
1443
+
1355
1444
  /* ---- \u6309\u94AE ---- */
1356
1445
  /* \u98DE\u4E66\u6309\u94AE\u6587\u672C\u662F\u7EAF\u6587\u672C\u3001\u5355\u884C\uFF1Amin-height \u5BB9\u7EB3\u5076\u53D1\u6362\u884C\uFF0C\u6587\u5B57\u8D85\u957F\u7701\u7565\u53F7\u6536\u5C3E\u3002
1357
1446
  \u6837\u5F0F\u5168\u96C6\u9006\u5411\u81EA\u7F51\u9875\u7248 ud__button\uFF1A\u4E09\u5927\u65CF\u2014\u2014outlined\uFF08\u63CF\u8FB9\uFF09/ text\uFF08\u7EAF\u6587\u5B57\uFF09/ filled\uFF08\u5B9E\u5FC3\uFF09\uFF0C
@@ -5988,6 +6077,123 @@ function registerBuiltins() {
5988
6077
  src ? h("img", { className: `${CLS}-image__host`, src, alt: altText, style: imgStyle }) : h("div", { className: `${CLS}-image__placeholder`, style: imgStyle }, altText || "[\u56FE\u7247]")
5989
6078
  );
5990
6079
  });
6080
+ const IMG_COMBINATION_LIMIT = {
6081
+ double: 2,
6082
+ triple: 3
6083
+ };
6084
+ function renderCombinationImage(item, cornerRadius, ctx) {
6085
+ const src = resolveImageSrc(item, ctx);
6086
+ const style = {
6087
+ width: "100%",
6088
+ height: "100%",
6089
+ objectFit: "cover",
6090
+ display: "block"
6091
+ };
6092
+ if (cornerRadius) style.borderRadius = cornerRadius;
6093
+ return src ? h("img", { className: `${CLS}-image__host`, src, alt: "", style }) : h("div", { className: `${CLS}-image__placeholder`, style }, "[\u56FE\u7247]");
6094
+ }
6095
+ registerComponent("img_combination", (el, ctx) => {
6096
+ const p = el.property || {};
6097
+ const rawList = Array.isArray(p.imgList) ? p.imgList : Array.isArray(p.img_list) ? p.img_list : [];
6098
+ if (rawList.length === 0) return null;
6099
+ const mode = String(p.combinationMode ?? p.combination_mode ?? "double");
6100
+ const limit = IMG_COMBINATION_LIMIT[mode];
6101
+ const list = (limit ? rawList.slice(0, limit) : rawList).map((item) => typeof item === "string" ? { img_key: item } : item).filter(isRecord);
6102
+ if (list.length === 0) return null;
6103
+ const cornerRadius = resolveCornerRadius(p);
6104
+ const containerStyle = {};
6105
+ applyOuterMargin(containerStyle, p.margin);
6106
+ const cls = `${CLS}-img-combination ${CLS}-img-combination--${mode}`;
6107
+ const itemNode = (item) => h(
6108
+ "div",
6109
+ { className: `${CLS}-img-combination__item` },
6110
+ renderCombinationImage(item, cornerRadius, ctx)
6111
+ );
6112
+ if (mode === "triple") {
6113
+ const [first, ...rest] = list;
6114
+ return h(
6115
+ "div",
6116
+ { className: cls, style: containerStyle },
6117
+ h(
6118
+ "div",
6119
+ { className: `${CLS}-img-combination__item ${CLS}-img-combination__left` },
6120
+ renderCombinationImage(first, cornerRadius, ctx)
6121
+ ),
6122
+ h("div", { className: `${CLS}-img-combination__right` }, rest.map(itemNode))
6123
+ );
6124
+ }
6125
+ return h("div", { className: cls, style: containerStyle }, list.map(itemNode));
6126
+ });
6127
+ function readSelectImgValues(p) {
6128
+ const raw = p.selectedValues ?? p.selected_values ?? p.value;
6129
+ let value = raw;
6130
+ if (typeof value === "string") {
6131
+ try {
6132
+ value = JSON.parse(value);
6133
+ } catch {
6134
+ return typeof value === "string" && value ? [value] : [];
6135
+ }
6136
+ }
6137
+ if (isRecord(value) && Array.isArray(value.value)) value = value.value;
6138
+ if (!Array.isArray(value)) return [];
6139
+ return value.filter((item) => typeof item === "string");
6140
+ }
6141
+ function renderSelectImgCheckbox(checked, disabled, multiple) {
6142
+ const boxClass = `${CLS}-select-img__checkbox-box` + (multiple ? ` ${CLS}-select-img__checkbox-box--square` : "") + (checked ? ` ${CLS}-select-img__checkbox-box--checked` : "");
6143
+ return h(
6144
+ "div",
6145
+ {
6146
+ className: `${CLS}-select-img__checkbox` + (disabled ? ` ${CLS}-select-img__checkbox--disabled` : "")
6147
+ },
6148
+ h("span", { className: boxClass })
6149
+ );
6150
+ }
6151
+ registerComponent("select_img", (el, ctx) => {
6152
+ const p = el.property || {};
6153
+ const options = Array.isArray(p.options) ? p.options : [];
6154
+ if (options.length === 0) return null;
6155
+ const layout = String(p.layout ?? "bisect");
6156
+ const aspectRatio = String(p.aspectRatio ?? p.aspect_ratio ?? "16:9").replace(":", "/");
6157
+ const multiple = p.multiSelect === true || p.multi_select === true;
6158
+ const disabledAll = p.disabled === true;
6159
+ const selected = readSelectImgValues(p);
6160
+ const containerStyle = {};
6161
+ applyOuterMargin(containerStyle, p.margin);
6162
+ const items = options.map((rawOption) => {
6163
+ const option = isRecord(rawOption) ? rawOption : { img_key: rawOption };
6164
+ const value = typeof option.value === "string" ? option.value : void 0;
6165
+ const isSelected = value !== void 0 && selected.includes(value);
6166
+ const disabled = disabledAll || option.disabled === true;
6167
+ const tip = disabled ? extractText(option.disabled_tips ?? option.disabledTips) : extractText(option.hover_tips ?? option.hoverTips);
6168
+ const src = resolveImageSrc(option, ctx);
6169
+ const wrapperClass = `${CLS}-select-img__item-wrapper` + (isSelected ? ` ${CLS}-select-img__item-wrapper--selected` : "") + (disabled ? ` ${CLS}-select-img__item-wrapper--disabled` : "");
6170
+ const itemClass = `${CLS}-select-img__item` + (disabled ? ` ${CLS}-select-img__item--disabled` : "");
6171
+ return h(
6172
+ "div",
6173
+ { className: wrapperClass },
6174
+ h(
6175
+ "div",
6176
+ { className: itemClass, style: { aspectRatio }, title: tip || void 0 },
6177
+ ctx.inForm ? renderSelectImgCheckbox(isSelected, disabled, multiple) : null,
6178
+ src ? h("img", {
6179
+ className: `${CLS}-image__host`,
6180
+ src,
6181
+ alt: "",
6182
+ style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
6183
+ }) : h(
6184
+ "div",
6185
+ { className: `${CLS}-image__placeholder`, style: { width: "100%", height: "100%" } },
6186
+ "[\u56FE\u7247]"
6187
+ )
6188
+ )
6189
+ );
6190
+ });
6191
+ return h(
6192
+ "div",
6193
+ { className: `${CLS}-select-img ${CLS}-select-img--${layout}`, style: containerStyle },
6194
+ items
6195
+ );
6196
+ });
5991
6197
  registerComponent("video", (el, ctx) => {
5992
6198
  const p = el.property || {};
5993
6199
  const src = resolveMediaSrc(p, "video", ctx);
@@ -6363,7 +6569,7 @@ function registerBuiltins() {
6363
6569
  );
6364
6570
  });
6365
6571
  registerComponent("form", (el, ctx) => {
6366
- const children = renderElements(el.property?.elements, ctx);
6572
+ const children = renderElements(el.property?.elements, { ...ctx, inForm: true });
6367
6573
  const className = `${CLS}-form`;
6368
6574
  if (!ctx.options.interactive) return h("div", { className }, children);
6369
6575
  const name = typeof el.property?.name === "string" ? el.property.name : "";
package/dist/index.d.cts CHANGED
@@ -464,6 +464,11 @@ interface RenderContext {
464
464
  * 客户端把 body paddingTop 从 12 改为 8。由 index.ts 在渲染 body 前注入。
465
465
  */
466
466
  bodyPaddingTop?: number;
467
+ /**
468
+ * 是否处于 form 容器内。真机 select_img 仅在表单内显示选中态 checkbox/radio,
469
+ * 表单外只是可点击的图片选项。form 渲染子元素时注入,不改变外部 ctx。
470
+ */
471
+ inForm?: boolean;
467
472
  }
468
473
  /** 组件渲染函数 */
469
474
  type ComponentFn = (el: CardElement, ctx: RenderContext) => VNode | null;
package/dist/index.d.ts CHANGED
@@ -464,6 +464,11 @@ interface RenderContext {
464
464
  * 客户端把 body paddingTop 从 12 改为 8。由 index.ts 在渲染 body 前注入。
465
465
  */
466
466
  bodyPaddingTop?: number;
467
+ /**
468
+ * 是否处于 form 容器内。真机 select_img 仅在表单内显示选中态 checkbox/radio,
469
+ * 表单外只是可点击的图片选项。form 渲染子元素时注入,不改变外部 ctx。
470
+ */
471
+ inForm?: boolean;
467
472
  }
468
473
  /** 组件渲染函数 */
469
474
  type ComponentFn = (el: CardElement, ctx: RenderContext) => VNode | null;
package/dist/index.js CHANGED
@@ -849,8 +849,13 @@ function resolveCornerRadius(property) {
849
849
  }
850
850
  if (direct != null) return sizeToPx(direct);
851
851
  const echo = property.cornerRadiusValue;
852
- if (echo && typeof echo.value === "number") return echo.value + "px";
853
- if (echo && typeof echo.value === "string" && echo.value.trim()) return sizeToPx(echo.value);
852
+ if (typeof echo === "number") return echo + "px";
853
+ if (typeof echo === "string" && echo.trim()) return sizeToPx(echo);
854
+ if (echo && typeof echo === "object") {
855
+ const value = echo.value;
856
+ if (typeof value === "number") return value + "px";
857
+ if (typeof value === "string" && value.trim()) return sizeToPx(value);
858
+ }
854
859
  return void 0;
855
860
  }
856
861
  function boxToStyle(box) {
@@ -1351,6 +1356,90 @@ var BASE_STYLES_CSS = `/**
1351
1356
  font-size: 12px;
1352
1357
  }
1353
1358
 
1359
+ /* ---- img_combination \u591A\u56FE\u6DF7\u6392\uFF08\u6BD4\u4F8B\u9006\u5411\u81EA\u7F51\u9875\u7248 .universal-card-img-combination\uFF09 ---- */
1360
+ .universal-card-img-combination { display: flex; width: 100%; justify-content: space-between; }
1361
+ .universal-card-img-combination__item { min-width: 0; overflow: hidden; }
1362
+ .universal-card-img-combination__item > .universal-card-image__host { display: block; width: 100%; height: 100%; object-fit: cover; }
1363
+ .universal-card-img-combination__item > .universal-card-image__placeholder { width: 100%; height: 100%; }
1364
+ /* double\uFF1A\u5DE6\u7A84(32.4%, 24:33) + \u53F3\u5BBD(67.6%, 49.33:33) */
1365
+ .universal-card-img-combination--double .universal-card-img-combination__item:nth-child(odd) {
1366
+ width: 32.4%; aspect-ratio: 24 / 33;
1367
+ }
1368
+ .universal-card-img-combination--double .universal-card-img-combination__item:nth-child(2n) {
1369
+ width: calc(67.6% - 4px); aspect-ratio: 49.33 / 33;
1370
+ }
1371
+ /* triple\uFF1A\u5DE6\u5927\u56FE(66.5%, 1:1) + \u53F3\u5217\u4E24\u5F20\u5C0F\u56FE */
1372
+ .universal-card-img-combination--triple .universal-card-img-combination__left {
1373
+ width: 66.5%; aspect-ratio: 1; margin-right: 1%;
1374
+ }
1375
+ .universal-card-img-combination--triple .universal-card-img-combination__right {
1376
+ width: 33%; display: flex; flex-direction: column; justify-content: space-between;
1377
+ }
1378
+ .universal-card-img-combination--triple .universal-card-img-combination__right .universal-card-img-combination__item {
1379
+ width: 100%; aspect-ratio: 1;
1380
+ }
1381
+ .universal-card-img-combination--triple .universal-card-img-combination__right .universal-card-img-combination__item:first-child {
1382
+ margin-bottom: 1.5%;
1383
+ }
1384
+ /* bisect / trisect\uFF1A\u7B49\u5BBD\u65B9\u683C\u6362\u884C */
1385
+ .universal-card-img-combination--bisect { flex-wrap: wrap; gap: 4px; }
1386
+ .universal-card-img-combination--bisect .universal-card-img-combination__item {
1387
+ width: calc(50% - 2px); aspect-ratio: 1;
1388
+ }
1389
+ .universal-card-img-combination--trisect { flex-wrap: wrap; justify-content: flex-start; gap: 4px; }
1390
+ .universal-card-img-combination--trisect .universal-card-img-combination__item {
1391
+ width: calc(33.33% - 2.67px); aspect-ratio: 1;
1392
+ }
1393
+
1394
+ /* ---- select_img \u591A\u56FE\u9009\u62E9\uFF08\u5BF9\u9F50\u7F51\u9875\u7248 .universal-card-select-img\uFF09 ---- */
1395
+ .universal-card-select-img {
1396
+ width: 100%; max-width: 100%; display: grid; grid-gap: 4px; box-sizing: border-box;
1397
+ }
1398
+ .universal-card-select-img--stretch { grid-template-columns: 1fr; }
1399
+ .universal-card-select-img--bisect { grid-template-columns: 1fr 1fr; }
1400
+ .universal-card-select-img--trisect { grid-template-columns: 1fr 1fr 1fr; }
1401
+ .universal-card-select-img__item-wrapper { position: relative; border-radius: 6px; }
1402
+ .universal-card-select-img__item-wrapper::after {
1403
+ content: " "; position: absolute; inset: 0; box-sizing: border-box;
1404
+ border: 1px solid var(--N300); border-radius: 6px; pointer-events: none; z-index: 1;
1405
+ }
1406
+ .universal-card-select-img__item-wrapper--selected::after { border: 2px solid var(--B500); }
1407
+ .universal-card-select-img__item {
1408
+ position: relative; overflow: hidden; box-sizing: border-box; border-radius: 6px;
1409
+ cursor: pointer; width: 100%;
1410
+ }
1411
+ .universal-card-select-img__item--disabled { cursor: not-allowed; }
1412
+ .universal-card-select-img__item--disabled::after {
1413
+ content: " "; position: absolute; inset: 0; z-index: 1;
1414
+ background-color: rgba(255, 255, 255, .5);
1415
+ }
1416
+ .universal-card-select-img__item > .universal-card-image__host {
1417
+ display: block; width: 100%; height: 100%; object-fit: cover;
1418
+ }
1419
+ .universal-card-select-img__item > .universal-card-image__placeholder {
1420
+ width: 100%; height: 100%;
1421
+ }
1422
+ /* \u8868\u5355\u5185\u590D\u9009/\u5355\u9009\u6807\u8BB0\uFF08\u771F\u673A\u4EC5 in-form \u663E\u793A\uFF1B\u8868\u5355\u5916\u7528\u9009\u4E2D\u63CF\u8FB9\u8868\u8FBE\u72B6\u6001\uFF09 */
1423
+ .universal-card-select-img__checkbox {
1424
+ position: absolute; top: 0; right: 0; z-index: 2; padding: 5px 8px; cursor: pointer;
1425
+ }
1426
+ .universal-card-select-img__checkbox--disabled { cursor: not-allowed; }
1427
+ .universal-card-select-img__checkbox-box {
1428
+ display: inline-flex; align-items: center; justify-content: center;
1429
+ width: 16px; height: 16px; box-sizing: border-box; border-radius: 50%;
1430
+ background-color: rgba(0, 0, 0, .15); border: 1px solid #eff0f1;
1431
+ box-shadow: 0 0 2px rgba(0, 0, 0, .1);
1432
+ }
1433
+ .universal-card-select-img__checkbox-box--square { border-radius: 4px; }
1434
+ .universal-card-select-img__checkbox-box--checked {
1435
+ background-color: var(--B500); border-color: var(--B500);
1436
+ }
1437
+ .universal-card-select-img__checkbox-box--checked::after {
1438
+ content: ""; width: 3.5px; height: 7px;
1439
+ border: solid #fff; border-width: 0 2px 2px 0;
1440
+ transform: rotate(45deg) translateY(-1px);
1441
+ }
1442
+
1354
1443
  /* ---- \u6309\u94AE ---- */
1355
1444
  /* \u98DE\u4E66\u6309\u94AE\u6587\u672C\u662F\u7EAF\u6587\u672C\u3001\u5355\u884C\uFF1Amin-height \u5BB9\u7EB3\u5076\u53D1\u6362\u884C\uFF0C\u6587\u5B57\u8D85\u957F\u7701\u7565\u53F7\u6536\u5C3E\u3002
1356
1445
  \u6837\u5F0F\u5168\u96C6\u9006\u5411\u81EA\u7F51\u9875\u7248 ud__button\uFF1A\u4E09\u5927\u65CF\u2014\u2014outlined\uFF08\u63CF\u8FB9\uFF09/ text\uFF08\u7EAF\u6587\u5B57\uFF09/ filled\uFF08\u5B9E\u5FC3\uFF09\uFF0C
@@ -5987,6 +6076,123 @@ function registerBuiltins() {
5987
6076
  src ? h("img", { className: `${CLS}-image__host`, src, alt: altText, style: imgStyle }) : h("div", { className: `${CLS}-image__placeholder`, style: imgStyle }, altText || "[\u56FE\u7247]")
5988
6077
  );
5989
6078
  });
6079
+ const IMG_COMBINATION_LIMIT = {
6080
+ double: 2,
6081
+ triple: 3
6082
+ };
6083
+ function renderCombinationImage(item, cornerRadius, ctx) {
6084
+ const src = resolveImageSrc(item, ctx);
6085
+ const style = {
6086
+ width: "100%",
6087
+ height: "100%",
6088
+ objectFit: "cover",
6089
+ display: "block"
6090
+ };
6091
+ if (cornerRadius) style.borderRadius = cornerRadius;
6092
+ return src ? h("img", { className: `${CLS}-image__host`, src, alt: "", style }) : h("div", { className: `${CLS}-image__placeholder`, style }, "[\u56FE\u7247]");
6093
+ }
6094
+ registerComponent("img_combination", (el, ctx) => {
6095
+ const p = el.property || {};
6096
+ const rawList = Array.isArray(p.imgList) ? p.imgList : Array.isArray(p.img_list) ? p.img_list : [];
6097
+ if (rawList.length === 0) return null;
6098
+ const mode = String(p.combinationMode ?? p.combination_mode ?? "double");
6099
+ const limit = IMG_COMBINATION_LIMIT[mode];
6100
+ const list = (limit ? rawList.slice(0, limit) : rawList).map((item) => typeof item === "string" ? { img_key: item } : item).filter(isRecord);
6101
+ if (list.length === 0) return null;
6102
+ const cornerRadius = resolveCornerRadius(p);
6103
+ const containerStyle = {};
6104
+ applyOuterMargin(containerStyle, p.margin);
6105
+ const cls = `${CLS}-img-combination ${CLS}-img-combination--${mode}`;
6106
+ const itemNode = (item) => h(
6107
+ "div",
6108
+ { className: `${CLS}-img-combination__item` },
6109
+ renderCombinationImage(item, cornerRadius, ctx)
6110
+ );
6111
+ if (mode === "triple") {
6112
+ const [first, ...rest] = list;
6113
+ return h(
6114
+ "div",
6115
+ { className: cls, style: containerStyle },
6116
+ h(
6117
+ "div",
6118
+ { className: `${CLS}-img-combination__item ${CLS}-img-combination__left` },
6119
+ renderCombinationImage(first, cornerRadius, ctx)
6120
+ ),
6121
+ h("div", { className: `${CLS}-img-combination__right` }, rest.map(itemNode))
6122
+ );
6123
+ }
6124
+ return h("div", { className: cls, style: containerStyle }, list.map(itemNode));
6125
+ });
6126
+ function readSelectImgValues(p) {
6127
+ const raw = p.selectedValues ?? p.selected_values ?? p.value;
6128
+ let value = raw;
6129
+ if (typeof value === "string") {
6130
+ try {
6131
+ value = JSON.parse(value);
6132
+ } catch {
6133
+ return typeof value === "string" && value ? [value] : [];
6134
+ }
6135
+ }
6136
+ if (isRecord(value) && Array.isArray(value.value)) value = value.value;
6137
+ if (!Array.isArray(value)) return [];
6138
+ return value.filter((item) => typeof item === "string");
6139
+ }
6140
+ function renderSelectImgCheckbox(checked, disabled, multiple) {
6141
+ const boxClass = `${CLS}-select-img__checkbox-box` + (multiple ? ` ${CLS}-select-img__checkbox-box--square` : "") + (checked ? ` ${CLS}-select-img__checkbox-box--checked` : "");
6142
+ return h(
6143
+ "div",
6144
+ {
6145
+ className: `${CLS}-select-img__checkbox` + (disabled ? ` ${CLS}-select-img__checkbox--disabled` : "")
6146
+ },
6147
+ h("span", { className: boxClass })
6148
+ );
6149
+ }
6150
+ registerComponent("select_img", (el, ctx) => {
6151
+ const p = el.property || {};
6152
+ const options = Array.isArray(p.options) ? p.options : [];
6153
+ if (options.length === 0) return null;
6154
+ const layout = String(p.layout ?? "bisect");
6155
+ const aspectRatio = String(p.aspectRatio ?? p.aspect_ratio ?? "16:9").replace(":", "/");
6156
+ const multiple = p.multiSelect === true || p.multi_select === true;
6157
+ const disabledAll = p.disabled === true;
6158
+ const selected = readSelectImgValues(p);
6159
+ const containerStyle = {};
6160
+ applyOuterMargin(containerStyle, p.margin);
6161
+ const items = options.map((rawOption) => {
6162
+ const option = isRecord(rawOption) ? rawOption : { img_key: rawOption };
6163
+ const value = typeof option.value === "string" ? option.value : void 0;
6164
+ const isSelected = value !== void 0 && selected.includes(value);
6165
+ const disabled = disabledAll || option.disabled === true;
6166
+ const tip = disabled ? extractText(option.disabled_tips ?? option.disabledTips) : extractText(option.hover_tips ?? option.hoverTips);
6167
+ const src = resolveImageSrc(option, ctx);
6168
+ const wrapperClass = `${CLS}-select-img__item-wrapper` + (isSelected ? ` ${CLS}-select-img__item-wrapper--selected` : "") + (disabled ? ` ${CLS}-select-img__item-wrapper--disabled` : "");
6169
+ const itemClass = `${CLS}-select-img__item` + (disabled ? ` ${CLS}-select-img__item--disabled` : "");
6170
+ return h(
6171
+ "div",
6172
+ { className: wrapperClass },
6173
+ h(
6174
+ "div",
6175
+ { className: itemClass, style: { aspectRatio }, title: tip || void 0 },
6176
+ ctx.inForm ? renderSelectImgCheckbox(isSelected, disabled, multiple) : null,
6177
+ src ? h("img", {
6178
+ className: `${CLS}-image__host`,
6179
+ src,
6180
+ alt: "",
6181
+ style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
6182
+ }) : h(
6183
+ "div",
6184
+ { className: `${CLS}-image__placeholder`, style: { width: "100%", height: "100%" } },
6185
+ "[\u56FE\u7247]"
6186
+ )
6187
+ )
6188
+ );
6189
+ });
6190
+ return h(
6191
+ "div",
6192
+ { className: `${CLS}-select-img ${CLS}-select-img--${layout}`, style: containerStyle },
6193
+ items
6194
+ );
6195
+ });
5990
6196
  registerComponent("video", (el, ctx) => {
5991
6197
  const p = el.property || {};
5992
6198
  const src = resolveMediaSrc(p, "video", ctx);
@@ -6362,7 +6568,7 @@ function registerBuiltins() {
6362
6568
  );
6363
6569
  });
6364
6570
  registerComponent("form", (el, ctx) => {
6365
- const children = renderElements(el.property?.elements, ctx);
6571
+ const children = renderElements(el.property?.elements, { ...ctx, inForm: true });
6366
6572
  const className = `${CLS}-form`;
6367
6573
  if (!ctx.options.interactive) return h("div", { className }, children);
6368
6574
  const name = typeof el.property?.name === "string" ? el.property.name : "";
package/dist/styles.css CHANGED
@@ -380,6 +380,90 @@
380
380
  font-size: 12px;
381
381
  }
382
382
 
383
+ /* ---- img_combination 多图混排(比例逆向自网页版 .universal-card-img-combination) ---- */
384
+ .universal-card-img-combination { display: flex; width: 100%; justify-content: space-between; }
385
+ .universal-card-img-combination__item { min-width: 0; overflow: hidden; }
386
+ .universal-card-img-combination__item > .universal-card-image__host { display: block; width: 100%; height: 100%; object-fit: cover; }
387
+ .universal-card-img-combination__item > .universal-card-image__placeholder { width: 100%; height: 100%; }
388
+ /* double:左窄(32.4%, 24:33) + 右宽(67.6%, 49.33:33) */
389
+ .universal-card-img-combination--double .universal-card-img-combination__item:nth-child(odd) {
390
+ width: 32.4%; aspect-ratio: 24 / 33;
391
+ }
392
+ .universal-card-img-combination--double .universal-card-img-combination__item:nth-child(2n) {
393
+ width: calc(67.6% - 4px); aspect-ratio: 49.33 / 33;
394
+ }
395
+ /* triple:左大图(66.5%, 1:1) + 右列两张小图 */
396
+ .universal-card-img-combination--triple .universal-card-img-combination__left {
397
+ width: 66.5%; aspect-ratio: 1; margin-right: 1%;
398
+ }
399
+ .universal-card-img-combination--triple .universal-card-img-combination__right {
400
+ width: 33%; display: flex; flex-direction: column; justify-content: space-between;
401
+ }
402
+ .universal-card-img-combination--triple .universal-card-img-combination__right .universal-card-img-combination__item {
403
+ width: 100%; aspect-ratio: 1;
404
+ }
405
+ .universal-card-img-combination--triple .universal-card-img-combination__right .universal-card-img-combination__item:first-child {
406
+ margin-bottom: 1.5%;
407
+ }
408
+ /* bisect / trisect:等宽方格换行 */
409
+ .universal-card-img-combination--bisect { flex-wrap: wrap; gap: 4px; }
410
+ .universal-card-img-combination--bisect .universal-card-img-combination__item {
411
+ width: calc(50% - 2px); aspect-ratio: 1;
412
+ }
413
+ .universal-card-img-combination--trisect { flex-wrap: wrap; justify-content: flex-start; gap: 4px; }
414
+ .universal-card-img-combination--trisect .universal-card-img-combination__item {
415
+ width: calc(33.33% - 2.67px); aspect-ratio: 1;
416
+ }
417
+
418
+ /* ---- select_img 多图选择(对齐网页版 .universal-card-select-img) ---- */
419
+ .universal-card-select-img {
420
+ width: 100%; max-width: 100%; display: grid; grid-gap: 4px; box-sizing: border-box;
421
+ }
422
+ .universal-card-select-img--stretch { grid-template-columns: 1fr; }
423
+ .universal-card-select-img--bisect { grid-template-columns: 1fr 1fr; }
424
+ .universal-card-select-img--trisect { grid-template-columns: 1fr 1fr 1fr; }
425
+ .universal-card-select-img__item-wrapper { position: relative; border-radius: 6px; }
426
+ .universal-card-select-img__item-wrapper::after {
427
+ content: " "; position: absolute; inset: 0; box-sizing: border-box;
428
+ border: 1px solid var(--N300); border-radius: 6px; pointer-events: none; z-index: 1;
429
+ }
430
+ .universal-card-select-img__item-wrapper--selected::after { border: 2px solid var(--B500); }
431
+ .universal-card-select-img__item {
432
+ position: relative; overflow: hidden; box-sizing: border-box; border-radius: 6px;
433
+ cursor: pointer; width: 100%;
434
+ }
435
+ .universal-card-select-img__item--disabled { cursor: not-allowed; }
436
+ .universal-card-select-img__item--disabled::after {
437
+ content: " "; position: absolute; inset: 0; z-index: 1;
438
+ background-color: rgba(255, 255, 255, .5);
439
+ }
440
+ .universal-card-select-img__item > .universal-card-image__host {
441
+ display: block; width: 100%; height: 100%; object-fit: cover;
442
+ }
443
+ .universal-card-select-img__item > .universal-card-image__placeholder {
444
+ width: 100%; height: 100%;
445
+ }
446
+ /* 表单内复选/单选标记(真机仅 in-form 显示;表单外用选中描边表达状态) */
447
+ .universal-card-select-img__checkbox {
448
+ position: absolute; top: 0; right: 0; z-index: 2; padding: 5px 8px; cursor: pointer;
449
+ }
450
+ .universal-card-select-img__checkbox--disabled { cursor: not-allowed; }
451
+ .universal-card-select-img__checkbox-box {
452
+ display: inline-flex; align-items: center; justify-content: center;
453
+ width: 16px; height: 16px; box-sizing: border-box; border-radius: 50%;
454
+ background-color: rgba(0, 0, 0, .15); border: 1px solid #eff0f1;
455
+ box-shadow: 0 0 2px rgba(0, 0, 0, .1);
456
+ }
457
+ .universal-card-select-img__checkbox-box--square { border-radius: 4px; }
458
+ .universal-card-select-img__checkbox-box--checked {
459
+ background-color: var(--B500); border-color: var(--B500);
460
+ }
461
+ .universal-card-select-img__checkbox-box--checked::after {
462
+ content: ""; width: 3.5px; height: 7px;
463
+ border: solid #fff; border-width: 0 2px 2px 0;
464
+ transform: rotate(45deg) translateY(-1px);
465
+ }
466
+
383
467
  /* ---- 按钮 ---- */
384
468
  /* 飞书按钮文本是纯文本、单行:min-height 容纳偶发换行,文字超长省略号收尾。
385
469
  样式全集逆向自网页版 ud__button:三大族——outlined(描边)/ text(纯文字)/ filled(实心),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-feishu-card/renderer",
3
- "version": "0.3.6",
3
+ "version": "0.4.0",
4
4
  "description": "零框架依赖的飞书卡片渲染引擎:Web / DOM / SSR,输入 RAW 或公开 DSL,输出真实 DOM 或 HTML 字符串",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -28,7 +28,7 @@
28
28
  "./dist/*.cjs"
29
29
  ],
30
30
  "dependencies": {
31
- "@open-feishu-card/adapter": "0.3.20"
31
+ "@open-feishu-card/adapter": "0.4.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "tsup": "^8.3.5",