openpencil/packages/pen-core/src/index.ts
Fini ec4fe44368 feat(pen-core): strip nested card-style decoration on inner frames
User-reported 2026-05-11 "Popular Restaurants" — inspecting the live
canvas via batch_get showed the LLM built each row as
`role:card` (outer) carrying stroke + cornerRadius:16 + 2-shadow
elevation, then nested an inner `Card Info` frame ALSO with
`role:card`, cornerRadius:12, and the SAME 2-shadow stack for the
right-hand text column. The doubled decoration rendered as a
visible "border" / box-in-box that the user called out as the
N-tools being "死板" — element-builders deterministically emit
their own card decoration without knowing they're being nested.

New post-pass `stripNestedCardDecoration` walks the page tree and,
for each non-protected frame:
  - if the frame has stroke AND any frame ancestor has stroke   → strip stroke
  - if the frame has cornerRadius > 0 AND any frame ancestor
    has cornerRadius > 0                                        → strip cornerRadius
  - if the frame has shadow AND any frame ancestor has shadow   → strip effects

Each decoration type is checked independently so e.g. a card inside
a shadow-only ancestor still keeps its cornerRadius. Fills are NOT
touched — stripRedundantSectionFills already handles fill heuristics
and a child fill may be an intentional surface change (dark accent
strip inside a white card).

KEEP_DECORATION_ROLES exempts elements that legitimately carry their
own affordance even when nested in a card: button, chip, search-bar,
input, badge, avatar, switch, etc. Those keep their click-target
visual whether or not the parent is decorated.

Wired in apps/web design-canvas-ops.ts at both finalize sites,
running AFTER stripRedundantSectionFills so the fill pass gets first
crack and this pass cleans up the leftover stroke/cornerRadius/
shadow stack.

Tests: 8 cases — basic strip, partial strip (only matched types),
top-level decoration preserved, protected-role exemption, fills
untouched, deep nesting, asymmetric cornerRadius arrays, no-op
return value.
2026-05-11 00:17:02 +08:00

639 lines
15 KiB
TypeScript

