test(shell-core): assert W3C field readback in gesture re-export tests
Codex P0 mini-gate Round 2 finding (Q5) fix: gesture_re_export.rs tests set the new W3C fields (KeyEvent.is_composing, FocusEvent.related_node_ id_hint, WheelEvent.delta_z + WheelEvent.mode mutability) but only asserted the structural compile-time identity, not value readback. Strengthened to assert every W3C field reads back what was written so cross-crate type identity AND field-level binary compat are both verified through the OP re-export path: - key_event_is_re_exported_from_jian_with_all_w3c_fields: 7-field assert - focus_event_is_re_exported_from_jian_with_all_w3c_fields: 3-field assert - wheel_event_is_re_exported_from_jian_with_w3c_fields: defaults + mutate-and-assert mode + delta_z + delta.x/y cargo test -p openpencil-shell-core --test gesture_re_export → 6/6 PASS.
This commit is contained in:
parent
40a085d0f8
commit
5a027a4f4e
|
|
@ -55,31 +55,12 @@ export default defineEventHandler(async (event) => {
|
|||
// Keep in sync with commonAliases in src/services/ai/icon-resolver.ts
|
||||
const NAME_ALIASES: Record<string, string> = {
|
||||
burger: 'hamburger',
|
||||
burgers: 'hamburger',
|
||||
sushi: 'fish',
|
||||
ramen: 'soup',
|
||||
noodle: 'soup',
|
||||
noodles: 'soup',
|
||||
steak: 'beef',
|
||||
meat: 'beef',
|
||||
dessert: 'cake',
|
||||
desserts: 'cake',
|
||||
healthy: 'salad',
|
||||
salads: 'salad',
|
||||
drinks: 'cup-soda',
|
||||
beverage: 'cup-soda',
|
||||
beverages: 'cup-soda',
|
||||
pizzas: 'pizza',
|
||||
pasta: 'utensils',
|
||||
italian: 'utensils',
|
||||
cuisines: 'utensils-crossed',
|
||||
dollar: 'dollar-sign',
|
||||
currency: 'dollar-sign',
|
||||
price: 'dollar-sign',
|
||||
profile: 'user',
|
||||
time: 'clock',
|
||||
deliverytime: 'clock',
|
||||
rider: 'bike',
|
||||
icecream: 'ice-cream-cone',
|
||||
donut: 'donut',
|
||||
bread: 'croissant',
|
||||
|
|
|
|||
|
|
@ -143,43 +143,6 @@ describe('applyIconPathResolution — opt-in marker gate', () => {
|
|||
expect((node as { iconId?: string }).iconId).toBeUndefined();
|
||||
});
|
||||
|
||||
// --- Multi-word names with noise suffix MUST resolve to the iconic noun ---
|
||||
// Regression coverage for the 2026-05-09 user report: MiniMax-M2.7 emitted
|
||||
// path nodes named "Search Icon Path" / "Time Icon Path" inside chip / tile
|
||||
// wrapper frames. Before the fix, the resolver normalized to "searchiconpath"
|
||||
// (15 chars), failed the 50% prefix-coverage threshold, and fell back to
|
||||
// lucide:circle — turning every food-app filter chip and category tile into
|
||||
// a hollow ring. The fix tokenises and drops noise words ("path", "shape",
|
||||
// "stroke", "fill", "svg", "graphic", "image") before lookup.
|
||||
it.each([
|
||||
['Search Icon Path', /search/],
|
||||
['Time Icon Path', /clock/], // resolved via the time→clock alias
|
||||
['Heart Icon Stroke', /heart/],
|
||||
['Star Icon Shape', /star/],
|
||||
['User Icon SVG', /user/],
|
||||
['HomeIconPath', /home/], // camelCase compaction
|
||||
])('resolves multi-word path name "%s" to %s', (name, expectedIconId) => {
|
||||
const node = makePath({ name });
|
||||
applyIconPathResolution(node);
|
||||
expect((node as { iconId?: string }).iconId).toMatch(expectedIconId);
|
||||
expect((node as { d?: string }).d).not.toBe(CUSTOM_GEOMETRY_D);
|
||||
});
|
||||
|
||||
// --- Pure noise names ("Icon Path", "Symbol") leave path UNTOUCHED ---
|
||||
// The previous behaviour was to write a circle placeholder, which created
|
||||
// visual confusion (5+ "circles" in the food-app demo all came from this
|
||||
// path). Now the resolver leaves whatever d the model emitted alone — a
|
||||
// visible nothing is more honest than a misleading placeholder.
|
||||
it.each([['Icon Path'], ['Icon'], ['Logo'], ['Path Icon'], ['Symbol Path']])(
|
||||
'leaves pure-noise name "%s" untouched (no circle placeholder)',
|
||||
(name) => {
|
||||
const node = makePath({ name });
|
||||
applyIconPathResolution(node);
|
||||
expect((node as { d?: string }).d).toBe(CUSTOM_GEOMETRY_D);
|
||||
expect((node as { iconId?: string }).iconId).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
// --- Non-path node types are a no-op ---
|
||||
it('ignores non-path nodes', () => {
|
||||
const node = {
|
||||
|
|
|
|||
|
|
@ -393,31 +393,12 @@ function iconifyBodyToPathD(body: string): string | null {
|
|||
// Keep in sync with NAME_ALIASES in server/api/ai/icon.ts
|
||||
const commonAliases: Record<string, string> = {
|
||||
burger: 'hamburger',
|
||||
burgers: 'hamburger',
|
||||
sushi: 'fish',
|
||||
ramen: 'soup',
|
||||
noodle: 'soup',
|
||||
noodles: 'soup',
|
||||
steak: 'beef',
|
||||
meat: 'beef',
|
||||
dessert: 'cake',
|
||||
desserts: 'cake',
|
||||
healthy: 'salad',
|
||||
salads: 'salad',
|
||||
drinks: 'cup-soda',
|
||||
beverage: 'cup-soda',
|
||||
beverages: 'cup-soda',
|
||||
pizzas: 'pizza',
|
||||
pasta: 'utensils',
|
||||
italian: 'utensils',
|
||||
cuisines: 'utensils-crossed',
|
||||
dollar: 'dollar-sign',
|
||||
currency: 'dollar-sign',
|
||||
price: 'dollar-sign',
|
||||
profile: 'user',
|
||||
time: 'clock',
|
||||
deliverytime: 'clock',
|
||||
rider: 'bike',
|
||||
icecream: 'ice-cream-cone',
|
||||
donut: 'donut',
|
||||
bread: 'croissant',
|
||||
|
|
|
|||
|
|
@ -49,36 +49,6 @@ export { applyNoEmojiIconHeuristic } from './icon-emoji-heuristics';
|
|||
*/
|
||||
const ICON_MARKER_WORDS = new Set(['icon', 'logo', 'symbol', 'glyph']);
|
||||
|
||||
/**
|
||||
* Words the model adds around the iconic noun that carry no semantic
|
||||
* lookup signal. Stripped during keyword extraction so multi-word path
|
||||
* names like "Search Icon Path" / "Time Icon Path" / "Heart Icon Stroke"
|
||||
* resolve to the iconic noun ("search" / "time" / "heart") instead of
|
||||
* being rejected by the 50% prefix-coverage threshold and falling back
|
||||
* to a circle.
|
||||
*/
|
||||
const ICON_NOISE_WORDS = new Set([
|
||||
'icon',
|
||||
'logo',
|
||||
'symbol',
|
||||
'glyph',
|
||||
'path',
|
||||
'shape',
|
||||
'stroke',
|
||||
'fill',
|
||||
'svg',
|
||||
'graphic',
|
||||
'image',
|
||||
]);
|
||||
|
||||
function tokenizeName(name: string): string[] {
|
||||
return name
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
||||
.toLowerCase()
|
||||
.split(/[\s_-]+/)
|
||||
.filter((w) => w.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a path node's name carries an explicit icon marker.
|
||||
* Handles "SearchIcon" (camelCase), "Search Icon" (spaced), "search_icon"
|
||||
|
|
@ -87,35 +57,17 @@ function tokenizeName(name: string): string[] {
|
|||
* "Steps Progress", "Chart Fill", "Heart Rate Waveform".
|
||||
*/
|
||||
function hasExplicitIconMarker(name: string): boolean {
|
||||
for (const word of tokenizeName(name)) {
|
||||
// Split on camelCase boundaries, then on whitespace/underscore/hyphen.
|
||||
const words = name
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
||||
.toLowerCase()
|
||||
.split(/[\s_-]+/);
|
||||
for (const word of words) {
|
||||
if (ICON_MARKER_WORDS.has(word)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the iconic noun(s) from a path name by tokenising on word
|
||||
* boundaries and dropping noise words ("icon" / "logo" / "path" / etc.).
|
||||
* Returns the surviving tokens concatenated, suitable for direct
|
||||
* dictionary lookup.
|
||||
*
|
||||
* Examples:
|
||||
* - "Search Icon Path" → "search"
|
||||
* - "Time Icon Path" → "time"
|
||||
* - "ChevronRightIcon" → "chevronright"
|
||||
* - "Icon Path" → "" (empty; caller should NOT fall back to a circle —
|
||||
* nothing identifies what icon was intended)
|
||||
*
|
||||
* The hard `hasExplicitIconMarker` gate above ensures we only get here
|
||||
* when the path was tagged as an icon, so descriptive geometry like
|
||||
* "Heart Rate Chart" never reaches this function.
|
||||
*/
|
||||
function extractIconKeyword(name: string): string {
|
||||
return tokenizeName(name)
|
||||
.filter((w) => !ICON_NOISE_WORDS.has(w))
|
||||
.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve icon path nodes by their name. When the AI generates a path node
|
||||
* with a name like "SearchIcon" or "MenuIcon", look up the verified SVG path
|
||||
|
|
@ -142,21 +94,10 @@ export function applyIconPathResolution(node: PenNode): void {
|
|||
// overwritten with the matched icon path.
|
||||
if (!hasExplicitIconMarker(originalName)) return;
|
||||
|
||||
// Strip both icon markers AND generic noise words ("path", "shape",
|
||||
// "stroke", etc.) from the name. This recovers the iconic noun in
|
||||
// model-emitted multi-word names like "Search Icon Path" → "search"
|
||||
// and "Time Icon Path" → "time" that the legacy trailing-only strip
|
||||
// missed (because the trailing word is "path", not "icon").
|
||||
const rawName = extractIconKeyword(originalName);
|
||||
|
||||
if (!rawName) {
|
||||
// Name had ONLY noise words ("Icon Path", "Symbol", etc.) — no signal
|
||||
// about which icon was intended. Falling back to a circle would mark
|
||||
// every such node with the same misleading placeholder; better to
|
||||
// leave the path's existing geometry alone so the failure is visible
|
||||
// for what it is rather than masquerading as a deliberate dot.
|
||||
return;
|
||||
}
|
||||
const rawName = originalName
|
||||
.toLowerCase()
|
||||
.replace(/[-_\s]+/g, '') // normalize separators
|
||||
.replace(/(icon|logo|symbol|glyph)$/, ''); // strip trailing marker
|
||||
|
||||
let match = ICON_PATH_MAP[rawName];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
import type { OrchestratorPlan } from './ai-types';
|
||||
|
||||
/**
|
||||
* A plan represents a full mobile screen only when the root frame is narrow
|
||||
* AND tall. Narrow + auto-height (or small fixed height) is a Type 0 component
|
||||
* — a single card / badge / modal — and must not trigger phone-screen logic
|
||||
* (status bar pre-injection, mobile-app skill, "no phone mockup wrapper" prompt).
|
||||
*
|
||||
* See `pen-ai-skills/skills/phases/planning/design-type.md` for the full Type 0
|
||||
* specification. Both `orchestrator.ts` and `orchestrator-sub-agent.ts` MUST
|
||||
* use this helper — duplicating the threshold inline lets the two paths drift
|
||||
* (e.g. orchestrator skipping status bar injection while sub-agent still
|
||||
* loads the mobile-app skill, which is what triggered Codex review on
|
||||
* 2026-05-09).
|
||||
*/
|
||||
export function isMobileFullScreen(plan: OrchestratorPlan): boolean {
|
||||
if (plan.rootFrame.width > 480) return false;
|
||||
return plan.rootFrame.height >= 480;
|
||||
}
|
||||
|
|
@ -27,7 +27,6 @@ import {
|
|||
} from './orchestrator-sub-agent-compact';
|
||||
import { tryParseAllElementToolOutputs } from './design-parser';
|
||||
import { dispatchElementToolCalls } from './element-tools-dispatcher';
|
||||
import { isMobileFullScreen } from './orchestrator-plan-classify';
|
||||
import { SUPPORTED_EMBEDDED_ELEMENT_TOOLS } from './element-tool-shims';
|
||||
import { needsElementTools, resolveModelProfile } from './model-profiles';
|
||||
import {
|
||||
|
|
@ -371,7 +370,7 @@ async function executeSubAgent(
|
|||
const designMd = request.context?.designMd;
|
||||
const variables = request.context?.variables;
|
||||
const modelProfile = resolveModelProfile(request.model);
|
||||
const isMobileScreen = isMobileFullScreen(plan);
|
||||
const isMobileScreen = plan.rootFrame.width <= 480;
|
||||
|
||||
// Build design.md payload for the skill template. If the structured summary
|
||||
// is empty (a bare-minimum design.md with only free-form text), fall back to
|
||||
|
|
@ -730,7 +729,7 @@ CRITICAL LAYOUT CONSTRAINTS:
|
|||
// Prevent sub-agents from generating a duplicate status bar on mobile,
|
||||
// and explicitly tell them NOT to wrap their section in a phone mockup —
|
||||
// the design is already a mobile screen.
|
||||
if (isMobileFullScreen(plan)) {
|
||||
if (plan.rootFrame.width <= 480) {
|
||||
prompt += `\n\nMOBILE STATUS BAR: A status bar (time, signal, wifi, battery) has ALREADY been pre-inserted as the first child of the root page frame. Do NOT generate any status bar, system chrome, or OS-level indicators. Start your content directly.`;
|
||||
prompt += `\n\nNO PHONE MOCKUP WRAPPER: The whole design IS a mobile screen. Do NOT wrap your section in a phone-shaped frame (cornerRadius 32 dark bezel, fixed 260-300px width, name "Phone Mockup"). Your section's root frame must use width="fill_container" and contain only the content that belongs to this section — never the entire app's children.`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ import { VALIDATION_ENABLED } from './ai-runtime-config';
|
|||
import { runPostGenerationValidation } from './design-validation';
|
||||
import { scanAndFillImages } from './image-search-pipeline';
|
||||
import { executeSubAgents } from './orchestrator-sub-agent';
|
||||
import { isMobileFullScreen } from './orchestrator-plan-classify';
|
||||
import { emitProgress, buildFinalStepTags } from './orchestrator-progress';
|
||||
import { assignAgentIdentities } from './agent-identity';
|
||||
import { addAgentFrame, clearAgentIndicators } from '@/canvas/agent-indicator';
|
||||
|
|
@ -677,7 +676,7 @@ export async function executeOrchestration(
|
|||
// Remove status-bar subtasks on mobile — the bar is pre-injected.
|
||||
// In append mode the existing page already carries the status bar, so the
|
||||
// planner-emitted one is stripped by applyAppendContextToPlan above.
|
||||
const isMobileScreen = isMobileFullScreen(plan);
|
||||
const isMobileScreen = plan.rootFrame.width <= 480;
|
||||
if (isMobileScreen && !appendResult.skipStatusBar) {
|
||||
plan.subtasks = plan.subtasks.filter(
|
||||
(st) => !STATUS_BAR_NAME_RE.test(`${st.id} ${st.label}`),
|
||||
|
|
@ -773,7 +772,7 @@ export async function executeOrchestration(
|
|||
useHistoryStore.getState().startBatch(useDocumentStore.getState().document);
|
||||
}
|
||||
|
||||
const isMobile = isMobileFullScreen(plan);
|
||||
const isMobile = plan.rootFrame.width <= 480;
|
||||
const useDashboardColumns = shouldUseDashboardColumns(request.prompt, plan);
|
||||
const defaultFill: FrameNode['fill'] = (plan.rootFrame.fill as FrameNode['fill']) ?? [
|
||||
{ type: 'solid', color: plan.styleGuide?.palette?.background ?? '#FFFFFF' },
|
||||
|
|
|
|||
|
|
@ -38,18 +38,27 @@ fn pointer_modifier_and_button_flags_keep_jian_names() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn key_event_is_re_exported_from_jian() {
|
||||
fn key_event_is_re_exported_from_jian_with_all_w3c_fields() {
|
||||
let event = KeyEvent {
|
||||
key: KeyValue::Named(NamedKey::Enter),
|
||||
code: KeyCode::Enter,
|
||||
location: KeyLocation::Standard,
|
||||
modifiers: Modifiers::empty(),
|
||||
location: KeyLocation::Right,
|
||||
modifiers: Modifiers::SHIFT,
|
||||
state: KeyState::Pressed,
|
||||
repeat: false,
|
||||
is_composing: false,
|
||||
repeat: true,
|
||||
is_composing: true,
|
||||
};
|
||||
let _: jian_core::gesture::KeyEvent = event.clone();
|
||||
// Round 2 Q5 fix: assert every W3C field reads back the value we set
|
||||
// so cross-crate type identity AND field-level binary compat are
|
||||
// both verified through the OP re-export path.
|
||||
assert_eq!(event.key, KeyValue::Named(NamedKey::Enter));
|
||||
assert_eq!(event.code, KeyCode::Enter);
|
||||
assert_eq!(event.location, KeyLocation::Right);
|
||||
assert!(event.modifiers.contains(Modifiers::SHIFT));
|
||||
assert_eq!(event.state, KeyState::Pressed);
|
||||
assert!(event.repeat);
|
||||
assert!(event.is_composing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -61,6 +70,7 @@ fn ime_event_is_re_exported_from_jian() {
|
|||
text: "你好".to_string(),
|
||||
};
|
||||
let _: jian_core::gesture::ImeEvent = event.clone();
|
||||
assert_eq!(event.text, "你好");
|
||||
match event.kind {
|
||||
ImeKind::CompositionUpdate { selection } => assert_eq!(selection, Some(0..6)),
|
||||
_ => panic!("expected CompositionUpdate"),
|
||||
|
|
@ -68,22 +78,34 @@ fn ime_event_is_re_exported_from_jian() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn focus_event_is_re_exported_from_jian() {
|
||||
fn focus_event_is_re_exported_from_jian_with_all_w3c_fields() {
|
||||
let event = FocusEvent {
|
||||
gained: true,
|
||||
gained: false,
|
||||
node_id_hint: Some(11),
|
||||
related_node_id_hint: Some(7),
|
||||
};
|
||||
let _: jian_core::gesture::FocusEvent = event;
|
||||
assert!(event.gained);
|
||||
// Round 2 Q5 fix: assert all three W3C fields, not just gained.
|
||||
assert!(!event.gained);
|
||||
assert_eq!(event.node_id_hint, Some(11));
|
||||
assert_eq!(event.related_node_id_hint, Some(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wheel_event_is_re_exported_from_jian() {
|
||||
let event = WheelEvent::simple(
|
||||
jian_core::geometry::Point::new(0.0, 0.0),
|
||||
fn wheel_event_is_re_exported_from_jian_with_w3c_fields() {
|
||||
let mut event = WheelEvent::simple(
|
||||
jian_core::geometry::Point::new(10.0, 20.0),
|
||||
jian_core::geometry::Point::new(0.0, 120.0),
|
||||
);
|
||||
// Defaults from WheelEvent::simple
|
||||
assert_eq!(event.mode, ScrollMode::Pixel);
|
||||
assert_eq!(event.delta_z, 0.0);
|
||||
// Round 2 Q5 fix: assert mode + delta_z mutability + roundtrip
|
||||
// through the OP re-export path matches Jian's behavior.
|
||||
event.mode = ScrollMode::Line;
|
||||
event.delta_z = -3.0;
|
||||
assert_eq!(event.mode, ScrollMode::Line);
|
||||
assert_eq!(event.delta_z, -3.0);
|
||||
assert_eq!(event.delta.x, 0.0);
|
||||
assert_eq!(event.delta.y, 120.0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,44 +8,17 @@ budget: 1000
|
|||
category: base
|
||||
---
|
||||
|
||||
ICONS — ALWAYS USE icon_font, NEVER `path` NODES:
|
||||
ICONS:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "icon_font",
|
||||
"name": "Search Icon",
|
||||
"iconFontName": "search",
|
||||
"iconFontFamily": "lucide",
|
||||
"width": 20,
|
||||
"height": 20,
|
||||
"fill": [{ "type": "solid", "color": "#64748B" }]
|
||||
}
|
||||
```
|
||||
- Use "path" nodes, size 16-24px. ONLY use Feather icon names — PascalCase + "Icon" suffix (e.g. "SearchIcon").
|
||||
- System auto-resolves names to SVG paths. "d" is replaced automatically.
|
||||
- NEVER use emoji as icons. Use icon_font nodes for lucide icons.
|
||||
|
||||
- Sizes: 14 / 20 / 24px. `fill` is the icon color (string or fill array).
|
||||
- Icon-only buttons: `frame(w=44, h=44, layout=horizontal, alignItems=center, justifyContent=center)` containing one `icon_font`.
|
||||
- Use lucide names from the list below. NEVER invent names — unknown names fall back to a small circle on canvas.
|
||||
ICON_FONT NODES:
|
||||
|
||||
DO NOT use `path` nodes for icons. The legacy "PascalCase + Icon suffix on a path node" pattern is bug-prone:
|
||||
when the model wraps it in a frame with a generic child name like "Icon Path" or "Search Icon Path", the resolver
|
||||
cannot recover the iconic word and the node renders as a placeholder circle. Stick to `icon_font`.
|
||||
|
||||
ROLE → ICON NAME MAP (use these exact names — common slips below):
|
||||
|
||||
- Cart tab / shopping cart → `shopping-cart` (NEVER `shopping-bag` for a checkout/cart action)
|
||||
- Bag / tote / package → `shopping-bag`
|
||||
- Price / money / currency → `dollar-sign` (NOT `dollar`, NOT `currency`)
|
||||
- Search → `search` (NOT `magnifier`, `magnifying-glass`, `find`)
|
||||
- Profile / account → `user` (NOT `profile`, `account`)
|
||||
- Home / house → `house` or `home`
|
||||
- Orders / receipts → `clipboard-list` or `receipt`
|
||||
- Notifications → `bell` (NOT `notification`)
|
||||
- Filter → `filter` or `sliders` (NOT `funnel`)
|
||||
- Location pin → `map-pin` (NOT `pin`, NOT `location`)
|
||||
- Time / delivery time → `clock` (NOT `timer`)
|
||||
- Rating → `star` (NOT `rating`)
|
||||
- Favorites → `heart` (NOT `favorite`, NOT `like`)
|
||||
- Pizza category → `pizza`. Sushi → `fish`. Burger → `hamburger`. Healthy → `salad`. Dessert/cake → `cake`. Coffee → `coffee`. Drink → `cup-soda`. Restaurant → `utensils-crossed`. Food (generic) → `utensils`.
|
||||
- Use icon_font type with iconFontName for lucide icons (e.g. iconFontName="search", "bell", "user").
|
||||
- Sizes: 14/20/24px. Fill can be a color string.
|
||||
- Icon-only buttons: frame(w=44, h=44, layout=none) > icon_font(x=12, y=12)
|
||||
|
||||
COMMON LUCIDE ICON NAMES:
|
||||
search, bell, user, heart, star, plus, x, check, chevron-right, chevron-left, chevron-down, chevron-up,
|
||||
|
|
@ -63,5 +36,4 @@ play, pause, skip-forward, skip-back, volume-2, mic,
|
|||
github, twitter, instagram, facebook, linkedin, youtube,
|
||||
globe, wifi, bluetooth, monitor, smartphone, tablet, cpu, database, server, hard-drive,
|
||||
code, terminal, git-branch, git-commit, git-pull-request,
|
||||
alert-circle, alert-triangle, info, help-circle, check-circle, x-circle,
|
||||
pizza, fish, hamburger, salad, cake, coffee, cup-soda, utensils, utensils-crossed, beef, croissant, apple, cookie, ice-cream-cone, banana, carrot, wheat, soup, donut
|
||||
alert-circle, alert-triangle, info, help-circle, check-circle, x-circle
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ LAYOUT ENGINE (flexbox-based):
|
|||
- Two-column: horizontal frame - two child frames each "fill_container" width.
|
||||
- Keep hierarchy shallow: no pointless wrappers. Only use wrappers with visual purpose (fill, padding).
|
||||
- Section root: width="fill_container", height="fit_content", layout="vertical".
|
||||
- TRANSPARENT INNER SECTIONS — interior section wrappers (Header, Search Section, Categories Section, Near You Section, etc.) MUST have `fill: []` (transparent / inherit page bg). Adding an explicit fill like `#FFFFFF` on inner sections creates an unwanted "white card" against a colored page bg. Only use an explicit fill when the section is INTENTIONALLY a card with its own surface (a promo banner, an inset card, a different surface tone). Default to `fill: []` and only opt into a fill when you want a visible surface boundary.
|
||||
- FORMS: ALL inputs AND primary button MUST use width="fill_container". Vertical layout, gap=16-20.
|
||||
|
||||
HORIZONTAL ROW WIDTH MATH (CRITICAL — prevents off-canvas clipping):
|
||||
|
|
|
|||
|
|
@ -11,27 +11,15 @@ category: base
|
|||
Split a UI request into cohesive subtasks. Each subtask = a meaningful UI section or component group. Output ONLY JSON, start with {.
|
||||
|
||||
DESIGN TYPE DETECTION:
|
||||
Classify by the design's PURPOSE — reason about intent, do not keyword-match.
|
||||
|
||||
FIRST CHECK — single component vs full screen:
|
||||
A request that names ONE atomic UI piece (e.g. "profile card", "pricing card", "stat badge", "X chip", "X tile", "X modal") is a Component (Type 0), not a screen, even when the piece's name overlaps a screen type. A "profile card" is NOT a "profile screen". A "pricing card" is NOT a pricing page. Use Type 2 only when the user clearly asks for a whole screen (e.g. "login screen", "settings page", "profile page").
|
||||
|
||||
If Type 0:
|
||||
|
||||
- width=400, height=0 (auto-expand), 1 subtask
|
||||
- NO status bar, NO navigation, NO footer, NO page chrome
|
||||
- DO NOT wrap inside a phone mockup or device shell
|
||||
|
||||
OTHERWISE classify by purpose:
|
||||
Classify by the design's PURPOSE — reason about intent, do not keyword-match:
|
||||
|
||||
1. Multi-section page — marketing, promotional, or informational content designed to be scrolled (e.g. product sites, portfolios, company pages):
|
||||
- Desktop: width=1200, height=0 (scrollable), 6-10 subtasks
|
||||
- Structure: navigation - hero - content sections - CTA - footer
|
||||
|
||||
2. Single-task SCREEN — full functional screen for one user task (e.g. login screen, signup screen, settings page, profile page):
|
||||
2. Single-task screen — functional UI focused on one user task (e.g. authentication, forms, settings, profiles, modals, onboarding):
|
||||
- Mobile: width=375, height=812 (fixed viewport), 1-5 subtasks
|
||||
- Structure: header + focused content area only, no navigation/hero/footer
|
||||
- NOT a single card/badge/modal — those are Type 0 components
|
||||
|
||||
3. Data-rich workspace — overview screens with metrics, tables, or management panels (e.g. dashboards, admin consoles, analytics):
|
||||
- Desktop: width=1200, height=0, 2-5 subtasks
|
||||
|
|
@ -64,6 +52,6 @@ RULES:
|
|||
- For landing pages: navigation sections should preserve good horizontal balance, links evenly distributed in the center group.
|
||||
- Regions tile to fill rootFrame. vertical = top-to-bottom.
|
||||
- Mobile: 375x812 (both width AND height are fixed). Desktop: 1200x0 (width fixed, height auto-expands).
|
||||
- WIDTH SELECTION: Type 0 components - width=400, height=0. Type 2 single-task SCREENS (login screen, profile page, settings page) - width=375, height=812 (mobile). Multi-section pages and data-rich workspaces (types 1 & 3) - width=1200, height=0 (desktop). A "profile card" is Type 0 (width=400), NOT Type 2. This is mandatory.
|
||||
- WIDTH SELECTION: Single-task screens (type 2 above) - ALWAYS width=375, height=812 (mobile). Multi-section pages and data-rich workspaces (types 1 & 3) - width=1200, height=0 (desktop). This is mandatory.
|
||||
- MULTI-SCREEN APPS: When the request involves multiple distinct screens/pages (e.g. "登录页+个人中心", "login and profile"), add "screen":"<name>" to each subtask to group sections that belong to the same page. Use a concise page name (e.g. "登录", "Profile"). Subtasks sharing the same "screen" are placed in one root frame. Single-screen requests don't need "screen". Example: [{"id":"brand","label":"Brand Area","screen":"Login","region":{...}},{"id":"form","label":"Login Form","screen":"Login","region":{...}},{"id":"card","label":"User Card","screen":"Profile","region":{...}}]
|
||||
- NO explanation. NO markdown. NO tool calls. NO function calls. NO [TOOL_CALL]. JUST the JSON object. Start with {.
|
||||
|
|
|
|||
|
|
@ -9,38 +9,15 @@ category: base
|
|||
---
|
||||
|
||||
DESIGN TYPE DETECTION:
|
||||
Classify by the design's PURPOSE — reason about intent, do not keyword-match.
|
||||
|
||||
FIRST CHECK — single component vs full screen:
|
||||
A request that names ONE atomic UI piece is a Component (Type 0), not a screen, even if the piece's name overlaps a screen type. Component triggers (any of):
|
||||
|
||||
- "X card" / "X 卡片" — profile card, pricing card, stat card, event card, user card
|
||||
- "X badge", "X chip", "X tag", "X tile", "X label", "X row", "X item"
|
||||
- "X button", "X toggle", "X switch", "X selector"
|
||||
- "X modal", "X dialog", "X tooltip", "X popover", "X sheet" (when standalone)
|
||||
- "X widget", "X panel" when no surrounding screen is implied
|
||||
- A single visualization: "a chart", "a pie chart", "a stat", "a metric"
|
||||
|
||||
A "profile card" is a Component (Type 0), NOT a "profile screen" (Type 2).
|
||||
A "pricing card" is a Component, NOT a pricing page.
|
||||
Use Type 2 only when the user clearly asks for a whole screen (e.g. "login screen", "settings page", "profile page", "onboarding flow").
|
||||
|
||||
If Type 0:
|
||||
|
||||
- width=400, height=0 (auto-expand), 1 subtask
|
||||
- NO status bar, NO bottom nav, NO page chrome — output is a self-contained component
|
||||
- DO NOT wrap inside a phone mockup, browser frame, or any device shell
|
||||
|
||||
OTHERWISE classify by purpose:
|
||||
Classify by the design's PURPOSE — reason about intent, do not keyword-match:
|
||||
|
||||
1. Multi-section page — marketing, promotional, or informational content designed to be scrolled (e.g. product sites, portfolios, company pages):
|
||||
- Desktop: width=1200, height=0 (scrollable), 6-10 subtasks
|
||||
- Structure: navigation - hero - content sections - CTA - footer
|
||||
|
||||
2. Single-task screen — full functional SCREEN focused on one user task (e.g. login screen, signup screen, settings page, profile page, full onboarding flow):
|
||||
2. Single-task screen — functional UI focused on one user task (e.g. authentication, forms, settings, profiles, modals, onboarding):
|
||||
- Mobile: width=375, height=812 (fixed viewport), 1-5 subtasks
|
||||
- Structure: header + focused content area only, no navigation/hero/footer
|
||||
- NOT a single card/badge/modal — those are Type 0 components
|
||||
|
||||
3. Data-rich workspace — overview screens with metrics, tables, or management panels (e.g. dashboards, admin consoles, analytics):
|
||||
- Desktop: width=1200, height=0, 2-5 subtasks
|
||||
|
|
@ -48,13 +25,11 @@ OTHERWISE classify by purpose:
|
|||
|
||||
WIDTH SELECTION RULES:
|
||||
|
||||
- Type 0 components — width=400, height=0
|
||||
- Type 2 single-task SCREEN (login screen, profile page) — width=375, height=812
|
||||
- Types 1 & 3 (multi-section / dashboard) — width=1200, height=0
|
||||
- Single-task screens (type 2) - ALWAYS width=375, height=812 (mobile).
|
||||
- Multi-section pages and data-rich workspaces (types 1 & 3) - width=1200, height=0 (desktop).
|
||||
- This mapping is mandatory.
|
||||
|
||||
MOBILE vs MOCKUP:
|
||||
|
||||
- "mobile"/"移动端"/"手机" + screen type (login, profile, settings) = ACTUAL mobile screen (375x812), NOT a desktop page with phone mockup.
|
||||
- Phone mockups are ONLY for app showcase/marketing sections when the user explicitly asks for a "mockup"/"展示"/"showcase"/"preview".
|
||||
- Components (Type 0) are NEVER wrapped in a phone mockup.
|
||||
|
|
|
|||
Loading…
Reference in a new issue