@open-feishu-card/renderer 0.2.14 → 0.2.16

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
@@ -530,25 +530,36 @@
530
530
  .universal-card-collapsible-panel {
531
531
  box-sizing: border-box; overflow: hidden;
532
532
  }
533
- /* header(<summary>):可点击整行;去掉浏览器默认三角标记,标题占满、箭头贴右
534
- (icon_position=left 时靠 DOM 顺序在左)。 */
533
+ /* header(<summary>):可点击整行;去掉浏览器默认三角标记。
534
+ 真机 color:var(--text-caption) → N600;标题/箭头继承此色。
535
+ 默认 title flex:1 把箭头推到右侧(icon_position=right);
536
+ icon_position=left 时靠 DOM 顺序在左。 */
535
537
  .universal-card-collapsible-panel__header {
536
538
  display: flex; align-items: center; gap: 8px;
537
539
  padding: 8px 12px;
538
540
  cursor: pointer; box-sizing: border-box;
539
541
  list-style: none; user-select: none;
542
+ color: var(--N600);
540
543
  }
541
544
  .universal-card-collapsible-panel__header::-webkit-details-marker { display: none; }
542
545
  .universal-card-collapsible-panel__header::marker { content: ''; }
543
546
  .universal-card-collapsible-panel__title {
544
- flex: 1; min-width: 0; font-size: 14px; line-height: 22px; color: #1f2329;
547
+ flex: 1; min-width: 0; font-size: 14px; line-height: 22px;
545
548
  word-break: break-word;
546
549
  }
550
+ /* follow_text:箭头紧贴文字(真机 __header-content--icon-follow),不拉满整行 */
551
+ .universal-card-collapsible-panel__header--icon-follow {
552
+ gap: 4px;
553
+ }
554
+ .universal-card-collapsible-panel__header--icon-follow .universal-card-collapsible-panel__title {
555
+ flex: 0 1 auto;
556
+ width: auto;
557
+ }
547
558
  /* 箭头图标包裹:收起态角度 0;展开态(details[open])旋转到 --panel-icon-angle(内联给出,默认 -180deg)。
548
- 由用户点击 <summary> 切换 [open],箭头方向随之自动更新,无需 JS。 */
559
+ 由用户点击 <summary> 切换 [open],箭头方向随之自动更新,无需 JS。颜色继承 header caption。 */
549
560
  .universal-card-collapsible-panel__icon {
550
561
  flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
551
- transition: transform .2s; color: #646a73; transform: rotate(0deg);
562
+ transition: transform .2s; transform: rotate(0deg);
552
563
  }
