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.
This commit is contained in:
1 parent
06e096cc9b
commit
b254c49c81
11 files changed
+126
-75
No files matched your search
@@ -15,8 +15,8 @@
|
|||||||
"repo": "https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git",
|
"repo": "https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"adapter": "claude-skill",
|
"adapter": "claude-skill",
|
||||||
"commit": "823b0a14d3539b5d78c0efb614426a4fab5983ec",
|
"commit": "09170eec67eefd46a7ae85de61b40c194020f997",
|
||||||
"syncedAt": "2026-09-26T16:00:00Z"
|
"syncedAt": "2026-09-27T16:00:00Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "caveman",
|
"id": "caveman",
|
||||||
@@ -78,8 +78,8 @@
|
|||||||
"repo": "https://github.com/hugohe3/ppt-master.git",
|
"repo": "https://github.com/hugohe3/ppt-master.git",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"adapter": "claude-skill",
|
"adapter": "claude-skill",
|
||||||
"commit": "481e057ecd9f5ff094c9c789b17b2d1331e278e8",
|
"commit": "680de11f1bef4628b68d5daad9dffec569fbd51f",
|
||||||
"syncedAt": "2026-09-23T16:00:00Z"
|
"syncedAt": "2026-09-27T16:00:00Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "grill-me",
|
"id": "grill-me",
|
||||||
@@ -168,8 +168,8 @@
|
|||||||
"repo": "https://github.com/vercel/next.js.git",
|
"repo": "https://github.com/vercel/next.js.git",
|
||||||
"ref": "canary",
|
"ref": "canary",
|
||||||
"adapter": "skill-collection",
|
"adapter": "skill-collection",
|
||||||
"commit": "1cc0f6617e2f7df9068083066a4c2db76b51fdcb",
|
"commit": "0423222b7eb3a1373b5bff4c939fd69858928993",
|
||||||
"syncedAt": "2026-09-26T16:00:00Z"
|
"syncedAt": "2026-09-27T16:00:00Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "shuorenhua",
|
"id": "shuorenhua",
|
||||||
@@ -177,8 +177,8 @@
|
|||||||
"repo": "https://github.com/MrGeDiao/shuorenhua.git",
|
"repo": "https://github.com/MrGeDiao/shuorenhua.git",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"adapter": "claude-skill",
|
"adapter": "claude-skill",
|
||||||
"commit": "cd681637edb6000bde4b09f63e489a7012dd2a72",
|
"commit": "9e6400d5e2cc050485c38fbf0dda682bce66c180",
|
||||||
"syncedAt": "2026-09-26T16:00:00Z"
|
"syncedAt": "2026-09-27T16:00:00Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -3,5 +3,5 @@
|
|||||||
"name": "playwright浏览器自动化操作",
|
"name": "playwright浏览器自动化操作",
|
||||||
"version": "20260605",
|
"version": "20260605",
|
||||||
"keySource": "none",
|
"keySource": "none",
|
||||||
"syncedAt": "2026-09-26T16:02:01Z"
|
"syncedAt": "2026-09-27T16:02:07Z"
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
"sourceId": "next-skills",
|
"sourceId": "next-skills",
|
||||||
"repo": "https://github.com/vercel/next.js.git",
|
"repo": "https://github.com/vercel/next.js.git",
|
||||||
"ref": "canary",
|
"ref": "canary",
|
||||||
"commit": "1cc0f6617e2f7df9068083066a4c2db76b51fdcb",
|
"commit": "0423222b7eb3a1373b5bff4c939fd69858928993",
|
||||||
"adapter": "skill-collection",
|
"adapter": "skill-collection",
|
||||||
"sourcePath": "skills",
|
"sourcePath": "skills",
|
||||||
"syncedAt": "2026-09-26T16:00:00Z"
|
"syncedAt": "2026-09-27T16:00:00Z"
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@ English | [中文](./README_CN.md)
|
|||||||
|
|
||||||
## ❤️ Sponsors
|
## ❤️ Sponsors
|
||||||
|
|
||||||
This project is kept free and open source with the support of <a href="https://www.kimi.com/code/?aff=ppt-master">Kimi</a>, <a href="https://www.packyapi.ai/register?aff=ppt-master">PackyCode</a>, <a href="https://apikey.fan/register?aff=PPT-MASTER">APIKEY.FAN</a>, <a href="https://runapi.host/register?aff=WMLJ">RunAPI</a>, <a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY-git_pptmaster0624">YouYun ZhiSuan</a>, <a href="https://go.apimart.ai/gh-ppt-master1">APIMart</a> and other sponsors.
|
This project is kept free and open source with the support of <a href="https://www.kimi.com/code/?aff=ppt-master">Kimi</a>, <a href="https://www.packyapi.ai/register?aff=ppt-master">PackyCode</a>, <a href="https://apikey.fan/register?aff=PPT-MASTER">APIKEY.FAN</a>, <a href="https://runapi.host/register?aff=WMLJ">RunAPI</a>, <a href="https://go.apimart.ai/gh-ppt-master1">APIMart</a> and other sponsors.
|
||||||
|
|
||||||
> **[Want to appear here?](SPONSORING.md)**
|
> **[Want to appear here?](SPONSORING.md)**
|
||||||
|
|
||||||
@@ -45,10 +45,6 @@ Thanks to [Kimi](https://www.kimi.com/code/?aff=ppt-master) for sponsoring this
|
|||||||
<td width="180"><a href="https://runapi.host/register?aff=WMLJ"><img src="docs/assets/sponsors/runapi.png" alt="RunAPI" width="150"></a></td>
|
<td width="180"><a href="https://runapi.host/register?aff=WMLJ"><img src="docs/assets/sponsors/runapi.png" alt="RunAPI" width="150"></a></td>
|
||||||
<td>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 <strong>10% of official rates</strong>, 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 <a href="https://runapi.host/register?aff=WMLJ">our dedicated link</a> to claim <strong>¥7 in free credit</strong>.</td>
|
<td>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 <strong>10% of official rates</strong>, 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 <a href="https://runapi.host/register?aff=WMLJ">our dedicated link</a> to claim <strong>¥7 in free credit</strong>.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td width="180"><a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY-git_pptmaster0624"><img src="docs/assets/sponsors/youyun.png" alt="YouYun ZhiSuan" width="150"></a></td>
|
|
||||||
<td>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 <a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY-git_pptmaster0624">this link</a> to receive up to <strong>¥10 in free credits</strong>. This project has been built into an Agent — <strong>PPT Master</strong> — ready to use without local deployment.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td width="180"><a href="https://go.apimart.ai/gh-ppt-master1"><img src="docs/assets/sponsors/apimart.png" alt="APIMart" width="150"></a></td>
|
<td width="180"><a href="https://go.apimart.ai/gh-ppt-master1"><img src="docs/assets/sponsors/apimart.png" alt="APIMart" width="150"></a></td>
|
||||||
<td>Thanks to APIMart for sponsoring this project! APIMart is a low-cost API platform for AI image & video generation — GPT-Image-2 from <strong>$0.006/image</strong>, 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 <a href="https://go.apimart.ai/gh-ppt-master1">here</a> to get started.</td>
|
<td>Thanks to APIMart for sponsoring this project! APIMart is a low-cost API platform for AI image & video generation — GPT-Image-2 from <strong>$0.006/image</strong>, 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 <a href="https://go.apimart.ai/gh-ppt-master1">here</a> to get started.</td>
|
||||||
@@ -418,8 +414,6 @@ PPT Master is currently built and maintained primarily by me. Every new template
|
|||||||
|
|
||||||
<a href="https://runapi.host/register?aff=WMLJ"><img src="docs/assets/sponsors/runapi.png" alt="RunAPI" height="40" /></a>
|
<a href="https://runapi.host/register?aff=WMLJ"><img src="docs/assets/sponsors/runapi.png" alt="RunAPI" height="40" /></a>
|
||||||
|
|
||||||
<a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY-git_pptmaster0624"><img src="docs/assets/sponsors/youyun.png" alt="YouYun ZhiSuan" height="40" /></a>
|
|
||||||
|
|
||||||
<a href="https://go.apimart.ai/gh-ppt-master1"><img src="docs/assets/sponsors/apimart.png" alt="APIMart" height="40" /></a>
|
<a href="https://go.apimart.ai/gh-ppt-master1"><img src="docs/assets/sponsors/apimart.png" alt="APIMart" height="40" /></a>
|
||||||
|
|
||||||
<a href="https://m.do.co/c/547f129aabe1"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" alt="Powered by DigitalOcean" height="40" /></a>
|
<a href="https://m.do.co/c/547f129aabe1"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" alt="Powered by DigitalOcean" height="40" /></a>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
"sourceId": "ppt-master",
|
"sourceId": "ppt-master",
|
||||||
"repo": "https://github.com/hugohe3/ppt-master.git",
|
"repo": "https://github.com/hugohe3/ppt-master.git",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"commit": "481e057ecd9f5ff094c9c789b17b2d1331e278e8",
|
"commit": "680de11f1bef4628b68d5daad9dffec569fbd51f",
|
||||||
"adapter": "claude-skill",
|
"adapter": "claude-skill",
|
||||||
"sourcePath": "skills/ppt-master",
|
"sourcePath": "skills/ppt-master",
|
||||||
"syncedAt": "2026-09-23T16:00:00Z"
|
"syncedAt": "2026-09-27T16:00:00Z"
|
||||||
}
|
}
|
||||||
@@ -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**.
|
[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
|
|
||||||
|
|
||||||
<a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY-git_pptmaster0624"><img src="https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/sponsors/youyun.png" alt="YouYun ZhiSuan" width="150"></a>
|
|
||||||
|
|
||||||
[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
|
||||||
|
|
||||||
<a href="https://go.apimart.ai/gh-ppt-master1"><img src="https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/sponsors/apimart.png" alt="APIMart" width="150"></a>
|
<a href="https://go.apimart.ai/gh-ppt-master1"><img src="https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/sponsors/apimart.png" alt="APIMart" width="150"></a>
|
||||||
|
|||||||
@@ -40,12 +40,6 @@ PPT Master 始终免费开源。以下赞助方共同支持项目的持续维护
|
|||||||
|
|
||||||
[RunAPI](https://runapi.host/register?aff=WMLJ) 通过一个 API Key 提供 OpenAI、Claude、Gemini、DeepSeek、Grok 等 150+ 主流模型。通过专属链接注册并联系管理员,即可领取 **¥7 免费额度**。
|
[RunAPI](https://runapi.host/register?aff=WMLJ) 通过一个 API Key 提供 OpenAI、Claude、Gemini、DeepSeek、Grok 等 150+ 主流模型。通过专属链接注册并联系管理员,即可领取 **¥7 免费额度**。
|
||||||
|
|
||||||
### 优云智算
|
|
||||||
|
|
||||||
<a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY-git_pptmaster0624"><img src="https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/sponsors/youyun.png" alt="优云智算" width="150"></a>
|
|
||||||
|
|
||||||
[优云智算](https://www.compshare.cn/coding-plan?ytag=GPU_YY-git_pptmaster0624) 是 UCloud 旗下 AI 云平台,提供国内外模型 API、CodingPlan 套餐、企业级并发、技术支持和开票服务。通过专属链接注册,最高可获得 **¥10 免费体验金**。平台还提供无需本地部署的 PPT Master Agent。
|
|
||||||
|
|
||||||
### APIMart
|
### APIMart
|
||||||
|
|
||||||
<a href="https://go.apimart.ai/gh-ppt-master1"><img src="https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/sponsors/apimart.png" alt="APIMart" width="150"></a>
|
<a href="https://go.apimart.ai/gh-ppt-master1"><img src="https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/sponsors/apimart.png" alt="APIMart" width="150"></a>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
"sourceId": "shuorenhua",
|
"sourceId": "shuorenhua",
|
||||||
"repo": "https://github.com/MrGeDiao/shuorenhua.git",
|
"repo": "https://github.com/MrGeDiao/shuorenhua.git",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"commit": "cd681637edb6000bde4b09f63e489a7012dd2a72",
|
"commit": "9e6400d5e2cc050485c38fbf0dda682bce66c180",
|
||||||
"adapter": "claude-skill",
|
"adapter": "claude-skill",
|
||||||
"sourcePath": ".",
|
"sourcePath": ".",
|
||||||
"syncedAt": "2026-09-26T16:00:00Z"
|
"syncedAt": "2026-09-27T16:00:00Z"
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
"sourceId": "ui-ux-pro-max",
|
"sourceId": "ui-ux-pro-max",
|
||||||
"repo": "https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git",
|
"repo": "https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git",
|
||||||
"ref": "main",
|
"ref": "main",
|
||||||
"commit": "823b0a14d3539b5d78c0efb614426a4fab5983ec",
|
"commit": "09170eec67eefd46a7ae85de61b40c194020f997",
|
||||||
"adapter": "claude-skill",
|
"adapter": "claude-skill",
|
||||||
"sourcePath": ".claude/skills/ui-ux-pro-max",
|
"sourcePath": ".claude/skills/ui-ux-pro-max",
|
||||||
"syncedAt": "2026-09-26T16:00:00Z"
|
"syncedAt": "2026-09-27T16:00:00Z"
|
||||||
}
|
}
|
||||||
@@ -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())
|
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 ============
|
# ============ DESIGN SYSTEM GENERATOR ============
|
||||||
class DesignSystemGenerator:
|
class DesignSystemGenerator:
|
||||||
"""Generates design system recommendations from aggregated searches."""
|
"""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", {})
|
motion_snippet = design_system.get("motion_snippet", {})
|
||||||
|
|
||||||
def wrap_text(text: str, prefix: str, width: int) -> list:
|
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:
|
if not text:
|
||||||
return []
|
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 = []
|
lines = []
|
||||||
current_line = prefix
|
current_line = prefix
|
||||||
for word in words:
|
for word in words:
|
||||||
@@ -667,6 +695,10 @@ def format_ascii_box(design_system: dict) -> str:
|
|||||||
lines.append(current_line)
|
lines.append(current_line)
|
||||||
return lines
|
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
|
# Build sections from pattern
|
||||||
sections = pattern.get("sections", "").split(" > ")
|
sections = pattern.get("sections", "").split(" > ")
|
||||||
sections = [s.strip() for s in sections if s.strip()]
|
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)
|
# 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")):
|
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:
|
if dials.get("variance") is not None:
|
||||||
lines.append(f"│ Variance: {dials['variance']}/10 — {dials['variance_label']}".ljust(BOX_WIDTH) + "│")
|
lines.append(f"│ Variance: {dials['variance']}/10 — {dials['variance_label']}".ljust(BOX_WIDTH) + "│")
|
||||||
if dials.get("motion") is not None:
|
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) + "│")
|
lines.append(f"│ Density: {dials['density']}/10 — {dials['density_label']}".ljust(BOX_WIDTH) + "│")
|
||||||
|
|
||||||
# Pattern section
|
# Pattern section
|
||||||
lines.append(section_header("PATTERN", BOX_WIDTH + 1))
|
lines.append(section_header("PATTERN", BOX_WIDTH))
|
||||||
lines.append(f"│ Name: {pattern.get('name', '')}".ljust(BOX_WIDTH) + "│")
|
add_wrapped(f"Name: {pattern.get('name', '')}", "│ ")
|
||||||
if pattern.get('conversion'):
|
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'):
|
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) + "│")
|
lines.append("│ Sections:".ljust(BOX_WIDTH) + "│")
|
||||||
for i, section in enumerate(sections, 1):
|
for i, section in enumerate(sections, 1):
|
||||||
lines.append(f"│ {i}. {section}".ljust(BOX_WIDTH) + "│")
|
add_wrapped(f"{i}. {section}", "│ ")
|
||||||
|
|
||||||
# Style section
|
# Style section
|
||||||
lines.append(section_header("STYLE", BOX_WIDTH + 1))
|
lines.append(section_header("STYLE", BOX_WIDTH))
|
||||||
lines.append(f"│ Name: {style.get('name', '')}".ljust(BOX_WIDTH) + "│")
|
add_wrapped(f"Name: {style.get('name', '')}", "│ ")
|
||||||
light = style.get("light_mode", "")
|
light, dark = _mode_support_labels(style, anti_patterns)
|
||||||
dark = style.get("dark_mode", "")
|
|
||||||
if light or dark:
|
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"):
|
if style.get("keywords"):
|
||||||
for line in wrap_text(f"Keywords: {style.get('keywords', '')}", "│ ", BOX_WIDTH):
|
add_wrapped(f"Keywords: {style.get('keywords', '')}")
|
||||||
lines.append(line.ljust(BOX_WIDTH) + "│")
|
|
||||||
if style.get("best_for"):
|
if style.get("best_for"):
|
||||||
for line in wrap_text(f"Best For: {style.get('best_for', '')}", "│ ", BOX_WIDTH):
|
add_wrapped(f"Best For: {style.get('best_for', '')}")
|
||||||
lines.append(line.ljust(BOX_WIDTH) + "│")
|
|
||||||
if style.get("performance") or style.get("accessibility"):
|
if style.get("performance") or style.get("accessibility"):
|
||||||
perf_a11y = f"Performance: {style.get('performance', '')} | Accessibility: {style.get('accessibility', '')}"
|
add_wrapped(f"Performance: {style.get('performance', '')} | Accessibility: {style.get('accessibility', '')}")
|
||||||
lines.append(f"│ {perf_a11y}".ljust(BOX_WIDTH) + "│")
|
|
||||||
|
|
||||||
# Colors section (extended palette with ANSI swatches)
|
# 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:
|
for label, key, css_var in SEMANTIC_COLOR_ENTRIES:
|
||||||
hex_val = colors.get(key, "")
|
hex_val = colors.get(key, "")
|
||||||
if not hex_val:
|
if not hex_val:
|
||||||
continue
|
continue
|
||||||
swatch = hex_to_ansi(hex_val)
|
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) + "│")
|
lines.append(ansi_ljust(content, BOX_WIDTH) + "│")
|
||||||
if colors.get("notes"):
|
if colors.get("notes"):
|
||||||
for line in wrap_text(f"Notes: {colors.get('notes', '')}", "│ ", BOX_WIDTH):
|
for line in wrap_text(f"Notes: {colors.get('notes', '')}", "│ ", BOX_WIDTH):
|
||||||
lines.append(line.ljust(BOX_WIDTH) + "│")
|
lines.append(line.ljust(BOX_WIDTH) + "│")
|
||||||
|
|
||||||
# Typography section
|
# Typography section
|
||||||
lines.append(section_header("TYPOGRAPHY", BOX_WIDTH + 1))
|
lines.append(section_header("TYPOGRAPHY", BOX_WIDTH))
|
||||||
lines.append(f"│ {typography.get('heading', '')} / {typography.get('body', '')}".ljust(BOX_WIDTH) + "│")
|
add_wrapped(f"{typography.get('heading', '')} / {typography.get('body', '')}", "│ ")
|
||||||
if typography.get("mood"):
|
if typography.get("mood"):
|
||||||
for line in wrap_text(f"Mood: {typography.get('mood', '')}", "│ ", BOX_WIDTH):
|
add_wrapped(f"Mood: {typography.get('mood', '')}")
|
||||||
lines.append(line.ljust(BOX_WIDTH) + "│")
|
|
||||||
if typography.get("best_for"):
|
if typography.get("best_for"):
|
||||||
for line in wrap_text(f"Best For: {typography.get('best_for', '')}", "│ ", BOX_WIDTH):
|
add_wrapped(f"Best For: {typography.get('best_for', '')}")
|
||||||
lines.append(line.ljust(BOX_WIDTH) + "│")
|
|
||||||
if typography.get("google_fonts_url"):
|
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"):
|
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
|
# Key Effects section
|
||||||
if effects:
|
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):
|
for line in wrap_text(effects, "│ ", BOX_WIDTH):
|
||||||
lines.append(line.ljust(BOX_WIDTH) + "│")
|
lines.append(line.ljust(BOX_WIDTH) + "│")
|
||||||
|
|
||||||
# Motion section (GSAP skeleton, only if --motion dial was set)
|
# Motion section (GSAP skeleton, only if --motion dial was set)
|
||||||
if motion_snippet:
|
if motion_snippet:
|
||||||
lines.append(section_header("MOTION", BOX_WIDTH + 1))
|
lines.append(section_header("MOTION", BOX_WIDTH))
|
||||||
lines.append(f"│ {motion_snippet.get('Category', '')} ({motion_snippet.get('Intensity Tier', '')})".ljust(BOX_WIDTH) + "│")
|
add_wrapped(f"{motion_snippet.get('Category', '')} ({motion_snippet.get('Intensity Tier', '')})", "│ ")
|
||||||
lines.append(f"│ Trigger: {motion_snippet.get('Trigger', '')} | Duration: {motion_snippet.get('Duration', '')} | Easing: {motion_snippet.get('Easing', '')}".ljust(BOX_WIDTH) + "│")
|
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):
|
for line in wrap_text(f"GSAP: {motion_snippet.get('GSAP Snippet', '')}", "│ ", BOX_WIDTH):
|
||||||
lines.append(line.ljust(BOX_WIDTH) + "│")
|
lines.append(line.ljust(BOX_WIDTH) + "│")
|
||||||
if motion_snippet.get("Framework Notes"):
|
if motion_snippet.get("Framework Notes"):
|
||||||
@@ -765,12 +791,12 @@ def format_ascii_box(design_system: dict) -> str:
|
|||||||
|
|
||||||
# Anti-patterns section
|
# Anti-patterns section
|
||||||
if anti_patterns:
|
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):
|
for line in wrap_text(anti_patterns, "│ ", BOX_WIDTH):
|
||||||
lines.append(line.ljust(BOX_WIDTH) + "│")
|
lines.append(line.ljust(BOX_WIDTH) + "│")
|
||||||
|
|
||||||
# Pre-Delivery Checklist section
|
# 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 = [
|
checklist_items = [
|
||||||
"[ ] No emojis as icons (use SVG: Heroicons/Lucide)",
|
"[ ] No emojis as icons (use SVG: Heroicons/Lucide)",
|
||||||
"[ ] cursor-pointer on all clickable elements",
|
"[ ] cursor-pointer on all clickable elements",
|
||||||
@@ -830,8 +856,7 @@ def format_markdown(design_system: dict) -> str:
|
|||||||
# Style section
|
# Style section
|
||||||
lines.append("### Style")
|
lines.append("### Style")
|
||||||
lines.append(f"- **Name:** {style.get('name', '')}")
|
lines.append(f"- **Name:** {style.get('name', '')}")
|
||||||
light = style.get("light_mode", "")
|
light, dark = _mode_support_labels(style, anti_patterns)
|
||||||
dark = style.get("dark_mode", "")
|
|
||||||
if light or dark:
|
if light or dark:
|
||||||
lines.append(f"- **Mode Support:** Light {light} | Dark {dark}")
|
lines.append(f"- **Mode Support:** Light {light} | Dark {dark}")
|
||||||
if style.get('keywords'):
|
if style.get('keywords'):
|
||||||
@@ -1194,7 +1219,7 @@ def format_master_md(design_system: dict) -> str:
|
|||||||
lines.append("/* Primary Button */")
|
lines.append("/* Primary Button */")
|
||||||
lines.append(".btn-primary {")
|
lines.append(".btn-primary {")
|
||||||
lines.append(f" background: {colors.get('cta', '#F97316')};")
|
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(" padding: 12px 24px;")
|
||||||
lines.append(" border-radius: 8px;")
|
lines.append(" border-radius: 8px;")
|
||||||
lines.append(" font-weight: 600;")
|
lines.append(" font-weight: 600;")
|
||||||
@@ -1210,7 +1235,7 @@ def format_master_md(design_system: dict) -> str:
|
|||||||
lines.append("/* Secondary Button */")
|
lines.append("/* Secondary Button */")
|
||||||
lines.append(".btn-secondary {")
|
lines.append(".btn-secondary {")
|
||||||
lines.append(f" background: transparent;")
|
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(f" border: 2px solid {colors.get('primary', '#2563EB')};")
|
||||||
lines.append(" padding: 12px 24px;")
|
lines.append(" padding: 12px 24px;")
|
||||||
lines.append(" border-radius: 8px;")
|
lines.append(" border-radius: 8px;")
|
||||||
|
|||||||
+44
@@ -16,6 +16,7 @@ or directly:
|
|||||||
python scripts/tests/test_design_system_mode.py
|
python scripts/tests/test_design_system_mode.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -26,13 +27,18 @@ sys.path.insert(0, str(SCRIPTS_DIR))
|
|||||||
from design_system import ( # noqa: E402
|
from design_system import ( # noqa: E402
|
||||||
_filter_anti_patterns_for_mode,
|
_filter_anti_patterns_for_mode,
|
||||||
_contrast_ratio,
|
_contrast_ratio,
|
||||||
|
_mode_support_labels,
|
||||||
_palette_is_dark,
|
_palette_is_dark,
|
||||||
_query_wants_dark,
|
_query_wants_dark,
|
||||||
_relative_luminance,
|
_relative_luminance,
|
||||||
_resolve_color_mode,
|
_resolve_color_mode,
|
||||||
_select_palette_for_mode,
|
_select_palette_for_mode,
|
||||||
_style_is_dark_primary,
|
_style_is_dark_primary,
|
||||||
|
BOX_WIDTH,
|
||||||
DesignSystemGenerator,
|
DesignSystemGenerator,
|
||||||
|
format_ascii_box,
|
||||||
|
format_markdown,
|
||||||
|
format_master_md,
|
||||||
) # noqa: I001 - private helpers first, public class last
|
) # noqa: I001 - private helpers first, public class last
|
||||||
|
|
||||||
LIGHT_PALETTE = {"Product Type": "SaaS", "Background": "#F8FAFC", "Foreground": "#020617"}
|
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"]}))
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
unittest.main(verbosity=2)
|
||||||
Reference in new issue
Block a user