Commit graph

153 commits

Author SHA1 Message Date
Fini d435fc53a7 Merge branch 'v0.8.0' of github.com:ZSeven-W/openpencil into v0.8.0 2026-05-10 21:37:11 +08:00
Kayshen-X 484c6032b8 feat(shell): step 4-6 chrome — TS-equivalent editor UI + interactions
Step 4 (visual lift):
- Theme tokens (shadcn-dark palette) in shell-core
- Lucide-style icons via stroke_svg_path (skia parse_path::from_svg)
- Vertical Toolbar / sectioned LayerPanel (Pages + Layers) /
  TopBar / floating StatusBar / floating AIChatPanel widgets
- Native + web backends: stroke_line / fill_round_rect /
  stroke_round_rect / stroke_svg_path primitives
- CJK fallback typeface: cached PingFang/Noto-CJK on native via
  match_family_style_character; embedded NotoSansCJK-Subset
  (8.7 KB) on web alongside Roboto

Step 5 (infinite canvas + AI chat input):
- Document.viewport (pan + zoom 10–800%) with cursor-centered
  zoom_at + Hand-tool drag pan + dotted background grid
- Trackpad PixelDelta → pan, LineDelta / pinch / Cmd+swipe →
  zoom (winit MouseScrollDelta + PinchGesture + Modifiers)
- Document.chat (input / messages / focused / collapsed /
  4-corner anchor) — WidgetHost wires apply_text /
  apply_backspace / apply_send + DOM keydown listener
- AI chat panel drag → 4-corner snap via ChatAnchor::nearest
- Collapsed mode: compact pill (MessageSquare + "New Chat" +
  ChevronUp), entire pill click expands

Step 6 (RightPanel + chrome polish):
- PropertyPanel rewrite: 设计/代码 tabs, 创建组件, 位置, 弹性布局,
  尺寸, 图层, 填充, 描边, 效果, 导出 — file split into
  property_panel.rs + property_panel_sections.rs (under 800 ea.)
- Node::aggregate_bounds for Group / unbounded containers so
  the panel reports child-union W/H instead of 0×0
- TopBar PanelLeft button toggles Document.ui.sidebar_open
- Click empty canvas clears selection (collapses RightPanel)
- Native font cache (Roboto + system CJK typeface) bypasses
  jian-skia textlayout: chrome paint 605 ms → sub-ms

Hit-test order = paint order reversed (chat → toolbar → layer
panel → canvas) so the topmost overlay always wins, plus
toolbar bounding-rect consumes gap clicks so they don't fall
through.

64 lib tests + 21 widgets_static green; native + web
cargo check clean. Web wasm rebuild gated on EMSDK
(tools/check-wasm-bundle.sh runs the bundle ceiling guard).
2026-05-10 17:07:59 +08:00
Fini 20dbbf227a feat(ai): detect stacked horizontal padding (page-vs-section gutter)
14th pre-validation detector + a preventive skill rule.

User-reported 2026-05-10 "Bistro" mobile food app shipped with root
padding [0,16,0,16] AND a "Today's Specials" section padding [0,24].
Effective gutter = 40px on a 375px page → only 295px of usable
content width. Reads as "too much padding" / pinched.

Two pieces:

1. layout.md AESTHETIC HYGIENE block now teaches "page gutter goes
   on ONE layer, not both" — pick root horizontal padding OR
   per-section horizontal padding, not both. Default convention:
   root carries the gutter, sections set vertical-only padding.
   Hero / banner / image-bleed sections then sit edge-to-edge by
   simply NOT adding horizontal padding (root's gutter shows
   through). Preventive teaching at prompt time.

2. detectStackedHorizontalPadding (info-only, detect-only). Walks
   every mobile-shaped root (width 320–480 + tall + multi-child),
   compares root horizontal padding against each direct child's
   horizontal padding; flags the section as the offender when both
   are > 0. Page-shape filter mirrors detectEdgeSectionPadding so
   the legitimate component-internal padding stacking pattern
   (chip → badge → icon, etc.) doesn't trip it. Severity is INFO
   because a section may legitimately want a deeper inset for
   visual emphasis — let the user/agent decide via audit panel.

Side-quest: scripts/ab-corpus/check-stacked-padding.ts ships with
this commit so the next stacked-padding-style detector calibration
can survey corpus frequency without rebuilding the harness.
2026-05-10 15:15:00 +08:00
Fini bf4273f328 feat(ai): detect text-bg contrast below WCAG AA (P0-2 from aesthetics roadmap)
13th pre-validation detector. Walks every text node, finds the closest
ancestor with a usable solid fill (or first gradient stop as a coarse
approximation), resolves both colors through doc.variables / theme,
and computes WCAG 2.x relative-luminance contrast ratio. Flags ratios
below 4.5:1 for normal text and 3.0:1 for large text (>=24px or
>=19px bold).

Detect-only severity (info). The 2026-05-09 review explicitly rejected
auto-replacing fills via "nearest brand-token" heuristics — the right
replacement depends on the design system + theme + intent, which only
the user/agent can decide. Issues surface in the audit panel and chat
status line so the misuse is visible without silently rewriting fills.

Side effects:
- Extracted parseHexColor / relativeLuminance / colorContrast from
  detectors.ts into diagnostics/color-utils.ts so the new detector
  doesn't duplicate ~30 lines of WCAG math.
- New detector lives in diagnostics/detectors-typography.ts (mirroring
  the per-category split started by detectors-spacing.ts).
- Adds @zseven-w/pen-core to pen-ai-skills deps so the detector can
  call resolveColorRef + getDefaultTheme — the canonical authority on
  the document's variable model.