// ID generation
export { generateId } from './id.js';
// Tree utilities
export {
DEFAULT_FRAME_ID,
DEFAULT_PAGE_ID,
createEmptyDocument,
getActivePage,
getActivePageChildren,
setActivePageChildren,
getAllChildren,
migrateToPages,
ensureDocumentNodeIds,
findNodeInTree,
findParentInTree,
removeNodeFromTree,
updateNodeInTree,
flattenNodes,
insertNodeInTree,
isDescendantOf,
getNodeBounds,
findClearX,
scaleChildrenInPlace,
rotateChildrenInPlace,
deepCloneNode,
cloneNodeWithNewIds,
cloneNodesWithNewIds,
nodeTreeToSummary,
} from './tree-utils.js';
// Variables
export {
isVariableRef,
getDefaultTheme,
resolveVariableRef,
resolveColorRef,
resolveNumericRef,
resolveNodeForCanvas,
} from './variables/resolve.js';
export { replaceVariableRefsInTree } from './variables/replace-refs.js';
export {
applySemanticPalette,
getSemanticPalette,
getSemanticPaletteDescription,
getSemanticPaletteHex,
hasSemanticPalette,
SEMANTIC_PALETTE_NAMES,
SEMANTIC_PALETTE_THEME_AXIS,
SEMANTIC_PALETTE_THEME_DARK,
SEMANTIC_PALETTE_THEME_LIGHT,
type SemanticPalette,
type SemanticPaletteMode,
} from './variables/semantic-palette.js';
// Normalization
export { normalizePenDocument } from './normalize.js';
// Layout
export {
type Padding,
resolvePadding,
isNodeVisible,
setRootChildrenProvider,
getRootFillWidthFallback,
inferLayout,
fitContentWidth,
fitContentHeight,
getNodeWidth,
getNodeHeight,
computeLayoutPositions,
} from './layout/engine.js';
export { normalizeTreeLayout } from './layout/normalize-tree.js';
export { unwrapFakePhoneMockups } from './layout/unwrap-fake-phone-mockup.js';
export { stripRedundantSectionFills } from './layout/strip-redundant-section-fills.js';
export { stripNestedCardDecoration } from './layout/strip-nested-card-decoration.js';
export { clipCardImageCorners } from './layout/clip-card-image-corners.js';
export { injectMissingNavSurfaceFill } from './layout/inject-nav-surface-fill.js';
export { expandOverflowingFixedHeightCards } from './layout/expand-overflowing-fixed-height-cards.js';
export { convertStackedOverlayToAbsolute } from './layout/convert-stacked-overlay-to-absolute.js';
export { normalizeStrokeFillSchema } from './normalize/normalize-stroke-fill-schema.js';
// Text measurement
export {
parseSizing,
defaultLineHeight,
isCjkCodePoint,
hasCjkText,
estimateGlyphWidth,
estimateLineWidth,
widthSafetyFactor,
estimateTextWidth,
estimateTextWidthPrecise,
resolveTextContent,
countExplicitTextLines,
getTextOpticalCenterYOffset,
countWrappedLinesFallback,
type WrappedLineCounter,
setWrappedLineCounter,
estimateTextHeight,
} from './layout/text-measure.js';
// Constants
export {
MIN_ZOOM,
MAX_ZOOM,
ZOOM_STEP,
SNAP_THRESHOLD,
DEFAULT_FILL,
DEFAULT_STROKE,
DEFAULT_STROKE_WIDTH,
CANVAS_BACKGROUND_LIGHT,
CANVAS_BACKGROUND_DARK,
SELECTION_BLUE,
COMPONENT_COLOR,
INSTANCE_COLOR,
HOVER_BLUE,
HOVER_LINE_WIDTH,
HOVER_DASH,
INDICATOR_BLUE,
INDICATOR_LINE_WIDTH,
INDICATOR_DASH,
INDICATOR_ENDPOINT_RADIUS,
FRAME_LABEL_FONT_SIZE,
FRAME_LABEL_OFFSET_Y,
FRAME_LABEL_COLOR,
PEN_ANCHOR_FILL,
PEN_ANCHOR_RADIUS,
PEN_ANCHOR_FIRST_RADIUS,
PEN_HANDLE_DOT_RADIUS,
PEN_HANDLE_LINE_STROKE,
PEN_RUBBER_BAND_STROKE,
PEN_RUBBER_BAND_DASH,
PEN_CLOSE_HIT_THRESHOLD,
DIMENSION_LABEL_OFFSET_Y,
DEFAULT_FRAME_FILL,
DEFAULT_TEXT_FILL,
GUIDE_COLOR,
GUIDE_LINE_WIDTH,
GUIDE_DASH,
} from './constants.js';
// Sync lock
export { isFabricSyncLocked, setFabricSyncLock } from './sync-lock.js';
// Arc path
export { buildEllipseArcPath, isArcEllipse } from './arc-path.js';
export {
anchorsToPathData,
getPathBoundsFromAnchors,
inferPathAnchorPointType,
pathDataToAnchors,
type PathBounds,
type PathAnchorParseResult,
} from './path-anchors.js';
// Boolean operations
export { type BooleanOpType, canBooleanOp, executeBooleanOp } from './boolean-ops.js';
// Font utilities
export { cssFontFamily } from './font-utils.js';
// Node helpers
export { isOverlayNode, isBadgeOverlayNode, sanitizeName } from './node-helpers.js';
// Design-MD parser
export {
parseDesignMd,
generateDesignMd,
designMdColorsToVariables,
extractDesignMdFromDocument,
} from './design-md-parser.js';
// --- Merge module ---
export type { NodePatch } from './merge/node-diff.js';
export { diffDocuments } from './merge/node-diff.js';
export type {
MergeInput,
MergeResult,
NodeConflict,
NodeConflictReason,
DocFieldConflict,
DocFieldName,
} from './merge/node-merge.js';
export { mergeDocuments } from './merge/node-merge.js';
// --- Element builders (shared by pen-mcp handlers + apps/web client shims) ---
// Pure tree-build functions matching pen-mcp's add_*_v0 tools. Browser-safe:
// no node:fs, no document-manager imports — just PenNode shape generation.
// Callers layer their own insert pipeline on top (pen-mcp adds parent_id
// validation + rollback; apps/web client shim calls document-store.addNode).
export {
assignIdsRecursively,
buildScrollWrapper,
buildCardRow,
buildMetricRow,
buildBottomNav,
buildSectionHeader,
buildTopNavBar,
buildHeading,
buildBodyText,
buildTextButton,
buildSearchBar,
buildListRow,
buildDivider,
buildBadge,
buildAvatar,
buildIconButton,
buildIconLabel,
buildStatGrid,
buildSwitch,
buildCheckbox,
buildRadio,
buildTabs,
buildSegmentedControl,
buildEmptyState,
buildAlert,
buildToast,
buildProgressBar,
buildFab,
buildBreadcrumb,
buildStepper,
buildFormField,
buildTextarea,
buildSkeleton,
buildSelect,
buildChartLine,
buildChartPie,
buildImagePlaceholder,
buildVideoPlaceholder,
buildComment,
buildModalShell,
buildStatusBadge,
buildSpinner,
buildTooltip,
buildMetricComparison,
buildNotificationRow,
buildNavChipRow,
buildActivityRing,
buildRatingStars,
buildCarouselDots,
buildLink,
buildKbd,
buildPrice,
buildQuoteBlock,
buildCodeBlock,
buildColorSwatch,
buildChartBars,
buildTimeline,
buildCalendarGrid,
buildPagination,
buildFaqItem,
buildChipInput,
buildEmptyChart,
buildActionMenu,
buildDatePicker,
buildModalShellV1,
buildUploadDropzone,
buildOtpInput,
buildAttachmentRow,
buildChatBubble,
buildStatCard,
buildSocialLoginRow,
buildPricingCard,
buildToastV1,
buildRangeSlider,
buildEmptyChartV1,
buildPhoneInput,
buildInputWithAction,
buildCookieBanner,
buildSidebarNav,
buildAvatarGroup,
buildDataTableRow,
buildTag,
buildUserCard,
buildDrawerShell,
buildCombobox,
buildToolbar,
buildCallout,
buildShareRow,
buildInlineAction,
buildLegendItem,
buildInboxMessage,
buildProfileHeader,
buildSettingRow,
buildMemberRow,
buildFilterGroup,
buildInviteRow,
buildActivityLog,
buildEventCard,
buildStepCard,
cjkFontFamily,
detectCjkScript,
type ElementTree,
type CjkScript,
type CardRowItem,
type CardRowParams,
type MetricRowItem,
type MetricRowParams,
type BottomNavItem,
type BottomNavParams,
type SectionHeaderAction,
type SectionHeaderParams,
type TopNavBarParams,
type HeadingLevel,
type HeadingParams,
type BodyTextParams,
type TextButtonParams,
type SearchBarParams,
type ListRowParams,
type DividerOrientation,
type DividerParams,
type BadgeParams,
type AvatarParams,
type IconButtonParams,
type IconLabelParams,
type StatGridItem,
type StatGridParams,
type SwitchParams,
type CheckboxParams,
type RadioParams,
type TabsItem,
type TabsParams,
type SegmentedControlItem,
type SegmentedControlParams,
type EmptyStateParams,
type AlertParams,
type ToastParams,
type ProgressBarParams,
type FabParams,
type BreadcrumbItem,
type BreadcrumbParams,
type StepperParams,
type FormFieldParams,
type TextareaParams,
type SkeletonParams,
type SelectParams,
type ChartLineParams,
type ChartPieParams,
type ImagePlaceholderParams,
type VideoPlaceholderParams,
type CommentParams,
type ModalShellParams,
type StatusBadgeParams,
type StatusBadgeTone,
type SpinnerParams,
type TooltipParams,
type MetricComparisonParams,
type MetricTrend,
type NotificationRowParams,
type NavChipRowItem,
type NavChipRowParams,
type ActivityRingParams,
type RatingStarsParams,
type CarouselDotsParams,
type LinkParams,
type KbdParams,
type PriceParams,
type QuoteBlockParams,
type CodeBlockParams,
type ColorSwatchParams,
type ChartBarsParams,
type TimelineItem,
type TimelineParams,
type CalendarGridParams,
type PaginationParams,
type FaqItemParams,
type ChipInputParams,
type EmptyChartParams,
type ActionMenuItem,
type ActionMenuParams,
type DatePickerParams,
type ModalShellV1Params,
type ModalShellV1Theme,
type UploadDropzoneParams,
type OtpInputParams,
type AttachmentRowParams,
type ChatBubbleParams,
type ChatBubbleSide,
type StatCardParams,
type StatCardTrend,
type SocialLoginRowParams,
type SocialLoginProvider,
type PricingCardParams,
type ToastV1Params,
type ToastV1Theme,
type RangeSliderParams,
type EmptyChartV1Params,
type EmptyChartV1Theme,
type PhoneInputParams,
type InputWithActionParams,
type InputWithActionKind,
type CookieBannerParams,
type SidebarNavItem,
type SidebarNavParams,
type AvatarGroupItem,
type AvatarGroupParams,
type DataTableRowColumn,
type DataTableRowParams,
type TagParams,
type TagTone,
type UserCardParams,
type DrawerSide,
type DrawerShellParams,
type ComboboxOption,
type ComboboxParams,
type ToolbarItem,
type ToolbarParams,
type CalloutTone,
type CalloutParams,
type ShareTarget,
type ShareRowParams,
type InlineActionParams,
type LegendItemParams,
type InboxMessageParams,
type ProfileHeaderParams,
type SettingRowParams,
type SettingRowTrailing,
type MemberRowParams,
type MemberRowTrailing,
type FilterGroupOption,
type FilterGroupParams,
type InviteRowParams,
type InviteStatus,
type ActivityLogParams,
type EventCardParams,
type StepCardParams,
resolveTheme,
type V1Theme,
type ThemeResolution,
buildHeadingV1,
type HeadingV1Params,
buildCardRowV1,
type CardRowV1Item,
type CardRowV1Params,
buildSettingRowV1,
type SettingRowV1Params,
type SettingRowV1Trailing,
buildMemberRowV1,
type MemberRowV1Params,
type MemberRowV1Trailing,
buildActivityLogV1,
type ActivityLogV1Params,
buildAvatarV1,
type AvatarV1Params,
buildBadgeV1,
type BadgeV1Params,
buildDividerV1,
type DividerV1Orientation,
type DividerV1Params,
buildBodyTextV1,
type BodyTextV1Params,
buildIconLabelV1,
type IconLabelV1Params,
buildAlertV1,
type AlertV1Params,
buildBottomNavV1,
type BottomNavV1Item,
type BottomNavV1Params,
buildBreadcrumbV1,
type BreadcrumbV1Item,
type BreadcrumbV1Params,
buildActivityRingV1,
type ActivityRingV1Params,
buildCarouselDotsV1,
type CarouselDotsV1Params,
buildActionMenuV1,
type ActionMenuV1Item,
type ActionMenuV1Params,
buildAttachmentRowV1,
type AttachmentRowV1Params,
buildCalendarGridV1,
type CalendarGridV1Params,
buildAvatarGroupV1,
type AvatarGroupV1Item,
type AvatarGroupV1Params,
buildCalloutV1,
type CalloutV1Tone,
type CalloutV1Params,
buildChartBarsV1,
type ChartBarsV1Params,
buildChartLineV1,
type ChartLineV1Params,
buildChartPieV1,
type ChartPieV1Params,
buildChatBubbleV1,
type ChatBubbleV1Params,
type ChatBubbleV1Side,
buildCheckboxV1,
type CheckboxV1Params,
buildChipInputV1,
type ChipInputV1Params,
buildCodeBlockV1,
type CodeBlockV1Params,
buildColorSwatchV1,
type ColorSwatchV1Params,
buildComboboxV1,
type ComboboxV1Option,
type ComboboxV1Params,
buildCommentV1,
type CommentV1Params,
buildCookieBannerV1,
type CookieBannerV1Params,
buildDataTableRowV1,
type DataTableRowV1Column,
type DataTableRowV1Params,
buildDatePickerV1,
type DatePickerV1Params,
buildDrawerShellV1,
type DrawerShellV1Side,
type DrawerShellV1Params,
buildEmptyStateV1,
type EmptyStateV1Params,
buildEventCardV1,
type EventCardV1Params,
buildFabV1,
type FabV1Params,
buildFaqItemV1,
type FaqItemV1Params,
buildFilterGroupV1,
type FilterGroupV1Option,
type FilterGroupV1Params,
buildFormFieldV1,
type FormFieldV1Params,
buildIconButtonV1,
type IconButtonV1Params,
buildImagePlaceholderV1,
type ImagePlaceholderV1Params,
buildInboxMessageV1,
type InboxMessageV1Params,
buildInlineActionV1,
type InlineActionV1Params,
buildInputWithActionV1,
type InputWithActionV1Kind,
type InputWithActionV1Params,
buildInviteRowV1,
type InviteV1Status,
type InviteRowV1Params,
buildKbdV1,
type KbdV1Params,
buildLegendItemV1,
type LegendItemV1Params,
buildLinkV1,
type LinkV1Params,
buildListRowV1,
type ListRowV1Params,
buildMetricComparisonV1,
type MetricComparisonV1Params,
buildMetricRowV1,
type MetricRowV1Params,
buildNavChipRowV1,
type NavChipRowV1Params,
buildNotificationRowV1,
type NotificationRowV1Params,
buildOtpInputV1,
type OtpInputV1Params,
buildPaginationV1,
type PaginationV1Params,
buildPhoneInputV1,
type PhoneInputV1Params,
buildPriceV1,
type PriceV1Params,
buildPricingCardV1,
type PricingCardV1Params,
buildProfileHeaderV1,
type ProfileHeaderV1Params,
buildProgressBarV1,
type ProgressBarV1Params,
buildQuoteBlockV1,
type QuoteBlockV1Params,
buildRadioV1,
type RadioV1Params,
buildRangeSliderV1,
type RangeSliderV1Params,
buildRatingStarsV1,
type RatingStarsV1Params,
buildSearchBarV1,
type SearchBarV1Params,
buildSectionHeaderV1,
type SectionHeaderV1Params,
buildSegmentedControlV1,
type SegmentedControlV1Params,
buildSelectV1,
type SelectV1Params,
buildShareRowV1,
type ShareRowV1Params,
buildSidebarNavV1,
type SidebarNavV1Item,
type SidebarNavV1Params,
buildSkeletonV1,
type SkeletonV1Params,
buildSocialLoginRowV1,
type SocialLoginV1Provider,
type SocialLoginRowV1Params,
buildSpinnerV1,
type SpinnerV1Params,
buildStatCardV1,
type StatCardV1Params,
type StatCardV1Trend,
buildStatGridV1,
type StatGridV1Item,
type StatGridV1Params,
buildStatusBadgeV1,
type StatusBadgeV1Params,
type StatusBadgeV1Tone,
buildStepCardV1,
type StepCardV1Params,
buildStepperV1,
type StepperV1Params,
buildSwitchV1,
type SwitchV1Params,
buildTabsV1,
type TabsV1Params,
type TabsV1Item,
buildTagV1,
type TagV1Params,
type TagV1Tone,
buildTextButtonV1,
type TextButtonV1Params,
buildTextareaV1,
type TextareaV1Params,
buildTimelineV1,
type TimelineV1Params,
type TimelineV1Item,
buildToolbarV1,
type ToolbarV1Params,
type ToolbarV1Item,
buildTooltipV1,
type TooltipV1Params,
buildTopNavBarV1,
type TopNavBarV1Params,
buildUploadDropzoneV1,
type UploadDropzoneV1Params,
buildUserCardV1,
type UserCardV1Params,
buildVideoPlaceholderV1,
type VideoPlaceholderV1Params,
} from './element-builders/index.js';