From b254c49c8199fba582daf6c731106472d730f6bd Mon Sep 17 00:00:00 2001 From: KeyInfo Bot Date: Mon, 28 Sep 2026 00:02:08 +0800 Subject: [PATCH] Sync third-party and MCP marketplace plugins Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources. Confidence: high Scope-risk: narrow Directive: Keep private/internal skills out of the public marketplace and preserve normal incremental market Git history. Tested: Marketplace validation passed. --- config/external-sources.lock.json | 16 +-- .../plugins/mcp-playwright/MCP_SOURCE.json | 2 +- .../next-skills/THIRD_PARTY_SOURCE.json | 4 +- plugins/codex/plugins/ppt-master/README.md | 8 +- .../ppt-master/THIRD_PARTY_SOURCE.json | 4 +- .../ppt-master/skills/ppt-master/SPONSORS.md | 6 - .../skills/ppt-master/SPONSORS_CN.md | 6 - .../shuorenhua/THIRD_PARTY_SOURCE.json | 4 +- .../ui-ux-pro-max/THIRD_PARTY_SOURCE.json | 4 +- .../ui-ux-pro-max/scripts/design_system.py | 103 +++++++++++------- .../scripts/tests/test_design_system_mode.py | 44 ++++++++ 11 files changed, 126 insertions(+), 75 deletions(-) diff --git a/config/external-sources.lock.json b/config/external-sources.lock.json index b0ff8389..3d871910 100644 --- a/config/external-sources.lock.json +++ b/config/external-sources.lock.json @@ -15,8 +15,8 @@ "repo": "https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git", "ref": "main", "adapter": "claude-skill", - "commit": "823b0a14d3539b5d78c0efb614426a4fab5983ec", - "syncedAt": "2026-09-26T16:00:00Z" + "commit": "09170eec67eefd46a7ae85de61b40c194020f997", + "syncedAt": "2026-09-27T16:00:00Z" }, { "id": "caveman", @@ -78,8 +78,8 @@ "repo": "https://github.com/hugohe3/ppt-master.git", "ref": "main", "adapter": "claude-skill", - "commit": "481e057ecd9f5ff094c9c789b17b2d1331e278e8", - "syncedAt": "2026-09-23T16:00:00Z" + "commit": "680de11f1bef4628b68d5daad9dffec569fbd51f", + "syncedAt": "2026-09-27T16:00:00Z" }, { "id": "grill-me", @@ -168,8 +168,8 @@ "repo": "https://github.com/vercel/next.js.git", "ref": "canary", "adapter": "skill-collection", - "commit": "1cc0f6617e2f7df9068083066a4c2db76b51fdcb", - "syncedAt": "2026-09-26T16:00:00Z" + "commit": "0423222b7eb3a1373b5bff4c939fd69858928993", + "syncedAt": "2026-09-27T16:00:00Z" }, { "id": "shuorenhua", @@ -177,8 +177,8 @@ "repo": "https://github.com/MrGeDiao/shuorenhua.git", "ref": "main", "adapter": "claude-skill", - "commit": "cd681637edb6000bde4b09f63e489a7012dd2a72", - "syncedAt": "2026-09-26T16:00:00Z" + "commit": "9e6400d5e2cc050485c38fbf0dda682bce66c180", + "syncedAt": "2026-09-27T16:00:00Z" } ] } diff --git a/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json b/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json index 6078c213..6d5ebe31 100644 --- a/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json +++ b/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json @@ -3,5 +3,5 @@ "name": "playwright浏览器自动化操作", "version": "20260605", "keySource": "none", - "syncedAt": "2026-09-26T16:02:01Z" + "syncedAt": "2026-09-27T16:02:07Z" } diff --git a/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json index 792115b2..e9e59636 100644 --- a/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "next-skills", "repo": "https://github.com/vercel/next.js.git", "ref": "canary", - "commit": "1cc0f6617e2f7df9068083066a4c2db76b51fdcb", + "commit": "0423222b7eb3a1373b5bff4c939fd69858928993", "adapter": "skill-collection", "sourcePath": "skills", - "syncedAt": "2026-09-26T16:00:00Z" + "syncedAt": "2026-09-27T16:00:00Z" } diff --git a/plugins/codex/plugins/ppt-master/README.md b/plugins/codex/plugins/ppt-master/README.md index b7d85bab..78f7b029 100644 --- a/plugins/codex/plugins/ppt-master/README.md +++ b/plugins/codex/plugins/ppt-master/README.md @@ -15,7 +15,7 @@ English | [中文](./README_CN.md) ## ❤️ Sponsors -This project is kept free and open source with the support of Kimi, PackyCode, APIKEY.FAN, RunAPI, YouYun ZhiSuan, APIMart and other sponsors. +This project is kept free and open source with the support of Kimi, PackyCode, APIKEY.FAN, RunAPI, APIMart and other sponsors. > **[Want to appear here?](SPONSORING.md)** @@ -45,10 +45,6 @@ Thanks to [Kimi](https://www.kimi.com/code/?aff=ppt-master) for sponsoring this RunAPI Thanks to RunAPI for sponsoring this project! RunAPI is an efficient and stable API platform — a single API Key gives you access to 150+ leading models, including OpenAI, Claude, Gemini, DeepSeek, and Grok, at prices as low as 10% of official rates, with exceptional stability and seamless compatibility with tools like Claude Code. RunAPI offers an exclusive perk for PPT Master users: register and contact an administrator via our dedicated link to claim ¥7 in free credit. - - YouYun ZhiSuan - Thanks to YouYun ZhiSuan for sponsoring this project! YouYun ZhiSuan is UCloud's AI cloud platform, providing one-stop API services for mainstream domestic and international models, all accessible with a single key. The platform features cost-effective CodingPlan packages for domestic models (including GLM5.2, Deepseek-v4, and more), along with official channels for stable access to overseas models, meeting diverse development needs. It's compatible with mainstream AI coding tools like Claude Code and Codex, as well as general API calls. The platform supports enterprise-level high concurrency, 24/7 technical support, and self-service invoicing. Register through this link to receive up to ¥10 in free credits. This project has been built into an Agent — PPT Master — ready to use without local deployment. - APIMart Thanks to APIMart for sponsoring this project! APIMart is a low-cost API platform for AI image & video generation — GPT-Image-2 from $0.006/image, 160+ images per dollar. One async API covers both image and video: submit a task, get an ID, fetch results via polling or callback. Batch tens of thousands of images without timeouts, switch models without changing code. Pay-as-you-go with no monthly fee — sign up here to get started. @@ -418,8 +414,6 @@ PPT Master is currently built and maintained primarily by me. Every new template   RunAPI   -YouYun ZhiSuan -  APIMart   Powered by DigitalOcean diff --git a/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json index eb65b86e..001bf4c4 100644 --- a/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "ppt-master", "repo": "https://github.com/hugohe3/ppt-master.git", "ref": "main", - "commit": "481e057ecd9f5ff094c9c789b17b2d1331e278e8", + "commit": "680de11f1bef4628b68d5daad9dffec569fbd51f", "adapter": "claude-skill", "sourcePath": "skills/ppt-master", - "syncedAt": "2026-09-23T16:00:00Z" + "syncedAt": "2026-09-27T16:00:00Z" } diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/SPONSORS.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/SPONSORS.md index 13d05f25..44d79af3 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/SPONSORS.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/SPONSORS.md @@ -40,12 +40,6 @@ Thanks to [Kimi](https://www.kimi.com/code/?aff=ppt-master) for sponsoring PPT M [RunAPI](https://runapi.host/register?aff=WMLJ) provides access to 150+ models, including OpenAI, Claude, Gemini, DeepSeek, and Grok, through one API key. Register through the dedicated link and contact an administrator to claim **¥7 in free credit**. -### YouYun ZhiSuan - -YouYun ZhiSuan - -[YouYun ZhiSuan](https://www.compshare.cn/coding-plan?ytag=GPU_YY-git_pptmaster0624), UCloud's AI cloud platform, provides domestic and international model APIs, CodingPlan packages, enterprise concurrency, technical support, and invoicing. Register through the dedicated link to receive up to **¥10 in free trial credit**. PPT Master is also available there as a hosted Agent for users who do not want to deploy it locally. - ### APIMart APIMart diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/SPONSORS_CN.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/SPONSORS_CN.md index a272df2c..b4159e68 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/SPONSORS_CN.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/SPONSORS_CN.md @@ -40,12 +40,6 @@ PPT Master 始终免费开源。以下赞助方共同支持项目的持续维护 [RunAPI](https://runapi.host/register?aff=WMLJ) 通过一个 API Key 提供 OpenAI、Claude、Gemini、DeepSeek、Grok 等 150+ 主流模型。通过专属链接注册并联系管理员,即可领取 **¥7 免费额度**。 -### 优云智算 - -优云智算 - -[优云智算](https://www.compshare.cn/coding-plan?ytag=GPU_YY-git_pptmaster0624) 是 UCloud 旗下 AI 云平台,提供国内外模型 API、CodingPlan 套餐、企业级并发、技术支持和开票服务。通过专属链接注册,最高可获得 **¥10 免费体验金**。平台还提供无需本地部署的 PPT Master Agent。 - ### APIMart APIMart diff --git a/plugins/codex/plugins/shuorenhua/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/shuorenhua/THIRD_PARTY_SOURCE.json index 3db57b0c..1b4b7131 100644 --- a/plugins/codex/plugins/shuorenhua/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/shuorenhua/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "shuorenhua", "repo": "https://github.com/MrGeDiao/shuorenhua.git", "ref": "main", - "commit": "cd681637edb6000bde4b09f63e489a7012dd2a72", + "commit": "9e6400d5e2cc050485c38fbf0dda682bce66c180", "adapter": "claude-skill", "sourcePath": ".", - "syncedAt": "2026-09-26T16:00:00Z" + "syncedAt": "2026-09-27T16:00:00Z" } diff --git a/plugins/codex/plugins/ui-ux-pro-max/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/ui-ux-pro-max/THIRD_PARTY_SOURCE.json index 77cb092e..fb51818c 100644 --- a/plugins/codex/plugins/ui-ux-pro-max/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/ui-ux-pro-max/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "ui-ux-pro-max", "repo": "https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git", "ref": "main", - "commit": "823b0a14d3539b5d78c0efb614426a4fab5983ec", + "commit": "09170eec67eefd46a7ae85de61b40c194020f997", "adapter": "claude-skill", "sourcePath": ".claude/skills/ui-ux-pro-max", - "syncedAt": "2026-09-26T16:00:00Z" + "syncedAt": "2026-09-27T16:00:00Z" } diff --git a/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/scripts/design_system.py b/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/scripts/design_system.py index 1114088e..d5fe1575 100644 --- a/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/scripts/design_system.py +++ b/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/scripts/design_system.py @@ -254,6 +254,31 @@ def _filter_anti_patterns_for_mode(anti_patterns: str, mode: str) -> str: return " + ".join(clause.strip() for clause in kept if clause.strip()) +def _mode_support_labels(style: dict, anti_patterns: str): + """Light/dark support labels that agree with the product's anti-patterns. + + A light result keeps its "avoid dark mode" clause, so a dark-capable style + must not read as a plain "Dark supported" next to it. + """ + light = style.get("light_mode", "") + dark = style.get("dark_mode", "") + avoids_dark = any( + marker in (anti_patterns or "").lower() for marker in _DARK_ANTI_PATTERN_MARKERS + ) + if dark and dark != "not-recommended" and avoids_dark: + dark = f"{dark}, but avoid for this product" + return light, dark + + +def _button_outline_text_color(colors: dict) -> str: + """Primary for outline-button text when it is readable, else the foreground.""" + primary = colors.get("primary", "#2563EB") + background = colors.get("background", "#FFFFFF") + if (_contrast_ratio(primary, background) or 0) >= 4.5: + return primary + return colors.get("foreground") or primary + + # ============ DESIGN SYSTEM GENERATOR ============ class DesignSystemGenerator: """Generates design system recommendations from aggregated searches.""" @@ -650,10 +675,13 @@ def format_ascii_box(design_system: dict) -> str: motion_snippet = design_system.get("motion_snippet", {}) def wrap_text(text: str, prefix: str, width: int) -> list: - """Wrap long text into multiple lines.""" + """Wrap long text into multiple lines, splitting tokens (URLs) wider than a line.""" if not text: return [] - words = text.split() + room = width - 2 - len(prefix) + words = [] + for word in text.split(): + words.extend(word[i:i + room] for i in range(0, len(word), room)) lines = [] current_line = prefix for word in words: @@ -667,6 +695,10 @@ def format_ascii_box(design_system: dict) -> str: lines.append(current_line) return lines + def add_wrapped(text: str, prefix: str = "│ ") -> None: + for line in wrap_text(text, prefix, BOX_WIDTH): + lines.append(line.ljust(BOX_WIDTH) + "│") + # Build sections from pattern sections = pattern.get("sections", "").split(" > ") sections = [s.strip() for s in sections if s.strip()] @@ -683,7 +715,7 @@ def format_ascii_box(design_system: dict) -> str: # Design Dials section (only if at least one dial was set) if any(dials.get(k) is not None for k in ("variance", "motion", "density")): - lines.append(section_header("DESIGN DIALS", BOX_WIDTH + 1)) + lines.append(section_header("DESIGN DIALS", BOX_WIDTH)) if dials.get("variance") is not None: lines.append(f"│ Variance: {dials['variance']}/10 — {dials['variance_label']}".ljust(BOX_WIDTH) + "│") if dials.get("motion") is not None: @@ -692,71 +724,65 @@ def format_ascii_box(design_system: dict) -> str: lines.append(f"│ Density: {dials['density']}/10 — {dials['density_label']}".ljust(BOX_WIDTH) + "│") # Pattern section - lines.append(section_header("PATTERN", BOX_WIDTH + 1)) - lines.append(f"│ Name: {pattern.get('name', '')}".ljust(BOX_WIDTH) + "│") + lines.append(section_header("PATTERN", BOX_WIDTH)) + add_wrapped(f"Name: {pattern.get('name', '')}", "│ ") if pattern.get('conversion'): - lines.append(f"│ Conversion: {pattern.get('conversion', '')}".ljust(BOX_WIDTH) + "│") + add_wrapped(f"Conversion: {pattern.get('conversion', '')}") if pattern.get('cta_placement'): - lines.append(f"│ CTA: {pattern.get('cta_placement', '')}".ljust(BOX_WIDTH) + "│") + add_wrapped(f"CTA: {pattern.get('cta_placement', '')}") lines.append("│ Sections:".ljust(BOX_WIDTH) + "│") for i, section in enumerate(sections, 1): - lines.append(f"│ {i}. {section}".ljust(BOX_WIDTH) + "│") + add_wrapped(f"{i}. {section}", "│ ") # Style section - lines.append(section_header("STYLE", BOX_WIDTH + 1)) - lines.append(f"│ Name: {style.get('name', '')}".ljust(BOX_WIDTH) + "│") - light = style.get("light_mode", "") - dark = style.get("dark_mode", "") + lines.append(section_header("STYLE", BOX_WIDTH)) + add_wrapped(f"Name: {style.get('name', '')}", "│ ") + light, dark = _mode_support_labels(style, anti_patterns) if light or dark: - lines.append(f"│ Mode Support: Light {light} Dark {dark}".ljust(BOX_WIDTH) + "│") + add_wrapped(f"Mode Support: Light {light} | Dark {dark}") if style.get("keywords"): - for line in wrap_text(f"Keywords: {style.get('keywords', '')}", "│ ", BOX_WIDTH): - lines.append(line.ljust(BOX_WIDTH) + "│") + add_wrapped(f"Keywords: {style.get('keywords', '')}") if style.get("best_for"): - for line in wrap_text(f"Best For: {style.get('best_for', '')}", "│ ", BOX_WIDTH): - lines.append(line.ljust(BOX_WIDTH) + "│") + add_wrapped(f"Best For: {style.get('best_for', '')}") if style.get("performance") or style.get("accessibility"): - perf_a11y = f"Performance: {style.get('performance', '')} | Accessibility: {style.get('accessibility', '')}" - lines.append(f"│ {perf_a11y}".ljust(BOX_WIDTH) + "│") + add_wrapped(f"Performance: {style.get('performance', '')} | Accessibility: {style.get('accessibility', '')}") # Colors section (extended palette with ANSI swatches) - lines.append(section_header("COLORS", BOX_WIDTH + 1)) + lines.append(section_header("COLORS", BOX_WIDTH)) for label, key, css_var in SEMANTIC_COLOR_ENTRIES: hex_val = colors.get(key, "") if not hex_val: continue swatch = hex_to_ansi(hex_val) - content = f"│ {swatch}{label + ':':14s} {hex_val:10s} ({css_var})" + content = f"│ {swatch}{label + ':':18s} {hex_val:10s} ({css_var})" lines.append(ansi_ljust(content, BOX_WIDTH) + "│") if colors.get("notes"): for line in wrap_text(f"Notes: {colors.get('notes', '')}", "│ ", BOX_WIDTH): lines.append(line.ljust(BOX_WIDTH) + "│") # Typography section - lines.append(section_header("TYPOGRAPHY", BOX_WIDTH + 1)) - lines.append(f"│ {typography.get('heading', '')} / {typography.get('body', '')}".ljust(BOX_WIDTH) + "│") + lines.append(section_header("TYPOGRAPHY", BOX_WIDTH)) + add_wrapped(f"{typography.get('heading', '')} / {typography.get('body', '')}", "│ ") if typography.get("mood"): - for line in wrap_text(f"Mood: {typography.get('mood', '')}", "│ ", BOX_WIDTH): - lines.append(line.ljust(BOX_WIDTH) + "│") + add_wrapped(f"Mood: {typography.get('mood', '')}") if typography.get("best_for"): - for line in wrap_text(f"Best For: {typography.get('best_for', '')}", "│ ", BOX_WIDTH): - lines.append(line.ljust(BOX_WIDTH) + "│") + add_wrapped(f"Best For: {typography.get('best_for', '')}") if typography.get("google_fonts_url"): - lines.append(f"│ Google Fonts: {typography.get('google_fonts_url', '')}".ljust(BOX_WIDTH) + "│") + add_wrapped(f"Google Fonts: {typography.get('google_fonts_url', '')}") if typography.get("css_import"): - lines.append(f"│ CSS Import: {typography.get('css_import', '')[:70]}...".ljust(BOX_WIDTH) + "│") + lines.append(f"│ CSS Import: {typography.get('css_import', '')[:67]}...".ljust(BOX_WIDTH) + "│") # Key Effects section if effects: - lines.append(section_header("KEY EFFECTS", BOX_WIDTH + 1)) + lines.append(section_header("KEY EFFECTS", BOX_WIDTH)) for line in wrap_text(effects, "│ ", BOX_WIDTH): lines.append(line.ljust(BOX_WIDTH) + "│") # Motion section (GSAP skeleton, only if --motion dial was set) if motion_snippet: - lines.append(section_header("MOTION", BOX_WIDTH + 1)) - lines.append(f"│ {motion_snippet.get('Category', '')} ({motion_snippet.get('Intensity Tier', '')})".ljust(BOX_WIDTH) + "│") - lines.append(f"│ Trigger: {motion_snippet.get('Trigger', '')} | Duration: {motion_snippet.get('Duration', '')} | Easing: {motion_snippet.get('Easing', '')}".ljust(BOX_WIDTH) + "│") + lines.append(section_header("MOTION", BOX_WIDTH)) + add_wrapped(f"{motion_snippet.get('Category', '')} ({motion_snippet.get('Intensity Tier', '')})", "│ ") + add_wrapped(f"Trigger: {motion_snippet.get('Trigger', '')} | Duration: {motion_snippet.get('Duration', '')} | Easing: {motion_snippet.get('Easing', '')}") for line in wrap_text(f"GSAP: {motion_snippet.get('GSAP Snippet', '')}", "│ ", BOX_WIDTH): lines.append(line.ljust(BOX_WIDTH) + "│") if motion_snippet.get("Framework Notes"): @@ -765,12 +791,12 @@ def format_ascii_box(design_system: dict) -> str: # Anti-patterns section if anti_patterns: - lines.append(section_header("AVOID", BOX_WIDTH + 1)) + lines.append(section_header("AVOID", BOX_WIDTH)) for line in wrap_text(anti_patterns, "│ ", BOX_WIDTH): lines.append(line.ljust(BOX_WIDTH) + "│") # Pre-Delivery Checklist section - lines.append(section_header("PRE-DELIVERY CHECKLIST", BOX_WIDTH + 1)) + lines.append(section_header("PRE-DELIVERY CHECKLIST", BOX_WIDTH)) checklist_items = [ "[ ] No emojis as icons (use SVG: Heroicons/Lucide)", "[ ] cursor-pointer on all clickable elements", @@ -830,8 +856,7 @@ def format_markdown(design_system: dict) -> str: # Style section lines.append("### Style") lines.append(f"- **Name:** {style.get('name', '')}") - light = style.get("light_mode", "") - dark = style.get("dark_mode", "") + light, dark = _mode_support_labels(style, anti_patterns) if light or dark: lines.append(f"- **Mode Support:** Light {light} | Dark {dark}") if style.get('keywords'): @@ -1194,7 +1219,7 @@ def format_master_md(design_system: dict) -> str: lines.append("/* Primary Button */") lines.append(".btn-primary {") lines.append(f" background: {colors.get('cta', '#F97316')};") - lines.append(" color: white;") + lines.append(f" color: {colors.get('on_cta') or 'white'};") lines.append(" padding: 12px 24px;") lines.append(" border-radius: 8px;") lines.append(" font-weight: 600;") @@ -1210,7 +1235,7 @@ def format_master_md(design_system: dict) -> str: lines.append("/* Secondary Button */") lines.append(".btn-secondary {") lines.append(f" background: transparent;") - lines.append(f" color: {colors.get('primary', '#2563EB')};") + lines.append(f" color: {_button_outline_text_color(colors)};") lines.append(f" border: 2px solid {colors.get('primary', '#2563EB')};") lines.append(" padding: 12px 24px;") lines.append(" border-radius: 8px;") diff --git a/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/scripts/tests/test_design_system_mode.py b/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/scripts/tests/test_design_system_mode.py index abe615ba..44c80c1c 100644 --- a/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/scripts/tests/test_design_system_mode.py +++ b/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/scripts/tests/test_design_system_mode.py @@ -16,6 +16,7 @@ or directly: python scripts/tests/test_design_system_mode.py """ +import re import sys import unittest from pathlib import Path @@ -26,13 +27,18 @@ sys.path.insert(0, str(SCRIPTS_DIR)) from design_system import ( # noqa: E402 _filter_anti_patterns_for_mode, _contrast_ratio, + _mode_support_labels, _palette_is_dark, _query_wants_dark, _relative_luminance, _resolve_color_mode, _select_palette_for_mode, _style_is_dark_primary, + BOX_WIDTH, DesignSystemGenerator, + format_ascii_box, + format_markdown, + format_master_md, ) # noqa: I001 - private helpers first, public class last LIGHT_PALETTE = {"Product Type": "SaaS", "Background": "#F8FAFC", "Foreground": "#020617"} @@ -185,5 +191,43 @@ class TestEndToEndCoherence(unittest.TestCase): self.assertFalse(_palette_is_dark({"Background": ds["colors"]["background"]})) +class TestLightResultOutputCoherence(unittest.TestCase): + """A light result that avoids dark mode must not also call dark "supported".""" + + QUERY = "beauty spa wellness" + + @classmethod + def setUpClass(cls): + cls.ds = DesignSystemGenerator().generate(cls.QUERY, "Serenity Spa") + + def test_dark_support_is_qualified_when_the_product_avoids_dark_mode(self): + self.assertIn("dark mode", self.ds["anti_patterns"].lower()) + light, dark = _mode_support_labels(self.ds["style"], self.ds["anti_patterns"]) + self.assertIn("avoid for this product", dark) + self.assertIn("avoid for this product", format_ascii_box(self.ds)) + self.assertIn("avoid for this product", format_markdown(self.ds)) + + def test_labels_unchanged_when_dark_mode_is_not_avoided(self): + style = {"light_mode": "supported", "dark_mode": "supported"} + self.assertEqual(_mode_support_labels(style, "Harsh animations"), + ("supported", "supported")) + + def test_ascii_box_lines_never_overflow_the_border(self): + for query in (self.QUERY, "SaaS invoicing fintech B2B professional dark mode"): + with self.subTest(query=query): + ds = DesignSystemGenerator().generate(query, motion=7) + widths = {len(re.sub(r"\033\[[0-9;]*m", "", line)) + for line in format_ascii_box(ds).splitlines()} + self.assertEqual(widths, {BOX_WIDTH + 1}) + + def test_master_button_text_meets_contrast_on_its_background(self): + colors = self.ds["colors"] + master = format_master_md(self.ds) + primary = re.search(r"\.btn-primary \{\n background: (#\w+);\n color: (#\w+);", master) + secondary = re.search(r"\.btn-secondary \{\n background: transparent;\n color: (#\w+);", master) + self.assertGreaterEqual(_contrast_ratio(primary.group(2), primary.group(1)), 4.5) + self.assertGreaterEqual(_contrast_ratio(secondary.group(1), colors["background"]), 4.5) + + if __name__ == "__main__": unittest.main(verbosity=2)