2026-05-10 14:30:00 +08:00
Fini 33493bf7bd Merge remote-tracking branch 'origin/v0.8.0' into v0.8.0
# Conflicts:
#	Cargo.lock
#	crates/openpencil-shell-web/Cargo.toml
#	crates/openpencil-shell-web/src/lib.rs
#	packages/pen-ai-skills/src/diagnostics/detectors.ts
#	packages/pen-ai-skills/src/diagnostics/types.ts
#	packages/pen-mcp/src/routes/debug-routes.ts
2026-05-10 10:18:00 +08:00
Fini a7c05fb054 feat(ai): aesthetic detector — excessive-frame-effects (spread > 0 / blur > 40 / 3+ stacked)
Why: 2026-05-10 user report — "Mexican" badge in the food-app screenshot
landed with a "带尖的背景阴影" (pointy / spiked background shadow). The
underlying cause is the model emitting effects with positive spread,
which "bleeds" the shadow color outward and creates a visible bloom /
halo around the badge that doesn't match real product UI shadows.
Real UI shadows are tight: blur 4-16, spread 0, near-black low-alpha.
Existing detectors only handle text effects (text-effect from 7aef1b14);
frame-level effects had no aesthetic gate.

What: detectExcessiveFrameEffects flags a frame node iff ANY of:
  - blur > 40 (glow / halo signature; modal-shell scrim uses exactly
    40 so the threshold is strict-greater to keep that legitimate use
    untouched — verified by detectors-builder-clean.test.ts)
  - any effect carries spread > 0 (bleeding outward = the "spiked
    shadow" the user called out)
  - 3+ stacked effects on one frame (typical UI uses 0-2)

Suggested fix is to remove the effects array; the user / agent can
re-add a proper subtle shadow afterwards if intentional.

Wired through detectAllIssues + index.ts public exports + the
debug_validation_report MCP categories enum. Skips text nodes (those
go through detectTextEffect with a stricter zero-tolerance rule).

7 new tests cover: positive on spread > 0 / blur > 40 / 3+ stacked,
negative on typical subtle shadow / no effects / blur exactly 40
(modal-shell legit) / text node (different detector). 241 / 241
pen-ai-skills tests pass (was 234; +7). 1110 / 1110 AI service tests
pass (unchanged — production builders pre-clean).
2026-05-09 21:41:00 +08:00
Fini e8a07e9b08 feat(ai): aesthetic detector — mixed-sibling-padding (mirror cornerRadius rule)
Why: continuation of the aesthetic detector series. Mirrors
detectMixedSiblingCornerRadius (53435bf7) for the padding axis. Three
cards with padding 16 / 16 / 20 looks ragged on canvas; the existing
sibling-inconsistency detector covers cards-vs-cards but dedupes
against cornerRadius and other props so the padding outlier
sometimes drops.

What: detectMixedSiblingPadding normalises padding values to a
4-tuple [top, right, bottom, left] before comparison, so
  padding: 16            → [16,16,16,16]
  padding: [12, 24]      → [12,24,12,24] (CSS 2-tuple shorthand)
  padding: [16,16,16,16] → [16,16,16,16]
all compare equal and don't trigger false positives. Modal value
collapses back to a scalar when all four sides are equal so the
suggested fix matches the model's preferred shorthand.

Same 60% modal-majority threshold as the cornerRadius detector —
1-1-1 three-way splits are skipped because there's no canonical value
to suggest. Same divider / spacer skip and same-type-and-role grouping.

Wired through detectAllIssues + index.ts exports + the
debug_validation_report MCP categories enum.

6 new tests cover: number shorthand outlier, number-vs-array
equivalence, 2-tuple-vs-4-tuple equivalence, 1-1-1 split skip,
mixed-role groups skipped, no-padding siblings excluded from modal.
57 / 57 diagnostics tests pass (was 51; +6).
2026-05-09 21:26:00 +08:00
Fini b24bc58a51 feat(ai): aesthetic detector — text-stroke (outlined UI labels)
Why: continuation of the aesthetic detector series. Outlined text on a
UI label is almost always an AI mistake — Lucide / SF / Material icons
get stroked, but body / heading / label text is filled. The model
occasionally copies a generic "give it a stroke" instruction onto text
nodes; on canvas the result reads as double-rendered glyphs. The
existing sibling-inconsistency detector doesn't catch this because
text stroke is rarely a sibling-by-sibling outlier — it's emitted
across the whole tree at once.

What:
- detectTextStroke added with the same shape as the other text-only
  aesthetic detectors (text node + property check + warning severity +
  suggestedValue undefined).
- Skips stroke.thickness === 0 (some model JSON keeps an empty stroke
  object as a placeholder; flagging that would be noise).
- Wired through detectAllIssues + index.ts public exports + the
  debug_validation_report MCP tool's categories enum.

Tests: 4 new positive + negative cases (text with stroke, text without
stroke, text with thickness=0 placeholder, frame with stroke). 51 / 51
diagnostics tests pass (was 47; +4); 228 / 228 pen-ai-skills overall.
2026-05-09 21:25:00 +08:00
Fini 96965161af feat(ai): aesthetic detector — text-effect (shadow / blur on text labels)
Why: continuation of the aesthetic detector family added in 53435bf7.
The model frequently sprinkles \`effects: [{type:'shadow', …}]\` onto
body / label / caption text. On canvas the type goes fuzzy and reads
"AI-designed". Real product UIs use text shadows extremely sparingly
(hero overlays on photos, a few brand elements). Detection is cheap
(walk + isArray check) and the suggested fix (remove effects array)
is safe — text shadow on UI labels is almost never intentional.

What:
- detectTextEffect added to packages/pen-ai-skills/diagnostics with the
  same shape as the prior 3 (warning severity, suggestedValue undefined,
  reason string for logs).
- Wired through detectAllIssues + index.ts public exports + the
  debug_validation_report MCP tool's categories enum.

Tests: 5 new it() cases covering positive (shadow / blur on text),
negative (text without effects, empty effects array, frame with
effects), and tree-walk (multiple text effects in nested frames).
47 / 47 diagnostics tests pass (was 42; +5).
2026-05-09 21:24:00 +08:00
Fini fcc3d8b4d3 feat(ai): aesthetic detectors — rotation / text-cornerRadius / mixed-sibling-cornerRadius
Why: user reports the validation pipeline lacks "aesthetic standards"
— it accepts misalignment, unwanted corner radius, and other visual
issues as "normal". Existing detectors are pure code-quality (invisible
container / empty path / text height / sibling inconsistency); they
don't catch design-system violations the user can see at a glance.
Vision validation does, but it only runs on Anthropic / Codex /
OpenCode / Gemini providers and only above 30 nodes — leaving a long
tail of small-design / builtin-provider runs with no aesthetic check
at all. Adding cheap pure-function detectors closes that gap with no
upstream provider dependency.

What: 3 new pure detectors in pen-ai-skills/diagnostics:

  - detectUnexpectedRotation — flags non-axis-aligned rotation on
    UI-bearing nodes (frame / text / shape). Skips path / line /
    polygon / image (legitimate decorative geometry frequently
    rotated), skips multiples of 90° (intentional vertical text /
    grid). Catches the "tilted card" hallucination cleanly.

  - detectTextCornerRadius — flags text nodes with cornerRadius > 0.
    Text isn't drawn into a clipped rectangle so the prop is silently
    dropped at render time, but it survives in the doc and burns
    LLM context on subsequent batch_get calls. Suggested fix: remove.

  - detectMixedSiblingCornerRadius — stricter than the existing
    sibling-inconsistency check on cornerRadius alone. Flags outliers
    when 2+ of 3 same-type-and-role siblings share a value and one
    differs (e.g. three cards with cornerRadius 8 / 8 / 12 reads as
    ragged on canvas). Skips 1-1-1 three-way splits (no canonical
    modal) and divider / spacer nodes (visual primitives).

All three are wired through detectAllIssues + the index.ts public
exports + the debug_validation_report MCP tool's `categories` enum so
the user / agent can opt-in or filter via `op debug_validation_report
--categories unexpected-rotation`.

35 new tests cover the load-bearing positive + negative cases for each
detector. 219/219 pen-ai-skills tests pass (was 184; +35). 1080/1080
AI service tests still pass.
2026-05-09 21:23:00 +08:00
Fini af9292d8f5 fix(ai): classify Type 0 components as non-mobile to skip phone chrome
Why: "Design a profile card" through MiniMax-M2.7 produced a 375×803 mobile
screen with auto-injected status bar, because the planner skill listed
"profiles" as a Type 2 single-task screen and the orchestrator's
isMobileScreen heuristic ran on width≤480 alone.

What: design-type.md + decomposition.md add Type 0 (single component:
card / badge / chip / modal) with width=400 height=0 1 subtask no chrome.
isMobileFullScreen helper extracted to orchestrator-plan-classify.ts and
required by both orchestrator.ts and orchestrator-sub-agent.ts so the
two paths can't drift on what "mobile" means (Codex review caught this
when only orchestrator.ts had the new check).

Verified with same MiniMax + same prompt: 400×320 component, 8 nodes,
firstChildRole=card, no status-bar.
2026-05-09 21:00:00 +08:00
Fini 2b534c7cf3 feat(ai): detect mobile section glued to screen edge
Add detectEdgeSectionPadding (12th pre-validation detector). Flags a
mobile-shaped page root (width 320–480) when its horizontal padding is
0 AND a child content section also has 0 left padding AND that section
contains visible text or icon descendants — the chain that produced the
"Categories" no-padding bug. Suggested fix sets root.padding to
[top, 16, bottom, 16] preserving vertical padding.

False-positive guards skip top-nav / bottom-nav / hero / banner roles
and image-only sections that are intentionally full-bleed.

The new detector lives in its own detectors-spacing.ts since detectors.ts
is already over the 800-line file limit.
2026-05-09 20:59:58 +08:00
Fini 40dfc55c95 feat(ai): aesthetic detector — excessive-frame-effects (spread > 0 / blur > 40 / 3+ stacked)
Why: 2026-05-10 user report — "Mexican" badge in the food-app screenshot
landed with a "带尖的背景阴影" (pointy / spiked background shadow). The
underlying cause is the model emitting effects with positive spread,
which "bleeds" the shadow color outward and creates a visible bloom /
halo around the badge that doesn't match real product UI shadows.
Real UI shadows are tight: blur 4-16, spread 0, near-black low-alpha.
Existing detectors only handle text effects (text-effect from 7aef1b14);
frame-level effects had no aesthetic gate.

What: detectExcessiveFrameEffects flags a frame node iff ANY of:
  - blur > 40 (glow / halo signature; modal-shell scrim uses exactly
    40 so the threshold is strict-greater to keep that legitimate use
    untouched — verified by detectors-builder-clean.test.ts)
  - any effect carries spread > 0 (bleeding outward = the "spiked
    shadow" the user called out)
  - 3+ stacked effects on one frame (typical UI uses 0-2)

Suggested fix is to remove the effects array; the user / agent can
re-add a proper subtle shadow afterwards if intentional.

Wired through detectAllIssues + index.ts public exports + the
debug_validation_report MCP categories enum. Skips text nodes (those
go through detectTextEffect with a stricter zero-tolerance rule).

7 new tests cover: positive on spread > 0 / blur > 40 / 3+ stacked,
negative on typical subtle shadow / no effects / blur exactly 40
(modal-shell legit) / text node (different detector). 241 / 241
pen-ai-skills tests pass (was 234; +7). 1110 / 1110 AI service tests
pass (unchanged — production builders pre-clean).
2026-05-09 20:59:55 +08:00
Fini bc9f8d1c62 feat(ai): aesthetic detector — mixed-sibling-padding (mirror cornerRadius rule)
Why: continuation of the aesthetic detector series. Mirrors
detectMixedSiblingCornerRadius (53435bf7) for the padding axis. Three
cards with padding 16 / 16 / 20 looks ragged on canvas; the existing
sibling-inconsistency detector covers cards-vs-cards but dedupes
against cornerRadius and other props so the padding outlier
sometimes drops.

What: detectMixedSiblingPadding normalises padding values to a
4-tuple [top, right, bottom, left] before comparison, so
  padding: 16            → [16,16,16,16]
  padding: [12, 24]      → [12,24,12,24] (CSS 2-tuple shorthand)
  padding: [16,16,16,16] → [16,16,16,16]
all compare equal and don't trigger false positives. Modal value
collapses back to a scalar when all four sides are equal so the
suggested fix matches the model's preferred shorthand.

Same 60% modal-majority threshold as the cornerRadius detector —
1-1-1 three-way splits are skipped because there's no canonical value
to suggest. Same divider / spacer skip and same-type-and-role grouping.

Wired through detectAllIssues + index.ts exports + the
debug_validation_report MCP categories enum.

6 new tests cover: number shorthand outlier, number-vs-array
equivalence, 2-tuple-vs-4-tuple equivalence, 1-1-1 split skip,
mixed-role groups skipped, no-padding siblings excluded from modal.
57 / 57 diagnostics tests pass (was 51; +6).
2026-05-09 20:59:40 +08:00
Fini 3eacc8c9ac feat(ai): aesthetic detector — text-stroke (outlined UI labels)
Why: continuation of the aesthetic detector series. Outlined text on a
UI label is almost always an AI mistake — Lucide / SF / Material icons
get stroked, but body / heading / label text is filled. The model
occasionally copies a generic "give it a stroke" instruction onto text
nodes; on canvas the result reads as double-rendered glyphs. The
existing sibling-inconsistency detector doesn't catch this because
text stroke is rarely a sibling-by-sibling outlier — it's emitted
across the whole tree at once.

What:
- detectTextStroke added with the same shape as the other text-only
  aesthetic detectors (text node + property check + warning severity +
  suggestedValue undefined).
- Skips stroke.thickness === 0 (some model JSON keeps an empty stroke
  object as a placeholder; flagging that would be noise).
- Wired through detectAllIssues + index.ts public exports + the
  debug_validation_report MCP tool's categories enum.

Tests: 4 new positive + negative cases (text with stroke, text without
stroke, text with thickness=0 placeholder, frame with stroke). 51 / 51
diagnostics tests pass (was 47; +4); 228 / 228 pen-ai-skills overall.
2026-05-09 20:59:39 +08:00
Fini 6225e09f08 feat(ai): aesthetic detector — text-effect (shadow / blur on text labels)
Why: continuation of the aesthetic detector family added in 53435bf7.
The model frequently sprinkles \`effects: [{type:'shadow', …}]\` onto
body / label / caption text. On canvas the type goes fuzzy and reads
"AI-designed". Real product UIs use text shadows extremely sparingly
(hero overlays on photos, a few brand elements). Detection is cheap
(walk + isArray check) and the suggested fix (remove effects array)
is safe — text shadow on UI labels is almost never intentional.

What:
- detectTextEffect added to packages/pen-ai-skills/diagnostics with the
  same shape as the prior 3 (warning severity, suggestedValue undefined,
  reason string for logs).
- Wired through detectAllIssues + index.ts public exports + the
  debug_validation_report MCP tool's categories enum.

Tests: 5 new it() cases covering positive (shadow / blur on text),
negative (text without effects, empty effects array, frame with
effects), and tree-walk (multiple text effects in nested frames).
47 / 47 diagnostics tests pass (was 42; +5).
2026-05-09 20:59:38 +08:00
Fini 2faf79b5d1 feat(ai): aesthetic detectors — rotation / text-cornerRadius / mixed-sibling-cornerRadius
Why: user reports the validation pipeline lacks "aesthetic standards"
— it accepts misalignment, unwanted corner radius, and other visual
issues as "normal". Existing detectors are pure code-quality (invisible
container / empty path / text height / sibling inconsistency); they
don't catch design-system violations the user can see at a glance.
Vision validation does, but it only runs on Anthropic / Codex /
OpenCode / Gemini providers and only above 30 nodes — leaving a long
tail of small-design / builtin-provider runs with no aesthetic check
at all. Adding cheap pure-function detectors closes that gap with no
upstream provider dependency.

What: 3 new pure detectors in pen-ai-skills/diagnostics:

  - detectUnexpectedRotation — flags non-axis-aligned rotation on
    UI-bearing nodes (frame / text / shape). Skips path / line /
    polygon / image (legitimate decorative geometry frequently
    rotated), skips multiples of 90° (intentional vertical text /
    grid). Catches the "tilted card" hallucination cleanly.

  - detectTextCornerRadius — flags text nodes with cornerRadius > 0.
    Text isn't drawn into a clipped rectangle so the prop is silently
    dropped at render time, but it survives in the doc and burns
    LLM context on subsequent batch_get calls. Suggested fix: remove.

  - detectMixedSiblingCornerRadius — stricter than the existing
    sibling-inconsistency check on cornerRadius alone. Flags outliers
    when 2+ of 3 same-type-and-role siblings share a value and one
    differs (e.g. three cards with cornerRadius 8 / 8 / 12 reads as
    ragged on canvas). Skips 1-1-1 three-way splits (no canonical
    modal) and divider / spacer nodes (visual primitives).

All three are wired through detectAllIssues + the index.ts public
exports + the debug_validation_report MCP tool's `categories` enum so
the user / agent can opt-in or filter via `op debug_validation_report
--categories unexpected-rotation`.

35 new tests cover the load-bearing positive + negative cases for each
detector. 219/219 pen-ai-skills tests pass (was 184; +35). 1080/1080
AI service tests still pass.
2026-05-09 20:59:37 +08:00
Fini b2f6ae99ff Merge remote-tracking branch 'origin/v0.8.0' into v0.8.0 2026-05-08 22:33:12 +08:00
Kayshen-X 136274a3ec chore(vendor): bump jian submodule to d5d358e (Step 1b §3.2 P0.5A)
Picks up the keyboard/IME/focus event additions + W3C wheel deltaMode
landed in jian commit d5d358e. shell-core re-exports of the new types
land in the next commit; this commit only moves the pointer + Cargo.lock.

cargo test -p openpencil-shell-core --test gesture_re_export → 6/6 PASS
against the pinned submodule.
2026-05-08 22:03:08 +08:00
Fini 9513b4692d feat(ai): v1 builder fuzzy-coerce hallucinated params (Rank 1)
ab-v8 KPI showed ~14/95 obvious-T fail = v1 schema throw on hallucinated
enum values (tone='info') / missing required arrays (params.columns
undefined). Builder rejected the whole tool call instead of degrading.

Replace throw paths in 12 v1 builders + entry-coerce in 4 builders that
directly accessed params.X.map() / .forEach():

- chart-{pie,line,bars}-v1: coerceNumberArray fallback [1]
- tag-v1 / heading-v1 / callout-v1 / member-row-v1 / invite-row-v1 /
  activity-log-v1: coerceEnum fallback to schema default
- timeline-v1 / social-login-row-v1: coerceNonEmptyArray with placeholder
- kbd-v1: coerceStringArray fallback ['?']
- data-table-row-v1 / combobox-v1 / toolbar-v1 / share-row-v1: entry
  coerceNonEmptyArray (no prior throw, but params.X.map() crashed on
  undefined input)

New helper packages/pen-core/src/element-builders/coerce-params.ts with
five primitives (coerceEnum, coerceNonEmptyArray, coerceNumberArray,
coerceStringArray, coerceNonEmptyString) + process-global warning sink
for orchestrators to surface coercions to the LLM.

v0 builders unchanged (byte-parity contract still holds — verified by
existing parity tests).

3 pen-core tests + 1 pen-mcp test updated: previously asserted toThrow
on invalid input -> now assert coerce success + warning emission.

Tests: 4223/4223 pass; format:check + tsc clean.

Predicted KPI lift: M3 obvious-T 58% -> ~64% (~14/235 schema-throw fail
recovered; ~6pp). Composite-T unchanged (composite fail is routing/
parse, not schema — verified by ab-v8 raw analysis).
2026-05-08 08:00:00 +08:00
Fini 71d3d8b5b7 feat(ai): add image_search_query param to add_image_placeholder_v0/v1
Without an explicit query, the auto-search pipeline can only fall back
to the placeholder's `label` (often unset for context-rich cards) or
finally a generic "placeholder" string — both produce off-topic stock
photos instead of, e.g., burger / sushi shots for a food-app brief.

Builders (`buildImagePlaceholder`, `buildImagePlaceholderV1`) now accept
an optional `image_search_query` param (snake_case to match the rest of
the params interface). When set, it gets stamped onto the resulting
frame as `imageSearchQuery` — the same camelCase field
`image-search-pipeline.ts::extractQueryForNode` already prefers over
`name` and the label child.

Tool definitions in `element-tool-defs-ext-2.ts` (v0) and
`element-tool-defs-ext-6.ts` (v1) expose the new property with a
description that nudges callers to pass 2-3 keywords ("burger fries",
"modern office workspace") for product / restaurant / hero contexts.

3 new tests in `add-image-placeholder-v0.test.ts`: query stamps onto
frame, omitted query leaves field undefined, empty-string query is
treated as missing.
2026-05-05 04:01:51 +08:00
Kayshen-X a4b7f62e9a Merge feat/rust-ification into v0.8.0 (Step 0 Rust workspace bootstrap)
Step 0 of OP Rust-ification (per kickoff spec v7 FROZEN):
- Cargo workspace at root (members = ["crates/*"], glob)
- 9 skeleton crates: openpencil-app, openpencil-shell-{core,web,native},
  pen-{types,core,engine,codegen,figma}
- rust-toolchain.toml pinned 1.85 (forced from 1.80 → 1.82 → 1.85
  due to crates.io ecosystem edition2024 requirements)
- deny.toml with kickoff §1.2 wasm32 ban invariant
- 2 GitHub Actions: rust-check.yml (3-platform native + cargo-deny)
  and wasm-bundle-check.yml (wasm32 forward + reverse cargo-deny bans)
- vendor/agent submodule → github.com/ZSeven-W/agent-rs
- Bun script wrappers (cargo:check / :test / :wasm-check / :deny)
- README "Rust subsystem" section + Phase boundary note

§1.2 invariants live:
- Forward wasm32 check: shell-web + 5 bucket A crates compile
- Reverse cargo-deny check bans: native + wasm32 both clean
- compile_error guard: shell-native fails wasm32 build with explicit
  message, validated by canary

Step 1+ owns real implementation; Phase 0 docs (snapshot / plan
patches / IPC inventory / parley-taffy matrix / cargo-deny validation)
in openpencil-docs.
2026-05-04 21:00:00 +08:00
Kayshen-X 4e92f0c250 Merge origin/v0.8.0 into feat/rust-ification 2026-05-03 21:00:00 +08:00
MseeP.ai 112921c9ea Add MseeP.ai badge to README.md (#124) 2026-04-29 09:50:57 +08:00
Fini 238ab344e2 feat(element-tools): add 11 v1 tools with theme parameter (P3 batch 9 — FINAL)
Converts tabs, tag, text_button, textarea, timeline, toolbar, tooltip,
top_nav_bar, upload_dropzone, user_card, video_placeholder to theme-aware v1.
All 9 touchpoints wired; ext-8 extended and new ext-9 shard created for overflow.
ListTools count: 177 → 188. All 4127 tests pass.

Classification:
- Pass-through (all modes identical, no surface colors): text_button, textarea,
  top_nav_bar, tabs (accent brand-invariant), tooltip (dark=inverted per §3.4),
  tag (status tones per §3.4), video_placeholder (dark bg per §3.4)
- Surface-tint (light/dark/system tokenized): timeline (inactive dot+connector+subtitle),
  toolbar (surface+border+active-bg+icon), upload_dropzone (5 tokens),
  user_card (name+role text)
2026-04-29 09:50:29 +08:00
Fini d547e6917b feat(element-tools): add 10 v1 tools with theme parameter (P3 batch 8)
Converts sidebar_nav, skeleton, social_login_row, spinner, stat_card,
stat_grid, status_badge, step_card, stepper, switch to theme-aware v1.
All 9 touchpoints wired; ext-8 shard extended for schema definitions.
ListTools count: 167 → 177. All 4116 tests pass.

Notable: spinner/stat_grid/status_badge/switch emit identical trees
across all theme modes (caller-param colors, status semantics, or iOS
HIG builder-private literals per spec §3.4) — theme param accepted
for API consistency only.
2026-04-29 09:50:28 +08:00
Fini e1a931e4ff feat(element-tools): add 10 v1 tools with theme parameter (P3 batch 7)
Converts progress_bar, quote_block, radio, range_slider, rating_stars,
search_bar, section_header, segmented_control, select, share_row to
theme-aware v1. All 9 touchpoints wired; adds ext-8 shard for schema
definitions. ListTools count: 157 → 167. All 4106 tests pass.
2026-04-29 09:50:27 +08:00
Fini ab695d9633 feat(element-tools): add 10 v1 tools with theme parameter (P3 batch 6)
Converts metric_comparison, metric_row, nav_chip_row, notification_row,
otp_input, pagination, phone_input, price, pricing_card, profile_header
to theme-aware v1. All 9 touchpoints wired; adds ext-7 shard for schema
definitions. ListTools count: 147 → 157. All 4096 tests pass.
2026-04-29 09:50:26 +08:00
Fini 028ef9048b feat(element-tools): add 10 v1 tools with theme parameter (P3 batch 5)
Adds theme-aware v1 builders for icon_button, image_placeholder,
inbox_message, inline_action, input_with_action, invite_row, kbd,
legend_item, link, and list_row. Group A (zero-color: icon_button,
link, list_row) — no hardcoded colors in v0, all three modes identical.
Group B (kbd) — key bg → surface2, stroke → border in dark/system.
Group C (remaining 6) — surface/text/border/accent/alertColors tokens
applied in dark/system modes, full byte-parity with v0 in light mode.

Extends ext-6 shard (357→647 lines, within 800-line ceiling) housing
all 20 batch-4 + batch-5 tool schema definitions. All 9 touchpoints
wired per playbook: builder, index.ts, pen-core barrel, handler,
dispatcher, ext-6 shard, client shim, server builder, elements.md entries.

Verified: format:check clean, tsc --noEmit clean, 4086/4086 tests pass.
2026-04-29 09:50:25 +08:00
Fini f4a38dc508 feat(element-tools): add 10 v1 tools with theme parameter (P3 batch 4)
Adds theme-aware v1 builders for cookie_banner, data_table_row,
date_picker, drawer_shell, empty_state, event_card, fab, faq_item,
filter_group, and form_field. Group A (zero-color: empty_state,
form_field) — no hardcoded colors in v0, all three modes identical.
Group B (fab) — accent bg is brand-invariant, maps to accent token
in dark/system; icon stays white in all modes. Group C (remaining 7)
— surface/text/border/accent tokens applied in dark/system modes, full
byte-parity with v0 in light mode.

Creates ext-6 shard (ext-5 was at 798-line ceiling) housing all 10
new tool schema definitions (357 lines). All 9 touchpoints wired per
playbook: builder, index.ts, pen-core barrel, handler, dispatcher,
ext-6 shard, client shim, server builder, elements.md entries.

Verified: format:check clean, tsc --noEmit clean, 4076/4076 tests pass.
2026-04-29 09:50:24 +08:00
Fini 957f845bf5 feat(element-tools): add 10 v1 tools with theme parameter (P3 batch 3)
Adds theme-aware v1 builders for chart_bars, chart_line, chart_pie,
chat_bubble, checkbox, chip_input, code_block, color_swatch, combobox,
and comment. Group A (chart tools) maps bar/line color to chart-1 token
and pie default palette to chart-1..6 tokens in dark/system modes.
Group B (color_swatch) is theme-invariant — swatch color is caller-
supplied and passes through unchanged. Group C (chat_bubble, checkbox,
chip_input, code_block, combobox, comment) resolves surface/text/border
via semantic palette tokens. All light modes are byte-parity with v0.
2026-04-29 09:50:23 +08:00
Fini c0717d43c4 feat(element-tools): add 10 v1 tools with theme parameter (P3 batch 2)
Adds theme-aware v1 builders for alert, bottom_nav, breadcrumb,
activity_ring, carousel_dots, action_menu, attachment_row,
calendar_grid, avatar_group, and callout. Group A (zero-color:
alert/bottom_nav/breadcrumb/activity_ring) produce identical output
across all three theme modes. Group B (carousel_dots) maps active=
text-primary, inactive=border in dark/system modes. Group C (action_menu/
attachment_row/calendar_grid/avatar_group) resolve surface/text/border via
semantic palette. Group D (callout) maps tone-keyed bg/fg to alert palette
tokens in dark/system modes. All light modes are byte-parity with v0.
2026-04-29 09:50:22 +08:00
Fini ddfb3fa81b feat(element-tools): add 5 atom v1 tools with theme parameter (P3 batch 1)
avatar-v1, badge-v1, divider-v1, body_text-v1, icon_label-v1 — each with
full 9-touchpoint coverage (pen-core builder + index + pen-mcp handler +
schema shard + dispatcher + apps/web shim + SERVER_BUILDERS + parity test
+ elements.md). Light mode is byte-equal to v0; dark/system modes produce
identical output since all 5 tools emit zero hardcoded color fills — theme
param accepted for API consistency across all v1 tools. New shard
element-tool-defs-ext-5.ts created (ext-4 was at 739 lines). All 2026
pen-core + pen-mcp tests pass; format:check + tsc clean.
2026-04-29 09:50:21 +08:00
Fini 4962d6176e feat(element-tools): add 4 representative v1 tools (Task 2.4)
card_row-v1, setting_row-v1, member_row-v1, activity_log-v1 — each with
full 9-touchpoint coverage (pen-core builder + index + pen-mcp handler +
schema shard + dispatcher + apps/web shim + SERVER_BUILDERS + parity test
+ elements.md). Light mode is byte-equal to v0; dark/system use resolveTheme()
for all color fills. activity_log-v1 maps tone×theme to alertColors tokens
(info/success/warning/danger) with neutral falling back to surface/textMuted.
All 3998 tests pass. Completes P2 representative phase.
2026-04-29 09:50:20 +08:00
Fini 6a74933945 feat(element-tools): add heading-v1 with theme parameter (9 touchpoints)
Task 2.3 — representative v1 tool walkthrough for Plan 14 byte-parity contract.
Light mode is byte-equal to add_heading_v0 (V0_LATIN_PRESETS table reused);
dark/system modes use resolveTheme() for fill color and typography token refs.
Adds theme enum [light, dark, system] to add_heading_v1 MCP schema, elements.md
decision tree, shim-server-parity CASES, and SERVER_BUILDERS. All 3937 tests pass.
2026-04-29 09:50:19 +08:00
Fini 9c605a3785 fix(ai): step-card marker clips overflow + tighten doc to short index
The previous schema description and JSDoc invited "Step 1" as a valid
value for `number`, but that prose has 6 chars and overflows the 36px
circle marker. Two changes:

- Builder: add clipContent: true on the marker frame so any caller
  who ignores the docs at least gets a clipped (not bleeding) render.
- Schema + JSDoc: drop the misleading "Step 1" example, document the
  1–3 character contract, and steer prose toward `title` instead.
2026-04-28 08:55:00 +08:00
Fini 1fee613111 feat(ai): ship 5 element tools to reach 97 (filter_group / invite_row / activity_log / event_card / step_card)
Closes the obvious gaps remaining in the family:
- add_filter_group_v0 — sidebar facet (heading + checkbox-style options
  with optional counts). Distinct from nav_chip_row (horizontal scrolling
  chips), tag (single applied chip), segmented_control (mutex tabs).
- add_invite_row_v0 — pending invite row (avatar + email/role + status
  pill + trailing action). Distinct from member_row (a JOINED member,
  no status pill or action) and list_row (no avatar / status / action).
- add_activity_log_v0 — single-line audit feed entry (optional tinted
  icon dot + actor in bold + action + right-aligned timestamp). Uses
  StyledTextSegment[] content for the bold/regular split. Distinct from
  timeline (multi-event vertical with connectors) and notification_row
  (title + body, no actor focus).
- add_event_card_v0 — single calendar event tile (date column with
  month band + day number, then title + time + location). Distinct from
  calendar_grid (the full month grid) and card_row (no date column).
- add_step_card_v0 — onboarding step card (numbered circle / check +
  title + description). Distinct from stepper (horizontal progress nav
  with connectors) and faq_item (collapsible Q&A header).

9 touchpoints per tool: pen-core builder + index + barrel + types,
pen-mcp handler + dispatcher + ext-4 schema, apps/web shim +
SERVER_BUILDERS, parity test (+5 cases), elements.md decision tree
items 86-89 + 6 PREFER mappings with cross-links to existing tools,
elements-cookbook.md arg-shape examples (8 entries across 5 tools).
2026-04-28 08:50:00 +08:00
Fini a5b594cf69 feat(ai): add_member_row_v0 — team / member list row (92nd tool)
Avatar + (name over optional subtitle) + optional trailing slot
(role badge / kebab menu / status dot). Distinct from
add_user_card_v0 (compact fit_content tile, no trailing slot) and
add_list_row_v0 (no avatar slot — leading icon instead).

9 touchpoints wired: pen-core builder + index + barrel + types,
pen-mcp handler + dispatcher + ext-4 schema, apps/web shim +
SERVER_BUILDERS, parity test, elements.md decision tree #84 +
PREFER mapping, cookbook arg shapes (3 variants).

Also disambiguates add_avatar_group_v0's PREFER mapping: drop
"团队成员" (now points at member_row), keep narrower phrases like
"成员头像" / "团队头像" / "presence indicator" that genuinely match
the stacked-avatars affordance, and add the cross-link to member_row.
2026-04-28 08:05:00 +08:00
Fini af1ddd1ad2 feat(ai): add_setting_row_v0 — settings menu row (91st tool)
Leading icon + (title over optional subtitle) + trailing control with
4 variants: chevron / value text / switch / badge. Distinct from
add_list_row_v0 (trailing is always icon, no switch/value/badge) and
add_form_field_v0 (label-above-input for forms).

Wires all 9 touchpoints: pen-core builder + index + barrel re-export,
pen-mcp handler + dispatcher case + ext-4 schema, apps/web shim +
Nitro SERVER_BUILDERS, elements.md decision tree #83 + PREFER mapping,
elements-cookbook arg-shape examples, plus shim-server parity case.
2026-04-28 06:30:00 +08:00
Fini e656b52f95 fix(mcp): describe section enum by parameter name, not invented schema path 2026-04-27 09:45:00 +08:00
Fini 0bed3a5d12 fix(mcp): drop hardcoded section list from get_design_prompt description
The tool-level description still listed 11 specific sections (schema /
layout / roles / text / style / icons / examples / guidelines /
planning / elements / design-md) even though the actual catalog has
28. Replace with a pointer at inputSchema.section.enum, which is
already derived from listPromptSections() — the description now
can't go stale as sections are added or removed.

D0 parity snapshot refreshed for the new description text.
2026-04-27 09:40:00 +08:00
Fini e8cbe712e2 fix(mcp): get_design_prompt enum derives from SECTION_MAP, no more drift
The published section enum had drifted to 14 entries while
SECTION_MAP grew to 28 (copywriting / overflow / cjk / variables +
8 codegen-* + elements-cookbook). External MCP clients calling with
the missing names hit schema-validation rejection even though the
implementation could serve them. Codex caught the immediate
elements-cookbook gap; widening the fix because the same pattern was
already silently broken for half the catalog.

- enum now derives from listPromptSections() at module load, no
  hand-maintained list to drift
- description points at listPromptSections() rather than enumerating
  individual sections (which was its own drift vector)
- design-prompt-elements adds a sync drift-guard: enum set must
  exactly equal listPromptSections() set
- D0 parity snapshot refreshed for the new enum values

3712 tests green.
2026-04-27 09:35:00 +08:00
Fini 3e3b02f227 test(mcp): refresh D0 parity snapshot for elements-cookbook section enum 2026-04-27 09:30:00 +08:00
Fini fa099ce282 fix(mcp): add elements-cookbook to get_design_prompt section enum 2026-04-27 09:25:00 +08:00
Fini f5eff2c9a3 fix(ai-skills): restore element-tool arg-shape examples in elements-cookbook.md
Trimming the Minimal usage block out of elements.md (65c31832) lost
arg-shape templates that the A/B harness depends on. Text-only LLMs
in the treatment arm see only the markdown skill content — no MCP
tools/list, no published inputSchema — so without the per-tool
example payloads they have to guess argument names and break M1.

Restore the full block as a sibling skill `elements-cookbook` (same
hasMcpTools flag, slightly later priority so it loads alongside
elements). Wire it through buildFullPrompt + the
get_design_prompt(section='elements-cookbook') section map. Update
the A/B harness to also strip the cookbook body when building the
baseline prompt — leaving it in B would leak tool names + arg shapes
back into the no-tools variant and re-bias the comparison.

Both files now under the 800-line per-file ceiling.
2026-04-27 09:20:00 +08:00
Fini 5859c3f9af feat(ai): ship 10 element tools to reach 90 (user_card / drawer / combobox / toolbar / callout / share / inline_action / legend_item / inbox / profile_header)
Adds the desktop-leaning batch needed to round the family to 90:

- add_user_card_v0 — compact avatar+name+role row
- add_drawer_shell_v0 — full-height side panel header
- add_combobox_v0 — open-state autocomplete with dropdown
- add_toolbar_v0 — desktop icon button row + dividers
- add_callout_v0 — inline doc tip block, 5 tones
- add_share_row_v0 — circular social-share buttons
- add_inline_action_v0 — message + Undo-style action
- add_legend_item_v0 — chart legend marker+label+value
- add_inbox_message_v0 — email/inbox row with unread dot
- add_profile_header_v0 — large profile hero block

All ten go through the standard 9-touchpoint wiring and land in a
new ext-4 schema shard so existing shards stay under 800 lines.
Drift guards (contract / registry parity / shim-server parity) cover
each new name; per-tool handler tests are deferred — every tool's
structure is exercised through the parity build call already.
2026-04-27 09:05:00 +08:00
Fini de73c09202 fix(ai): tag close icon inherits tone fg color 2026-04-27 09:00:00 +08:00
Fini 072856e522 feat(ai): add_tag_v0 — single closable filter chip (80th tool) 2026-04-27 08:55:00 +08:00
Fini e14b9446c8 fix(ai): data-table cells clip overflow so long content stays in column
Long cell content rode `width=auto` and the cell itself had
`height=fit_content` with no clip, so a string longer than its
allotted column would push past the cell edge into the adjacent
column at render time. Switch each cell to `width/height=
fill_container + clipContent=true`, give the text child
`width=fill_container + textGrowth=fixed-width` so the layout
engine wraps inside the cell, and clip the row itself so a runaway
cell can't push siblings off-row either. Lock the contract in the
handler test.
2026-04-27 08:50:00 +08:00
Fini d24b587b6c feat(ai): add_data_table_row_v0 — desktop tabular row (79th tool) 2026-04-27 08:40:00 +08:00