553
564
  .universal-card-collapsible-panel[open] .universal-card-collapsible-panel__icon {
554
565
  transform: rotate(var(--panel-icon-angle, -180deg));
package/dist/index.cjs CHANGED
@@ -710,6 +710,91 @@ function marginToStyle(margin) {
710
710
  if (typeof margin === "string" && margin.trim()) return margin.trim();
711
711
  return boxToStyle(margin);
712
712
  }
713
+ function horizontalMarginPx(margin) {
714
+ if (typeof margin === "string" && margin.trim()) {
715
+ const parts = margin.trim().split(/\s+/).map((t) => spacingToPx(t));
716
+ if (parts.length === 1) return { left: parts[0], right: parts[0] };
717
+ if (parts.length === 2 || parts.length === 3) return { left: parts[1], right: parts[1] };
718
+ if (parts.length >= 4) return { left: parts[3], right: parts[1] };
719
+ }
720
+ if (margin && typeof margin === "object") {
721
+ const m = margin;
722
+ return { left: Number(m.left?.value ?? 0), right: Number(m.right?.value ?? 0) };
723
+ }
724
+ return { left: 0, right: 0 };
725
+ }
726
+ function applyOuterMargin(style, margin) {
727
+ const mg = marginToStyle(margin);
728
+ if (!mg) return;
729
+ style.margin = mg;
730
+ const { left, right } = horizontalMarginPx(margin);
731
+ const inset = left + right;
732
+ if (inset <= 0) return;
733
+ const w = style.width;
734
+ if (w && /px$/i.test(w) && !w.includes("%")) return;
735
+ if (!w || w === "100%" || w === "fill") {
736
+ style.width = `calc(100% - ${inset}px)`;
737
+ }
738
+ }
739
+ function resolveDimensionPx(direct, valueObj) {
740
+ const fromObj = () => {
741
+ if (!valueObj) return void 0;
742
+ const t = String(valueObj.type || "").toLowerCase();
743
+ if (t === "pixels" || t === "pixel") {
744
+ const n = Number(valueObj.value);
745
+ return Number.isFinite(n) ? n + "px" : void 0;
746
+ }
747
+ const v = valueObj.value;
748
+ if (v === "fill") return "100%";
749
+ if (v === "auto" || v === "default") return void 0;
750
+ if (typeof v === "number" && Number.isFinite(v)) return v + "px";
751
+ if (typeof v === "string") {
752
+ const m = /^(-?\d+(?:\.\d+)?)(?:px)?$/i.exec(v.trim());
753
+ if (m) return m[1] + "px";
754
+ }
755
+ return void 0;
756
+ };
757
+ if (typeof direct === "number" && Number.isFinite(direct)) return direct + "px";
758
+ if (typeof direct === "string") {
759
+ const s = direct.trim();
760
+ if (s === "fill") return "100%";
761
+ if (s === "auto" || s === "default" || !s) return void 0;
762
+ const m = /^(-?\d+(?:\.\d+)?)(?:px)?$/i.exec(s);
763
+ if (m) return m[1] + "px";
764
+ }
765
+ return fromObj();
766
+ }
767
+ function resolveImageSize(property) {
768
+ if (!property) return {};
769
+ const size = property.size ?? property.size_value;
770
+ if (typeof size === "string") {
771
+ const s = size.trim();
772
+ const pxPair = /^(\d+(?:\.\d+)?)px\s+(\d+(?:\.\d+)?)px$/i.exec(s);
773
+ if (pxPair) return { width: pxPair[1] + "px", height: pxPair[2] + "px" };
774
+ const ratio = /^(\d+(?:\.\d+)?)\s*:\s*(\d+(?:\.\d+)?)$/.exec(s);
775
+ if (ratio) return { aspectRatio: `${ratio[1]} / ${ratio[2]}` };
776
+ }
777
+ const echo = property.sizeValue ?? property.size_value;
778
+ if (echo && typeof echo === "object") {
779
+ const t = String(echo.type || "").toLowerCase();
780
+ const v = echo.value;
781
+ if (t === "width_height_pixels" && v && typeof v === "object") {
782
+ const w = Number(v.width);
783
+ const h2 = Number(v.height);
784
+ if (Number.isFinite(w) && Number.isFinite(h2)) {
785
+ return { width: w + "px", height: h2 + "px" };
786
+ }
787
+ }
788
+ if (t === "aspect_ratio" && v && typeof v === "object") {
789
+ const w = Number(v.width);
790
+ const h2 = Number(v.height);
791
+ if (Number.isFinite(w) && Number.isFinite(h2) && h2 !== 0) {
792
+ return { aspectRatio: `${w} / ${h2}` };
793
+ }
794
+ }
795
+ }
796
+ return {};
797
+ }
713
798
  function resolveCornerRadius(property) {
714
799
  if (!property) return void 0;
715
800
  const direct = property.corner_radius ?? property.cornerRadius;
@@ -1367,25 +1452,36 @@ var BASE_STYLES_CSS = `/**
1367
1452
  .universal-card-collapsible-panel {
1368
1453
  box-sizing: border-box; overflow: hidden;
1369
1454
  }
1370
- /* header\uFF08<summary>\uFF09\uFF1A\u53EF\u70B9\u51FB\u6574\u884C\uFF1B\u53BB\u6389\u6D4F\u89C8\u5668\u9ED8\u8BA4\u4E09\u89D2\u6807\u8BB0\uFF0C\u6807\u9898\u5360\u6EE1\u3001\u7BAD\u5934\u8D34\u53F3
1371
- \uFF08icon_position=left \u65F6\u9760 DOM \u987A\u5E8F\u5728\u5DE6\uFF09\u3002 */
1455
+ /* header\uFF08<summary>\uFF09\uFF1A\u53EF\u70B9\u51FB\u6574\u884C\uFF1B\u53BB\u6389\u6D4F\u89C8\u5668\u9ED8\u8BA4\u4E09\u89D2\u6807\u8BB0\u3002
1456
+ \u771F\u673A color:var(--text-caption) \u2192 N600\uFF1B\u6807\u9898/\u7BAD\u5934\u7EE7\u627F\u6B64\u8272\u3002
1457
+ \u9ED8\u8BA4 title flex:1 \u628A\u7BAD\u5934\u63A8\u5230\u53F3\u4FA7\uFF08icon_position=right\uFF09\uFF1B
1458
+ icon_position=left \u65F6\u9760 DOM \u987A\u5E8F\u5728\u5DE6\u3002 */
1372
1459
  .universal-card-collapsible-panel__header {
1373
1460
  display: flex; align-items: center; gap: 8px;
1374
1461
  padding: 8px 12px;
1375
1462
  cursor: pointer; box-sizing: border-box;
1376
1463
  list-style: none; user-select: none;
1464
+ color: var(--N600);
1377
1465
  }
1378
1466
  .universal-card-collapsible-panel__header::-webkit-details-marker { display: none; }
1379
1467
  .universal-card-collapsible-panel__header::marker { content: ''; }
1380
1468
  .universal-card-collapsible-panel__title {
1381
- flex: 1; min-width: 0; font-size: 14px; line-height: 22px; color: #1f2329;
1469
+ flex: 1; min-width: 0; font-size: 14px; line-height: 22px;
1382
1470
  word-break: break-word;
1383
1471
  }
1472
+ /* follow_text\uFF1A\u7BAD\u5934\u7D27\u8D34\u6587\u5B57\uFF08\u771F\u673A __header-content--icon-follow\uFF09\uFF0C\u4E0D\u62C9\u6EE1\u6574\u884C */
1473
+ .universal-card-collapsible-panel__header--icon-follow {
1474
+ gap: 4px;
1475
+ }
1476
+ .universal-card-collapsible-panel__header--icon-follow .universal-card-collapsible-panel__title {
1477
+ flex: 0 1 auto;
1478
+ width: auto;
1479
+ }
1384
1480
  /* \u7BAD\u5934\u56FE\u6807\u5305\u88F9\uFF1A\u6536\u8D77\u6001\u89D2\u5EA6 0\uFF1B\u5C55\u5F00\u6001\uFF08details[open]\uFF09\u65CB\u8F6C\u5230 --panel-icon-angle\uFF08\u5185\u8054\u7ED9\u51FA\uFF0C\u9ED8\u8BA4 -180deg\uFF09\u3002
1385
- \u7531\u7528\u6237\u70B9\u51FB <summary> \u5207\u6362 [open]\uFF0C\u7BAD\u5934\u65B9\u5411\u968F\u4E4B\u81EA\u52A8\u66F4\u65B0\uFF0C\u65E0\u9700 JS\u3002 */
1481
+ \u7531\u7528\u6237\u70B9\u51FB <summary> \u5207\u6362 [open]\uFF0C\u7BAD\u5934\u65B9\u5411\u968F\u4E4B\u81EA\u52A8\u66F4\u65B0\uFF0C\u65E0\u9700 JS\u3002\u989C\u8272\u7EE7\u627F header caption\u3002 */
1386
1482
  .universal-card-collapsible-panel__icon {
1387
1483
  flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
1388
- transition: transform .2s; color: #646a73; transform: rotate(0deg);
1484
+ transition: transform .2s; transform: rotate(0deg);
1389
1485
  }
1390
1486
  .universal-card-collapsible-panel[open] .universal-card-collapsible-panel__icon {
1391
1487
  transform: rotate(var(--panel-icon-angle, -180deg));
@@ -5435,8 +5531,7 @@ function registerBuiltins() {
5435
5531
  registerComponent("markdown", (el, ctx) => {
5436
5532
  const p = el.property || {};
5437
5533
  const style = { ...alignToStyle(p.textAlign), ...textStyleToStyle(p.textStyle) };
5438
- const mg = marginToStyle(p.margin);
5439
- if (mg) style.margin = mg;
5534
+ applyOuterMargin(style, p.margin);
5440
5535
  const sizeKey = resolveTextSizeKey(p);
5441
5536
  let fontSize = BASE_FONT_SIZE;
5442
5537
  let lineHeight = BASE_FONT_SIZE + 8;
@@ -5502,8 +5597,7 @@ function registerBuiltins() {
5502
5597
  if (hAlign) {
5503
5598
  style.justifyContent = { left: "flex-start", center: "center", right: "flex-end" }[hAlign] || "flex-start";
5504
5599
  }
5505
- const mg = boxToStyle(p.margin);
5506
- if (mg) style.margin = mg;
5600
+ applyOuterMargin(style, p.margin);
5507
5601
  const bg = propertyBackgroundColor(p);
5508
5602
  if (bg) style.background = bg;
5509
5603
  const pad = boxToStyle(p.padding);
@@ -5557,7 +5651,21 @@ function registerBuiltins() {
5557
5651
  const wrapStyle = {};
5558
5652
  const imgStyle = {};
5559
5653
  if (p.corner_radius != null) imgStyle.borderRadius = sizeToPx(p.corner_radius);
5560
- if (scale === "fit_horizontal" || scale === "stretch") {
5654
+ const sized = resolveImageSize(p);
5655
+ if (sized.width && sized.height) {
5656
+ wrapStyle.width = sized.width;
5657
+ wrapStyle.height = sized.height;
5658
+ wrapStyle.flexShrink = "0";
5659
+ imgStyle.width = "100%";
5660
+ imgStyle.height = "100%";
5661
+ imgStyle.objectFit = scale === "fit_horizontal" || scale === "stretch" ? "fill" : "cover";
5662
+ } else if (sized.aspectRatio) {
5663
+ wrapStyle.width = "100%";
5664
+ imgStyle.width = "100%";
5665
+ imgStyle.aspectRatio = sized.aspectRatio;
5666
+ imgStyle.height = "auto";
5667
+ imgStyle.objectFit = "cover";
5668
+ } else if (scale === "fit_horizontal" || scale === "stretch") {
5561
5669
  wrapStyle.width = "100%";
5562
5670
  imgStyle.width = "100%";
5563
5671
  imgStyle.height = "auto";
@@ -5565,7 +5673,7 @@ function registerBuiltins() {
5565
5673
  return h(
5566
5674
  "div",
5567
5675
  { className: `${CLS}-image__wrapper`, style: wrapStyle },
5568
- src ? h("img", { className: `${CLS}-image__host`, src, alt: altText, style: imgStyle }) : h("div", { className: `${CLS}-image__placeholder` }, altText || "[\u56FE\u7247]")
5676
+ src ? h("img", { className: `${CLS}-image__host`, src, alt: altText, style: imgStyle }) : h("div", { className: `${CLS}-image__placeholder`, style: imgStyle }, altText || "[\u56FE\u7247]")
5569
5677
  );
5570
5678
  });
5571
5679
  registerComponent("video", (el, ctx) => {
@@ -5713,7 +5821,7 @@ function registerBuiltins() {
5713
5821
  const tip = extractText(p.hoverTips);
5714
5822
  const borderNamed = p.border_color ?? p.borderColor;
5715
5823
  const borderCustom = adapter.resolveCustomBorderLight(p);
5716
- const borderColor = resolveColor(borderNamed) ?? (borderCustom ? resolveColor(borderCustom) : void 0);
5824
+ const borderColor = (borderCustom ? resolveColor(borderCustom) : void 0) ?? resolveColor(borderNamed);
5717
5825
  const hasBorder = borderColor != null || p.hasBorder === true || p.has_border === true;
5718
5826
  const cls = `${CLS}-interactive-container` + (clickable ? ` ${CLS}-interactive-container--action` : "") + (hasBorder && !borderColor ? ` ${CLS}-interactive-container--bordered` : "") + (disabled ? ` ${CLS}-interactive-container--disabled` : "");
5719
5827
  const style = {};
@@ -5727,7 +5835,16 @@ function registerBuiltins() {
5727
5835
  const vGap = p.verticalSpacingValue?.value ?? p.vertical_spacing;
5728
5836
  if (vGap != null) style.rowGap = spacingToPx(vGap) + "px";
5729
5837
  const icWv = p.widthValue;
5730
- if ((p.width ?? icWv?.value) === "fill") style.width = "100%";
5838
+ const icHv = p.heightValue;
5839
+ const w = resolveDimensionPx(p.width, icWv);
5840
+ const hPx = resolveDimensionPx(p.height, icHv);
5841
+ if (w) style.width = w;
5842
+ if (hPx) {
5843
+ style.height = hPx;
5844
+ style.flexShrink = "0";
5845
+ style.boxSizing = "border-box";
5846
+ }
5847
+ applyOuterMargin(style, p.margin);
5731
5848
  const props = { className: cls };
5732
5849
  if (Object.keys(style).length) props.style = style;
5733
5850
  if (url && !disabled) props["data-url"] = url;
@@ -5747,10 +5864,16 @@ function registerBuiltins() {
5747
5864
  const bg = bgStyleToColor(bgRaw);
5748
5865
  if (bg) style.background = bg;
5749
5866
  }
5750
- const border = p.border && typeof p.border === "object" ? p.border : {};
5751
- const borderColor = resolveColor(border.color) || "#dee0e3";
5752
- style.border = `1px solid ${borderColor}`;
5753
- style.borderRadius = border.corner_radius != null ? sizeToPx(border.corner_radius) : "8px";
5867
+ const borderRaw = p.border;
5868
+ if (borderRaw && typeof borderRaw === "object") {
5869
+ const border = borderRaw;
5870
+ const borderColor = resolveColor(border.color) || "#dee0e3";
5871
+ style.border = `1px solid ${borderColor}`;
5872
+ style.borderRadius = border.corner_radius != null ? sizeToPx(border.corner_radius) : "8px";
5873
+ } else {
5874
+ style.borderRadius = "8px";
5875
+ }
5876
+ applyOuterMargin(style, p.margin);
5754
5877
  const headerStyle = {};
5755
5878
  const hpad = typeof hdr.padding === "string" ? hdr.padding : boxToStyle(hdr.padding);
5756
5879
  if (hpad) headerStyle.padding = hpad;
@@ -5758,7 +5881,7 @@ function registerBuiltins() {
5758
5881
  const titleVNode = h("div", { className: `${CLS}-collapsible-panel__title` }, title);
5759
5882
  let iconVNode = null;
5760
5883
  const iconRaw = hdr.icon;
5761
- const openAngle = Number(hdr.icon_expanded_angle ?? -180);
5884
+ const openAngle = Number(hdr.icon_expanded_angle ?? hdr.iconExpandedAngle ?? -180);
5762
5885
  if (iconRaw && iconRaw.tag) {
5763
5886
  const iconEl = iconRaw.property ? iconRaw : { tag: iconRaw.tag, property: iconRaw };
5764
5887
  const inner = renderIcon(iconEl, ctx);
@@ -5766,10 +5889,13 @@ function registerBuiltins() {
5766
5889
  iconVNode = h("div", { className: `${CLS}-collapsible-panel__icon` }, inner);
5767
5890
  }
5768
5891
  }
5769
- const iconLeft = hdr.icon_position === "left";
5892
+ const iconPos = String(hdr.icon_position ?? hdr.iconPosition ?? "right");
5893
+ const iconLeft = iconPos === "left";
5894
+ const iconFollow = iconPos === "follow_text" || iconPos === "followText";
5770
5895
  const headerChildren = iconLeft ? [iconVNode, titleVNode] : [titleVNode, iconVNode];
5771
5896
  const summaryStyle = { ...headerStyle, "--panel-icon-angle": `${openAngle}deg` };
5772
- const summary = h("summary", { className: `${CLS}-collapsible-panel__header`, style: summaryStyle }, headerChildren);
5897
+ const headerCls = `${CLS}-collapsible-panel__header` + (iconFollow ? ` ${CLS}-collapsible-panel__header--icon-follow` : "");
5898
+ const summary = h("summary", { className: headerCls, style: summaryStyle }, headerChildren);
5773
5899
  const contentStyle = {};
5774
5900
  const cpad = typeof p.padding === "string" ? p.padding : boxToStyle(p.padding);
5775
5901
  if (cpad) contentStyle.padding = cpad;
package/dist/index.js CHANGED
@@ -709,6 +709,91 @@ function marginToStyle(margin) {
709
709
  if (typeof margin === "string" && margin.trim()) return margin.trim();
710
710
  return boxToStyle(margin);
711
711
  }
712
+ function horizontalMarginPx(margin) {
713
+ if (typeof margin === "string" && margin.trim()) {
714
+ const parts = margin.trim().split(/\s+/).map((t) => spacingToPx(t));
715
+ if (parts.length === 1) return { left: parts[0], right: parts[0] };
716
+ if (parts.length === 2 || parts.length === 3) return { left: parts[1], right: parts[1] };
717
+ if (parts.length >= 4) return { left: parts[3], right: parts[1] };
718
+ }
719
+ if (margin && typeof margin === "object") {
720
+ const m = margin;
721
+ return { left: Number(m.left?.value ?? 0), right: Number(m.right?.value ?? 0) };
722
+ }
723
+ return { left: 0, right: 0 };
724
+ }
725
+ function applyOuterMargin(style, margin) {
726
+ const mg = marginToStyle(margin);
727
+ if (!mg) return;
728
+ style.margin = mg;
729
+ const { left, right } = horizontalMarginPx(margin);
730
+ const inset = left + right;
731
+ if (inset <= 0) return;
732
+ const w = style.width;
733
+ if (w && /px$/i.test(w) && !w.includes("%")) return;
734
+ if (!w || w === "100%" || w === "fill") {
735
+ style.width = `calc(100% - ${inset}px)`;
736
+ }
737
+ }
738
+ function resolveDimensionPx(direct, valueObj) {
739
+ const fromObj = () => {
740
+ if (!valueObj) return void 0;
741
+ const t = String(valueObj.type || "").toLowerCase();
742
+ if (t === "pixels" || t === "pixel") {
743
+ const n = Number(valueObj.value);
744
+ return Number.isFinite(n) ? n + "px" : void 0;
745
+ }
746
+ const v = valueObj.value;
747
+ if (v === "fill") return "100%";
748
+ if (v === "auto" || v === "default") return void 0;
749
+ if (typeof v === "number" && Number.isFinite(v)) return v + "px";
750
+ if (typeof v === "string") {
751
+ const m = /^(-?\d+(?:\.\d+)?)(?:px)?$/i.exec(v.trim());
752
+ if (m) return m[1] + "px";
753
+ }
754
+ return void 0;
755
+ };
756
+ if (typeof direct === "number" && Number.isFinite(direct)) return direct + "px";
757
+ if (typeof direct === "string") {
758
+ const s = direct.trim();
759
+ if (s === "fill") return "100%";
760
+ if (s === "auto" || s === "default" || !s) return void 0;
761
+ const m = /^(-?\d+(?:\.\d+)?)(?:px)?$/i.exec(s);
762
+ if (m) return m[1] + "px";
763
+ }
764
+ return fromObj();
765
+ }
766
+ function resolveImageSize(property) {
767
+ if (!property) return {};
768
+ const size = property.size ?? property.size_value;
769
+ if (typeof size === "string") {
770
+ const s = size.trim();
771
+ const pxPair = /^(\d+(?:\.\d+)?)px\s+(\d+(?:\.\d+)?)px$/i.exec(s);
772
+ if (pxPair) return { width: pxPair[1] + "px", height: pxPair[2] + "px" };
773
+ const ratio = /^(\d+(?:\.\d+)?)\s*:\s*(\d+(?:\.\d+)?)$/.exec(s);
774
+ if (ratio) return { aspectRatio: `${ratio[1]} / ${ratio[2]}` };
775
+ }
776
+ const echo = property.sizeValue ?? property.size_value;
777
+ if (echo && typeof echo === "object") {
778
+ const t = String(echo.type || "").toLowerCase();
779
+ const v = echo.value;
780
+ if (t === "width_height_pixels" && v && typeof v === "object") {
781
+ const w = Number(v.width);
782
+ const h2 = Number(v.height);
783
+ if (Number.isFinite(w) && Number.isFinite(h2)) {
784
+ return { width: w + "px", height: h2 + "px" };
785
+ }
786
+ }
787
+ if (t === "aspect_ratio" && v && typeof v === "object") {
788
+ const w = Number(v.width);
789
+ const h2 = Number(v.height);
790
+ if (Number.isFinite(w) && Number.isFinite(h2) && h2 !== 0) {
791
+ return { aspectRatio: `${w} / ${h2}` };
792
+ }
793
+ }
794
+ }
795
+ return {};
796
+ }
712
797
  function resolveCornerRadius(property) {
713
798
  if (!property) return void 0;
714
799
  const direct = property.corner_radius ?? property.cornerRadius;
@@ -1366,25 +1451,36 @@ var BASE_STYLES_CSS = `/**
1366
1451
  .universal-card-collapsible-panel {
1367
1452
  box-sizing: border-box; overflow: hidden;
1368
1453
  }
1369
- /* header\uFF08<summary>\uFF09\uFF1A\u53EF\u70B9\u51FB\u6574\u884C\uFF1B\u53BB\u6389\u6D4F\u89C8\u5668\u9ED8\u8BA4\u4E09\u89D2\u6807\u8BB0\uFF0C\u6807\u9898\u5360\u6EE1\u3001\u7BAD\u5934\u8D34\u53F3
1370
- \uFF08icon_position=left \u65F6\u9760 DOM \u987A\u5E8F\u5728\u5DE6\uFF09\u3002 */
1454
+ /* header\uFF08<summary>\uFF09\uFF1A\u53EF\u70B9\u51FB\u6574\u884C\uFF1B\u53BB\u6389\u6D4F\u89C8\u5668\u9ED8\u8BA4\u4E09\u89D2\u6807\u8BB0\u3002
1455
+ \u771F\u673A color:var(--text-caption) \u2192 N600\uFF1B\u6807\u9898/\u7BAD\u5934\u7EE7\u627F\u6B64\u8272\u3002
1456
+ \u9ED8\u8BA4 title flex:1 \u628A\u7BAD\u5934\u63A8\u5230\u53F3\u4FA7\uFF08icon_position=right\uFF09\uFF1B
1457
+ icon_position=left \u65F6\u9760 DOM \u987A\u5E8F\u5728\u5DE6\u3002 */
1371
1458
  .universal-card-collapsible-panel__header {
1372
1459
  display: flex; align-items: center; gap: 8px;
1373
1460
  padding: 8px 12px;
1374
1461
  cursor: pointer; box-sizing: border-box;
1375
1462
  list-style: none; user-select: none;
1463
+ color: var(--N600);
1376
1464
  }
1377
1465
  .universal-card-collapsible-panel__header::-webkit-details-marker { display: none; }
1378
1466
  .universal-card-collapsible-panel__header::marker { content: ''; }
1379
1467
  .universal-card-collapsible-panel__title {
1380
- flex: 1; min-width: 0; font-size: 14px; line-height: 22px; color: #1f2329;
1468
+ flex: 1; min-width: 0; font-size: 14px; line-height: 22px;
1381
1469
  word-break: break-word;
1382
1470
  }
1471
+ /* follow_text\uFF1A\u7BAD\u5934\u7D27\u8D34\u6587\u5B57\uFF08\u771F\u673A __header-content--icon-follow\uFF09\uFF0C\u4E0D\u62C9\u6EE1\u6574\u884C */
1472
+ .universal-card-collapsible-panel__header--icon-follow {
1473
+ gap: 4px;
1474
+ }
1475
+ .universal-card-collapsible-panel__header--icon-follow .universal-card-collapsible-panel__title {
1476
+ flex: 0 1 auto;
1477
+ width: auto;
1478
+ }
1383
1479
  /* \u7BAD\u5934\u56FE\u6807\u5305\u88F9\uFF1A\u6536\u8D77\u6001\u89D2\u5EA6 0\uFF1B\u5C55\u5F00\u6001\uFF08details[open]\uFF09\u65CB\u8F6C\u5230 --panel-icon-angle\uFF08\u5185\u8054\u7ED9\u51FA\uFF0C\u9ED8\u8BA4 -180deg\uFF09\u3002
1384
- \u7531\u7528\u6237\u70B9\u51FB <summary> \u5207\u6362 [open]\uFF0C\u7BAD\u5934\u65B9\u5411\u968F\u4E4B\u81EA\u52A8\u66F4\u65B0\uFF0C\u65E0\u9700 JS\u3002 */
1480
+ \u7531\u7528\u6237\u70B9\u51FB <summary> \u5207\u6362 [open]\uFF0C\u7BAD\u5934\u65B9\u5411\u968F\u4E4B\u81EA\u52A8\u66F4\u65B0\uFF0C\u65E0\u9700 JS\u3002\u989C\u8272\u7EE7\u627F header caption\u3002 */
1385
1481
  .universal-card-collapsible-panel__icon {
1386
1482
  flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
1387
- transition: transform .2s; color: #646a73; transform: rotate(0deg);
1483
+ transition: transform .2s; transform: rotate(0deg);
1388
1484
  }
1389
1485
  .universal-card-collapsible-panel[open] .universal-card-collapsible-panel__icon {
1390
1486
  transform: rotate(var(--panel-icon-angle, -180deg));
@@ -5434,8 +5530,7 @@ function registerBuiltins() {
5434
5530
  registerComponent("markdown", (el, ctx) => {
5435
5531
  const p = el.property || {};
5436
5532
  const style = { ...alignToStyle(p.textAlign), ...textStyleToStyle(p.textStyle) };
5437
- const mg = marginToStyle(p.margin);
5438
- if (mg) style.margin = mg;
5533
+ applyOuterMargin(style, p.margin);
5439
5534
  const sizeKey = resolveTextSizeKey(p);
5440
5535
  let fontSize = BASE_FONT_SIZE;
5441
5536
  let lineHeight = BASE_FONT_SIZE + 8;
@@ -5501,8 +5596,7 @@ function registerBuiltins() {
5501
5596
  if (hAlign) {
5502
5597
  style.justifyContent = { left: "flex-start", center: "center", right: "flex-end" }[hAlign] || "flex-start";
5503
5598
  }
5504
- const mg = boxToStyle(p.margin);
5505
- if (mg) style.margin = mg;
5599
+ applyOuterMargin(style, p.margin);
5506
5600
  const bg = propertyBackgroundColor(p);
5507
5601
  if (bg) style.background = bg;
5508
5602
  const pad = boxToStyle(p.padding);
@@ -5556,7 +5650,21 @@ function registerBuiltins() {
5556
5650
  const wrapStyle = {};
5557
5651
  const imgStyle = {};
5558
5652
  if (p.corner_radius != null) imgStyle.borderRadius = sizeToPx(p.corner_radius);
5559
- if (scale === "fit_horizontal" || scale === "stretch") {
5653
+ const sized = resolveImageSize(p);
5654
+ if (sized.width && sized.height) {
5655
+ wrapStyle.width = sized.width;
5656
+ wrapStyle.height = sized.height;
5657
+ wrapStyle.flexShrink = "0";
5658
+ imgStyle.width = "100%";
5659
+ imgStyle.height = "100%";
5660
+ imgStyle.objectFit = scale === "fit_horizontal" || scale === "stretch" ? "fill" : "cover";
5661
+ } else if (sized.aspectRatio) {
5662
+ wrapStyle.width = "100%";
5663
+ imgStyle.width = "100%";
5664
+ imgStyle.aspectRatio = sized.aspectRatio;
5665
+ imgStyle.height = "auto";
5666
+ imgStyle.objectFit = "cover";
5667
+ } else if (scale === "fit_horizontal" || scale === "stretch") {
5560
5668
  wrapStyle.width = "100%";
5561
5669
  imgStyle.width = "100%";
5562
5670
  imgStyle.height = "auto";
@@ -5564,7 +5672,7 @@ function registerBuiltins() {
5564
5672
  return h(
5565
5673
  "div",
5566
5674
  { className: `${CLS}-image__wrapper`, style: wrapStyle },
5567
- src ? h("img", { className: `${CLS}-image__host`, src, alt: altText, style: imgStyle }) : h("div", { className: `${CLS}-image__placeholder` }, altText || "[\u56FE\u7247]")
5675
+ src ? h("img", { className: `${CLS}-image__host`, src, alt: altText, style: imgStyle }) : h("div", { className: `${CLS}-image__placeholder`, style: imgStyle }, altText || "[\u56FE\u7247]")
5568
5676
  );
5569
5677
  });
5570
5678
  registerComponent("video", (el, ctx) => {
@@ -5712,7 +5820,7 @@ function registerBuiltins() {
5712
5820
  const tip = extractText(p.hoverTips);
5713
5821
  const borderNamed = p.border_color ?? p.borderColor;
5714
5822
  const borderCustom = resolveCustomBorderLight(p);
5715
- const borderColor = resolveColor(borderNamed) ?? (borderCustom ? resolveColor(borderCustom) : void 0);
5823
+ const borderColor = (borderCustom ? resolveColor(borderCustom) : void 0) ?? resolveColor(borderNamed);
5716
5824
  const hasBorder = borderColor != null || p.hasBorder === true || p.has_border === true;
5717
5825
  const cls = `${CLS}-interactive-container` + (clickable ? ` ${CLS}-interactive-container--action` : "") + (hasBorder && !borderColor ? ` ${CLS}-interactive-container--bordered` : "") + (disabled ? ` ${CLS}-interactive-container--disabled` : "");
5718
5826
  const style = {};
@@ -5726,7 +5834,16 @@ function registerBuiltins() {
5726
5834
  const vGap = p.verticalSpacingValue?.value ?? p.vertical_spacing;
5727
5835
  if (vGap != null) style.rowGap = spacingToPx(vGap) + "px";
5728
5836
  const icWv = p.widthValue;
5729
- if ((p.width ?? icWv?.value) === "fill") style.width = "100%";
5837
+ const icHv = p.heightValue;
5838
+ const w = resolveDimensionPx(p.width, icWv);
5839
+ const hPx = resolveDimensionPx(p.height, icHv);
5840
+ if (w) style.width = w;
5841
+ if (hPx) {
5842
+ style.height = hPx;
5843
+ style.flexShrink = "0";
5844
+ style.boxSizing = "border-box";
5845
+ }
5846
+ applyOuterMargin(style, p.margin);
5730
5847
  const props = { className: cls };
5731
5848
  if (Object.keys(style).length) props.style = style;
5732
5849
  if (url && !disabled) props["data-url"] = url;
@@ -5746,10 +5863,16 @@ function registerBuiltins() {
5746
5863
  const bg = bgStyleToColor(bgRaw);
5747
5864
  if (bg) style.background = bg;
5748
5865
  }
5749
- const border = p.border && typeof p.border === "object" ? p.border : {};
5750
- const borderColor = resolveColor(border.color) || "#dee0e3";
5751
- style.border = `1px solid ${borderColor}`;
5752
- style.borderRadius = border.corner_radius != null ? sizeToPx(border.corner_radius) : "8px";
5866
+ const borderRaw = p.border;
5867
+ if (borderRaw && typeof borderRaw === "object") {
5868
+ const border = borderRaw;
5869
+ const borderColor = resolveColor(border.color) || "#dee0e3";
5870
+ style.border = `1px solid ${borderColor}`;
5871
+ style.borderRadius = border.corner_radius != null ? sizeToPx(border.corner_radius) : "8px";
5872
+ } else {
5873
+ style.borderRadius = "8px";
5874
+ }
5875
+ applyOuterMargin(style, p.margin);
5753
5876
  const headerStyle = {};
5754
5877
  const hpad = typeof hdr.padding === "string" ? hdr.padding : boxToStyle(hdr.padding);
5755
5878
  if (hpad) headerStyle.padding = hpad;
@@ -5757,7 +5880,7 @@ function registerBuiltins() {
5757
5880
  const titleVNode = h("div", { className: `${CLS}-collapsible-panel__title` }, title);
5758
5881
  let iconVNode = null;
5759
5882
  const iconRaw = hdr.icon;
5760
- const openAngle = Number(hdr.icon_expanded_angle ?? -180);
5883
+ const openAngle = Number(hdr.icon_expanded_angle ?? hdr.iconExpandedAngle ?? -180);
5761
5884
  if (iconRaw && iconRaw.tag) {
5762
5885
  const iconEl = iconRaw.property ? iconRaw : { tag: iconRaw.tag, property: iconRaw };
5763
5886
  const inner = renderIcon(iconEl, ctx);
@@ -5765,10 +5888,13 @@ function registerBuiltins() {
5765
5888
  iconVNode = h("div", { className: `${CLS}-collapsible-panel__icon` }, inner);
5766
5889
  }
5767
5890
  }
5768
- const iconLeft = hdr.icon_position === "left";
5891
+ const iconPos = String(hdr.icon_position ?? hdr.iconPosition ?? "right");
5892
+ const iconLeft = iconPos === "left";
5893
+ const iconFollow = iconPos === "follow_text" || iconPos === "followText";
5769
5894
  const headerChildren = iconLeft ? [iconVNode, titleVNode] : [titleVNode, iconVNode];
5770
5895
  const summaryStyle = { ...headerStyle, "--panel-icon-angle": `${openAngle}deg` };
5771
- const summary = h("summary", { className: `${CLS}-collapsible-panel__header`, style: summaryStyle }, headerChildren);
5896
+ const headerCls = `${CLS}-collapsible-panel__header` + (iconFollow ? ` ${CLS}-collapsible-panel__header--icon-follow` : "");
5897
+ const summary = h("summary", { className: headerCls, style: summaryStyle }, headerChildren);
5772
5898
  const contentStyle = {};
5773
5899
  const cpad = typeof p.padding === "string" ? p.padding : boxToStyle(p.padding);
5774
5900
  if (cpad) contentStyle.padding = cpad;
package/dist/styles.css CHANGED
@@ -530,25 +530,36 @@
530
530
  .universal-card-collapsible-panel {
531
531
  box-sizing: border-box; overflow: hidden;
532
532
  }
533
- /* header(<summary>):可点击整行;去掉浏览器默认三角标记,标题占满、箭头贴右
534
- (icon_position=left 时靠 DOM 顺序在左)。 */
533
+ /* header(<summary>):可点击整行;去掉浏览器默认三角标记。
534
+ 真机 color:var(--text-caption) → N600;标题/箭头继承此色。
535
+ 默认 title flex:1 把箭头推到右侧(icon_position=right);
536
+ icon_position=left 时靠 DOM 顺序在左。 */
535
537
  .universal-card-collapsible-panel__header {
536
538
  display: flex; align-items: center; gap: 8px;
537
539
  padding: 8px 12px;
538
540
  cursor: pointer; box-sizing: border-box;
539
541
  list-style: none; user-select: none;
542
+ color: var(--N600);
540
543
  }
541
544
  .universal-card-collapsible-panel__header::-webkit-details-marker { display: none; }
542
545
  .universal-card-collapsible-panel__header::marker { content: ''; }
543
546
  .universal-card-collapsible-panel__title {
544
- flex: 1; min-width: 0; font-size: 14px; line-height: 22px; color: #1f2329;
547
+ flex: 1; min-width: 0; font-size: 14px; line-height: 22px;
545
548
  word-break: break-word;
546
549
  }
550
+ /* follow_text:箭头紧贴文字(真机 __header-content--icon-follow),不拉满整行 */
551
+ .universal-card-collapsible-panel__header--icon-follow {
552
+ gap: 4px;
553
+ }
554
+ .universal-card-collapsible-panel__header--icon-follow .universal-card-collapsible-panel__title {
555
+ flex: 0 1 auto;
556
+ width: auto;
557
+ }
547
558
  /* 箭头图标包裹:收起态角度 0;展开态(details[open])旋转到 --panel-icon-angle(内联给出,默认 -180deg)。
548
- 由用户点击 <summary> 切换 [open],箭头方向随之自动更新,无需 JS。 */
559
+ 由用户点击 <summary> 切换 [open],箭头方向随之自动更新,无需 JS。颜色继承 header caption。 */
549
560
  .universal-card-collapsible-panel__icon {
550
561
  flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
551
- transition: transform .2s; color: #646a73; transform: rotate(0deg);
562
+ transition: transform .2s; transform: rotate(0deg);
552
563
  }
553
564
  .universal-card-collapsible-panel[open] .universal-card-collapsible-panel__icon {
554
565
  transform: rotate(var(--panel-icon-angle, -180deg));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-feishu-card/renderer",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
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.10"
31
+ "@open-feishu-card/adapter": "0.3.11"
32
32
  },
33
33
  "devDependencies": {
34
34
  "tsup": "^8.3.5",