diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index b0a0e897..86dd6dbe 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -268,18 +268,6 @@ }, "category": "文档处理" }, - { - "name": "ui-ux-pro-max", - "source": { - "source": "local", - "path": "./plugins/codex/plugins/ui-ux-pro-max" - }, - "policy": { - "installation": "AVAILABLE", - "authentication": "ON_INSTALL" - }, - "category": "设计" - }, { "name": "taste-skill", "source": { @@ -304,6 +292,18 @@ }, "category": "开发工具" }, + { + "name": "ui-ux-pro-max", + "source": { + "source": "local", + "path": "./plugins/codex/plugins/ui-ux-pro-max" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "设计" + }, { "name": "shadcn", "source": { diff --git a/config/external-sources.lock.json b/config/external-sources.lock.json index d61e4e43..64d6e55d 100644 --- a/config/external-sources.lock.json +++ b/config/external-sources.lock.json @@ -15,8 +15,8 @@ "repo": "https://github.com/Yeachan-Heo/oh-my-codex.git", "ref": "main", "adapter": "codex-plugin", - "commit": "f4110b0ad672c87f2ca31677ba7faee70332875a", - "syncedAt": "2026-07-08T16:00:01Z" + "commit": "5d43a5bf6f008de17f9425bee4495c457c60b96a", + "syncedAt": "2026-07-10T16:00:00Z" }, { "id": "ui-ux-pro-max", @@ -24,8 +24,8 @@ "repo": "https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git", "ref": "main", "adapter": "claude-skill", - "commit": "12b486b22e67f5d887962ef8351c1ac863bfaeb9", - "syncedAt": "2026-07-06T16:00:00Z" + "commit": "3da52ff1cab1be91848072ec1be5f493d730fd5f", + "syncedAt": "2026-07-10T16:00:00Z" }, { "id": "caveman", @@ -51,8 +51,8 @@ "repo": "https://github.com/shadcn-ui/ui.git", "ref": "main", "adapter": "claude-skill", - "commit": "21e4ceb94418096e21a7f1990027741a8f9b085d", - "syncedAt": "2026-07-09T15:59:59Z" + "commit": "3cdaa6eb2f0da27aca8598cb752c32d840e06940", + "syncedAt": "2026-07-10T16:00:00Z" }, { "id": "frontend-slides", @@ -87,8 +87,8 @@ "repo": "https://github.com/hugohe3/ppt-master.git", "ref": "main", "adapter": "claude-skill", - "commit": "b0beba5b659c664bdbf0c07227fbdee313698dd7", - "syncedAt": "2026-07-09T15:59:59Z" + "commit": "3f3f2418163ccb438d010f3acbce4a3651b758a5", + "syncedAt": "2026-07-10T16:00:00Z" }, { "id": "next-skills", @@ -96,8 +96,8 @@ "repo": "https://github.com/vercel/next.js.git", "ref": "canary", "adapter": "skill-collection", - "commit": "34d92a50266d2b70be5ac8ac147bd270f52d4a12", - "syncedAt": "2026-07-09T15:59:59Z" + "commit": "46681d90f0fd92bb22fbf38b43b507faad47a812", + "syncedAt": "2026-07-10T16:00:00Z" } ] } diff --git a/plugins/codex/.agents/plugins/marketplace.json b/plugins/codex/.agents/plugins/marketplace.json index 34e912fd..9654e912 100644 --- a/plugins/codex/.agents/plugins/marketplace.json +++ b/plugins/codex/.agents/plugins/marketplace.json @@ -268,18 +268,6 @@ }, "category": "文档处理" }, - { - "name": "ui-ux-pro-max", - "source": { - "source": "local", - "path": "./plugins/ui-ux-pro-max" - }, - "policy": { - "installation": "AVAILABLE", - "authentication": "ON_INSTALL" - }, - "category": "设计" - }, { "name": "taste-skill", "source": { @@ -304,6 +292,18 @@ }, "category": "开发工具" }, + { + "name": "ui-ux-pro-max", + "source": { + "source": "local", + "path": "./plugins/ui-ux-pro-max" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "设计" + }, { "name": "shadcn", "source": { diff --git a/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json b/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json index 0fcfd8e7..b8f84f38 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-07-09T16:01:35Z" + "syncedAt": "2026-07-10T16:01:26Z" } diff --git a/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json index e018d51e..28e58567 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": "34d92a50266d2b70be5ac8ac147bd270f52d4a12", + "commit": "46681d90f0fd92bb22fbf38b43b507faad47a812", "adapter": "skill-collection", "sourcePath": "skills", - "syncedAt": "2026-07-09T15:59:59Z" + "syncedAt": "2026-07-10T16:00:00Z" } diff --git a/plugins/codex/plugins/oh-my-codex/.codex-plugin/plugin.json b/plugins/codex/plugins/oh-my-codex/.codex-plugin/plugin.json index a35fb119..408705be 100644 --- a/plugins/codex/plugins/oh-my-codex/.codex-plugin/plugin.json +++ b/plugins/codex/plugins/oh-my-codex/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "oh-my-codex", - "version": "0.19.1", + "version": "0.20.0", "description": "oh-my-codex 是 Codex CLI 的多 Agent 编排、结构化工作流、插件级 hooks、MCP 和 HUD 扩展插件。", "author": { "name": "Yeachan Heo", diff --git a/plugins/codex/plugins/oh-my-codex/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/oh-my-codex/THIRD_PARTY_SOURCE.json index 84607bc0..1c2ff842 100644 --- a/plugins/codex/plugins/oh-my-codex/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/oh-my-codex/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "oh-my-codex", "repo": "https://github.com/Yeachan-Heo/oh-my-codex.git", "ref": "main", - "commit": "f4110b0ad672c87f2ca31677ba7faee70332875a", + "commit": "5d43a5bf6f008de17f9425bee4495c457c60b96a", "adapter": "codex-plugin", "sourcePath": "plugins/oh-my-codex", - "syncedAt": "2026-07-08T16:00:01Z" + "syncedAt": "2026-07-10T16:00:00Z" } diff --git a/plugins/codex/plugins/oh-my-codex/hooks/codex-native-hook.mjs b/plugins/codex/plugins/oh-my-codex/hooks/codex-native-hook.mjs index 356bc23a..046deea2 100644 --- a/plugins/codex/plugins/oh-my-codex/hooks/codex-native-hook.mjs +++ b/plugins/codex/plugins/oh-my-codex/hooks/codex-native-hook.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import { spawn } from 'node:child_process'; import { extname } from 'node:path'; -import { existsSync, readFileSync, realpathSync } from 'node:fs'; +import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -131,20 +131,82 @@ function detectCompactHookInput(input) { } } -async function readBoundedStdin() { +function parseHookPayload(input) { + try { + const parsed = JSON.parse(input.toString('utf8')); + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}; + } catch { + return {}; + } +} + +function sanitizeLaunchId(value) { + return String(value ?? '').trim().replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 128); +} + +function resolveLaunchClaimPath(payload, launchId) { + const cwd = typeof payload.cwd === 'string' && payload.cwd.trim() ? payload.cwd : process.cwd(); + const stateRoot = typeof process.env.OMX_ROOT === 'string' && process.env.OMX_ROOT.trim() + ? process.env.OMX_ROOT.trim() + : join(cwd, '.omx'); + return join(stateRoot, 'state', 'plugin-hook-launches', `${sanitizeLaunchId(launchId)}.json`); +} + +function hookPayloadSessionId(input, payload) { + const parsedSessionId = typeof payload.session_id === 'string' ? payload.session_id.trim() : ''; + if (parsedSessionId) return parsedSessionId; + return extractTopLevelStringField(input.toString('utf8'), ['session_id', 'sessionId'])?.trim() ?? ''; +} + +function isOmxLauncherSession(input, payload) { + const launchId = process.env.OMX_CODEX_LAUNCH_ID?.trim(); + const entryPath = process.env.OMX_ENTRY_PATH?.trim(); + const sessionId = hookPayloadSessionId(input, payload); + if (!launchId || !entryPath || !sessionId) return false; + + const claimPath = resolveLaunchClaimPath(payload, launchId); + + try { + if (existsSync(claimPath)) { + const claimed = JSON.parse(readFileSync(claimPath, 'utf8')); + return claimed?.sessionId === sessionId; + } + mkdirSync(dirname(claimPath), { recursive: true }); + writeFileSync(claimPath, `${JSON.stringify({ sessionId })}\n`, { encoding: 'utf8', mode: 0o600 }); + return true; + } catch { + return false; + } +} + +function writePlainCodexNoop(isStop) { + if (isStop) process.stdout.write('{}\n'); + process.exitCode = 0; +} + +async function readBoundedStdin({ drainOversized = false } = {}) { const chunks = []; let totalBytes = 0; + let storedBytes = 0; + let oversized = false; for await (const rawChunk of process.stdin) { const chunk = Buffer.isBuffer(rawChunk) ? rawChunk : Buffer.from(rawChunk); totalBytes += chunk.length; - if (totalBytes > MAX_WRAPPER_STDIN_BYTES) { - const remaining = MAX_WRAPPER_STDIN_BYTES - Buffer.concat(chunks).length; + + if (oversized) continue; + + const remaining = MAX_WRAPPER_STDIN_BYTES - storedBytes; + if (chunk.length > remaining) { if (remaining > 0) chunks.push(chunk.subarray(0, remaining)); - return { input: Buffer.concat(chunks), oversized: true, totalBytes }; + storedBytes += Math.max(remaining, 0); + oversized = true; + if (!drainOversized) return { input: Buffer.concat(chunks), oversized: true, totalBytes }; + continue; } chunks.push(chunk); + storedBytes += chunk.length; } - return { input: Buffer.concat(chunks), oversized: false, totalBytes }; + return { input: Buffer.concat(chunks), oversized, totalBytes }; } function stopFallbackOutput(stopReason, detail) { @@ -326,9 +388,7 @@ function hasActiveAutopilotStateForOversizedStop(input) { } -function parseSingleJsonObjectOutput(raw) { - const text = String(raw ?? '').trim(); - if (!text) return null; +function parseJsonObjectCandidate(text) { try { const parsed = JSON.parse(text); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null; @@ -338,11 +398,37 @@ function parseSingleJsonObjectOutput(raw) { } } +function isJsonObjectFragmentLine(text) { + return text.startsWith('{') || text.endsWith('}') || /^"[^"]+"\s*:/.test(text); +} + +function parseSingleJsonObjectOutput(raw) { + const text = String(raw ?? '').trim(); + if (!text) return null; + const direct = parseJsonObjectCandidate(text); + if (direct) return direct; + + const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean); + const lastLine = lines.at(-1); + if (!lastLine || !lastLine.startsWith('{') || !lastLine.endsWith('}')) return null; + for (const line of lines.slice(0, -1)) { + if (parseJsonObjectCandidate(line) || isJsonObjectFragmentLine(line)) return null; + } + return parseJsonObjectCandidate(lastLine); +} + async function main() { - const { input, oversized, totalBytes } = await readBoundedStdin(); + const { input, oversized, totalBytes } = await readBoundedStdin({ drainOversized: true }); + const payload = parseHookPayload(input); + const launchedByOmx = isOmxLauncherSession(input, payload); const isStop = detectStopHookInput(input); const isCompact = detectCompactHookInput(input); + if (!launchedByOmx) { + writePlainCodexNoop(isStop); + return; + } + if (oversized) { const message = `plugin hook stdin exceeded ${MAX_WRAPPER_STDIN_BYTES} bytes before launcher delegation; totalBytes>${totalBytes}`; if (isStop) { diff --git a/plugins/codex/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md b/plugins/codex/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md index cc8fa1a1..71db69d2 100644 --- a/plugins/codex/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +++ b/plugins/codex/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md @@ -15,7 +15,7 @@ Use this skill when: - Follow-up implementation left duplicate code, dead code, weak boundaries, missing tests, fallback-like code, or unnecessary wrapper layers - You need a disciplined cleanup workflow without broad rewrites -## GPT-5.5 Guidance Alignment +## GPT-5.6 Guidance Alignment - Keep outputs concise and evidence-dense unless risk or the user requests more detail. - Treat newer user instructions as local workflow updates without discarding earlier non-conflicting constraints. diff --git a/plugins/codex/plugins/oh-my-codex/skills/code-review/SKILL.md b/plugins/codex/plugins/oh-my-codex/skills/code-review/SKILL.md index 36db42bc..06bb78da 100644 --- a/plugins/codex/plugins/oh-my-codex/skills/code-review/SKILL.md +++ b/plugins/codex/plugins/oh-my-codex/skills/code-review/SKILL.md @@ -15,7 +15,7 @@ This skill activates when: - After implementing a major feature - User wants quality assessment -## GPT-5.5 Guidance Alignment +## GPT-5.6 Guidance Alignment - Default to outcome-first progress and completion reporting: state the target result, evidence, validation status, and stop condition before adding process detail. - Treat newer user task updates as local overrides for the active workflow branch while preserving earlier non-conflicting constraints. diff --git a/plugins/codex/plugins/oh-my-codex/skills/omx-setup/SKILL.md b/plugins/codex/plugins/oh-my-codex/skills/omx-setup/SKILL.md index 97bb156f..476749d4 100644 --- a/plugins/codex/plugins/oh-my-codex/skills/omx-setup/SKILL.md +++ b/plugins/codex/plugins/oh-my-codex/skills/omx-setup/SKILL.md @@ -35,11 +35,11 @@ Supported setup flags (current implementation): - if a TTY user has persisted setup preferences, `omx setup` first summarizes the recorded choices and asks whether to **keep**, **review/change**, or **reset** them - else interactive prompt on TTY (default `user`) - else default `user` (safe for CI/tests) -2. If scope is `user`, resolve user skill delivery mode: +2. Resolve setup install mode: - explicit `--plugin`, `--legacy`, or `--install-mode legacy|plugin`, if present - persisted install mode in `./.omx/setup-scope.json`, if present and the TTY review decision is `keep` - - else discovered installed plugin cache under `${CODEX_HOME:-~/.codex}/plugins/cache/**/.codex-plugin/plugin.json` with `name: oh-my-codex` makes `plugin` the default - - else interactive prompt on TTY (`legacy` by default, or `plugin` when a plugin cache is discovered) + - else discovered installed plugin cache under `${CODEX_HOME:-~/.codex}/plugins/cache/**/.codex-plugin/plugin.json` with `name: oh-my-codex` makes `plugin` the default for both `user` and `project` scope, so project setup does not duplicate plugin-provided skills/hooks with legacy `.codex/skills` and `.codex/hooks.json` + - else in `user` scope, interactive prompt on TTY (`legacy` by default, or `plugin` when a plugin cache is discovered) - else default `legacy` unless a plugin cache is discovered 3. Create directories and persist effective scope/install mode 4. In legacy mode, install prompts/native agents/skills and merge full config.toml. In plugin mode, archive/remove legacy OMX-managed prompts/skills, refresh installable native agent TOMLs for `agent_type` routing, clean up stale generated non-installable native agents, and keep native Codex hooks installed. @@ -51,7 +51,7 @@ Supported setup flags (current implementation): - `omx setup` prompts for scope when no scope is provided and stdin/stdout are TTY. If `./.omx/setup-scope.json` already exists, setup now summarizes the saved choices first and asks whether to keep them, review/change them, or reset and behave like a fresh setup run. - Non-interactive setup never blocks for this review prompt: it keeps deterministic CLI/persisted/default behavior for CI and scripted installs. -- In `user` scope, `omx setup` also prompts for skill delivery mode when no prior install mode is kept; installed plugin cache discovery makes plugin mode the default prompt/non-interactive choice. +- In `user` scope, `omx setup` also prompts for skill delivery mode when no prior install mode is kept; installed plugin cache discovery makes plugin mode the default prompt/non-interactive choice. In `project` scope, installed plugin cache discovery selects plugin mode non-interactively to avoid overlapping project legacy skills/hooks with plugin-provided surfaces. - Local project orchestration file is `./AGENTS.md` (project root). - If `AGENTS.md` exists and neither `--force` nor `--merge-agents` is used, interactive TTY runs ask whether to overwrite. Non-interactive runs preserve the file. - Use `--merge-agents` to keep existing project guidance while allowing setup to refresh OMX-managed AGENTS sections and the generated model capability table idempotently. @@ -73,7 +73,7 @@ Use this map when reconciling setup behavior or debugging a confusing install: | Surface | Owner | Notes | | --- | --- | --- | -| `./.omx/setup-scope.json` | `omx setup` | Persists setup scope and user-scope skill delivery mode. TTY reruns summarize it and offer keep/review/reset. | +| `./.omx/setup-scope.json` | `omx setup` | Persists setup scope and install mode when needed. TTY reruns summarize it and offer keep/review/reset. | | `~/.codex/config.toml` / `./.codex/config.toml` | `omx setup` generated blocks + user edits | Setup refreshes OMX-managed blocks while preserving supported manual content; setup-owned runtime feature flags include `multi_agent`, `child_agents_md`, the Codex hook feature flag (`hooks` or legacy `codex_hooks`), and `goals`. | | `~/.codex/hooks.json` / `./.codex/hooks.json` | `omx setup` shared ownership | Setup owns OMX native hook wrappers and preserves user-owned hooks. | | prompts, skills, native agents | `omx setup` or Codex plugin delivery | Legacy mode installs local files; plugin mode relies on plugin discovery for bundled skills, archives/removes legacy OMX-managed prompt/skill copies, and refreshes setup-owned native agent TOMLs for `agent_type` routing while cleaning up stale generated/non-installable native agents. | diff --git a/plugins/codex/plugins/oh-my-codex/skills/prometheus-strict/SKILL.md b/plugins/codex/plugins/oh-my-codex/skills/prometheus-strict/SKILL.md index bd4e098a..e50e2fc7 100644 --- a/plugins/codex/plugins/oh-my-codex/skills/prometheus-strict/SKILL.md +++ b/plugins/codex/plugins/oh-my-codex/skills/prometheus-strict/SKILL.md @@ -42,7 +42,7 @@ OMX already has `$plan`, `$ralplan`, and `$deep-interview`. Prometheus Strict ex - Ask high-leverage questions as a batched round when the answers materially change scope, safety, or validation. Reserve one-at-a-time questioning only for dependent question chains where the next question depends on the previous answer. - If a safe assumption is available, state it and continue. - Use repository reads when needed to make paths, tests, and handoff commands concrete. -- During Metis planning, run pre-question research fan-out for every non-trivial intent unless the task is trivial, the cited spec is self-contained, or cached evidence already covers the same surface; use `explore` for repo facts and the exact cheap `gpt-5.4-mini` `researcher` lane for external docs / OSS references before asking the user. Prometheus Strict may fan out up to `2 explore + 4 researcher` agents per round so breadth comes from more citation-focused mini researchers while Metis/Momus/Oracle keep stronger judgment roles. +- During Metis planning, run pre-question research fan-out for every non-trivial intent unless the task is trivial, the cited spec is self-contained, or cached evidence already covers the same surface; use `explore` for repo facts and the exact cheap `gpt-5.6-terra` `researcher` lane for external docs / OSS references before asking the user. Prometheus Strict may fan out up to `2 explore + 4 researcher` agents per round so breadth comes from more citation-focused mini researchers while Metis/Momus/Oracle keep stronger judgment roles. - Recommend `$team` only when Oracle identifies independent, bounded, verifiable lanes. ### Structured Question Surface @@ -93,7 +93,7 @@ If the prompt contains destructive, credential-gated, external-production, or ma Use `prometheus-strict-metis` as the interview voice. When native subagents are available, invoke the dedicated agent; otherwise run the same role in-context without editing files. -Metis discovers success criteria, non-goals, evidence versus assumptions, required artifacts, likely execution lanes, and missing decisions. Before the first user-facing question batch, Metis must actively fan out repo/external research per intent: `explore` maps local surfaces and exact `gpt-5.4-mini` `researcher` lanes gather official/upstream or OSS-reference evidence. Research-heavy intents use more cheap researchers rather than downgrading Metis/Momus/Oracle judgment. +Metis discovers success criteria, non-goals, evidence versus assumptions, required artifacts, likely execution lanes, and missing decisions. Before the first user-facing question batch, Metis must actively fan out repo/external research per intent: `explore` maps local surfaces and exact `gpt-5.6-terra` `researcher` lanes gather official/upstream or OSS-reference evidence. Research-heavy intents use more cheap researchers rather than downgrading Metis/Momus/Oracle judgment. Run the interview as a bounded loop: @@ -148,7 +148,7 @@ $team :executor "execute the approved Ultragoal story in parallel lanes" # o - Use read-only repository inspection to verify referenced files, commands, and existing conventions. -- Treat Metis research fan-out as part of planning, not execution: dispatch `explore` / exact `gpt-5.4-mini` `researcher` evidence-gathering before question generation for non-trivial intents, then re-prefill and ask only surviving CRITICAL gaps. +- Treat Metis research fan-out as part of planning, not execution: dispatch `explore` / exact `gpt-5.6-terra` `researcher` evidence-gathering before question generation for non-trivial intents, then re-prefill and ask only surviving CRITICAL gaps. - Use `prometheus-strict-metis`, `prometheus-strict-momus`, and `prometheus-strict-oracle` sequentially; do not fan out implementation work from this skill. - Use `$ultragoal` only as the recommended execution handoff after the plan is ready. - Use `$team` only when parallel lanes are independent and verifiable. diff --git a/plugins/codex/plugins/oh-my-codex/skills/ralplan/SKILL.md b/plugins/codex/plugins/oh-my-codex/skills/ralplan/SKILL.md index c51f21d3..7d67156d 100644 --- a/plugins/codex/plugins/oh-my-codex/skills/ralplan/SKILL.md +++ b/plugins/codex/plugins/oh-my-codex/skills/ralplan/SKILL.md @@ -30,7 +30,7 @@ $ralplan --interactive "task description" ## Behavior -## GPT-5.5 Guidance Alignment +## GPT-5.6 Guidance Alignment Use the shared workflow guidance pattern: outcome-first framing, concise visible updates for multi-step planning, local overrides for the active workflow branch, evidence-backed planning and validation expectations, explicit stop rules, right-sized implementation/PRD shape, and automatic continuation for safe reversible steps. Ask only for material, destructive, credentialed, external-production, or preference-dependent branches. diff --git a/plugins/codex/plugins/oh-my-codex/skills/team/SKILL.md b/plugins/codex/plugins/oh-my-codex/skills/team/SKILL.md index 589c7d78..40569a61 100644 --- a/plugins/codex/plugins/oh-my-codex/skills/team/SKILL.md +++ b/plugins/codex/plugins/oh-my-codex/skills/team/SKILL.md @@ -17,7 +17,7 @@ This skill is operationally sensitive. Treat it as an operator workflow, not a g ## What This Skill Must Do -## GPT-5.5 Guidance Alignment +## GPT-5.6 Guidance Alignment Use the shared workflow guidance pattern: outcome-first framing, concise visible updates for multi-step work, local overrides for the active workflow branch, validation proportional to risk, explicit stop rules, and automatic continuation for safe reversible steps. Ask only for material, destructive, credentialed, external-production, or preference-dependent branches. diff --git a/plugins/codex/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md b/plugins/codex/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md index 50fac2fe..a1f3e366 100644 --- a/plugins/codex/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md +++ b/plugins/codex/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md @@ -47,8 +47,8 @@ Use role to choose responsibility, tier to choose depth, and posture to choose o - Prioritizes intent classification, delegation, verification, and architectural judgment. - Typical roles: `planner`, `analyst`, `architect`, `critic`, `code-reviewer`. - Ralplan keeps `planner` and `architect` in this posture; `planner` - uses exact `gpt-5.5` with medium reasoning, `architect` uses exact - `gpt-5.5` with xhigh reasoning, and the `critic` consensus gate stays + uses exact `gpt-5.6-sol` with medium reasoning, `architect` uses exact + `gpt-5.6-sol` with xhigh reasoning, and the `critic` consensus gate stays on the frontier lane. - `deep-worker`: diff --git a/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json index 26db449c..21320ad4 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": "b0beba5b659c664bdbf0c07227fbdee313698dd7", + "commit": "3f3f2418163ccb438d010f3acbce4a3651b758a5", "adapter": "claude-skill", "sourcePath": "skills/ppt-master", - "syncedAt": "2026-07-09T15:59:59Z" + "syncedAt": "2026-07-10T16:00:00Z" } diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/SKILL.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/SKILL.md index 5a7df1ec..6e81d7a5 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/SKILL.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/SKILL.md @@ -9,6 +9,18 @@ description: "多格式源文档到高质量 SVG 页面再导出 PPTX 的多阶 **Core Pipeline**: `Source Document → Create Project → [Template] → Strategist → [Image_Generator] → Executor Live Preview → Quality Check → Post-processing → Export` +### SVG Page-Design Boundary + +| Scope | Contract | +|---|---| +| Any route that authors or regenerates slide visuals through SVG | `svg_output/` is the complete page-design source: every visible text, image, shape, chart/table fallback, and layout element that should appear on the exported slide is present in that page SVG or referenced by it. | +| Templates, `design_spec.md`, and `spec_lock.md` | Authoring/control inputs. They guide SVG creation but MUST NOT supply visible slide content that is absent from the completed SVG during export. | +| Semantic SVG markers | Minimal rendering-neutral compiler hints used only after existing Layout/Layer/Placeholder/Native metadata has been considered. They never replace native SVG geometry, text, styles, grouping, or asset references. | +| SVG-to-PPTX export | Translation only: map SVG content to DrawingML/native objects and reorganize represented content into Master/Layout/Slide structure without inventing new visible page content. | +| Direct PPTX and presentation-behavior workflows | Remain separate. `template-fill-pptx`, `native-enhance-pptx`, animations, transitions, speaker notes, narration, and package relationships are not required to round-trip through SVG. | + +**MUST — page-design closure**: For an SVG-authoring route, inspect the final page SVG to determine what the exported slide looks like. Do not reinterpret “SVG is the page-design language” as “SVG is the complete PPTX package description language.” + > [!CAUTION] > ## 🚨 Global Execution Discipline (MANDATORY) > @@ -21,7 +33,7 @@ description: "多格式源文档到高质量 SVG 页面再导出 PPTX 的多阶 > 5. **NO SPECULATIVE EXECUTION** — "Pre-preparing" content for subsequent Steps is FORBIDDEN (e.g., writing SVG code during the Strategist phase) > 6. **NO SUB-AGENT SVG GENERATION** — Executor Step 6 SVG generation is context-dependent and MUST be completed by the current main agent end-to-end. Delegating page SVG generation to sub-agents is FORBIDDEN > 7. **SEQUENTIAL PAGE GENERATION ONLY** — In Executor Step 6, after the global design context is confirmed, SVG pages MUST be generated sequentially page by page in one continuous pass. Grouped page batches (for example, 5 pages at a time) are FORBIDDEN -> 8. **SPEC_LOCK RE-READ PER PAGE** — Before generating each SVG page, Executor MUST `read_file /spec_lock.md`. All colors / fonts / icons / images MUST come from this file — no values from memory or invented on the fly. Executor MUST also look up the current page's `page_rhythm` (`anchor` / `dense` / `breathing`), `page_layouts` (which template SVG to inherit, if any), and `page_charts` (which chart template to adapt, if any). Empty / absent entries are intentional Strategist signals — see executor-base.md §2.1. This rule exists to resist context-compression drift on long decks and to break the uniform "every page is a card grid" default +> 8. **SPEC_LOCK RE-READ PER PAGE** — Before generating each SVG page, Executor MUST `read_file /spec_lock.md`. All colors / fonts / icons / images MUST come from this file — no values from memory or invented on the fly. Executor MUST also look up the confirmed `pptx_structure.template_adherence` when present, the current page's `page_rhythm` (`anchor` / `dense` / `breathing`), `page_layouts` (which template SVG to inherit), `page_charts` (which chart template to adapt, if any), and — when `pptx_structure.mode` is `template` or `preserve` — `pptx_layouts` (which native PowerPoint layout the page references). Deck/layout template routes require both mappings for every page: `strict` reuses the selected layout contract; `adaptive` may create a new explicit layout contract from the selected reference. Empty / absent `page_layouts` rows are reserved for free design and brand-only routes — see executor-base.md §2.1. This rule exists to resist context-compression drift on long decks and to break the uniform "every page is a card grid" default > 9. **SVG MUST BE HAND-WRITTEN, NOT SCRIPT-GENERATED** — Every SVG page is written by the main agent directly, one page at a time (see rules 6 and 7). Writing or running a Python / Node / shell script that produces the SVG files in batch — looping over pages, templating from data, or emitting them via a generator — is FORBIDDEN, including under "save tokens", "quick draft", or "user is in a hurry" pretexts. The script-generation path was tried on a feature branch and abandoned: cross-page visual consistency depends on per-page authoring with full upstream context, which a generator script cannot reproduce > 10. **FOLLOW DETERMINISTIC ROUTING RULES** — Do not add blocking routing questions when this skill defines a route. If the user request violates a route precondition, state the required prerequisite and stop that route instead of asking the user to choose around the rule. Ordinary finite options, stylistic preferences, and recoverable details are surfaced with a recommended value plus alternatives at the next existing confirmation gate. @@ -259,6 +271,10 @@ find /templates -type f \( -iname '*.png' -o -iname '*.jpg' -o -in The same split applies to all three kinds — bitmaps always land in `images/`, the rest in `templates/`. The spec's `kind` field tells Strategist how to read the `templates/` side; downstream code doesn't distinguish. (Template SVGs in `templates/` are reference material only — the rendered pages live in `svg_output/` and reference images via `../images/`.) +Legacy template packages may ship `native_structure.json` + `source_template.pptx`; keep the pair together in `/templates/` for backward-compatible `preserve` export. Current `create-template` output does not package this pair: it rebuilds complete, explicitly layered SVG templates instead. During fusion, never mix one legacy contract with another template's source package. + +The Strategist confirmation stage decides whether the selected template is used `strict` or `adaptive`. Both use `pptx_structure.mode: template` for current templates and map every page to one reference SVG plus one output PowerPoint layout. `strict` keeps the referenced layout contract unchanged; `adaptive` may create a new layout key while keeping the template Master contract. Legacy `preserve` remains strict-only compatibility behavior. + #### Multi-path fusion When the user gives two or more paths of **different kinds**, Step 3 fuses them into a single `/templates/design_spec.md`. **Default granularity is segment-level integer replacement** — entire identity / structure / middle segments are taken from the highest-priority source for that segment, no implicit field-level mixing. @@ -312,6 +328,8 @@ When fusion happens (any multi-path case), the resulting `/templates/de Single-path Step 3 does **not** add provenance (the source is self-evident from the copied files). +The fused frontmatter `kind` describes the resulting bundle: `deck` when both identity and structure are present, `layout` when only structure is present, and `brand` when only identity is present. Keep this field accurate; the Strategist confirmation server uses it to show template adherence only for bundles that actually own page structure. + **✅ Checkpoint — Default path proceeds to Step 4 without user interaction. If the user supplied one or more explicit template paths, those have been dispatched (or fused) into `/templates/` before advancing.** --- @@ -341,16 +359,17 @@ Read references/strategist.md 2. Page count range 3. Target audience 4. Style objective -5. Color scheme -6. Icon usage approach -7. Typography plan, including formula rendering policy -8. Image usage approach +5. Template adherence — `strict` / `adaptive` (only when Step 3 loaded a deck/layout template) +6. Color scheme +7. Icon usage approach +8. Typography plan, including formula rendering policy +9. Image usage approach **Confirm UI Auto-Launch (Mandatory — default visual confirmation surface)**: by default the Strategist confirmation stage is presented through an interactive local page in **three stages within one browser session** — Stage 1 confirms the direction anchors; the AI then re-derives the design-system layer from the **user's actual** anchors; Stage 2 confirms that layer; the AI then re-derives image and execution choices from the confirmed direction + design system; Stage 3 confirms the final operational layer. Color swatches, live font previews, icon samples, image-style reference previews, and candidate picks appear where they help judgment; the chat path is the always-valid fallback. [`scripts/docs/confirm_ui.md`](scripts/docs/confirm_ui.md) owns the schema, server lifecycle, port strategy, and fallback details; this section keeps the orchestration contract. The split: | Stage | Confirms | Driven by | |---|---|---| -| **1 — direction anchors** | canvas · audience + core message + `content_divergence` + `delivery_purpose` *(PPT only — omitted on non-PPT canvases)* (all §c key info) · `mode` + `visual_style` | the source + user intent | +| **1 — direction anchors** | canvas · audience + core message + `content_divergence` + `delivery_purpose` *(PPT only — omitted on non-PPT canvases)* (all §c key info) · `mode` + `visual_style` · `template_adherence` *(only when Step 3 loaded a deck/layout template)* | the source + user intent | | **2 — design system** (re-derived from Stage 1) | page count · color · typography (font + size) · icons · formula policy | the confirmed Stage 1 | | **3 — images / execution** (re-derived from Stage 1 + Stage 2) | image usage · generated-image style · AI-image generation path · generation mode · refine-spec toggle | the confirmed direction + design system | @@ -360,7 +379,7 @@ Steps: > ⛔ **Steps 2 → 3 → 4 are ONE uninterrupted run — do NOT yield to the user mid-flow.** When an intermediate `--wait` returns, the AI **immediately and autonomously** re-derives and writes the next stage in the **same turn**: do **not** summarize, ask a question, report progress, or end the turn in between. The browser is sitting on a "deriving…" spinner polling for the next stage you must write — stopping here strands the page and the user must prod you in chat to finish (a bug, not the intended flow). **Stage-1 and Stage-2 confirmations are intermediate machine handoffs, not stopping points.** The single ⛔ BLOCKING wait is the **final** confirmation at the end of step 4. (Chat-fallback path — only when the page never opened — is the exception: there you do present each stage in chat and wait for a reply.) -1. **Write Stage 1** to `/confirm_ui/recommendations.json` with `"stage": "stage1"` and only the anchor fields. New recommendations MUST use the canonical `stage` selector. Enumerable anchors (`canvas` / `mode` / `visual_style` / `delivery_purpose`) name a recommended canonical `id` in a `recommend` block (the page lists common options from `confirm_ui/static/catalogs.json`); `visual_style` also carries the ≥3-style `visual_style_spectrum` (safe / shifted / bold — same hard rule as h.5). `audience` and `content_divergence` are plain `{ "value": "" }`. `content_divergence` is the **free-text** field shown under audience in §c — how closely to follow the source vs how freely to reshape it (blank = balanced; facts stay sourced at every level); it is consumed by Strategist when authoring `§IX`, recorded in `design_spec.md §I`, carries no page-count coupling, and is **not** written to `spec_lock.md`. Set `lang` to the page language (`zh` / `en` / `ja`); visible text matches `lang`, or provide multilingual `name_zh` / `name_en` / `name_ja` + `note_zh` / `note_en` / `note_ja` — when the user's language is Japanese, set `lang: "ja"` and always include the `_ja` variants (labels resolve in the page language first — a `ja` page falls back ja → en → zh, so missing `_ja` labels silently render in English; zh/en pages keep their zh↔en fallback and only try `_ja` last). +1. **Write Stage 1** to `/confirm_ui/recommendations.json` with `"stage": "stage1"` and only the anchor fields. New recommendations MUST use the canonical `stage` selector. Enumerable anchors (`canvas` / `mode` / `visual_style` / `delivery_purpose`) name a recommended canonical `id` in a `recommend` block (the page lists common options from `confirm_ui/static/catalogs.json`); `visual_style` also carries the ≥3-style `visual_style_spectrum` (safe / shifted / bold — same hard rule as h.5). When Step 3 loaded a deck/layout template, also set `recommend.template_adherence` to `strict` or `adaptive`; omit the field entirely for free design and brand-only templates so the page does not display it. `audience` and `content_divergence` are plain `{ "value": "" }`. `content_divergence` is the **free-text** field shown under audience in §c — how closely to follow the source vs how freely to reshape it (blank = balanced; facts stay sourced at every level); it is consumed by Strategist when authoring `§IX`, recorded in `design_spec.md §I`, carries no page-count coupling, and is **not** written to `spec_lock.md`. Set `lang` to the page language (`zh` / `en` / `ja`); visible text matches `lang`, or provide multilingual `name_zh` / `name_en` / `name_ja` + `note_zh` / `note_en` / `note_ja` — when the user's language is Japanese, set `lang: "ja"` and always include the `_ja` variants (labels resolve in the page language first — a `ja` page falls back ja → en → zh, so missing `_ja` labels silently render in English; zh/en pages keep their zh↔en fallback and only try `_ja` last). 2. **Launch + wait for Stage 1.** Background launch; the parent returns when the page writes the stage-1 `result.json`. **Long tool timeout — 600000 ms** (the `--wait` ≈590 s budget): ```bash python3 ${SKILL_DIR}/scripts/confirm_ui/server.py --daemon --wait @@ -571,13 +590,17 @@ python3 ${SKILL_DIR}/scripts/svg_editor/server.py --live --daemon > Image facts: trust the `analysis/image_analysis.csv` regenerated at the end of Step 5. If `images/` changed since (the user swapped or added files), re-run `python3 ${SKILL_DIR}/scripts/analyze_images.py /images` before laying images out — facts are re-derived on use, never a stale store (Step 4 image-facts note). -**Per-page spec_lock re-read (Mandatory)**: before **each** SVG page, `read_file /spec_lock.md` and use only its colors / fonts / icons / images, plus the per-page `page_rhythm` / `page_layouts` / `page_charts` lookups (resolves to template SVGs already loaded in the batch read above). Resists context-compression drift on long decks. See executor-base.md §2.1. +**Per-page spec_lock re-read (Mandatory)**: before **each** SVG page, `read_file /spec_lock.md` and use only its colors / fonts / icons / images, plus the per-page `page_rhythm` / `page_layouts` / `page_charts` lookups and the route-specific `pptx_structure` / `pptx_layouts` native-layout contract. The contract is absent only on legacy/free inputs that have not yet been normalized; it is mandatory for every current template page. Resists context-compression drift on long decks. See executor-base.md §2.1. > ⚠️ **Main-agent only**: SVG generation MUST stay in the current main agent — page design depends on full upstream context. Do NOT delegate to sub-agents. > ⚠️ **Generation rhythm**: generate pages sequentially, one at a time, in the same continuous context. Do NOT batch (e.g., 5 per group). **Visual Construction Phase**: generate SVG pages sequentially, one at a time, in one continuous pass → `/svg_output/` +Each completed SVG MUST be a standalone, complete representation of that slide's visible design. Template SVGs and locked planning artifacts may guide construction, but export must not reach back to them to add visible objects omitted from `svg_output/`. Speaker notes, animation, narration, transitions, and direct native-PPTX workflows remain separately owned artifacts/capabilities. + +Every `baseline` / free-design page root MUST declare one canonical `data-pptx-page-role`; `template` / `preserve` pages already declare `data-pptx-layout` and MUST NOT duplicate that identity merely to satisfy this rule. Add `data-pptx-role` only to structural page-frame objects whose package, page-number, or animation behavior is not already expressed by `data-pptx-layer`, `data-pptx-placeholder`, or `data-pptx-native`; such an element needs a stable unique `id`. Do not add generic content roles to ordinary titles, body text, cards, KPIs, diagrams, charts, icons, or images. Full contract: [`references/semantic-svg.md`](references/semantic-svg.md). + **Quality Check Gate (Mandatory)** — after all SVGs, BEFORE annotation handling and speaker notes: ```bash python3 ${SKILL_DIR}/scripts/svg_quality_checker.py @@ -637,6 +660,9 @@ python3 ${SKILL_DIR}/scripts/svg_to_pptx.py # Output (default-flow mode): # exports/_.pptx ← native pptx (canonical output, reads svg_output/) # backup//svg_output/ ← Executor SVG source backup (always written) +# backup//templates/{native_structure.json,source_template.pptx} +# ← preserve mode only +# backup//spec_lock.md ← preserve mode only # # Add --svg-snapshot to additionally emit the SVG-image preview pptx alongside the native pptx: # exports/__svg.pptx ← SVG preview pptx (reads svg_final/) @@ -660,10 +686,100 @@ python3 ${SKILL_DIR}/scripts/svg_to_pptx.py > The `svg_output/` > snapshot in `backup//` is always written so the project can be > re-exported from frozen SVG sources without re-running the LLM. The SVG-rendered -> preview pptx is opt-in via `--svg-snapshot` — live preview already provides the +> preview pptx is opt-in via `--svg-snapshot`. Preserve mode also backs up the +> paired native structure/source files and `spec_lock.md` under the same timestamp. +> Live preview already provides the > SVG visual reference, so it's only needed when you want a self-contained file > to share. Pass `-s output` or `-s final` to force a single source if you need it. +> **PPTX structure mode** — native export first reads +> `spec_lock.md` `pptx_structure.mode`, then falls back to `baseline` when the +> section is absent. An explicit `--pptx-structure` CLI value overrides the +> lock for diagnostics. Baseline keeps the real slide master/layout +> relationship from the base deck and promotes the strict-majority identical +> native slide background into the slide master (every slide must carry an +> explicit background; minority slides keep their own, which overrides the +> master fill). Baseline may also promote a shared **leading** prefix of +> top-level SVG elements whose explicit `data-pptx-role` marks page chrome +> (`logo`, `footer`, `header`, `watermark`, `chrome`, `page-number`). Exact id +> tokens remain a compatibility fallback only when the role marker is absent. +> Promotion requires identical generated OOXML on a strict majority of slides +> sharing the master, no slide-timing reference, and z-order safety; overlay +> chrome stays slide-local. Minority slides (covers, section pages) keep every +> shape slide-local and are bound to a generated `Cover` layout with +> `showMasterSp="0"`, so promoted master chrome never appears on them. Image +> relationships for promoted chrome are copied to the master. After the SVG +> pages are complete, baseline assigns conservative semantic layout families: +> root `data-pptx-page-role` values `cover` / `toc` / `section` / `ending` become +> `Cover` / `Agenda` / `Section` / `Closing`, while `content` becomes `Content`. +> Filename tokens remain a compatibility fallback for marker-free legacy SVGs. +> It also +> preserves an existing safety `Cover` assignment made while hiding promoted +> Master chrome. It does not inspect visual similarity or infer placeholders, +> and all actual page content remains slide-local. When every slide in one +> family carries the exact same explicit background, that background moves to +> the family layout. The same family may also promote an identical leading +> prefix of explicitly named chrome when every family member carries the same +> generated OOXML, relationships are safe, no timing targets it, and z-order is +> preserved. This is +> an export-only package pass: SVG authoring, live preview, and model context are +> unchanged. Baseline also prunes base-template slide layouts no generated slide +> references, so the PowerPoint new-slide picker only offers layouts that belong +> to the deck, and +> converts `data-pptx-role="page-number"` chrome whose text exactly equals the +> slide's display number into an auto-updating PowerPoint slide-number field +> (legacy page-number id tokens remain a fallback; other numbering schemes keep +> their literal text). For projects with a +> `spec_lock.md` typography contract, baseline also installs `title_family` as +> the PowerPoint theme major font and `body_family` / `font_family` as the minor +> font; matching SVG runs use theme tokens so later theme edits can cascade +> without changing initial rendering. Baseline/template export likewise maps +> the locked background, text, primary/accent, and border roles into the +> PowerPoint color scheme. Exact matching SVG/native-object colors use +> context-safe theme tokens; unrelated local colors, inverse white/black, and +> effects stay concrete, so the first rendering remains the SVG design while +> later theme edits can cascade selectively. Add +> `--pptx-structure flat` only for debugging/comparison when all generated +> backgrounds and chrome must remain slide-local and fonts must stay concrete. + +> **Legacy preserved source-template export** — an existing template package +> from a structured PPTX may ship `native_structure.json` plus +> `source_template.pptx`. This pair records a reusable capability; it does not +> select the downstream route by itself. Only a confirmed +> `template_adherence: strict` deck may set `pptx_structure.mode: preserve`. +> Add project-relative +> `source_template` / `native_structure` rows, and map every page in +> `pptx_layouts` to an exact imported layout key/name. Native export verifies the +> source SHA-256 and package parts, creates the generated slides against the +> original layouts (including multiple source masters), removes SVG-only preview +> copies of inherited master/layout visuals, and binds slide content to the +> original placeholder type/index. Preserve mode keeps the source layout picker +> roster; it does not prune unused source layouts. +> +> Current template creation no longer emits this pair. `preserve` is retained +> only so existing template packages and projects remain exportable; do not +> select it for newly created templates. + +> **Explicit reusable template export** — deck/layout template routes set +> `spec_lock.md` `pptx_structure.mode: template`. Every SVG root declares +> `data-pptx-layout`, and direct children +> explicitly declare master/layout layers, an optional Slide background, or +> PowerPoint placeholders. This +> mode builds named reusable layouts and placeholder mappings; it does not +> infer them from visual similarity. Static master/layout elements must be +> repeated consistently in PowerPoint paint order. Chart/table placeholders +> additionally require `--native-objects`. The metadata contract and authoring +> example live in +> [`references/shared-standards.md`](references/shared-standards.md#explicit-pptx-master--layout--placeholder-metadata-template-export). +> Raw PPTX templates still route to `template-fill-pptx`; `template` structure +> mode is for explicitly annotated SVG authoring. Both `strict` and `adaptive` +> template use stay on this deterministic route. Adaptive pages may define new +> layout keys, but they repeat the same Master layer and one internally +> consistent Layout/placeholder contract. +> In template mode, `pptx_layouts` contains exactly one locked +> layout key/name per page; reuse a key for a shared structure instead of +> creating one key per content instance. + > **Paragraph editability vs line fidelity** — by default, mergeable dy-stacked > paragraph blocks collapse into one editable PowerPoint text frame with multiple > ``, improving body-text editing and resize/reflow behavior. Add `--no-merge` @@ -679,7 +795,10 @@ python3 ${SKILL_DIR}/scripts/svg_to_pptx.py > PowerPoint-editable native tables/charts and accepts that those objects may > render differently across PowerPoint / Keynote / LibreOffice / WPS. Without > the flag, marked groups export through their SVG fallback children like -> ordinary SVG content. +> ordinary SVG content. Imported objects that carry +> `data-pptx-native-status` are fallback-only; the quality checker and +> `--native-objects` export surface their reason as warnings rather than silently +> claiming editability. **Optional animation flags** (page transitions are on by default; per-element entrance is off by default — turn it on only when the user asks for it): - `-t ` — page transition. Default `fade`. Options: `fade` / `push` / `wipe` / `split` / `strips` / `cover` / `random` / `none`. @@ -702,7 +821,8 @@ Full effect list, anchor logic, and limits: [`references/animations.md`](referen > ❌ **NEVER** substitute `cp` for `finalize_svg.py` — finalize performs multiple critical processing steps > ❌ **NEVER** force `-s output` for the legacy/preview pptx (PowerPoint's internal SVG parser drops icons and rounded corners). The default auto-split already gives native the high-fidelity source it needs without touching legacy. -> ❌ **NEVER** use `--only` (it suppresses one of the two output files) +> ❌ **NEVER** use `--only` in the standard pipeline. Keep it only for explicit +> one-product diagnostics or compatibility checks. > **Post-export annotation window**: the preview service from Step 6 typically remains running after export. If the user submitted annotations in the browser (during Executor or after export) and now asks to apply them — they may quote the browser prompt (`Changes saved to svg_output...` / `修改已保存到 svg_output...`), say "apply my annotations" / "应用注解" / equivalent — run [`live-preview`](workflows/live-preview.md) Step 2 to apply and re-export. Annotations submitted during generation are also handled here, not earlier. @@ -729,6 +849,7 @@ Before switching roles, **MUST first read** the corresponding reference file. Ou | Resource | Path | |----------|------| | Shared technical constraints | `references/shared-standards.md` | +| Semantic SVG marker contract | `references/semantic-svg.md` | | Canvas format specification | `references/canvas-formats.md` | | Image-text layout patterns (Primary structures + Modifier layers — combine freely) | `references/image-layout-patterns.md` | | Image layout sizing (math for side-by-side container dimensions) | `references/image-layout-spec.md` | diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/artifact-ownership.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/artifact-ownership.md index f3d7e84e..c7d31820 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/artifact-ownership.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/artifact-ownership.md @@ -24,7 +24,7 @@ Global artifact ownership rules for PPT Master projects. | `templates/` | Project template reference | Step 3 imported specs, template SVGs, and non-image assets | Strategist/Executor read only when Step 3 is triggered | | `confirm_ui/recommendations.json` | Confirmation proposal | Strategist-authored confirmation payload | Confirm UI reads; rewritten between Stage 1, Stage 2, and Stage 3 | | `confirm_ui/result.json` | Confirmation result | User-confirmed values | Strategist treats final result as authoritative over recommendations | -| `svg_output/` | Author source | Main-agent handwritten SVG pages | Quality checker and native PPTX export read this as canonical page source | +| `svg_output/` | Page-design author source | Main-agent handwritten SVG pages containing the complete visible design | Quality checker and native PPTX export read this as the canonical visual/page-layout source; templates and locks do not add missing visible objects at export | | `notes/total.md` | Speaker-note source | Complete notes before splitting | Step 6 writes; Step 7.1 splits | | `notes/slide_*.md` | Split notes | Per-slide notes generated from `total.md` | Derived by `total_md_split.py` | | `svg_final/` | Derived preview/export SVGs | Self-contained post-processed SVGs | Rebuild from `svg_output/` with `finalize_svg.py` | @@ -44,6 +44,8 @@ Global artifact ownership rules for PPT Master projects. | Design contract | `design_spec.md` explains; `spec_lock.md` executes. Executor must not infer execution values from prose. | | Image facts | `images/` is live state; `analysis/image_analysis.csv` is a regenerated view, not a durable cache. | | SVG source | `svg_output/` is the only author source for generated pages. | +| Page-design closure | On SVG-authoring routes, every visible exported-slide object exists in the corresponding page SVG or an explicitly referenced visual asset. | +| Package-behavior separation | Speaker notes, animations, transitions, narration, and direct native-PPTX workflows keep their owning artifacts; do not force them into SVG metadata. | | Post-processed SVG | `svg_final/` is disposable and must be rebuildable from `svg_output/`. | | Export source | Native PPTX export reads `svg_output/` by default. SVG snapshot export reads `svg_final/` only when requested. | | Confirmation | Final `confirm_ui/result.json` or chat confirmation overrides recommendations. | diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-base.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-base.md index 022903d1..8e3e3378 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-base.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-base.md @@ -2,6 +2,12 @@ > Narrative skeleton and visual aesthetic come from this deck's locked files under [`modes/`](./modes/_index.md) and [`visual-styles/`](./visual-styles/_index.md). Technical constraints are in shared-standards.md. +**Hard rule — complete page SVG**: Every visible object intended for the exported slide MUST exist in the final page SVG or be explicitly referenced by it. Templates and `spec_lock.md` guide construction; they are not export-time overlays for missing visible content. + +**Hard rule — minimal semantics without semantic loss**: `baseline` / free-design roots declare `data-pptx-page-role`; `template` / `preserve` roots already use `data-pptx-layout` and do not duplicate that identity. Add `data-pptx-role` only to structural page-frame objects whose package, page-number, or animation behavior is not already expressed by layer, placeholder, or native-object metadata; the marked element uses a stable unique `id`. Ordinary page content keeps normal SVG structure without duplicate semantic classification. See [`semantic-svg.md`](./semantic-svg.md). + +> Note: this rule covers page design only. Speaker notes, animations, transitions, narration, and direct native-PPTX workflows retain their separate artifacts and package-level processing. + --- ## 1. Template Adherence Rules @@ -13,6 +19,7 @@ | Source list | Read path | |---|---| | Chosen template's `design_spec.md` (read frontmatter to detect `replication_mode`) | `templates/design_spec.md` | +| Legacy preserve-mode native layout/placeholder contract, when present | `templates/native_structure.json` | | Every distinct `` in `spec_lock.md page_layouts` | `templates/.svg` | | Every distinct chart name in `spec_lock.md page_charts` | `templates/charts/.svg` | | Chart types in `design_spec.md §VII` not covered above | `templates/charts/.svg` | @@ -33,9 +40,9 @@ Resolve the per-page template SVG via `spec_lock.md page_layouts` (authoritative 1. **Mirror-mode template** (template's `design_spec.md` frontmatter has `replication_mode: mirror`) → see §1.1 below. The page is consumed as a **visual reference**, not as a placeholder shell. 2. `spec_lock.md page_layouts` has `P: ` for this page → inherit the structure of `templates/.svg` (already in context from §1.0). -3. `page_layouts` exists but **no entry** for this page → **free design**, no template inheritance. -4. `page_layouts` section absent (legacy deck) **and** `templates/` directory exists → fall back to the page-type table below, matching by SVG filename keyword (cover/chapter/content/ending/toc). Read the matched file at first use if §1.0 batch did not cover it. -5. No template at all → free design. +3. `pptx_structure.mode: template` but this page has no `page_layouts` entry → stop; the template contract is incomplete. Adaptive mode must still select a reference SVG. +4. `page_layouts` section absent in a legacy project and `templates/` exists → fall back to the page-type table below, matching by SVG filename keyword. Read the matched file at first use if §1.0 did not cover it. +5. No deck/layout template at all → free design. > Note: `page_layouts` disambiguates the multiple content variants modern templates ship (e.g., `graduation_defense` has 8); the legacy table cannot. @@ -58,14 +65,14 @@ Resolve the per-page template SVG via `spec_lock.md page_layouts` (authoritative When the project's chosen template is a `mirror` template (`design_spec.md` frontmatter declares `replication_mode: mirror`), Executor switches to a **reference-style** consumption path that bypasses placeholder substitution: 1. **Per-page reference selection** — Strategist selects one mirror page per project page via `spec_lock.md page_layouts` (e.g., `P04: 015_content`). The basename is the mirror filename without extension; Strategist made this choice by reading `design_spec.md §V Page Roster` descriptions, not by guessing. -2. **Copy, don't fill** — open the referenced mirror SVG (already in context from §1.0). **Copy it as the starting point for the project page**, then edit text elements in place to express the project's content for `P`. Preserve every non-text element verbatim: backgrounds, decorative shapes, sprite-cropped images, charts, icon usage, color values, font families, geometry, sprite `` wrappers, and **which image** each `` points at. +2. **Copy, don't fill** — open the referenced mirror SVG (already in context from §1.0). Copy it as the starting point, then edit slide-specific text in place. Preserve every non-text element and every `data-pptx-*` structure attribute verbatim unless adaptive mode intentionally assigns a new Layout contract. 3. **What you may edit** — the visible text content of `` / `` elements that express slide-specific content (title, body, captions, KPI labels, dates, page numbers). Replace the source deck's example text with the project's text for this page from `design_spec.md §IX` and `notes/_*.md`. 4. **What you must not touch** — element positions, sizes, fonts, colors, fills, strokes, gradients, **which image each `` points at**, `` grouping, sprite-sheet `` wrappers, decorative `` / `` / `` / `` shapes, `` markers, embedded chart data structures. Mirror's value is preserving the source deck's visual identity — any geometric / decorative drift defeats the purpose. **The `href` path is not the image**: normalizing a bare `href="cover_bg.png"` to `href="../images/"` (when Step 3 relocated the asset to `images/`) points at the *same* image and changes nothing visual — that is an allowed path fix, not a fidelity edit. Leaving the bare href as-is is also fine; the exporter and live preview resolve bare hrefs against `images/` either way. 5. **Content fit** — the mirror page was chosen by Strategist because its layout matches the content slot. If the project's content for `P` legitimately needs more / fewer items than the mirror page provides (e.g. mirror shows 3 KPI cards, project has 4 metrics), keep the mirror page's visual rhythm and either drop one metric to fit or split across two pages — do **not** restructure the mirror page's grid. If neither works, surface a `warning: P content does not fit mirror reference ; suggest different reference page` and proceed with the closest-fit edit. -6. **No `{{}}` substitution** — mirror SVGs do not contain placeholder markers. Do not search for `{{TITLE}}` / `{{CONTENT_AREA}}` etc.; do not invent placeholders. The whole mirror contract is "verbatim source + in-place text edit". +6. **Visible text editing** — mirror SVGs may keep literal source text rather than `{{...}}` authoring markers. Edit visible text in place, but retain any imported semantic `data-pptx-placeholder` identity. 7. **Output filename** — follow the standard project SVG naming convention (`_.svg` where `` matches the project page index, not the mirror source index). The mirror filename is the *reference*, not the *output*. -**Detecting mirror mode**: read the chosen template's `design_spec.md` frontmatter once during §1.0 batch read. If `replication_mode: mirror`, every page that hits `page_layouts` follows §1.1 above; pages without a `page_layouts` entry still fall through to free design (resolution rule 3 above). +**Detecting mirror mode**: read the chosen template's `design_spec.md` frontmatter once during §1.0 batch read. If `replication_mode: mirror`, every page follows §1.1 through its mandatory `page_layouts` reference. **Mirror + chart pages**: chart structures inside a mirror SVG are already drawn (axis, series, labels). Treat them as visual references — replace the data labels and series text content to match the project's chart spec, but do not redraw the chart from a `templates/charts/.svg` baseline. A mirror template's `page_charts` entries are normally absent for this reason. @@ -84,12 +91,44 @@ When the project's chosen template is a `mirror` template (`design_spec.md` fron Before generating each page, output which template is used: ``` -📝 **Template mapping**: `templates/03a_content_image_text.svg` (or "None (free design)") +📝 **Template mapping**: `templates/03a_content_image_text.svg` (free-design routes may use "None") 🎯 **Adherence rules / layout strategy**: [specific description] ``` - **Content pages**: template defines only header/footer; content area is free -- **No template**: generate entirely per the Design Spec +- **No template**: allowed only on free-design or brand-only routes + +### 1.2 PowerPoint Master / Layout Mapping + +`page_layouts` selects an SVG design reference; `pptx_layouts` declares the native PowerPoint layout produced at export. They are independent contracts. + +`pptx_structure.template_adherence` records the Strategist's confirmed template-use policy. Both `strict` and `adaptive` require one real `page_layouts` reference and one `pptx_layouts` output mapping per page. Strict keeps the referenced Layout contract; adaptive may create a new explicit Layout while retaining the template Master. + +| `pptx_structure.mode` | Executor behavior | +|---|---| +| `baseline` or missing | Free design or brand-only route. Author complete standalone SVG pages with root page-role and only necessary structural hints; export assigns conservative marker-backed Layout families after generation. | +| `template` | Every generated SVG MUST implement the matching `pptx_layouts` row and the explicit structure contract below. | +| `preserve` | Legacy strict-only compatibility. Use the locked source layout key/name and retain marked preview layers for source-package reuse. | + +**Hard rule — every structured page references one layout**: In `template` or `preserve` mode, read `P: | ` from `pptx_layouts`, then put both values on the root SVG as `data-pptx-layout` and `data-pptx-layout-name`. Strict copies the selected template key/name. Adaptive may use the new key/name already locked by Strategist; Executor must not improvise another. + +**Hard rule — PowerPoint paint order**: Direct visual children appear in this order: Master background, Layout background, optional Slide background, shared Master shapes, same-key Layout shapes, then slide-local content/placeholders. Backgrounds are the special plane beneath all inherited shapes. Repeat the same Master contract on every page and the same Layout contract on every page sharing a key. + +**Placeholder ownership**: Keep actual title/subtitle/body/picture/chart/table/object/media/date/footer/slide-number content on the Slide and add the matching `data-pptx-placeholder`. Do not duplicate placeholder identity with `data-pptx-role`; in particular, `data-pptx-placeholder="slide-number"` already owns page-number field behavior. A reconstructed `title` normally omits `data-pptx-placeholder-idx`; preserve an explicit imported title index when present. Every indexed placeholder on one layout uses a unique index. Chart/table placeholders require native markers and a later `--native-objects` export; object/media placeholders must each resolve to one top-level DrawingML object. + +**Adaptive Layout creation**: Start from the selected template SVG, keep every Master element identical, and change only the Layout layer/placeholder contract needed for the new composition. Pages sharing the new key repeat the same static Layout elements and placeholder ids/types in the same order. + +**Legacy preserve identity**: In `preserve` mode only, copy source `data-pptx-placeholder-idx` values and retain the source placeholder type/index pairing. Master/Layout preview elements keep `data-pptx-layer` plus `data-pptx-editable="false"`. + +**Background ownership**: + +| Scope | SVG authoring | +|---|---| +| Deck-wide default | Direct full-canvas solid `` repeated identically on every page | +| Page-type default | Direct full-canvas solid `` repeated on every page sharing that layout key | +| One-page exception | Direct full-canvas solid `` | + +The exporter writes these solid fills as real Master/Layout/Slide `p:bg`, not selectable full-canvas shapes. Gradients, images, textures, and overlay panels stay explicit shapes unless the shared standard says otherwise. --- @@ -146,11 +185,18 @@ Before drawing each page, look up its entry in `page_rhythm` (key format `P` Before drawing each page, look up its entry in `page_layouts` to decide which basename to inherit (the SVG itself was loaded in §1.0): -- Entry present (e.g., `P04: 03a_content_image_text`) → inherit the corresponding SVG already in context. The basename **must match** an actual file in the chosen template directory; if it doesn't, emit `warning: page_layouts P references missing file .svg — falling back to free design` and proceed. -- No entry for this page → free design, no inheritance. **Not an error** — Strategist intentionally left this page free. -- Whole section absent → see §1 fallback (legacy page-type matching). +- Entry present (e.g., `P04: 03a_content_image_text`) → inherit the corresponding SVG already in context. The basename **must match** an actual file in the chosen template directory. If it does not, stop before drawing and report the invalid mapping; neither `strict` nor `adaptive` may fall back to free design inside a template deck. +- No entry for this page with `template_adherence: strict|adaptive` → stop before drawing and report the missing Strategist mapping. Adaptive mode still selects the closest template SVG as its visual/structural reference; flexibility applies to the new output Layout, not to whether a template reference exists. +- Whole section absent with `pptx_structure.mode: template` → stop before drawing; the current template contract is incomplete. +- Whole section absent only in a legacy project without the current template contract → see §1 fallback (legacy page-type matching). -Do **not** invent a layout entry, and do **not** assume a template just because `templates/` exists — if `page_layouts` is present but silent for this page, that silence is the instruction. +Do **not** invent a layout entry, and do **not** assume a template just because `templates/` exists. For either template-adherence value, a missing or invalid `page_layouts` row is an upstream contract error. Free design is a separate deck route, never a per-page fallback inside template mode. + +**Per-page PowerPoint layout lookup — `pptx_layouts` section**: + +- `pptx_structure.mode: template` or `preserve` → a `P` row is mandatory; apply §1.2 to the root and direct children. Preserve mode uses the exact source key/name and placeholder indices. +- `pptx_structure.mode: baseline` or missing → omit explicit PPTX layer/layout metadata, but keep the root page role and only necessary structural hints. Baseline export owns conservative Master/background/chrome promotion plus page-role-backed Layout family assignment; filenames and ids are legacy fallbacks only when the corresponding marker is absent. +- A layout key may repeat across non-adjacent pages. Reuse is based on identical static/placeholder contracts, not page proximity or content wording. **Per-page chart reference — `page_charts` section**: diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/semantic-svg.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/semantic-svg.md new file mode 100644 index 00000000..e37ebb8f --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/semantic-svg.md @@ -0,0 +1,126 @@ +# Minimal Semantic SVG Markers + +PPT Master uses a small set of rendering-neutral compiler hints only where +ordinary SVG cannot reliably express a required PowerPoint packaging decision. +These markers are not a second content model and do not abbreviate SVG. + +## 1. Boundary + +| Marker | Placement | Purpose | +|---|---|---| +| `data-pptx-page-role` | Root `` | Select the baseline PowerPoint Layout family. Required on newly generated baseline/free-design pages; template/preserve pages already use `data-pptx-layout`. | +| `data-pptx-role` | A structural page-frame element | Identify the few objects whose package or animation behavior is not already expressed by specialized metadata. The element also needs a stable unique `id`. | + +The complete geometry, text, styles, grouping, and asset references remain in +ordinary SVG. Removing these markers must not change browser rendering. Do not +copy visible values into metadata, and do not mark ordinary titles, body text, +cards, KPIs, diagrams, charts, icons, or images merely to describe their +content. + +Use the existing specialized contracts for specialized facts: + +- `data-pptx-layout` and `data-pptx-layer` own Master/Layout/Slide structure; +- `data-pptx-placeholder` owns PowerPoint placeholder identity; +- `data-pptx-native` owns native chart/table reconstruction. + +Do not duplicate those facts with `data-pptx-role`. Consumers resolve semantics +in this order: specialized metadata, minimal compiler hints, then legacy +filename/id conventions. + +## 2. Canonical Values + +### Page roles + +| Value | Meaning | Baseline Layout | +|---|---|---| +| `cover` | Opening cover | `Cover` | +| `toc` | Agenda or contents page | `Agenda` | +| `section` | Chapter divider or transition | `Section` | +| `content` | Ordinary information page | `Content` | +| `ending` | Closing, thanks, Q&A, or contact page | `Closing` | + +### Structural roles + +| Value | Compiler behavior | +|---|---| +| `background` | Treat an otherwise unmarked background as static page framing for animation purposes. | +| `decoration` | Treat decorative page framing as static for animation purposes. | +| `header` | Eligible for conservative repeated-chrome promotion; skip automatic entrance animation. | +| `footer` | Eligible for conservative repeated-chrome promotion; skip automatic entrance animation. | +| `logo` | Eligible for conservative repeated-chrome promotion; skip automatic entrance animation. | +| `watermark` | Eligible for conservative repeated-chrome promotion; skip automatic entrance animation. | +| `chrome` | Generic repeated page-frame object eligible for conservative promotion. | +| `page-number` | Identify a free-design page-number object; template `data-pptx-placeholder="slide-number"` already owns this behavior. | + +`background` and `decoration` do not by themselves authorize Master/Layout +promotion. The existing background and exact-shared-structure safety checks +continue to own that decision. + +## 3. Examples + +### Free-design page + +```xml + + + + + + Quarterly growth + + + 7 + +``` + +### Reusable template page + +```xml + + + + + + + + + {{PAGE_TITLE}} + + + ACME + + + 7 + +``` + +## 4. Validation and Compatibility + +The quality checker validates marker placement, canonical values, and stable +unique IDs. Baseline export consumes explicit markers before compatibility +heuristics: + +- root page role is preferred over filename-based Layout classification; +- `data-pptx-placeholder="slide-number"` is preferred over a generic role or id; +- explicit structural role is preferred over id-token chrome detection; +- animation target scanning uses the structural role before id-token fallback. + +Filename and id heuristics remain compatibility fallbacks only for older SVGs +that lack the corresponding marker. A canonical page role is authoritative over +the filename. Any explicit structural role prevents id-based reinterpretation; +an unknown role remains renderable but produces a quality-check warning. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/shared-standards.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/shared-standards.md index 4c11c203..1decf297 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/shared-standards.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/shared-standards.md @@ -169,6 +169,29 @@ One offending character invalidates the file and aborts export. Numeric refs (`& ## 4. Basic SVG Rules +### 4.0 Complete Page-Design Contract + +| Concern | Requirement | +|---|---| +| Visible slide result | The completed `svg_output/.svg` MUST contain every visible text, image, shape, diagram, chart/table fallback, background, and template-derived layout element intended for that slide. External visual assets are valid when the SVG references them explicitly. | +| Template/control inputs | Templates, `design_spec.md`, and `spec_lock.md` guide authoring. Do not depend on them to add visible elements after the page SVG is complete. | +| PPTX translation | The exporter may map represented SVG content to DrawingML/native objects and deduplicate represented elements into Master/Layout/Slide parts. It MUST NOT invent visible slide content absent from the SVG. | +| Excluded package behavior | Speaker notes, animations, transitions, narration audio, PPTX relationships, and direct native-PPTX workflows remain separately owned. They are not part of the SVG page-design contract. | + +**Hard rule — page-design closure**: A final page SVG is the sole visual/design authority for that page on every SVG-authoring route. SVG is not the authority for the entire PPTX package. + +### 4.1 Semantic SVG Marker Contract + +Semantic markers are minimal compiler hints orthogonal to native SVG semantics. +Existing `data-pptx-layout` / layer / placeholder / native-object metadata is +authoritative and read first. A `baseline` / free-design root declares +`data-pptx-page-role`; template/preserve roots already declare their Layout. +Add `data-pptx-role` only when no specialized marker expresses the required +page-frame behavior; the element also uses a stable unique `id`. Do not classify +ordinary page content or move visible facts out of SVG attributes/text into +metadata. See +[`semantic-svg.md`](semantic-svg.md) for the canonical vocabulary and examples. + - **viewBox** MUST match the canvas dimensions; it is the single source of truth for canvas size. Root `width`/`height` are optional compatibility attributes and are not used as PPT Master canvas authority. - **Background**: Use `` to define the page background color - **``** has two purposes: (1) manual line breaks (use `dy` or explicit `y`); (2) inline run formatting on the same line (color/weight/size). `` is FORBIDDEN. See "Single logical line" rule below. @@ -253,7 +276,9 @@ Wrap logically related elements in top-level `` groups. Produces Pow **Animation-ready rule**: direct children of `` should be semantic groups, not raw drawing atoms. Aim for **3–8 top-level content `` groups per slide** (the 3–8 budget excludes page chrome — see below); each content group becomes one entrance step under the chosen `--animation-trigger` mode (one click in `on-click`, one cascade slot in `after-previous`, parallel in `with-previous`). -**Chrome groups are excluded automatically.** The exporter treats top-level groups whose id contains chrome tokens as page chrome and skips them in the animation sequence — they appear together with the slide. Tokens (matched against id after splitting on `-` / `_`): `background`, `bg`, `decoration` / `decorations` / `decor`, `header`, `footer`, `chrome`, `watermark`, `pagenumber` / `pagenum` / `page-number`, `nav`, `logo`, `rule`. So ``, ``, ``, ``, ``, ``, `` all skip animation while keeping their `` wrapper for editing/grouping. Use these naming conventions for chrome — do **not** strip the `` wrapper. +**Chrome groups are excluded automatically.** Existing `data-pptx-layer` and `data-pptx-placeholder="slide-number"` semantics are read first. Otherwise, explicit `data-pptx-role` values `background`, `decoration`, `header`, `footer`, `chrome`, `watermark`, `page-number`, and `logo` identify static page framing. For marker-free legacy SVGs, ids remain a compatibility fallback using these tokens after splitting on `-` / `_`: `background`, `bg`, `decoration` / `decorations` / `decor`, `header`, `footer`, `chrome`, `watermark`, `pagenumber` / `pagenum` / `page-number`, `nav`, `logo`, `rule`. Keep the `` wrapper for editing/grouping. + +Minimal structural roles also drive slide-master promotion in the default native export: `logo` / `footer` / `header` / `watermark` / `chrome` elements drawn **before** content and repeated **verbatim** across pages may move into the slide master (pages without them, like covers, are isolated automatically). A `data-pptx-placeholder="slide-number"` text, or a free-design `data-pptx-role="page-number"` text, whose content exactly equals the page's display number becomes a self-renumbering PowerPoint field. Exact id tokens remain a fallback only when specialized/minimal markers are absent. **What to group**: @@ -337,7 +362,7 @@ Full reference: [`animations.md`](animations.md). **Prohibited**: - NEVER use `cp` as a substitute for `finalize_svg.py` - NEVER force `-s output` for the legacy/preview pptx (PowerPoint's internal SVG parser drops icons and rounded corners). Default auto-split already gives native the high-fidelity source it needs without affecting legacy. -- NEVER use `--only` (it suppresses one of the two output files) +- NEVER use `--only` in the standard pipeline; keep it for explicit one-product diagnostics or compatibility checks > Source-directory split: by default `svg_to_pptx.py` reads `svg_output/` for the native pptx (preserves icon ``, image `preserveAspectRatio` as native picture-crop metadata, rounded rect `rx/ry` → `prstGeom roundRect`) and `svg_final/` for the legacy/preview pptx (PowerPoint's internal SVG parser needs the flattened form). Pass `-s output` or `-s final` only when you specifically want both products to read from a single source. @@ -690,7 +715,7 @@ The child ``'s `stroke` becomes the foreground color (the pattern's line c > `svg_quality_checker.py` warns on missing `data-pptx-pattern` and errors on values outside the enum. Catch these pre-export — PowerPoint's repair dialog hides which pattern broke. -### Native PPTX Table / Chart Markers (Experimental) +### Native PPTX Table / Chart Markers (Opt-in) Native PowerPoint tables and Excel-backed charts activate at export time only. The default chart/table route remains hand-authored SVG geometry so the deck stays pixel-stable across PowerPoint / Keynote / LibreOffice / WPS. @@ -709,7 +734,12 @@ Native PowerPoint tables and Excel-backed charts activate at export time only. T `data-pptx-x` / `data-pptx-y` / `data-pptx-width` / `data-pptx-height` on the marker group. If any bound is omitted, the exporter infers the object frame from the visible fallback geometry; this keeps SVG fallback and native object -placement aligned. +placement aligned. Complete explicit bounds are absolute slide coordinates; +marker/ancestor `translate` and `scale` transforms apply only when at least one +bound is inferred. `x`, `y`, `width`, and `height` must be finite and resolve +inside PowerPoint's 32-bit DrawingML coordinate range; `width` and `height` +must resolve to at least one EMU. Native table frames must additionally resolve +to at least one EMU per resolved row and column. **Validation**: `svg_quality_checker.py` validates native marker kind, JSON metadata, bounds/fallback availability, table rows/columns, supported chart @@ -735,8 +765,13 @@ type, and chart data shape before export. **Table schema**: Native tables are rectangular DrawingML grids. Use `columns` for the optional header row and `rows` for body rows; shorter rows are padded -with blank cells unless `strict_grid: true` is set. Use `column_widths` and -`row_heights` as relative weights. Cell objects accept `text`, `fill`, `color`, +with blank cells unless `strict_grid: true` is set. Tables may contain at most +1000 resolved rows and 1000 resolved columns. Use `column_widths` and +`row_heights` as relative weights. Weight lists must match the resolved grid, +contain finite non-negative numbers, and include at least one positive value. +If present, `header_rows` must be an integer from `0` through the resolved row +count. Write `strict_grid`, `style.band_row`, and cell `bold` as JSON booleans. +Cell objects accept `text`, `fill`, `color`, `align`, `valign`, `bold`, `font_size`, `padding`, `border_color`, and `border_width`; the same `padding`, `border_color`, and `border_width` keys may also live under `style` as table defaults. Native table typography mirrors the @@ -913,6 +948,234 @@ steal plot area in PPT. Add `show_legend: true` only when the legend is needed; **Forbidden — native marker transforms**: Do not rotate, skew, or matrix-transform native table/chart marker groups. Translate / scale is accepted; complex transforms fail export because PowerPoint native table/chart frames do not preserve arbitrary SVG transforms. +### Baseline Layout Family Extraction + +Native `baseline` export assigns layout families after every SVG page has been +converted. This package-only pass does not change SVG authoring or live preview. + +| Root `data-pptx-page-role` | Output layout | +|---|---| +| `cover` | `Cover` | +| `toc` | `Agenda` | +| `section` | `Section` | +| `ending` | `Closing` | +| `content` | `Content` | + +Marker-free legacy SVGs retain conservative filename-token fallback: explicit +cover / agenda / section / closing tokens select those families, and every +other page becomes `Content`. When a valid root marker exists, it is +authoritative even if the filename suggests another family. + +Keep an existing `Cover` assignment when the Master chrome safety pass already +used it to hide promoted Master shapes from a minority page. + +**Hard rule — no visual inference**: Keep every actual title, body, picture, +chart, table, and page-specific shape on the Slide. Baseline layouts do not +infer placeholders or promote visually similar content. + +**Background rule**: Move a Slide `p:bg` to its family Layout only when every +slide in that family carries exactly the same explicit background. Otherwise, +keep each background on its Slide. Preserve whether each family shows or hides +the parent Master shape tree. + +**Layout chrome rule**: After family assignment, move only the identical +leading prefix of explicitly marked chrome (`logo`, `footer`, `header`, +`watermark`, `chrome`) carried by every family member. Legacy id tokens are +consulted only when `data-pptx-role` is absent. Generated OOXML and +image relationships must match exactly, no animation may target the shapes, +and moving them behind Slide content must preserve z-order. Keep page numbers +and every non-identical object Slide-local. + +### Theme Font Inheritance (Baseline and Template Export) + +New projects derive PowerPoint theme fonts from `spec_lock.md` typography when +native export uses `baseline` or `template` structure mode: + +- `title_family` (falling back to `font_family`) becomes the theme major font. +- `body_family` (falling back to `font_family`) becomes the theme minor font. +- SVG text whose resolved exported face matches either locked family emits + DrawingML `+mj-*` / `+mn-*` tokens instead of a fixed typeface. +- When major and minor resolve to the same face, ordinary slide-local text uses + the minor role; semantic `title` placeholders are forced to major, while all + other semantic placeholders are forced to minor. +- Local emphasis, code, brand, or other families that do not match the locked + title/body faces remain concrete per-run fonts and do not change with the + theme. + +This substitution is package-only: the SVG stays the live-preview truth, and +the installed theme resolves the tokens to the same concrete fonts on initial +open. Changing the PowerPoint theme later can therefore update matching text +without changing the SVG geometry or the first export's visual design. + +`preserve` mode never rewrites source theme fonts, and `flat` remains the +diagnostic slide-local/fixed-font comparison path. A project without a usable +`spec_lock.md` typography section keeps the legacy concrete-font behavior. + +### Theme Color Inheritance (Baseline and Template Export) + +Native `baseline` and `template` export also derive the PowerPoint color scheme +from `spec_lock.md` colors. The canonical mapping is: + +| Lock role | PowerPoint scheme slot | +|---|---| +| `bg` / `background` / `master_bg` | `lt1` | +| `secondary_bg` / `bg_secondary` | `lt2` | +| `text` / `body_text` | `dk1` | +| `text_secondary` | `dk2` | +| `primary` | `accent1` | +| `accent` | `accent2` | +| `secondary_accent` | `accent3` | +| `border` | `accent4` | +| First two additional non-black/non-white roles | `accent5` / `accent6` | + +The converter promotes an exact locked HEX to `a:schemeClr` only when its +usage is compatible with the role: backgrounds prefer background roles, text +prefers text/accent roles, strokes prefer `border`, and chart series use only +the primary/accent family. This prevents the same literal HEX from coupling +unrelated semantics such as a white page background and fixed white inverse +text. Gradients, patterns, bullets, native tables, and exact native-chart +series colors follow the same rule. Shadows, effects, unmatched local colors, +and additional palette roles remain concrete `a:srgbClr` values. + +The initial rendering is unchanged because every scheme slot resolves to the +same locked HEX. A later PowerPoint theme edit can update only the promoted +roles; it does not rewrite the SVG or local exceptions. `preserve` never +rewrites the imported source color scheme, `flat` keeps concrete colors for +diagnostics, and projects without a usable colors section retain legacy fixed +colors. + +### Explicit PPTX Master / Layout / Placeholder Metadata (Template Export) + +**Trigger**: Deck/layout template routes set `spec_lock.md` +`pptx_structure.mode` to `template`; direct diagnostics may pass +`--pptx-structure template`. Both strict and adaptive template adherence use +this mode. Without either trigger, metadata stays visually dormant. + +**Project lock**: In the standard project pipeline, template mode requires one +`pptx_layouts` row per page using +`P: | `. The SVG root values MUST +match that row. Strict uses the selected template key/name. Adaptive may create +a new key/name while repeating the same Master contract. Reuse one layout key +only when pages share the same static Layout layer and placeholder contract; +different content is not a reason to create a new layout. Direct diagnostic +exports may pass the CLI flag without a spec lock. + +| Metadata | Placement | Behavior | +|---|---|---| +| `data-pptx-layout="content"` | root `` | Binds the slide to one generated reusable layout key | +| `data-pptx-layout-name="Title and Content"` | root `` | Sets the PowerPoint layout-picker name; defaults from the layout key | +| `data-pptx-layer="master"` | direct visual child | Moves one repeated static object/background into the slide master | +| `data-pptx-layer="layout"` | direct visual child | Moves one repeated static object/background into the selected layout | +| `data-pptx-layer="slide"` | direct full-canvas solid `` only | Writes a one-page override as Slide `p:bg` | +| `data-pptx-placeholder="..."` | direct visual child | Keeps actual content on the slide and maps it to a generated layout placeholder | +| `data-pptx-placeholder-bounds="x y width height"` | placeholder element | Overrides the reusable placeholder frame in SVG user units | +| `data-pptx-placeholder-idx="1"` | placeholder element | Retains an imported source layout placeholder index; optional for reconstructed layouts | +| `data-pptx-editable="false"` | master/layout element or slide background | Declares intentional editing outside ordinary slide content | + +**Hard rule — explicit only**: Template export never promotes visually similar +content by inference. Every SVG requires a root `data-pptx-layout`; every +master/layout/placeholder element requires a unique `id` and must be a direct +child of the root SVG. + +**Layer order**: Author the SVG in PowerPoint paint order: Master background, +Layout background, optional Slide background, Master shapes, Layout shapes, +then slide-local content/placeholders. Backgrounds are a special inheritance +plane beneath every shape; this order keeps standalone SVG preview and +PowerPoint rendering aligned. The exporter rejects interleaved layers. + +**Solid background ownership**: A direct full-canvas solid `` becomes a +real `p:bg`, not a selectable shape. Mark it `data-pptx-layer="master"` for the +deck-wide default, `data-pptx-layer="layout"` for a page-type override, or +`data-pptx-layer="slide"` for a one-slide override. An unmarked direct +full-canvas solid rect in the background plane is also treated as Slide scope. A +Layout background overrides the Master background; a Slide background +overrides both. Use the Master for a globally stable color and the Layout for +cover/section/content variants under the same design language. Gradients, +images, textures, transformed rects, and visible-stroke rects are not promoted +by this solid-background rule. + +| Placeholder value | SVG element | PowerPoint placeholder | +|---|---|---| +| `title`, `subtitle`, `body` | direct `` | `title`, `subTitle`, `body` | +| `date`, `footer`, `slide-number` | direct `` | `dt`, `ftr`, `sldNum` | +| `picture` | direct `` or imported crop `` | `pic` | +| `chart`, `table` | direct matching `data-pptx-native` marker group | `chart`, `tbl` | +| `object` | one direct text, image, or basic SVG shape | `obj` | +| `media` | direct `` or imported crop `` | `media` | + +`title` is normally type-matched without an index in reconstructed layouts; if +an imported source title explicitly has one, preserve that exact index. Every +indexed placeholder on one layout uses a unique non-negative index. Template +export writes the semantic type on both the Layout and Slide placeholder +(except `obj`, whose OOXML default is already +`obj`) so PowerPoint and `python-pptx` retain the same identity. A `date` +placeholder also enables the layout date flag and gets a +`datetimeFigureOut` field in the reusable Layout definition; the current +Slide keeps its authored date content. + +**Placeholder prototype**: The first slide using a layout key supplies that +layout's placeholder formatting. `data-pptx-placeholder-bounds` supplies the +reusable frame; when omitted, the exporter uses the prototype object's native +DrawingML bounds. Repeat the same placeholder ids/types on every slide using +that layout. Actual slide content and local geometry may differ. + +**Static structure consistency**: Repeat the same master element ids on every +slide and the same layout element ids on every slide sharing a layout. Their +generated OOXML must be identical within the affected master/layout group. +Static structure may carry shapes, text, or images; non-image/external +relationships are rejected. A full-canvas first rect/group may be marked as a +master or layout background. + +**Native object placeholders**: `chart` / `table` placeholders require +`--native-objects`; fallback groups contain several shapes and cannot map to one +PowerPoint placeholder. `object` is the generic PowerPoint content slot and +must still resolve to one top-level DrawingML object. `media` currently binds +an authored image/crop to a native `media` placeholder; it does not synthesize +video or audio media from a decorative SVG group. + +### Legacy Preserved Source Master / Layout Contract + +**Trigger**: An existing project already ships `native_structure.json` and `source_template.pptx`, has strict template adherence, and sets `pptx_structure.mode: preserve`. Current `create-template` output does not emit this pair; retain this contract only for backward compatibility. + +| Artifact | Authority | +|---|---| +| `source_template.pptx` | Original master/layout/theme/package parts | +| `native_structure.json` | Stable layout keys, picker names, parent masters, placeholder types/indices, source SHA-256 | +| `pptx_layouts` | Per-generated-page source layout selection | +| SVG metadata | Standalone preview layers and slide-content placeholder binding | + +**Hard rule — source package wins**: Mark source master/layout visuals as direct `data-pptx-layer="master|layout"` preview children. Preserve export removes those generated copies and renders the original source parts. Unmarked content stays slide-local. + +**Placeholder identity**: Keep actual content on the slide. Copy the source placeholder index into `data-pptx-placeholder-idx` when present; the exporter restores the source placeholder type/idx pair. Imported `subTitle`, `obj`, `media`, and `dt` placeholders retain distinct `subtitle`, `object`, `media`, and `date` semantic roles instead of collapsing into body/other. Multiple placeholders with the same semantic role require explicit indices. + +**Multi-master boundary**: Preserve every source master already present in the package. Do not synthesize a new master merely for cover/section differences; rebuilt templates continue to prefer one master plus semantic layouts. + +```xml + + + + + + + Actual page title + + +``` + ### transform: rotate — Element Rotation Rotation converts to native PPTX ``. Supported on all element types: `rect`, `circle`, `ellipse`, `line`, `path`, `polygon`, `polyline`, `image`, and `text`. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist.md index 798b2fa1..a22f8f81 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist.md @@ -28,7 +28,7 @@ As a top-tier AI presentation strategist, receive source documents, perform cont | Stage | Items | Role | |---|---|---| -| **1 — direction anchors** | `a` canvas · `c` key info — audience + `content_divergence` + **delivery purpose** *(PPT only)* (promoted out of `g`) · `d` mode + visual_style | confirmed first | +| **1 — direction anchors** | `a` canvas · `c` key info — audience + `content_divergence` + **delivery purpose** *(PPT only)* (promoted out of `g`) · `d` mode + visual_style · template adherence *(only when Step 3 loaded a deck/layout template)* | confirmed first | | **2 — design system** (re-derived from the user's *actual* Stage 1) | `b` page count · `e` color · `f` icon · `g` typography (font + size, formula policy) | derived from Stage 1 | | **3 — images / execution** (re-derived from the user's *actual* Stage 1 + Stage 2) | `h` image source + generated-image style · generation mode · refine-spec toggle | derived from confirmed direction + design system | @@ -100,6 +100,19 @@ Write the locked value to `spec_lock.md` `- visual_style:` and the rationale to > **Template vs preset**: a style mention may sound like a template name ("academic style" vs the `academic_defense/` template directory). Step 3 only triggers on an explicit template directory path supplied by the user — bare names and style words never copy templates; they map to a visual-style preset here. If a template was triggered upstream, its files are already in `/templates/` and its fused design_spec governs. +**Legacy native contract**: When `/templates/native_structure.json` exists, treat it as backward-compatible `preserve` input only. Current templates declare their PowerPoint structure directly in the SVG roster. + +**Template adherence confirmation**: Surface this closed Stage-1 choice only when Step 3 loaded a `kind: deck` or `kind: layout` template. Omit it for free design and brand-only templates. + +| Value | Planning and export behavior | +|---|---| +| `strict` | Map every page to one template SVG. Keep its explicit Master/Layout/placeholder contract and output layout key unchanged. | +| `adaptive` | Map every page to the closest template SVG. Keep the template Master contract, but allow a new explicit Layout key when the selected composition must adapt to the content. | + +**Default — adaptive (may override when the user requests exact template use)**: Recommend `adaptive` when the user supplied only a template path with no stricter instruction. Preselect `strict` when the user explicitly asks to follow or preserve the template closely; the Stage-1 confirmation still records the final choice. Record the confirmed value in `design_spec.md §I` and as `template_adherence` under `spec_lock.md pptx_structure`. + +> Note: `content_divergence` controls how source material is reorganized; `template_adherence` controls how template visuals/layouts are used. Never infer one from the other. + **Downstream effect**: e / f / g / h realize the locked mode + visual style. Example: `showcase` + `dark-tech` → e applies one luminous accent on a dark field; g pairs a clean sans with mono; f minimal glow icons; h the `digital-dashboard` rendering. ### e. Color Scheme Recommendation @@ -839,9 +852,11 @@ This is what makes the axis meaningful: a `presentation` deck and a `text` deck - **Cover impact is mandatory**: Page `P01` is the deck's first visual contract, not a generic title slide. In `design_spec.md §IX`, add a `Cover impact` line for `P01` that names one concrete hook and one concrete composition strategy. Use the source's strongest available signal: a provocative core claim, object / scene metaphor, hero number, founder / product / audience moment, or a distilled conflict. Pair it with one concrete composition strategy — such as `full-bleed image + floating title`, `typographic poster`, `hero object`, `data hook`, `editorial scene`, `high-contrast abstract geometry`, or a fresh composition the deck's subject suggests (these are starting points, not the allowed set). If no external or AI image is available, still specify a native-SVG visual hook; do not fall back to "title + subtitle + decorative background". (Beautify / template-fill keep the source cover verbatim — this rule does not apply on those preservation paths.) - **Cover rhythm lock**: `P01` remains `anchor` in `spec_lock.md page_rhythm`, but its §IX `Cover impact` must prevent content-page patterns. Do not plan multi-card grids, agenda-like bullets, or equal-weight columns on the cover unless a template explicitly requires that structure, or a preservation path (beautify / template-fill) is transcribing the source cover verbatim. - **Closing impact (only when the deck closes)**: the deck's last page is its final visual contract — the strongest impression after the cover. When the deck genuinely lands on a conclusion / call-to-action / final-takeaway page, give it a `Closing impact` line in §IX: name the one thing the audience should leave with (a distilled takeaway, a forward call, a memorable restatement of the core claim) + one composition that delivers it — never a generic "Thank you" / contact-only slide or a centered-title reprise of the cover. **Do NOT invent a closing page to satisfy this** — the filler-page ban above still holds; apply it only to the page where the deck actually resolves. Same exemptions as the cover: skip on template / beautify / template-fill preservation paths. - - **page_layouts (write only when a template is in use)**: For each page that inherits a template SVG, add `P: ` (e.g., `P04: 03a_content_image_text`). Pages designed freely get **no entry** — Executor reads the absence as "free design, no inheritance". If zero pages use a template, omit the section entirely. + - **pptx_structure is mandatory**: Write `mode: baseline` for free design and brand-only routes. When a deck/layout template is confirmed, write `mode: template` plus `template_adherence: strict|adaptive`. Both adherence values stay on explicit template export. Use `mode: preserve` only when reopening a legacy strict project that already carries a validated `native_structure.json` + `source_template.pptx` pair. Do not choose `flat`; it is a CLI-only diagnostic override. + - **pptx_layouts (template/preserve structure)**: In template mode, write one `P: | ` row for every page. Strict uses the selected template SVG's exact key/name. Adaptive may introduce a new stable key/name, but pages reuse a key only when their static Layout layer and placeholder contract match. In legacy preserve mode, use the exact source key/name. Omit the whole section in baseline mode. + - **page_layouts (deck/layout templates only)**: Add one `P: ` row for every page. Strict inherits that SVG contract unchanged. Adaptive uses it as the architecture reference and may evolve the Layout layer under a new `pptx_layouts` key; it must not drop into free design. Omit the section only for free design and brand-only routes. - **page_charts (write only for chart pages that match a catalog template)**: For each page in `design_spec.md §VII` whose `reference template path` points to `templates/charts/.svg`, add `P: `. Pages with `no-template-match` in §VII MUST NOT appear here (Executor would look for a non-existent reference). If the deck has no data-visualization pages, omit the section. - - **Hard rule**: Use both `page_layouts` and `page_charts` for the same page only when the layout template is a compatible shell for the chart. Do not pair chart pages with conflicting page layouts (e.g., `waterfall_chart` + timeline layout, KPI cards + circle-diagram layout). If no compatible layout exists, omit the page from `page_layouts`. + - **Hard rule**: Use both `page_layouts` and `page_charts` only with a compatible template shell. For a chart page without an exact roster match, adaptive mode selects the closest neutral content template and assigns a new explicit Layout key; strict mode must select an existing compatible Layout or revise the outline. Never omit `page_layouts` inside a deck/layout template route. --- diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/template-designer.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/template-designer.md index 5175b899..8a537575 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/template-designer.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/template-designer.md @@ -18,6 +18,8 @@ When the workflow provides a PPTX reference source, the effective input package - finalized template brief - `manifest.json` — single source of truth (slide size, theme, per-master themes, assets, asset map, placeholders, layouts, masters, slides, SVG file paths, page-type candidates) +- `native_structure.json` — stable source master/layout keys, picker names, parent-master relationships, placeholder type/index/geometry, source hash, and source-graph quality facts +- `source_template.pptx` — byte-preserved analysis copy for visual/package cross-checking; never a final template asset - `summary.md` — short orientation digest derived from manifest.json - exported `assets/` - `svg/master_*.svg` / `svg/layout_*.svg` — every master / layout in the deck rendered once as standalone SVG, including ones no sample slide references (template packages often ship more design surfaces than the embedded samples exercise) @@ -35,12 +37,17 @@ PPTX import interpretation: Input priority for PPTX-backed template creation: 1. `manifest.json` for all factual metadata (theme, assets, unique layout/master structure, slide reuse, page-type guidance) -2. `svg/master_*.svg` + `svg/layout_*.svg` — the **primary source for the deck's shared visual language**: backgrounds, page chrome, decorative bars, recurring brand motifs. These are what the new template's fixed structure should adopt or reinterpret. Read these before any slide SVG. -3. `svg/inheritance.json` for confirming which slide uses which layout / master -4. exported `assets/` for reusable visual resources -5. `svg/slide_NN.svg` — each slide's unique content, useful for judging composition rhythm and content density (not for fixed structure) -6. `summary.md` only as a fast scan; never as the canonical fact source -7. screenshots / original PPTX only for style verification +2. `native_structure.json` for source PowerPoint identities, placeholder indices, relationship completeness, and reconstruction facts +3. `svg/master_*.svg` + `svg/layout_*.svg` — the **primary source for the deck's shared visual language**: backgrounds, page chrome, decorative bars, recurring brand motifs. These are what the new template's fixed structure should adopt or reinterpret. Read these before any slide SVG. +4. `svg/inheritance.json` for confirming which slide uses which layout / master +5. exported `assets/` for reusable visual resources +6. `svg/slide_NN.svg` — each slide's unique content, useful for judging composition rhythm and content density (not for fixed structure) +7. `summary.md` only as a fast scan; never as the canonical fact source +8. screenshots / original PPTX only for style verification + +**Native structure output**: Always set `native_structure_mode: template`. Do not ship `native_structure.json` or `source_template.pptx`. Reconstruct one clean Master plus semantic Layouts through explicit SVG structure metadata. Every page remains a complete standalone SVG preview. + +**Downstream boundary**: The Strategist confirmation stage selects `strict` or `adaptive` when a project consumes this package. Both export through explicit template mode. Strict keeps the referenced Layout contract; adaptive may create a new Layout while retaining the template Master. Template_Designer does not preselect that project-level choice. --- @@ -52,7 +59,7 @@ The output page set is determined by **replication mode**, declared in the final |------|-------------|--------| | `standard` (default) | Most templates — clean, reusable, balanced coverage | `01_cover`, `02_chapter`, `03_content`, `04_ending`, optional `02_toc` | | `fidelity` | User explicitly wants strict replication of a source PPTX, but still wants the AI to clean / cluster / cap variants | Standard roster + one variant per distinct layout cluster found in `manifest.json` | -| `mirror` | Creation-time harvesting mode: every source slide is preserved 1:1 as a reference page for the template package. Verbatim copy, no abstraction, no placeholders | One SVG per source slide, named `_.svg` by source order | +| `mirror` | Creation-time harvesting mode: every source slide keeps literal visual geometry while gaining explicit Master/Layout metadata | One SVG per source slide, named `_.svg` by source order | ### Standard mode @@ -93,13 +100,14 @@ Extension page types beyond the canonical four (transition / appendix / disclaim ### Mirror mode -When the brief sets `Replication mode: mirror`, the role does **not** abstract or reconstruct. Each source slide becomes one template page **byte-for-byte**: +When the brief sets `Replication mode: mirror`, preserve literal page appearance while reconstructing layer ownership: -- Source: `/svg-flat/slide_NN.svg` (the self-contained "what PowerPoint shows" view). Do **not** read or use `svg/master_*.svg`, `svg/layout_*.svg`, or `svg/inheritance.json` — chrome / content separation is irrelevant because mirror does not insert placeholders. +- Visual source: `/svg-flat/slide_NN.svg` (the self-contained "what PowerPoint shows" view). Structural source: `svg/master_*.svg`, `svg/layout_*.svg`, and `svg/inheritance.json`. - Output: `templates///_.svg`, where `` is the zero-padded source slide index (3 digits) and `` is derived from `manifest.json` `pageTypeCandidates` — `cover` / `toc` / `chapter` / `content` / `ending`. When the page-type heuristic is ambiguous, fall back to `content`. Preserve source slide order via the numeric prefix. -- Modifications allowed: **only** rewriting `` paths to point at the local `assets/` copy, and renaming asset files to semantic names. Everything else — geometry, decoration, sprite-sheet wrappers, original example text, chart placeholders, embedded fonts — is copied as-is. -- Modifications forbidden: inserting `{{TITLE}}` / `{{CONTENT_AREA}}` / any other placeholder; "cleaning up" decorative complexity; merging similar slides; dropping master/layout chrome (it's already baked into the flat SVG, leave it). -- `design_spec.md` §V Page Roster lists every emitted file with **a one-line description** of what the page contains and what content slot it suits — Strategist selects pages from these descriptions, since the SVG itself carries no placeholder contract. +- Required metadata rewrite: declare the output Layout on the root, mark inherited Master/Layout visuals as direct preview layers, and map source content slots to semantic `data-pptx-placeholder` markers where the imported contract exposes them. Add `data-pptx-role` only to structural page-frame objects whose behavior is not already expressed by those specialized markers. +- Other allowed modifications: rewrite `` paths to local assets and rename assets semantically. Keep geometry, decoration, sprite-sheet wrappers, original example text, chart previews, and fonts visually unchanged. +- Forbidden: simplifying decorative complexity, merging similar slides, or dropping inherited preview chrome. +- `design_spec.md` §V Page Roster lists every emitted file with a one-line content-fit description; SVG metadata owns native reconstruction. **Mirror consumption boundary**: `mirror` applies only while creating the template package from the source deck. Once created, the package is consumed as an ordinary deck / layout template roster: downstream generation may select, repeat, skip, reorder, or adapt pages according to the new content. The `replication_mode: mirror` field must not force the generated deck to preserve the source page count, source order, or one-output-slide-per-template-slide mapping. @@ -138,8 +146,10 @@ source_canvas_width: 1280 source_canvas_height: 720 source_viewbox: "0 0 1280 720" replication_mode: standard | fidelity | mirror +# Required for every deck/layout template. Source packages remain analysis-only. +native_structure_mode: template # Optional — only when this template overrides canonical placeholder vocabulary. -# Omit entirely for `mirror` mode (mirror has no placeholders). +# Omit only when the page truly exposes no replaceable content slots. # placeholders: # 01_cover: ["{{TITLE}}", "{{SUBTITLE}}", "{{BRAND_LOGO}}"] # 03_content: ["{{KEY_MESSAGE}}", "{{CONTENT_AREA}}"] @@ -166,7 +176,7 @@ replication_mode: standard | fidelity | mirror - Optional XML snippet for any reusable component unique to this template ## V. Page Roster -One row per emitted SVG describing what this template's version of cover / chapter / content / ending looks like (background treatment, decorative anchors, layout rhythm, image behavior, content density, intended content slot). For `fidelity` mode, also note the cluster source and visual differentiator. For `mirror` mode the roster is the **load-bearing artifact** — Strategist picks pages from these descriptions, so each row must include enough detail to distinguish it from siblings (column count, hero element, content density, what kind of content slot it fits — "three-column KPI grid with large numbers, suits quarterly summary"). Roster entries must match the actual SVG files on disk. +One row per emitted SVG describing what this template's version of cover / chapter / content / ending looks like (background treatment, decorative anchors, layout rhythm, image behavior, content density, intended content slot). Record the rebuilt Layout key and PowerPoint picker name. For `fidelity` mode, note the cluster source and visual differentiator. For `mirror` mode the roster is the load-bearing content-fit index, so each row must distinguish siblings by column count, hero element, density, and suitable content. Roster entries must match the actual SVG files on disk. ## VI. Assets (omit when none) Logos, cover backgrounds, brand textures bundled with the template package — file name, dimensions, intended usage. @@ -229,21 +239,43 @@ Do not: - mirror PPT-specific complexity when it makes the resulting SVG brittle or hard to edit - introduce dense low-value vector detail that does not materially improve template reuse +**Explicit template SVG contract**: + +| Rebuilt fact | Template SVG declaration | +|---|---| +| Output semantic layout | Root `data-pptx-layout` + stable `data-pptx-layout-name` | +| Rebuilt master/layout visual | Direct preview child with `data-pptx-layer="master|layout"` and `data-pptx-editable="false"` | +| Semantic content slot | Direct content child with `data-pptx-placeholder`; add `data-pptx-placeholder-idx` when same-role slots require stable disambiguation | +| Page-only background | Direct full-canvas solid rect with `data-pptx-layer="slide"` | +| Structural page-frame hint | Optional `data-pptx-role` only when background/decoration/header/footer/logo/watermark/chrome/page-number behavior is not already expressed by layer/placeholder metadata; stable unique `id` required | + +Repeat inherited visuals in every standalone SVG so browser preview remains complete. Template export validates their equality, moves one copy into the generated Master/Layout parts, and removes the repeated Slide copies. Do not flatten inherited visuals into unmarked slide content. + +Use the imported placeholder types verbatim: `title`, `subtitle`, `body`, +`picture`, `chart`, `table`, `object`, `media`, `date`, `footer`, and +`slide-number`. In particular, do not collapse source `subTitle`, `obj`, +`media`, or `dt` placeholders into a generic body marker. A reconstructed +title normally has no index. Assign stable indices only when repeated roles need +disambiguation inside the rebuilt Layout. + ### 3. Placeholder Markers -> **Mirror mode skips this section entirely.** Mirror SVGs are verbatim copies of the source flat slides — they carry no `{{}}` markers. Mirror is only a template-creation mode; downstream generation treats the finished files as normal template roster candidates. The rest of this section applies to `standard` and `fidelity` only. +> Mirror may retain literal example text instead of `{{...}}` authoring markers, but imported semantic content slots still receive native `data-pptx-placeholder` metadata. The rest of this section defines the preferred authoring vocabulary for standard and fidelity modes. Use clear placeholder markers for replaceable content: ```xml - + {{TITLE}} - + {{CONTENT_AREA}} ``` @@ -347,7 +379,7 @@ templates/// └── *.png / *.jpg ``` -Filenames preserve the source slide order via the 3-digit prefix; `` is derived from `manifest.json` `pageTypeCandidates`. No `{{}}` placeholders appear in any mirror SVG. +Filenames preserve the source slide order via the 3-digit prefix; `` is derived from `manifest.json` `pageTypeCandidates`. Literal source text may remain, but every page still carries explicit native structure metadata. ### Template Preview @@ -357,6 +389,7 @@ If the template is based on PPTX import output, briefly note: - which extracted assets were reused directly - which complex original decorations were intentionally simplified - whether any page-type mapping required judgment beyond the import heuristic +- how the source Master/Layout graph was consolidated into the rebuilt Master and semantic Layout roster --- @@ -364,7 +397,7 @@ If the template is based on PPTX import output, briefly note: If suitable template resources already exist, use them directly instead of generating new ones: -1. **Copy template**: copy the spec + template SVGs into the project's `templates/`, and any bundled bitmaps into the project's `images/` (the runtime image pool, referenced as `../images/`) +1. **Copy template**: copy the spec + explicitly layered template SVGs into the project's `templates/`, and any bundled bitmaps into the project's `images/` (the runtime image pool, referenced as `../images/`). 2. **Adjust colors**: Modify colors per the project design spec 3. **Customize**: Make project-specific adjustments @@ -397,6 +430,7 @@ templates/ - [x] Every page listed in `design_spec.md §V Page Roster` saved to `templates///` (decks for full-PPT replicas, layouts for structure-only) - [x] Naming convention applied (standard / fidelity: letter-suffix variants; mirror: `_.svg`) - [x] Templates follow design spec (colors, fonts, layout) -- [x] Placeholder markers are clear and standardized (standard / fidelity); mirror SVGs contain **no** `{{}}` markers +- [x] Placeholder markers are clear and standardized; mirror may keep literal text but still maps imported semantic slots +- [x] Every SVG is a complete preview with explicit Master/Layout/Slide/placeholder metadata and `native_structure_mode: template` - [ ] **Next step**: Validate assets and register the template via `register_template.py --kind ` ``` diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/README.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/README.md index 8fca4444..9fef3b2f 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/README.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/README.md @@ -43,7 +43,7 @@ python3 scripts/update_repo.py | Area | Primary scripts | Documentation | |------|-----------------|---------------| -| Conversion | `source_to_md.py`, `source_to_md/pdf_to_md.py`, `source_to_md/doc_to_md.py`, `source_to_md/excel_to_md.py`, `source_to_md/ppt_to_md.py`, `source_to_md/web_to_md.py`, `pptx_intake.py` | [docs/conversion.md](./docs/conversion.md) | +| Conversion | `source_to_md.py`, `source_to_md/pdf_to_md.py`, `source_to_md/doc_to_md.py`, `source_to_md/excel_to_md.py`, `source_to_md/ppt_to_md.py`, `source_to_md/web_to_md.py`, `pptx_intake.py`, `pptx_to_svg.py` | [docs/conversion.md](./docs/conversion.md) | | Project management | `project_manager.py`, `batch_validate.py`, `generate_examples_index.py`, `error_helper.py`, `pptx_template_import.py`, `template_fill_pptx.py`, `native_enhance_pptx.py` | [docs/project.md](./docs/project.md) | | SVG pipeline | `finalize_svg.py`, `svg_to_pptx.py`, `total_md_split.py`, `svg_quality_checker.py`, `extract_svg_assets.py`, `animation_config.py`, `notes_to_audio.py` | [docs/svg-pipeline.md](./docs/svg-pipeline.md) | | Spec maintenance | `update_spec.py` | [docs/update_spec.md](./docs/update_spec.md) | @@ -62,6 +62,7 @@ python3 scripts/source_to_md/ppt_to_md.py python3 scripts/source_to_md/doc_to_md.py python3 scripts/source_to_md/excel_to_md.py python3 scripts/source_to_md/web_to_md.py +python3 scripts/pptx_to_svg.py -o # reconstruction/reference SVG import ``` Project setup: @@ -115,6 +116,22 @@ python3 scripts/svg_to_pptx.py `finalize_svg.py` optimizes raster images by default using `2x` display pixels and max `2560px`. Native `svg_to_pptx.py` defaults to `--image-sizing cap`: only oversized full source images are reduced to max `2560px`, so later PowerPoint resizing keeps more image detail. Use `svg_to_pptx.py --image-sizing display --image-scale 2` only for aggressive size reduction, or `--no-image-optimize` when the native PPTX must embed original image bytes. +For SVG-authoring routes, `svg_output/` is the complete visible page-design source: every exported text, image, shape, background, and template-derived layout element is present in the page SVG or explicitly referenced by it. Export may translate represented content into Master/Layout/Slide parts or native objects, but it does not retrieve missing visible content from templates or planning files. Speaker notes, animation, narration, transitions, `template-fill-pptx`, and `native-enhance-pptx` remain separately owned capabilities. + +When `spec_lock.md` has no `pptx_structure` section, native `svg_to_pptx.py` falls back to `baseline`: the generated deck keeps a standard Master/Layout relationship and promotes only exact, z-order-safe shared backgrounds/chrome. Root `data-pptx-page-role` selects `Cover`, `Agenda`, `Section`, `Closing`, or `Content`; filenames are a fallback for marker-free legacy SVGs. A family-wide exact background and an exact leading structurally marked chrome prefix may move into that Layout; ids are consulted only when the structural role is absent. Actual titles, body content, pictures, charts, tables, page numbers, and page-specific shapes remain on Slide. No visual-similarity or placeholder inference is performed. Baseline also prunes unused layouts and maps locked typography/colors into the PowerPoint theme. Use `--pptx-structure flat` for slide-local diagnostics. + +Deck/layout template routes use explicit template structure. Each complete SVG names its output Layout, repeats inherited Master/Layout preview layers, and marks supported content placeholders. Strict keeps the selected Layout contract; adaptive may create a new Layout while retaining the Master. The exporter validates cross-slide equality, creates one reusable Layout per key, removes repeated inherited copies, and keeps actual content Slide-local. The complete contract lives in [`references/shared-standards.md`](../references/shared-standards.md#explicit-pptx-master--layout--placeholder-metadata-template-export). + +Current `create-template` output always rebuilds explicit SVG structure and does not package `native_structure.json` or `source_template.pptx`. `preserve` remains available only for existing projects that already carry the legacy pair. + +`pptx_to_svg.py` annotates supported unmerged tables and conservative classic-chart caches with `data-pptx-native` metadata. Source table-style inheritance, supported solid cell fills/basic text formatting, chart title/legend/axis titles, and plot-level data-label flags for area/bar/column/line charts are retained when the current schema can represent them. Tables with direct borders, non-solid fills, or mixed rich-text formatting remain fallback-only, as do charts with unsupported label scopes/types, custom axis semantics, trendlines/error bars, or subtype options. Unsupported tables keep their rendered SVG table; unsupported charts keep a baked preview or explicit placeholder. Both carry `data-pptx-native-status`, which `svg_quality_checker.py` and `svg_to_pptx.py --native-objects` report as a warning. + +Exporter-canonical classic charts also recover canonical solid series/slice +colors and exact one- or two-paragraph title styling; two paragraphs retain +their `title` / `subtitle` roles. Slide-number fields resolve to the display +number defined by `firstSlideNum`; standalone master/layout SVGs retain their +literal field fallback because they are shared by multiple slides. + Image generation: ```bash @@ -137,7 +154,7 @@ python3 scripts/update_repo.py --skip-pip - Keep one user-facing entry point per workflow at the top level of `scripts/` - Move provider-specific or helper internals into subdirectories - Prefer the unified entry points `project_manager.py`, `finalize_svg.py`, and `image_gen.py` -- Prefer `svg_final/` over `svg_output/` when exporting +- Use `svg_output/` for native export and `svg_final/` for SVG snapshot/preview export ## Related Docs @@ -148,4 +165,4 @@ python3 scripts/update_repo.py --skip-pip - [Troubleshooting](./docs/troubleshooting.md) - [Skill Entry](../SKILL.md) -_Last updated: 2026-04-09_ +_Last updated: 2026-07-10_ diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/server.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/server.py index 6e257e4e..ea8d8c8e 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/server.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/server.py @@ -460,11 +460,36 @@ def _sync_session_state( # browser STATE — lost on a refresh. Folding them from result.json into the # served recommendations lets a refresh / reopen re-initialize from the user's # actual choices instead of catalog defaults. -_ANCHOR_RECOMMEND_KEYS = ('canvas', 'mode', 'visual_style', 'delivery_purpose') +_ANCHOR_RECOMMEND_KEYS = ( + 'canvas', + 'mode', + 'visual_style', + 'delivery_purpose', + 'template_adherence', +) _ANCHOR_VALUE_KEYS = ('audience', 'content_divergence') _DESIGN_RECOMMEND_KEYS = ('icons', 'formula_policy') +def _template_adherence_enabled(project_path: Path) -> bool: + """Return whether Step 3 loaded a deck/layout template into the project.""" + spec_path = project_path / 'templates' / 'design_spec.md' + try: + lines = spec_path.read_text(encoding='utf-8').splitlines() + except OSError: + return False + if not lines or lines[0].strip() != '---': + return False + for line in lines[1:]: + stripped = line.strip() + if stripped == '---': + return False + match = re.fullmatch(r'kind\s*:\s*["\']?(brand|layout|deck)["\']?', stripped) + if match: + return match.group(1) in {'layout', 'deck'} + return False + + def _merge_confirmed_choices(data: dict, result_file: Path) -> None: """Fold already-confirmed choices into later-stage recommendations.""" try: @@ -818,6 +843,17 @@ def create_app( # the user's choices instead of catalog defaults. if _recommendation_stage(data) >= 2 and result_file.exists(): _merge_confirmed_choices(data, result_file) + template_adherence_enabled = _template_adherence_enabled(project_path) + data['_template_adherence_enabled'] = template_adherence_enabled + recommend = data.get('recommend') + if template_adherence_enabled: + if not isinstance(recommend, dict): + recommend = data['recommend'] = {} + recommend.setdefault('template_adherence', 'adaptive') + else: + if isinstance(recommend, dict): + recommend.pop('template_adherence', None) + data.pop('template_adherence', None) # The page polls this endpoint after a stage-1 confirm until the AI # overwrites the file with the re-derived stage-2 recommendations, so it # must never be served from a cache. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/app.js b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/app.js index 5f1457c2..11c224dd 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/app.js +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/app.js @@ -1,6 +1,6 @@ /* PPT Master - Strategist confirmation stage UI - * Finite/enumerable fields (canvas, mode, visual style, icons, image usage, - * AI source, formula policy, generation mode) list ALL options from + * Finite/enumerable fields (canvas, mode, visual style, template adherence, + * icons, image usage, AI source, formula policy, generation mode) list ALL options from * /static/catalogs.json with the AI's recommendation marked. Open/generative * fields (color, typography, generated-image style) show >=3 AI candidates. Open fields also expose * Custom controls. On confirm the page saves result.json and closes. @@ -38,6 +38,7 @@ sec_refine: "Refine spec first", sub_mode: "Narrative mode", sub_visual: "Visual style", + sub_template_adherence: "Template adherence", sub_divergence: "Material divergence (how freely to reshape vs. stay close to the source)", placeholder_divergence: "In your words — e.g. \"stick closely to the document\" / \"freely restructure and expand within the source\". Leave blank for a balanced default.", custom: "Custom", @@ -149,6 +150,7 @@ sec_refine: "先に設計仕様を精査", sub_mode: "ナラティブモード", sub_visual: "ビジュアルスタイル", + sub_template_adherence: "テンプレートの適用方法", sub_divergence: "素材からの発散度(どこまで自由に再構成するか、原文に忠実か)", placeholder_divergence: "自分の言葉でどうぞ — 例:「文書に忠実に」「元素材の範囲内で自由に再構成・展開」。空欄ならバランス型になります。", custom: "カスタム", @@ -260,6 +262,7 @@ sec_refine: "先精修设计规范", sub_mode: "叙事模式", sub_visual: "视觉风格", + sub_template_adherence: "模板遵循方式", sub_divergence: "材料发散度(多大程度重塑,还是贴近源材料)", placeholder_divergence: "用你自己的话写,例如「严格贴着文档来」/「在源材料范围内自由重组并展开」。留空则按平衡处理。", custom: "自定义", @@ -719,6 +722,15 @@ function recValue(field) { return (REC && REC.recommend && REC.recommend[field]) || legacyRecId(field); } + + function hasTemplateAdherence() { + if (!REC) return false; + if (typeof REC._template_adherence_enabled === "boolean") { + return REC._template_adherence_enabled; + } + if (REC.recommend && REC.recommend.template_adherence != null) return true; + return REC.template_adherence != null; + } // Guaranteed recommendation: the AI's pick, or the first catalog option as a // fallback so an enumerable field ALWAYS shows a badged recommendation. function recOrFirst(field, list) { @@ -1054,6 +1066,15 @@ function () { return STATE.visual_style; }, function (v) { STATE.visual_style = v; refreshDirectionPreview(); }, { allowCustom: true, spectrum: REC && REC.visual_style_spectrum }); sec.appendChild(sub2); + if (hasTemplateAdherence()) { + var templateField = el("div", "subfield"); + templateField.appendChild(el("div", "subfield-label", t("sub_template_adherence"))); + enumField(templateField, CAT.template_adherence, + recOrFirst("template_adherence", CAT.template_adherence), + function () { return STATE.template_adherence; }, + function (v) { STATE.template_adherence = v; }); + sec.appendChild(templateField); + } host.appendChild(sec); } @@ -2074,6 +2095,11 @@ STATE.content_divergence = (REC.content_divergence && REC.content_divergence.value) || ""; // free text; blank = balanced default STATE.mode = pick("mode", CAT.modes); STATE.visual_style = pick("visual_style", CAT.visual_styles); + if (hasTemplateAdherence()) { + STATE.template_adherence = pick("template_adherence", CAT.template_adherence); + } else { + delete STATE.template_adherence; + } // Delivery purpose drives the PPT body px baseline; default balanced // (not the catalog-first id) when the Strategist did not recommend one. STATE.delivery_purpose = recId("delivery_purpose") || "balanced"; @@ -2150,6 +2176,7 @@ mode: STATE.mode, visual_style: STATE.visual_style }; + if (STATE.template_adherence) payload.template_adherence = STATE.template_adherence; // Delivery purpose is PPT-only and rendered only on PPT canvases (§c). if (isPptCanvas(STATE.canvas)) payload.delivery_purpose = STATE.delivery_purpose; return payload; @@ -2169,6 +2196,7 @@ typography: STATE.typography, formula_policy: STATE.formula_policy }; + if (STATE.template_adherence) payload.template_adherence = STATE.template_adherence; if (isPptCanvas(STATE.canvas)) payload.delivery_purpose = STATE.delivery_purpose; normalizeTypographyForSubmit(payload); return payload; diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/catalogs.json b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/catalogs.json index b62ed9d6..3acbb5ab 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/catalogs.json +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/confirm_ui/static/catalogs.json @@ -555,6 +555,28 @@ "label_ja": "分割モード" } ], + "template_adherence": [ + { + "id": "adaptive", + "label": "adaptive", + "label_zh": "适应性使用", + "label_en": "adaptive", + "label_ja": "適応的に使用", + "desc_zh": "每页选择一个模板 SVG 作为架构参考;没有合适版式时沿用同一 Master 创建新的显式 Layout。", + "desc_en": "Choose a template SVG as the architecture reference for every page; when no layout fits, create a new explicit Layout under the same Master.", + "desc_ja": "各ページでテンプレートSVGを構造参照として選び、適合する版式がない場合は同じMaster配下に新しい明示的Layoutを作成する。" + }, + { + "id": "strict", + "label": "strict", + "label_zh": "严格套用", + "label_en": "strict", + "label_ja": "厳密に適用", + "desc_zh": "每一页都从模板 SVG roster 选择版式,并保持其显式 Master、Layout 与 Placeholder 契约不变。", + "desc_en": "Map every page to the template SVG roster and keep its explicit Master, Layout, and Placeholder contract unchanged.", + "desc_ja": "すべてのページをテンプレートSVG一覧へ割り当て、明示されたMaster/Layout/Placeholder契約を変更せずに維持する。" + } + ], "delivery_purpose": [ { "id": "text", diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/confirm_ui.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/confirm_ui.md index 8b1ef4a4..8a79e758 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/confirm_ui.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/confirm_ui.md @@ -1,6 +1,6 @@ # Confirm UI — Strategist Confirmation Stage Page -> The interactive, visual surface for SKILL.md Step 4 (the Strategist confirmation stage). Enumerable fields list **all** options from a catalog with the AI's recommendation badged; generative fields (color, typography, generated-image style) show **≥3** AI candidates (creative recommendations always offer real choice — same rule as the h.5 image strategy; fewer only on the honest-shortfall exception, with a stated reason). Fields whose universe is open (canvas, mode, visual style, icons) also get a **Custom** box; image usage is a multi-select source list plus a free-text `image_notes` box. Fully closed fields (AI source when applicable, formula policy, generation mode, refine spec) do not. The AI writes its recommendation to `recommendations.json`; the user's final choices are written back to `result.json` for the AI to read. On confirm the page saves the result and shuts the server down (auto-close). The chat path is always a valid fallback — if the browser cannot open (remote / headless / web host), the AI presents the same staged confirmation in chat. +> The interactive, visual surface for SKILL.md Step 4 (the Strategist confirmation stage). Enumerable fields list **all** options from a catalog with the AI's recommendation badged; generative fields (color, typography, generated-image style) show **≥3** AI candidates (creative recommendations always offer real choice — same rule as the h.5 image strategy; fewer only on the honest-shortfall exception, with a stated reason). Fields whose universe is open (canvas, mode, visual style, icons) also get a **Custom** box; image usage is a multi-select source list plus a free-text `image_notes` box. Fully closed fields (template adherence when a deck/layout template is active, AI source when applicable, formula policy, generation mode, refine spec) do not. The AI writes its recommendation to `recommendations.json`; the user's final choices are written back to `result.json` for the AI to read. On confirm the page saves the result and shuts the server down (auto-close). The chat path is always a valid fallback — if the browser cannot open (remote / headless / web host), the AI presents the same staged confirmation in chat. ## Authority and Scope @@ -40,6 +40,7 @@ python3 scripts/confirm_ui/server.py --shutdown # Step 4 clean - Refuses to start unless `/confirm_ui/recommendations.json` exists (except `--shutdown`, which needs no recommendations). - Per-project lock at `/.confirm_ui.lock` — duplicate launches are refused; stale locks (dead pid) are overwritten. - Idle auto-shutdown after 900 s by default; `/api/shutdown` exits gracefully and releases the lock. +- `/api/recommendations` derives `_template_adherence_enabled` from `/templates/design_spec.md` frontmatter. Only `kind: deck` / `kind: layout` enables the Stage-1 field; no template and `kind: brand` force it off even if a stale recommendation contains the key. When enabled but no recommendation was authored, the server supplies `adaptive` as the default recommendation. Dependency: @@ -52,16 +53,16 @@ pip install flask - **Enumerable + custom** — canvas / mode / visual_style / icons. The page lists common options from `static/catalogs.json`, badges the AI's recommendation, and still offers a Custom box for edge cases (custom canvas size, bespoke narrative mode, self-provided icon system, etc.). `visual_style` additionally honors an optional `visual_style_spectrum` that badges a 3-pick personality spectrum (safe / shifted / bold, each with a temperament tag + analogy) in place of the single recommendation — see the schema below. - **Visual examples for hard-to-name choices** — the full-screen confirmation page loads real SVG page samples from `static/style_previews/` for `visual_style`, and renders real sample SVGs from `templates/icons` for `icons`. These thumbnails make style and icon-library choices visually comparable before the user locks them. Preview copy is fixed role text (big title / section title / body / points), not project content from `recommendations.json`, so users compare visual treatment rather than copywriting. These previews are a confirmation aid only: they do not add fields to `recommendations.json` or `result.json`, and they do not replace the later Step 6 live preview. - **Image usage multi-select** — image sources are selected as one or more catalog ids: `ai` = AI-generated, `web` = Web-sourced, `provided` = User-provided, `placeholder` = Placeholder, `none` = No images. `none` is exclusive. Recommendation and result values may be a legacy single string, but new files should use an array. When several sources are recommended, write the source ids to `recommend.image_usage` and write the actual usage strategy to `image_notes`, not a custom prose value. -- **Closed enumerable** — formula policy / generation mode / refine spec, plus AI source only when image usage includes `ai`. These have no Custom box; out-of-catalog values snap back to the recommended option. Use pipeline vocabulary: icon ids are actual library ids such as `tabler-outline`, or `emoji` for system emoji. +- **Closed enumerable** — template adherence (conditional), formula policy / generation mode / refine spec, plus AI source only when image usage includes `ai`. These have no Custom box; out-of-catalog values snap back to the recommended option. Use pipeline vocabulary: icon ids are actual library ids such as `tabler-outline`, or `emoji` for system emoji. - **Generative (open)** — color, typography, generated-image style. No finite catalog; the AI authors **≥3 candidates** the page renders as cards (never a single option — creative fields must offer real choice; fewer than 3 only on the honest-shortfall exception). `page_count`, `audience`, and `content_divergence` are free inputs (`content_divergence` is a free-text intent shown under audience in §c, not a fixed-option field). -**Custom box** appears only on fields whose universe is genuinely open — `canvas`, `mode`, `visual_style`, and `icons`. Image usage uses a multi-select source list plus `image_notes` instead of a Custom box. Fully closed sets — `image_ai_path`, `formula_policy`, `generation_mode`, `refine_spec` — have **no** Custom box; an out-of-catalog value there is snapped back to the recommended option. +**Custom box** appears only on fields whose universe is genuinely open — `canvas`, `mode`, `visual_style`, and `icons`. Image usage uses a multi-select source list plus `image_notes` instead of a Custom box. Fully closed sets — `template_adherence`, `image_ai_path`, `formula_policy`, `generation_mode`, `refine_spec` — have **no** Custom box; an out-of-catalog value there is snapped back to the recommended option. `image_ai_path` is conditional: the page shows it and writes it to `result.json` only when `image_usage` includes `ai`. Web-sourced / User-provided / Placeholder / No images paths do not carry an AI backend choice. ## Catalogs — `static/catalogs.json` (the finite option universe) -The front-end loads `/api/catalogs` (served by the confirm server) and falls back to the static `/static/catalogs.json` if that route is unavailable. `/api/catalogs` returns the static file **with the `canvas` list synced live from `config.py CANVAS_FORMATS`** — the set of formats and their `dim` come from config (single source of truth, zero drift), while trilingual labels / use text stay in catalogs.json (a plain fallback label is synthesized for any new id config adds). Keys: `canvas`, `modes`, `visual_styles` (grouped), `icons`, `image_usage`, `image_ai_path`, `formula_policy`, `generation_mode`, `delivery_purpose`. Each entry is `{ "id", "label", "label_zh", "label_en", "label_ja", ... }`; descriptions use `desc_zh` / `desc_en` / `desc_ja`, and `visual_styles` groups use `group_zh` / `group_en` / `group_ja`. The front-end falls back to legacy `label` / `desc` / `group`, so old catalogs still load, but new user-facing catalog text must cover all three languages (zh / en / ja). English labels should mirror canonical reference names (`pyramid`, `swiss-minimal`, `Path A`, `mixed`, etc.); Chinese and Japanese labels should be translated for users. Descriptions render inline after the option title, not as a separate selected-option line. `visual_styles` is `[{ "group", "group_zh", "group_en", "group_ja", "items": [...] }]`. For `canvas` you only need to maintain the trilingual labels in catalogs.json; the format set and dimensions are authoritative in `config.py CANVAS_FORMATS`. +The front-end loads `/api/catalogs` (served by the confirm server) and falls back to the static `/static/catalogs.json` if that route is unavailable. `/api/catalogs` returns the static file **with the `canvas` list synced live from `config.py CANVAS_FORMATS`** — the set of formats and their `dim` come from config (single source of truth, zero drift), while trilingual labels / use text stay in catalogs.json (a plain fallback label is synthesized for any new id config adds). Keys: `canvas`, `modes`, `visual_styles` (grouped), `template_adherence`, `icons`, `image_usage`, `image_ai_path`, `formula_policy`, `generation_mode`, `delivery_purpose`. Each entry is `{ "id", "label", "label_zh", "label_en", "label_ja", ... }`; descriptions use `desc_zh` / `desc_en` / `desc_ja`, and `visual_styles` groups use `group_zh` / `group_en` / `group_ja`. The front-end falls back to legacy `label` / `desc` / `group`, so old catalogs still load, but new user-facing catalog text must cover all three languages (zh / en / ja). English labels should mirror canonical reference names (`pyramid`, `swiss-minimal`, `Path A`, `mixed`, etc.); Chinese and Japanese labels should be translated for users. Descriptions render inline after the option title, not as a separate selected-option line. `visual_styles` is `[{ "group", "group_zh", "group_en", "group_ja", "items": [...] }]`. For `canvas` you only need to maintain the trilingual labels in catalogs.json; the format set and dimensions are authoritative in `config.py CANVAS_FORMATS`. ## Round-trip data contract @@ -73,7 +74,7 @@ The page runs as a **three-stage wizard in one browser session**. `recommendatio | `recommendations.json stage` | Page renders | Button | On submit | |---|---|---|---| -| `"stage1"` | direction anchors — canvas, audience + `content_divergence` + `delivery_purpose` *(PPT only — omitted on non-PPT canvases, not written to the result)*, mode + visual_style | **Next** | writes `result.json` `{ stage: "stage1", status: "stage1-confirmed", }`; the page does **not** close — it shows a "deriving…" state and polls `GET /api/recommendations` | +| `"stage1"` | direction anchors — canvas, audience + `content_divergence` + `delivery_purpose` *(PPT only — omitted on non-PPT canvases, not written to the result)*, mode + visual_style, plus `template_adherence` only when a deck/layout template is active | **Next** | writes `result.json` `{ stage: "stage1", status: "stage1-confirmed", }`; the page does **not** close — it shows a "deriving…" state and polls `GET /api/recommendations` | | `"stage2"` | design system — page count, color, icons, typography, formula policy | **Next** | writes `result.json` `{ stage: "stage2", status: "stage2-confirmed", }`; the page stays open and polls for Stage 3 | | `"stage3"` | images and execution — image usage + generated-image style, generation mode, refine spec | **Confirm** | writes `result.json` `{ stage: "final", status: "confirmed", }`, then shuts the page down | | *(absent)* | legacy single-pass — every section on one page | **Confirm** | single final write (`status: "confirmed"`) — backward-compatible | @@ -90,6 +91,7 @@ The AI launches Stage 1 (`--daemon --wait`), reads the stage-1 result, **re-deri "canvas": "ppt169", "mode": "pyramid", "visual_style": "swiss-minimal", + "template_adherence": "adaptive", "icons": "tabler-outline", "image_usage": ["ai", "provided"], "image_ai_path": "auto", @@ -151,6 +153,7 @@ The AI launches Stage 1 (`--daemon --wait`), reads the stage-1 result, **re-deri > Each `candidates` array above shows **one** entry for brevity — `color` and `typography` must each carry **≥3** in a real file, while `image_strategy.candidates` should carry **exactly 3 non-custom recommendation** entries when AI image generation is offered. The UI adds the fourth **Custom** card itself; `selected` indexes the recommended default among the recommendation entries. - `recommend.*` names the recommended `id` for each enumerable field (must match a `catalogs.json` id, or be a free string for a recommended custom value). The page badges and pre-selects it. **Guarantee**: if a `recommend.*` is omitted, the page falls back to the first catalog option so every enumerable field always shows one badged recommendation — but the AI should still set them for a meaningful default. Legacy aliases are accepted for old files (`line` → `tabler-outline`, `filled` → `tabler-filled`, `monochrome` → `chunk-filled`, `search` → `web`, `default` → `auto`, `builtin` → `host-native`), but new files should write canonical ids. +- `recommend.template_adherence` is conditional. Write `adaptive` or `strict` only when Step 3 loaded a `kind: deck` or `kind: layout` template. The server independently checks the copied template spec and enables the field only for those two kinds; free design and brand-only templates cannot display or return it. If the template is active but the recommendation key is missing, the server supplies `adaptive`. Both values use explicit template export and require a reference SVG for every page: `strict` keeps the selected Layout contract; `adaptive` may create a new Layout under the same Master. - `recommend.image_usage` should be an array of source ids when more than one source applies, e.g. `["ai", "provided"]`. A single string is still accepted for backward compatibility. Do not write bare `"custom"` and do not encode a mixed-source plan as prose here; write the prose to top-level `image_notes.value`. - `image_notes` is the initial strategy note shown under the image source chips. Use it for page-role guidance and constraints: which source applies where, what to avoid, which user assets are authoritative, how realistic / abstract the imagery should be, and what can remain as placeholders. It is intent guidance, not a separate finite option. - When `recommend.image_usage` includes `ai`, also set `recommend.image_ai_path` to one of `auto` / `api` / `host-native` / `manual`; the page presents these as explicit choices. @@ -176,6 +179,7 @@ The AI launches Stage 1 (`--daemon --wait`), reads the stage-1 result, **re-deri "content_divergence": "freely restructure and expand within the source", "mode": "pyramid", "visual_style": "swiss-minimal", + "template_adherence": "adaptive", "color": { "name": "...", "palette": { "background": "#...", "secondary_bg": "#...", "primary": "#...", "accent": "#...", "secondary_accent": "#...", "body_text": "#..." } }, "icons": "tabler-outline", "typography": { "name": "...", "heading": { "cjk": "...", "latin": "...", "css": "..." }, "body": { "cjk": "...", "latin": "...", "css": "..." }, "body_size": 24, "body_size_unit": "px", "sizes": { "title": 42, "subtitle": 32, "annotation": 18 } }, diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/conversion.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/conversion.md index cf193d8e..f32f1e32 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/conversion.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/conversion.md @@ -228,6 +228,36 @@ Usage boundary: - Beautify promotes selected identity/content fields into locked constraints after confirmation. - Template-fill uses the slide library as the native PPTX fill contract. +## `pptx_to_svg.py` + +Reconstruct a PPTX package as editable SVG views by reading OOXML directly. + +```bash +python3 scripts/pptx_to_svg.py deck.pptx --inheritance-mode both +python3 scripts/pptx_to_svg.py deck.pptx --inheritance-mode layered +python3 scripts/pptx_to_svg.py deck.pptx --inheritance-mode flat +``` + +| Mode | Output | +|---|---| +| `both` (default) | Layered master/layout/slide SVGs under `svg/`, plus self-contained slides under `svg-flat/` | +| `layered` | Only the layered `svg/` view and inheritance metadata | +| `flat` | One self-contained slide SVG per page under `svg/` | + +Supported unmerged tables and conservative classic-chart caches carry +`data-pptx-native` metadata beside their SVG fallback. Markers remain dormant +unless a later export uses `--native-objects`. Unsupported tables keep their +rendered SVG table; unsupported charts keep a baked preview or explicit +placeholder. Both carry `data-pptx-native-status`. + +Exporter-canonical charts recover canonical solid series/slice colors and exact +one- or two-paragraph title styling; two paragraphs retain their `title` / +`subtitle` roles. This is not a general source-chart style round-trip guarantee. + +Concrete slide SVGs resolve `` using the presentation's +`firstSlideNum` display numbering. Standalone master/layout SVGs keep the +literal field fallback because one shared part can serve multiple slides. + ## `source_to_md/web_to_md.py` Convert web pages to Markdown and download images locally. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/project.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/project.md index a461d56a..1fe40590 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/project.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/project.md @@ -110,7 +110,7 @@ Notes: - **SVG output emits two views by default** (`--inheritance-mode both`): - `svg/` — layered template view for designers: every master and layout in the deck rendered once as `svg/master_*.svg` / `svg/layout_*.svg` (including ones no sample slide currently references); `svg/slide_NN.svg` contains only that slide's own shapes; `svg/inheritance.json` records which layout / master each slide consumes. - `svg-flat/` — companion view: each `slide_NN.svg` is self-contained (master + layout + slide painted into one file), so opening any slide in isolation shows the full page like PowerPoint would. Useful for previews, screenshots, and "did this slide actually render correctly" sanity checks. -- `manifest.json` records `svgFile` for slides / layouts / masters, `flatSvgFile` for slides when `svg-flat/` exists, placeholder type / index / geometry / base style, an asset map used by SVG `href` values, and common assets reused through slide / layout / master inheritance +- `manifest.json` records `svgFile` for slides / layouts / masters, `flatSvgFile` for slides when `svg-flat/` exists, placeholder type / index / geometry / base style, an asset map used by SVG `href` values, and common assets reused through slide / layout / master inheritance. Placeholder semantics keep `subTitle`, `obj`, `media`, and `dt` distinct as `subtitle`, `object`, `media`, and `date`. - Layered slide SVGs keep only the slide's own background; inherited master / layout backgrounds stay in the corresponding master / layout SVGs - Placeholder guides are intentionally lightweight in `svg/` master / layout files; `svg-flat/` hides those guides and is the visual preview source - Charts, SmartArt, diagrams, and OLE objects become typed placeholders in `svg/`; `svg-flat/` shows a preview image with a corner badge when one exists, otherwise a visible placeholder. Tables are converted into real SVG content. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/svg-pipeline.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/svg-pipeline.md index 849c36e6..a82b9a94 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/svg-pipeline.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/svg-pipeline.md @@ -30,8 +30,11 @@ Convert project SVGs into PPTX. ```bash python3 scripts/svg_to_pptx.py -python3 scripts/svg_to_pptx.py --only native -python3 scripts/svg_to_pptx.py --only legacy +python3 scripts/svg_to_pptx.py --native-objects +python3 scripts/svg_to_pptx.py --pptx-structure template # explicit SVG template metadata +python3 scripts/svg_to_pptx.py --pptx-structure preserve # imported source package contract +python3 scripts/svg_to_pptx.py --pptx-structure flat # structure diagnostic +# Template-import visual round-trip diagnostic only: python3 scripts/svg_to_pptx.py --only native -s svg-flat python3 scripts/svg_to_pptx.py --no-notes python3 scripts/svg_to_pptx.py -t none @@ -55,9 +58,20 @@ Behavior: - With `--no-merge`: every dy-stacked `` becomes its own text frame — exact SVG line layout is preserved but a 12-line paragraph is 12 separate textboxes - Side effect: PowerPoint may wrap merged paragraphs to a different line count than the SVG source. Long body text (abstracts, multi-paragraph sections, reference lists) usually benefits from the default; pages with tight typographic alignment (covers, charts, tables) usually want `--no-merge` - Mergeable detection is conservative: only fires when the children form a clean paragraph block; mixed-layout `` falls through to the default per-line path -- Recommended source directory: `svg_final/` +- Native export reads `svg_output/`; `--svg-snapshot` and legacy export read + `svg_final/`. Use `-s` only as an explicit diagnostic override. +- On every SVG-authoring route, each file in `svg_output/` is the complete visible + page-design source. Templates and locks may guide authoring, but finalize/export + never use them to overlay visible content missing from the SVG. Notes, animation, + narration, transitions, and direct native-PPTX workflows keep their separate + inputs and package-level processing. - For PPTX template-import workspaces, use `-s svg-flat` when you need a visual round-trip check. The layered `svg/` tree is the machine-readable template source and intentionally does not inline inherited master / layout decoration into each slide. - Native mode is strict about unsupported visual SVG elements: if a visual element cannot be represented or safely preserved, export fails with the SVG file, element tag, and position instead of silently dropping content. +- Omitting `--pptx-structure` reads `spec_lock.md` and falls back to `baseline`. Baseline assigns Layout families from root `data-pptx-page-role`, keeps content Slide-local, and promotes only exact family-wide backgrounds plus exact leading structurally marked chrome; filenames and ids are compatibility fallbacks for marker-free legacy SVGs. It never infers placeholders or visual similarity. Template mode builds reusable PowerPoint structure only from explicit SVG metadata and validates every `pptx_layouts` row plus cross-slide equality. Both strict and adaptive template adherence use this mode; adaptive may introduce a new Layout key under the same Master. +- Template/preserve placeholder semantics distinguish title, subtitle, body, picture, chart, table, generic object, media, date, footer, and slide number. Reconstructed titles are normally type-matched without an index; explicit imported title indices and all other source indices are retained. Imported `subTitle`, `obj`, `media`, and `dt` identities remain distinct through `manifest.json`, `native_structure.json`, Layout XML, and Slide XML. +- Baseline/template native export reads `spec_lock.md` typography into the PowerPoint theme: `title_family` becomes the major font and `body_family` / `font_family` becomes the minor font. Matching SVG text emits `+mj-*` / `+mn-*` tokens, while unrelated emphasis/code/brand families stay concrete. Preserve mode keeps the imported source theme; flat mode keeps fixed-font diagnostic output. +- Baseline/template native export also maps canonical `spec_lock.md` color roles into the PowerPoint color scheme and emits context-safe `schemeClr` tokens for exact matches in SVG fills/text/strokes, gradients/patterns/bullets, native tables, and native-chart accent series. Local colors, inverse white/black, and effects stay concrete. Preserve mode keeps the imported source color scheme; flat mode keeps fixed-color diagnostic output. +- Preserve mode is legacy strict-only compatibility for existing projects that already carry `native_structure.json` + `source_template.pptx`. Current template creation does not emit the pair. - Native output uses content-hash media filenames, so identical images are reused and different images cannot overwrite each other by sharing a basename. - `[Content_Types].xml` is generated from the actual media extensions written into the PPTX. Unknown media extensions fail unless Python's `mimetypes` can identify them. - Native export writes to a temporary file first and publishes the requested PPTX only after conversion succeeds. A failed conversion does not replace the main output file. @@ -74,11 +88,16 @@ Behavior: - Long-audio import and automatic long-audio splitting are not supported; keep narration assets page-level - Voice choices can be listed with `python3 scripts/notes_to_audio.py --list-common-voices`, `python3 scripts/notes_to_audio.py --list-voices --locale zh-CN`, or provider-specific `--provider --list-voices` - Page transitions are controlled by `-t/--transition`; per-element entrance animations are controlled by `-a/--animation` -- Per-element animation applies to top-level SVG `` groups in z-order; aim for 3–8 content groups per slide. Page chrome (background / header / footer / decorations / watermark / page number, by id token) is skipped automatically +- Per-element animation applies to top-level SVG `` groups in z-order; aim for 3–8 content groups per slide. Existing layer/slide-number placeholder semantics are read before minimal structural roles; exact id tokens remain a fallback only when all explicit markers are absent - Start mode is set by `--animation-trigger`, mirroring PowerPoint's Start dropdown: `after-previous` (default, cascade with `--animation-stagger` spacing on slide entry), `on-click` (presenter-paced), `with-previous` (all together on slide entry) - `on-click` is for live presentations only; recorded narration rejects it because the tool does not generate object-level click timings - Flat SVG roots without top-level groups fall back to at most 8 visible primitives; beyond that, animation is skipped on the slide -- `auto` (default) maps effect from the group's SVG id: information-dense elements get a single stable effect (chart→wipe, card-/step-/pillar-→fly, title/takeaway→fade); image-like ids (hero/figure-/image/img-/kpi) cycle through a richer visual pool (zoom/dissolve/circle/box/diamond/wheel) so multiple images vary across the deck; unmatched ids cycle through a fade/wipe/fly/zoom fallback pool +- Per-element animation defaults to `none`. `auto` is opt-in (`-a auto`) and maps + effects from the group's SVG id: information-dense elements get a stable + effect (chart→wipe, card-/step-/pillar-→fly, title/takeaway→fade); image-like + ids (hero/figure-/image/img-/kpi) cycle through a richer pool + (zoom/dissolve/circle/box/diamond/wheel), while unmatched ids cycle through + fade/wipe/fly/zoom. - `mixed` (legacy) is deterministic: the first animated group on each slide uses `fade`, then later groups cycle through a larger 16-effect pool across the whole deck; `random` samples from that same legacy pool - `--animation-duration` controls per-element entrance length (default `0.4`); `--animation-stagger` adds gap between elements in `after-previous` mode (default `0.5`) - Optional object-level overrides live in `/animations.json` or a path passed via `--animation-config`; build and validate them with `animation_config.py scaffold|validate` diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_template_import.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_template_import.py index 59d9295d..5bfab469 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_template_import.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_template_import.py @@ -5,6 +5,8 @@ Reads OOXML directly via `pptx_to_svg` and writes a reusable reference workspace - `manifest.json` — single source of truth for slide size, theme colors, fonts, asset inventory, and per-slide / per-layout / per-master metadata +- `native_structure.json` + `source_template.pptx` — source-structure facts and + a byte-identical analysis copy used to rebuild explicit SVG structure - `summary.md` — short human-readable digest derived from manifest.json - `assets/` — extracted reusable image assets - `svg/` — primary view: by default the layered template view (every master @@ -24,6 +26,7 @@ from pathlib import Path from console_encoding import configure_utf8_stdio from template_import.manifest import build_manifest +from template_import.native_structure import write_native_structure_bundle configure_utf8_stdio() @@ -42,14 +45,17 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--skip-manifest", action="store_true", - help="Skip PPTX metadata extraction and asset inventory generation", + help=( + "Skip metadata, asset inventory, native structure contract, and " + "preserved source-package generation" + ), ) parser.add_argument( "--manifest-only", action="store_true", help=( - "Only extract manifest.json + summary.md + reusable assets, " - "without exporting slides to SVG" + "Only extract manifest.json + summary.md + reusable assets + the " + "native structure/source pair, without exporting slides to SVG" ), ) parser.add_argument( @@ -98,6 +104,7 @@ def main() -> int: return 1 manifest = None + native_structure = None manifest_path = output_dir / "manifest.json" if not args.skip_manifest: try: @@ -110,12 +117,28 @@ def main() -> int: json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) + try: + native_structure = write_native_structure_bundle( + pptx_path, + output_dir, + manifest, + ) + except (OSError, ValueError) as exc: + print(f"Error: failed to write native structure bundle: {exc}") + return 1 if args.manifest_only: print(f"Imported PPTX template source: {pptx_path.name}") print(f"Output directory: {output_dir}") if manifest is not None: print(f"Manifest: {manifest_path.name}") + print("Native structure: native_structure.json") + print("Source package analysis copy: source_template.pptx") + print( + "Source structure assessment: " + f"{native_structure['strategy']['recommendedMode']}" + ) + print("Template output mode: explicit SVG structure") print("Summary: summary.md") print(f"Assets exported: {len(manifest['assets']['allAssets'])}") print(f"Common assets: {len(manifest['assets']['commonAssets'])}") @@ -147,6 +170,12 @@ def main() -> int: print(f"Flat companion slides: {len(result.flat_slides)} (svg-flat/)") print(f"SVG bytes (primary): {total_bytes}") print(f"Output directory: {output_dir}") + if native_structure is not None: + print( + "Source structure assessment: " + f"{native_structure['strategy']['recommendedMode']}; " + "create-template rebuilds explicit SVG structure" + ) return 0 diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/chart_to_svg.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/chart_to_svg.py new file mode 100644 index 00000000..5f75add3 --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/chart_to_svg.py @@ -0,0 +1,1028 @@ +"""Extract editable native chart metadata from PPTX chart parts. + +The visual chart preview still comes from the existing graphicFrame fallback. +This module only builds a conservative ``data-pptx-native="chart"`` payload +when the chart XML cache can be mapped to the current native chart schema. +""" + +from __future__ import annotations + +import math +import re +from dataclasses import dataclass +from typing import Any +from xml.etree import ElementTree as ET + +from svg_to_pptx.drawingml.utils import parse_font_family +from svg_to_pptx.native_objects.chart_data import ( + validate_chart_payload, + validate_data_label_position, +) + +from .emu_units import NS, Xfrm, ooxml_bool +from .ooxml_loader import OoxmlPackage, PartRef + + +CHART_URI = "http://schemas.openxmlformats.org/drawingml/2006/chart" +CHARTEX_URI = "http://schemas.microsoft.com/office/drawing/2014/chartex" + +C_NS = { + **NS, + "c": "http://schemas.openxmlformats.org/drawingml/2006/chart", + "cx": CHARTEX_URI, +} + + +@dataclass +class ChartResult: + """Native chart marker payload or a transparent unsupported status.""" + + native_payload: dict[str, Any] | None = None + native_status: str | None = None + + +class _UnsupportedChart(RuntimeError): + """Raised when a chart should keep its visual fallback only.""" + + def __init__(self, status: str) -> None: + super().__init__(status) + self.status = status + + +def extract_native_chart_payload( + graphic_data: ET.Element | None, + xfrm: Xfrm, + slide_part: PartRef, + pkg: OoxmlPackage, +) -> ChartResult: + """Return native chart metadata for a supported classic chart.""" + if graphic_data is None: + return ChartResult(native_status="unsupported-chart-reference") + + uri = graphic_data.attrib.get("uri", "") + if uri == CHARTEX_URI or graphic_data.find("cx:chart", C_NS) is not None: + return ChartResult(native_status="unsupported-chartex") + if uri != CHART_URI: + return ChartResult(native_status="unsupported-chart-uri") + if xfrm.rot or xfrm.flip_h or xfrm.flip_v: + return ChartResult(native_status="unsupported-native-transform") + + chart_ref = graphic_data.find("c:chart", C_NS) + if chart_ref is None: + return ChartResult(native_status="unsupported-chart-reference") + rid = chart_ref.attrib.get(f"{{{NS['r']}}}id") + if not rid: + return ChartResult(native_status="unsupported-chart-reference") + + chart_path = slide_part.resolve_rel(rid) + if not chart_path: + return ChartResult(native_status="unsupported-chart-relationship") + chart_part = pkg.load_part(chart_path) + if chart_part is None: + return ChartResult(native_status="unsupported-chart-part") + + try: + payload = _payload_from_chart_xml(chart_part.xml, xfrm) + validate_chart_payload(payload) + except _UnsupportedChart as exc: + return ChartResult(native_status=exc.status) + except RuntimeError: + return ChartResult(native_status="unsupported-chart-schema") + except (TypeError, ValueError, AttributeError): + return ChartResult(native_status="unsupported-chart-parse") + return ChartResult(native_payload=payload) + + +def _payload_from_chart_xml(chart_root: ET.Element, xfrm: Xfrm) -> dict[str, Any]: + plot_area = chart_root.find(".//c:plotArea", C_NS) + if plot_area is None: + raise _UnsupportedChart("unsupported-chart-plot") + + chart_nodes = [ + child + for child in list(plot_area) + if _local_name(child.tag).endswith("Chart") + ] + if not chart_nodes: + raise _UnsupportedChart("unsupported-chart-plot") + if len(chart_nodes) > 1: + raise _UnsupportedChart("unsupported-combo-chart") + if plot_area.find("c:dateAx", C_NS) is not None: + raise _UnsupportedChart("unsupported-date-axis") + + chart = chart_nodes[0] + chart_tag = _local_name(chart.tag) + if chart_tag in {"area3DChart", "bar3DChart", "line3DChart", "pie3DChart", "surface3DChart"}: + raise _UnsupportedChart("unsupported-3d-chart") + if chart_tag == "barChart": + payload = _category_payload(chart, _bar_chart_type(chart), xfrm) + elif chart_tag in {"areaChart", "lineChart", "ofPieChart", "pieChart", "doughnutChart"}: + chart_type = { + "areaChart": "area", + "lineChart": "line", + "ofPieChart": "of_pie", + "pieChart": "pie", + "doughnutChart": "doughnut", + }[chart_tag] + payload = _category_payload(chart, chart_type, xfrm) + elif chart_tag == "scatterChart": + payload = _xy_payload(chart, "scatter", xfrm) + elif chart_tag == "bubbleChart": + payload = _xy_payload(chart, "bubble", xfrm) + else: + raise _UnsupportedChart("unsupported-chart-type") + + _validate_chart_semantics(payload, plot_area, chart) + _apply_chart_metadata(payload, chart_root, plot_area, chart) + return payload + + +def _canonical_srgb_color(fill: ET.Element | None) -> str: + """Return an exporter-canonical solid RGB fill, or reject the style.""" + if fill is None or fill.attrib: + raise _UnsupportedChart("unsupported-chart-series-style") + children = list(fill) + if ( + len(children) != 1 + or _local_name(children[0].tag) != "srgbClr" + or set(children[0].attrib) != {"val"} + or list(children[0]) + ): + raise _UnsupportedChart("unsupported-chart-series-style") + color = children[0].attrib["val"].strip() + if len(color) != 6 or any(char not in "0123456789abcdefABCDEF" for char in color): + raise _UnsupportedChart("unsupported-chart-series-style") + return color.upper() + + +def _canonical_sp_pr_color(sp_pr: ET.Element, *, line_visible: bool) -> str: + """Extract a color only from the exact series style emitted here.""" + if sp_pr.attrib: + raise _UnsupportedChart("unsupported-chart-series-style") + children = list(sp_pr) + if [_local_name(child.tag) for child in children] != ["solidFill", "ln"]: + raise _UnsupportedChart("unsupported-chart-series-style") + color = _canonical_srgb_color(children[0]) + line = children[1] + if line.attrib or len(line) != 1: + raise _UnsupportedChart("unsupported-chart-series-style") + line_child = line[0] + if line_visible: + if _local_name(line_child.tag) != "solidFill": + raise _UnsupportedChart("unsupported-chart-series-style") + if _canonical_srgb_color(line_child) != color: + raise _UnsupportedChart("unsupported-chart-series-style") + elif ( + _local_name(line_child.tag) != "noFill" + or line_child.attrib + or list(line_child) + ): + raise _UnsupportedChart("unsupported-chart-series-style") + return color + + +def _canonical_chart_colors(payload: dict[str, Any], plot: ET.Element) -> list[str] | None: + """Recover colors from canonical series or pie data-point formatting.""" + chart_type = payload["type"] + series_nodes = plot.findall("c:ser", C_NS) + if chart_type in {"pie", "doughnut", "of_pie"}: + if any(series.find("c:spPr", C_NS) is not None for series in series_nodes): + raise _UnsupportedChart("unsupported-chart-series-style") + point_nodes = [ + point + for series in series_nodes + for point in series.findall("c:dPt", C_NS) + ] + if not point_nodes: + return None + expected_count = len(payload["categories"]) + (1 if chart_type == "of_pie" else 0) + colors: dict[int, str] = {} + for point in point_nodes: + if point.attrib or [_local_name(child.tag) for child in point] != ["idx", "spPr"]: + raise _UnsupportedChart("unsupported-chart-series-style") + idx = point.find("c:idx", C_NS) + sp_pr = point.find("c:spPr", C_NS) + try: + point_index = int(idx.attrib.get("val", "")) if idx is not None else -1 + except ValueError: + raise _UnsupportedChart("unsupported-chart-series-style") from None + if ( + idx is None + or set(idx.attrib) != {"val"} + or list(idx) + or sp_pr is None + or point_index in colors + ): + raise _UnsupportedChart("unsupported-chart-series-style") + colors[point_index] = _canonical_sp_pr_color(sp_pr, line_visible=True) + if set(colors) != set(range(expected_count)): + raise _UnsupportedChart("unsupported-chart-series-style") + return [f"#{colors[index]}" for index in range(expected_count)] + + if any(series.find("c:dPt", C_NS) is not None for series in series_nodes): + raise _UnsupportedChart("unsupported-chart-series-style") + sp_pr_nodes = [series.find("c:spPr", C_NS) for series in series_nodes] + if not any(sp_pr is not None for sp_pr in sp_pr_nodes): + return None + if any(sp_pr is None for sp_pr in sp_pr_nodes): + raise _UnsupportedChart("unsupported-chart-series-style") + line_visible = not ( + chart_type == "scatter" + and payload.get("scatter_style", "marker") == "marker" + ) + return [ + f"#{_canonical_sp_pr_color(sp_pr, line_visible=line_visible)}" + for sp_pr in sp_pr_nodes + if sp_pr is not None + ] + + +def _validate_chart_semantics( + payload: dict[str, Any], + plot_area: ET.Element, + plot: ET.Element, +) -> None: + """Reject valid chart features the compact marker cannot reproduce.""" + chart_type = payload["type"] + colors = _canonical_chart_colors(payload, plot) + if colors: + payload["style"] = {"colors": colors} + for tag in ( + "trendline", "errBars", "dropLines", "hiLowLines", "upDownBars", + ): + if plot.find(f".//c:{tag}", C_NS) is not None: + raise _UnsupportedChart("unsupported-chart-analysis-features") + if plot_area.find("c:dTable", C_NS) is not None: + raise _UnsupportedChart("unsupported-chart-data-table") + ser_lines = plot.find("c:serLines", C_NS) + if ser_lines is not None and ( + chart_type != "of_pie" or ser_lines.attrib or list(ser_lines) + ): + raise _UnsupportedChart("unsupported-chart-analysis-features") + + grouping = payload.get("grouping") + axes = plot_area.findall("c:catAx", C_NS) + plot_area.findall("c:valAx", C_NS) + for axis in axes: + axis_kind = _local_name(axis.tag) + axis_position = _element_val(axis.find("c:axPos", C_NS)) + delete = axis.find("c:delete", C_NS) + if delete is not None and ooxml_bool(delete.attrib.get("val"), True): + raise _UnsupportedChart("unsupported-chart-axis-options") + scaling = axis.find("c:scaling", C_NS) + if scaling is not None: + for tag in ("logBase", "min", "max"): + if scaling.find(f"c:{tag}", C_NS) is not None: + raise _UnsupportedChart("unsupported-chart-axis-options") + orientation = _element_val(scaling.find("c:orientation", C_NS)) + if orientation not in {None, "minMax"}: + raise _UnsupportedChart("unsupported-chart-axis-options") + for tag in ( + "majorUnit", "minorUnit", "crossesAt", "dispUnits", + "tickLblSkip", "tickMarkSkip", + ): + if axis.find(f"c:{tag}", C_NS) is not None: + raise _UnsupportedChart("unsupported-chart-axis-options") + crosses = _element_val(axis.find("c:crosses", C_NS)) + if crosses not in {None, "autoZero"}: + raise _UnsupportedChart("unsupported-chart-axis-options") + auto = axis.find("c:auto", C_NS) + if auto is not None and not ooxml_bool(auto.attrib.get("val"), True): + raise _UnsupportedChart("unsupported-chart-axis-options") + num_fmt = axis.find("c:numFmt", C_NS) + if num_fmt is not None: + format_code = num_fmt.attrib.get("formatCode", "").strip() + allowed_formats = {"", "General"} + if grouping == "percentStacked": + allowed_formats.add("0%") + if format_code not in allowed_formats: + raise _UnsupportedChart("unsupported-chart-axis-number-format") + tick_label_position = _element_val(axis.find("c:tickLblPos", C_NS)) + if payload["type"] in {"scatter", "bubble"} or axis_kind == "catAx": + if tick_label_position not in {None, "nextTo"}: + raise _UnsupportedChart("unsupported-chart-axis-options") + elif tick_label_position == "none": + payload["show_value_axis_labels"] = False + elif tick_label_position not in {None, "nextTo"}: + raise _UnsupportedChart("unsupported-chart-axis-options") + + has_major_gridlines = axis.find("c:majorGridlines", C_NS) is not None + if payload["type"] in {"scatter", "bubble"}: + expected_major_gridlines = axis_position in {"l", "r"} + else: + expected_major_gridlines = axis_kind == "valAx" + if has_major_gridlines != expected_major_gridlines: + raise _UnsupportedChart("unsupported-chart-axis-options") + if axis.find("c:minorGridlines", C_NS) is not None: + raise _UnsupportedChart("unsupported-chart-axis-options") + + if chart_type == "line": + smooth_nodes = [plot.find("c:smooth", C_NS), *plot.findall("c:ser/c:smooth", C_NS)] + if any( + node is not None and ooxml_bool(node.attrib.get("val"), True) + for node in smooth_nodes + ): + raise _UnsupportedChart("unsupported-chart-line-style") + plot_marker = plot.find("c:marker", C_NS) + plot_has_markers = bool( + plot_marker is not None + and ooxml_bool(plot_marker.attrib.get("val"), True) + ) + marker_states: set[bool] = set() + for series in plot.findall("c:ser", C_NS): + marker_node = series.find("c:marker", C_NS) + if marker_node is not None and any( + child.tag.rsplit("}", 1)[-1] != "symbol" + for child in marker_node + ): + raise _UnsupportedChart("unsupported-chart-line-style") + symbol = _element_val(series.find("c:marker/c:symbol", C_NS)) + if symbol not in {None, "circle", "none"}: + raise _UnsupportedChart("unsupported-chart-line-style") + marker_states.add(plot_has_markers if symbol is None else symbol != "none") + if len(marker_states) > 1: + raise _UnsupportedChart("unsupported-chart-line-style") + + if chart_type in {"bar", "column"}: + gap_width = _element_val(plot.find("c:gapWidth", C_NS)) + if gap_width not in {None, "150"}: + raise _UnsupportedChart("unsupported-chart-bar-options") + overlap = _element_val(plot.find("c:overlap", C_NS)) + expected_overlap = "100" if grouping in {"stacked", "percentStacked"} else "0" + if overlap is None: + if expected_overlap != "0": + raise _UnsupportedChart("unsupported-chart-bar-options") + elif overlap != expected_overlap: + raise _UnsupportedChart("unsupported-chart-bar-options") + + if chart_type == "bubble": + bubble_scale = _element_val(plot.find("c:bubbleScale", C_NS)) + if bubble_scale not in {None, "100"}: + raise _UnsupportedChart("unsupported-chart-bubble-options") + show_negative = plot.find("c:showNegBubbles", C_NS) + if show_negative is not None and ooxml_bool( + show_negative.attrib.get("val"), + True, + ): + raise _UnsupportedChart("unsupported-chart-bubble-options") + size_represents = _element_val(plot.find("c:sizeRepresents", C_NS)) + if size_represents not in {None, "area"}: + raise _UnsupportedChart("unsupported-chart-bubble-options") + bubble_3d_nodes = [ + plot.find("c:bubble3D", C_NS), + *plot.findall("c:ser/c:bubble3D", C_NS), + ] + if any( + node is not None and ooxml_bool(node.attrib.get("val"), True) + for node in bubble_3d_nodes + ): + raise _UnsupportedChart("unsupported-chart-bubble-options") + + if chart_type == "scatter": + scatter_style = _element_val(plot.find("c:scatterStyle", C_NS)) or "marker" + expected_marker = ( + "circle" + if scatter_style in {"lineMarker", "marker", "smoothMarker"} + else "none" + ) + expected_smooth = scatter_style in {"smooth", "smoothMarker"} + for series in plot.findall("c:ser", C_NS): + marker = series.find("c:marker", C_NS) + if marker is None: + raise _UnsupportedChart("unsupported-chart-scatter-style") + if any( + child.tag.rsplit("}", 1)[-1] != "symbol" + for child in marker + ): + raise _UnsupportedChart("unsupported-chart-scatter-style") + symbol = _element_val(marker.find("c:symbol", C_NS)) + if symbol != expected_marker: + raise _UnsupportedChart("unsupported-chart-scatter-style") + smooth = series.find("c:smooth", C_NS) + actual_smooth = bool( + smooth is not None and ooxml_bool(smooth.attrib.get("val"), True) + ) + if actual_smooth != expected_smooth: + raise _UnsupportedChart("unsupported-chart-scatter-style") + + if chart_type in {"pie", "doughnut", "of_pie"}: + for explosion in plot.findall(".//c:explosion", C_NS): + if _element_val(explosion) not in {None, "0"}: + raise _UnsupportedChart("unsupported-chart-pie-options") + first_slice = _element_val(plot.find("c:firstSliceAng", C_NS)) + if first_slice not in {None, "0"}: + raise _UnsupportedChart("unsupported-chart-pie-options") + if chart_type == "doughnut": + hole_size = _element_val(plot.find("c:holeSize", C_NS)) + if hole_size != "75": + raise _UnsupportedChart("unsupported-chart-doughnut-options") + if chart_type == "of_pie": + for tag in ("splitType", "splitPos", "custSplit"): + if plot.find(f"c:{tag}", C_NS) is not None: + raise _UnsupportedChart("unsupported-chart-of-pie-options") + gap_width = _element_val(plot.find("c:gapWidth", C_NS)) + if gap_width != "100": + raise _UnsupportedChart("unsupported-chart-of-pie-options") + second_size = _element_val(plot.find("c:secondPieSize", C_NS)) + if second_size not in {None, "75"}: + raise _UnsupportedChart("unsupported-chart-of-pie-options") + + +def _apply_chart_metadata( + payload: dict[str, Any], + chart_root: ET.Element, + plot_area: ET.Element, + plot: ET.Element, +) -> None: + """Copy visible classic-chart chrome supported by the native schema.""" + chart = chart_root.find("c:chart", C_NS) + if chart is None: + return + + title_element = chart.find("c:title", C_NS) + overlay = title_element.find("c:overlay", C_NS) if title_element is not None else None + if overlay is not None and ooxml_bool(overlay.attrib.get("val"), True): + raise _UnsupportedChart("unsupported-chart-title-overlay") + title_entries = _canonical_title_entries(title_element) + if ( + title_element is not None + and title_element.find("c:tx/c:rich", C_NS) is not None + and title_entries is None + ): + raise _UnsupportedChart("unsupported-chart-title-format") + title = _chart_text(title_element) + if title: + if title_entries is not None: + payload["title"] = title_entries[0] + if len(title_entries) == 2: + payload["subtitle"] = title_entries[1] + else: + payload["title"] = title + + legend = chart.find("c:legend", C_NS) + if legend is not None: + delete = legend.find("c:delete", C_NS) + if delete is None or not ooxml_bool(delete.attrib.get("val"), True): + if legend.find("c:legendEntry", C_NS) is not None: + raise _UnsupportedChart("unsupported-chart-legend-filter") + overlay = legend.find("c:overlay", C_NS) + if overlay is not None and ooxml_bool(overlay.attrib.get("val"), True): + raise _UnsupportedChart("unsupported-chart-legend-overlay") + position = _element_val(legend.find("c:legendPos", C_NS)) or "r" + if position not in {"b", "l", "r", "t"}: + raise _UnsupportedChart("unsupported-chart-legend-position") + payload["show_legend"] = True + payload["legend_position"] = position + + if plot.find("c:ser/c:dLbls", C_NS) is not None: + raise _UnsupportedChart("unsupported-chart-series-data-labels") + data_labels = _data_labels_payload(plot.find("c:dLbls", C_NS)) + if data_labels: + if payload["type"] not in {"area", "bar", "column", "line"}: + raise _UnsupportedChart("unsupported-chart-data-labels") + try: + validate_data_label_position( + data_labels.get("position"), + payload["type"], + payload.get("grouping"), + ) + except RuntimeError: + raise _UnsupportedChart("unsupported-chart-data-labels") from None + payload["data_labels"] = data_labels + + axis_titles: dict[str, str] = {} + category_titles = [ + text + for axis in plot_area.findall("c:catAx", C_NS) + if (text := _chart_text(axis.find("c:title", C_NS))) + ] + value_titles = [ + text + for axis in plot_area.findall("c:valAx", C_NS) + if (text := _chart_text(axis.find("c:title", C_NS))) + ] + if payload["type"] in {"scatter", "bubble"}: + if category_titles: + raise _UnsupportedChart("unsupported-chart-axis-titles") + titled_value_axes = [ + ( + _element_val(axis.find("c:axPos", C_NS)), + _chart_text(axis.find("c:title", C_NS)), + ) + for axis in plot_area.findall("c:valAx", C_NS) + ] + for position, text in titled_value_axes: + if not text: + continue + key = "x" if position in {"b", "t"} else "y" + if key in axis_titles: + raise _UnsupportedChart("unsupported-chart-axis-titles") + axis_titles[key] = text + else: + if len(category_titles) > 1 or len(value_titles) > 1: + raise _UnsupportedChart("unsupported-chart-axis-titles") + if category_titles: + axis_titles["category"] = category_titles[0] + if value_titles: + axis_titles["value"] = value_titles[0] + if axis_titles: + payload["axis_titles"] = axis_titles + + +def _chart_text(container: ET.Element | None) -> str: + if container is None: + return "" + paragraphs: list[str] = [] + for paragraph in container.findall(".//a:p", C_NS): + text = "".join(node.text or "" for node in paragraph.findall(".//a:t", C_NS)) + if text: + paragraphs.append(text) + if paragraphs: + return "\n".join(paragraphs) + values = [node.text or "" for node in container.findall(".//c:v", C_NS)] + return "".join(values) + + +def _canonical_title_paragraph(paragraph: ET.Element) -> dict[str, Any] | None: + """Return one exporter-canonical or basic Office title paragraph.""" + if paragraph.attrib or [_local_name(child.tag) for child in paragraph] != ["r"]: + return None + run = paragraph.find("a:r", C_NS) + if run is None or run.attrib: + return None + run_child_names = [_local_name(child.tag) for child in run] + if run_child_names == ["t"]: + text = run.find("a:t", C_NS) + if ( + text is None + or text.attrib + or list(text) + or not (text.text or "") + or text.text != text.text.strip() + ): + return None + return {"text": text.text} + if run_child_names != ["rPr", "t"]: + return None + run_props = run.find("a:rPr", C_NS) + text = run.find("a:t", C_NS) + if ( + run_props is None + or set(run_props.attrib) != {"lang", "sz"} + or text is None + or text.attrib + or list(text) + or not (text.text or "") + or text.text != text.text.strip() + ): + return None + size_token = run_props.attrib["sz"] + if re.fullmatch(r"[0-9]+", size_token) is None: + return None + size = int(size_token) + if size % 10 != 0 or not 100 <= size <= 400000: + return None + child_names = [_local_name(child.tag) for child in run_props] + if child_names not in ([], ["solidFill"], ["latin", "ea"], ["solidFill", "latin", "ea"]): + return None + solid_fill = run_props.find("a:solidFill", C_NS) + color = None + if solid_fill is not None: + try: + color = _canonical_srgb_color(solid_fill) + except _UnsupportedChart: + return None + latin = run_props.find("a:latin", C_NS) + east_asian = run_props.find("a:ea", C_NS) + if (latin is None) != (east_asian is None): + return None + for font in (latin, east_asian): + if font is not None and ( + set(font.attrib) != {"typeface"} + or not font.attrib["typeface"].strip() + or list(font) + ): + return None + entry: dict[str, Any] = { + "text": text.text, + "font_size": _round_payload_number(size / 75.0), + } + if color is not None: + entry["color"] = f"#{color}" + if latin is not None and east_asian is not None: + latin_name = latin.attrib["typeface"] + east_asian_name = east_asian.attrib["typeface"] + font_family = ( + latin_name + if latin_name == east_asian_name + else f"{latin_name}, {east_asian_name}" + ) + resolved_fonts = parse_font_family(font_family) + if ( + resolved_fonts["latin"] != latin_name + or resolved_fonts["ea"] != east_asian_name + ): + return None + entry["font_family"] = font_family + return entry + + +def _canonical_title_entries(title: ET.Element | None) -> list[dict[str, Any]] | None: + """Recognize exporter-canonical or basic Office rich title structure.""" + if title is None: + return None + title_child_names = [_local_name(child.tag) for child in title] + if title_child_names not in (["tx", "layout"], ["tx", "layout", "overlay"]): + return None + overlay = title.find("c:overlay", C_NS) + if overlay is not None and ( + set(overlay.attrib) != {"val"} + or list(overlay) + or ooxml_bool(overlay.attrib.get("val"), True) + ): + return None + tx = title.find("c:tx", C_NS) + layout = title.find("c:layout", C_NS) + rich = title.find("c:tx/c:rich", C_NS) + if ( + tx is None + or tx.attrib + or [_local_name(child.tag) for child in tx] != ["rich"] + or layout is None + or layout.attrib + or list(layout) + or rich is None + or rich.attrib + ): + return None + children = list(rich) + child_names = [_local_name(child.tag) for child in children] + if child_names not in ( + ["bodyPr", "lstStyle", "p"], + ["bodyPr", "lstStyle", "p", "p"], + ): + return None + if children[0].attrib or list(children[0]) or children[1].attrib or list(children[1]): + return None + entries = [_canonical_title_paragraph(paragraph) for paragraph in children[2:]] + if any(entry is None for entry in entries): + return None + return [entry for entry in entries if entry is not None] + + +def _data_label_text_style(tx_pr: ET.Element) -> dict[str, Any]: + """Extract the subset of label text properties emitted by this exporter.""" + body_pr = tx_pr.find("a:bodyPr", C_NS) + list_style = tx_pr.find("a:lstStyle", C_NS) + if body_pr is None or body_pr.attrib or list(body_pr): + raise _UnsupportedChart("unsupported-chart-data-labels") + if list_style is None or list_style.attrib or list(list_style): + raise _UnsupportedChart("unsupported-chart-data-labels") + paragraphs = tx_pr.findall("a:p", C_NS) + if len(paragraphs) != 1: + raise _UnsupportedChart("unsupported-chart-data-labels") + paragraph = paragraphs[0] + if any( + child.tag.rsplit("}", 1)[-1] not in {"pPr", "endParaRPr"} + for child in paragraph + ): + raise _UnsupportedChart("unsupported-chart-data-labels") + p_pr = paragraph.find("a:pPr", C_NS) + if p_pr is None or p_pr.attrib: + raise _UnsupportedChart("unsupported-chart-data-labels") + if any( + child.tag.rsplit("}", 1)[-1] != "defRPr" + for child in p_pr + ): + raise _UnsupportedChart("unsupported-chart-data-labels") + end_r_pr = paragraph.find("a:endParaRPr", C_NS) + if end_r_pr is not None and ( + any(name not in {"lang", "altLang"} for name in end_r_pr.attrib) + or list(end_r_pr) + ): + raise _UnsupportedChart("unsupported-chart-data-labels") + + r_pr = p_pr.find("a:defRPr", C_NS) + if r_pr is None: + return {} + allowed_attrs = {"sz", "b"} + if any(name not in allowed_attrs for name in r_pr.attrib): + raise _UnsupportedChart("unsupported-chart-data-labels") + allowed_children = {"solidFill", "latin", "ea"} + if any( + child.tag.rsplit("}", 1)[-1] not in allowed_children + for child in r_pr + ): + raise _UnsupportedChart("unsupported-chart-data-labels") + + style: dict[str, Any] = {} + raw_size = r_pr.attrib.get("sz") + if raw_size is not None: + try: + size_px = float(raw_size) / 75.0 + except ValueError: + raise _UnsupportedChart("unsupported-chart-data-labels") from None + if size_px <= 0 or not math.isfinite(size_px): + raise _UnsupportedChart("unsupported-chart-data-labels") + style["font_size"] = int(size_px) if size_px.is_integer() else round(size_px, 3) + if r_pr.attrib.get("b") is not None: + style["bold"] = ooxml_bool(r_pr.attrib.get("b"), True) + + solid_fill = r_pr.find("a:solidFill", C_NS) + if solid_fill is not None: + color_children = list(solid_fill) + if ( + len(color_children) != 1 + or color_children[0].tag.rsplit("}", 1)[-1] != "srgbClr" + or list(color_children[0]) + ): + raise _UnsupportedChart("unsupported-chart-data-labels") + color = color_children[0].attrib.get("val", "").strip() + if len(color) != 6 or any(char not in "0123456789abcdefABCDEF" for char in color): + raise _UnsupportedChart("unsupported-chart-data-labels") + style["color"] = f"#{color.upper()}" + + latin = r_pr.find("a:latin", C_NS) + east_asian = r_pr.find("a:ea", C_NS) + latin_face = latin.attrib.get("typeface", "").strip() if latin is not None else "" + east_asian_face = ( + east_asian.attrib.get("typeface", "").strip() + if east_asian is not None else "" + ) + font_face = ( + f"{latin_face}, {east_asian_face}" + if latin_face and east_asian_face and latin_face != east_asian_face + else latin_face or east_asian_face + ) + if font_face: + style["font_family"] = font_face + return style + + +def _data_labels_payload(dlabels: ET.Element | None) -> dict[str, Any] | None: + if dlabels is None: + return None + if dlabels.find("c:dLbl", C_NS) is not None: + raise _UnsupportedChart("unsupported-chart-point-labels") + allowed_children = { + "numFmt", "txPr", "dLblPos", "showLegendKey", "showVal", + "showCatName", "showSerName", "showPercent", "showBubbleSize", + "showLeaderLines", + } + if any( + child.tag.rsplit("}", 1)[-1] not in allowed_children + for child in dlabels + ): + raise _UnsupportedChart("unsupported-chart-data-labels") + for tag in ("showLegendKey", "showBubbleSize"): + elem = dlabels.find(f"c:{tag}", C_NS) + if elem is not None and ooxml_bool(elem.attrib.get("val"), True): + raise _UnsupportedChart("unsupported-chart-data-labels") + + config: dict[str, Any] = {} + for tag, field in ( + ("showVal", "show_value"), + ("showCatName", "show_category"), + ("showSerName", "show_series"), + ("showPercent", "show_percent"), + ): + elem = dlabels.find(f"c:{tag}", C_NS) + config[field] = ( + ooxml_bool(elem.attrib.get("val"), True) + if elem is not None else False + ) + if not any(config.values()): + return None + + leader_lines = dlabels.find("c:showLeaderLines", C_NS) + if leader_lines is not None: + config["show_leader_lines"] = ooxml_bool( + leader_lines.attrib.get("val"), + True, + ) + + position = _element_val(dlabels.find("c:dLblPos", C_NS)) + if position: + position_aliases = { + "bestFit": "best_fit", + "ctr": "center", + "inBase": "inside_base", + "inEnd": "inside_end", + "outEnd": "outside_end", + "t": "above", + } + normalized_position = position_aliases.get(position) + if normalized_position is None: + raise _UnsupportedChart("unsupported-chart-data-labels") + config["position"] = normalized_position + num_fmt = dlabels.find("c:numFmt", C_NS) + if num_fmt is not None and num_fmt.attrib.get("formatCode"): + config["number_format"] = num_fmt.attrib["formatCode"] + tx_pr = dlabels.find("c:txPr", C_NS) + if tx_pr is not None: + config.update(_data_label_text_style(tx_pr)) + return config + + +def _category_payload(chart: ET.Element, chart_type: str, xfrm: Xfrm) -> dict[str, Any]: + series_nodes = chart.findall("c:ser", C_NS) + if not series_nodes: + raise _UnsupportedChart("unsupported-chart-cache") + + categories = _category_values(series_nodes[0].find("c:cat", C_NS)) + if not categories: + raise _UnsupportedChart("unsupported-chart-cache") + + series: list[dict[str, Any]] = [] + for idx, ser in enumerate(series_nodes, start=1): + if _category_values(ser.find("c:cat", C_NS)) != categories: + raise _UnsupportedChart("unsupported-chart-cache") + values = _numeric_values(ser.find("c:val", C_NS)) + if not values or len(values) != len(categories): + raise _UnsupportedChart("unsupported-chart-cache") + series.append({ + "name": _series_name(ser, idx), + "values": values, + }) + + payload: dict[str, Any] = { + **_bounds_payload(xfrm), + "categories": categories, + "series": series, + "type": chart_type, + } + grouping = _element_val(chart.find("c:grouping", C_NS)) + if grouping and chart_type in {"area", "bar", "column", "line"}: + payload["grouping"] = grouping + if chart_type == "line": + payload["line_style"] = _line_style(chart, series_nodes) + if chart_type == "of_pie": + payload["of_pie_type"] = _element_val(chart.find("c:ofPieType", C_NS)) or "pie" + return payload + + +def _xy_payload(chart: ET.Element, chart_type: str, xfrm: Xfrm) -> dict[str, Any]: + series_nodes = chart.findall("c:ser", C_NS) + if not series_nodes: + raise _UnsupportedChart("unsupported-chart-cache") + + series: list[dict[str, Any]] = [] + for idx, ser in enumerate(series_nodes, start=1): + x_values = _numeric_values(ser.find("c:xVal", C_NS)) + y_values = _numeric_values(ser.find("c:yVal", C_NS)) + if not x_values or len(x_values) != len(y_values): + raise _UnsupportedChart("unsupported-chart-cache") + item: dict[str, Any] = { + "name": _series_name(ser, idx), + "x": x_values, + "y": y_values, + } + if chart_type == "bubble": + sizes = _numeric_values(ser.find("c:bubbleSize", C_NS)) + if len(sizes) != len(x_values): + raise _UnsupportedChart("unsupported-chart-cache") + item["sizes"] = sizes + series.append(item) + + payload: dict[str, Any] = { + **_bounds_payload(xfrm), + "series": series, + "type": chart_type, + } + if chart_type == "scatter": + style = _element_val(chart.find("c:scatterStyle", C_NS)) + if style: + payload["scatter_style"] = style + return payload + + +def _bar_chart_type(chart: ET.Element) -> str: + return "bar" if _element_val(chart.find("c:barDir", C_NS)) == "bar" else "column" + + +def _line_style(chart: ET.Element, series_nodes: list[ET.Element]) -> str: + symbols = [ + _element_val(ser.find("c:marker/c:symbol", C_NS)) + for ser in series_nodes + ] + if symbols and all(symbol == "none" for symbol in symbols): + return "line" + if any(symbol not in {None, "none"} for symbol in symbols): + return "lineMarker" + marker = chart.find("c:marker", C_NS) + if marker is not None and ooxml_bool(marker.attrib.get("val"), True): + return "lineMarker" + return "line" + + +def _category_values(cat: ET.Element | None) -> list[str]: + cache = _first_cache(cat, ("strCache", "strLit")) + if cache is not None: + return [str(value) for value in _cache_point_values(cache)] + cache = _first_cache(cat, ("numCache", "numLit")) + if cache is None: + return [] + format_code = cache.findtext("c:formatCode", default="", namespaces=C_NS).strip() + if format_code and format_code.lower() != "general": + raise _UnsupportedChart("unsupported-formatted-category-cache") + numbers = _numeric_cache_values(cache) + return [str(value) for value in numbers] + + +def _series_name(ser: ET.Element, index: int) -> str: + tx = ser.find("c:tx", C_NS) + values = _text_cache_values(tx) + if values: + return values[0] + direct = tx.findtext("c:v", default="", namespaces=C_NS) if tx is not None else "" + return direct or f"Series {index}" + + +def _text_cache_values(parent: ET.Element | None) -> list[str]: + cache = _first_cache(parent, ("strCache", "strLit")) + if cache is not None: + return [str(value) for value in _cache_point_values(cache)] + cache = _first_cache(parent, ("numCache", "numLit")) + return [str(value) for value in _cache_point_values(cache)] + + +def _numeric_values(parent: ET.Element | None) -> list[int | float]: + cache = _first_cache(parent, ("numCache", "numLit")) + if cache is None: + return [] + return _numeric_cache_values(cache) + + +def _numeric_cache_values(cache: ET.Element) -> list[int | float]: + values: list[int | float] = [] + for value in _cache_point_values(cache): + number = float(value) + if not math.isfinite(number): + raise _UnsupportedChart("unsupported-chart-cache") + values.append(int(number) if number.is_integer() else number) + return values + + +def _first_cache(parent: ET.Element | None, names: tuple[str, ...]) -> ET.Element | None: + if parent is None: + return None + for name in names: + found = parent.find(f".//c:{name}", C_NS) + if found is not None: + return found + return None + + +def _cache_point_values(cache: ET.Element | None) -> list[str]: + if cache is None: + return [] + points: dict[int, str] = {} + for idx, point in enumerate(cache.findall("c:pt", C_NS)): + raw_idx = point.attrib.get("idx") + try: + point_idx = int(raw_idx) if raw_idx is not None else idx + except ValueError: + raise _UnsupportedChart("unsupported-chart-cache") + if point_idx < 0 or point_idx in points: + raise _UnsupportedChart("unsupported-chart-cache") + value = point.findtext("c:v", default="", namespaces=C_NS) + points[point_idx] = value + + count_elem = cache.find("c:ptCount", C_NS) + if count_elem is not None: + try: + point_count = int(count_elem.attrib.get("val", "")) + except ValueError: + raise _UnsupportedChart("unsupported-chart-cache") + else: + point_count = len(points) + if ( + point_count < 0 + or point_count != len(points) + or any(idx not in points for idx in range(point_count)) + ): + raise _UnsupportedChart("unsupported-chart-cache") + return [points[idx] for idx in range(point_count)] + + +def _element_val(elem: ET.Element | None) -> str | None: + if elem is None: + return None + return elem.attrib.get("val") + + +def _bounds_payload(xfrm: Xfrm) -> dict[str, int | float]: + return { + "height": _round_payload_number(xfrm.h), + "width": _round_payload_number(xfrm.w), + "x": _round_payload_number(xfrm.x), + "y": _round_payload_number(xfrm.y), + } + + +def _round_payload_number(value: float) -> int | float: + rounded = round(float(value), 3) + return int(rounded) if rounded.is_integer() else rounded + + +def _local_name(tag: str) -> str: + return tag.rsplit("}", 1)[-1] if "}" in tag else tag diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/emu_units.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/emu_units.py index dd146181..af28850f 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/emu_units.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/emu_units.py @@ -93,6 +93,18 @@ def percent_to_ratio(val: float | int | str | None, default: float = 0.0) -> flo return default +def ooxml_bool(value: str | None, default: bool = False) -> bool: + """Parse the boolean lexical forms accepted by OOXML.""" + if value is None: + return default + normalized = value.strip().lower() + if normalized in {"1", "true", "on"}: + return True + if normalized in {"0", "false", "off"}: + return False + return default + + # --------------------------------------------------------------------------- # xfrm / transform parsing # --------------------------------------------------------------------------- @@ -179,8 +191,8 @@ def parse_xfrm(xfrm_elem: ET.Element | None) -> Xfrm: return Xfrm() rot = angle_to_deg(xfrm_elem.get("rot")) - flip_h = xfrm_elem.get("flipH") == "1" - flip_v = xfrm_elem.get("flipV") == "1" + flip_h = ooxml_bool(xfrm_elem.get("flipH")) + flip_v = ooxml_bool(xfrm_elem.get("flipV")) off = xfrm_elem.find("a:off", NS) ext = xfrm_elem.find("a:ext", NS) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/ooxml_loader.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/ooxml_loader.py index a18fa3ee..e3de69f3 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/ooxml_loader.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/ooxml_loader.py @@ -8,6 +8,7 @@ accessible for downstream shape conversion. from __future__ import annotations import posixpath +import re import zipfile from dataclasses import dataclass, field from pathlib import Path, PurePosixPath @@ -57,8 +58,8 @@ def _parse_rels(zf: zipfile.ZipFile, rels_path: str) -> dict[str, dict[str, str] return {} try: root = ET.fromstring(zf.read(rels_path)) - except ET.ParseError: - return {} + except ET.ParseError as exc: + raise RuntimeError(f"Invalid relationships XML in {rels_path}: {exc}") from exc base = rels_path.replace("/_rels/", "/") # source part path base = base[:-len(".rels")] # strip .rels suffix @@ -82,8 +83,8 @@ def _load_xml(zf: zipfile.ZipFile, part_path: str) -> ET.Element | None: return None try: return ET.fromstring(zf.read(part_path)) - except ET.ParseError: - return None + except ET.ParseError as exc: + raise RuntimeError(f"Invalid OOXML part {part_path}: {exc}") from exc # --------------------------------------------------------------------------- @@ -150,6 +151,7 @@ class OoxmlPackage: self.presentation: PartRef | None = None self.slide_size_px: tuple[float, float] = (1280.0, 720.0) self.slide_size_emu: tuple[int, int] = (12192000, 6858000) + self.first_slide_number: int = 1 self._slides: list[SlideRef] = [] self._layouts: dict[str, PartRef] = {} self._masters: dict[str, PartRef] = {} @@ -186,6 +188,10 @@ class OoxmlPackage: rels = _parse_rels(self.zip, _rels_path_for(part_path)) return PartRef(path=part_path, xml=xml, rels=rels) + def load_part(self, part_path: str) -> PartRef | None: + """Load an arbitrary XML part by package path.""" + return self._load_part(part_path) + def read_media(self, part_path: str) -> bytes | None: """Return raw bytes of an embedded media part (e.g. ppt/media/image1.png).""" assert self.zip is not None @@ -215,6 +221,25 @@ class OoxmlPackage: if self.presentation is None: raise RuntimeError(f"presentation.xml missing in {self.path}") + raw_first_slide_number = self.presentation.xml.attrib.get("firstSlideNum") + if raw_first_slide_number is not None: + first_slide_token = raw_first_slide_number.strip(" \t\r\n") + if re.fullmatch(r"[+-]?[0-9]+", first_slide_token) is None: + raise RuntimeError( + f"Invalid presentation firstSlideNum: {raw_first_slide_number!r}" + ) + digits = first_slide_token.lstrip("+-").lstrip("0") or "0" + if len(digits) > 10: + raise RuntimeError("Presentation firstSlideNum is outside xsd:int") + first_slide_number = int(digits) + if first_slide_token.startswith("-"): + first_slide_number = -first_slide_number + if not -(2**31) <= first_slide_number <= 2**31 - 1: + raise RuntimeError( + f"Presentation firstSlideNum is outside xsd:int: {first_slide_number}" + ) + self.first_slide_number = first_slide_number + # slide size size = self.presentation.xml.find("p:sldSz", NS) if size is not None: @@ -372,4 +397,3 @@ class OoxmlPackage: continue self._layouts[target] = cached yield cached, master - diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/shape_walker.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/shape_walker.py index 97f92de9..79600e26 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/shape_walker.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/shape_walker.py @@ -10,6 +10,8 @@ Handles: - -> GROUP (recurses; resolves a:chOff/a:chExt frame) - -> GRAPHIC (table / chart / SmartArt — emitted as opaque placeholder for v1 so callers can decide a fallback) +- -> supported Choice shape with the baked Fallback + preview retained for graphic frames """ from __future__ import annotations @@ -17,7 +19,7 @@ from __future__ import annotations from dataclasses import dataclass, field from xml.etree import ElementTree as ET -from .emu_units import NS, Xfrm, parse_xfrm +from .emu_units import NS, Xfrm, ooxml_bool, parse_xfrm # --------------------------------------------------------------------------- @@ -84,7 +86,7 @@ def _read_nv_sp_pr(parent: ET.Element, nv_tag: str) -> tuple[str, str, bool, Pla if cnv is not None: name = cnv.attrib.get("name", "") spid = cnv.attrib.get("id", "") - if cnv.attrib.get("hidden") == "1": + if ooxml_bool(cnv.attrib.get("hidden")): hidden = True nv_pr = container.find("p:nvPr", NS) @@ -160,6 +162,44 @@ _KIND_MAP = { } +def _first_shape_child(container: ET.Element | None) -> ET.Element | None: + if container is None: + return None + for child in list(container): + if not isinstance(child.tag, str): + continue + if child.tag.split("}", 1)[-1] in _KIND_MAP: + return child + return None + + +def _resolve_alternate_content(wrapper: ET.Element) -> ET.Element | None: + """Select an AlternateContent shape while retaining its baked preview.""" + choice = wrapper.find("mc:Choice", NS) + fallback = wrapper.find("mc:Fallback", NS) + selected = _first_shape_child(choice) + selected_from_choice = selected is not None + if selected is None: + selected = _first_shape_child(fallback) + if selected is None: + return None + + clone = ET.fromstring(ET.tostring(selected, encoding="utf-8")) + if ( + selected_from_choice + and clone.tag.split("}", 1)[-1] == "graphicFrame" + and fallback is not None + ): + graphic_data = clone.find("a:graphic/a:graphicData", NS) + if graphic_data is not None: + preview = ET.Element(f"{{{NS['mc']}}}AlternateContent") + preview.append( + ET.fromstring(ET.tostring(fallback, encoding="utf-8")) + ) + graphic_data.append(preview) + return clone + + def _walk_container( container: ET.Element, parent_group_xfrm: Xfrm | None, @@ -176,6 +216,12 @@ def _walk_container( if not isinstance(child.tag, str): continue local = child.tag.split("}", 1)[-1] + if local == "AlternateContent": + resolved = _resolve_alternate_content(child) + if resolved is None: + continue + child = resolved + local = child.tag.split("}", 1)[-1] kind_info = _KIND_MAP.get(local) if kind_info is None: continue diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/slide_to_svg.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/slide_to_svg.py index 10696ed6..ac56ea67 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/slide_to_svg.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/slide_to_svg.py @@ -22,10 +22,12 @@ animation anchor. from __future__ import annotations +import json from dataclasses import dataclass, field from xml.etree import ElementTree as ET from .color_resolver import ColorPalette, find_color_elem, resolve_color +from .chart_to_svg import CHART_URI, CHARTEX_URI, extract_native_chart_payload from .custgeom_to_svg import convert_custom_geom from .effect_to_svg import convert_effects from .emu_units import NS, Xfrm, fmt_num @@ -59,6 +61,7 @@ class AssemblyContext: palette: ColorPalette | None pkg: OoxmlPackage slide_part: PartRef + slide_number: int | None = None theme_fonts: dict[str, str] = field(default_factory=dict) media_subdir: str = "assets" embed_images: bool = False @@ -110,6 +113,7 @@ def assemble_slide( palette=palette, pkg=pkg, slide_part=slide.part, + slide_number=pkg.first_slide_number + slide.index - 1, theme_fonts=theme_fonts or {}, media_subdir=media_subdir, embed_images=embed_images, @@ -321,6 +325,7 @@ def _convert_shape(node: ShapeNode, ctx: AssemblyContext, *, top_level: bool) -> text_result = convert_vertical_txbody( tx_body, node.xfrm, ctx.palette, theme_fonts=ctx.theme_fonts, + slide_number=ctx.slide_number, default_fill=text_default_fill, default_font_size_px=DEFAULT_FONT_SIZE_PX, fallback_lst_styles=node.inherited_lst_styles, @@ -331,6 +336,7 @@ def _convert_shape(node: ShapeNode, ctx: AssemblyContext, *, top_level: bool) -> text_result = convert_txbody( tx_body, node.xfrm, ctx.palette, theme_fonts=ctx.theme_fonts, + slide_number=ctx.slide_number, default_fill=text_default_fill, default_font_size_px=DEFAULT_FONT_SIZE_PX, fallback_lst_styles=node.inherited_lst_styles, @@ -579,17 +585,35 @@ def _convert_graphic_fallback(node: ShapeNode, ctx: AssemblyContext, - ``...presentationml/2006/ole`` → render the ``mc:Fallback`` preview bitmap that PowerPoint bakes alongside every embedded OLE object. Visually identical to what PowerPoint shows for an unedited embed. - - everything else (chart / SmartArt / diagram) → labelled bounding - rectangle so the slide composition is preserved even though the inner - content can't be drawn yet. + - supported classic charts → baked preview plus native chart metadata. + - everything else (SmartArt / diagram / unsupported chart) → labelled + preview or bounding rectangle plus transparent unsupported metadata. """ graphic_data = node.xml.find("a:graphic/a:graphicData", NS) uri = graphic_data.attrib.get("uri", "graphicFrame") if graphic_data is not None else "graphicFrame" if uri == "http://schemas.openxmlformats.org/drawingml/2006/table": - rendered = _render_graphic_table(node, ctx, graphic_data) + rendered, native_attrs = _render_graphic_table(node, ctx, graphic_data) if rendered: - return _wrap_shape_group(rendered, node, ctx, top_level=top_level) + return _wrap_shape_group( + rendered, + node, + ctx, + top_level=top_level, + extra_attrs=native_attrs, + ) + + chart_native_attrs: list[str] = [] + if uri in {CHART_URI, CHARTEX_URI}: + rendered, chart_native_attrs = _render_graphic_chart(node, ctx, graphic_data) + if rendered: + return _wrap_shape_group( + rendered, + node, + ctx, + top_level=top_level, + extra_attrs=chart_native_attrs, + ) if uri == "http://schemas.openxmlformats.org/presentationml/2006/ole" and ctx.render_graphic_previews: rendered = _render_graphic_preview(node, ctx) @@ -613,7 +637,9 @@ def _convert_graphic_fallback(node: ShapeNode, ctx: AssemblyContext, f'text-anchor="middle" font-size="14" fill="#999999">' f"[{_xml_escape(label)}]" ) - return _wrap_shape_group(placeholder, node, ctx, top_level=top_level) + return _wrap_shape_group( + placeholder, node, ctx, top_level=top_level, extra_attrs=chart_native_attrs, + ) def _graphic_preview_label(node: ShapeNode, label: str) -> str: @@ -626,24 +652,83 @@ def _graphic_preview_label(node: ShapeNode, label: str) -> str: ) -def _render_graphic_table(node: ShapeNode, ctx: AssemblyContext, - graphic_data: ET.Element | None) -> str: - """Convert the child of a graphicFrame to SVG, or return ''.""" +def _render_graphic_table( + node: ShapeNode, + ctx: AssemblyContext, + graphic_data: ET.Element | None, +) -> tuple[str, list[str]]: + """Convert the child of a graphicFrame to SVG plus metadata.""" if graphic_data is None: - return "" + return "", [] tbl = graphic_data.find("a:tbl", NS) if tbl is None: - return "" + return "", [] result = convert_tbl( tbl, node.xfrm, ctx.palette, theme_fonts=ctx.theme_fonts, + slide_number=ctx.slide_number, id_prefix=f"tbl{ctx.shape_seq[0]}", grad_seq=ctx.grad_seq, marker_seq=ctx.marker_seq, ) if result.defs: ctx.defs.extend(result.defs) - return result.svg + native_attrs: list[str] = ['data-pptx-native-source="pptx"'] + if result.native_payload: + if node.name and not result.native_payload.get("name"): + result.native_payload["name"] = node.name + payload_json = json.dumps( + result.native_payload, + allow_nan=False, + ensure_ascii=False, + separators=(",", ":"), + ) + native_attrs.extend([ + 'data-pptx-native="table"', + f'data-pptx-json="{_xml_escape(payload_json)}"', + ]) + elif result.native_status: + native_attrs.append( + f'data-pptx-native-status="{_xml_escape(result.native_status)}"' + ) + return result.svg, native_attrs + + +def _render_graphic_chart( + node: ShapeNode, + ctx: AssemblyContext, + graphic_data: ET.Element | None, +) -> tuple[str, list[str]]: + """Return a chart preview plus native chart marker attributes.""" + result = extract_native_chart_payload( + graphic_data, + node.xfrm, + ctx.slide_part, + ctx.pkg, + ) + native_attrs: list[str] = ['data-pptx-native-source="pptx"'] + if result.native_payload: + if node.name and not result.native_payload.get("name"): + result.native_payload["name"] = node.name + payload_json = json.dumps( + result.native_payload, + allow_nan=False, + ensure_ascii=False, + separators=(",", ":"), + ) + native_attrs.extend([ + 'data-pptx-native="chart"', + f'data-pptx-json="{_xml_escape(payload_json)}"', + ]) + elif result.native_status: + native_attrs.append( + f'data-pptx-native-status="{_xml_escape(result.native_status)}"' + ) + + rendered = "" + if ctx.render_graphic_previews: + rendered = _render_graphic_preview(node, ctx) + return rendered, native_attrs def _render_graphic_preview(node: ShapeNode, ctx: AssemblyContext) -> str: @@ -889,8 +974,14 @@ def _convert_placeholder_guide(node: ShapeNode, ctx: AssemblyContext, # Wrap / utilities # --------------------------------------------------------------------------- -def _wrap_shape_group(inner: str, node: ShapeNode, ctx: AssemblyContext, - *, top_level: bool) -> str: +def _wrap_shape_group( + inner: str, + node: ShapeNode, + ctx: AssemblyContext, + *, + top_level: bool, + extra_attrs: list[str] | None = None, +) -> str: """Wrap a shape's body in a that carries the transform (rotation / flip) and an id for animation anchoring.""" if not inner.strip(): @@ -907,6 +998,8 @@ def _wrap_shape_group(inner: str, node: ShapeNode, ctx: AssemblyContext, attrs.append(f'data-name="{_xml_escape(node.name)}"') if node.placeholder is not None and node.placeholder.type: attrs.append(f'data-ph-type="{_xml_escape(node.placeholder.type)}"') + if extra_attrs: + attrs.extend(extra_attrs) if transform: attrs.append(f'transform="{transform}"') return f"\n{inner}\n" diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/tbl_to_svg.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/tbl_to_svg.py index 705e680c..1bfa5636 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/tbl_to_svg.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/tbl_to_svg.py @@ -37,10 +37,18 @@ Cell painting order: from __future__ import annotations from dataclasses import dataclass +from typing import Any from xml.etree import ElementTree as ET -from .color_resolver import ColorPalette -from .emu_units import NS, Xfrm, emu_to_px, fmt_num +from .color_resolver import ColorPalette, find_color_elem, resolve_color +from .emu_units import ( + NS, + Xfrm, + emu_to_px, + fmt_num, + hundredths_pt_to_px, + ooxml_bool, +) from .fill_to_svg import resolve_fill from .ln_to_svg import resolve_stroke from .txbody_to_svg import convert_txbody @@ -48,10 +56,12 @@ from .txbody_to_svg import convert_txbody @dataclass class TableResult: - """Composite render output: SVG body + accumulated .""" + """Composite render output plus optional native table metadata.""" svg: str = "" defs: list[str] = None + native_payload: dict[str, Any] | None = None + native_status: str | None = None def __post_init__(self) -> None: if self.defs is None: @@ -68,6 +78,7 @@ def convert_tbl( palette: ColorPalette | None, *, theme_fonts: dict[str, str] | None = None, + slide_number: int | None = None, id_prefix: str = "tbl", grad_seq: list[int] | None = None, marker_seq: list[int] | None = None, @@ -102,6 +113,35 @@ def convert_tbl( # and dropped cells don't render anything. PowerPoint expresses merges via # gridSpan/rowSpan on the anchor cell + hMerge/vMerge on the dropped cells. cells = _build_cell_grid(rows, len(col_widths)) + if xfrm.rot or xfrm.flip_h or xfrm.flip_v: + native_status = "unsupported-native-transform" + elif _table_has_merges(rows): + native_status = "unsupported-merge" + elif len(rows) > 1000 or len(col_widths) > 1000: + native_status = "unsupported-table-size" + elif ( + any(width < 0 for width in col_widths) + or any(height < 0 for height in row_heights) + or sum(col_widths) <= 0 + or sum(row_heights) <= 0 + ): + native_status = "unsupported-table-geometry" + elif _table_has_unsupported_style(tbl): + native_status = "unsupported-table-style" + elif _table_has_unsupported_direct_formatting(tbl, palette): + native_status = "unsupported-table-direct-formatting" + else: + native_status = None + native_payload = ( + None if native_status else _native_table_payload( + tbl, + xfrm, + col_widths, + row_heights, + cells, + palette, + ) + ) body_parts: list[str] = [] defs: list[str] = [] @@ -146,6 +186,7 @@ def convert_tbl( cell_xfrm = Xfrm(x=cell_x, y=cell_y, w=cell_w, h=cell_h) text_result = _convert_cell_text( tx_body, tcPr, cell_xfrm, palette, theme_fonts, + slide_number=slide_number, id_prefix=f"{id_prefix}txt", id_seq=grad_seq, ) @@ -176,7 +217,12 @@ def convert_tbl( if line_xml: body_parts.append(line_xml) - return TableResult(svg="\n".join(body_parts), defs=defs) + return TableResult( + svg="\n".join(body_parts), + defs=defs, + native_payload=native_payload, + native_status=native_status, + ) # --------------------------------------------------------------------------- @@ -252,8 +298,8 @@ def _build_cell_grid(rows: list[ET.Element], col_count: int) -> list[list[_CellS grid_span = _safe_int(tc.attrib.get("gridSpan"), 1) row_span = _safe_int(tc.attrib.get("rowSpan"), 1) - h_merge = tc.attrib.get("hMerge") == "1" - v_merge = tc.attrib.get("vMerge") == "1" + h_merge = ooxml_bool(tc.attrib.get("hMerge")) + v_merge = ooxml_bool(tc.attrib.get("vMerge")) if h_merge or v_merge: # This cell is a merge slave; leave the slot tied to the anchor @@ -294,6 +340,345 @@ def _safe_int(value: str | None, default: int) -> int: return default +def _table_has_merges(rows: list[ET.Element]) -> bool: + """Return True when the table uses merged cells.""" + for row in rows: + for tc in row.findall("a:tc", NS): + if ooxml_bool(tc.attrib.get("hMerge")) or ooxml_bool(tc.attrib.get("vMerge")): + return True + if _safe_int(tc.attrib.get("gridSpan"), 1) > 1: + return True + if _safe_int(tc.attrib.get("rowSpan"), 1) > 1: + return True + return False + + +def _table_has_unsupported_style(tbl: ET.Element) -> bool: + tbl_pr = tbl.find("a:tblPr", NS) + if tbl_pr is None: + return False + allowed_attrs = { + "firstRow", "bandRow", "firstCol", "lastCol", "lastRow", + "bandCol", "rtl", + } + if any(name not in allowed_attrs for name in tbl_pr.attrib): + return True + if any( + ooxml_bool(tbl_pr.attrib.get(name)) + for name in ("firstCol", "lastCol", "lastRow", "bandCol", "rtl") + ): + return True + return any( + child.tag.rsplit("}", 1)[-1] != "tableStyleId" + for child in tbl_pr + ) + + +def _table_has_unsupported_direct_formatting( + tbl: ET.Element, + palette: ColorPalette | None, +) -> bool: + """Reject direct cell features the compact native schema cannot retain.""" + for tc in tbl.findall(".//a:tc", NS): + tc_pr = tc.find("a:tcPr", NS) + if tc_pr is not None: + allowed_attrs = {"marL", "marR", "marT", "marB", "anchor"} + if any(name not in allowed_attrs for name in tc_pr.attrib): + return True + if tc_pr.get("anchor") not in {None, "t", "ctr", "b"}: + return True + if any( + child.tag.rsplit("}", 1)[-1] != "solidFill" + for child in tc_pr + ): + return True + solid_fill = tc_pr.find("a:solidFill", NS) + if solid_fill is not None: + if solid_fill.find(".//a:alpha", NS) is not None: + return True + if _cell_fill_hex(tc_pr, palette) is None: + return True + tx_body = tc.find("a:txBody", NS) + if _text_body_has_unsupported_formatting(tx_body): + return True + if ( + tx_body is not None + and tx_body.find(".//a:solidFill", NS) is not None + and _cell_text_color(tx_body, palette) is None + ): + return True + return False + + +def _text_body_has_unsupported_formatting(tx_body: ET.Element | None) -> bool: + if tx_body is None: + return False + body_pr = tx_body.find("a:bodyPr", NS) + if body_pr is not None and (body_pr.attrib or list(body_pr)): + return True + list_style = tx_body.find("a:lstStyle", NS) + if list_style is not None and (list_style.attrib or list(list_style)): + return True + if ( + tx_body.find(".//a:br", NS) is not None + or tx_body.find(".//a:fld", NS) is not None + or tx_body.find(".//a:tab", NS) is not None + ): + return True + + paragraphs = tx_body.findall("a:p", NS) + if len(paragraphs) > 1: + return True + + alignments: set[str | None] = set() + run_signatures: set[tuple[str | None, str | None, bytes | None]] = set() + for paragraph in paragraphs: + p_pr = paragraph.find("a:pPr", NS) + alignment = p_pr.get("algn") if p_pr is not None else None + if alignment not in {None, "l", "ctr", "r"}: + return True + alignments.add(alignment) + if p_pr is not None: + if any(name != "algn" for name in p_pr.attrib): + return True + if any( + child.tag.rsplit("}", 1)[-1] not in {"defRPr", "buNone"} + for child in p_pr + ): + return True + + default_r_pr = p_pr.find("a:defRPr", NS) if p_pr is not None else None + if _run_props_have_unsupported_formatting(default_r_pr): + return True + for run in paragraph.findall("a:r", NS): + r_pr = run.find("a:rPr", NS) + if _run_props_have_unsupported_formatting(r_pr): + return True + run_signatures.add(_effective_run_signature(r_pr, default_r_pr)) + end_r_pr = paragraph.find("a:endParaRPr", NS) + if _run_props_have_unsupported_formatting(end_r_pr): + return True + if not paragraph.findall("a:r", NS) and end_r_pr is not None: + run_signatures.add(_effective_run_signature(end_r_pr, default_r_pr)) + + return len(alignments) > 1 or len(run_signatures) > 1 + + +def _run_props_have_unsupported_formatting(r_pr: ET.Element | None) -> bool: + if r_pr is None: + return False + if ooxml_bool(r_pr.get("i")): + return True + if r_pr.get("u") not in {None, "none"}: + return True + if r_pr.get("strike") not in {None, "noStrike"}: + return True + if r_pr.get("baseline") not in {None, "0"}: + return True + if r_pr.get("cap") not in {None, "none"}: + return True + if r_pr.get("spc") not in {None, "0"}: + return True + allowed_attrs = { + "lang", "altLang", "sz", "b", "i", "u", "strike", "dirty", + "baseline", "cap", "spc", + } + if any(name not in allowed_attrs for name in r_pr.attrib): + return True + solid_fill = r_pr.find("a:solidFill", NS) + if solid_fill is not None and solid_fill.find(".//a:alpha", NS) is not None: + return True + return any( + child.tag.rsplit("}", 1)[-1] != "solidFill" + for child in r_pr + ) + + +def _effective_run_signature( + r_pr: ET.Element | None, + default_r_pr: ET.Element | None, +) -> tuple[str | None, str | None, bytes | None]: + def attr(name: str) -> str | None: + if r_pr is not None and r_pr.get(name) is not None: + return r_pr.get(name) + return default_r_pr.get(name) if default_r_pr is not None else None + + solid_fill = r_pr.find("a:solidFill", NS) if r_pr is not None else None + if solid_fill is None and default_r_pr is not None: + solid_fill = default_r_pr.find("a:solidFill", NS) + fill_xml = ( + ET.tostring(solid_fill, encoding="utf-8") + if solid_fill is not None else None + ) + return attr("b"), attr("sz"), fill_xml + + +def _round_payload_number(value: float) -> int | float: + rounded = round(float(value), 3) + return int(rounded) if rounded.is_integer() else rounded + + +def _native_table_payload( + tbl: ET.Element, + xfrm: Xfrm, + column_widths: list[float], + row_heights: list[float], + cells: list[list[_CellSlot | None]], + palette: ColorPalette | None, +) -> dict[str, Any]: + """Build the SVG data-pptx-native payload for an unmerged table.""" + tbl_pr = tbl.find("a:tblPr", NS) + payload: dict[str, Any] = { + "x": _round_payload_number(xfrm.x), + "y": _round_payload_number(xfrm.y), + "width": _round_payload_number(xfrm.w), + "height": _round_payload_number(xfrm.h), + "strict_grid": True, + "header_rows": ( + 1 if tbl_pr is not None and ooxml_bool(tbl_pr.get("firstRow")) else 0 + ), + "column_widths": [_round_payload_number(width) for width in column_widths], + "row_heights": [_round_payload_number(height) for height in row_heights], + "rows": [], + } + style: dict[str, Any] = { + "band_row": bool(tbl_pr is not None and ooxml_bool(tbl_pr.get("bandRow"))), + } + if tbl_pr is not None: + table_style_id = tbl_pr.findtext("a:tableStyleId", default="", namespaces=NS).strip() + if table_style_id: + style["table_style_id"] = table_style_id + payload["style"] = style + + rows_payload: list[list[Any]] = [] + for row_cells in cells: + row_payload: list[Any] = [] + for slot in row_cells: + if slot is None or slot.is_dropped: + row_payload.append("") + continue + row_payload.append(_native_cell_payload(slot.element, palette)) + rows_payload.append(row_payload) + payload["rows"] = rows_payload + return payload + + +def _native_cell_payload(tc: ET.Element, palette: ColorPalette | None) -> dict[str, Any]: + tx_body = tc.find("a:txBody", NS) + tc_pr = tc.find("a:tcPr", NS) + cell: dict[str, Any] = {"text": _cell_plain_text(tx_body)} + + fill = _cell_fill_hex(tc_pr, palette) + if fill: + cell["fill"] = fill + color = _cell_text_color(tx_body, palette) + if color: + cell["color"] = color + font_size = _cell_font_size_px(tx_body) + if font_size: + cell["font_size"] = font_size + align = _cell_align(tx_body) + if align: + cell["align"] = align + valign = _cell_valign(tc_pr) + if valign: + cell["valign"] = valign + bold = _cell_bold(tx_body) + if bold is not None: + cell["bold"] = bold + _copy_cell_margins(tc_pr, cell) + return cell + + +def _cell_plain_text(tx_body: ET.Element | None) -> str: + if tx_body is None: + return "" + paragraphs: list[str] = [] + for paragraph in tx_body.findall("a:p", NS): + text = "".join(node.text or "" for node in paragraph.findall(".//a:t", NS)) + if text: + paragraphs.append(text) + return "\n".join(paragraphs) + + +def _cell_fill_hex(tc_pr: ET.Element | None, palette: ColorPalette | None) -> str | None: + fill = resolve_fill(tc_pr, palette) + color = fill.attrs.get("fill") if fill.attrs else None + if color and color.startswith("#"): + return color + return None + + +def _cell_text_color(tx_body: ET.Element | None, palette: ColorPalette | None) -> str | None: + for r_pr in _text_run_props_in_priority(tx_body): + solid_fill = r_pr.find("a:solidFill", NS) + if solid_fill is not None: + color, _alpha = resolve_color(find_color_elem(solid_fill), palette) + return color + return None + + +def _cell_font_size_px(tx_body: ET.Element | None) -> int | float | None: + for r_pr in _text_run_props_in_priority(tx_body): + if not r_pr.get("sz"): + continue + size = hundredths_pt_to_px(r_pr.get("sz")) + if size > 0: + return _round_payload_number(size) + return None + + +def _cell_align(tx_body: ET.Element | None) -> str | None: + if tx_body is None: + return None + p_pr = tx_body.find("a:p/a:pPr", NS) + align = p_pr.get("algn") if p_pr is not None else None + if align in {"l", "ctr", "r"}: + return align + return None + + +def _cell_valign(tc_pr: ET.Element | None) -> str | None: + anchor = tc_pr.get("anchor") if tc_pr is not None else None + return { + "t": "top", + "ctr": "middle", + "b": "bottom", + }.get(anchor) + + +def _cell_bold(tx_body: ET.Element | None) -> bool | None: + for r_pr in _text_run_props_in_priority(tx_body): + if r_pr.get("b") is not None: + return ooxml_bool(r_pr.get("b")) + return None + + +def _text_run_props_in_priority(tx_body: ET.Element | None) -> list[ET.Element]: + if tx_body is None: + return [] + props: list[ET.Element] = [] + for path in (".//a:r/a:rPr", ".//a:pPr/a:defRPr", ".//a:endParaRPr"): + r_pr = tx_body.find(path, NS) + if r_pr is not None: + props.append(r_pr) + return props + + +def _copy_cell_margins(tc_pr: ET.Element | None, cell: dict[str, Any]) -> None: + if tc_pr is None: + return + for source, target in ( + ("marL", "padding_left"), + ("marR", "padding_right"), + ("marT", "padding_top"), + ("marB", "padding_bottom"), + ): + if source not in tc_pr.attrib: + continue + cell[target] = _round_payload_number(emu_to_px(tc_pr.attrib[source])) + + # --------------------------------------------------------------------------- # Cell text & borders # --------------------------------------------------------------------------- @@ -305,6 +690,7 @@ def _convert_cell_text( palette: ColorPalette | None, theme_fonts: dict[str, str] | None, *, + slide_number: int | None, id_prefix: str, id_seq: list[int] | None, ): @@ -323,6 +709,7 @@ def _convert_cell_text( try: return convert_txbody( tx_body, cell_xfrm, palette, theme_fonts=theme_fonts, + slide_number=slide_number, id_prefix=id_prefix, id_seq=id_seq, ) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/txbody_to_svg.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/txbody_to_svg.py index 511b2466..f342deef 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/txbody_to_svg.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_to_svg/txbody_to_svg.py @@ -103,6 +103,7 @@ def convert_txbody( palette: ColorPalette | None, *, theme_fonts: dict[str, str] | None = None, + slide_number: int | None = None, default_fill: str = DEFAULT_FILL_HEX, default_font_size_px: float = DEFAULT_FONT_SIZE_PX, fallback_lst_styles: tuple[ET.Element, ...] = (), @@ -118,7 +119,7 @@ def convert_txbody( tx_body, palette, theme_fonts or {}, default_fill=default_fill, default_font_size_px=default_font_size_px, fallback_lst_styles=fallback_lst_styles, - id_prefix=id_prefix, id_seq=id_seq, + slide_number=slide_number, id_prefix=id_prefix, id_seq=id_seq, ) if not paragraphs or not _has_visible_text(paragraphs): return TextResult() @@ -188,6 +189,7 @@ def convert_vertical_txbody( palette: ColorPalette | None, *, theme_fonts: dict[str, str] | None = None, + slide_number: int | None = None, default_fill: str = DEFAULT_FILL_HEX, default_font_size_px: float = DEFAULT_FONT_SIZE_PX, fallback_lst_styles: tuple[ET.Element, ...] = (), @@ -208,7 +210,7 @@ def convert_vertical_txbody( tx_body, palette, theme_fonts or {}, default_fill=default_fill, default_font_size_px=default_font_size_px, fallback_lst_styles=fallback_lst_styles, - id_prefix=id_prefix, id_seq=id_seq, + slide_number=slide_number, id_prefix=id_prefix, id_seq=id_seq, ) runs = [ run @@ -342,6 +344,7 @@ def _parse_paragraphs( default_fill: str = DEFAULT_FILL_HEX, default_font_size_px: float = DEFAULT_FONT_SIZE_PX, fallback_lst_styles: tuple[ET.Element, ...] = (), + slide_number: int | None = None, id_prefix: str = "txt", id_seq: list[int] | None = None, ) -> list[TextParagraph]: @@ -360,6 +363,7 @@ def _parse_paragraphs( lst_styles=lst_styles, default_fill=default_fill, default_font_size_px=default_font_size_px, + slide_number=slide_number, id_prefix=id_prefix, id_seq=id_seq, ) paragraphs.append(para) @@ -376,6 +380,7 @@ def _parse_paragraph( lst_styles: tuple[ET.Element, ...] = (), default_fill: str = DEFAULT_FILL_HEX, default_font_size_px: float = DEFAULT_FONT_SIZE_PX, + slide_number: int | None = None, id_prefix: str = "txt", id_seq: list[int] | None = None, ) -> TextParagraph: @@ -430,10 +435,15 @@ def _parse_paragraph( is_break=True, )) elif local == "fld": - # Field (datetime / slidenum). Use the literal a:t fallback. + # Slide SVGs have a concrete page context, so resolve slide-number + # fields there. Standalone master/layout renders keep the literal + # fallback because one shared part can serve many slide numbers. rpr = child.find("a:rPr", NS) text_elem = child.find("a:t", NS) text = text_elem.text or "" if text_elem is not None else "" + field_type = child.attrib.get("type", "").strip().lower() + if field_type == "slidenum" and slide_number is not None: + text = str(slide_number) if text: run = _build_run( text, rpr, end_rpr, palette, theme_fonts, diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_quality_checker.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_quality_checker.py index 74196dd2..36a05ba2 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_quality_checker.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_quality_checker.py @@ -73,6 +73,43 @@ try: except ImportError: _native_object_marker_warnings = None +try: + from svg_to_pptx.semantic_markers import ( + SEMANTIC_ATTRS as _SEMANTIC_ATTRS, + validate_semantic_markers as _validate_semantic_markers, + ) +except ImportError: + _SEMANTIC_ATTRS = frozenset({ + 'data-pptx-page-role', + 'data-pptx-role', + }) + _validate_semantic_markers = None + +try: + from svg_to_pptx.pptx_package.template_structure import ( + TemplateStructureError as _TemplateStructureError, + PptxLayoutReference as _PptxLayoutReference, + PptxStructureLock as _PptxStructureLock, + load_native_structure_contract as _load_native_structure_contract, + load_pptx_structure_lock as _load_pptx_structure_lock, + native_structure_lock_errors as _native_structure_lock_errors, + parse_preserve_slides as _parse_preserve_structure_slides, + parse_template_slides as _parse_template_structure_slides, + template_lock_errors as _template_lock_errors, + validate_template_svg as _validate_template_structure_svg, + ) +except ImportError: + _TemplateStructureError = None + _PptxLayoutReference = None + _PptxStructureLock = None + _load_native_structure_contract = None + _load_pptx_structure_lock = None + _native_structure_lock_errors = None + _parse_preserve_structure_slides = None + _parse_template_structure_slides = None + _template_lock_errors = None + _validate_template_structure_svg = None + try: from svg_finalize.embed_icons import ( resolve_icon_path as _resolve_icon_path, @@ -310,6 +347,7 @@ class SVGQualityChecker: self._template_issues: List[Tuple[str, str, str]] = [] self._animation_issues: List[Tuple[str, str]] = [] self._illustration_issues: List[Tuple[str, str, str]] = [] + self._pptx_structure_issues: List[Tuple[str, str]] = [] self._aggregate_counts_applied = False def check_file(self, svg_file: str, expected_format: str = None) -> Dict: @@ -383,6 +421,12 @@ class SVGQualityChecker: # 8c. Check opt-in native table/chart markers before export. self._check_native_object_markers(root, result) + # 8d. Validate explicit master/layout/placeholder metadata. + self._check_pptx_structure_metadata(root, svg_path, result) + + # 8e. Validate rendering-neutral page/structure compiler hints. + self._check_semantic_markers(root, svg_path, result) + # 9. Check spec_lock drift (colors / font-family / font-size). # Templates do not ship a spec_lock.md, so skip in template # mode to avoid noise. @@ -765,6 +809,8 @@ class SVGQualityChecker: href = image.get('href') or image.get(f'{{{XLINK_NS}}}href') if not href or href.startswith('data:'): continue + if self.template_mode and '{{' in href and '}}' in href: + continue if _resolve_external_image_reference is None or _unresolved_external_image_reference_path is None: result['warnings'].append( "Detected image references, but shared image resolver could not be imported; " @@ -926,6 +972,15 @@ class SVGQualityChecker: def _check_native_object_markers(self, root: ET.Element, result: Dict) -> None: """Validate opt-in native table/chart markers before PPTX export.""" + for elem in root.iter(): + status = elem.get('data-pptx-native-status') + if not status or elem.tag.rsplit('}', 1)[-1] == 'metadata': + continue + marker_id = elem.get('id') or elem.get('data-name') or '' + result['warnings'].append( + f"Native PPTX object {marker_id} is fallback-only: {status}" + ) + markers = [ elem for elem in root.iter() if elem.get('data-pptx-native') and elem.tag.rsplit('}', 1)[-1] != 'metadata' @@ -939,11 +994,27 @@ class SVGQualityChecker: ) return + parent_map = { + child: parent + for parent in root.iter() + for child in parent + } + for marker in markers: marker_id = marker.get('id') or '' + ancestors = [] + parent = parent_map.get(marker) + while parent is not None and parent is not root: + if parent.tag.rsplit('}', 1)[-1] == 'g': + ancestors.append(parent) + parent = parent_map.get(parent) + ancestors_tuple = tuple(reversed(ancestors)) if _validate_native_object_marker_with_warnings is not None: try: - warnings = _validate_native_object_marker_with_warnings(marker) + warnings = _validate_native_object_marker_with_warnings( + marker, + ancestors=ancestors_tuple, + ) except RuntimeError as exc: result['errors'].append( f"Invalid data-pptx-native marker {marker_id}: {exc}" @@ -956,7 +1027,7 @@ class SVGQualityChecker: continue try: - _validate_native_object_marker(marker) + _validate_native_object_marker(marker, ancestors=ancestors_tuple) except RuntimeError as exc: result['errors'].append( f"Invalid data-pptx-native marker {marker_id}: {exc}" @@ -964,11 +1035,76 @@ class SVGQualityChecker: continue if _native_object_marker_warnings is None: continue - for warning in _native_object_marker_warnings(marker): + for warning in _native_object_marker_warnings( + marker, + ancestors=ancestors_tuple, + ): result['warnings'].append( f"data-pptx-native marker {marker_id}: {warning}" ) + def _check_pptx_structure_metadata( + self, + root: ET.Element, + svg_path: Path, + result: Dict, + ) -> None: + """Validate explicit template-export metadata when any is present.""" + structure_attrs = { + 'data-pptx-layer', + 'data-pptx-layout', + 'data-pptx-layout-name', + 'data-pptx-placeholder', + 'data-pptx-placeholder-bounds', + 'data-pptx-placeholder-idx', + 'data-pptx-editable', + } + if not any( + elem.get(attr) is not None + for elem in root.iter() + for attr in structure_attrs + ): + return + if _validate_template_structure_svg is None: + result['warnings'].append( + "Detected PPTX template structure metadata, but its validator " + "could not be imported; template export will still validate it." + ) + return + result['errors'].extend(_validate_template_structure_svg(svg_path)) + + def _check_semantic_markers( + self, + root: ET.Element, + svg_path: Path, + result: Dict, + ) -> None: + """Validate minimal compiler hints without changing SVG rendering.""" + has_semantics = any( + elem.get(attr) is not None + for elem in root.iter() + for attr in _SEMANTIC_ATTRS + ) + require_page_role = ( + svg_path.parent.name in {'svg_output', 'svg_final'} + and root.get('data-pptx-layout') is None + ) + if _validate_semantic_markers is None: + if has_semantics: + result['warnings'].append( + "Detected Semantic SVG markers, but their validator could " + "not be imported." + ) + return + for issue in _validate_semantic_markers( + root, + require_page_role=require_page_role, + ): + if issue.severity == 'error': + result['errors'].append(issue.message) + else: + result['warnings'].append(issue.message) + def _get_spec_lock(self, svg_path: Path): """Locate and parse spec_lock.md near the SVG. Returns dict or None. @@ -1348,6 +1484,7 @@ class SVGQualityChecker: result = self.check_file(str(svg_file), expected_format) self._print_result(result) + self._check_pptx_structure_contract(dir_path, svg_files) if self.template_mode and dir_path.is_dir(): self._check_template_contract(dir_path, svg_files) elif dir_path.is_dir(): @@ -1356,6 +1493,126 @@ class SVGQualityChecker: return self.results + def _check_pptx_structure_contract( + self, + target_path: Path, + svg_files: List[Path], + ) -> None: + """Validate project-level layout reuse and spec_lock mappings.""" + if ( + _load_pptx_structure_lock is None + or _parse_template_structure_slides is None + or _template_lock_errors is None + or _TemplateStructureError is None + ): + return + project_path = self._resolve_project_path(target_path) + try: + structure_lock = _load_pptx_structure_lock(project_path) + except _TemplateStructureError as exc: + self._pptx_structure_issues.append(('error', str(exc))) + return + + if ( + structure_lock is not None + and structure_lock.mode == 'template' + and structure_lock.template_adherence in {'strict', 'adaptive'} + and _parse_spec_lock is not None + ): + lock_path = project_path / 'spec_lock.md' + lock = _parse_spec_lock(lock_path) + page_layouts = lock.get('page_layouts', {}) + expected_pages = {f'P{index:02d}' for index in range(1, len(svg_files) + 1)} + missing_pages = sorted(expected_pages - set(page_layouts)) + if missing_pages: + self._pptx_structure_issues.append(( + 'error', + 'spec_lock.md template export requires page_layouts ' + 'rows for every generated page; missing: ' + ', '.join(missing_pages), + )) + template_dir = project_path / 'templates' + template_svgs = {path.stem for path in template_dir.glob('*.svg')} + missing_templates = sorted({ + value.strip().removesuffix('.svg') + for page, value in page_layouts.items() + if page in expected_pages + and value.strip().removesuffix('.svg') not in template_svgs + }) + if missing_templates: + self._pptx_structure_issues.append(( + 'error', + 'spec_lock.md template export references missing ' + 'template SVG(s): ' + ', '.join(missing_templates), + )) + + structure_attrs = { + 'data-pptx-layer', + 'data-pptx-layout', + 'data-pptx-layout-name', + 'data-pptx-placeholder', + 'data-pptx-placeholder-bounds', + 'data-pptx-placeholder-idx', + 'data-pptx-editable', + } + has_metadata = False + for svg_path in svg_files: + try: + root = ET.parse(svg_path).getroot() + except (OSError, ET.ParseError): + continue + if any( + elem.get(attr) is not None + for elem in root.iter() + for attr in structure_attrs + ): + has_metadata = True + break + locked_mode = structure_lock.mode if structure_lock is not None else None + structure_locked = locked_mode in {'template', 'preserve'} + if not has_metadata and not structure_locked: + return + + try: + if locked_mode == 'preserve': + if _parse_preserve_structure_slides is None: + return + specs = _parse_preserve_structure_slides(svg_files) + else: + specs = _parse_template_structure_slides(svg_files) + except _TemplateStructureError as exc: + self._pptx_structure_issues.append(('error', str(exc))) + return + if not structure_locked: + return + self._pptx_structure_issues.extend( + ('error', message) + for message in _template_lock_errors(specs, structure_lock) + ) + if locked_mode != 'preserve': + return + if ( + _load_native_structure_contract is None + or _native_structure_lock_errors is None + ): + self._pptx_structure_issues.append(( + 'error', + 'Preserve mode validator could not load the native structure contract.', + )) + return + try: + contract = _load_native_structure_contract(structure_lock) + except _TemplateStructureError as exc: + self._pptx_structure_issues.append(('error', str(exc))) + return + self._pptx_structure_issues.extend( + ('error', message) + for message in _native_structure_lock_errors( + specs, + structure_lock, + contract, + ) + ) + def _check_illustration_resource_contract(self, dir_path: Path) -> None: """Project-level illustration resource checks.""" project_path = self._resolve_project_path(dir_path) @@ -1743,11 +2000,12 @@ class SVGQualityChecker: def _check_template_contract(self, dir_path: Path, svg_files: List[Path]) -> None: - """Template-mode-only checks: roster ↔ design_spec consistency and - per-page placeholder hints. + """Check reusable-template structure, roster, and placeholder hints. - **Roster mismatch (orphan / missing)** is reported as an *error*: a stale roster will produce a wrong ``layouts_index.json`` entry. + - **Explicit structure gaps** are errors: every reusable SVG declares + its Layout and at least one direct structure/placeholder element. - **Placeholder gaps** are reported as *warnings*. Templates may legitimately omit conventional placeholders or swap them out (e.g. ``{{CLOSING_MESSAGE}}`` instead of ``{{THANK_YOU}}``), and a content @@ -1758,8 +2016,134 @@ class SVGQualityChecker: Issues are aggregated and printed in :py:meth:`print_summary` so the per-file report stays focused on intrinsic SVG validity. """ + native_contract_path = dir_path / 'native_structure.json' + source_template_path = dir_path / 'source_template.pptx' + native_contract = None + for svg_file in svg_files: + try: + root = ET.parse(svg_file).getroot() + except (OSError, ET.ParseError): + continue + if not root.get('data-pptx-layout'): + self._template_issues.append(( + 'error', + 'explicit_structure_missing', + f"{svg_file.name}: reusable templates require root " + "data-pptx-layout metadata", + )) + if not root.get('data-pptx-layout-name'): + self._template_issues.append(( + 'error', + 'explicit_structure_name_missing', + f"{svg_file.name}: reusable templates require root " + "data-pptx-layout-name metadata", + )) + has_structure_child = any( + child.get('data-pptx-layer') in {'master', 'layout'} + or child.get('data-pptx-placeholder') + for child in list(root) + ) + if not has_structure_child: + self._template_issues.append(( + 'error', + 'explicit_structure_empty', + f"{svg_file.name}: reusable templates require at least one " + "direct master/layout/placeholder declaration", + )) + if native_contract_path.exists() != source_template_path.exists(): + self._template_issues.append(( + 'error', + 'native_structure_pair', + "native_structure.json and source_template.pptx must be shipped together", + )) + elif native_contract_path.exists(): + if ( + _PptxStructureLock is None + or _load_native_structure_contract is None + or _TemplateStructureError is None + ): + self._template_issues.append(( + 'error', + 'native_structure_validator', + "native structure validator is unavailable", + )) + else: + try: + native_contract = _load_native_structure_contract(_PptxStructureLock( + mode='preserve', + source_template=source_template_path, + native_structure=native_contract_path, + )) + except _TemplateStructureError as exc: + self._template_issues.append(( + 'error', + 'native_structure_invalid', + str(exc), + )) + if ( + native_contract is not None + and _PptxLayoutReference is not None + and _parse_preserve_structure_slides is not None + and _template_lock_errors is not None + and _native_structure_lock_errors is not None + ): + try: + native_specs = _parse_preserve_structure_slides(svg_files) + native_layouts = { + layout.key: layout for layout in native_contract.layouts + } + references = tuple( + _PptxLayoutReference( + slide_num=spec.slide_num, + layout_key=spec.layout_key, + layout_name=( + native_layouts[spec.layout_key].name + if spec.layout_key in native_layouts + else spec.layout_name + ), + ) + for spec in native_specs + ) + native_lock = _PptxStructureLock( + mode='preserve', + layouts=references, + source_template=source_template_path, + native_structure=native_contract_path, + ) + native_errors = _template_lock_errors(native_specs, native_lock) + native_errors.extend(_native_structure_lock_errors( + native_specs, + native_lock, + native_contract, + )) + for message in native_errors: + self._template_issues.append(( + 'error', + 'native_structure_svg', + message, + )) + except _TemplateStructureError as exc: + self._template_issues.append(( + 'error', + 'native_structure_svg', + str(exc), + )) + spec_path = dir_path / 'design_spec.md' spec_text = spec_path.read_text(encoding='utf-8') if spec_path.exists() else "" + mode_match = re.search( + r'^native_structure_mode:\s*([A-Za-z0-9_-]+)\s*$', + spec_text, + re.MULTILINE, + ) + declared_structure_mode = mode_match.group(1).lower() if mode_match else None + if native_contract is None and declared_structure_mode != 'template': + self._template_issues.append(( + 'error', + 'explicit_structure_mode', + "design_spec.md frontmatter must declare " + "native_structure_mode: template", + )) spec_pages = self._extract_spec_roster(spec_text) if spec_text else [] custom_contract = self._extract_frontmatter_placeholders(spec_text) if spec_text else {} @@ -1890,18 +2274,40 @@ class SVGQualityChecker: that as "skip orphan/missing checks", not as "no pages declared". """ # Pass 1: explicit roster section, any roman numeral. - section = re.search( - r"^##\s+[IVX]+\.\s+(?:Page Roster|Page Structure|Pages|Page Types)\b.*?(?=^##\s+|\Z)", + sections = list(re.finditer( + r"^##\s+[IVX]+\.\s+(?:(?:SVG\s+)?Page Roster|Page Structure|Pages|Page Types)\b.*?(?=^##\s+|\Z)", spec_text, re.MULTILINE | re.DOTALL | re.IGNORECASE, + )) + roster_scope = next( + ( + section.group(0) + for section in sections + if re.match( + r"^##\s+[IVX]+\.\s+(?:SVG\s+)?Page Roster\b", + section.group(0), + re.IGNORECASE, + ) + ), + None, + ) + scope = roster_scope or next( + ( + section.group(0) + for section in sections + if re.search(r"[`\(][0-9A-Za-z_]+\.svg[`\)]", section.group(0)) + ), + sections[0].group(0) if sections else None, ) - scope = section.group(0) if section else None # Pass 2: full document. We *only* trust this scan when the explicit # roster scan came up empty (no `.svg` references inside it) — # otherwise the explicit section's deliberate roster wins over loose # mentions elsewhere. - if scope and re.search(r"[`\(][0-9A-Za-z_]+\.svg[`\)]", scope): + explicit_scope = bool( + scope and re.search(r"[`\(][0-9A-Za-z_]+\.svg[`\)]", scope) + ) + if explicit_scope: text = scope else: text = spec_text @@ -1913,7 +2319,7 @@ class SVGQualityChecker: svg_ref_re = re.compile(r"[`\(]([0-9A-Za-z_]+\.svg)[`\)]") for match in svg_ref_re.finditer(text): stem = match.group(1)[:-4] - if stem in seen or not re.match(r"^\d", stem): + if stem in seen or (not explicit_scope and not re.match(r"^\d", stem)): continue seen.add(stem) stems.append(stem) @@ -2034,6 +2440,9 @@ class SVGQualityChecker: # Illustration strategy aggregation. self._print_illustration_summary() + # Explicit PowerPoint master/layout structure aggregation. + self._print_pptx_structure_summary() + # Fix suggestions if self.summary['errors'] > 0 or self.summary['warnings'] > 0: print(f"\n[TIP] Common fixes:") @@ -2074,6 +2483,14 @@ class SVGQualityChecker: for _severity, kind, msg in warnings: print(f" [{kind}] {msg}") + def _print_pptx_structure_summary(self): + """Print project-level PowerPoint structure contract issues.""" + if not self._pptx_structure_issues: + return + print("\n[PPTX STRUCTURE] Master/layout contract checks") + for severity, message in self._pptx_structure_issues: + print(f" [{severity.upper()}] {message}") + def _print_template_summary(self): """Aggregate template-mode roster / placeholder issues at the bottom. @@ -2127,6 +2544,13 @@ class SVGQualityChecker: for severity, kind, _msg in self._illustration_issues: self.issue_types[f'illustration_{kind}_{severity}'] += 1 + structure_errors = [item for item in self._pptx_structure_issues if item[0] == 'error'] + structure_warnings = [item for item in self._pptx_structure_issues if item[0] == 'warning'] + self.summary['errors'] += len(structure_errors) + self.summary['warnings'] += len(structure_warnings) + for severity, _msg in self._pptx_structure_issues: + self.issue_types[f'pptx_structure_{severity}'] += 1 + def _print_drift_summary(self): """Print spec_lock drift aggregation if any was observed. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx.py index 7befb9af..13d83aff 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx.py @@ -17,4 +17,4 @@ from svg_to_pptx import main configure_utf8_stdio() if __name__ == '__main__': - main() + raise SystemExit(main()) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/animation_config.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/animation_config.py index ebb6af78..57ecf3f7 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/animation_config.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/animation_config.py @@ -10,6 +10,7 @@ from typing import Any from xml.etree import ElementTree as ET from .drawingml.utils import SVG_NS +from .semantic_markers import is_static_page_frame try: from pptx_animations import ANIMATIONS, TRANSITIONS @@ -24,8 +25,8 @@ _CHROME_ID_TOKENS = frozenset({ 'decoration', 'decorations', 'decor', 'header', 'footer', 'chrome', 'watermark', - 'pagenumber', 'pagenum', - 'page-number', + 'pagenumber', 'pagenum', 'slidenumber', 'slidenum', + 'logo', 'nav', 'rule', }) @@ -73,12 +74,21 @@ def scan_svg_targets(svg_path: Path) -> tuple[list[GroupTarget], list[str]]: if not group_id: anonymous_groups.append(f'{svg_path.stem}: top-level group #{visual_index}') continue + role = child.get('data-pptx-role') + placeholder = child.get('data-pptx-placeholder') + has_explicit_semantics = role is not None or placeholder is not None + if child.get('data-pptx-layer') is not None: + chrome = True + elif has_explicit_semantics: + chrome = is_static_page_frame(role, placeholder) + else: + chrome = is_chrome_id(group_id) targets.append( GroupTarget( slide=svg_path.stem, group_id=group_id, order=visual_index, - chrome=is_chrome_id(group_id), + chrome=chrome, ) ) @@ -204,10 +214,12 @@ def _validate_scope_effects(scope: dict[str, Any], label: str, warnings: list[st def build_scaffold(project_path: Path) -> dict[str, Any]: """Build an editable animation override scaffold from current SVGs. - Chrome groups are omitted — exporter auto-detects them as ``none`` via - ``is_chrome_id`` at render time, so listing them in the scaffold is pure - noise. A ``defaults`` stub is emitted up front to remind the editor that - deck-wide overrides exist and most pages should inherit them. + Chrome groups are omitted — layer/slide-number placeholder semantics are + authoritative, followed by an explicit structural role. ``is_chrome_id`` + remains only for marker-free legacy SVGs. Listing static page framing in + the scaffold would be pure noise. A ``defaults`` stub is emitted up front + to remind the editor that deck-wide overrides exist and most pages should + inherit them. """ targets_by_slide, _anonymous = scan_project_targets(project_path) slides: dict[str, Any] = {} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/context.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/context.py index 72024d1f..d855ed59 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/context.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/context.py @@ -3,10 +3,14 @@ from __future__ import annotations from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any from xml.etree import ElementTree as ET from dataclasses import dataclass, field +if TYPE_CHECKING: + from .theme_colors import ThemeColorSpec + from .theme_fonts import ThemeFontSpec + AffineMatrix = tuple[float, float, float, float, float, float] IDENTITY_MATRIX: AffineMatrix = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0) @@ -67,6 +71,12 @@ class ConvertContext: # Optional per-element conversion diagnostics. Shared by child contexts so # callers can inspect native / skipped / unsupported decisions per slide. trace_events: list[dict[str, Any]] | None = None + # Optional project theme contract. Matching SVG title/body families emit + # DrawingML +mj/+mn tokens instead of fixed typeface names. + theme_font_spec: ThemeFontSpec | None = None + # Optional project theme-color contract. Exact locked colors are promoted + # to context-safe DrawingML scheme slots while local colors stay concrete. + theme_color_spec: ThemeColorSpec | None = None def next_id(self) -> int: """Allocate the next shape ID.""" @@ -178,6 +188,8 @@ class ConvertContext: image_scale=self.image_scale, image_quality=self.image_quality, trace_events=self.trace_events, + theme_font_spec=self.theme_font_spec, + theme_color_spec=self.theme_color_spec, ) def sync_from_child(self, child_ctx: ConvertContext) -> None: diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/converter.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/converter.py index 809f39d3..4aecdb5b 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/converter.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/converter.py @@ -11,6 +11,8 @@ from xml.etree import ElementTree as ET from resource_paths import icon_search_dirs_for_svg from .context import ConvertContext, ShapeResult +from .theme_colors import ThemeColorSpec +from .theme_fonts import ThemeFontSpec from .utils import ( SVG_NS, EMU_PER_PX, _extract_inheritable_styles, _f, _get_attr, parse_transform_matrix, resolve_url_id, @@ -25,42 +27,15 @@ from .elements import ( convert_polygon, convert_polyline, convert_text, convert_image, convert_nested_svg, ) -from ..native_objects import convert_native_object +from ..animation_config import is_chrome_id +from ..native_objects import convert_native_object, native_marker_transform +from ..semantic_markers import is_static_page_frame class SvgNativeConversionError(RuntimeError): """Raised when an SVG cannot be faithfully converted to native DrawingML.""" -# --------------------------------------------------------------------------- -# Animation anchor selection -# --------------------------------------------------------------------------- - -# Tokens that mark a top-level as page chrome rather than animated -# content. When any token (after splitting id on '-' and '_') matches, the group -# is excluded from the per-element entrance animation cascade so background, -# header/footer, decorations etc. appear together with the slide instead of -# requiring presenter clicks. -_CHROME_ID_TOKENS = frozenset({ - 'background', 'bg', - 'decoration', 'decorations', 'decor', - 'header', 'footer', - 'chrome', 'watermark', - 'pagenumber', 'pagenum', - 'nav', 'logo', 'rule', -}) - - -def _is_chrome_id(elem_id: str | None) -> bool: - if not elem_id: - return False - lower = elem_id.lower() - if lower.replace('-', '').replace('_', '') in _CHROME_ID_TOKENS: - return True - tokens = re.split(r'[-_]', lower) - return any(t in _CHROME_ID_TOKENS for t in tokens if t) - - # --------------------------------------------------------------------------- # Transform & layout helpers # --------------------------------------------------------------------------- @@ -166,18 +141,42 @@ def convert_g(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None: keep their absolute slide coordinates unchanged. """ transform = elem.get('transform', '') - dx, dy, sx, sy, angle_deg = parse_transform(transform) + native_subtree_active = ctx.native_objects_enabled and any( + descendant.get('data-pptx-native') + and descendant.tag.replace(f'{{{SVG_NS}}}', '') != 'metadata' + for descendant in elem.iter() + ) + if native_subtree_active: + dx, dy, sx, sy = native_marker_transform(transform) + angle_deg = 0.0 + else: + dx, dy, sx, sy, angle_deg = parse_transform(transform) filter_id = resolve_url_id(elem.get('filter', '')) style_overrides = _extract_inheritable_styles(elem) elem_id = elem.get('id') - should_animate_group = ctx.depth == 0 and elem_id and not _is_chrome_id(elem_id) + semantic_role = elem.get('data-pptx-role') + placeholder = elem.get('data-pptx-placeholder') + has_explicit_semantics = ( + semantic_role is not None or placeholder is not None + ) + is_chrome = ( + is_static_page_frame(semantic_role, placeholder) + if has_explicit_semantics + else is_chrome_id(elem_id) + ) + should_animate_group = ( + ctx.depth == 0 + and elem_id + and not is_chrome + and not elem.get('data-pptx-layer') + ) visual_children = [ child for child in elem if child.tag.replace(f'{{{SVG_NS}}}', '') not in _NON_VISUAL_TAGS ] - matrix_supported = bool(transform) and visual_children and all( + matrix_supported = not native_subtree_active and bool(transform) and visual_children and all( _supports_matrix_transform(child) for child in visual_children ) # A pure ``rotate(angle [cx cy])`` falls through to the fallback path @@ -203,7 +202,14 @@ def convert_g(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None: style_overrides=style_overrides, ) else: - child_ctx = ctx.child(dx, dy, sx, sy, filter_id=filter_id, style_overrides=style_overrides) + child_ctx = ctx.child( + ctx.scale_x * dx if native_subtree_active else dx, + ctx.scale_y * dy if native_subtree_active else dy, + sx, + sy, + filter_id=filter_id, + style_overrides=style_overrides, + ) if child_ctx.native_objects_enabled: native_result = convert_native_object(elem, child_ctx) @@ -420,7 +426,12 @@ def _background_xml_from_rect( ctx: ConvertContext, ) -> str: """Build native ``p:bg`` XML from a full-slide SVG background rect.""" - fill_xml = build_fill_xml(elem, ctx, get_fill_opacity(elem, ctx)) + fill_xml = build_fill_xml( + elem, + ctx, + get_fill_opacity(elem, ctx), + usage="background", + ) if not fill_xml or '{fill_xml}' @@ -506,6 +517,16 @@ def convert_element(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None } if elem_id: event['id'] = elem_id + for attr in ( + 'data-pptx-layer', + 'data-pptx-placeholder', + 'data-pptx-placeholder-bounds', + 'data-pptx-placeholder-idx', + 'data-pptx-role', + ): + value = elem.get(attr) + if value is not None: + event[attr] = value event.update(metadata) ctx.trace_events.append(event) @@ -573,6 +594,8 @@ def convert_svg_to_slide_shapes( image_scale: float = 2.0, image_quality: int = 85, native_objects: bool = False, + theme_font_spec: ThemeFontSpec | None = None, + theme_color_spec: ThemeColorSpec | None = None, trace_out: list[dict[str, Any]] | None = None, ) -> tuple[ str, @@ -600,6 +623,11 @@ def convert_svg_to_slide_shapes( image_quality: JPEG quality used for opaque optimized rasters. native_objects: Convert explicit ``data-pptx-native`` table/chart markers to native PowerPoint objects. Default off. + theme_font_spec: Optional major/minor theme-font contract. Matching SVG + families emit DrawingML theme tokens instead of fixed typefaces. + theme_color_spec: Optional context-aware theme-color contract. Exact + locked colors emit DrawingML scheme tokens while local colors stay + fixed. trace_out: Optional list populated with one per-slide trace dictionary. Returns: @@ -677,6 +705,8 @@ def convert_svg_to_slide_shapes( image_quality=image_quality, native_objects_enabled=native_objects, trace_events=trace_events, + theme_font_spec=theme_font_spec, + theme_color_spec=theme_color_spec, ) shapes: list[str] = [] @@ -705,8 +735,12 @@ def convert_svg_to_slide_shapes( shapes.append(result.xml) converted += 1 m = re.search(r' ShapeResult | None: op = get_fill_opacity(elem, ctx) grad_id = resolve_url_id(stroke_val) if stroke_val else None if grad_id and grad_id in ctx.defs: - fill = build_gradient_fill(ctx.defs[grad_id], op) + fill = build_gradient_fill( + ctx.defs[grad_id], + op, + ctx.theme_color_spec, + "fill", + ) elif stroke_val: color = parse_hex_color(stroke_val) - fill = build_solid_fill(color, op) if color else '' + fill = ( + build_solid_fill(color, op, ctx.theme_color_spec, "fill") + if color else '' + ) else: fill = '' @@ -1209,12 +1219,18 @@ def _bullet_indent_px(bullet: dict[str, Any], font_size: float) -> float: return -_bullet_margin_px(bullet, font_size) -def _build_bullet_xml(bullet: dict[str, Any] | None) -> str: +def _build_bullet_xml( + bullet: dict[str, Any] | None, + ctx: ConvertContext | None, +) -> str: if not bullet: return '' fill = bullet.get('fill') if isinstance(fill, str) and re.fullmatch(r'[0-9A-Fa-f]{6}', fill): - color_xml = f'' + theme_spec = ctx.theme_color_spec if ctx is not None else None + color_xml = ( + f'{color_node_xml(fill, theme_spec, "text")}' + ) else: color_xml = '' return ( @@ -1229,13 +1245,14 @@ def _paragraph_pr_xml( font_size: float, body_xml: str = '', bullet: dict[str, Any] | None = None, + ctx: ConvertContext | None = None, ) -> str: attrs = f'algn="{algn}"' if bullet: margin = px_to_emu(_bullet_margin_px(bullet, font_size)) indent = px_to_emu(_bullet_indent_px(bullet, font_size)) attrs += f' marL="{margin}" indent="{indent}"' - return f'{body_xml}{_build_bullet_xml(bullet)}' + return f'{body_xml}{_build_bullet_xml(bullet, ctx)}' def _estimate_bullet_line_width(runs: list[dict[str, Any]]) -> float: @@ -1387,15 +1404,28 @@ def _build_text_fill_xml( grad_id = resolve_url_id(fill_raw) if grad_id and ctx and grad_id in ctx.defs: - return build_gradient_fill(ctx.defs[grad_id], opacity) + return build_gradient_fill( + ctx.defs[grad_id], + opacity, + ctx.theme_color_spec, + "text", + ) alpha_xml = '' if opacity is not None and opacity < 1.0: alpha_xml = f'' - return f'{alpha_xml}' + theme_spec = ctx.theme_color_spec if ctx is not None else None + return ( + '' + f'{color_node_xml(fill, theme_spec, "text", alpha_xml)}' + '' + ) -def _build_text_outline_xml(run: dict[str, Any]) -> str: +def _build_text_outline_xml( + run: dict[str, Any], + ctx: ConvertContext | None, +) -> str: """Build DrawingML outline XML for a text run from SVG stroke attributes.""" stroke_raw = run.get('stroke_raw') if not stroke_raw or stroke_raw.strip().lower() in ('none', 'transparent'): @@ -1411,9 +1441,12 @@ def _build_text_outline_xml(run: dict[str, Any]) -> str: if stroke_opacity is not None and stroke_opacity < 1.0: alpha_xml = f'' + theme_spec = ctx.theme_color_spec if ctx is not None else None return ( f'' - f'{alpha_xml}' + '' + f'{color_node_xml(color, theme_spec, "stroke", alpha_xml)}' + '' '' ) @@ -1449,11 +1482,14 @@ def _build_run_xml( spc_attr = _letter_spacing_to_drawingml_spc(letter_spacing_px) fonts = parse_font_family(ff) if ff else default_fonts - run_fonts = resolve_text_run_fonts(text, fonts) + run_fonts = theme_font_tokens( + fonts, + ctx.theme_font_spec if ctx is not None else None, + ) or resolve_text_run_fonts(text, fonts) lang = detect_text_lang(text) fill_xml = _build_text_fill_xml(fill, fill_raw, opacity, ctx) - outline_xml = _build_text_outline_xml(run) + outline_xml = _build_text_outline_xml(run, ctx) space_attr = ' xml:space="preserve"' if text != text.strip() or ' ' in text else '' @@ -1711,6 +1747,7 @@ def convert_text(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None: font_size=float(line[0].get('font_size', font_size)) if line else font_size, body_xml=f'{ln_spc_xml}{spc_bef_xml}', bullet=bullet, + ctx=ctx, ) paragraph_xml_chunks.append( f'\n{p_pr_xml}\n{runs_inner}\n' @@ -1722,6 +1759,7 @@ def convert_text(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None: algn=algn, font_size=float(runs[0].get('font_size', font_size)) if runs else font_size, bullet=single_bullet, + ctx=ctx, ) paragraphs_xml = f'\n{p_pr_xml}\n{runs_xml}\n' diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/styles.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/styles.py index f6280c36..554e52cb 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/styles.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/styles.py @@ -7,6 +7,7 @@ import re from xml.etree import ElementTree as ET from .context import ConvertContext +from .theme_colors import ThemeColorSpec, color_node_xml from .utils import ( SVG_NS, ANGLE_UNIT, DASH_PRESETS, px_to_emu, _f, _get_attr, parse_svg_length, @@ -14,17 +15,28 @@ from .utils import ( ) -def build_solid_fill(color: str, opacity: float | None = None) -> str: +def build_solid_fill( + color: str, + opacity: float | None = None, + theme_color_spec: ThemeColorSpec | None = None, + usage: str = "fill", +) -> str: """Build XML.""" alpha = '' if opacity is not None and opacity < 1.0: alpha = f'' - return f'{alpha}' + return ( + '' + f'{color_node_xml(color, theme_color_spec, usage, alpha)}' + '' + ) def build_gradient_fill( grad_elem: ET.Element, opacity: float | None = None, + theme_color_spec: ThemeColorSpec | None = None, + usage: str = "fill", ) -> str: """Build from SVG linearGradient or radialGradient element.""" tag = grad_elem.tag.replace(f'{{{SVG_NS}}}', '') @@ -67,7 +79,9 @@ def build_gradient_fill( alpha_xml = f'' stops_xml.append( - f'{alpha_xml}' + f'' + f'{color_node_xml(color, theme_color_spec, usage, alpha_xml)}' + '' ) if not stops_xml: @@ -112,6 +126,7 @@ def build_fill_xml( elem: ET.Element, ctx: ConvertContext, opacity: float | None = None, + usage: str = "fill", ) -> str: """Build fill XML for a shape element, with inherited style support.""" fill = _get_attr(elem, 'fill', ctx) @@ -126,15 +141,25 @@ def build_fill_xml( ref_elem = ctx.defs[ref_id] ref_tag = ref_elem.tag.replace(f'{{{SVG_NS}}}', '') if ref_tag == 'pattern': - patt_xml = build_pattern_fill(ref_elem, opacity) + patt_xml = build_pattern_fill( + ref_elem, + opacity, + ctx.theme_color_spec, + usage, + ) if patt_xml: return patt_xml return '' - return build_gradient_fill(ref_elem, opacity) + return build_gradient_fill( + ref_elem, + opacity, + ctx.theme_color_spec, + usage, + ) color = parse_hex_color(fill) if color: - return build_solid_fill(color, opacity) + return build_solid_fill(color, opacity, ctx.theme_color_spec, usage) return '' @@ -142,6 +167,8 @@ def build_fill_xml( def build_pattern_fill( pattern_elem: ET.Element, opacity: float | None = None, + theme_color_spec: ThemeColorSpec | None = None, + usage: str = "fill", ) -> str: """Build from an SVG emitted by pptx_to_svg. @@ -172,11 +199,11 @@ def build_pattern_fill( if opacity is not None and opacity < 1.0: alpha_xml = f'' - fg_xml = f'{alpha_xml}' + fg_xml = color_node_xml(fg_hex, theme_color_spec, usage, alpha_xml) if bg_hex: - bg_xml = f'' + bg_xml = color_node_xml(bg_hex, theme_color_spec, usage) else: - bg_xml = '' + bg_xml = color_node_xml('FFFFFF', theme_color_spec, usage) return ( f'' @@ -396,7 +423,12 @@ def build_stroke_xml( # Gradient stroke grad_id = resolve_url_id(stroke) if grad_id and grad_id in ctx.defs: - grad_fill = build_gradient_fill(ctx.defs[grad_id], opacity) + grad_fill = build_gradient_fill( + ctx.defs[grad_id], + opacity, + ctx.theme_color_spec, + "stroke", + ) return f'{grad_fill}{dash_xml}{join_xml}{line_ends}' # Solid color stroke @@ -408,8 +440,9 @@ def build_stroke_xml( if opacity is not None and opacity < 1.0: alpha_xml = f'' + color_xml = color_node_xml(color, ctx.theme_color_spec, "stroke", alpha_xml) return f''' -{alpha_xml}{dash_xml}{join_xml}{line_ends} +{color_xml}{dash_xml}{join_xml}{line_ends} ''' diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/theme_colors.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/theme_colors.py new file mode 100644 index 00000000..2a5bf880 --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/theme_colors.py @@ -0,0 +1,276 @@ +"""Theme-color contracts shared by SVG conversion and PPTX package assembly.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from pathlib import Path +from xml.etree import ElementTree as ET + +from .utils import parse_hex_color + + +DML_NS = "http://schemas.openxmlformats.org/drawingml/2006/main" +_LOCK_ROW_RE = re.compile(r"^-\s+([A-Za-z0-9_]+)\s*:\s*(.+?)\s*$") +_SRGB_PAIR_RE = re.compile( + r"[^>]*?)(?(?P.*?)", + re.DOTALL, +) +_SRGB_EMPTY_RE = re.compile(r"[^>]*)/>") +_VAL_RE = re.compile(r'\bval="(?P[0-9A-Fa-f]{6})"') + +_OFFICE_DEFAULTS = { + "dk1": "000000", + "lt1": "FFFFFF", + "dk2": "1F497D", + "lt2": "EEECE1", + "accent1": "4F81BD", + "accent2": "C0504D", + "accent3": "9BBB59", + "accent4": "8064A2", + "accent5": "4BACC6", + "accent6": "F79646", + "hlink": "0000FF", + "folHlink": "800080", +} +_ROLE_SLOTS = { + "bg": "lt1", + "background": "lt1", + "master_bg": "lt1", + "secondary_bg": "lt2", + "bg_secondary": "lt2", + "text": "dk1", + "body_text": "dk1", + "text_secondary": "dk2", + "primary": "accent1", + "accent": "accent2", + "secondary_accent": "accent3", + "border": "accent4", +} +_USAGE_ROLE_ORDER = { + "background": ( + "bg", "background", "master_bg", "secondary_bg", "bg_secondary", + "primary", "accent", "secondary_accent", + ), + "fill": ( + "primary", "accent", "secondary_accent", "bg", "background", + "master_bg", "secondary_bg", "bg_secondary", "text", "body_text", + "text_secondary", "border", + ), + "text": ( + "text", "body_text", "text_secondary", "primary", "accent", + "secondary_accent", + ), + "stroke": ( + "border", "primary", "accent", "secondary_accent", "text", + "body_text", "text_secondary", + ), + "chart": ("primary", "accent", "secondary_accent"), +} +_BACKGROUND_ROLE_TOKENS = ( + "bg", "background", "surface", "paper", "card", "panel", "tint", +) +_TEXT_ROLE_TOKENS = ("text", "ink", "muted") +_STROKE_ROLE_TOKENS = ("border", "grid", "line", "stroke") +_BACKGROUND_USAGES = frozenset({"background", "fill"}) +_TEXT_USAGES = frozenset({"text"}) +_STROKE_USAGES = frozenset({"stroke", "fill"}) +_SEMANTIC_USAGES = frozenset({"fill", "text", "stroke"}) + + +class ThemeColorError(RuntimeError): + """Raised when a project theme-color contract cannot be loaded or applied.""" + + +@dataclass(frozen=True) +class ThemeColorSpec: + """PowerPoint color scheme derived from one project's color lock.""" + + slots: dict[str, str] + roles: dict[str, str] + role_slots: dict[str, str] + extra_roles: tuple[str, ...] = () + + def scheme_for(self, color: str, usage: str) -> str | None: + """Resolve one concrete color to a safe scheme slot for its usage.""" + normalized = parse_hex_color(color) + if normalized is None: + return None + for role in _USAGE_ROLE_ORDER.get(usage, ()): + if self.roles.get(role) == normalized: + slot = self.role_slots[role] + if self.slots.get(slot) == normalized: + return slot + for role in self.extra_roles: + if usage in _extra_role_usages(role) and self.roles.get(role) == normalized: + slot = self.role_slots[role] + if self.slots.get(slot) == normalized: + return slot + return None + + +def _extra_role_usages(role: str) -> frozenset[str]: + """Limit extra theme slots to contexts implied by their lock role name.""" + normalized = role.lower() + if any(token in normalized for token in _BACKGROUND_ROLE_TOKENS): + return _BACKGROUND_USAGES + if any(token in normalized for token in _TEXT_ROLE_TOKENS): + return _TEXT_USAGES + if any(token in normalized for token in _STROKE_ROLE_TOKENS): + return _STROKE_USAGES + return _SEMANTIC_USAGES + + +def _color_rows(lock_path: Path) -> dict[str, str]: + rows: dict[str, str] = {} + current_section: str | None = None + try: + lines = lock_path.read_text(encoding="utf-8").splitlines() + except OSError as exc: + raise ThemeColorError(f"Cannot read {lock_path}: {exc}") from exc + for raw_line in lines: + line = raw_line.strip() + if line.startswith("## "): + current_section = line[3:].strip() + continue + if current_section != "colors": + continue + match = _LOCK_ROW_RE.fullmatch(line) + if not match: + continue + color = parse_hex_color(match.group(2)) + if color is not None: + rows[match.group(1)] = color + return rows + + +def _first_color(roles: dict[str, str], *keys: str, default: str) -> str: + return next((roles[key] for key in keys if key in roles), default) + + +def load_theme_color_spec(project_path: Path) -> ThemeColorSpec | None: + """Load a PowerPoint color scheme from ``spec_lock.md`` color rows.""" + lock_path = project_path / "spec_lock.md" + if not lock_path.is_file(): + return None + roles = _color_rows(lock_path) + if not roles: + return None + + extra_roles = tuple( + key + for key, color in roles.items() + if key not in _ROLE_SLOTS and color not in {"000000", "FFFFFF"} + )[:2] + extra_slots = dict(zip(extra_roles, ("accent5", "accent6"))) + role_slots = {**_ROLE_SLOTS, **extra_slots} + + slots = dict(_OFFICE_DEFAULTS) + slots.update({ + "lt1": _first_color( + roles, + "bg", "background", "master_bg", + default=slots["lt1"], + ), + "dk1": _first_color( + roles, + "text", "body_text", "primary", + default=slots["dk1"], + ), + "lt2": _first_color( + roles, + "secondary_bg", "bg_secondary", "bg", "background", "master_bg", + default=slots["lt2"], + ), + "dk2": _first_color(roles, "text_secondary", "text", default=slots["dk2"]), + "accent1": _first_color(roles, "primary", "accent", default=slots["accent1"]), + "accent2": _first_color( + roles, + "accent", "secondary_accent", "primary", + default=slots["accent2"], + ), + "accent3": _first_color(roles, "secondary_accent", "accent", default=slots["accent3"]), + "accent4": _first_color(roles, "border", "primary", default=slots["accent4"]), + "hlink": _first_color(roles, "accent", "primary", default=slots["hlink"]), + "folHlink": _first_color(roles, "secondary_accent", "accent", default=slots["folHlink"]), + }) + for role, slot in extra_slots.items(): + slots[slot] = roles[role] + return ThemeColorSpec( + slots=slots, + roles=roles, + role_slots=role_slots, + extra_roles=extra_roles, + ) + + +def color_node_xml( + color: str, + spec: ThemeColorSpec | None, + usage: str, + inner_xml: str = "", +) -> str: + """Build an srgbClr or schemeClr node while preserving child transforms.""" + normalized = parse_hex_color(color) or color.strip().lstrip("#").upper() + scheme = spec.scheme_for(normalized, usage) if spec is not None else None + if scheme: + return f'{inner_xml}' + return f'{inner_xml}' + + +def _set_scheme_color(parent: ET.Element, slot: str, color: str) -> None: + target = parent.find(f"{{{DML_NS}}}{slot}") + if target is None: + target = ET.SubElement(parent, f"{{{DML_NS}}}{slot}") + for child in list(target): + target.remove(child) + ET.SubElement(target, f"{{{DML_NS}}}srgbClr", {"val": color}) + + +def apply_theme_color_spec(extract_dir: Path, spec: ThemeColorSpec) -> None: + """Install the locked color scheme into every existing PPTX theme part.""" + theme_dir = extract_dir / "ppt" / "theme" + theme_paths = sorted(theme_dir.glob("theme*.xml")) + if not theme_paths: + raise ThemeColorError(f"PPTX package has no theme part under {theme_dir}") + + ET.register_namespace("a", DML_NS) + for theme_path in theme_paths: + try: + tree = ET.parse(theme_path) + except (OSError, ET.ParseError) as exc: + raise ThemeColorError(f"Cannot parse {theme_path}: {exc}") from exc + color_scheme = tree.getroot().find(f".//{{{DML_NS}}}clrScheme") + if color_scheme is None: + raise ThemeColorError(f"Theme has no clrScheme: {theme_path}") + color_scheme.set("name", "PPT Master") + for slot, color in spec.slots.items(): + _set_scheme_color(color_scheme, slot, color) + tree.write(theme_path, encoding="utf-8", xml_declaration=True) + + +def rewrite_chart_accent_colors(data: bytes, spec: ThemeColorSpec | None) -> bytes: + """Promote exact locked accent colors inside native chart XML.""" + if spec is None or b" str: + attrs = match.group("attrs") + value_match = _VAL_RE.search(attrs) + if value_match is None: + return match.group(0) + scheme = spec.scheme_for(value_match.group("color"), "chart") + if scheme is None: + return match.group(0) + new_attrs = _VAL_RE.sub(f'val="{scheme}"', attrs, count=1) + if paired: + return f'{match.group("body")}' + return f'' + + text = _SRGB_PAIR_RE.sub(lambda match: replacement(match, paired=True), text) + text = _SRGB_EMPTY_RE.sub(lambda match: replacement(match, paired=False), text) + return text.encode("utf-8") diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/theme_fonts.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/theme_fonts.py new file mode 100644 index 00000000..0280990b --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/drawingml/theme_fonts.py @@ -0,0 +1,153 @@ +"""Theme-font contracts shared by SVG conversion and PPTX package assembly.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from pathlib import Path +from xml.etree import ElementTree as ET + +from .utils import parse_font_family + + +DML_NS = "http://schemas.openxmlformats.org/drawingml/2006/main" +_LOCK_ROW_RE = re.compile(r"^-\s+([A-Za-z0-9_]+)\s*:\s*(.+?)\s*$") +_CJK_THEME_SCRIPTS = frozenset({"Hans", "Hant", "Jpan", "Hang"}) + + +class ThemeFontError(RuntimeError): + """Raised when a project theme-font contract cannot be loaded or applied.""" + + +@dataclass(frozen=True) +class ThemeFontFace: + """Concrete Latin, East Asian, and complex-script theme faces.""" + + latin: str + ea: str + cs: str + + def matches(self, fonts: dict[str, str]) -> bool: + """Return whether resolved SVG fonts represent this theme face.""" + return fonts.get("latin") == self.latin and fonts.get("ea") == self.ea + + +@dataclass(frozen=True) +class ThemeFontSpec: + """Major/minor theme fonts derived from one project's typography lock.""" + + major: ThemeFontFace + minor: ThemeFontFace + major_family: str + minor_family: str + + +def _font_face(font_family: str) -> ThemeFontFace: + fonts = parse_font_family(font_family) + return ThemeFontFace( + latin=fonts["latin"], + ea=fonts["ea"], + cs=fonts["latin"], + ) + + +def _typography_rows(lock_path: Path) -> dict[str, str]: + rows: dict[str, str] = {} + current_section: str | None = None + try: + lines = lock_path.read_text(encoding="utf-8").splitlines() + except OSError as exc: + raise ThemeFontError(f"Cannot read {lock_path}: {exc}") from exc + + for raw_line in lines: + line = raw_line.strip() + if line.startswith("## "): + current_section = line[3:].strip() + continue + if current_section != "typography": + continue + match = _LOCK_ROW_RE.fullmatch(line) + if match: + rows[match.group(1)] = match.group(2) + return rows + + +def load_theme_font_spec(project_path: Path) -> ThemeFontSpec | None: + """Load major/minor theme fonts from ``spec_lock.md`` typography rows.""" + lock_path = project_path / "spec_lock.md" + if not lock_path.is_file(): + return None + rows = _typography_rows(lock_path) + default_family = rows.get("font_family") + major_family = rows.get("title_family") or default_family + minor_family = rows.get("body_family") or default_family + if not major_family or not minor_family: + return None + return ThemeFontSpec( + major=_font_face(major_family), + minor=_font_face(minor_family), + major_family=major_family, + minor_family=minor_family, + ) + + +def theme_font_tokens( + fonts: dict[str, str], + spec: ThemeFontSpec | None, +) -> dict[str, str] | None: + """Return DrawingML major/minor tokens for a locked SVG font face.""" + if spec is None: + return None + major_match = spec.major.matches(fonts) + minor_match = spec.minor.matches(fonts) + if major_match and not minor_match: + prefix = "+mj" + elif minor_match: + # When title/body use the same family, minor is the least surprising + # default for ordinary text boxes. Template assembly forces semantic + # title placeholders to the major role after SVG conversion. + prefix = "+mn" + else: + return None + return { + "latin": f"{prefix}-lt", + "ea": f"{prefix}-ea", + "cs": f"{prefix}-cs", + } + + +def _patch_font_collection(collection: ET.Element, face: ThemeFontFace) -> None: + for tag, value in (("latin", face.latin), ("ea", face.ea), ("cs", face.cs)): + elem = collection.find(f"{{{DML_NS}}}{tag}") + if elem is None: + elem = ET.SubElement(collection, f"{{{DML_NS}}}{tag}") + elem.set("typeface", value) + for supplemental in collection.findall(f"{{{DML_NS}}}font"): + if supplemental.get("script") in _CJK_THEME_SCRIPTS: + supplemental.set("typeface", face.ea) + + +def apply_theme_font_spec(extract_dir: Path, spec: ThemeFontSpec) -> None: + """Install locked major/minor fonts into every existing PPTX theme part.""" + theme_dir = extract_dir / "ppt" / "theme" + theme_paths = sorted(theme_dir.glob("theme*.xml")) + if not theme_paths: + raise ThemeFontError(f"PPTX package has no theme part under {theme_dir}") + + ET.register_namespace("a", DML_NS) + for theme_path in theme_paths: + try: + tree = ET.parse(theme_path) + except (OSError, ET.ParseError) as exc: + raise ThemeFontError(f"Cannot parse {theme_path}: {exc}") from exc + font_scheme = tree.getroot().find(f".//{{{DML_NS}}}fontScheme") + if font_scheme is None: + raise ThemeFontError(f"Theme has no fontScheme: {theme_path}") + major = font_scheme.find(f"{{{DML_NS}}}majorFont") + minor = font_scheme.find(f"{{{DML_NS}}}minorFont") + if major is None or minor is None: + raise ThemeFontError(f"Theme has no major/minor font collection: {theme_path}") + font_scheme.set("name", "PPT Master") + _patch_font_collection(major, spec.major) + _patch_font_collection(minor, spec.minor) + tree.write(theme_path, encoding="utf-8", xml_declaration=True) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/__init__.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/__init__.py index 3b8ae27d..2d202a88 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/__init__.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/__init__.py @@ -18,6 +18,7 @@ from .chart_style import ( _native_chart_chrome_errors, _native_chart_chrome_warnings, _native_chart_export_payload, + _validate_chart_companion_boxes, ) from .chart_xml import _chart_rels_xml, _chart_xml from .chartex import ( @@ -39,7 +40,9 @@ from .marker_common import ( _bounds, _load_payload, _local_tag, + _native_marker_validation_context, _validate_bounds_inputs, + native_marker_transform, ) from .table import ( _build_native_table, @@ -55,6 +58,7 @@ from .workbook import ( __all__ = [ "convert_native_object", "native_object_marker_warnings", + "native_marker_transform", "validate_native_object_marker", "validate_native_object_marker_with_warnings", ] @@ -164,6 +168,8 @@ def _validate_native_object_marker_payload( elem: ET.Element, *, validate_chrome: bool = True, + ctx: ConvertContext | None = None, + ancestors: tuple[ET.Element, ...] = (), ) -> tuple[str, dict[str, Any], list[list[Any]] | None]: kind = (elem.get("data-pptx-native") or "").strip().lower() if not kind: @@ -172,32 +178,52 @@ def _validate_native_object_marker_payload( raise RuntimeError(f"Unsupported data-pptx-native value: {kind}") if _local_tag(elem) != "g": raise RuntimeError("Native PPTX table/chart markers must be elements") - transform = elem.get("transform", "") - if transform and any(token in transform for token in ("rotate", "matrix", "skew")): - raise RuntimeError("Native PPTX table/chart markers support translate/scale transforms only") + native_marker_transform(elem.get("transform")) payload = _load_payload(elem, kind) - _validate_bounds_inputs(elem, payload) + bounds_ctx = ctx or _native_marker_validation_context(elem, ancestors) + off_x, off_y, ext_cx, ext_cy, _ = _validate_bounds_inputs(elem, payload, bounds_ctx) table_rows = None if kind == "table": - table_rows, _ = _validate_table_payload(payload) + table_rows, col_count = _validate_table_payload(payload) + if ext_cx < col_count or ext_cy < len(table_rows): + raise RuntimeError( + "Native PPTX table bounds must provide at least one EMU per row and column" + ) else: - _chart_data(payload) - if validate_chrome: + chart_data = _chart_data(payload) + _validate_chart_companion_boxes( + payload, + chart_bounds=(off_x, off_y, ext_cx, ext_cy), + include_title=chart_data["kind"] == "chartex", + include_subtitle_as_caption=chart_data["kind"] == "chartex", + ) + if validate_chrome and elem.get("data-pptx-native-source") != "pptx": chrome_errors = _native_chart_chrome_errors(elem, payload) if chrome_errors: raise RuntimeError("; ".join(chrome_errors)) return kind, payload, table_rows -def validate_native_object_marker(elem: ET.Element) -> None: +def validate_native_object_marker( + elem: ET.Element, + *, + ancestors: tuple[ET.Element, ...] = (), +) -> None: """Validate a data-pptx-native marker without mutating the PPTX package.""" - _validate_native_object_marker_payload(elem) + _validate_native_object_marker_payload(elem, ancestors=ancestors) -def validate_native_object_marker_with_warnings(elem: ET.Element) -> list[str]: +def validate_native_object_marker_with_warnings( + elem: ET.Element, + *, + ancestors: tuple[ET.Element, ...] = (), +) -> list[str]: """Validate a data-pptx-native marker and return non-fatal warnings.""" - kind, payload, table_rows = _validate_native_object_marker_payload(elem) + kind, payload, table_rows = _validate_native_object_marker_payload( + elem, + ancestors=ancestors, + ) if kind == "table" and table_rows is not None: return _native_table_warnings(elem, table_rows) if kind == "chart": @@ -205,9 +231,13 @@ def validate_native_object_marker_with_warnings(elem: ET.Element) -> list[str]: return [] -def native_object_marker_warnings(elem: ET.Element) -> list[str]: +def native_object_marker_warnings( + elem: ET.Element, + *, + ancestors: tuple[ET.Element, ...] = (), +) -> list[str]: """Return non-fatal warnings for a data-pptx-native marker.""" - return validate_native_object_marker_with_warnings(elem) + return validate_native_object_marker_with_warnings(elem, ancestors=ancestors) def convert_native_object(elem: ET.Element, ctx: ConvertContext) -> ShapeResult | None: @@ -216,7 +246,11 @@ def convert_native_object(elem: ET.Element, ctx: ConvertContext) -> ShapeResult if not kind: return None - kind, payload, _ = _validate_native_object_marker_payload(elem, validate_chrome=False) + kind, payload, _ = _validate_native_object_marker_payload( + elem, + validate_chrome=False, + ctx=ctx, + ) if kind == "table": return _build_native_table(elem, ctx, payload) payload, warnings = _native_chart_export_payload(elem, payload) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_data.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_data.py index 284be141..71443167 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_data.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_data.py @@ -2,6 +2,7 @@ from __future__ import annotations +import math from typing import Any from .marker_common import ( @@ -9,7 +10,9 @@ from .marker_common import ( _clean_hex, _compact_key, _first_present, + _hex_or_none, _number, + _powerpoint_line_width_emu, ) @@ -18,8 +21,10 @@ def _chart_number(value: Any) -> int | float: raise RuntimeError("Native PPTX chart values must be numeric") try: number = float(value) - except (TypeError, ValueError) as exc: - raise RuntimeError(f"Native PPTX chart value is not numeric: {value}") from exc + except (TypeError, ValueError, OverflowError) as exc: + raise RuntimeError("Native PPTX chart value is not numeric") from exc + if not math.isfinite(number): + raise RuntimeError(f"Native PPTX chart value must be finite: {value}") return int(number) if number.is_integer() else number @@ -42,6 +47,136 @@ def _data_labels_config(payload: dict[str, Any]) -> dict[str, Any] | None: return raw +def _data_label_position(value: Any, chart_type: str, grouping: str | None) -> str | None: + """Normalize and validate a label position for its chart plot.""" + if chart_type == "area": + if value is not None: + raise RuntimeError("Native PPTX area data labels do not support label position") + return None + is_stacked = chart_type in {"bar", "column"} and grouping in { + "percentStacked", "stacked", + } + default = "ctr" if is_stacked else ( + "outEnd" if chart_type in {"bar", "column"} else "t" + ) + if value is None: + return default + aliases = { + "above": "t", + "bestfit": "bestFit", + "center": "ctr", + "inbase": "inBase", + "insidebase": "inBase", + "insideend": "inEnd", + "inend": "inEnd", + "outend": "outEnd", + "outsideend": "outEnd", + } + position = aliases.get(_compact_key(value)) + if not position: + raise RuntimeError( + "Native PPTX chart data label position must be one of: " + "above, best_fit, center, inside_base, inside_end, outside_end" + ) + if chart_type in {"bar", "column"}: + if position not in {"ctr", "inBase", "inEnd", "outEnd"}: + raise RuntimeError( + "Native PPTX bar/column data label position must be one of: " + "center, inside_base, inside_end, outside_end" + ) + if is_stacked and position == "outEnd": + raise RuntimeError( + "Native PPTX stacked bar/column data labels do not support outside_end" + ) + elif chart_type == "line" and position not in {"bestFit", "ctr", "t"}: + raise RuntimeError( + "Native PPTX line data label position must be one of: above, best_fit, center" + ) + return position + + +def _chart_data_labels( + payload: dict[str, Any], + chart_type: str, + grouping: str | None, + point_count: int, +) -> dict[str, Any] | None: + config = _data_labels_config(payload) + if config is None: + return None + if chart_type not in {"area", "bar", "column", "line"}: + raise RuntimeError( + f"Native PPTX {chart_type} chart data labels are outside current support" + ) + _data_label_position(config.get("position"), chart_type, grouping) + if config.get("color") is not None and _hex_or_none(config["color"]) is None: + raise RuntimeError("Native PPTX chart data_labels.color must be a color") + point_items = _data_label_point_items( + config, + chart_type, + grouping, + point_count, + ) + for item in point_items: + if item.get("color") is not None and _hex_or_none(item["color"]) is None: + raise RuntimeError( + "Native PPTX chart data_labels.points color must be a color" + ) + colors = _chart_list( + _first_present( + config.get("colors"), + config.get("label_colors"), + config.get("labelColors"), + ), + "data_labels.colors", + ) + if colors and len(colors) != point_count: + raise RuntimeError("Native PPTX chart data_labels.colors must match point count") + if any(_hex_or_none(color) is None for color in colors): + raise RuntimeError("Native PPTX chart data_labels.colors entries must be colors") + return config + + +def _data_label_point_items( + config: dict[str, Any], + chart_type: str, + grouping: str | None, + point_count: int, +) -> list[dict[str, Any]]: + """Normalize selected point labels and validate their plot semantics.""" + raw_points = config.get("points") + if raw_points is None: + return [] + items: list[dict[str, Any]] = [] + seen: set[int] = set() + for item in _chart_list(raw_points, "data_labels.points"): + if isinstance(item, dict): + raw_index = item.get("idx") + data = dict(item) + else: + raw_index = item + data = {} + if isinstance(raw_index, bool): + raise RuntimeError("Native PPTX chart data_labels.points idx must be an integer") + index_value = _number(raw_index, "data_labels.points idx") + if not index_value.is_integer(): + raise RuntimeError("Native PPTX chart data_labels.points idx must be an integer") + index = int(index_value) + if index < 0 or index >= point_count: + raise RuntimeError("Native PPTX chart data_labels.points idx is outside point range") + if index in seen: + raise RuntimeError("Native PPTX chart data_labels.points idx values must be unique") + _data_label_position( + _first_present(data.get("position"), config.get("position")), + chart_type, + grouping, + ) + seen.add(index) + data["idx"] = index + items.append(data) + return items + + _CATEGORY_CHART_TYPES = { "area", "bar", @@ -345,12 +480,21 @@ def _category_series(payload: dict[str, Any], categories: list[str]) -> list[dic item.get("fillOpacity"), ) if fill_opacity is not None: + fill_opacity = _number(fill_opacity, "series fill_opacity") + if not 0 <= fill_opacity <= 1: + raise RuntimeError( + "Native PPTX chart series fill_opacity must be between 0 and 1" + ) series_item["fill_opacity"] = fill_opacity line_width = _first_present( item.get("line_width"), item.get("lineWidth"), ) if line_width is not None: + line_width = _number(line_width, "series line_width") + if line_width <= 0: + raise RuntimeError("Native PPTX chart series line_width must be positive") + _powerpoint_line_width_emu(line_width, "series line_width") series_item["line_width"] = line_width series.append(series_item) return series @@ -398,13 +542,16 @@ def _category_chart_data( if alias_style == "exploded" or payload.get("exploded"): raise RuntimeError("Native PPTX exploded pie/doughnut is outside current basic chart support") + grouping = ( + _chart_grouping(chart_type, payload, alias_grouping) + if chart_type in {"bar", "column", "line", "area"} + else None + ) return { "kind": "category", "type": chart_type, "categories": categories, - "grouping": _chart_grouping(chart_type, payload, alias_grouping) - if chart_type in {"bar", "column", "line", "area"} - else None, + "grouping": grouping, "of_pie_type": of_pie_type, "line_style": line_style, "radar_marker_style": radar_marker_style, @@ -418,7 +565,12 @@ def _category_chart_data( ), True, ), - "data_labels": _data_labels_config(payload), + "data_labels": _chart_data_labels( + payload, + chart_type, + grouping, + len(categories), + ), "series": series, } @@ -475,12 +627,20 @@ def _combo_plot_entry( "Native PPTX combo line plot with series fill_opacity requires area_fill: true" ) plot_series = fallback_series or _category_series(plot_payload, categories) + grouping = ( + _chart_grouping(chart_type, plot_payload, alias_grouping) + if chart_type in {"area", "column", "line"} + else None + ) entry: dict[str, Any] = { "axis": _combo_axis_name(plot_payload), - "data_labels": _data_labels_config(plot_payload), - "grouping": _chart_grouping(chart_type, plot_payload, alias_grouping) - if chart_type in {"area", "column", "line"} - else None, + "data_labels": _chart_data_labels( + plot_payload, + chart_type, + grouping, + len(categories), + ), + "grouping": grouping, "series": plot_series, "type": chart_type, } @@ -523,7 +683,11 @@ def _combo_chart_data(payload: dict[str, Any]) -> dict[str, Any]: previous.get("line_style"), previous.get("type"), ) if previous else None - if previous is not None and signature == previous_signature: + if ( + previous is not None + and signature == previous_signature + and plot.get("data_labels") == previous.get("data_labels") + ): previous["series"].extend(plot["series"]) else: plots.append(plot) @@ -645,11 +809,27 @@ def _chartex_chart_data(payload: dict[str, Any], chart_type: str) -> dict[str, A "values": values, } if chart_type == "waterfall": - subtotals = payload.get("subtotals", payload.get("subtotal_indices", [])) - data["subtotals"] = [ - int(_chart_number(value)) - for value in _chart_list(subtotals, "subtotals") - ] + raw_subtotals = payload.get( + "subtotals", + payload.get("subtotal_indices", []), + ) + subtotals: list[int] = [] + seen_subtotals: set[int] = set() + for value in _chart_list(raw_subtotals, "subtotals"): + index = _chart_number(value) + if not isinstance(index, int): + raise RuntimeError("Native PPTX waterfall subtotal indices must be integers") + if index < 0 or index >= len(values): + raise RuntimeError( + "Native PPTX waterfall subtotal index is outside point range" + ) + if index in seen_subtotals: + raise RuntimeError( + "Native PPTX waterfall subtotal indices must be unique" + ) + seen_subtotals.add(index) + subtotals.append(index) + data["subtotals"] = subtotals return data if chart_type == "box_whisker": @@ -684,6 +864,8 @@ def _chartex_chart_data(payload: dict[str, Any], chart_type: str) -> dict[str, A def _stock_chart_data(payload: dict[str, Any]) -> dict[str, Any]: + if _data_labels_config(payload) is not None: + raise RuntimeError("Native PPTX stock chart data labels are outside current support") categories = [ _chart_number(item) for item in _chart_list(payload.get("categories", payload.get("dates", [])), "categories") @@ -725,6 +907,10 @@ def _xy_chart_data( chart_type: str, alias_style: str | None, ) -> dict[str, Any]: + if _data_labels_config(payload) is not None: + raise RuntimeError( + f"Native PPTX {chart_type} chart data labels are outside current support" + ) raw_series = payload.get("series", []) if not isinstance(raw_series, list) or not raw_series: raise RuntimeError("Native PPTX XY chart requires non-empty series") @@ -789,6 +975,13 @@ def _xy_chart_data( def _chart_data(payload: dict[str, Any]) -> dict[str, Any]: chart_type, alias_grouping, alias_style = _chart_kind(payload) + if ( + chart_type not in _CATEGORY_CHART_TYPES | {"combo", "stock"} | _XY_CHART_TYPES + and _data_labels_config(payload) is not None + ): + raise RuntimeError( + f"Native PPTX {chart_type} chart data labels are outside current support" + ) if chart_type == "combo": return _combo_chart_data(payload) if chart_type in _CHARTEX_CHART_TYPES: @@ -798,3 +991,25 @@ def _chart_data(payload: dict[str, Any]) -> dict[str, Any]: if chart_type in _XY_CHART_TYPES: return _xy_chart_data(payload, chart_type, alias_style) return _category_chart_data(payload, chart_type, alias_grouping, alias_style) + + +def validate_chart_payload(payload: dict[str, Any]) -> None: + """Check a native chart payload against the export schema. + + Public contract for the pptx_to_svg importer: raises RuntimeError on any + payload the native chart exporter cannot represent. + """ + _chart_data(payload) + + +def validate_data_label_position( + value: Any, + chart_type: str, + grouping: str | None, +) -> None: + """Check a data-label position against the export schema. + + Public contract for the pptx_to_svg importer: raises RuntimeError when the + position is not representable for the given plot. + """ + _data_label_position(value, chart_type, grouping) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_style.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_style.py index 7204a0dc..18e6e089 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_style.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_style.py @@ -2,6 +2,7 @@ from __future__ import annotations +import math from typing import Any from xml.etree import ElementTree as ET @@ -26,6 +27,8 @@ from .marker_common import ( _most_common_color, _number, _normalized_fallback_text, + _powerpoint_emu, + _powerpoint_emu_value, _relative_luminance, _style_attr, _visible_fallback_texts, @@ -354,8 +357,10 @@ def _alpha_xml(value: Any, field_name: str = "fill_opacity") -> str: raise RuntimeError(f"Native PPTX chart {field_name} must be numeric") try: alpha = float(value) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): raise RuntimeError(f"Native PPTX chart {field_name} must be numeric") from None + if not math.isfinite(alpha): + raise RuntimeError(f"Native PPTX chart {field_name} must be finite") if alpha < 0 or alpha > 1: raise RuntimeError(f"Native PPTX chart {field_name} must be between 0 and 1") return f'' @@ -476,6 +481,8 @@ def _native_chart_export_payload( elem: ET.Element, payload: dict[str, Any], ) -> tuple[dict[str, Any], list[str]]: + if elem.get("data-pptx-native-source") == "pptx": + return payload, [] fallback_texts = set(_visible_fallback_texts(elem)) output = payload messages: list[str] = [] @@ -705,6 +712,52 @@ def _chart_companion_entries( return entries +def _chart_companion_box(item: dict[str, Any]) -> tuple[int, int, int, int] | None: + """Validate and resolve an optional explicit companion text box.""" + box_keys = ("x", "y", "width", "height") + provided_box_keys = [key for key in box_keys if key in item] + if provided_box_keys and len(provided_box_keys) != len(box_keys): + raise RuntimeError( + "Native PPTX chart companion text boxes require x/y/width/height together" + ) + if not provided_box_keys: + return None + return ( + _powerpoint_emu(item["x"], "companion text x"), + _powerpoint_emu(item["y"], "companion text y"), + _powerpoint_emu(item["width"], "companion text width", positive=True), + _powerpoint_emu(item["height"], "companion text height", positive=True), + ) + + +def _validate_chart_companion_boxes( + payload: dict[str, Any], + *, + chart_bounds: tuple[int, int, int, int], + include_title: bool, + include_subtitle_as_caption: bool, +) -> None: + """Validate companion boxes without allocating shapes or relationships.""" + _, chart_off_y, _, chart_ext_cy = chart_bounds + below_index = 0 + for item in _chart_companion_entries( + payload, + include_title=include_title, + include_subtitle_as_caption=include_subtitle_as_caption, + ): + text = str(item.get("text") or "").strip() + if not text: + continue + if _chart_companion_box(item) is not None: + continue + if str(item.get("role") or "note") != "title": + _powerpoint_emu_value( + chart_off_y + chart_ext_cy + px_to_emu(4 + below_index * 18), + "companion text y", + ) + below_index += 1 + + def _chart_companion_text_xml( ctx: ConvertContext, payload: dict[str, Any], @@ -742,12 +795,9 @@ def _chart_companion_text_xml( font_face = _chart_text_entry_font_face(item, chart_style.get("font_face")) align = str(item.get("align") or ("ctr" if role == "title" else "l")) bold = bool(item.get("bold", role == "title")) - has_box = all(_maybe_number(item.get(key)) is not None for key in ("x", "y", "width", "height")) - if has_box: - off_x = px_to_emu(_number(item["x"], "companion text x")) - off_y = px_to_emu(_number(item["y"], "companion text y")) - ext_cx = px_to_emu(_number(item["width"], "companion text width")) - ext_cy = px_to_emu(_number(item["height"], "companion text height")) + explicit_box = _chart_companion_box(item) + if explicit_box is not None: + off_x, off_y, ext_cx, ext_cy = explicit_box elif role == "title": off_x = chart_off_x off_y = chart_off_y @@ -755,7 +805,10 @@ def _chart_companion_text_xml( ext_cy = px_to_emu(28) else: off_x = chart_off_x - off_y = chart_off_y + chart_ext_cy + px_to_emu(4 + below_index * 18) + off_y = _powerpoint_emu_value( + chart_off_y + chart_ext_cy + px_to_emu(4 + below_index * 18), + "companion text y", + ) ext_cx = chart_ext_cx ext_cy = px_to_emu(16) below_index += 1 diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_xml.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_xml.py index e3fa8292..d6500514 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_xml.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/chart_xml.py @@ -5,8 +5,14 @@ from __future__ import annotations from typing import Any from xml.etree import ElementTree as ET -from ..drawingml.utils import detect_text_lang, px_to_emu, _xml_escape -from .chart_data import _DEFAULT_CHART_COLORS, _chart_list, _data_labels_config +from ..drawingml.utils import detect_text_lang, _xml_escape +from .chart_data import ( + _DEFAULT_CHART_COLORS, + _chart_list, + _data_label_position, + _data_label_point_items, + _data_labels_config, +) from .chart_style import ( _alpha_xml, _axis_title_xml, @@ -33,7 +39,7 @@ from .marker_common import ( _first_present, _font_size_hpt, _hex_or_none, - _number, + _powerpoint_line_width_emu, ) @@ -69,7 +75,9 @@ def _series_color_xml( alpha_xml = _alpha_xml(fill_opacity, "series fill_opacity") line_width_xml = "" if line_width is not None: - line_width_xml = f' w="{max(px_to_emu(_number(line_width, "series line_width")), 1)}"' + line_width_xml = ( + f' w="{_powerpoint_line_width_emu(line_width, "series line_width")}"' + ) line_xml = ( f'' if line else '' @@ -82,49 +90,6 @@ def _series_color_xml( ) -def _data_label_position(value: Any, chart_type: str, grouping: str | None) -> str | None: - if chart_type == "area": - if value is not None: - raise RuntimeError("Native PPTX area data labels do not support label position") - return None - is_stacked = chart_type in {"bar", "column"} and grouping in {"percentStacked", "stacked"} - default = "ctr" if is_stacked else ("outEnd" if chart_type in {"bar", "column"} else "t") - if value is None: - return default - aliases = { - "above": "t", - "bestfit": "bestFit", - "center": "ctr", - "inbase": "inBase", - "insidebase": "inBase", - "insideend": "inEnd", - "inend": "inEnd", - "outend": "outEnd", - "outsideend": "outEnd", - } - position = aliases.get(_compact_key(value)) - if not position: - raise RuntimeError( - "Native PPTX chart data label position must be one of: " - "above, best_fit, center, inside_base, inside_end, outside_end" - ) - if chart_type in {"bar", "column"}: - if position not in {"ctr", "inBase", "inEnd", "outEnd"}: - raise RuntimeError( - "Native PPTX bar/column data label position must be one of: " - "center, inside_base, inside_end, outside_end" - ) - if is_stacked and position == "outEnd": - raise RuntimeError( - "Native PPTX stacked bar/column data labels do not support outside_end" - ) - elif chart_type == "line" and position not in {"bestFit", "ctr", "t"}: - raise RuntimeError( - "Native PPTX line data label position must be one of: above, best_fit, center" - ) - return position - - def _data_label_flags_xml(config: dict[str, Any]) -> str: show_value = _chart_bool( _first_present(config.get("show_value"), config.get("showValue"), config.get("value")), @@ -152,35 +117,6 @@ def _data_label_flags_xml(config: dict[str, Any]) -> str: ) -def _data_label_point_items(config: dict[str, Any], point_count: int) -> list[dict[str, Any]]: - raw_points = config.get("points") - if raw_points is None: - return [] - items: list[dict[str, Any]] = [] - seen: set[int] = set() - for item in _chart_list(raw_points, "data_labels.points"): - if isinstance(item, dict): - raw_index = item.get("idx") - data = dict(item) - else: - raw_index = item - data = {} - if isinstance(raw_index, bool): - raise RuntimeError("Native PPTX chart data_labels.points idx must be an integer") - index_value = _number(raw_index, "data_labels.points idx") - if not index_value.is_integer(): - raise RuntimeError("Native PPTX chart data_labels.points idx must be an integer") - index = int(index_value) - if index < 0 or index >= point_count: - raise RuntimeError("Native PPTX chart data_labels.points idx is outside point range") - if index in seen: - raise RuntimeError("Native PPTX chart data_labels.points idx values must be unique") - seen.add(index) - data["idx"] = index - items.append(data) - return items - - def _data_labels_xml( config: dict[str, Any] | None, *, @@ -193,6 +129,14 @@ def _data_labels_xml( ) -> str: if config is None: return "" + show_leader_lines = _chart_bool( + _first_present( + config.get("show_leader_lines"), + config.get("showLeaderLines"), + ), + False, + ) + leader_lines_xml = f'' position = _data_label_position(config.get("position"), chart_type, grouping) raw_font_size = _first_present(config.get("font_size"), config.get("fontSize")) label_font_size = _font_size_hpt(raw_font_size, 12) if raw_font_size is not None else font_size @@ -210,7 +154,12 @@ def _data_labels_xml( if num_fmt else "" ) flags_xml = _data_label_flags_xml(config) - point_items = _data_label_point_items(config, point_count) + point_items = _data_label_point_items( + config, + chart_type, + grouping, + point_count, + ) if point_items: selected_items = {int(item["idx"]): item for item in point_items} point_label_xml = "" @@ -251,7 +200,7 @@ def _data_labels_xml( f"{_data_label_flags_xml({**config, **item})}" "" ) - return f"{point_label_xml}" + return f"{point_label_xml}{leader_lines_xml}" label_colors = [ _clean_hex(item, "#404040") @@ -280,11 +229,21 @@ def _data_labels_xml( f"{num_fmt_xml}{tx_pr_xml}" f"{position_xml}" f"{flags_xml}" - '' + f'{leader_lines_xml}' "" ) +def _series_scoped_data_labels(config: dict[str, Any] | None) -> bool: + """Return whether point-level overrides require a series ``c:dLbls``.""" + if config is None: + return False + return any( + bool(config.get(key)) + for key in ("points", "colors", "label_colors", "labelColors") + ) + + def _chart_color(colors: list[str], index: int) -> str: if index < len(colors): return colors[index] @@ -379,7 +338,9 @@ def _series_xml( default_color=data_label_color, default_font_face=data_label_font_face, ) - if chart_type in {"area", "bar", "column", "line"} else "" + if _series_scoped_data_labels(data_labels) + and chart_type in {"area", "bar", "column", "line"} + else "" ) parts.append( "" @@ -569,6 +530,7 @@ def _bar_chart_group_xml( cat_ax_id: str, val_ax_id: str, vary_colors: bool = False, + data_labels_xml: str = "", ) -> str: bar_dir = "bar" if chart_type == "bar" else "col" vary_colors_xml = '' if vary_colors else '' @@ -582,6 +544,7 @@ def _bar_chart_group_xml( f'' f"{vary_colors_xml}" f"{ser_xml}" + f"{data_labels_xml}" '' f"{overlap_xml}" f'' @@ -596,12 +559,14 @@ def _line_area_chart_group_xml( *, cat_ax_id: str, val_ax_id: str, + data_labels_xml: str = "", ) -> str: tag = "lineChart" if chart_type == "line" else "areaChart" line_tail_xml = '' if chart_type == "line" else "" return ( f'' f"{ser_xml}" + f"{data_labels_xml}" f"{line_tail_xml}" f'' f"" @@ -708,6 +673,20 @@ def _combo_plot_xml( start_column=2 + start_index, start_index=start_index, ) + data_labels = _data_labels_config(plot) + data_labels_xml = ( + _data_labels_xml( + data_labels, + chart_type=chart_type, + grouping=grouping, + point_count=len(plot["series"][0]["values"]), + font_size=axis_font_size, + default_color=chart_style.get("text_color"), + default_font_face=chart_style.get("font_face"), + ) + if not _series_scoped_data_labels(data_labels) + else "" + ) if chart_type == "column": parts.append(_bar_chart_group_xml( chart_type, @@ -716,6 +695,7 @@ def _combo_plot_xml( cat_ax_id=cat_ax_id, val_ax_id=val_ax_id, vary_colors=any(item.get("point_colors") for item in plot["series"]), + data_labels_xml=data_labels_xml, )) elif chart_type in {"area", "line"}: parts.append(_line_area_chart_group_xml( @@ -724,6 +704,7 @@ def _combo_plot_xml( ser_xml, cat_ax_id=cat_ax_id, val_ax_id=val_ax_id, + data_labels_xml=data_labels_xml, )) else: raise RuntimeError("Native PPTX combo plots support column, line, and area only") @@ -856,6 +837,20 @@ def _chart_plot_xml( data_label_color=chart_style.get("text_color"), data_label_font_face=chart_style.get("font_face"), ) + data_labels_xml = ( + _data_labels_xml( + chart_data.get("data_labels"), + chart_type=chart_type, + grouping=series_grouping, + point_count=len(series[0]["values"]), + font_size=axis_font_size, + default_color=chart_style.get("text_color"), + default_font_face=chart_style.get("font_face"), + ) + if chart_type in {"area", "bar", "column", "line"} + and not _series_scoped_data_labels(chart_data.get("data_labels")) + else "" + ) if chart_type in {"bar", "column"}: bar_dir = "bar" if chart_type == "bar" else "col" @@ -886,6 +881,7 @@ def _chart_plot_xml( f'' f"{vary_colors_xml}" f"{ser_xml}" + f"{data_labels_xml}" '' f"{overlap_xml}" f'' @@ -910,6 +906,7 @@ def _chart_plot_xml( return ( f'' f"{ser_xml}" + f"{data_labels_xml}" f"{line_tail_xml}" f'' f"" diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/marker_common.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/marker_common.py index fa943c91..a86b9d7b 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/marker_common.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/marker_common.py @@ -3,12 +3,15 @@ from __future__ import annotations import json +import math import re +from collections.abc import Iterable from typing import Any from xml.etree import ElementTree as ET from ..drawingml.context import ConvertContext, IDENTITY_MATRIX from ..drawingml.utils import ( + EMU_PER_PX, FONT_PX_TO_HUNDREDTHS_PT, ctx_h, ctx_w, @@ -16,7 +19,6 @@ from ..drawingml.utils import ( ctx_y, matrix_multiply, parse_transform_matrix, - px_to_emu, transform_point, ) @@ -34,6 +36,20 @@ CHART_COLOR_STYLE_CONTENT_TYPE = "application/vnd.ms-office.chartcolorstyle+xml" CHART_STYLE_CONTENT_TYPE = "application/vnd.ms-office.chartstyle+xml" _NATIVE_KINDS = {"table", "chart"} +_POWERPOINT_COORD_MIN = -(2**31) +_POWERPOINT_COORD_MAX = 2**31 - 1 +_POWERPOINT_LINE_WIDTH_MAX = 20116800 +_TEXT_FONT_SIZE_MIN = 100 +_TEXT_FONT_SIZE_MAX = 400000 +_NATIVE_TRANSFORM_OPERATION_RE = re.compile(r"([A-Za-z]+)\s*\(([^()]*)\)") +_NATIVE_TRANSFORM_SEPARATOR_RE = re.compile(r"\s*,?\s*") +_NATIVE_TRANSFORM_ARGS_RE = re.compile( + r"\s*" + r"([-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?)" + r"(?:\s*(?:,\s*|\s+)" + r"([-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?))?" + r"\s*" +) _HEX_RE = re.compile(r"^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$") _RGB_RE = re.compile(r"^rgba?\(([^)]+)\)$", re.IGNORECASE) _POINT_RE = re.compile(r"[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?") @@ -101,6 +117,8 @@ def _hex_or_none(value: Any) -> str | None: value_float = float(part) except ValueError: return None + if not math.isfinite(value_float): + return None channels.append(max(0, min(255, int(round(value_float))))) return "".join(f"{channel:02X}" for channel in channels) @@ -164,19 +182,125 @@ def _visible_fallback_texts(elem: ET.Element, *, include_metadata: bool = False) def _number(value: Any, field_name: str) -> float: + if isinstance(value, bool): + raise RuntimeError(f"Native PPTX object requires numeric {field_name}") try: - return float(value) - except (TypeError, ValueError) as exc: + number = float(value) + except (TypeError, ValueError, OverflowError) as exc: raise RuntimeError(f"Native PPTX object requires numeric {field_name}") from exc + if not math.isfinite(number): + raise RuntimeError(f"Native PPTX object requires finite numeric {field_name}") + return number def _maybe_number(value: Any) -> float | None: - if value is None: + if value is None or isinstance(value, bool): return None try: - return float(value) - except (TypeError, ValueError): + number = float(value) + except (TypeError, ValueError, OverflowError): return None + return number if math.isfinite(number) else None + + +def _powerpoint_emu_value( + emu: int, + field_name: str, + *, + positive: bool = False, +) -> int: + """Validate an already-resolved EMU value for a PowerPoint coordinate.""" + lower_bound = 1 if positive else _POWERPOINT_COORD_MIN + if emu < lower_bound or emu > _POWERPOINT_COORD_MAX: + qualifier = "positive " if positive else "" + raise RuntimeError( + f"Native PPTX object {field_name} must resolve to a {qualifier}" + "32-bit PowerPoint coordinate" + ) + return emu + + +def _powerpoint_emu(value: Any, field_name: str, *, positive: bool = False) -> int: + """Convert SVG px to an EMU value that PowerPoint can represent.""" + number = _number(value, field_name) + scaled = number * EMU_PER_PX + if not math.isfinite(scaled): + raise RuntimeError(f"Native PPTX object {field_name} exceeds PowerPoint coordinates") + return _powerpoint_emu_value(round(scaled), field_name, positive=positive) + + +def _powerpoint_line_width_emu(value: Any, field_name: str) -> int: + """Convert SVG px to a legal DrawingML ``ST_LineWidth`` value.""" + emu = _powerpoint_emu(value, field_name, positive=True) + if emu > _POWERPOINT_LINE_WIDTH_MAX: + raise RuntimeError( + f"Native PPTX object {field_name} exceeds DrawingML line-width range" + ) + return emu + + +def native_marker_transform(transform: str | None) -> tuple[float, float, float, float]: + """Return a strict native-marker transform as ``dx, dy, sx, sy``.""" + raw = (transform or "").strip() + if not raw: + return 0.0, 0.0, 1.0, 1.0 + + cursor = 0 + operation_count = 0 + for match in _NATIVE_TRANSFORM_OPERATION_RE.finditer(raw): + gap = raw[cursor:match.start()] + valid_gap = ( + not gap + if operation_count == 0 + else _NATIVE_TRANSFORM_SEPARATOR_RE.fullmatch(gap) is not None + ) + if not valid_gap: + raise RuntimeError( + "Native PPTX table/chart markers support translate/scale transforms only" + ) + name = match.group(1).lower() + args_match = _NATIVE_TRANSFORM_ARGS_RE.fullmatch(match.group(2)) + if name not in {"translate", "scale"} or args_match is None: + raise RuntimeError( + "Native PPTX table/chart markers support translate/scale transforms only" + ) + values = [float(value) for value in args_match.groups() if value is not None] + if not all(math.isfinite(value) for value in values): + raise RuntimeError("Native PPTX marker transform values must be finite") + operation_count += 1 + cursor = match.end() + + if operation_count == 0 or raw[cursor:]: + raise RuntimeError( + "Native PPTX table/chart markers support translate/scale transforms only" + ) + + a, b, c, d, e, f = parse_transform_matrix(raw) + components = (a, b, c, d, e, f) + if not all(math.isfinite(value) for value in components): + raise RuntimeError("Native PPTX marker transform exceeds finite coordinates") + if b != 0.0 or c != 0.0: + raise RuntimeError( + "Native PPTX table/chart markers support translate/scale transforms only" + ) + return e, f, a, d + + +def _native_marker_validation_context( + elem: ET.Element, + ancestors: Iterable[ET.Element] = (), +) -> ConvertContext: + """Build the scalar context used by native export for one marker path.""" + ctx = ConvertContext() + for current in (*ancestors, elem): + dx, dy, sx, sy = native_marker_transform(current.get("transform")) + ctx = ctx.child( + dx=ctx.scale_x * dx, + dy=ctx.scale_y * dy, + sx=sx, + sy=sy, + ) + return ctx def _bbox_union( @@ -560,8 +684,12 @@ def _relative_luminance(color: str) -> float: return linear[0] * 0.2126 + linear[1] * 0.7152 + linear[2] * 0.0722 -def _bounds(elem: ET.Element, payload: dict[str, Any], ctx: ConvertContext) -> tuple[int, int, int, int]: - """Return object bounds as DrawingML EMU tuple.""" +def _resolved_bounds( + elem: ET.Element, + payload: dict[str, Any], + ctx: ConvertContext, +) -> tuple[float, float, float, float, bool]: + """Resolve object bounds in SVG px plus whether all bounds were explicit.""" if ctx.use_transform_matrix: raise RuntimeError("Native PPTX table/chart markers support translate/scale only") @@ -601,40 +729,61 @@ def _bounds(elem: ET.Element, payload: dict[str, Any], ctx: ConvertContext) -> t resolved_y = ctx_y(y, ctx) resolved_w = ctx_w(width, ctx) resolved_h = ctx_h(height, ctx) - off_x = px_to_emu(resolved_x) - off_y = px_to_emu(resolved_y) - ext_cx = px_to_emu(resolved_w) - ext_cy = px_to_emu(resolved_h) - return off_x, off_y, ext_cx, ext_cy + return resolved_x, resolved_y, resolved_w, resolved_h, explicit_bounds -def _validate_bounds_inputs(elem: ET.Element, payload: dict[str, Any]) -> None: - raw_x = payload.get("x", elem.get("data-pptx-x")) - raw_y = payload.get("y", elem.get("data-pptx-y")) - raw_width = payload.get("width", elem.get("data-pptx-width")) - raw_height = payload.get("height", elem.get("data-pptx-height")) - inferred = None - if any(value is None for value in (raw_x, raw_y, raw_width, raw_height)): - inferred = _inferred_bounds(elem) - if inferred is None: - raise RuntimeError( - "Native PPTX object requires x/y/width/height or visible fallback geometry" - ) - - width = ( - _number(raw_width, "width") - if raw_width is not None else inferred[2] - inferred[0] # type: ignore[index] +def _bounds(elem: ET.Element, payload: dict[str, Any], ctx: ConvertContext) -> tuple[int, int, int, int]: + """Return object bounds as DrawingML EMU tuple.""" + x, y, width, height, _ = _resolved_bounds(elem, payload, ctx) + return ( + _powerpoint_emu(x, "x"), + _powerpoint_emu(y, "y"), + _powerpoint_emu(width, "width", positive=True), + _powerpoint_emu(height, "height", positive=True), ) - height = ( - _number(raw_height, "height") - if raw_height is not None else inferred[3] - inferred[1] # type: ignore[index] + + +def _validate_bounds_inputs( + elem: ET.Element, + payload: dict[str, Any], + ctx: ConvertContext, +) -> tuple[int, int, int, int, bool]: + x, y, width, height, explicit_bounds = _resolved_bounds(elem, payload, ctx) + return ( + _powerpoint_emu(x, "x"), + _powerpoint_emu(y, "y"), + _powerpoint_emu(width, "width", positive=True), + _powerpoint_emu(height, "height", positive=True), + explicit_bounds, ) - if raw_x is not None: - _number(raw_x, "x") - if raw_y is not None: - _number(raw_y, "y") - if width <= 0 or height <= 0: - raise RuntimeError("Native PPTX object width/height must be positive") + + +def _validate_payload_xml_strings(value: Any) -> None: + """Reject JSON strings that cannot be serialized into PPTX XML 1.0 parts.""" + if isinstance(value, dict): + for key, item in value.items(): + _validate_payload_xml_strings(key) + _validate_payload_xml_strings(item) + return + if isinstance(value, list): + for item in value: + _validate_payload_xml_strings(item) + return + if not isinstance(value, str): + return + for char in value: + codepoint = ord(char) + if ( + codepoint in {0x09, 0x0A, 0x0D} + or 0x20 <= codepoint <= 0xD7FF + or 0xE000 <= codepoint <= 0xFFFD + or 0x10000 <= codepoint <= 0x10FFFF + ): + continue + raise RuntimeError( + "Native PPTX metadata contains an XML 1.0-invalid character " + f"U+{codepoint:04X}" + ) def _load_payload(elem: ET.Element, kind: str) -> dict[str, Any]: @@ -656,17 +805,29 @@ def _load_payload(elem: ET.Element, kind: str) -> dict[str, Any]: payload = json.loads(raw) except json.JSONDecodeError as exc: raise RuntimeError(f"Native PPTX {kind} metadata is not valid JSON: {exc.msg}") from exc + except (ValueError, RecursionError) as exc: + raise RuntimeError(f"Native PPTX {kind} metadata cannot be decoded") from exc if not isinstance(payload, dict): raise RuntimeError(f"Native PPTX {kind} metadata must be a JSON object") + _validate_payload_xml_strings(payload) return payload def _font_size_hpt(value: Any, default_px: int = 18) -> int: - try: - px = float(value) - except (TypeError, ValueError): - px = float(default_px) - return int(round(px * FONT_PX_TO_HUNDREDTHS_PT / 10.0)) * 10 + def convert(raw: Any) -> int | None: + try: + px = float(raw) + except (TypeError, ValueError, OverflowError): + return None + scaled = px * FONT_PX_TO_HUNDREDTHS_PT + if not math.isfinite(scaled): + return None + size = int(round(scaled / 10.0)) * 10 + if not _TEXT_FONT_SIZE_MIN <= size <= _TEXT_FONT_SIZE_MAX: + return None + return size + + return convert(value) or convert(default_px) or 1350 def _first_present(*values: Any) -> Any: diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/table.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/table.py index 6640a0ff..f655fb35 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/table.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/table.py @@ -6,19 +6,21 @@ from typing import Any from xml.etree import ElementTree as ET from ..drawingml.context import ConvertContext, ShapeResult -from ..drawingml.utils import px_to_emu, _xml_escape +from ..drawingml.theme_colors import ThemeColorSpec, color_node_xml +from ..drawingml.utils import _xml_escape from .chart_style import _font_face_xml from .marker_common import ( TABLE_URI, _bool_attr, _bounds, - _chart_bool, _clean_hex, _compact_key, _first_present, _font_size_hpt, _normalized_fallback_text, _number, + _powerpoint_emu, + _powerpoint_line_width_emu, _visible_fallback_texts, ) @@ -26,18 +28,25 @@ from .marker_common import ( def _table_text_run( text: str, *, - color: str, - bold: bool, - font_size: int, + color: str | None, + bold: bool | None, + font_size: int | None, font_face: str | None, + theme_color_spec: ThemeColorSpec | None, ) -> str: - bold_attr = ' b="1"' if bold else "" + size_attr = f' sz="{font_size}"' if font_size is not None else "" + bold_attr = f' b="{_bool_attr(bold)}"' if bold is not None else "" + color_xml = ( + f'{color_node_xml(color, theme_color_spec, "text")}' + if color else "" + ) + space_attr = ' xml:space="preserve"' if text != text.strip() else "" return ( - f'' - f'' + f'' + f'{color_xml}' f'{_font_face_xml(font_face)}' "" - f"{_xml_escape(text)}" + f"{_xml_escape(text)}" ) @@ -65,6 +74,8 @@ _TABLE_TOP_LEVEL_SPAN_KEYS = { "merges", "spans", } +_TABLE_MAX_ROWS = 1000 +_TABLE_MAX_COLUMNS = 1000 def _table_rows(payload: dict[str, Any]) -> list[list[Any]]: @@ -102,7 +113,36 @@ def _check_table_spans(payload: dict[str, Any], table_rows: list[list[Any]]) -> def _grid_is_strict(payload: dict[str, Any]) -> bool: - return bool(payload.get("strict_grid", payload.get("strictGrid", False))) + value = payload.get("strict_grid", payload.get("strictGrid")) + return _table_bool(value, "strict_grid", default=False) + + +def _table_bool(value: Any, field_name: str, *, default: bool) -> bool: + if value is None: + return default + if isinstance(value, bool): + return value + if value in (0, 1): + return bool(value) + key = _compact_key(value) + if key in {"1", "on", "true", "yes"}: + return True + if key in {"0", "false", "no", "off"}: + return False + raise RuntimeError(f"Native PPTX table {field_name} must be a boolean") + + +def _table_header_rows(payload: dict[str, Any], row_count: int) -> int: + default = 1 if payload.get("columns") else 0 + value = _number(payload.get("header_rows", default), "table header_rows") + if not value.is_integer(): + raise RuntimeError("Native PPTX table header_rows must be an integer") + header_rows = int(value) + if not 0 <= header_rows <= row_count: + raise RuntimeError( + "Native PPTX table header_rows must be between zero and the resolved row count" + ) + return header_rows def _validate_table_lengths(payload: dict[str, Any], table_rows: list[list[Any]]) -> int: @@ -111,6 +151,8 @@ def _validate_table_lengths(payload: dict[str, Any], table_rows: list[list[Any]] col_count = max(len(row) for row in table_rows) if col_count <= 0: raise RuntimeError("Native PPTX table requires at least one column") + if len(table_rows) > _TABLE_MAX_ROWS or col_count > _TABLE_MAX_COLUMNS: + raise RuntimeError("Native PPTX table supports at most 1000 rows and columns") if _grid_is_strict(payload) and any(len(row) != col_count for row in table_rows): raise RuntimeError("Native PPTX table strict_grid requires every row to have the same length") @@ -118,27 +160,33 @@ def _validate_table_lengths(payload: dict[str, Any], table_rows: list[list[Any]] if column_widths is not None: if not isinstance(column_widths, list) or len(column_widths) != col_count: raise RuntimeError("Native PPTX table column_widths must match the resolved column count") - for idx, width in enumerate(column_widths, start=1): - _number(width, f"column_widths[{idx}]") + _table_weights(column_widths, "column_widths") row_heights = payload.get("row_heights") if row_heights is not None: if not isinstance(row_heights, list) or len(row_heights) != len(table_rows): raise RuntimeError("Native PPTX table row_heights must match the resolved row count") - for idx, height in enumerate(row_heights, start=1): - _number(height, f"row_heights[{idx}]") + _table_weights(row_heights, "row_heights") return col_count def _validate_table_cell_formatting(payload: dict[str, Any], table_rows: list[list[Any]]) -> None: style = payload.get("style") if isinstance(payload.get("style"), dict) else {} + _table_bool(style.get("band_row"), "style.band_row", default=True) for row in table_rows: for cell in row: cell_data = _cell_payload(cell) + if "bold" in cell_data: + _table_bool(cell_data["bold"], "cell bold", default=False) for side in ("left", "right", "top", "bottom"): _table_padding_value(cell_data, style, side) - _table_border_width(cell_data, style) + border_width = _table_border_width(cell_data, style) + if border_width > 0: + _powerpoint_line_width_emu( + border_width, + "table border_width", + ) _table_anchor(cell_data, style) @@ -146,6 +194,7 @@ def _validate_table_payload(payload: dict[str, Any]) -> tuple[list[list[Any]], i table_rows = _table_rows(payload) _check_table_spans(payload, table_rows) col_count = _validate_table_lengths(payload, table_rows) + _table_header_rows(payload, len(table_rows)) _validate_table_cell_formatting(payload, table_rows) return table_rows, col_count @@ -190,19 +239,46 @@ def _weighted_lengths( *, field_name: str, ) -> list[int]: + if total < count: + raise RuntimeError( + f"Native PPTX table {field_name} cannot fit {count} positive grid lengths" + ) if weights is None: - base = max(total // count, 1) - values = [base] * count - values[-1] += total - sum(values) - return values + base, remainder = divmod(total, count) + return [base + (1 if idx < remainder else 0) for idx in range(count)] - numeric = [max(_number(weight, field_name), 0.0) for weight in weights] - numeric_total = sum(numeric) - if numeric_total <= 0: - raise RuntimeError(f"Native PPTX table {field_name} values must sum to a positive number") - values = [max(round(total * weight / numeric_total), 1) for weight in numeric] - values[-1] += total - sum(values) - return values + numeric = _table_weights(weights, field_name) + largest = max(numeric) + normalized = [weight / largest for weight in numeric] + normalized_total = sum(normalized) + distributable = total - count + quotas = [distributable * weight / normalized_total for weight in normalized] + extras = [int(quota) for quota in quotas] + remainder = distributable - sum(extras) + if remainder < 0 or remainder > count: + raise RuntimeError(f"Native PPTX table {field_name} allocation overflowed") + order = sorted( + range(count), + key=lambda idx: (quotas[idx] - extras[idx], normalized[idx], -idx), + reverse=True, + ) + for idx in order[:remainder]: + extras[idx] += 1 + return [extra + 1 for extra in extras] + + +def _table_weights(weights: list[Any], field_name: str) -> list[float]: + numeric = [ + _number(weight, f"{field_name}[{idx}]") + for idx, weight in enumerate(weights, start=1) + ] + if any(weight < 0 for weight in numeric): + raise RuntimeError(f"Native PPTX table {field_name} values must be non-negative") + if max(numeric, default=0.0) <= 0: + raise RuntimeError( + f"Native PPTX table {field_name} values must sum to a positive number" + ) + return numeric def _table_padding_value( @@ -235,7 +311,8 @@ def _table_padding_value( value = from_source(style) if value is None: return None - return max(px_to_emu(max(_number(value, f"table {side} padding"), 0.0)), 0) + pixels = max(_number(value, f"table {side} padding"), 0.0) + return _powerpoint_emu(pixels, f"table {side} padding") def _table_padding_attrs(cell_data: dict[str, Any], style: dict[str, Any]) -> str: @@ -283,17 +360,21 @@ def _table_border_width(cell_data: dict[str, Any], style: dict[str, Any]) -> flo return _number(1 if width_raw is None else width_raw, "table border_width") -def _table_border_xml(cell_data: dict[str, Any], style: dict[str, Any]) -> str: +def _table_border_xml( + cell_data: dict[str, Any], + style: dict[str, Any], + theme_color_spec: ThemeColorSpec | None, +) -> str: color_raw = cell_data.get("border_color", cell_data.get("borderColor", style.get("border_color"))) width = _table_border_width(cell_data, style) if width <= 0: return "" color = _clean_hex(color_raw, "#D9DEE7") line = ( - f'' + f'{color_node_xml(color, theme_color_spec, "stroke")}' '' ) - line_width = max(px_to_emu(width), 1) + line_width = _powerpoint_line_width_emu(width, "table border_width") return "".join( f'{line}' for tag in ("lnL", "lnR", "lnT", "lnB") @@ -302,8 +383,8 @@ def _table_border_xml(cell_data: dict[str, Any], style: dict[str, Any]) -> str: def _build_native_table(elem: ET.Element, ctx: ConvertContext, payload: dict[str, Any]) -> ShapeResult: table_rows, col_count = _validate_table_payload(payload) - has_columns = bool(payload.get("columns") or []) - header_rows = int(payload.get("header_rows", 1 if has_columns else 0)) + header_rows = _table_header_rows(payload, len(table_rows)) + preserve_source_style = elem.get("data-pptx-native-source") == "pptx" for row in table_rows: row.extend([""] * (col_count - len(row))) @@ -345,44 +426,95 @@ def _build_native_table(elem: ET.Element, ctx: ConvertContext, payload: dict[str cells_xml: list[str] = [] for cell in row: cell_data = _cell_payload(cell) - fill = _clean_hex( - cell_data.get("fill"), - header_fill if is_header else (band_fill if row_idx % 2 == 0 and row_idx else body_fill), - ) - color = _clean_hex(cell_data.get("color"), header_text if is_header else body_text) - align = str(cell_data.get("align") or ("ctr" if is_header else "l")) + if preserve_source_style: + fill = ( + _clean_hex(cell_data.get("fill"), "#FFFFFF") + if cell_data.get("fill") is not None else None + ) + color = ( + _clean_hex(cell_data.get("color"), "#000000") + if cell_data.get("color") is not None else None + ) + align = str(cell_data.get("align") or "l") + else: + fill = _clean_hex( + cell_data.get("fill"), + header_fill if is_header else ( + band_fill if row_idx % 2 == 0 and row_idx else body_fill + ), + ) + color = _clean_hex( + cell_data.get("color"), + header_text if is_header else body_text, + ) + align = str(cell_data.get("align") or ("ctr" if is_header else "l")) if align not in {"l", "ctr", "r"}: align = "l" text = "" if cell_data.get("text") is None else str(cell_data.get("text")) - bold = bool(cell_data.get("bold", is_header)) - cell_font_size = ( - _font_size_hpt(cell_data.get("font_size"), 18) - if "font_size" in cell_data - else body_font_size - ) - if is_header and "font_size" not in cell_data: - cell_font_size = header_font_size + if preserve_source_style: + bold = ( + _table_bool(cell_data["bold"], "cell bold", default=False) + if "bold" in cell_data else None + ) + cell_font_size = ( + _font_size_hpt(cell_data.get("font_size"), 18) + if "font_size" in cell_data else None + ) + else: + bold = _table_bool(cell_data.get("bold"), "cell bold", default=is_header) + cell_font_size = ( + _font_size_hpt(cell_data.get("font_size"), 18) + if "font_size" in cell_data + else body_font_size + ) + if is_header and "font_size" not in cell_data: + cell_font_size = header_font_size paragraph_props = f'' if align != "l" else "" - tc_pr_attrs = ( - f' anchor="{_table_anchor(cell_data, style)}"' - f'{_table_padding_attrs(cell_data, style)}' + anchor_keys = {"valign", "vertical_align"} + anchor_attr = "" + if not preserve_source_style or anchor_keys.intersection(cell_data) or anchor_keys.intersection(style): + anchor_attr = f' anchor="{_table_anchor(cell_data, style)}"' + tc_pr_attrs = f'{anchor_attr}{_table_padding_attrs(cell_data, style)}' + border_xml = _table_border_xml( + cell_data, + style, + ctx.theme_color_spec, + ) + fill_xml = ( + '' + f'{color_node_xml(fill, ctx.theme_color_spec, "fill")}' + '' + if fill else "" + ) + text_run_xml = _table_text_run( + text, + color=color, + bold=bold, + font_size=cell_font_size, + font_face=font_face, + theme_color_spec=ctx.theme_color_spec, ) - border_xml = _table_border_xml(cell_data, style) cells_xml.append( "" "" f"{paragraph_props}" - f"{_table_text_run(text, color=color, bold=bold, font_size=cell_font_size, font_face=font_face)}" + f"{text_run_xml}" "" - f'{border_xml}' + f'{border_xml}{fill_xml}' "" ) rows_xml.append(f'{"".join(cells_xml)}') shape_id = ctx.next_id() first_row = _bool_attr(header_rows > 0) - band_row = _bool_attr(bool(style.get("band_row", True))) - table_style_id = str(style.get("table_style_id") or "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}") + band_row = _bool_attr(_table_bool(style.get("band_row"), "style.band_row", default=True)) + table_style_id = style.get("table_style_id") + if table_style_id is None and not preserve_source_style: + table_style_id = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}" + table_style_xml = ( + f'{_xml_escape(str(table_style_id))}' + if table_style_id else "" + ) name = _xml_escape(str(payload.get("name") or elem.get("id") or f"Native Table {shape_id}")) xml = f''' @@ -395,7 +527,7 @@ def _build_native_table(elem: ET.Element, ctx: ConvertContext, payload: dict[str -{_xml_escape(table_style_id)} +{table_style_xml} {grid_xml} {''.join(rows_xml)} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/workbook.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/workbook.py index ebf7d7c9..f2ce2c4d 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/workbook.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/native_objects/workbook.py @@ -38,7 +38,11 @@ def _xlsx_cell(value: Any, row: int, col: int) -> str: def _minimal_workbook(rows: list[list[Any]]) -> bytes: if XlsxWriterWorkbook is not None: buffer = io.BytesIO() - workbook = XlsxWriterWorkbook(buffer, {"in_memory": True}) + workbook = XlsxWriterWorkbook(buffer, { + "in_memory": True, + "strings_to_formulas": False, + "strings_to_urls": False, + }) worksheet = workbook.add_worksheet("Sheet1") for row_index, row in enumerate(rows): for col_index, value in enumerate(row): @@ -50,8 +54,11 @@ def _minimal_workbook(rows: list[list[Any]]) -> bytes: workbook = OpenpyxlWorkbook() worksheet = workbook.active worksheet.title = "Sheet1" - for row in rows: - worksheet.append(row) + for row_index, row in enumerate(rows, start=1): + for col_index, value in enumerate(row, start=1): + cell = worksheet.cell(row=row_index, column=col_index, value=value) + if isinstance(value, str): + cell.data_type = "s" buffer = io.BytesIO() workbook.save(buffer) workbook.close() diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/builder.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/builder.py index 759a288a..f98afa3c 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/builder.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/builder.py @@ -6,8 +6,8 @@ import hashlib import json import mimetypes import os -import re import posixpath +import re import shutil import stat import subprocess @@ -15,15 +15,32 @@ import tempfile import uuid import zipfile from concurrent.futures import ProcessPoolExecutor, as_completed +from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path from typing import Any +from urllib.parse import urlsplit +from xml.etree import ElementTree as ET from xml.sax.saxutils import escape from pptx import Presentation from pptx.util import Emu from ..drawingml.converter import convert_svg_to_slide_shapes +from ..drawingml.theme_colors import ( + ThemeColorSpec, + apply_theme_color_spec, + rewrite_chart_accent_colors, +) +from ..drawingml.theme_fonts import ( + ThemeFontSpec, + apply_theme_font_spec, +) +from ..drawingml.utils import EMU_PER_PX +from ..semantic_markers import ( + chrome_token_from_markers, + page_layout_name_from_svg, +) from .dimensions import ( CANVAS_FORMATS, get_slide_dimensions, get_pixel_dimensions, @@ -55,6 +72,15 @@ from .slide_xml import ( ANIMATIONS_AVAILABLE, TRANSITIONS, create_slide_xml_with_svg, create_slide_rels_xml, ) +from .template_structure import ( + NativeStructureContract, + TemplateElementSpec, + TemplateSlideSpec, + TemplateStructureError, + match_native_placeholders, + parse_preserve_slides, + parse_template_slides, +) # Re-import create_transition_xml only if available try: @@ -69,6 +95,2445 @@ except ImportError: pick_animation_effect = None +SLIDE_LAYOUT_REL_TYPE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" +) +SLIDE_MASTER_REL_TYPE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" +) +PML_NS = "http://schemas.openxmlformats.org/presentationml/2006/main" +DML_NS = "http://schemas.openxmlformats.org/drawingml/2006/main" +REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" +P14_NS = "http://schemas.microsoft.com/office/powerpoint/2010/main" + +for _prefix, _uri in (("p", PML_NS), ("a", DML_NS), ("r", REL_NS), ("p14", P14_NS)): + try: + ET.register_namespace(_prefix, _uri) + except (ValueError, AttributeError): + pass + + +@dataclass(frozen=True) +class PptxStructureContext: + """Resolved base package structure reused when slide XML is regenerated.""" + + slide_layout_targets: dict[int, str] + slide_master_parts: dict[int, str] + + def slide_layout_target(self, slide_num: int) -> str: + """Return the slide layout target for a generated slide.""" + try: + return self.slide_layout_targets[slide_num] + except KeyError as exc: + raise RuntimeError( + f"Missing slide layout relationship for generated slide {slide_num}" + ) from exc + + def slide_master_part(self, slide_num: int) -> str: + """Return the slide master package part for a generated slide.""" + try: + return self.slide_master_parts[slide_num] + except KeyError as exc: + raise RuntimeError( + f"Missing slide master relationship for generated slide {slide_num}" + ) from exc + + +@dataclass +class _TemplateRuntimeSlide: + """Parsed slide package state used by explicit template structure export.""" + + spec: TemplateSlideSpec + slide_path: Path + rels_path: Path + tree: ET.ElementTree + root: ET.Element + rels: dict[str, dict[str, str]] + shapes: dict[str, ET.Element] + shape_ids_by_svg_id: dict[str, list[str]] + + +def _relationship_attrs(elem: ET.Element) -> dict[str, str]: + return {key.rsplit("}", 1)[-1]: value for key, value in elem.attrib.items()} + + +def _resolve_package_target(source_part: str, target: str) -> str: + """Resolve a relationship target relative to a package part path.""" + return posixpath.normpath(posixpath.join(posixpath.dirname(source_part), target)) + + +def _relationships_path_for_part(extract_dir: Path, part_name: str) -> Path: + """Return the package relationship sidecar path for a part name.""" + path = Path(part_name) + return extract_dir / path.parent / "_rels" / f"{path.name}.rels" + + +def _find_relationship_target( + rels_path: Path, + rel_type: str, +) -> str | None: + """Find the first relationship target for a relationship type.""" + if not rels_path.exists(): + return None + root = ET.parse(rels_path).getroot() + for elem in root: + attrs = _relationship_attrs(elem) + if attrs.get("Type") == rel_type: + return attrs.get("Target") + return None + + +def _read_relationships(rels_path: Path) -> dict[str, dict[str, str]]: + """Return relationship attributes keyed by rId.""" + if not rels_path.exists(): + return {} + root = ET.parse(rels_path).getroot() + rels: dict[str, dict[str, str]] = {} + for elem in root: + attrs = _relationship_attrs(elem) + rel_id = attrs.get("Id") + if rel_id: + rels[rel_id] = attrs + return rels + + +def _find_relationship_id( + rels_path: Path, + rel_type: str, + target: str, +) -> str | None: + """Find an existing relationship by type and target.""" + for rel_id, attrs in _read_relationships(rels_path).items(): + if attrs.get("Type") == rel_type and attrs.get("Target") == target: + return rel_id + return None + + +def _read_slide_layout_targets(extract_dir: Path, slide_count: int) -> PptxStructureContext: + """Read the actual layout relationship target for every generated slide.""" + slide_layout_targets: dict[int, str] = {} + slide_master_parts: dict[int, str] = {} + rels_dir = extract_dir / "ppt" / "slides" / "_rels" + for slide_num in range(1, slide_count + 1): + rels_path = rels_dir / f"slide{slide_num}.xml.rels" + if not rels_path.exists(): + raise RuntimeError(f"Missing slide relationship file: {rels_path}") + target = _find_relationship_target(rels_path, SLIDE_LAYOUT_REL_TYPE) + if not target: + raise RuntimeError(f"Slide {slide_num} has no slide layout relationship") + slide_layout_targets[slide_num] = target + + slide_part = f"ppt/slides/slide{slide_num}.xml" + layout_part = _resolve_package_target(slide_part, target) + layout_rels_path = _relationships_path_for_part(extract_dir, layout_part) + master_target = _find_relationship_target(layout_rels_path, SLIDE_MASTER_REL_TYPE) + if not master_target: + raise RuntimeError( + f"Slide {slide_num} layout has no slide master relationship" + ) + slide_master_parts[slide_num] = _resolve_package_target(layout_part, master_target) + return PptxStructureContext( + slide_layout_targets=slide_layout_targets, + slide_master_parts=slide_master_parts, + ) + + +_SLIDE_BACKGROUND_RE = re.compile( + r"(?P]*>\s*)" + r"(?P)" + r"(?P\s* str | None: + """Return the slide-level p:bg XML when it directly precedes spTree.""" + match = _SLIDE_BACKGROUND_RE.search(slide_xml) + return match.group("bg") if match else None + + +def _remove_slide_background_xml(slide_xml: str) -> str: + """Remove a promoted slide-level p:bg from cSld.""" + return _SLIDE_BACKGROUND_RE.sub(r"\g\g", slide_xml, count=1) + + +def _put_background_on_part(part_xml: str, background_xml: str) -> str | None: + """Replace or insert p:bg before a slide/master/layout spTree. + + Returns None when the part carries a p:bg the canonical pattern cannot + replace; inserting there would leave two p:bg children under p:cSld. + """ + match = _SLIDE_BACKGROUND_RE.search(part_xml) + if match: + return ( + part_xml[:match.start("bg")] + + background_xml + + part_xml[match.end("bg"):] + ) + if "]*>)", part_xml) + if not cslide_match: + raise RuntimeError("PPTX slide/master/layout part has no p:cSld element") + return ( + part_xml[:cslide_match.end()] + + background_xml + + part_xml[cslide_match.end():] + ) + + +def _dominant_variant( + values_by_slide: dict[int, Any], +) -> tuple[Any | None, list[int]]: + """Return the most common value and its slides, or None on a tie.""" + slides_by_value: dict[Any, list[int]] = {} + for slide_num, value in sorted(values_by_slide.items()): + slides_by_value.setdefault(value, []).append(slide_num) + if not slides_by_value: + return None, [] + best_count = max(len(slides) for slides in slides_by_value.values()) + dominant = [ + (value, slides) + for value, slides in slides_by_value.items() + if len(slides) == best_count + ] + if len(dominant) != 1: + return None, [] + return dominant[0] + + +def _is_strict_majority(subset_size: int, total: int) -> bool: + return subset_size >= 2 and subset_size * 2 > total + + +def _promote_common_slide_backgrounds_to_masters( + extract_dir: Path, + structure: PptxStructureContext, + slide_count: int, + *, + verbose: bool = False, +) -> int: + """Promote the majority slide background to its shared slide master. + + Every slide in the master group must carry an explicit background — + a slide without one would start inheriting the promoted master fill. + Minority slides keep their own slide-level background, which always + overrides the master fill. + """ + slides_by_master: dict[str, list[int]] = {} + for slide_num in range(1, slide_count + 1): + master_part = structure.slide_master_part(slide_num) + slides_by_master.setdefault(master_part, []).append(slide_num) + + promoted = 0 + for master_part, slide_nums in slides_by_master.items(): + slide_backgrounds: dict[int, str] = {} + for slide_num in slide_nums: + slide_path = extract_dir / "ppt" / "slides" / f"slide{slide_num}.xml" + slide_xml = slide_path.read_text(encoding="utf-8") + background_xml = _extract_slide_background_xml(slide_xml) + if not background_xml: + slide_backgrounds = {} + break + slide_backgrounds[slide_num] = background_xml + + if not slide_backgrounds: + continue + background_xml, dominant_slides = _dominant_variant(slide_backgrounds) + if background_xml is None: + continue + if not _is_strict_majority(len(dominant_slides), len(slide_nums)): + continue + + master_path = extract_dir / master_part + master_xml = master_path.read_text(encoding="utf-8") + promoted_master_xml = _put_background_on_part(master_xml, background_xml) + if promoted_master_xml is None: + continue + master_path.write_text(promoted_master_xml, encoding="utf-8") + + for slide_num in dominant_slides: + slide_path = extract_dir / "ppt" / "slides" / f"slide{slide_num}.xml" + slide_xml = slide_path.read_text(encoding="utf-8") + slide_path.write_text( + _remove_slide_background_xml(slide_xml), + encoding="utf-8", + ) + promoted += 1 + + if verbose and promoted: + print(f" Baseline master background: promoted {promoted} slide background(s)") + return promoted + + +_CHROME_TRACE_TOKENS = ( + "logo", + "footer", + "header", + "watermark", + "chrome", + "pagenumber", + "slidenumber", + "pagenum", + "slidenum", +) +_TOP_LEVEL_SHAPE_TAGS = { + f"{{{PML_NS}}}sp", + f"{{{PML_NS}}}grpSp", + f"{{{PML_NS}}}pic", + f"{{{PML_NS}}}cxnSp", + f"{{{PML_NS}}}graphicFrame", +} +_REL_ATTRS = { + f"{{{REL_NS}}}embed", + f"{{{REL_NS}}}link", + f"{{{REL_NS}}}id", +} + + +def _chrome_token_from_svg_id(svg_id: str | None) -> str | None: + """Return the baseline chrome token encoded in a source SVG id.""" + if not svg_id: + return None + lower = svg_id.lower() + compact = re.sub(r"[-_\s]+", "", lower) + if compact in _CHROME_TRACE_TOKENS: + return compact + split_tokens = {token for token in re.split(r"[-_\s]+", lower) if token} + for token in _CHROME_TRACE_TOKENS: + if token in split_tokens: + return token + return None + + +def _trace_chrome_shape_ids( + trace: dict[str, Any] | None, +) -> dict[str, list[str]]: + """Map chrome token to generated top-level shape ids for one slide.""" + result: dict[str, list[str]] = {} + if not trace: + return result + for event in trace.get("events", []): + if event.get("decision") != "native": + continue + semantic_role = event.get("data-pptx-role") + placeholder = event.get("data-pptx-placeholder") + has_explicit_semantics = ( + semantic_role is not None or placeholder is not None + ) + token = ( + chrome_token_from_markers(semantic_role, placeholder) + if has_explicit_semantics + else _chrome_token_from_svg_id(event.get("id")) + ) + shape_id = event.get("shape_id") + if token and shape_id is not None: + shape_ids = result.setdefault(token, []) + normalized_shape_id = str(shape_id) + if normalized_shape_id not in shape_ids: + shape_ids.append(normalized_shape_id) + return result + + +def _trace_native_shape_ids( + trace: dict[str, Any] | None, +) -> dict[str, list[str]]: + """Map every traced SVG id to generated top-level shape ids.""" + result: dict[str, list[str]] = {} + if not trace: + return result + for event in trace.get("events", []): + if event.get("decision") != "native": + continue + svg_id = event.get("id") + shape_id = event.get("shape_id") + if not svg_id or shape_id is None: + continue + shape_ids = result.setdefault(str(svg_id), []) + normalized = str(shape_id) + if normalized not in shape_ids: + shape_ids.append(normalized) + return result + + +def _shape_id(elem: ET.Element) -> str | None: + for cnv in elem.iter(f"{{{PML_NS}}}cNvPr"): + return cnv.attrib.get("id") + return None + + +def _top_level_shapes_by_id(root: ET.Element) -> dict[str, ET.Element]: + sp_tree = root.find(f".//{{{PML_NS}}}cSld/{{{PML_NS}}}spTree") + if sp_tree is None: + return {} + shapes: dict[str, ET.Element] = {} + for child in list(sp_tree): + if child.tag not in _TOP_LEVEL_SHAPE_TAGS: + continue + shape_id = _shape_id(child) + if shape_id: + shapes[shape_id] = child + return shapes + + +def _timing_shape_ids(root: ET.Element) -> set[str]: + """Return slide-local shape ids referenced by animation timing.""" + return { + elem.attrib["spid"] + for elem in root.findall(f".//{{{PML_NS}}}timing//{{{PML_NS}}}spTgt") + if elem.attrib.get("spid") + } + + +def _relationship_ids_in_shape(elem: ET.Element) -> set[str]: + rel_ids: set[str] = set() + for node in elem.iter(): + for attr_name, value in node.attrib.items(): + if attr_name in _REL_ATTRS and value: + rel_ids.add(value) + return rel_ids + + +def _shape_relationships_supported( + elem: ET.Element, + rels: dict[str, dict[str, str]], +) -> bool: + """Only image relationships are safe to copy into a slide master here.""" + for rel_id in _relationship_ids_in_shape(elem): + attrs = rels.get(rel_id) + if not attrs: + return False + if attrs.get("TargetMode"): + return False + if attrs.get("Type") != IMAGE_REL_TYPE: + return False + return True + + +def _canonical_shape_xml( + elem: ET.Element, + rels: dict[str, dict[str, str]], +) -> bytes: + """Canonicalize ids and relationship ids for cross-slide equality.""" + clone = ET.fromstring(ET.tostring(elem, encoding="utf-8")) + for cnv in clone.iter(f"{{{PML_NS}}}cNvPr"): + cnv.set("id", "ID") + # Generated names include the slide-local shape id (for example, + # ``Image 2`` versus ``Image 8``) but do not affect rendering. + if "name" in cnv.attrib: + cnv.set("name", "NAME") + for fld in clone.iter(f"{{{DML_NS}}}fld"): + # The literal inside a slide-number field is a per-slide render + # cache that PowerPoint recomputes from the slide position. + if fld.attrib.get("type") == "slidenum": + cached = fld.find(f"{{{DML_NS}}}t") + if cached is not None: + cached.text = "" + for node in clone.iter(): + for attr_name, value in list(node.attrib.items()): + if attr_name not in _REL_ATTRS: + continue + attrs = rels.get(value, {}) + node.set( + attr_name, + f"{attrs.get('Type', '')}|{attrs.get('Target', '')}", + ) + return ET.tostring(clone, encoding="utf-8") + + +def _ensure_relationship( + rels_path: Path, + rel_type: str, + target: str, +) -> str: + existing = _find_relationship_id(rels_path, rel_type, target) + if existing: + return existing + return _append_relationship(rels_path, rel_type, target) + + +def _copy_shape_relationships_to_part( + elem: ET.Element, + slide_rels: dict[str, dict[str, str]], + target_rels_path: Path, +) -> ET.Element: + """Clone a shape and retarget supported relationship ids to another part.""" + clone = ET.fromstring(ET.tostring(elem, encoding="utf-8")) + for node in clone.iter(): + for attr_name, value in list(node.attrib.items()): + if attr_name not in _REL_ATTRS: + continue + rel = slide_rels.get(value) + if not rel: + raise RuntimeError(f"Missing slide relationship for {value}") + new_rid = _ensure_relationship( + target_rels_path, + rel["Type"], + rel["Target"], + ) + node.set(attr_name, new_rid) + return clone + + +def _copy_shape_relationships_to_master( + elem: ET.Element, + slide_rels: dict[str, dict[str, str]], + master_rels_path: Path, +) -> ET.Element: + """Clone a shape and retarget supported relationship ids to the master.""" + return _copy_shape_relationships_to_part(elem, slide_rels, master_rels_path) + + +def _next_master_shape_id(master_xml: str) -> int: + ids = [ + int(match) + for match in re.findall(r"]*\bid=\"(\d+)\"", master_xml) + ] + return max(ids, default=1) + 1 + + +def _renumber_shape_ids(elem: ET.Element, start_id: int) -> None: + next_id = start_id + for cnv in elem.iter(f"{{{PML_NS}}}cNvPr"): + cnv.set("id", str(next_id)) + next_id += 1 + + +def _append_shape_to_master(master_path: Path, elem: ET.Element) -> None: + master_xml = master_path.read_text(encoding="utf-8") + _renumber_shape_ids(elem, _next_master_shape_id(master_xml)) + shape_xml = ET.tostring(elem, encoding="unicode") + if "" not in master_xml: + raise RuntimeError(f"Slide master has no p:spTree: {master_path}") + master_path.write_text( + master_xml.replace("", f"{shape_xml}\n", 1), + encoding="utf-8", + ) + + +def _append_shape_to_part(part_path: Path, elem: ET.Element) -> None: + """Append a top-level shape to a master/layout spTree with fresh ids.""" + tree = ET.parse(part_path) + root = tree.getroot() + sp_tree = root.find(f".//{{{PML_NS}}}cSld/{{{PML_NS}}}spTree") + if sp_tree is None: + raise RuntimeError(f"PPTX part has no p:spTree: {part_path}") + existing_ids = [ + int(cnv.attrib["id"]) + for cnv in root.iter(f"{{{PML_NS}}}cNvPr") + if cnv.attrib.get("id", "").isdigit() + ] + clone = ET.fromstring(ET.tostring(elem, encoding="utf-8")) + _renumber_shape_ids(clone, max(existing_ids, default=1) + 1) + sp_tree.append(clone) + _write_xml_tree(part_path, tree) + + +def _write_xml_tree(path: Path, tree: ET.ElementTree) -> None: + tree.write(path, encoding="utf-8", xml_declaration=True) + + +COVER_LAYOUT_NAME = "Cover" + + +def _next_layout_part_number(extract_dir: Path) -> int: + layouts_dir = extract_dir / "ppt" / "slideLayouts" + numbers = [ + int(match.group(1)) + for path in layouts_dir.glob("slideLayout*.xml") + if (match := re.fullmatch(r"slideLayout(\d+)\.xml", path.name)) + ] + return max(numbers, default=0) + 1 + + +def _next_slide_layout_id(extract_dir: Path, master_xml: str) -> int: + """Return an unused id for a new sldLayoutId entry.""" + ids = [int(value) for value in re.findall(r'\bid="(\d{9,})"', master_xml)] + presentation_path = extract_dir / "ppt" / "presentation.xml" + if presentation_path.exists(): + ids.extend( + int(value) + for value in re.findall( + r'\bid="(\d{9,})"', presentation_path.read_text(encoding="utf-8") + ) + ) + return max([*ids, 2147483648]) + 1 + + +def _create_cover_layout(extract_dir: Path, master_part: str, base_layout_part: str) -> str: + """Clone a layout into a Cover layout that hides master shapes. + + Returns the new layout target relative to slide parts. + """ + base_layout_path = extract_dir / base_layout_part + layout_xml = base_layout_path.read_text(encoding="utf-8") + + root_match = re.search(r"]*>", layout_xml) + if not root_match: + raise RuntimeError(f"Slide layout has no p:sldLayout root: {base_layout_part}") + root_tag = root_match.group(0) + if "showMasterSp=" in root_tag: + new_root_tag = re.sub(r'showMasterSp="[^"]*"', 'showMasterSp="0"', root_tag) + else: + new_root_tag = root_tag[:-1] + ' showMasterSp="0">' + layout_xml = layout_xml.replace(root_tag, new_root_tag, 1) + layout_xml = re.sub( + r"(]*?)\s+name=\"[^\"]*\"", + rf'\g<1> name="{COVER_LAYOUT_NAME}"', + layout_xml, + count=1, + ) + + layout_num = _next_layout_part_number(extract_dir) + new_layout_part = f"ppt/slideLayouts/slideLayout{layout_num}.xml" + new_layout_path = extract_dir / new_layout_part + new_layout_path.write_text(layout_xml, encoding="utf-8") + + base_rels_path = _relationships_path_for_part(extract_dir, base_layout_part) + new_rels_path = _relationships_path_for_part(extract_dir, new_layout_part) + new_rels_path.parent.mkdir(exist_ok=True) + new_rels_path.write_text( + base_rels_path.read_text(encoding="utf-8"), encoding="utf-8" + ) + + master_path = extract_dir / master_part + master_rels_path = _relationships_path_for_part(extract_dir, master_part) + layout_target = posixpath.relpath( + new_layout_part, posixpath.dirname(master_part) + ) + rel_id = _append_relationship(master_rels_path, SLIDE_LAYOUT_REL_TYPE, layout_target) + + master_xml = master_path.read_text(encoding="utf-8") + layout_id = _next_slide_layout_id(extract_dir, master_xml) + entry = f'' + if "" not in master_xml: + raise RuntimeError(f"Slide master has no sldLayoutIdLst: {master_part}") + master_path.write_text( + master_xml.replace("", f"{entry}", 1), + encoding="utf-8", + ) + + content_types_path = extract_dir / "[Content_Types].xml" + content_types_path.write_text( + _add_content_type_override( + content_types_path.read_text(encoding="utf-8"), + new_layout_part, + "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml", + ), + encoding="utf-8", + ) + return posixpath.relpath(new_layout_part, "ppt/slides") + + +def _create_custom_layout( + extract_dir: Path, + master_part: str, + base_layout_part: str, + layout_name: str, + *, + show_master_shapes: bool = True, +) -> tuple[str, str]: + """Clone a clean custom layout and register it under its slide master. + + Returns ``(slide_relationship_target, package_part)``. + """ + base_layout_path = extract_dir / base_layout_part + tree = ET.parse(base_layout_path) + root = tree.getroot() + root.set("type", "cust") + root.set("preserve", "1") + root.set("showMasterSp", "1" if show_master_shapes else "0") + + c_sld = root.find(f"{{{PML_NS}}}cSld") + if c_sld is None: + raise RuntimeError(f"Slide layout has no p:cSld: {base_layout_part}") + c_sld.set("name", layout_name) + sp_tree = c_sld.find(f"{{{PML_NS}}}spTree") + if sp_tree is None: + raise RuntimeError(f"Slide layout has no p:spTree: {base_layout_part}") + for child in list(sp_tree): + if child.tag in _TOP_LEVEL_SHAPE_TAGS: + sp_tree.remove(child) + + layout_num = _next_layout_part_number(extract_dir) + new_layout_part = f"ppt/slideLayouts/slideLayout{layout_num}.xml" + new_layout_path = extract_dir / new_layout_part + _write_xml_tree(new_layout_path, tree) + + base_rels_path = _relationships_path_for_part(extract_dir, base_layout_part) + new_rels_path = _relationships_path_for_part(extract_dir, new_layout_part) + new_rels_path.parent.mkdir(exist_ok=True) + new_rels_path.write_text( + base_rels_path.read_text(encoding="utf-8"), + encoding="utf-8", + ) + + master_path = extract_dir / master_part + master_rels_path = _relationships_path_for_part(extract_dir, master_part) + layout_target = posixpath.relpath(new_layout_part, posixpath.dirname(master_part)) + rel_id = _append_relationship(master_rels_path, SLIDE_LAYOUT_REL_TYPE, layout_target) + master_xml = master_path.read_text(encoding="utf-8") + layout_id = _next_slide_layout_id(extract_dir, master_xml) + entry = f'' + if "" not in master_xml: + raise RuntimeError(f"Slide master has no sldLayoutIdLst: {master_part}") + master_path.write_text( + master_xml.replace( + "", + f"{entry}", + 1, + ), + encoding="utf-8", + ) + + content_types_path = extract_dir / "[Content_Types].xml" + content_types_path.write_text( + _add_content_type_override( + content_types_path.read_text(encoding="utf-8"), + new_layout_part, + "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml", + ), + encoding="utf-8", + ) + return posixpath.relpath(new_layout_part, "ppt/slides"), new_layout_part + + +def _clear_master_placeholder_shapes(master_path: Path) -> None: + """Remove base-template placeholders before installing an explicit template.""" + tree = ET.parse(master_path) + root = tree.getroot() + sp_tree = root.find(f".//{{{PML_NS}}}cSld/{{{PML_NS}}}spTree") + if sp_tree is None: + raise RuntimeError(f"Slide master has no p:spTree: {master_path}") + for child in list(sp_tree): + if child.find(f".//{{{PML_NS}}}ph") is not None: + sp_tree.remove(child) + _write_xml_tree(master_path, tree) + + +def _set_slide_layout_target(rels_path: Path, target: str) -> None: + """Point a slide's layout relationship at a different layout part.""" + content = rels_path.read_text(encoding="utf-8") + rel_id = None + for existing_id, attrs in _read_relationships(rels_path).items(): + if attrs.get("Type") == SLIDE_LAYOUT_REL_TYPE: + rel_id = existing_id + break + if rel_id is None: + raise RuntimeError(f"No slide layout relationship in {rels_path}") + pattern = re.compile( + rf'(]*\bId="{re.escape(rel_id)}"[^>]*\bTarget=")[^"]*(")' + ) + new_content, replaced = pattern.subn(rf"\g<1>{target}\g<2>", content, count=1) + if not replaced: + raise RuntimeError(f"Could not retarget layout relationship in {rels_path}") + rels_path.write_text(new_content, encoding="utf-8") + + +_BASELINE_LAYOUT_ROLE_TOKENS = ( + ("Cover", frozenset({"cover", "frontcover"}), ("封面",)), + ( + "Agenda", + frozenset({"agenda", "contents", "outline", "toc"}), + ("目录", "议程"), + ), + ( + "Section", + frozenset({"chapter", "divider", "section", "transition"}), + ("章节", "过渡页"), + ), + ( + "Closing", + frozenset({"closing", "end", "ending", "qa", "thankyou", "thanks"}), + ("封底", "结束", "结尾", "结语", "致谢", "谢谢"), + ), +) + + +def _baseline_layout_role(svg_path: Path) -> str: + """Use explicit page semantics, then fall back to conservative filename roles.""" + semantic_role = page_layout_name_from_svg(svg_path) + if semantic_role: + return semantic_role + stem = svg_path.stem.casefold() + tokens = { + token + for token in re.split(r"[^0-9a-z]+", stem) + if token and not token.isdigit() + } + for role, english_tokens, cjk_tokens in _BASELINE_LAYOUT_ROLE_TOKENS: + if tokens.intersection(english_tokens) or any( + token in stem for token in cjk_tokens + ): + return role + return "Content" + + +def _layout_identity(layout_path: Path) -> tuple[str, bool]: + """Return a layout's picker name and master-shape visibility.""" + root = ET.parse(layout_path).getroot() + c_sld = root.find(f"{{{PML_NS}}}cSld") + name = c_sld.attrib.get("name", "") if c_sld is not None else "" + return name, root.attrib.get("showMasterSp", "1") != "0" + + +def _shared_explicit_slide_background( + extract_dir: Path, + slide_nums: list[int], +) -> str | None: + """Return one exact background only when every family slide carries it.""" + backgrounds: list[str] = [] + for slide_num in slide_nums: + slide_path = extract_dir / "ppt" / "slides" / f"slide{slide_num}.xml" + background = _extract_slide_background_xml( + slide_path.read_text(encoding="utf-8") + ) + if background is None: + return None + backgrounds.append(background) + if not backgrounds or len(set(backgrounds)) != 1: + return None + return backgrounds[0] + + +def _extract_baseline_layout_families( + extract_dir: Path, + structure: PptxStructureContext, + svg_files: list[Path], + *, + verbose: bool = False, +) -> int: + """Build conservative post-generation layout families for a free SVG deck.""" + families: dict[tuple[str, str, bool], list[int]] = {} + base_layouts: dict[tuple[str, str, bool], str] = {} + for slide_num, svg_path in enumerate(svg_files, 1): + rels_path = ( + extract_dir + / "ppt" + / "slides" + / "_rels" + / f"slide{slide_num}.xml.rels" + ) + layout_target = _find_relationship_target(rels_path, SLIDE_LAYOUT_REL_TYPE) + if not layout_target: + raise RuntimeError(f"Slide {slide_num} has no slide layout relationship") + layout_part = _resolve_package_target( + f"ppt/slides/slide{slide_num}.xml", layout_target + ) + layout_name, show_master_shapes = _layout_identity(extract_dir / layout_part) + role = _baseline_layout_role(svg_path) + if role == "Content" and layout_name == COVER_LAYOUT_NAME: + role = COVER_LAYOUT_NAME + key = ( + structure.slide_master_part(slide_num), + role, + show_master_shapes, + ) + families.setdefault(key, []).append(slide_num) + base_layouts.setdefault(key, layout_part) + + created = 0 + lifted_backgrounds = 0 + role_counts: dict[tuple[str, str], int] = {} + for key, slide_nums in families.items(): + master_part, role, show_master_shapes = key + role_key = (master_part, role) + role_counts[role_key] = role_counts.get(role_key, 0) + 1 + variant = role_counts[role_key] + layout_name = role if variant == 1 else f"{role} {variant}" + layout_target, layout_part = _create_custom_layout( + extract_dir, + master_part, + base_layouts[key], + layout_name, + show_master_shapes=show_master_shapes, + ) + + background_xml = _shared_explicit_slide_background( + extract_dir, slide_nums + ) + if background_xml is not None: + layout_path = extract_dir / layout_part + layout_xml = layout_path.read_text(encoding="utf-8") + updated_layout_xml = _put_background_on_part(layout_xml, background_xml) + if updated_layout_xml is not None: + layout_path.write_text(updated_layout_xml, encoding="utf-8") + for slide_num in slide_nums: + slide_path = ( + extract_dir / "ppt" / "slides" / f"slide{slide_num}.xml" + ) + slide_path.write_text( + _remove_slide_background_xml( + slide_path.read_text(encoding="utf-8") + ), + encoding="utf-8", + ) + lifted_backgrounds += len(slide_nums) + + for slide_num in slide_nums: + rels_path = ( + extract_dir + / "ppt" + / "slides" + / "_rels" + / f"slide{slide_num}.xml.rels" + ) + _set_slide_layout_target(rels_path, layout_target) + created += 1 + + if verbose and created: + print( + " Baseline layout families: " + f"created {created} reusable layout(s), " + f"lifted {lifted_backgrounds} slide background(s)" + ) + return created + + +def _promote_common_chrome_shapes_to_layouts( + extract_dir: Path, + slide_count: int, + conversion_traces: list[dict[str, Any]] | None, + *, + verbose: bool = False, +) -> int: + """Promote exact leading chrome shared by every slide in one layout.""" + if not conversion_traces: + return 0 + trace_by_slide = { + int(trace.get("slide_num", 0)): trace + for trace in conversion_traces + if trace.get("slide_num") is not None + } + if len(trace_by_slide) < slide_count: + return 0 + + slides_by_layout: dict[str, list[int]] = {} + for slide_num in range(1, slide_count + 1): + rels_path = ( + extract_dir + / "ppt" + / "slides" + / "_rels" + / f"slide{slide_num}.xml.rels" + ) + layout_target = _find_relationship_target(rels_path, SLIDE_LAYOUT_REL_TYPE) + if not layout_target: + raise RuntimeError(f"Slide {slide_num} has no slide layout relationship") + layout_part = _resolve_package_target( + f"ppt/slides/slide{slide_num}.xml", + layout_target, + ) + slides_by_layout.setdefault(layout_part, []).append(slide_num) + + promoted = 0 + promoted_roles = 0 + for layout_part, slide_nums in slides_by_layout.items(): + if len(slide_nums) < 2: + continue + slide_state: dict[int, dict[str, Any]] = {} + for slide_num in slide_nums: + slide_path = extract_dir / "ppt" / "slides" / f"slide{slide_num}.xml" + rels_path = ( + extract_dir + / "ppt" + / "slides" + / "_rels" + / f"slide{slide_num}.xml.rels" + ) + tree = ET.parse(slide_path) + root = tree.getroot() + slide_state[slide_num] = { + "path": slide_path, + "rels": _read_relationships(rels_path), + "root": root, + "shapes": _top_level_shapes_by_id(root), + "timing_shape_ids": _timing_shape_ids(root), + "tokens": _trace_chrome_shape_ids(trace_by_slide.get(slide_num)), + "tree": tree, + } + + common_tokens = set.intersection(*( + set(state["tokens"]) + for state in slide_state.values() + )) + common_tokens.difference_update(_PAGE_NUMBER_TOKENS) + candidates: dict[str, dict[int, str]] = {} + for token in sorted(common_tokens): + shape_ids_by_slide: dict[int, str] = {} + canonical_shapes: set[bytes] = set() + for slide_num in slide_nums: + state = slide_state[slide_num] + shape_ids = state["tokens"].get(token, []) + if len(shape_ids) != 1: + break + shape_id = shape_ids[0] + shape = state["shapes"].get(shape_id) + if shape is None or shape_id in state["timing_shape_ids"]: + break + if not _shape_relationships_supported(shape, state["rels"]): + break + shape_ids_by_slide[slide_num] = shape_id + canonical_shapes.add(_canonical_shape_xml(shape, state["rels"])) + if len(shape_ids_by_slide) == len(slide_nums) and len(canonical_shapes) == 1: + candidates[token] = shape_ids_by_slide + + if not candidates: + continue + + # Layout shapes render behind slide-local shapes. Keep visual z-order by + # accepting only the identical leading prefix shared by every family page. + token_by_shape_id = { + slide_num: { + shape_ids[slide_num]: token + for token, shape_ids in candidates.items() + } + for slide_num in slide_nums + } + leading_orders: list[list[str]] = [] + for slide_num in slide_nums: + order: list[str] = [] + for shape_id in slide_state[slide_num]["shapes"]: + token = token_by_shape_id[slide_num].get(shape_id) + if token is None: + break + order.append(token) + leading_orders.append(order) + safe_tokens = list(leading_orders[0]) + for order in leading_orders[1:]: + common_length = 0 + for expected, actual in zip(safe_tokens, order): + if expected != actual: + break + common_length += 1 + safe_tokens = safe_tokens[:common_length] + if not safe_tokens: + break + if not safe_tokens: + continue + + layout_path = extract_dir / layout_part + layout_rels_path = _relationships_path_for_part(extract_dir, layout_part) + for token in safe_tokens: + shape_ids_by_slide = candidates[token] + first_slide = slide_nums[0] + first_state = slide_state[first_slide] + shape = first_state["shapes"][shape_ids_by_slide[first_slide]] + layout_shape = _copy_shape_relationships_to_part( + shape, + first_state["rels"], + layout_rels_path, + ) + _append_shape_to_part(layout_path, layout_shape) + promoted_roles += 1 + for slide_num, shape_id in shape_ids_by_slide.items(): + state = slide_state[slide_num] + shape_to_remove = state["shapes"].get(shape_id) + sp_tree = state["root"].find( + f".//{{{PML_NS}}}cSld/{{{PML_NS}}}spTree" + ) + if sp_tree is not None and shape_to_remove is not None: + sp_tree.remove(shape_to_remove) + promoted += 1 + + for slide_num in slide_nums: + state = slide_state[slide_num] + _write_xml_tree(state["path"], state["tree"]) + + if verbose and promoted: + print( + " Baseline layout chrome: " + f"promoted {promoted} slide shape(s) across " + f"{promoted_roles} shared object(s)" + ) + return promoted + + +_TEMPLATE_PLACEHOLDER_TYPES = { + "title": "title", + "subtitle": "subTitle", + "body": "body", + "picture": "pic", + "chart": "chart", + "table": "tbl", + "object": "obj", + "media": "media", + "date": "dt", + "footer": "ftr", + "slide-number": "sldNum", +} +_TEMPLATE_PLACEHOLDER_PROMPTS = { + "title": "Click to add title", + "subtitle": "Click to add subtitle", + "body": "Click to add text", + "picture": "Click to add picture", + "chart": "Click to add chart", + "table": "Click to add table", + "object": "Click to add content", + "media": "Click to add media", + "date": "Date", + "footer": "Footer", +} + + +def _template_runtime_slides( + extract_dir: Path, + specs: list[TemplateSlideSpec], + conversion_traces: list[dict[str, Any]] | None, +) -> list[_TemplateRuntimeSlide]: + """Load slide XML state and join it with SVG-to-shape trace ids.""" + if not conversion_traces: + raise TemplateStructureError( + "Template export requires native conversion traces for every slide" + ) + trace_by_slide = { + int(trace.get("slide_num", 0)): trace + for trace in conversion_traces + if trace.get("slide_num") is not None + } + states: list[_TemplateRuntimeSlide] = [] + for spec in specs: + trace = trace_by_slide.get(spec.slide_num) + if trace is None: + raise TemplateStructureError( + f"{spec.svg_path.name}: missing native conversion trace" + ) + slide_path = extract_dir / "ppt" / "slides" / f"slide{spec.slide_num}.xml" + rels_path = ( + extract_dir + / "ppt" + / "slides" + / "_rels" + / f"slide{spec.slide_num}.xml.rels" + ) + tree = ET.parse(slide_path) + root = tree.getroot() + states.append(_TemplateRuntimeSlide( + spec=spec, + slide_path=slide_path, + rels_path=rels_path, + tree=tree, + root=root, + rels=_read_relationships(rels_path), + shapes=_top_level_shapes_by_id(root), + shape_ids_by_svg_id=_trace_native_shape_ids(trace), + )) + return states + + +def _template_shape_for_item( + state: _TemplateRuntimeSlide, + item: TemplateElementSpec, +) -> ET.Element | None: + """Resolve one metadata item to its generated top-level DrawingML shape.""" + shape_ids = [ + shape_id + for shape_id in state.shape_ids_by_svg_id.get(item.element_id, []) + if shape_id in state.shapes + ] + if len(shape_ids) == 1: + return state.shapes[shape_ids[0]] + if not shape_ids and item.layer and item.order == 0 and item.tag in {"rect", "g"}: + if _extract_slide_background_xml( + state.slide_path.read_text(encoding="utf-8") + ): + return None + if not shape_ids: + raise TemplateStructureError( + f"{state.spec.svg_path.name}: metadata element {item.element_id!r} " + "did not produce one top-level native shape" + ) + raise TemplateStructureError( + f"{state.spec.svg_path.name}: metadata element {item.element_id!r} " + f"resolved to {len(shape_ids)} top-level shapes; use one direct SVG element" + ) + + +def _slide_sp_tree(state: _TemplateRuntimeSlide) -> ET.Element: + sp_tree = state.root.find(f".//{{{PML_NS}}}cSld/{{{PML_NS}}}spTree") + if sp_tree is None: + raise RuntimeError(f"Slide has no p:spTree: {state.slide_path}") + return sp_tree + + +def _presentation_slide_size_emu(extract_dir: Path) -> tuple[int, int]: + """Return the package slide size in EMU.""" + presentation_path = extract_dir / "ppt" / "presentation.xml" + root = ET.parse(presentation_path).getroot() + slide_size = root.find(f"{{{PML_NS}}}sldSz") + if slide_size is None: + raise TemplateStructureError("presentation.xml has no p:sldSz") + try: + width = int(slide_size.attrib["cx"]) + height = int(slide_size.attrib["cy"]) + except (KeyError, ValueError) as exc: + raise TemplateStructureError("presentation.xml has an invalid p:sldSz") from exc + if width <= 0 or height <= 0: + raise TemplateStructureError("presentation.xml p:sldSz must be positive") + return width, height + + +def _solid_background_xml_from_shape( + shape: ET.Element, + slide_size_emu: tuple[int, int], +) -> str | None: + """Convert one exact full-slide solid rectangle shape into p:bg XML.""" + if shape.tag != f"{{{PML_NS}}}sp": + return None + if shape.find(f"{{{PML_NS}}}txBody") is not None: + return None + sp_pr = shape.find(f"{{{PML_NS}}}spPr") + if sp_pr is None: + return None + xfrm = sp_pr.find(f"{{{DML_NS}}}xfrm") + if xfrm is None or xfrm.attrib: + return None + off = xfrm.find(f"{{{DML_NS}}}off") + ext = xfrm.find(f"{{{DML_NS}}}ext") + if off is None or ext is None: + return None + try: + bounds = ( + int(off.attrib["x"]), + int(off.attrib["y"]), + int(ext.attrib["cx"]), + int(ext.attrib["cy"]), + ) + except (KeyError, ValueError): + return None + if bounds != (0, 0, *slide_size_emu): + return None + + geometry = sp_pr.find(f"{{{DML_NS}}}prstGeom") + if geometry is None or geometry.attrib.get("prst") != "rect": + return None + solid_fill = sp_pr.find(f"{{{DML_NS}}}solidFill") + if solid_fill is None: + return None + competing_fills = { + f"{{{DML_NS}}}noFill", + f"{{{DML_NS}}}gradFill", + f"{{{DML_NS}}}blipFill", + f"{{{DML_NS}}}pattFill", + f"{{{DML_NS}}}grpFill", + } + if any(child.tag in competing_fills for child in sp_pr): + return None + line = sp_pr.find(f"{{{DML_NS}}}ln") + if line is not None and line.find(f"{{{DML_NS}}}noFill") is None: + return None + for effect_tag in (f"{{{DML_NS}}}effectLst", f"{{{DML_NS}}}effectDag"): + effect = sp_pr.find(effect_tag) + if effect is not None and (effect.attrib or list(effect)): + return None + + background = ET.Element(f"{{{PML_NS}}}bg") + background_props = ET.SubElement(background, f"{{{PML_NS}}}bgPr") + background_props.append( + ET.fromstring(ET.tostring(solid_fill, encoding="utf-8")) + ) + ET.SubElement(background_props, f"{{{DML_NS}}}effectLst") + return ET.tostring(background, encoding="unicode") + + +def _remove_template_shape( + state: _TemplateRuntimeSlide, + shape: ET.Element, +) -> None: + sp_tree = _slide_sp_tree(state) + if shape not in list(sp_tree): + raise TemplateStructureError( + f"{state.spec.svg_path.name}: structure shape is not slide-local" + ) + sp_tree.remove(shape) + + +def _move_template_background( + states: list[_TemplateRuntimeSlide], + target_path: Path, +) -> None: + backgrounds = [ + _extract_slide_background_xml(state.slide_path.read_text(encoding="utf-8")) + for state in states + ] + if not backgrounds or any(background is None for background in backgrounds): + raise TemplateStructureError( + "Template background metadata must resolve to an explicit background " + "on every affected slide" + ) + canonical_backgrounds = set() + for background in backgrounds: + if background is None: + continue + wrapper = ET.fromstring( + f'{background}' + ) + canonical_backgrounds.add( + ET.tostring(list(wrapper)[0], encoding="utf-8") + ) + if len(canonical_backgrounds) != 1: + slide_names = ", ".join(state.spec.svg_path.name for state in states) + raise TemplateStructureError( + f"Explicit template background differs across slides: {slide_names}" + ) + background_xml = backgrounds[0] + if background_xml is None: + raise TemplateStructureError("Template background is unexpectedly empty") + target_xml = target_path.read_text(encoding="utf-8") + updated = _put_background_on_part(target_xml, background_xml) + if updated is None: + raise TemplateStructureError( + f"Cannot install explicit background on {target_path.name}" + ) + target_path.write_text(updated, encoding="utf-8") + for state in states: + c_sld = state.root.find(f"{{{PML_NS}}}cSld") + background = ( + c_sld.find(f"{{{PML_NS}}}bg") if c_sld is not None else None + ) + if c_sld is None or background is None: + raise TemplateStructureError( + f"{state.spec.svg_path.name}: explicit background disappeared " + "during template structure assembly" + ) + c_sld.remove(background) + + +def _move_template_solid_background_shapes( + states: list[_TemplateRuntimeSlide], + shapes: list[ET.Element], + target_path: Path, + slide_size_emu: tuple[int, int], +) -> bool: + """Move repeated full-slide solid rects into a master/layout p:bg.""" + backgrounds = [ + _solid_background_xml_from_shape(shape, slide_size_emu) + for shape in shapes + ] + if not any(backgrounds): + return False + if any(background is None for background in backgrounds): + raise TemplateStructureError( + "A template background resolves to a full-slide solid rect on only " + "some slides sharing the structure" + ) + canonical = {background for background in backgrounds if background is not None} + if len(canonical) != 1: + slide_names = ", ".join(state.spec.svg_path.name for state in states) + raise TemplateStructureError( + f"Explicit template solid background differs across slides: {slide_names}" + ) + background_xml = backgrounds[0] + if background_xml is None: + return False + target_xml = target_path.read_text(encoding="utf-8") + updated = _put_background_on_part(target_xml, background_xml) + if updated is None: + raise TemplateStructureError( + f"Cannot install explicit solid background on {target_path.name}" + ) + target_path.write_text(updated, encoding="utf-8") + for state, shape in zip(states, shapes): + _remove_template_shape(state, shape) + return True + + +def _set_slide_tree_background( + state: _TemplateRuntimeSlide, + background_xml: str, +) -> None: + """Replace the slide tree's p:bg with explicit background XML.""" + c_sld = state.root.find(f"{{{PML_NS}}}cSld") + if c_sld is None: + raise TemplateStructureError( + f"{state.spec.svg_path.name}: slide has no p:cSld" + ) + for existing in list(c_sld): + if existing.tag == f"{{{PML_NS}}}bg": + c_sld.remove(existing) + background = ET.fromstring(background_xml) + sp_tree_tag = f"{{{PML_NS}}}spTree" + insert_at = next( + (index for index, child in enumerate(c_sld) if child.tag == sp_tree_tag), + 0, + ) + c_sld.insert(insert_at, background) + + +def _apply_template_slide_backgrounds( + states: list[_TemplateRuntimeSlide], + slide_size_emu: tuple[int, int], +) -> int: + """Compile one-page solid backgrounds into slide-level p:bg.""" + applied = 0 + for state in states: + items = [ + item for item in state.spec.elements + if item.layer == "slide" and item.is_background + ] + if not items: + continue + item = items[0] + shape = _template_shape_for_item(state, item) + if shape is None: + c_sld = state.root.find(f"{{{PML_NS}}}cSld") + if c_sld is None or c_sld.find(f"{{{PML_NS}}}bg") is None: + raise TemplateStructureError( + f"{state.spec.svg_path.name}: slide background disappeared " + "during template structure assembly" + ) + applied += 1 + continue + background_xml = _solid_background_xml_from_shape(shape, slide_size_emu) + if background_xml is None: + raise TemplateStructureError( + f"{state.spec.svg_path.name}: {item.element_id!r} must remain an " + "exact full-slide solid rectangle" + ) + _remove_template_shape(state, shape) + _set_slide_tree_background(state, background_xml) + applied += 1 + return applied + + +def _move_template_static_shape( + states: list[_TemplateRuntimeSlide], + item: TemplateElementSpec, + target_path: Path, + target_rels_path: Path, + slide_size_emu: tuple[int, int], +) -> None: + shapes = [_template_shape_for_item(state, item) for state in states] + if any(shape is None for shape in shapes): + if not all(shape is None for shape in shapes): + raise TemplateStructureError( + f"{item.element_id}: structure item is a background on only some slides" + ) + _move_template_background(states, target_path) + return + + resolved_shapes = [shape for shape in shapes if shape is not None] + if ( + item.is_background + and _move_template_solid_background_shapes( + states, + resolved_shapes, + target_path, + slide_size_emu, + ) + ): + return + canonical = { + _canonical_shape_xml(shape, state.rels) + for state, shape in zip(states, resolved_shapes) + } + if len(canonical) != 1: + slide_names = ", ".join(state.spec.svg_path.name for state in states) + raise TemplateStructureError( + f"Explicit structure element {item.element_id!r} differs across slides: " + f"{slide_names}" + ) + for state, shape in zip(states, resolved_shapes): + shape_id = _shape_id(shape) + if shape_id and shape_id in _timing_shape_ids(state.root): + raise TemplateStructureError( + f"{state.spec.svg_path.name}: structure element {item.element_id!r} " + "is referenced by slide timing" + ) + if not _shape_relationships_supported(shape, state.rels): + raise TemplateStructureError( + f"{state.spec.svg_path.name}: structure element {item.element_id!r} " + "uses a non-image or external relationship" + ) + + prototype_state = states[0] + prototype_shape = resolved_shapes[0] + target_shape = _copy_shape_relationships_to_part( + prototype_shape, + prototype_state.rels, + target_rels_path, + ) + _append_shape_to_part(target_path, target_shape) + for state, shape in zip(states, resolved_shapes): + _remove_template_shape(state, shape) + + +def _shape_bounds_emu( + shape: ET.Element, + override_px: tuple[float, float, float, float] | None, +) -> tuple[int, int, int, int]: + if override_px is not None: + x, y, width, height = override_px + return tuple( + round(value * EMU_PER_PX) + for value in (x, y, width, height) + ) + + xfrm = shape.find(f"{{{PML_NS}}}spPr/{{{DML_NS}}}xfrm") + if xfrm is None: + xfrm = shape.find(f"{{{PML_NS}}}xfrm") + if xfrm is None: + raise TemplateStructureError( + "Placeholder shape has no directly readable DrawingML transform; " + "set data-pptx-placeholder-bounds" + ) + off = xfrm.find(f"{{{DML_NS}}}off") + ext = xfrm.find(f"{{{DML_NS}}}ext") + if off is None or ext is None: + raise TemplateStructureError("Placeholder transform has no a:off/a:ext") + try: + return ( + int(off.attrib["x"]), + int(off.attrib["y"]), + int(ext.attrib["cx"]), + int(ext.attrib["cy"]), + ) + except (KeyError, ValueError) as exc: + raise TemplateStructureError("Placeholder transform is invalid") from exc + + +def _replace_shape_xfrm( + sp_pr: ET.Element, + bounds: tuple[int, int, int, int], +) -> None: + for existing in list(sp_pr): + if existing.tag == f"{{{DML_NS}}}xfrm": + sp_pr.remove(existing) + x, y, width, height = bounds + xfrm = ET.Element(f"{{{DML_NS}}}xfrm") + ET.SubElement(xfrm, f"{{{DML_NS}}}off", {"x": str(x), "y": str(y)}) + ET.SubElement( + xfrm, + f"{{{DML_NS}}}ext", + {"cx": str(width), "cy": str(height)}, + ) + sp_pr.insert(0, xfrm) + + +def _placeholder_text_body( + source_shape: ET.Element, + item: TemplateElementSpec, +) -> ET.Element: + tx_body = ET.Element(f"{{{PML_NS}}}txBody") + source_tx_body = source_shape.find(f"{{{PML_NS}}}txBody") + source_body_pr = ( + source_tx_body.find(f"{{{DML_NS}}}bodyPr") + if source_tx_body is not None + else None + ) + source_lst_style = ( + source_tx_body.find(f"{{{DML_NS}}}lstStyle") + if source_tx_body is not None + else None + ) + tx_body.append( + ET.fromstring(ET.tostring(source_body_pr, encoding="utf-8")) + if source_body_pr is not None + else ET.Element(f"{{{DML_NS}}}bodyPr") + ) + tx_body.append( + ET.fromstring(ET.tostring(source_lst_style, encoding="utf-8")) + if source_lst_style is not None + else ET.Element(f"{{{DML_NS}}}lstStyle") + ) + + paragraph = ET.SubElement(tx_body, f"{{{DML_NS}}}p") + if item.placeholder == "body": + paragraph_props = ET.SubElement(paragraph, f"{{{DML_NS}}}pPr") + ET.SubElement(paragraph_props, f"{{{DML_NS}}}buNone") + source_run_pr = ( + source_tx_body.find(f".//{{{DML_NS}}}rPr") + if source_tx_body is not None + else None + ) + if item.placeholder in {"slide-number", "date"}: + field_type = ( + "slidenum" + if item.placeholder == "slide-number" + else "datetimeFigureOut" + ) + field = ET.SubElement( + paragraph, + f"{{{DML_NS}}}fld", + {"id": f"{{{str(uuid.uuid4()).upper()}}}", "type": field_type}, + ) + if source_run_pr is not None: + field.append(ET.fromstring(ET.tostring(source_run_pr, encoding="utf-8"))) + source_text = "" + if source_tx_body is not None: + source_text = "".join( + text.text or "" + for text in source_tx_body.findall(f".//{{{DML_NS}}}t") + ) + field_text = ( + "‹#›" + if item.placeholder == "slide-number" + else source_text or "Date" + ) + ET.SubElement(field, f"{{{DML_NS}}}t").text = field_text + else: + run = ET.SubElement(paragraph, f"{{{DML_NS}}}r") + if source_run_pr is not None: + run.append(ET.fromstring(ET.tostring(source_run_pr, encoding="utf-8"))) + ET.SubElement(run, f"{{{DML_NS}}}t").text = _TEMPLATE_PLACEHOLDER_PROMPTS.get( + item.placeholder or "", + "Click to add content", + ) + ET.SubElement(paragraph, f"{{{DML_NS}}}endParaRPr", {"lang": "en-US"}) + return tx_body + + +def _set_body_placeholder_no_bullets( + shape: ET.Element, + item: TemplateElementSpec, +) -> None: + """Preserve SVG prose instead of inheriting master bullet defaults.""" + if item.placeholder != "body": + return + tx_body = shape.find(f"{{{PML_NS}}}txBody") + if tx_body is None: + return + bullet_tags = { + f"{{{DML_NS}}}buNone", + f"{{{DML_NS}}}buAutoNum", + f"{{{DML_NS}}}buChar", + f"{{{DML_NS}}}buBlip", + } + trailing_tags = { + f"{{{DML_NS}}}tabLst", + f"{{{DML_NS}}}defRPr", + f"{{{DML_NS}}}extLst", + } + for paragraph in tx_body.findall(f"{{{DML_NS}}}p"): + paragraph_props = paragraph.find(f"{{{DML_NS}}}pPr") + if paragraph_props is None: + paragraph_props = ET.Element(f"{{{DML_NS}}}pPr") + paragraph.insert(0, paragraph_props) + for child in list(paragraph_props): + if child.tag in bullet_tags: + paragraph_props.remove(child) + insert_at = next( + ( + index + for index, child in enumerate(paragraph_props) + if child.tag in trailing_tags + ), + len(paragraph_props), + ) + paragraph_props.insert(insert_at, ET.Element(f"{{{DML_NS}}}buNone")) + + +def _set_placeholder_theme_font_role( + shape: ET.Element, + item: TemplateElementSpec, + theme_font_spec: ThemeFontSpec | None, +) -> None: + """Force semantic text placeholders onto the correct theme font role.""" + if theme_font_spec is None: + return + if item.placeholder == "title": + prefix = "+mj" + elif item.placeholder in _TEMPLATE_PLACEHOLDER_TYPES: + prefix = "+mn" + else: + return + for props_tag in ("rPr", "defRPr", "endParaRPr"): + for props in shape.iter(f"{{{DML_NS}}}{props_tag}"): + for font_tag, suffix in (("latin", "lt"), ("ea", "ea"), ("cs", "cs")): + font = props.find(f"{{{DML_NS}}}{font_tag}") + if font is not None: + font.set("typeface", f"{prefix}-{suffix}") + + +def _layout_placeholder_shape( + source_shape: ET.Element, + item: TemplateElementSpec, + placeholder_idx: int | None, + theme_font_spec: ThemeFontSpec | None = None, +) -> ET.Element: + """Build one reusable p:sp placeholder from a prototype slide object.""" + placeholder_type = _TEMPLATE_PLACEHOLDER_TYPES.get(item.placeholder or "") + if placeholder_type is None: + raise TemplateStructureError( + f"Unsupported placeholder type: {item.placeholder!r}" + ) + bounds = _shape_bounds_emu(source_shape, item.placeholder_bounds) + shape = ET.Element(f"{{{PML_NS}}}sp") + nv_sp_pr = ET.SubElement(shape, f"{{{PML_NS}}}nvSpPr") + ET.SubElement( + nv_sp_pr, + f"{{{PML_NS}}}cNvPr", + {"id": "2", "name": f"{item.element_id} Placeholder"}, + ) + c_nv_sp_pr = ET.SubElement(nv_sp_pr, f"{{{PML_NS}}}cNvSpPr") + ET.SubElement(c_nv_sp_pr, f"{{{DML_NS}}}spLocks", {"noGrp": "1"}) + nv_pr = ET.SubElement(nv_sp_pr, f"{{{PML_NS}}}nvPr") + placeholder_attrs = {"type": placeholder_type} + if placeholder_idx is not None: + placeholder_attrs["idx"] = str(placeholder_idx) + elif item.placeholder != "title": + raise TemplateStructureError( + f"Placeholder {item.element_id!r} requires an idx" + ) + ET.SubElement(nv_pr, f"{{{PML_NS}}}ph", placeholder_attrs) + + source_sp_pr = ( + source_shape.find(f"{{{PML_NS}}}spPr") + if source_shape.tag == f"{{{PML_NS}}}sp" + else None + ) + if source_sp_pr is not None: + sp_pr = ET.fromstring(ET.tostring(source_sp_pr, encoding="utf-8")) + else: + sp_pr = ET.Element(f"{{{PML_NS}}}spPr") + geometry = ET.SubElement(sp_pr, f"{{{DML_NS}}}prstGeom", {"prst": "rect"}) + ET.SubElement(geometry, f"{{{DML_NS}}}avLst") + ET.SubElement(sp_pr, f"{{{DML_NS}}}noFill") + line = ET.SubElement(sp_pr, f"{{{DML_NS}}}ln") + ET.SubElement(line, f"{{{DML_NS}}}noFill") + _replace_shape_xfrm(sp_pr, bounds) + shape.append(sp_pr) + shape.append(_placeholder_text_body(source_shape, item)) + _set_placeholder_theme_font_role(shape, item, theme_font_spec) + return shape + + +def _patch_slide_placeholder( + shape: ET.Element, + item: TemplateElementSpec, + placeholder_idx: int | None, + placeholder_type: str | None = None, + theme_font_spec: ThemeFontSpec | None = None, +) -> None: + resolved_type = placeholder_type or _TEMPLATE_PLACEHOLDER_TYPES.get( + item.placeholder or "" + ) + if resolved_type is None: + raise TemplateStructureError( + f"Unsupported placeholder type: {item.placeholder!r}" + ) + nv_paths = { + f"{{{PML_NS}}}sp": f"{{{PML_NS}}}nvSpPr/{{{PML_NS}}}nvPr", + f"{{{PML_NS}}}pic": f"{{{PML_NS}}}nvPicPr/{{{PML_NS}}}nvPr", + f"{{{PML_NS}}}graphicFrame": ( + f"{{{PML_NS}}}nvGraphicFramePr/{{{PML_NS}}}nvPr" + ), + } + nv_path = nv_paths.get(shape.tag) + if nv_path is None: + raise TemplateStructureError( + f"Placeholder {item.element_id!r} converted to unsupported " + f"DrawingML element {shape.tag.rsplit('}', 1)[-1]!r}; text/picture/" + "native chart/table placeholders must remain one top-level object" + ) + nv_pr = shape.find(nv_path) + if nv_pr is None: + raise TemplateStructureError( + f"Placeholder {item.element_id!r} has no non-visual properties" + ) + _set_body_placeholder_no_bullets(shape, item) + _set_placeholder_theme_font_role(shape, item, theme_font_spec) + for existing in list(nv_pr): + if existing.tag == f"{{{PML_NS}}}ph": + nv_pr.remove(existing) + placeholder_attrs: dict[str, str] = {} + if ( + placeholder_type is not None + or resolved_type != "obj" + or placeholder_idx is None + ): + placeholder_attrs["type"] = resolved_type + if placeholder_idx is not None: + placeholder_attrs["idx"] = str(placeholder_idx) + ph = ET.Element(f"{{{PML_NS}}}ph", placeholder_attrs) + ext_tag = f"{{{PML_NS}}}extLst" + insert_at = next( + (idx for idx, child in enumerate(nv_pr) if child.tag == ext_tag), + len(nv_pr), + ) + nv_pr.insert(insert_at, ph) + + +def _set_template_layout_header_footer( + layout_path: Path, + placeholders: tuple[TemplateElementSpec, ...], +) -> None: + """Enable declared footer fields for slides newly created from the layout.""" + kinds = {item.placeholder for item in placeholders} + if not kinds.intersection({"date", "footer", "slide-number"}): + return + tree = ET.parse(layout_path) + root = tree.getroot() + hf = root.find(f"{{{PML_NS}}}hf") + if hf is None: + hf = ET.Element(f"{{{PML_NS}}}hf") + trailing_tags = { + f"{{{PML_NS}}}timing", + f"{{{PML_NS}}}transition", + f"{{{PML_NS}}}extLst", + } + insert_at = next( + (idx for idx, child in enumerate(root) if child.tag in trailing_tags), + len(root), + ) + root.insert(insert_at, hf) + hf.set("hdr", "0") + hf.set("dt", "1" if "date" in kinds else "0") + hf.set("ftr", "1" if "footer" in kinds else "0") + hf.set("sldNum", "1" if "slide-number" in kinds else "0") + _write_xml_tree(layout_path, tree) + + +def _apply_template_structure( + extract_dir: Path, + structure: PptxStructureContext, + specs: list[TemplateSlideSpec], + conversion_traces: list[dict[str, Any]] | None, + theme_font_spec: ThemeFontSpec | None, + *, + verbose: bool = False, +) -> None: + """Materialize explicit SVG master/layout/placeholder metadata into OOXML.""" + states = _template_runtime_slides(extract_dir, specs, conversion_traces) + states_by_slide = {state.spec.slide_num: state for state in states} + slide_size_emu = _presentation_slide_size_emu(extract_dir) + + master_items = specs[0].master_elements + states_by_master: dict[str, list[_TemplateRuntimeSlide]] = {} + for state in states: + master_part = structure.slide_master_part(state.spec.slide_num) + states_by_master.setdefault(master_part, []).append(state) + for master_part, master_states in states_by_master.items(): + master_path = extract_dir / master_part + master_rels_path = _relationships_path_for_part(extract_dir, master_part) + _clear_master_placeholder_shapes(master_path) + for item in master_items: + _move_template_static_shape( + master_states, + item, + master_path, + master_rels_path, + slide_size_emu, + ) + + specs_by_layout: dict[str, list[TemplateSlideSpec]] = {} + for spec in specs: + specs_by_layout.setdefault(spec.layout_key, []).append(spec) + placeholder_count = 0 + layout_shape_count = 0 + for layout_key, layout_specs in specs_by_layout.items(): + layout_states = [states_by_slide[spec.slide_num] for spec in layout_specs] + master_parts = { + structure.slide_master_part(spec.slide_num) for spec in layout_specs + } + if len(master_parts) != 1: + raise TemplateStructureError( + f"Layout {layout_key!r} spans multiple slide masters; use distinct " + "layout keys per master" + ) + master_part = next(iter(master_parts)) + prototype = layout_specs[0] + base_target = structure.slide_layout_target(prototype.slide_num) + base_layout_part = _resolve_package_target( + f"ppt/slides/slide{prototype.slide_num}.xml", + base_target, + ) + layout_target, layout_part = _create_custom_layout( + extract_dir, + master_part, + base_layout_part, + prototype.layout_name, + ) + layout_path = extract_dir / layout_part + layout_rels_path = _relationships_path_for_part(extract_dir, layout_part) + + placeholder_idx = 1 + assigned_placeholder_indices: set[int] = set() + for item in prototype.elements: + if item.layer == "layout": + _move_template_static_shape( + layout_states, + item, + layout_path, + layout_rels_path, + slide_size_emu, + ) + layout_shape_count += 1 + continue + if not item.placeholder: + continue + prototype_shape = _template_shape_for_item(layout_states[0], item) + if prototype_shape is None: + raise TemplateStructureError( + f"Placeholder {item.element_id!r} cannot be a slide background" + ) + if item.placeholder == "title": + assigned_idx = item.placeholder_idx + else: + assigned_idx = ( + item.placeholder_idx + if item.placeholder_idx is not None + else placeholder_idx + ) + if ( + assigned_idx is not None + and assigned_idx in assigned_placeholder_indices + ): + raise TemplateStructureError( + f"Layout {layout_key!r} repeats placeholder idx {assigned_idx}" + ) + layout_placeholder = _layout_placeholder_shape( + prototype_shape, + item, + assigned_idx, + theme_font_spec, + ) + _append_shape_to_part(layout_path, layout_placeholder) + for state in layout_states: + shape = _template_shape_for_item(state, item) + if shape is None: + raise TemplateStructureError( + f"{state.spec.svg_path.name}: placeholder {item.element_id!r} " + "did not produce a shape" + ) + _patch_slide_placeholder( + shape, + item, + assigned_idx, + theme_font_spec=theme_font_spec, + ) + if assigned_idx is not None: + assigned_placeholder_indices.add(assigned_idx) + placeholder_idx = max(placeholder_idx, assigned_idx + 1) + placeholder_count += 1 + + _set_template_layout_header_footer(layout_path, prototype.placeholders) + for state in layout_states: + _set_slide_layout_target(state.rels_path, layout_target) + + slide_background_count = _apply_template_slide_backgrounds( + states, + slide_size_emu, + ) + for state in states: + _write_xml_tree(state.slide_path, state.tree) + + if verbose: + print( + " Template structure: " + f"{len(specs_by_layout)} layout(s), " + f"{len(master_items)} master element(s), " + f"{layout_shape_count} layout element(s), " + f"{slide_background_count} slide background(s), " + f"{placeholder_count} placeholder definition(s)" + ) + + +def _apply_preserved_structure( + extract_dir: Path, + specs: list[TemplateSlideSpec], + contract: NativeStructureContract, + conversion_traces: list[dict[str, Any]] | None, + *, + verbose: bool = False, +) -> None: + """Drop preview-only inherited layers and bind content to source placeholders.""" + states = _template_runtime_slides(extract_dir, specs, conversion_traces) + removed_preview_shapes = 0 + removed_preview_backgrounds = 0 + placeholder_count = 0 + for state in states: + removed_background = False + for item in state.spec.elements: + if item.layer not in {"master", "layout"}: + continue + shape = _template_shape_for_item(state, item) + if shape is not None: + _remove_template_shape(state, shape) + removed_preview_shapes += 1 + continue + if removed_background: + raise TemplateStructureError( + f"{state.spec.svg_path.name}: multiple inherited preview " + "backgrounds resolved to one slide background" + ) + common_slide = state.root.find(f"{{{PML_NS}}}cSld") + background = ( + common_slide.find(f"{{{PML_NS}}}bg") + if common_slide is not None + else None + ) + if common_slide is None or background is None: + raise TemplateStructureError( + f"{state.spec.svg_path.name}: inherited preview background " + "did not produce a removable slide background" + ) + common_slide.remove(background) + removed_background = True + removed_preview_backgrounds += 1 + + layout = contract.layout(state.spec.layout_key) + for item, source_placeholder in match_native_placeholders(state.spec, layout): + shape = _template_shape_for_item(state, item) + if shape is None: + raise TemplateStructureError( + f"{state.spec.svg_path.name}: placeholder {item.element_id!r} " + "cannot resolve to a slide background" + ) + _patch_slide_placeholder( + shape, + item, + source_placeholder.idx, + source_placeholder.placeholder_type, + ) + placeholder_count += 1 + _write_xml_tree(state.slide_path, state.tree) + + if verbose: + print( + " Preserved structure: " + f"{len({spec.layout_key for spec in specs})} source layout(s), " + f"{removed_preview_shapes} preview shape(s) removed, " + f"{removed_preview_backgrounds} preview background(s) removed, " + f"{placeholder_count} source placeholder binding(s)" + ) + + +def _promote_common_chrome_shapes_to_masters( + extract_dir: Path, + structure: PptxStructureContext, + slide_count: int, + conversion_traces: list[dict[str, Any]] | None, + *, + verbose: bool = False, +) -> int: + """Promote explicit repeated chrome SVG ids to their shared master.""" + if not conversion_traces: + return 0 + trace_by_slide = { + int(trace.get("slide_num", 0)): trace + for trace in conversion_traces + if trace.get("slide_num") is not None + } + if len(trace_by_slide) < slide_count: + return 0 + + slides_by_master: dict[str, list[int]] = {} + for slide_num in range(1, slide_count + 1): + master_part = structure.slide_master_part(slide_num) + slides_by_master.setdefault(master_part, []).append(slide_num) + + promoted = 0 + promoted_roles = 0 + for master_part, slide_nums in slides_by_master.items(): + if len(slide_nums) < 2: + continue + slide_state: dict[int, dict[str, Any]] = {} + for slide_num in slide_nums: + slide_path = extract_dir / "ppt" / "slides" / f"slide{slide_num}.xml" + rels_path = extract_dir / "ppt" / "slides" / "_rels" / f"slide{slide_num}.xml.rels" + tree = ET.parse(slide_path) + root = tree.getroot() + slide_state[slide_num] = { + "path": slide_path, + "rels": _read_relationships(rels_path), + "root": root, + "shapes": _top_level_shapes_by_id(root), + "timing_shape_ids": _timing_shape_ids(root), + "tokens": _trace_chrome_shape_ids(trace_by_slide.get(slide_num)), + "tree": tree, + } + + # Per token, find the strict-majority identical variant. Slides + # outside every dominant set become cover-layout minority slides. + candidate_sets: dict[str, dict[int, str]] = {} + all_tokens = sorted({ + token + for state in slide_state.values() + for token in state["tokens"] + }) + for token in all_tokens: + carriers: dict[int, str] = {} + canonical_by_slide: dict[int, bytes] = {} + for slide_num in slide_nums: + state = slide_state[slide_num] + shape_ids = state["tokens"].get(token, []) + if len(shape_ids) != 1: + continue + shape_id = shape_ids[0] + shape = state["shapes"].get(shape_id) + if shape is None: + continue + if shape_id in state["timing_shape_ids"]: + continue + if not _shape_relationships_supported(shape, state["rels"]): + continue + carriers[slide_num] = shape_id + canonical_by_slide[slide_num] = _canonical_shape_xml( + shape, + state["rels"], + ) + dominant_xml, dominant_slides = _dominant_variant(canonical_by_slide) + if dominant_xml is None: + continue + if not _is_strict_majority(len(dominant_slides), len(slide_nums)): + continue + candidate_sets[token] = { + slide_num: carriers[slide_num] for slide_num in dominant_slides + } + + if not candidate_sets: + continue + content_slides = sorted(set.intersection( + *(set(slides) for slides in candidate_sets.values()) + )) + if not _is_strict_majority(len(content_slides), len(slide_nums)): + continue + + promotions: list[tuple[str, dict[int, str]]] = [] + claimed_shape_ids: dict[int, set[str]] = { + slide_num: set() for slide_num in content_slides + } + for token in sorted(candidate_sets): + shape_ids_by_slide = { + slide_num: candidate_sets[token][slide_num] + for slide_num in content_slides + } + # A flattened nested chrome group can emit several semantic trace + # ids for the same generated DrawingML shape. Claim it once. + if any( + shape_ids_by_slide[slide_num] in claimed_shape_ids[slide_num] + for slide_num in content_slides + ): + continue + for slide_num, shape_id in shape_ids_by_slide.items(): + claimed_shape_ids[slide_num].add(shape_id) + promotions.append((token, shape_ids_by_slide)) + + if not promotions: + continue + + # Master shapes always render behind slide-local shapes. Preserve the + # original z-order by promoting only a common leading chrome prefix; + # overlay headers/footers remain slide-local. + token_by_shape_id = { + slide_num: { + shape_ids[slide_num]: token + for token, shape_ids in promotions + } + for slide_num in content_slides + } + leading_token_orders: list[list[str]] = [] + for slide_num in content_slides: + order: list[str] = [] + for shape_id in slide_state[slide_num]["shapes"]: + token = token_by_shape_id[slide_num].get(shape_id) + if token is None: + break + order.append(token) + leading_token_orders.append(order) + + safe_tokens = list(leading_token_orders[0]) + for order in leading_token_orders[1:]: + common_length = 0 + for expected, actual in zip(safe_tokens, order): + if expected != actual: + break + common_length += 1 + safe_tokens = safe_tokens[:common_length] + if not safe_tokens: + break + promotion_by_token = {token: shape_ids for token, shape_ids in promotions} + promotions = [ + (token, promotion_by_token[token]) + for token in safe_tokens + ] + + if not promotions: + continue + + master_path = extract_dir / master_part + master_rels_path = _relationships_path_for_part(extract_dir, master_part) + for _token, shape_ids_by_slide in promotions: + first_slide = content_slides[0] + first_state = slide_state[first_slide] + shape = first_state["shapes"][shape_ids_by_slide[first_slide]] + master_shape = _copy_shape_relationships_to_master( + shape, + first_state["rels"], + master_rels_path, + ) + _append_shape_to_master(master_path, master_shape) + promoted_roles += 1 + + for slide_num, shape_id in shape_ids_by_slide.items(): + state = slide_state[slide_num] + shape_to_remove = state["shapes"].get(shape_id) + sp_tree = state["root"].find(f".//{{{PML_NS}}}cSld/{{{PML_NS}}}spTree") + if sp_tree is not None and shape_to_remove is not None: + sp_tree.remove(shape_to_remove) + promoted += 1 + + for slide_num in content_slides: + state = slide_state[slide_num] + _write_xml_tree(state["path"], state["tree"]) + + # Minority slides (covers, section pages) keep every shape + # slide-local and move to a Cover layout that hides the newly + # promoted master chrome, so their rendering never changes. + minority_slides = [ + slide_num for slide_num in slide_nums + if slide_num not in set(content_slides) + ] + if minority_slides: + first_minority_rels = ( + extract_dir / "ppt" / "slides" / "_rels" + / f"slide{minority_slides[0]}.xml.rels" + ) + base_target = _find_relationship_target( + first_minority_rels, SLIDE_LAYOUT_REL_TYPE + ) + if not base_target: + raise RuntimeError( + f"Slide {minority_slides[0]} has no slide layout relationship" + ) + base_layout_part = _resolve_package_target( + f"ppt/slides/slide{minority_slides[0]}.xml", base_target + ) + cover_target = _create_cover_layout( + extract_dir, master_part, base_layout_part + ) + for slide_num in minority_slides: + rels_path = ( + extract_dir / "ppt" / "slides" / "_rels" + / f"slide{slide_num}.xml.rels" + ) + _set_slide_layout_target(rels_path, cover_target) + if verbose: + print( + " Baseline cover layout: " + f"{len(minority_slides)} slide(s) keep slide-local chrome" + ) + + if verbose and promoted: + print( + " Baseline master chrome: " + f"promoted {promoted} slide shape(s) across {promoted_roles} shared object(s)" + ) + return promoted + + +_PAGE_NUMBER_TOKENS = {"pagenumber", "pagenum", "slidenumber"} + + +def _first_slide_number(extract_dir: Path) -> int: + """Read firstSlideNum from presentation.xml (defaults to 1).""" + presentation_path = extract_dir / "ppt" / "presentation.xml" + try: + root = ET.parse(presentation_path).getroot() + except (OSError, ET.ParseError): + return 1 + raw = root.attrib.get("firstSlideNum") + if raw is None: + return 1 + try: + return int(raw) + except ValueError: + return 1 + + +def _shape_with_id(root: ET.Element, shape_id: str) -> ET.Element | None: + """Find a p:sp anywhere in the slide tree by its cNvPr id.""" + for shape in root.iter(f"{{{PML_NS}}}sp"): + cnv = shape.find(f"{{{PML_NS}}}nvSpPr/{{{PML_NS}}}cNvPr") + if cnv is not None and cnv.attrib.get("id") == shape_id: + return shape + return None + + +def _replace_literal_run_with_slidenum_field( + shape: ET.Element, + expected_text: str, + field_guid: str, +) -> bool: + """Swap a single literal page-number run for an a:fld slidenum field.""" + tx_body = shape.find(f"{{{PML_NS}}}txBody") + if tx_body is None: + return False + a_t = f"{{{DML_NS}}}t" + total_text = "".join(t.text or "" for t in tx_body.iter(a_t)) + if total_text.strip() != expected_text: + return False + text_runs = [ + (paragraph, run) + for paragraph in tx_body.iter(f"{{{DML_NS}}}p") + for run in paragraph.findall(f"{{{DML_NS}}}r") + if (run.findtext(a_t) or "").strip() + ] + if len(text_runs) != 1: + return False + paragraph, run = text_runs[0] + if (run.findtext(a_t) or "").strip() != expected_text: + return False + + fld = ET.Element(f"{{{DML_NS}}}fld", {"id": field_guid, "type": "slidenum"}) + r_pr = run.find(f"{{{DML_NS}}}rPr") + if r_pr is not None: + fld.append(ET.fromstring(ET.tostring(r_pr, encoding="utf-8"))) + fld_text = ET.SubElement(fld, a_t) + fld_text.text = expected_text + index = list(paragraph).index(run) + paragraph.remove(run) + paragraph.insert(index, fld) + return True + + +def _convert_page_number_texts_to_fields( + extract_dir: Path, + slide_count: int, + conversion_traces: list[dict[str, Any]] | None, + *, + context: str = "Baseline", + verbose: bool = False, +) -> int: + """Replace literal page-number chrome text with auto-updating fields. + + Only converts when the traced pageNumber/slideNumber shape's whole text + equals the slide's expected display number (honoring firstSlideNum), so + schemes like content-only numbering keep their literal text untouched. + """ + if not conversion_traces: + return 0 + trace_by_slide = { + int(trace.get("slide_num", 0)): trace + for trace in conversion_traces + if trace.get("slide_num") is not None + } + first_slide_number = _first_slide_number(extract_dir) + field_guid = f"{{{str(uuid.uuid4()).upper()}}}" + + converted = 0 + for slide_num in range(1, slide_count + 1): + tokens = _trace_chrome_shape_ids(trace_by_slide.get(slide_num)) + shape_ids = sorted({ + shape_id + for token, ids in tokens.items() + if token in _PAGE_NUMBER_TOKENS + for shape_id in ids + }) + if len(shape_ids) != 1: + continue + slide_path = extract_dir / "ppt" / "slides" / f"slide{slide_num}.xml" + tree = ET.parse(slide_path) + shape = _shape_with_id(tree.getroot(), shape_ids[0]) + if shape is None: + continue + expected_text = str(first_slide_number + slide_num - 1) + if _replace_literal_run_with_slidenum_field(shape, expected_text, field_guid): + _write_xml_tree(slide_path, tree) + converted += 1 + + if verbose and converted: + print( + f" {context} slide-number fields: " + f"converted {converted} page number(s)" + ) + return converted + + +def _remove_relationship(rels_path: Path, rel_id: str) -> None: + """Remove one relationship entry by rId.""" + rels_content = rels_path.read_text(encoding="utf-8") + pattern = re.compile( + rf'[ \t]*]*\bId="{re.escape(rel_id)}"[^>]*/>[ \t]*\n?' + ) + new_content, removed = pattern.subn("", rels_content, count=1) + if not removed: + raise RuntimeError(f"Relationship {rel_id} not found in {rels_path}") + rels_path.write_text(new_content, encoding="utf-8") + + +def _remove_content_type_override(content_types_path: Path, part_name: str) -> None: + """Remove the Override content-type entry for a deleted package part.""" + normalized = "/" + part_name.lstrip("/") + content = content_types_path.read_text(encoding="utf-8") + pattern = re.compile( + rf'[ \t]*]*\bPartName="{re.escape(normalized)}"[^>]*/>[ \t]*\n?' + ) + new_content, removed = pattern.subn("", content, count=1) + if removed: + content_types_path.write_text(new_content, encoding="utf-8") + + +def _prune_unused_slide_layouts( + extract_dir: Path, + structure: PptxStructureContext, + slide_count: int, + *, + verbose: bool = False, +) -> int: + """Remove base-template slide layouts no generated slide references. + + The python-pptx base package ships the full Office layout set; unused + entries only pollute the PowerPoint new-slide picker. Layouts referenced + by any generated slide are always kept, and a master keeps its layout + list untouched unless at least one referenced layout remains in it. + """ + # Read layout references live: earlier baseline passes may have rebound + # minority slides to a Cover layout that the initial structure context + # does not know about. + referenced_layouts: set[str] = set() + for slide_num in range(1, slide_count + 1): + rels_path = ( + extract_dir / "ppt" / "slides" / "_rels" / f"slide{slide_num}.xml.rels" + ) + target = _find_relationship_target(rels_path, SLIDE_LAYOUT_REL_TYPE) + if not target: + raise RuntimeError(f"Slide {slide_num} has no slide layout relationship") + referenced_layouts.add( + _resolve_package_target(f"ppt/slides/slide{slide_num}.xml", target) + ) + + pruned = 0 + content_types_path = extract_dir / "[Content_Types].xml" + for master_part in sorted(set(structure.slide_master_parts.values())): + master_path = extract_dir / master_part + master_rels_path = _relationships_path_for_part(extract_dir, master_part) + layout_rels = { + rel_id: _resolve_package_target(master_part, attrs.get("Target", "")) + for rel_id, attrs in _read_relationships(master_rels_path).items() + if attrs.get("Type") == SLIDE_LAYOUT_REL_TYPE + } + if not any(part in referenced_layouts for part in layout_rels.values()): + continue + + master_xml = master_path.read_text(encoding="utf-8") + for rel_id, layout_part in sorted(layout_rels.items()): + if layout_part in referenced_layouts: + continue + entry_re = re.compile( + rf'[ \t]*]*\br:id="{re.escape(rel_id)}"[^>]*/>[ \t]*\n?' + ) + master_xml, removed = entry_re.subn("", master_xml, count=1) + if not removed: + raise RuntimeError( + f"Slide master {master_part} has no sldLayoutId entry for {rel_id}" + ) + _remove_relationship(master_rels_path, rel_id) + (extract_dir / layout_part).unlink() + layout_rels_path = _relationships_path_for_part(extract_dir, layout_part) + if layout_rels_path.exists(): + layout_rels_path.unlink() + _remove_content_type_override(content_types_path, layout_part) + pruned += 1 + master_path.write_text(master_xml, encoding="utf-8") + + if verbose and pruned: + print(f" Baseline layout prune: removed {pruned} unused base layout(s)") + return pruned + + def _append_relationship( rels_path: Path, rel_type: str, @@ -94,24 +2559,6 @@ def _append_relationship( return next_rid -def _find_relationship_id( - rels_path: Path, - rel_type: str, - target: str, -) -> str | None: - """Find an existing relationship id by type and target.""" - if not rels_path.exists(): - return None - rels_content = rels_path.read_text(encoding='utf-8') - pattern = ( - r']*\bId="([^"]+)"[^>]*' - rf'\bType="{re.escape(rel_type)}"[^>]*' - rf'\bTarget="{re.escape(target)}"[^>]*/>' - ) - match = re.search(pattern, rels_content) - return match.group(1) if match else None - - def _add_default_content_type(content_types: str, extension: str, content_type: str) -> str: """Add a Default content type if it is not already present.""" ext = extension.lstrip(".") @@ -479,9 +2926,93 @@ def _prerender_legacy_pngs( return results -_REL_TARGET_RE = re.compile(r'', re.DOTALL) -_TARGET_ATTR_RE = re.compile(r'Target="([^"]+)"') -_TARGET_MODE_EXT_RE = re.compile(r'TargetMode="External"') +_OPC_UNRESERVED = frozenset( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" +) +_ASCII_LOWER_TRANSLATION = str.maketrans( + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "abcdefghijklmnopqrstuvwxyz", +) + + +def _canonical_opc_part_path(path: str) -> str | None: + """Return an OPC-equivalent package path key, or None when invalid.""" + if ( + not path + or "\\" in path + or path.endswith("/") + or "//" in path + or any(ord(char) <= 0x20 for char in path) + ): + return None + output: list[str] = [] + index = 0 + while index < len(path): + char = path[index] + if char != "%": + output.append(char) + index += 1 + continue + if index + 2 >= len(path) or not re.fullmatch(r"[0-9A-Fa-f]{2}", path[index + 1:index + 3]): + return None + value = int(path[index + 1:index + 3], 16) + decoded = chr(value) + if value in {0, ord("/"), ord("\\")}: + return None + output.append(decoded if decoded in _OPC_UNRESERVED else f"%{value:02X}") + index += 3 + + decoded_path = "".join(output) + if decoded_path.rsplit("/", 1)[-1] in {".", ".."}: + return None + normalized = posixpath.normpath(decoded_path) + if ( + not normalized + or normalized in {".", ".."} + or normalized.startswith("/") + or normalized.startswith("../") + ): + return None + return normalized.translate(_ASCII_LOWER_TRANSLATION) + + +def _source_part_for_rels(rels_path: str) -> str | None: + """Return the source part path represented by a relationship sidecar.""" + filename = posixpath.basename(rels_path) + if filename == ".rels" or not filename.endswith(".rels"): + return None + source_dir = posixpath.dirname(posixpath.dirname(rels_path)) + source_name = filename[:-len(".rels")] + return posixpath.join(source_dir, source_name) if source_dir else source_name + + +def _resolve_internal_opc_target(rels_path: str, target: str) -> str | None: + """Resolve one valid internal OPC Target to its canonical package key.""" + target_path_query = target.split("#", 1)[0] + if ( + "\\" in target + or "?" in target_path_query + or any(ord(char) <= 0x20 for char in target) + ): + return None + try: + parsed = urlsplit(target) + except ValueError: + return None + if parsed.scheme or parsed.netloc or parsed.query: + return None + + source_part = _source_part_for_rels(rels_path) + if parsed.path.startswith("/"): + resolved = parsed.path[1:] + elif parsed.path: + base_dir = posixpath.dirname(source_part) if source_part else "" + resolved = posixpath.join(base_dir, parsed.path) if base_dir else parsed.path + elif source_part and "#" in target: + resolved = source_part + else: + return None + return _canonical_opc_part_path(resolved) def _verify_internal_rels_targets(extract_dir: Path) -> list[str]: @@ -490,25 +3021,34 @@ def _verify_internal_rels_targets(extract_dir: Path) -> list[str]: Each entry is formatted as " -> ". An empty list means every internal Target resolves to a real file in the package. """ + package_parts: set[str] = set() + for path in extract_dir.rglob("*"): + if not path.is_file(): + continue + key = _canonical_opc_part_path(path.relative_to(extract_dir).as_posix()) + if key is not None: + package_parts.add(key) problems: list[str] = [] for rels_path in extract_dir.rglob('*.rels'): rels_rel = rels_path.relative_to(extract_dir).as_posix() - # `_rels/foo.xml.rels` lives one level below its referent's directory; - # Targets resolve relative to the parent of that `_rels` folder. - base_dir = posixpath.dirname(posixpath.dirname(rels_rel)) - content = rels_path.read_text(encoding='utf-8') - for match in _REL_TARGET_RE.finditer(content): - element = match.group(0) - if _TARGET_MODE_EXT_RE.search(element): + try: + root = ET.parse(rels_path).getroot() + except ET.ParseError as exc: + problems.append(f'{rels_rel} -> ') + continue + for elem in root: + attrs = _relationship_attrs(elem) + if attrs.get('TargetMode', '').lower() == 'external': continue - target_match = _TARGET_ATTR_RE.search(element) - if not target_match: + target = attrs.get('Target') + if not target: + problems.append(f'{rels_rel} -> ') continue - target = target_match.group(1) - if target.startswith(('http://', 'https://', 'mailto:')): + resolved = _resolve_internal_opc_target(rels_rel, target) + if resolved is None: + problems.append(f'{rels_rel} -> ') continue - resolved = posixpath.normpath(posixpath.join(base_dir, target)) if base_dir else posixpath.normpath(target) - if not (extract_dir / resolved).exists(): + if resolved not in package_parts: problems.append(f'{rels_rel} -> {resolved}') return problems @@ -609,6 +3149,66 @@ def _stamp_docprops( app_path.write_text(app, encoding='utf-8') +def _create_preserved_base_pptx( + contract: NativeStructureContract, + specs: list[TemplateSlideSpec], + output_path: Path, + slide_size_emu: tuple[int, int], +) -> None: + """Create empty slides bound to the source package's original layouts.""" + presentation = Presentation(str(contract.source_template)) + actual_size = (int(presentation.slide_width), int(presentation.slide_height)) + if actual_size != contract.slide_size_emu: + raise TemplateStructureError( + f"{contract.source_template.name} slide size does not match " + f"{contract.contract_path.name}" + ) + if actual_size != slide_size_emu: + raise TemplateStructureError( + "Generated SVG canvas does not match the preserved source template size" + ) + + layouts_by_part = { + str(layout.part.partname).lstrip("/"): layout + for master in presentation.slide_masters + for layout in master.slide_layouts + } + slide_ids = presentation.slides._sldIdLst + for slide_id in list(slide_ids): + presentation.part.drop_rel(slide_id.rId) + slide_ids.remove(slide_id) + + for spec in specs: + layout_contract = contract.layout(spec.layout_key) + layout = layouts_by_part.get(layout_contract.package_part) + if layout is None: + raise TemplateStructureError( + f"Preserved source package did not load layout part " + f"{layout_contract.package_part!r}" + ) + presentation.slides.add_slide(layout) + presentation.save(str(output_path)) + + +def _clear_preserved_slide_collections(extract_dir: Path) -> None: + """Remove source slide-order metadata that cannot apply to generated pages.""" + presentation_path = extract_dir / "ppt" / "presentation.xml" + tree = ET.parse(presentation_path) + root = tree.getroot() + custom_shows = root.find(f"{{{PML_NS}}}custShowLst") + if custom_shows is not None: + root.remove(custom_shows) + for extension_list in root.findall(f".//{{{PML_NS}}}extLst"): + for extension in list(extension_list): + if any( + child.tag.rsplit("}", 1)[-1] == "sectionLst" + for child in extension.iter() + if isinstance(child.tag, str) + ): + extension_list.remove(extension) + _write_xml_tree(presentation_path, tree) + + def create_pptx_with_native_svg( svg_files: list[Path], output_path: Path, @@ -641,6 +3241,10 @@ def create_pptx_with_native_svg( native_objects: bool = False, conversion_trace_path: Path | None = None, doc_metadata: dict[str, Any] | None = None, + pptx_structure: str = "baseline", + native_structure_contract: NativeStructureContract | None = None, + theme_font_spec: ThemeFontSpec | None = None, + theme_color_spec: ThemeColorSpec | None = None, ) -> bool: """Create a PPTX file with native SVG. @@ -677,6 +3281,21 @@ def create_pptx_with_native_svg( native_objects: Convert explicit ``data-pptx-native`` table/chart markers to native PowerPoint objects. Default off. conversion_trace_path: Optional JSON path for native conversion diagnostics. + pptx_structure: PPTX structure strategy. ``baseline`` promotes safe + shared native backgrounds and leading chrome to slide masters, + then extracts semantic page-role layout families and exact + family-wide structurally marked leading chrome; marker-free legacy + SVGs retain filename/id fallback; + ``template`` consumes explicit SVG master/layout/placeholder + metadata; ``preserve`` reuses an imported source PPTX package; + ``flat`` keeps generated structure slide-local. + native_structure_contract: Validated source package contract for + ``preserve`` mode. + theme_font_spec: Locked project major/minor fonts for baseline/template + theme inheritance. Preserve and flat modes ignore this value. + theme_color_spec: Locked project color scheme for context-aware + baseline/template theme inheritance. Preserve and flat modes + ignore this value. Returns: Whether all slides were successfully created. @@ -688,6 +3307,33 @@ def create_pptx_with_native_svg( # Native shapes mode takes priority over compat mode if use_native_shapes: use_compat_mode = False + if pptx_structure not in {"baseline", "template", "preserve", "flat"}: + raise ValueError(f"Unsupported pptx_structure: {pptx_structure}") + if use_native_shapes and pptx_structure == "template": + template_specs = parse_template_slides(svg_files) + elif use_native_shapes and pptx_structure == "preserve": + if native_structure_contract is None: + raise TemplateStructureError( + "Preserve export requires a validated native structure contract" + ) + template_specs = parse_preserve_slides(svg_files) + for spec in template_specs: + native_structure_contract.layout(spec.layout_key) + else: + template_specs = None + if template_specs is not None and not native_objects: + native_placeholders = sorted({ + item.placeholder + for spec in template_specs + for item in spec.placeholders + if item.placeholder in {"chart", "table"} + }) + if native_placeholders: + kinds = ", ".join(str(kind) for kind in native_placeholders) + raise TemplateStructureError( + f"Template {kinds} placeholder(s) require --native-objects so each " + "marker becomes one native PowerPoint object" + ) # Check compatibility mode dependencies renderer_name, renderer_status, renderer_hint = get_png_renderer_info() @@ -727,6 +3373,7 @@ def create_pptx_with_native_svg( " Native table/chart objects: " f"{'Enabled' if native_objects else 'Disabled'}" ) + print(f" PPTX structure: {pptx_structure}") if image_optimize: if image_sizing == 'display': image_mode = ( @@ -762,22 +3409,51 @@ def create_pptx_with_native_svg( temp_dir = _create_writable_work_dir(output_path) try: - # Create base PPTX with python-pptx - prs = Presentation() - prs.slide_width = width_emu - prs.slide_height = height_emu - - blank_layout = prs.slide_layouts[6] - for _ in svg_files: - prs.slides.add_slide(blank_layout) - base_pptx = temp_dir / 'base.pptx' - prs.save(str(base_pptx)) + if ( + use_native_shapes + and pptx_structure == "preserve" + and native_structure_contract is not None + and template_specs is not None + ): + _create_preserved_base_pptx( + native_structure_contract, + template_specs, + base_pptx, + (width_emu, height_emu), + ) + else: + # Create the standard base PPTX with python-pptx. + prs = Presentation() + prs.slide_width = width_emu + prs.slide_height = height_emu + + blank_layout = prs.slide_layouts[6] + for _ in svg_files: + prs.slides.add_slide(blank_layout) + prs.save(str(base_pptx)) # Extract PPTX extract_dir = temp_dir / 'pptx_content' with zipfile.ZipFile(base_pptx, 'r') as zf: zf.extractall(extract_dir) + if use_native_shapes and pptx_structure == "preserve": + _clear_preserved_slide_collections(extract_dir) + active_theme_font_spec = ( + theme_font_spec + if use_native_shapes and pptx_structure in {"baseline", "template"} + else None + ) + if active_theme_font_spec is not None: + apply_theme_font_spec(extract_dir, active_theme_font_spec) + active_theme_color_spec = ( + theme_color_spec + if use_native_shapes and pptx_structure in {"baseline", "template"} + else None + ) + if active_theme_color_spec is not None: + apply_theme_color_spec(extract_dir, active_theme_color_spec) + structure = _read_slide_layout_targets(extract_dir, len(svg_files)) media_dir = extract_dir / 'ppt' / 'media' media_dir.mkdir(exist_ok=True) @@ -810,6 +3486,11 @@ def create_pptx_with_native_svg( audio_exts_used: set[str] = set() mixed_animation_offset = 0 conversion_trace: list[dict[str, Any]] | None = [] if conversion_trace_path else None + structure_trace: list[dict[str, Any]] | None = ( + [] + if use_native_shapes and pptx_structure in {"baseline", "template", "preserve"} + else None + ) for i, svg_path in enumerate(svg_files, 1): slide_num = i @@ -835,7 +3516,11 @@ def create_pptx_with_native_svg( image_scale=image_scale, image_quality=image_quality, native_objects=native_objects, - trace_out=conversion_trace, + theme_font_spec=active_theme_font_spec, + theme_color_spec=active_theme_color_spec, + trace_out=conversion_trace + if conversion_trace is not None + else structure_trace, ) ) slide_transition, slide_transition_duration, slide_auto_advance = ( @@ -933,6 +3618,14 @@ def create_pptx_with_native_svg( # object converters, e.g. chart XML, chart rels, and # embedded workbooks. for part_name, part_data in package_files_dict.items(): + if ( + part_name.startswith('ppt/charts/') + and part_name.endswith('.xml') + ): + part_data = rewrite_chart_accent_colors( + part_data, + active_theme_color_spec, + ) package_path = extract_dir / part_name package_path.parent.mkdir(parents=True, exist_ok=True) with open(package_path, 'wb') as f: @@ -957,8 +3650,8 @@ def create_pptx_with_native_svg( rels_xml = f''' {extra_rels} + Type="{SLIDE_LAYOUT_REL_TYPE}" + Target="{structure.slide_layout_target(slide_num)}"/>{extra_rels} ''' with open(rels_path, 'w', encoding='utf-8') as f: f.write(rels_xml) @@ -1031,6 +3724,7 @@ def create_pptx_with_native_svg( png_rid=png_rid, png_filename=png_filename, svg_rid=svg_rid, svg_filename=svg_filename, use_compat_mode=(use_compat_mode and slide_has_png), + slide_layout_target=structure.slide_layout_target(slide_num), ) with open(rels_path, 'w', encoding='utf-8') as f: f.write(rels_xml) @@ -1147,6 +3841,104 @@ def create_pptx_with_native_svg( if use_native_shapes: raise + if ( + use_native_shapes + and pptx_structure == "baseline" + and success_count == len(svg_files) + ): + _convert_page_number_texts_to_fields( + extract_dir, + len(svg_files), + conversion_trace if conversion_trace is not None else structure_trace, + verbose=verbose, + ) + _promote_common_slide_backgrounds_to_masters( + extract_dir, + structure, + len(svg_files), + verbose=verbose, + ) + _promote_common_chrome_shapes_to_masters( + extract_dir, + structure, + len(svg_files), + conversion_trace if conversion_trace is not None else structure_trace, + verbose=verbose, + ) + _extract_baseline_layout_families( + extract_dir, + structure, + svg_files, + verbose=verbose, + ) + _promote_common_chrome_shapes_to_layouts( + extract_dir, + len(svg_files), + conversion_trace if conversion_trace is not None else structure_trace, + verbose=verbose, + ) + _prune_unused_slide_layouts( + extract_dir, + structure, + len(svg_files), + verbose=verbose, + ) + + if ( + use_native_shapes + and pptx_structure == "template" + and success_count == len(svg_files) + ): + _convert_page_number_texts_to_fields( + extract_dir, + len(svg_files), + conversion_trace if conversion_trace is not None else structure_trace, + context="Template", + verbose=verbose, + ) + if template_specs is None: + raise TemplateStructureError( + "Template structure metadata was not parsed before export" + ) + _apply_template_structure( + extract_dir, + structure, + template_specs, + conversion_trace if conversion_trace is not None else structure_trace, + active_theme_font_spec, + verbose=verbose, + ) + _prune_unused_slide_layouts( + extract_dir, + structure, + len(svg_files), + verbose=verbose, + ) + + if ( + use_native_shapes + and pptx_structure == "preserve" + and success_count == len(svg_files) + ): + _convert_page_number_texts_to_fields( + extract_dir, + len(svg_files), + conversion_trace if conversion_trace is not None else structure_trace, + context="Preserve", + verbose=verbose, + ) + if template_specs is None or native_structure_contract is None: + raise TemplateStructureError( + "Preserved structure metadata was not parsed before export" + ) + _apply_preserved_structure( + extract_dir, + template_specs, + native_structure_contract, + conversion_trace if conversion_trace is not None else structure_trace, + verbose=verbose, + ) + # Update [Content_Types].xml content_types_path = extract_dir / '[Content_Types].xml' with open(content_types_path, 'r', encoding='utf-8') as f: diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/cli.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/cli.py index 2d08022e..59e92eff 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/cli.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/cli.py @@ -8,6 +8,7 @@ import shutil import argparse from datetime import datetime from pathlib import Path +from xml.etree import ElementTree as ET _SCRIPTS_DIR = Path(__file__).resolve().parents[1] if str(_SCRIPTS_DIR) not in sys.path: @@ -28,8 +29,19 @@ if __package__ in {None, ''}: from .dimensions import CANVAS_FORMATS, get_project_info, get_viewbox_dimensions from .discovery import find_svg_files, find_notes_files from .builder import create_pptx_with_native_svg +from ..drawingml.theme_colors import ThemeColorError, load_theme_color_spec +from ..drawingml.theme_fonts import ThemeFontError, load_theme_font_spec from .narration import NARRATION_EXTENSIONS, find_narration_files, probe_audio_duration from .slide_xml import TRANSITIONS +from .template_structure import ( + TemplateStructureError, + load_native_structure_contract, + load_pptx_structure_lock, + native_structure_lock_errors, + parse_preserve_slides, + parse_template_slides, + template_lock_errors, +) from ..animation_config import load_animation_config, validate_animation_config try: @@ -42,6 +54,23 @@ def _as_dict(value: object) -> dict: return value if isinstance(value, dict) else {} +def _native_object_fallbacks(svg_files: list[Path]) -> list[tuple[str, str, str]]: + """Return fallback-only native object statuses from SVG inputs.""" + fallbacks: list[tuple[str, str, str]] = [] + for svg_path in svg_files: + try: + root = ET.parse(svg_path).getroot() + except (OSError, ET.ParseError): + continue + for elem in root.iter(): + status = elem.get('data-pptx-native-status') + if not status or elem.tag.rsplit('}', 1)[-1] == 'metadata': + continue + marker_id = elem.get('id') or elem.get('data-name') or '' + fallbacks.append((svg_path.name, marker_id, status)) + return fallbacks + + def _recorded_narration_on_click_slides( ref_files: list[Path], animation_config: dict | None, @@ -193,6 +222,22 @@ Recorded narration: 'groups export through their SVG fallback children. When set, ' 'the default-flow export is named __native_charts.pptx ' 'to tell it apart from a plain shape export.') + parser.add_argument( + '--pptx-structure', + choices=['baseline', 'template', 'preserve', 'flat'], + default=None, + help=( + 'PPTX structure strategy for native export. When omitted, read ' + 'spec_lock.md pptx_structure.mode, falling back to baseline. baseline ' + 'promotes safe repeated background/chrome and extracts conservative ' + 'semantic page-role layout families plus exact family-wide ' + 'structurally marked chrome (legacy filenames/ids remain fallbacks); ' + 'template consumes explicit ' + 'data-pptx-layout/layer/placeholder metadata to build reusable layouts; ' + 'preserve is legacy compatibility for imported source packages; ' + 'flat leaves generated structure slide-local for debugging/comparison.' + ), + ) parser.add_argument('--svg-snapshot', action='store_true', default=False, help='Also emit the SVG-rendered snapshot pptx alongside ' 'the native pptx in exports/ (named ' @@ -289,6 +334,47 @@ Recorded narration: if not project_path.exists(): print(f"Error: Path does not exist: {project_path}") return 1 + structure_lock = None + native_structure_contract = None + pptx_structure = args.pptx_structure + if pptx_structure is None: + try: + structure_lock = load_pptx_structure_lock(project_path) + except TemplateStructureError as exc: + print(f"Error: {exc}", file=sys.stderr) + return 1 + pptx_structure = structure_lock.mode if structure_lock else 'baseline' + elif pptx_structure == 'preserve': + try: + structure_lock = load_pptx_structure_lock(project_path) + except TemplateStructureError as exc: + print(f"Error: {exc}", file=sys.stderr) + return 1 + if structure_lock is None or structure_lock.mode != 'preserve': + print( + "Error: --pptx-structure preserve requires a preserve-mode " + "spec_lock.md with source_template/native_structure rows", + file=sys.stderr, + ) + return 1 + if pptx_structure == 'preserve': + if structure_lock is None: + print("Error: preserve mode requires spec_lock.md", file=sys.stderr) + return 1 + try: + native_structure_contract = load_native_structure_contract(structure_lock) + except TemplateStructureError as exc: + print(f"Error: {exc}", file=sys.stderr) + return 1 + theme_font_spec = None + theme_color_spec = None + if pptx_structure in {'baseline', 'template'}: + try: + theme_font_spec = load_theme_font_spec(project_path) + theme_color_spec = load_theme_color_spec(project_path) + except (ThemeFontError, ThemeColorError) as exc: + print(f"Error: {exc}", file=sys.stderr) + return 1 if args.image_max_dimension < 1: print("Error: --image-max-dimension must be >= 1", file=sys.stderr) return 1 @@ -351,6 +437,57 @@ Recorded narration: print("Error: No SVG files found") return 1 + if ( + gen_native + and pptx_structure in {'template', 'preserve'} + and structure_lock is not None + ): + try: + template_specs = ( + parse_preserve_slides(native_files) + if pptx_structure == 'preserve' + else parse_template_slides(native_files) + ) + except TemplateStructureError as exc: + print(f"Error: {exc}", file=sys.stderr) + return 1 + lock_errors = template_lock_errors(template_specs, structure_lock) + if lock_errors: + print("Error: PPTX structure does not match spec_lock.md:", file=sys.stderr) + for message in lock_errors: + print(f" {message}", file=sys.stderr) + return 1 + if pptx_structure == 'preserve': + if native_structure_contract is None: + print("Error: preserve mode contract is unavailable", file=sys.stderr) + return 1 + preserve_errors = native_structure_lock_errors( + template_specs, + structure_lock, + native_structure_contract, + ) + if preserve_errors: + print( + "Error: PPTX structure does not match native_structure.json:", + file=sys.stderr, + ) + for message in preserve_errors: + print(f" {message}", file=sys.stderr) + return 1 + + if args.native_objects: + fallbacks = _native_object_fallbacks(native_files) + if fallbacks: + print( + "Warning: --native-objects found fallback-only PPTX objects; " + "they will export through their SVG preview instead of editable objects.", + file=sys.stderr, + ) + for filename, marker_id, status in fallbacks[:20]: + print(f" {filename}: {marker_id} ({status})", file=sys.stderr) + if len(fallbacks) > 20: + print(f" ... and {len(fallbacks) - 20} more", file=sys.stderr) + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") backup_dir: Path | None = None @@ -618,6 +755,10 @@ Recorded narration: image_scale=args.image_scale, image_quality=args.image_quality, native_objects=args.native_objects, + pptx_structure=pptx_structure, + native_structure_contract=native_structure_contract, + theme_font_spec=theme_font_spec, + theme_color_spec=theme_color_spec, ) success = True @@ -632,16 +773,20 @@ Recorded narration: print(f" Output file: {native_path}") print() - ok = create_pptx_with_native_svg( - output_path=native_path, - use_native_shapes=True, - svg_files=native_files, - conversion_trace_path=( - native_path.with_name(native_path.name + '.trace.json') - if args.conversion_trace else None - ), - **shared_kwargs, - ) + try: + ok = create_pptx_with_native_svg( + output_path=native_path, + use_native_shapes=True, + svg_files=native_files, + conversion_trace_path=( + native_path.with_name(native_path.name + '.trace.json') + if args.conversion_trace else None + ), + **shared_kwargs, + ) + except TemplateStructureError as exc: + print(f"Error: {exc}", file=sys.stderr) + return 1 success = success and ok # --- SVG image reference version --- @@ -682,6 +827,26 @@ Recorded narration: print(f" [warn] svg_output backup skipped: {exc}") elif verbose: print(f" [info] svg_output/ not found, backup skipped") + if pptx_structure == 'preserve' and structure_lock is not None: + try: + preserve_sources = [ + project_path / 'spec_lock.md', + structure_lock.source_template, + structure_lock.native_structure, + ] + for source in preserve_sources: + if source is None or not source.is_file(): + continue + source_path = source.resolve() + relative = source_path.relative_to(project_path.resolve()) + destination = backup_dir / relative + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source_path, destination) + if verbose: + print(f" preserve contract backup: {backup_dir}") + except (OSError, ValueError) as exc: + if verbose: + print(f" [warn] preserve contract backup skipped: {exc}") if success and cache_dir is not None and cache_dir.is_dir() and not args.keep_cache: try: diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/notes.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/notes.py index 4533e3ee..b5c9cf6f 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/notes.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/notes.py @@ -114,7 +114,7 @@ def create_notes_slide_xml(slide_num: int, notes_text: str) -> str: - + @@ -126,7 +126,7 @@ def create_notes_slide_xml(slide_num: int, notes_text: str) -> str: - + @@ -187,7 +187,13 @@ def create_notes_master_xml() -> str: - + + + + + + + @@ -196,7 +202,13 @@ def create_notes_master_xml() -> str: - + + + + + + + @@ -205,7 +217,13 @@ def create_notes_master_xml() -> str: - + + + + + + + @@ -213,7 +231,13 @@ def create_notes_master_xml() -> str: - + + + + + + + @@ -222,7 +246,13 @@ def create_notes_master_xml() -> str: - + + + + + + + @@ -231,7 +261,13 @@ def create_notes_master_xml() -> str: - + + + + + + + diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/slide_xml.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/slide_xml.py index da326225..04835879 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/slide_xml.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/slide_xml.py @@ -111,6 +111,7 @@ def create_slide_rels_xml( svg_rid: str, svg_filename: str, use_compat_mode: bool = True, + slide_layout_target: str = "../slideLayouts/slideLayout1.xml", ) -> str: """Create slide relationship file XML. @@ -120,17 +121,18 @@ def create_slide_rels_xml( svg_rid: SVG relationship ID. svg_filename: SVG filename. use_compat_mode: Whether to use compatibility mode. + slide_layout_target: Target for the slide's actual layout part. """ if use_compat_mode: return f''' - + ''' else: return f''' - + ''' diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/template_structure.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/template_structure.py new file mode 100644 index 00000000..4d8fdeac --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/template_structure.py @@ -0,0 +1,1132 @@ +#!/usr/bin/env python3 +""" +PPT Master - Template Structure Metadata + +Parse and validate explicit SVG metadata consumed by template-mode PPTX export. + +Usage: + Imported by svg_to_pptx.pptx_package.builder and svg_quality_checker.py. + +Examples: + parse_template_slides([Path("projects/demo/svg_output/01_cover.svg")]) + +Dependencies: + None (only uses standard library) +""" + +from __future__ import annotations + +import hashlib +import json +import math +import re +import zipfile +from dataclasses import dataclass +from pathlib import Path +from typing import Any +from xml.etree import ElementTree as ET + + +_NON_VISUAL_TAGS = frozenset({"defs", "title", "desc", "metadata", "style"}) +_STRUCTURE_ATTRS = frozenset({ + "data-pptx-layer", + "data-pptx-layout", + "data-pptx-layout-name", + "data-pptx-placeholder", + "data-pptx-placeholder-bounds", + "data-pptx-placeholder-idx", + "data-pptx-editable", +}) +_LAYERS = frozenset({"master", "layout", "slide"}) +_PLACEHOLDERS = frozenset({ + "title", + "subtitle", + "body", + "picture", + "chart", + "table", + "object", + "media", + "date", + "footer", + "slide-number", +}) +_TEXT_PLACEHOLDERS = frozenset({ + "title", + "subtitle", + "body", + "date", + "footer", + "slide-number", +}) +_OBJECT_PLACEHOLDER_TAGS = frozenset({ + "rect", + "circle", + "ellipse", + "line", + "path", + "polygon", + "polyline", + "text", + "image", +}) +_LAYOUT_KEY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$") +_LOCK_ROW_RE = re.compile(r"^-\s+([A-Za-z0-9_]+)\s*:\s*(.+?)\s*$") +_LOCK_PAGE_RE = re.compile(r"^P(\d+)$") +PPTX_STRUCTURE_MODES = frozenset({"baseline", "template", "preserve", "flat"}) +TEMPLATE_ADHERENCE_MODES = frozenset({"strict", "adaptive"}) +NATIVE_STRUCTURE_SCHEMA = "ppt-master.native-structure.v1" + + +class TemplateStructureError(RuntimeError): + """Reject invalid or ambiguous template-structure metadata.""" + + +@dataclass(frozen=True) +class PptxLayoutReference: + """One spec_lock page-to-PowerPoint-layout declaration.""" + + slide_num: int + layout_key: str + layout_name: str | None = None + + +@dataclass(frozen=True) +class PptxStructureLock: + """Optional project-level PPTX structure export policy.""" + + mode: str + template_adherence: str | None = None + layouts: tuple[PptxLayoutReference, ...] = () + source_template: Path | None = None + native_structure: Path | None = None + + +@dataclass(frozen=True) +class NativePlaceholderSpec: + """One placeholder exposed by a preserved source layout.""" + + semantic_role: str + placeholder_type: str + idx: int | None + geometry: tuple[float, float, float, float] | None = None + + +@dataclass(frozen=True) +class NativeLayoutSpec: + """One named layout retained from the source PPTX package.""" + + key: str + name: str + package_part: str + master_key: str + placeholders: tuple[NativePlaceholderSpec, ...] = () + + +@dataclass(frozen=True) +class NativeStructureContract: + """Validated portable contract for a preserved source PPTX package.""" + + source_template: Path + contract_path: Path + source_sha256: str + slide_size_emu: tuple[int, int] + layouts: tuple[NativeLayoutSpec, ...] + + def layout(self, key: str) -> NativeLayoutSpec: + for layout in self.layouts: + if layout.key == key: + return layout + raise TemplateStructureError( + f"native_structure.json has no layout key {key!r}" + ) + + +@dataclass(frozen=True) +class TemplateElementSpec: + """One direct SVG child carrying explicit PPTX structure metadata.""" + + element_id: str + order: int + tag: str + layer: str | None = None + placeholder: str | None = None + placeholder_bounds: tuple[float, float, float, float] | None = None + placeholder_idx: int | None = None + is_background: bool = False + + def contract_signature(self) -> tuple[object, ...]: + """Return metadata that must agree across slides sharing a structure.""" + return ( + self.element_id, + self.tag, + self.layer, + self.placeholder, + self.placeholder_bounds, + self.placeholder_idx, + self.is_background, + ) + + +@dataclass(frozen=True) +class TemplateSlideSpec: + """Explicit structure contract parsed from one SVG slide.""" + + slide_num: int + svg_path: Path + layout_key: str + layout_name: str + elements: tuple[TemplateElementSpec, ...] + + @property + def master_elements(self) -> tuple[TemplateElementSpec, ...]: + return tuple(item for item in self.elements if item.layer == "master") + + @property + def layout_elements(self) -> tuple[TemplateElementSpec, ...]: + return tuple(item for item in self.elements if item.layer == "layout") + + @property + def placeholders(self) -> tuple[TemplateElementSpec, ...]: + return tuple(item for item in self.elements if item.placeholder) + + @property + def layout_contract(self) -> tuple[tuple[object, ...], ...]: + return tuple( + item.contract_signature() + for item in self.elements + if item.layer == "layout" or item.placeholder + ) + + +def _local_tag(elem: ET.Element) -> str: + return elem.tag.rsplit("}", 1)[-1] if isinstance(elem.tag, str) else "" + + +def _svg_canvas(root: ET.Element) -> tuple[float, float, float, float]: + raw_viewbox = (root.get("viewBox") or "").strip() + values = [part for part in re.split(r"[\s,]+", raw_viewbox) if part] + if len(values) != 4: + return 0.0, 0.0, 0.0, 0.0 + try: + x, y, width, height = (float(value) for value in values) + except ValueError: + return 0.0, 0.0, 0.0, 0.0 + if not all(math.isfinite(value) for value in (x, y, width, height)): + return 0.0, 0.0, 0.0, 0.0 + return x, y, width, height + + +def _is_full_canvas_solid_rect( + elem: ET.Element, + canvas: tuple[float, float, float, float], +) -> bool: + """Return whether a direct rect is eligible for scoped p:bg compilation.""" + if canvas[2] <= 0 or canvas[3] <= 0: + return False + if _local_tag(elem) != "rect": + return False + if any(elem.get(attr) for attr in ("transform", "filter", "clip-path")): + return False + try: + geometry = ( + float(elem.get("x", "0")), + float(elem.get("y", "0")), + float(elem.get("width", "0")), + float(elem.get("height", "0")), + ) + corner_radius = ( + float(elem.get("rx", "0")), + float(elem.get("ry", "0")), + ) + except ValueError: + return False + if not all(math.isfinite(value) for value in (*geometry, *corner_radius)): + return False + if corner_radius != (0.0, 0.0): + return False + if any(abs(actual - expected) > 0.5 for actual, expected in zip(geometry, canvas)): + return False + fill = (elem.get("fill") or "").strip().lower() + if not fill or fill == "none" or fill.startswith("url("): + return False + stroke = (elem.get("stroke") or "none").strip().lower() + if stroke != "none": + try: + if float(elem.get("stroke-opacity", "1")) != 0: + return False + except ValueError: + return False + return True + + +def _portable_project_file( + project_path: Path, + raw_value: str, + field_name: str, + suffix: str, +) -> Path: + """Resolve a project-relative structure file without allowing escape.""" + value = raw_value.strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}: + value = value[1:-1].strip() + if not value: + raise TemplateStructureError( + f"spec_lock.md pptx_structure.{field_name} cannot be empty" + ) + candidate = Path(value) + if candidate.is_absolute(): + raise TemplateStructureError( + f"spec_lock.md pptx_structure.{field_name} must be project-relative" + ) + root = project_path.resolve() + resolved = (root / candidate).resolve() + try: + resolved.relative_to(root) + except ValueError as exc: + raise TemplateStructureError( + f"spec_lock.md pptx_structure.{field_name} escapes the project directory" + ) from exc + if resolved.suffix.lower() != suffix: + raise TemplateStructureError( + f"spec_lock.md pptx_structure.{field_name} must reference a {suffix} file" + ) + if not resolved.is_file(): + raise TemplateStructureError( + f"spec_lock.md pptx_structure.{field_name} does not exist: {candidate}" + ) + return resolved + + +def load_pptx_structure_lock(project_path: Path) -> PptxStructureLock | None: + """Load optional pptx_structure/pptx_layouts sections from spec_lock.md.""" + lock_path = project_path / "spec_lock.md" + if not lock_path.is_file(): + return None + try: + lines = lock_path.read_text(encoding="utf-8").splitlines() + except OSError as exc: + raise TemplateStructureError(f"Cannot read {lock_path}: {exc}") from exc + + sections: dict[str, list[tuple[str, str]]] = {} + current_section: str | None = None + for raw_line in lines: + line = raw_line.strip() + if line.startswith("## "): + current_section = line[3:].strip() + sections.setdefault(current_section, []) + continue + if current_section not in {"pptx_structure", "pptx_layouts"}: + continue + match = _LOCK_ROW_RE.fullmatch(line) + if match: + sections[current_section].append((match.group(1), match.group(2))) + + structure_rows = sections.get("pptx_structure", []) + layout_rows = sections.get("pptx_layouts", []) + if not structure_rows and not layout_rows: + return None + mode_rows = [value.strip().lower() for key, value in structure_rows if key == "mode"] + if len(mode_rows) != 1: + raise TemplateStructureError( + "spec_lock.md pptx_structure requires exactly one '- mode:' row" + ) + mode = mode_rows[0] + if mode not in PPTX_STRUCTURE_MODES: + allowed = ", ".join(sorted(PPTX_STRUCTURE_MODES)) + raise TemplateStructureError( + f"spec_lock.md pptx_structure.mode must be one of: {allowed}" + ) + + adherence_rows = [ + value.strip().lower() + for key, value in structure_rows + if key == "template_adherence" + ] + if len(adherence_rows) > 1: + raise TemplateStructureError( + "spec_lock.md pptx_structure allows at most one " + "'- template_adherence:' row" + ) + template_adherence = adherence_rows[0] if adherence_rows else None + if template_adherence and template_adherence not in TEMPLATE_ADHERENCE_MODES: + allowed = ", ".join(sorted(TEMPLATE_ADHERENCE_MODES)) + raise TemplateStructureError( + "spec_lock.md pptx_structure.template_adherence must be one of: " + f"{allowed}" + ) + if mode == "preserve" and template_adherence == "adaptive": + raise TemplateStructureError( + "spec_lock.md preserve mode requires template_adherence: strict; " + "adaptive template use must export through template mode" + ) + + source_rows = [ + value for key, value in structure_rows if key == "source_template" + ] + contract_rows = [ + value for key, value in structure_rows if key == "native_structure" + ] + source_template = None + native_structure = None + if mode == "preserve": + if len(source_rows) != 1 or len(contract_rows) != 1: + raise TemplateStructureError( + "spec_lock.md preserve mode requires exactly one '- source_template:' " + "row and one '- native_structure:' row" + ) + source_template = _portable_project_file( + project_path, + source_rows[0], + "source_template", + ".pptx", + ) + native_structure = _portable_project_file( + project_path, + contract_rows[0], + "native_structure", + ".json", + ) + elif source_rows or contract_rows: + raise TemplateStructureError( + "spec_lock.md source_template/native_structure rows are allowed only " + "when pptx_structure.mode is preserve" + ) + + references: list[PptxLayoutReference] = [] + seen_slides: set[int] = set() + for page_key, raw_value in layout_rows: + page_match = _LOCK_PAGE_RE.fullmatch(page_key) + if not page_match or int(page_match.group(1)) <= 0: + raise TemplateStructureError( + f"spec_lock.md pptx_layouts key {page_key!r} must be P" + ) + slide_num = int(page_match.group(1)) + if slide_num in seen_slides: + raise TemplateStructureError( + f"spec_lock.md pptx_layouts repeats page P{slide_num:02d}" + ) + seen_slides.add(slide_num) + value_parts = raw_value.split("|", 1) + layout_key = value_parts[0].strip() + layout_name = value_parts[1].strip() if len(value_parts) == 2 else None + if not _LAYOUT_KEY_RE.fullmatch(layout_key): + raise TemplateStructureError( + f"spec_lock.md P{slide_num:02d} has invalid layout key " + f"{layout_key!r}" + ) + if len(value_parts) == 2 and not layout_name: + raise TemplateStructureError( + f"spec_lock.md P{slide_num:02d} has an empty layout name" + ) + references.append(PptxLayoutReference( + slide_num=slide_num, + layout_key=layout_key, + layout_name=layout_name, + )) + + if mode in {"template", "preserve"} and not references: + raise TemplateStructureError( + f"spec_lock.md {mode} mode requires one pptx_layouts row per page" + ) + if mode not in {"template", "preserve"} and references: + raise TemplateStructureError( + "spec_lock.md pptx_layouts is allowed only when pptx_structure.mode " + "is template or preserve" + ) + return PptxStructureLock( + mode=mode, + template_adherence=template_adherence, + layouts=tuple(sorted(references, key=lambda item: item.slide_num)), + source_template=source_template, + native_structure=native_structure, + ) + + +def _file_sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _native_geometry(raw: Any, context: str) -> tuple[float, float, float, float] | None: + if raw is None: + return None + if not isinstance(raw, dict): + raise TemplateStructureError(f"{context} geometry must be an object or null") + try: + values = tuple(float(raw[key]) for key in ("x", "y", "width", "height")) + except (KeyError, TypeError, ValueError) as exc: + raise TemplateStructureError(f"{context} geometry is invalid") from exc + if not all(math.isfinite(value) for value in values) or values[2] <= 0 or values[3] <= 0: + raise TemplateStructureError(f"{context} geometry must be finite and positive") + return values + + +def load_native_structure_contract( + structure_lock: PptxStructureLock, +) -> NativeStructureContract: + """Load and verify the native structure bundle selected by preserve mode.""" + if structure_lock.mode != "preserve": + raise TemplateStructureError( + "native structure contracts are available only in preserve mode" + ) + source_template = structure_lock.source_template + contract_path = structure_lock.native_structure + if source_template is None or contract_path is None: + raise TemplateStructureError( + "preserve mode is missing source_template or native_structure" + ) + try: + raw = json.loads(contract_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise TemplateStructureError( + f"Cannot read native structure contract {contract_path}: {exc}" + ) from exc + if not isinstance(raw, dict) or raw.get("schema") != NATIVE_STRUCTURE_SCHEMA: + raise TemplateStructureError( + f"{contract_path.name} must use schema {NATIVE_STRUCTURE_SCHEMA!r}" + ) + + source = raw.get("source") + expected_sha = source.get("sha256") if isinstance(source, dict) else None + if not isinstance(expected_sha, str) or not re.fullmatch(r"[0-9a-f]{64}", expected_sha): + raise TemplateStructureError( + f"{contract_path.name} source.sha256 must be a lowercase SHA-256 digest" + ) + actual_sha = _file_sha256(source_template) + if actual_sha != expected_sha: + raise TemplateStructureError( + f"{source_template.name} does not match {contract_path.name} source.sha256" + ) + + slide_size = raw.get("slideSize") + try: + slide_size_emu = ( + int(slide_size["width_emu"]), + int(slide_size["height_emu"]), + ) + except (KeyError, TypeError, ValueError) as exc: + raise TemplateStructureError( + f"{contract_path.name} slideSize must contain width_emu/height_emu" + ) from exc + if slide_size_emu[0] <= 0 or slide_size_emu[1] <= 0: + raise TemplateStructureError( + f"{contract_path.name} slideSize values must be positive" + ) + + raw_layouts = raw.get("layouts") + if not isinstance(raw_layouts, list) or not raw_layouts: + raise TemplateStructureError( + f"{contract_path.name} must contain at least one layout" + ) + layouts: list[NativeLayoutSpec] = [] + seen_keys: set[str] = set() + seen_parts: set[str] = set() + for index, item in enumerate(raw_layouts, start=1): + context = f"{contract_path.name} layouts[{index}]" + if not isinstance(item, dict): + raise TemplateStructureError(f"{context} must be an object") + key = str(item.get("key") or "") + name = str(item.get("name") or "").strip() + package_part = str(item.get("packagePart") or "") + master_key = str(item.get("masterKey") or "") + if not _LAYOUT_KEY_RE.fullmatch(key): + raise TemplateStructureError(f"{context} has invalid key {key!r}") + if key in seen_keys: + raise TemplateStructureError(f"{context} repeats layout key {key!r}") + if not name: + raise TemplateStructureError(f"{context} name cannot be empty") + if ( + not package_part.startswith("ppt/slideLayouts/") + or ".." in Path(package_part).parts + or not package_part.endswith(".xml") + ): + raise TemplateStructureError( + f"{context} packagePart must be a ppt/slideLayouts/*.xml part" + ) + if package_part in seen_parts: + raise TemplateStructureError( + f"{context} repeats package part {package_part!r}" + ) + if not master_key: + raise TemplateStructureError(f"{context} masterKey cannot be empty") + + raw_placeholders = item.get("placeholders", []) + if not isinstance(raw_placeholders, list): + raise TemplateStructureError(f"{context} placeholders must be a list") + placeholders: list[NativePlaceholderSpec] = [] + for ph_index, placeholder in enumerate(raw_placeholders, start=1): + ph_context = f"{context} placeholders[{ph_index}]" + if not isinstance(placeholder, dict): + raise TemplateStructureError(f"{ph_context} must be an object") + semantic_role = str(placeholder.get("semanticRole") or "other") + placeholder_type = str(placeholder.get("type") or "obj") + raw_idx = placeholder.get("idx") + try: + placeholder_idx = int(raw_idx) if raw_idx is not None else None + except (TypeError, ValueError) as exc: + raise TemplateStructureError( + f"{ph_context} idx must be an integer or null" + ) from exc + if placeholder_idx is not None and placeholder_idx < 0: + raise TemplateStructureError(f"{ph_context} idx cannot be negative") + placeholders.append(NativePlaceholderSpec( + semantic_role=semantic_role, + placeholder_type=placeholder_type, + idx=placeholder_idx, + geometry=_native_geometry(placeholder.get("geometry"), ph_context), + )) + layouts.append(NativeLayoutSpec( + key=key, + name=name, + package_part=package_part, + master_key=master_key, + placeholders=tuple(placeholders), + )) + seen_keys.add(key) + seen_parts.add(package_part) + + try: + with zipfile.ZipFile(source_template, "r") as package: + package_parts = set(package.namelist()) + except (OSError, zipfile.BadZipFile) as exc: + raise TemplateStructureError( + f"Cannot open preserved source template {source_template}: {exc}" + ) from exc + missing_parts = sorted(seen_parts - package_parts) + if missing_parts: + raise TemplateStructureError( + f"{source_template.name} is missing layout part(s): " + ", ".join(missing_parts) + ) + + return NativeStructureContract( + source_template=source_template, + contract_path=contract_path, + source_sha256=expected_sha, + slide_size_emu=slide_size_emu, + layouts=tuple(layouts), + ) + + +def _parse_placeholder_bounds( + raw: str | None, + *, + svg_path: Path, + element_id: str, +) -> tuple[float, float, float, float] | None: + if raw is None: + return None + parts = [part for part in re.split(r"[\s,]+", raw.strip()) if part] + if len(parts) != 4: + raise TemplateStructureError( + f"{svg_path.name}: {element_id} data-pptx-placeholder-bounds must be " + "'x y width height'" + ) + try: + x, y, width, height = (float(part) for part in parts) + except ValueError as exc: + raise TemplateStructureError( + f"{svg_path.name}: {element_id} placeholder bounds must be numeric" + ) from exc + if not all(math.isfinite(value) for value in (x, y, width, height)): + raise TemplateStructureError( + f"{svg_path.name}: {element_id} placeholder bounds must be finite" + ) + if width <= 0 or height <= 0: + raise TemplateStructureError( + f"{svg_path.name}: {element_id} placeholder width/height must be positive" + ) + return x, y, width, height + + +def _parse_placeholder_idx( + raw: str | None, + *, + svg_path: Path, + element_id: str, +) -> int | None: + if raw is None: + return None + value = raw.strip() + if not value or not value.isdigit(): + raise TemplateStructureError( + f"{svg_path.name}: {element_id} data-pptx-placeholder-idx must be " + "a non-negative integer" + ) + return int(value) + + +def _validate_placeholder_element( + elem: ET.Element, + placeholder: str, + *, + svg_path: Path, + element_id: str, +) -> None: + tag = _local_tag(elem) + if placeholder in _TEXT_PLACEHOLDERS and tag != "text": + raise TemplateStructureError( + f"{svg_path.name}: {element_id} placeholder '{placeholder}' must be " + "declared on a direct element" + ) + if placeholder == "picture" and tag not in {"image", "svg"}: + raise TemplateStructureError( + f"{svg_path.name}: {element_id} picture placeholder must be declared " + "on a direct or nested crop element" + ) + if placeholder == "media" and tag not in {"image", "svg"}: + raise TemplateStructureError( + f"{svg_path.name}: {element_id} media placeholder must be declared " + "on a direct or nested crop element" + ) + if placeholder == "object" and tag not in _OBJECT_PLACEHOLDER_TAGS: + raise TemplateStructureError( + f"{svg_path.name}: {element_id} object placeholder must resolve to " + "one direct text, image, or basic SVG shape" + ) + if placeholder in {"chart", "table"}: + native_kind = (elem.get("data-pptx-native") or "").strip().lower() + if tag != "g" or native_kind != placeholder: + raise TemplateStructureError( + f"{svg_path.name}: {element_id} placeholder '{placeholder}' must be " + f"a direct marker" + ) + + +def parse_template_slide(svg_path: Path, slide_num: int) -> TemplateSlideSpec: + """Parse one SVG's explicit template layout and structure elements.""" + try: + root = ET.parse(svg_path).getroot() + except (OSError, ET.ParseError) as exc: + raise TemplateStructureError( + f"{svg_path.name}: unable to parse SVG structure metadata: {exc}" + ) from exc + + if _local_tag(root) != "svg": + raise TemplateStructureError(f"{svg_path.name}: root element must be ") + + layout_key = (root.get("data-pptx-layout") or "").strip() + if not layout_key: + raise TemplateStructureError( + f"{svg_path.name}: template export requires root data-pptx-layout" + ) + if not _LAYOUT_KEY_RE.fullmatch(layout_key): + raise TemplateStructureError( + f"{svg_path.name}: invalid data-pptx-layout {layout_key!r}; use 1-64 " + "ASCII letters, digits, dots, underscores, or hyphens" + ) + layout_name = (root.get("data-pptx-layout-name") or "").strip() + if not layout_name: + layout_name = re.sub(r"[-_.]+", " ", layout_key).strip().title() or layout_key + + illegal_root_attrs = sorted( + attr for attr in _STRUCTURE_ATTRS + if ( + attr not in {"data-pptx-layout", "data-pptx-layout-name"} + and root.get(attr) is not None + ) + ) + if illegal_root_attrs: + raise TemplateStructureError( + f"{svg_path.name}: root cannot use {', '.join(illegal_root_attrs)}" + ) + + id_counts: dict[str, int] = {} + for elem in root.iter(): + element_id = elem.get("id") + if element_id: + id_counts[element_id] = id_counts.get(element_id, 0) + 1 + duplicate_ids = sorted(element_id for element_id, count in id_counts.items() if count > 1) + if duplicate_ids: + raise TemplateStructureError( + f"{svg_path.name}: duplicate SVG id(s) are not allowed in template mode: " + + ", ".join(duplicate_ids) + ) + + direct_children = set(root) + for elem in root.iter(): + if elem is root or elem in direct_children: + continue + nested_attrs = sorted(attr for attr in _STRUCTURE_ATTRS if elem.get(attr) is not None) + if nested_attrs: + element_id = elem.get("id") or _local_tag(elem) or "" + raise TemplateStructureError( + f"{svg_path.name}: {element_id} uses template metadata below the SVG " + "root; structure metadata is allowed only on direct children" + ) + + elements: list[TemplateElementSpec] = [] + canvas = _svg_canvas(root) + last_order_rank = -1 + visual_order = 0 + for elem in root: + tag = _local_tag(elem) + if tag in _NON_VISUAL_TAGS: + continue + + element_id = (elem.get("id") or "").strip() + layer_raw = elem.get("data-pptx-layer") + layer = (layer_raw or "").strip().lower() or None + placeholder_raw = elem.get("data-pptx-placeholder") + placeholder = ( + (placeholder_raw or "").strip().lower() or None + ) + bounds_raw = elem.get("data-pptx-placeholder-bounds") + placeholder_idx_raw = elem.get("data-pptx-placeholder-idx") + editable_raw = elem.get("data-pptx-editable") + is_background = _is_full_canvas_solid_rect(elem, canvas) + effective_layer = layer or ("slide" if is_background else None) + + if ( + elem.get("data-pptx-layout") is not None + or elem.get("data-pptx-layout-name") is not None + ): + raise TemplateStructureError( + f"{svg_path.name}: data-pptx-layout and data-pptx-layout-name belong " + "on the root only" + ) + if layer and layer not in _LAYERS: + raise TemplateStructureError( + f"{svg_path.name}: {element_id or tag} has unsupported " + f"data-pptx-layer={layer!r}" + ) + if layer_raw is not None and layer is None: + raise TemplateStructureError( + f"{svg_path.name}: {element_id or tag} has empty data-pptx-layer" + ) + if placeholder and placeholder not in _PLACEHOLDERS: + raise TemplateStructureError( + f"{svg_path.name}: {element_id or tag} has unsupported " + f"data-pptx-placeholder={placeholder!r}" + ) + if placeholder_raw is not None and placeholder is None: + raise TemplateStructureError( + f"{svg_path.name}: {element_id or tag} has empty " + "data-pptx-placeholder" + ) + if effective_layer and placeholder: + raise TemplateStructureError( + f"{svg_path.name}: {element_id or tag} cannot be both a static " + "structure/background layer and a content placeholder" + ) + if layer == "slide" and not is_background: + raise TemplateStructureError( + f"{svg_path.name}: data-pptx-layer='slide' is allowed only on a " + "direct full-canvas solid background rect" + ) + if bounds_raw is not None and not placeholder: + raise TemplateStructureError( + f"{svg_path.name}: {element_id or tag} has placeholder bounds without " + "data-pptx-placeholder" + ) + if placeholder_idx_raw is not None and not placeholder: + raise TemplateStructureError( + f"{svg_path.name}: {element_id or tag} has placeholder idx without " + "data-pptx-placeholder" + ) + if (effective_layer or placeholder) and not element_id: + raise TemplateStructureError( + f"{svg_path.name}: direct <{tag}> with template metadata requires an id" + ) + if editable_raw is not None: + if not effective_layer or editable_raw.strip().lower() != "false": + raise TemplateStructureError( + f"{svg_path.name}: data-pptx-editable currently supports only " + "'false' on master/layout elements or slide backgrounds" + ) + + if is_background: + order_rank = {"master": 0, "layout": 1, "slide": 2}[effective_layer] + elif effective_layer == "master": + order_rank = 3 + elif effective_layer == "layout": + order_rank = 4 + else: + order_rank = 5 + if order_rank < last_order_rank: + raise TemplateStructureError( + f"{svg_path.name}: {element_id or tag} violates template paint order; " + "use Master background, Layout background, Slide background, " + "Master shapes, Layout shapes, then Slide content/placeholders" + ) + last_order_rank = order_rank + + if placeholder: + _validate_placeholder_element( + elem, + placeholder, + svg_path=svg_path, + element_id=element_id, + ) + placeholder_bounds = _parse_placeholder_bounds( + bounds_raw, + svg_path=svg_path, + element_id=element_id or tag, + ) + placeholder_idx = _parse_placeholder_idx( + placeholder_idx_raw, + svg_path=svg_path, + element_id=element_id or tag, + ) + + if effective_layer or placeholder: + elements.append(TemplateElementSpec( + element_id=element_id, + order=visual_order, + tag=tag, + layer=effective_layer, + placeholder=placeholder, + placeholder_bounds=placeholder_bounds, + placeholder_idx=placeholder_idx, + is_background=is_background, + )) + visual_order += 1 + + for scope in ("master", "layout", "slide"): + backgrounds = [ + item for item in elements + if item.layer == scope and item.is_background + ] + if len(backgrounds) > 1: + raise TemplateStructureError( + f"{svg_path.name}: template mode allows at most one {scope} " + "solid background" + ) + + return TemplateSlideSpec( + slide_num=slide_num, + svg_path=svg_path, + layout_key=layout_key, + layout_name=layout_name, + elements=tuple(elements), + ) + + +def parse_template_slides(svg_files: list[Path]) -> list[TemplateSlideSpec]: + """Parse a deck and enforce cross-slide master/layout contracts.""" + specs = [ + parse_template_slide(svg_path, slide_num) + for slide_num, svg_path in enumerate(svg_files, start=1) + ] + if not specs: + raise TemplateStructureError("Template export requires at least one SVG slide") + + expected_master = tuple( + item.contract_signature() for item in specs[0].master_elements + ) + for spec in specs[1:]: + actual_master = tuple( + item.contract_signature() for item in spec.master_elements + ) + if actual_master != expected_master: + raise TemplateStructureError( + f"{spec.svg_path.name}: master layer contract differs from " + f"{specs[0].svg_path.name}; every template slide must repeat the same " + "explicit master elements in the same order" + ) + + by_layout: dict[str, list[TemplateSlideSpec]] = {} + for spec in specs: + by_layout.setdefault(spec.layout_key, []).append(spec) + for layout_key, layout_specs in by_layout.items(): + prototype = layout_specs[0] + for spec in layout_specs[1:]: + if spec.layout_name != prototype.layout_name: + raise TemplateStructureError( + f"{spec.svg_path.name}: layout {layout_key!r} uses name " + f"{spec.layout_name!r}, expected {prototype.layout_name!r}" + ) + if spec.layout_contract != prototype.layout_contract: + raise TemplateStructureError( + f"{spec.svg_path.name}: layout {layout_key!r} structure differs " + f"from prototype {prototype.svg_path.name}; repeat the same layout " + "layers and placeholder ids/types in the same order" + ) + return specs + + +def parse_preserve_slides(svg_files: list[Path]) -> list[TemplateSlideSpec]: + """Parse preserve-mode slides before source master grouping is known.""" + specs = [ + parse_template_slide(svg_path, slide_num) + for slide_num, svg_path in enumerate(svg_files, start=1) + ] + if not specs: + raise TemplateStructureError("Preserve export requires at least one SVG slide") + return specs + + +def template_lock_errors( + specs: list[TemplateSlideSpec], + structure_lock: PptxStructureLock, +) -> list[str]: + """Return mismatches between parsed SVG layouts and the project lock.""" + if structure_lock.mode not in {"template", "preserve"}: + return [] + errors: list[str] = [] + references = { + reference.slide_num: reference + for reference in structure_lock.layouts + } + actual_slides = {spec.slide_num for spec in specs} + expected_slides = set(references) + missing = sorted(actual_slides - expected_slides) + extra = sorted(expected_slides - actual_slides) + if missing: + pages = ", ".join(f"P{slide_num:02d}" for slide_num in missing) + errors.append( + f"spec_lock.md pptx_layouts is missing generated page(s): {pages}" + ) + if extra: + pages = ", ".join(f"P{slide_num:02d}" for slide_num in extra) + errors.append( + f"spec_lock.md pptx_layouts references absent page(s): {pages}" + ) + for spec in specs: + reference = references.get(spec.slide_num) + if reference is None: + continue + if spec.layout_key != reference.layout_key: + errors.append( + f"{spec.svg_path.name}: data-pptx-layout={spec.layout_key!r} " + f"does not match spec_lock P{spec.slide_num:02d} layout key " + f"{reference.layout_key!r}" + ) + if reference.layout_name and spec.layout_name != reference.layout_name: + errors.append( + f"{spec.svg_path.name}: data-pptx-layout-name={spec.layout_name!r} " + f"does not match spec_lock P{spec.slide_num:02d} layout name " + f"{reference.layout_name!r}" + ) + return errors + + +_PRESERVE_PLACEHOLDER_TYPE_ORDER = { + "title": ("title", "ctrTitle"), + "subtitle": ("subTitle", "body", "obj"), + "body": ("body", "obj", "subTitle"), + "picture": ("pic", "obj"), + "chart": ("chart", "obj"), + "table": ("tbl", "obj"), + "object": ("obj",), + "media": ("media", "obj", "pic"), + "date": ("dt",), + "footer": ("ftr",), + "slide-number": ("sldNum",), +} + + +def match_native_placeholders( + spec: TemplateSlideSpec, + layout: NativeLayoutSpec, +) -> tuple[tuple[TemplateElementSpec, NativePlaceholderSpec], ...]: + """Match slide placeholder markers to source layout placeholder identities.""" + available = list(layout.placeholders) + matches: list[tuple[TemplateElementSpec, NativePlaceholderSpec]] = [] + for item in spec.placeholders: + allowed_types = _PRESERVE_PLACEHOLDER_TYPE_ORDER.get( + item.placeholder or "", + (), + ) + candidate_index = None + for placeholder_type in allowed_types: + for index, candidate in enumerate(available): + if candidate.placeholder_type != placeholder_type: + continue + if ( + item.placeholder_idx is not None + and candidate.idx != item.placeholder_idx + ): + continue + candidate_index = index + break + if candidate_index is not None: + break + if candidate_index is None: + idx_note = ( + f" idx={item.placeholder_idx}" + if item.placeholder_idx is not None + else "" + ) + raise TemplateStructureError( + f"{spec.svg_path.name}: placeholder {item.element_id!r} " + f"({item.placeholder}{idx_note}) has no compatible source placeholder " + f"in layout {layout.key!r}" + ) + matches.append((item, available.pop(candidate_index))) + return tuple(matches) + + +def native_structure_lock_errors( + specs: list[TemplateSlideSpec], + structure_lock: PptxStructureLock, + contract: NativeStructureContract, +) -> list[str]: + """Return preserve-mode mismatches against the imported source contract.""" + if structure_lock.mode != "preserve": + return [] + errors: list[str] = [] + references = {item.slide_num: item for item in structure_lock.layouts} + contract_layouts = {layout.key: layout for layout in contract.layouts} + + for reference in structure_lock.layouts: + layout = contract_layouts.get(reference.layout_key) + if layout is None: + errors.append( + f"spec_lock.md P{reference.slide_num:02d} references unknown source " + f"layout key {reference.layout_key!r}" + ) + continue + if reference.layout_name and reference.layout_name != layout.name: + errors.append( + f"spec_lock.md P{reference.slide_num:02d} layout name " + f"{reference.layout_name!r} does not match source name {layout.name!r}" + ) + + master_contracts: dict[str, tuple[tuple[object, ...], ...]] = {} + layout_contracts: dict[str, tuple[tuple[object, ...], ...]] = {} + for spec in specs: + reference = references.get(spec.slide_num) + if reference is None: + continue + layout = contract_layouts.get(reference.layout_key) + if layout is None: + continue + master_contract = tuple( + item.contract_signature() for item in spec.master_elements + ) + expected_master = master_contracts.setdefault( + layout.master_key, + master_contract, + ) + if master_contract != expected_master: + errors.append( + f"{spec.svg_path.name}: preview master layer differs from another " + f"page using source master {layout.master_key!r}" + ) + expected_layout = layout_contracts.setdefault( + layout.key, + spec.layout_contract, + ) + if spec.layout_contract != expected_layout: + errors.append( + f"{spec.svg_path.name}: preview layout/placeholder contract differs " + f"from another page using source layout {layout.key!r}" + ) + try: + match_native_placeholders(spec, layout) + except TemplateStructureError as exc: + errors.append(str(exc)) + return errors + + +def validate_template_svg(svg_path: Path) -> list[str]: + """Return per-file template metadata errors for quality-check integration.""" + try: + parse_template_slide(svg_path, 1) + except TemplateStructureError as exc: + return [str(exc)] + return [] diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/semantic_markers.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/semantic_markers.py new file mode 100644 index 00000000..f5cc0772 --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/semantic_markers.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +""" +PPT Master - Semantic SVG Markers + +Owns the minimal page and structural-role marker vocabulary used by SVG +authoring, validation, conversion traces, and native PPTX structure +reconstruction. These markers are compiler hints, not a parallel content model. + +Usage: + Import from svg_quality_checker.py or svg_to_pptx internals. + +Examples: + validate_semantic_markers(root, require_page_role=True) + +Dependencies: + None (standard library only) +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from pathlib import Path +from xml.etree import ElementTree as ET + + +PAGE_ROLE_TO_LAYOUT = { + "cover": "Cover", + "toc": "Agenda", + "section": "Section", + "content": "Content", + "ending": "Closing", +} + +STRUCTURAL_ROLES = frozenset({ + "background", + "chrome", + "decoration", + "footer", + "header", + "logo", + "page-number", + "watermark", +}) + +CHROME_ROLE_TO_TOKEN = { + "chrome": "chrome", + "footer": "footer", + "header": "header", + "logo": "logo", + "page-number": "pagenumber", + "watermark": "watermark", +} + +ANIMATION_CHROME_ROLES = STRUCTURAL_ROLES + +SEMANTIC_ATTRS = frozenset({ + "data-pptx-page-role", + "data-pptx-role", +}) + +_NON_VISUAL_TAGS = frozenset({"defs", "desc", "metadata", "style", "title"}) +_MARKER_TOKEN_RE = re.compile(r"^[a-z][a-z0-9-]*$") + + +@dataclass(frozen=True) +class SemanticMarkerIssue: + """One semantic-marker validation issue.""" + + severity: str + message: str + + +def _local_tag(elem: ET.Element) -> str: + """Return one element's local tag name.""" + return elem.tag.rsplit("}", 1)[-1] if "}" in str(elem.tag) else str(elem.tag) + + +def _normalized_marker(raw: str | None) -> str | None: + """Normalize one marker value, preserving absence.""" + if raw is None: + return None + return raw.strip().lower() + + +def chrome_token_from_role(role: str | None) -> str | None: + """Return the baseline chrome token represented by a semantic role.""" + normalized = _normalized_marker(role) + return CHROME_ROLE_TO_TOKEN.get(normalized or "") + + +def chrome_token_from_markers( + role: str | None, + placeholder: str | None, +) -> str | None: + """Return chrome behavior from specialized markers before generic roles.""" + if _normalized_marker(placeholder) == "slide-number": + return "pagenumber" + return chrome_token_from_role(role) + + +def is_chrome_role(role: str | None) -> bool: + """Return whether a semantic role represents non-content page chrome.""" + normalized = _normalized_marker(role) + return normalized in ANIMATION_CHROME_ROLES + + +def is_static_page_frame( + role: str | None, + placeholder: str | None, +) -> bool: + """Return whether explicit markers identify a static page-frame object.""" + return ( + _normalized_marker(placeholder) == "slide-number" + or is_chrome_role(role) + ) + + +def page_layout_name_from_svg(svg_path: Path) -> str | None: + """Return the native baseline Layout name declared by the SVG page role.""" + root = ET.parse(svg_path).getroot() + role = _normalized_marker(root.get("data-pptx-page-role")) + return PAGE_ROLE_TO_LAYOUT.get(role or "") + + +def validate_semantic_markers( + root: ET.Element, + *, + require_page_role: bool = False, +) -> list[SemanticMarkerIssue]: + """Validate semantic markers without changing SVG rendering semantics.""" + issues: list[SemanticMarkerIssue] = [] + page_role_raw = root.get("data-pptx-page-role") + page_role = _normalized_marker(page_role_raw) + if page_role_raw is None: + if require_page_role: + issues.append(SemanticMarkerIssue( + "warning", + "page SVG is missing root data-pptx-page-role", + )) + elif not page_role: + issues.append(SemanticMarkerIssue( + "error", + "root data-pptx-page-role must not be empty", + )) + elif not _MARKER_TOKEN_RE.fullmatch(page_role): + issues.append(SemanticMarkerIssue( + "error", + f"invalid data-pptx-page-role={page_role_raw!r}; use lowercase kebab-case", + )) + elif page_role not in PAGE_ROLE_TO_LAYOUT: + issues.append(SemanticMarkerIssue( + "warning", + f"unknown data-pptx-page-role={page_role_raw!r}", + )) + elif page_role_raw != page_role: + issues.append(SemanticMarkerIssue( + "warning", + f"data-pptx-page-role should use canonical lowercase value {page_role!r}", + )) + + id_counts: dict[str, int] = {} + for elem in root.iter(): + elem_id = (elem.get("id") or "").strip() + if elem_id: + id_counts[elem_id] = id_counts.get(elem_id, 0) + 1 + + marked_ids: set[str] = set() + for elem in root.iter(): + tag = _local_tag(elem) + elem_id = (elem.get("id") or "").strip() + if elem is not root and elem.get("data-pptx-page-role") is not None: + issues.append(SemanticMarkerIssue( + "error", + f"{elem_id or tag}: data-pptx-page-role belongs on the root only", + )) + + role_raw = elem.get("data-pptx-role") + role = _normalized_marker(role_raw) + if role_raw is not None: + if elem is root or tag in _NON_VISUAL_TAGS: + issues.append(SemanticMarkerIssue( + "error", + f"{elem_id or tag}: data-pptx-role belongs on a visual SVG element", + )) + if not role: + issues.append(SemanticMarkerIssue( + "error", + f"{elem_id or tag}: data-pptx-role must not be empty", + )) + elif not _MARKER_TOKEN_RE.fullmatch(role): + issues.append(SemanticMarkerIssue( + "error", + f"{elem_id or tag}: invalid data-pptx-role={role_raw!r}; " + "use lowercase kebab-case", + )) + elif role not in STRUCTURAL_ROLES: + issues.append(SemanticMarkerIssue( + "warning", + f"{elem_id or tag}: unknown data-pptx-role={role_raw!r}", + )) + elif role_raw != role: + issues.append(SemanticMarkerIssue( + "warning", + f"{elem_id or tag}: data-pptx-role should use canonical " + f"lowercase value {role!r}", + )) + if not elem_id: + issues.append(SemanticMarkerIssue( + "error", + f"<{tag}> with data-pptx-role requires a stable id", + )) + else: + marked_ids.add(elem_id) + + layer = _normalized_marker(elem.get("data-pptx-layer")) + placeholder = _normalized_marker(elem.get("data-pptx-placeholder")) + if layer: + issues.append(SemanticMarkerIssue( + "warning", + f"{elem_id or tag}: data-pptx-role is redundant when " + "data-pptx-layer already owns structure/animation behavior", + )) + if role == "page-number" and placeholder == "slide-number": + issues.append(SemanticMarkerIssue( + "warning", + f"{elem_id or tag}: data-pptx-role='page-number' is redundant; " + "data-pptx-placeholder='slide-number' already owns the behavior", + )) + + duplicates = sorted( + elem_id for elem_id in marked_ids + if id_counts.get(elem_id, 0) > 1 + ) + if duplicates: + issues.append(SemanticMarkerIssue( + "error", + "semantic markers require unique ids; duplicate: " + ", ".join(duplicates), + )) + return issues diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_import/manifest.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_import/manifest.py index 40c4f190..4c1e3340 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_import/manifest.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_import/manifest.py @@ -65,6 +65,7 @@ class SlideRecord: text_samples: list[str] text_count: int shape_count: int + placeholders: list[dict[str, Any]] page_type: str svg_file: str flat_svg_file: str @@ -87,9 +88,13 @@ def summarize_part_record( bg_asset = detect_background_asset(root, rels) image_targets = extract_image_targets(root, rels) + display_name = part_display_name(root, part_path) + layout_type = root.attrib.get("type") if root is not None else None return { "path": part_path, "name": PurePosixPath(part_path).name, + "displayName": display_name, + "layoutType": layout_type, "svgFile": svg_file, "parentPath": parent_path, "themePath": theme_path, @@ -100,6 +105,7 @@ def summarize_part_record( "textSamples": extract_text_samples(root), "textCount": len(root.findall(".//a:t", NS)) if root is not None else 0, "shapeCount": count_slide_shapes(root), + "drawableShapeCount": count_drawable_shapes(root), "usedBySlides": used_by_slides, } @@ -201,6 +207,40 @@ def parse_xfrm_record(sp: ET.Element) -> dict[str, int] | None: } +def part_display_name(root: ET.Element | None, part_path: str) -> str: + """Return the PowerPoint picker name, falling back to the package stem.""" + if root is not None: + common_slide = root.find("p:cSld", NS) + if common_slide is not None: + name = (common_slide.attrib.get("name") or "").strip() + if name: + return name + matching_name = (root.attrib.get("matchingName") or "").strip() + if matching_name: + return matching_name + return PurePosixPath(part_path).stem + + +def placeholder_semantic_role(placeholder_type: str | None) -> str: + """Map an OOXML placeholder type to the exporter role vocabulary.""" + normalized = placeholder_type or "obj" + role_by_type = { + "title": "title", + "ctrTitle": "title", + "body": "body", + "subTitle": "subtitle", + "obj": "object", + "pic": "picture", + "chart": "chart", + "tbl": "table", + "media": "media", + "dt": "date", + "ftr": "footer", + "sldNum": "slide-number", + } + return role_by_type.get(normalized, "other") + + def extract_placeholders(root: ET.Element | None) -> list[dict[str, Any]]: if root is None: return [] @@ -209,11 +249,16 @@ def extract_placeholders(root: ET.Element | None) -> list[dict[str, Any]]: ph = sp.find("p:nvSpPr/p:nvPr/p:ph", NS) if ph is None: continue + non_visual = sp.find("p:nvSpPr/p:cNvPr", NS) + placeholder_type = ph.attrib.get("type") record: dict[str, Any] = { - "type": ph.attrib.get("type"), + "type": placeholder_type, "idx": ph.attrib.get("idx"), "size": ph.attrib.get("sz"), "orient": ph.attrib.get("orient"), + "semanticRole": placeholder_semantic_role(placeholder_type), + "shapeId": non_visual.attrib.get("id") if non_visual is not None else None, + "shapeName": non_visual.attrib.get("name") if non_visual is not None else None, "geometry": parse_xfrm_record(sp), "textSamples": extract_text_samples(sp, limit=2), } @@ -224,6 +269,30 @@ def extract_placeholders(root: ET.Element | None) -> list[dict[str, Any]]: return placeholders +def count_drawable_shapes(root: ET.Element | None) -> int: + """Count top-level visual shapes that are not placeholder definitions.""" + if root is None: + return 0 + sp_tree = root.find("p:cSld/p:spTree", NS) + if sp_tree is None: + return 0 + visual_tags = { + f"{{{NS['p']}}}sp", + f"{{{NS['p']}}}grpSp", + f"{{{NS['p']}}}graphicFrame", + f"{{{NS['p']}}}pic", + f"{{{NS['p']}}}cxnSp", + } + count = 0 + for child in sp_tree: + if child.tag not in visual_tags: + continue + if child.find("p:nvSpPr/p:nvPr/p:ph", NS) is not None: + continue + count += 1 + return count + + def extract_placeholder_text_style(sp: ET.Element) -> dict[str, Any]: style: dict[str, Any] = {} rpr = sp.find(".//a:rPr", NS) or sp.find(".//a:endParaRPr", NS) @@ -572,6 +641,7 @@ def build_manifest(pptx_path: Path, output_dir: Path) -> dict[str, Any]: image_targets = extract_image_targets(slide_root, slide_rels) texts = extract_text_samples(slide_root) shape_count = count_slide_shapes(slide_root) + placeholders = extract_placeholders(slide_root) page_type = classify_slide(index, len(slide_parts), texts, len(image_targets), shape_count) resolved_bg = copied_assets.get(bg_asset, PurePosixPath(bg_asset).name if bg_asset else None) @@ -623,6 +693,7 @@ def build_manifest(pptx_path: Path, output_dir: Path) -> dict[str, Any]: text_samples=texts, text_count=len(texts), shape_count=shape_count, + placeholders=placeholders, page_type=page_type, svg_file=slide_svg_filename(index), flat_svg_file=slide_svg_filename(index), @@ -740,6 +811,7 @@ def build_manifest(pptx_path: Path, output_dir: Path) -> dict[str, Any]: "textSamples": slide.text_samples, "textCount": slide.text_count, "shapeCount": slide.shape_count, + "placeholders": slide.placeholders, "pageType": slide.page_type, } for slide in slide_records diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_import/native_structure.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_import/native_structure.py new file mode 100644 index 00000000..aea0a290 --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/template_import/native_structure.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +""" +PPT Master - Native Template Structure Contract + +Build a portable master/layout contract from the PPTX template import manifest. + +Usage: + Imported by pptx_template_import.py. + +Examples: + write_native_structure_bundle(source_pptx, output_dir, manifest) + +Dependencies: + None (only uses standard library) +""" + +from __future__ import annotations + +import hashlib +import json +import shutil +from pathlib import Path +from typing import Any + + +SCHEMA = "ppt-master.native-structure.v1" +SOURCE_TEMPLATE_NAME = "source_template.pptx" +CONTRACT_NAME = "native_structure.json" + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _copy_source_template(source: Path, destination: Path) -> None: + if source.resolve() == destination.resolve(): + return + shutil.copy2(source, destination) + + +def _has_reusable_structure(manifest: dict[str, Any]) -> bool: + layouts = manifest.get("layouts", []) + masters = manifest.get("masters", []) + slides = manifest.get("slides", []) + used_layouts = [layout for layout in layouts if layout.get("usedBySlides")] + candidate_layouts = used_layouts or layouts + if any( + layout.get("backgroundAsset") + or int(layout.get("drawableShapeCount") or 0) > 0 + or any( + placeholder.get("semanticRole") + not in {"date", "footer", "slide-number", "other"} + for placeholder in layout.get("placeholders", []) + ) + for layout in candidate_layouts + ): + return True + if any( + master.get("backgroundAsset") + or int(master.get("drawableShapeCount") or 0) > 0 + or master.get("imageAssets") + for master in masters + ): + return True + return any(slide.get("placeholders") for slide in slides) + + +def build_native_structure( + source_pptx: Path, + manifest: dict[str, Any], +) -> dict[str, Any]: + """Build the portable source-package structure contract.""" + masters = manifest.get("masters", []) + layouts = manifest.get("layouts", []) + slides = manifest.get("slides", []) + master_key_by_path = { + master["path"]: f"master_{index:02d}" + for index, master in enumerate(masters, start=1) + } + layout_key_by_path = { + layout["path"]: f"layout_{index:02d}" + for index, layout in enumerate(layouts, start=1) + } + complete_graph = ( + bool(masters and layouts) + and all( + layout.get("parentPath") in master_key_by_path + for layout in layouts + ) + and all( + slide.get("layoutPath") in layout_key_by_path + and slide.get("masterPath") in master_key_by_path + for slide in slides + ) + ) + reusable_structure = _has_reusable_structure(manifest) + recommended_mode = "preserve" if complete_graph and reusable_structure else "template" + reasons: list[str] = [] + if not complete_graph: + reasons.append("incomplete-master-layout-graph") + if not reusable_structure: + reasons.append("source-structure-is-minimal") + if complete_graph and reusable_structure: + reasons.append("source-master-layout-contract-is-reusable") + if len(masters) > 1: + reasons.append("source-uses-multiple-masters") + + master_contracts = [] + for master in masters: + key = master_key_by_path[master["path"]] + master_contracts.append({ + "key": key, + "name": master.get("displayName") or master.get("name") or key, + "packagePart": master["path"], + "themePart": master.get("themePath"), + "theme": master.get("theme") or {"colors": {}, "fonts": {}}, + "backgroundAsset": master.get("backgroundAsset"), + "imageAssets": master.get("imageAssets", []), + "drawableShapeCount": master.get("drawableShapeCount", 0), + "layoutKeys": [ + layout_key_by_path[layout["path"]] + for layout in layouts + if layout.get("parentPath") == master["path"] + ], + }) + + layout_contracts = [] + for layout in layouts: + key = layout_key_by_path[layout["path"]] + layout_contracts.append({ + "key": key, + "name": layout.get("displayName") or layout.get("name") or key, + "type": layout.get("layoutType"), + "packagePart": layout["path"], + "masterKey": master_key_by_path.get(layout.get("parentPath")), + "backgroundAsset": layout.get("backgroundAsset"), + "imageAssets": layout.get("imageAssets", []), + "drawableShapeCount": layout.get("drawableShapeCount", 0), + "placeholders": layout.get("placeholders", []), + "usedBySlides": layout.get("usedBySlides", []), + "svgFile": layout.get("svgFile"), + }) + + slide_contracts = [] + for slide in slides: + slide_contracts.append({ + "index": slide["index"], + "pageType": slide.get("pageType"), + "layoutKey": layout_key_by_path.get(slide.get("layoutPath")), + "masterKey": master_key_by_path.get(slide.get("masterPath")), + "placeholders": slide.get("placeholders", []), + "layeredSvgFile": slide.get("svgFile"), + "flatSvgFile": slide.get("flatSvgFile"), + }) + + return { + "schema": SCHEMA, + "source": { + "name": source_pptx.name, + "templateFile": SOURCE_TEMPLATE_NAME, + "sha256": _sha256(source_pptx), + }, + "slideSize": manifest.get("slideSize", {}), + "strategy": { + "preservationEligible": complete_graph, + "reusableStructureDetected": reusable_structure, + "recommendedMode": recommended_mode, + "recommendationScope": "source-structure-assessment", + "templateOutputMode": "template", + "downstreamTemplateAdherence": "strategist-confirmed-explicit-structure", + "hasMultipleMasters": len(masters) > 1, + "reasonCodes": reasons, + }, + "masters": master_contracts, + "layouts": layout_contracts, + "slides": slide_contracts, + } + + +def write_native_structure_bundle( + source_pptx: Path, + output_dir: Path, + manifest: dict[str, Any], +) -> dict[str, Any]: + """Copy the source template and write its portable structure contract.""" + source_copy = output_dir / SOURCE_TEMPLATE_NAME + _copy_source_template(source_pptx, source_copy) + contract = build_native_structure(source_pptx, manifest) + (output_dir / CONTRACT_NAME).write_text( + json.dumps(contract, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + return contract diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/01_cover.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/01_cover.svg index bed8c1cc..083b8a63 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/01_cover.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/01_cover.svg @@ -1,6 +1,6 @@ - - + + @@ -13,8 +13,8 @@ - {{TITLE}} - {{SUBTITLE}} + {{TITLE}} + {{SUBTITLE}} {{AUTHOR}} China Telecom Digital Transformation Template diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/02_chapter.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/02_chapter.svg index 8f3c95b4..6ef815b3 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/02_chapter.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/02_chapter.svg @@ -1,11 +1,11 @@ - - + + {{CHAPTER_NUM}} - {{CHAPTER_TITLE}} + {{CHAPTER_TITLE}} DIGITAL TRANSFORMATION / CHINA TELECOM diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/02_toc.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/02_toc.svg index 306416bf..4db96446 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/02_toc.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/02_toc.svg @@ -1,6 +1,6 @@ - - + + diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/03_content.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/03_content.svg index 7154ef91..6710bb4d 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/03_content.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/03_content.svg @@ -1,22 +1,22 @@ - - + + {{SECTION_NAME}} - {{PAGE_TITLE}} + {{PAGE_TITLE}} - {{CONTENT_AREA}} + {{CONTENT_AREA}} {{SOURCE}} - {{PAGE_NUM}} + {{PAGE_NUM}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/04_ending.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/04_ending.svg index ca0e1ae7..0e0ad5a0 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/04_ending.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/04_ending.svg @@ -1,6 +1,6 @@ - - + + {{THANK_YOU}} @@ -14,5 +14,5 @@ - {{PAGE_NUM}} + {{PAGE_NUM}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/design_spec.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/design_spec.md index 6e85a4ac..3afae042 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/design_spec.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电信/design_spec.md @@ -1,6 +1,7 @@ --- deck_id: 中国电信 kind: deck +native_structure_mode: template summary: China Telecom related briefings, 政企数字化方案, 转型规划, 内部汇报. canvas_format: ppt169 page_count: 5 diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/01_cover.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/01_cover.svg index b8d67c55..301e8f3c 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/01_cover.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/01_cover.svg @@ -1,5 +1,5 @@ - + @@ -27,10 +27,10 @@ - + - + @@ -60,12 +60,12 @@ - + {{TITLE}} - + {{SUBTITLE}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/02_chapter.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/02_chapter.svg index 041ea9d9..09f6c9c9 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/02_chapter.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/02_chapter.svg @@ -1,5 +1,5 @@ - + @@ -21,10 +21,10 @@ - + - + diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/02_toc.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/02_toc.svg index 58e244dc..85842de7 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/02_toc.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/02_toc.svg @@ -1,5 +1,5 @@ - + @@ -16,7 +16,7 @@ - + @@ -104,7 +104,7 @@ - {{PAGE_NUM}} + {{PAGE_NUM}} POWERCHINA diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/03_content.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/03_content.svg index d1934052..066aa337 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/03_content.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/03_content.svg @@ -1,5 +1,5 @@ - + @@ -16,7 +16,7 @@ - + @@ -27,7 +27,7 @@ {{CHAPTER_NUM}} - {{PAGE_TITLE}} + {{PAGE_TITLE}} POWERCHINA @@ -38,7 +38,7 @@ - + {{CONTENT_AREA}} @@ -49,7 +49,7 @@ - {{PAGE_NUM}} + {{PAGE_NUM}} POWERCHINA diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/04_ending.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/04_ending.svg index e41d8051..4d7f9d23 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/04_ending.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/04_ending.svg @@ -1,5 +1,5 @@ - + @@ -21,10 +21,10 @@ - + - + diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/design_spec.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/design_spec.md index a4c72627..7518d0d2 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/design_spec.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中国电建/design_spec.md @@ -1,6 +1,7 @@ --- deck_id: 中国电建 kind: deck +native_structure_mode: template summary: Engineering project reports, technical proposal presentations, business negotiations, corporate promotion, annual summaries. canvas_format: ppt169 page_count: 5 diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/01_cover.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/01_cover.svg index 8a7aa636..bfe13501 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/01_cover.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/01_cover.svg @@ -1,30 +1,35 @@ - + + + + preserveAspectRatio="xMidYMid slice" id="semantic-01_cover-background-1" data-pptx-role="background" /> - + + preserveAspectRatio="xMidYMid meet" id="semantic-01_cover-logo-1" data-pptx-role="logo" /> + font-weight="bold" fill="#FFFFFF" id="slot-01_cover-title-1" data-pptx-placeholder="title"> {{TITLE}} + fill="#FFFFFF" fill-opacity="0.85" id="slot-01_cover-subtitle-2" data-pptx-placeholder="subtitle"> {{SUBTITLE}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/02_chapter.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/02_chapter.svg index 3b68bdd9..355a09f8 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/02_chapter.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/02_chapter.svg @@ -1,5 +1,5 @@ - + @@ -11,7 +11,7 @@ - + diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/02_toc.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/02_toc.svg index f5ff3836..573ef4a8 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/02_toc.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/02_toc.svg @@ -1,9 +1,9 @@ - + - + @@ -12,7 +12,7 @@ 目 录 - + @@ -68,6 +68,6 @@ - {{PAGE_NUM}} + {{PAGE_NUM}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/03_content.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/03_content.svg index 211f26da..2ed8376f 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/03_content.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/03_content.svg @@ -1,9 +1,9 @@ - + - + @@ -16,11 +16,11 @@ {{PAGE_TITLE}} + fill="#333333" id="slot-03_content-title-1" data-pptx-placeholder="title">{{PAGE_TITLE}} + preserveAspectRatio="xMidYMid meet" id="semantic-03_content-logo-1" data-pptx-role="logo" /> - + {{CONTENT_AREA}} @@ -52,6 +52,6 @@ {{PAGE_NUM}} + fill="#999999" id="slot-03_content-slide_number-3" data-pptx-placeholder="slide-number">{{PAGE_NUM}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/04_ending.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/04_ending.svg index 4e2b833c..1fddae3f 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/04_ending.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/中汽研/04_ending.svg @@ -1,9 +1,9 @@ - + - + @@ -23,7 +23,7 @@ + preserveAspectRatio="xMidYMid meet" id="semantic-04_ending-logo-1" data-pptx-role="logo" /> - - + + - + @@ -11,10 +12,10 @@ - + {{TITLE}} - + {{SUBTITLE}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/02_chapter.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/02_chapter.svg index a602d70f..682c155c 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/02_chapter.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/02_chapter.svg @@ -1,6 +1,6 @@ - - + + @@ -16,7 +16,7 @@ {{CHAPTER_NUM}} - + {{CHAPTER_TITLE}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/02_toc.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/02_toc.svg index f41a1508..fba944ba 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/02_toc.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/02_toc.svg @@ -1,6 +1,6 @@ - - + + @@ -36,7 +36,7 @@ 分隔线: 列分隔: (纵向) --> - + {{CONTENT_AREA}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/03_content.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/03_content.svg index 24ae70cc..e3569cf7 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/03_content.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/03_content.svg @@ -1,6 +1,6 @@ - - + + @@ -12,7 +12,7 @@ - + {{PAGE_TITLE}} @@ -23,7 +23,7 @@ - + {{KEY_MESSAGE}} @@ -65,7 +65,7 @@ 数据来源:{{SOURCE}} - + {{PAGE_NUM}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/04_ending.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/04_ending.svg index 4dc647b5..06ecdc14 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/04_ending.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/04_ending.svg @@ -1,8 +1,9 @@ - - + + - + diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/design_spec.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/design_spec.md index 3c082212..0c81d9d2 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/design_spec.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/招商银行/design_spec.md @@ -1,6 +1,7 @@ --- deck_id: 招商银行 kind: deck +native_structure_mode: template summary: 交易银行产品介绍、销售收款方案汇报、客户案例拆解、分行培训材料. canvas_format: ppt169 page_count: 5 diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/01_cover.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/01_cover.svg index da233936..94e7839b 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/01_cover.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/01_cover.svg @@ -1,4 +1,4 @@ - + @@ -14,7 +14,7 @@ - + @@ -36,10 +36,10 @@ - {{TITLE}} + {{TITLE}} - {{SUBTITLE}} + {{SUBTITLE}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/02_chapter.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/02_chapter.svg index 8dbd5cf1..52eb3491 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/02_chapter.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/02_chapter.svg @@ -1,4 +1,4 @@ - + @@ -8,7 +8,7 @@ - + @@ -34,7 +34,7 @@ - {{CHAPTER_TITLE}} + {{CHAPTER_TITLE}} {{CHAPTER_DESC}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/02_toc.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/02_toc.svg index c3d1bb54..2a3533a1 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/02_toc.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/02_toc.svg @@ -1,4 +1,4 @@ - + @@ -13,14 +13,14 @@ - + - {{TOC_TITLE}} + {{TOC_TITLE}} @@ -72,7 +72,7 @@ - {{PAGE_NUM}} + {{PAGE_NUM}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/03_content.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/03_content.svg index 450ea472..92e1d3e2 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/03_content.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/03_content.svg @@ -1,4 +1,4 @@ - + @@ -8,7 +8,7 @@ - + @@ -17,7 +17,7 @@ - {{PAGE_TITLE}} + {{PAGE_TITLE}} @@ -29,7 +29,7 @@ 核心观点 - {{KEY_MESSAGE}} + {{KEY_MESSAGE}} @@ -49,7 +49,7 @@ - {{PAGE_NUM}} + {{PAGE_NUM}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/04_ending.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/04_ending.svg index 5a7e68f9..001bd4e5 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/04_ending.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/04_ending.svg @@ -1,4 +1,4 @@ - + @@ -14,7 +14,7 @@ - + @@ -52,7 +52,7 @@ {{COPYRIGHT}} - {{PAGE_NUM}} + {{PAGE_NUM}} CQU diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/design_spec.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/design_spec.md index 13e7cec5..1d0a200b 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/design_spec.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/decks/重庆大学/design_spec.md @@ -1,6 +1,7 @@ --- deck_id: 重庆大学 kind: deck +native_structure_mode: template summary: Academic defense, research reports, teaching presentations, scholarly exchange. canvas_format: ppt169 page_count: 5 diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/design_spec_reference.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/design_spec_reference.md index 86c8067a..8532fab7 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/design_spec_reference.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/design_spec_reference.md @@ -16,6 +16,7 @@ | **Use Case** | [Filled by Strategist] | | **Delivery Purpose** | [`text` read-close / `balanced` business / `presentation` — confirmed at c; a deck-wide consumption mode that drives per-page density, page-count recommendation, page_rhythm lean, and the body baseline (px). See strategist.md §6.1.] | | **Content Strategy** | [Material divergence — the user's free-text intent on how closely to follow the source vs how freely to reshape it (or "balanced default"); facts stay sourced however free. Confirmed at c; consumed when authoring §IX. Not in spec_lock.] | +| **Template Adherence** | [`adaptive` / `strict` — include only when Step 3 loaded a deck/layout template; omit for free design and brand-only templates. Also written under `spec_lock.md pptx_structure`.] | | **Created Date** | {date_str} | --- diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/01_cover.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/01_cover.svg index 39adfce6..07b45bd4 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/01_cover.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/01_cover.svg @@ -1,7 +1,7 @@ - + - + @@ -11,18 +11,18 @@ - + {{LOGO}} - + {{TITLE}} - + {{SUBTITLE}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/02_chapter.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/02_chapter.svg index 964f8ea8..8f5e63ce 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/02_chapter.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/02_chapter.svg @@ -1,7 +1,7 @@ - + - + @@ -17,7 +17,7 @@ - + {{CHAPTER_TITLE}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/02_toc.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/02_toc.svg index 8c123925..4301801d 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/02_toc.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/02_toc.svg @@ -1,7 +1,7 @@ - + - + @@ -13,7 +13,7 @@ - + {{LOGO}} @@ -59,5 +59,5 @@ - {{PAGE_NUM}} + {{PAGE_NUM}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/03_content.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/03_content.svg index 17c8f287..7180d958 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/03_content.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/03_content.svg @@ -1,7 +1,7 @@ - + - + @@ -10,20 +10,20 @@ - + {{SECTION_NUM}} {{PAGE_TITLE}} - + {{LOGO}} - + {{KEY_MESSAGE}} @@ -52,7 +52,7 @@ {{SECTION_NAME}} - + {{PAGE_NUM}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/04_ending.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/04_ending.svg index 9976c51c..75047dbc 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/04_ending.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/04_ending.svg @@ -1,14 +1,14 @@ - + - + - + {{LOGO}} @@ -46,7 +46,7 @@ {{COPYRIGHT}} - + {{PAGE_NUM}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/design_spec.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/design_spec.md index fdc6b455..ee9dde1c 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/design_spec.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/academic_defense/design_spec.md @@ -1,6 +1,7 @@ --- layout_id: academic_defense kind: layout +native_structure_mode: template summary: Thesis defense, academic presentations, research progress reports, grant applications. canvas_format: ppt169 page_count: 5 diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/01_cover.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/01_cover.svg index 72779a60..b1c79d48 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/01_cover.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/01_cover.svg @@ -1,6 +1,6 @@ - + - + @@ -13,10 +13,10 @@ - {{TITLE}} + {{TITLE}} - {{SUBTITLE}} + {{SUBTITLE}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/02_chapter.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/02_chapter.svg index c477c66b..ce4bbae6 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/02_chapter.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/02_chapter.svg @@ -1,6 +1,6 @@ - + - + @@ -21,7 +21,7 @@ {{CHAPTER_NUM}} - {{CHAPTER_TITLE}} + {{CHAPTER_TITLE}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/02_toc.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/02_toc.svg index b50822d6..11cc8708 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/02_toc.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/02_toc.svg @@ -1,6 +1,6 @@ - + - + diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/03_content.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/03_content.svg index a4993f5e..e5270a2a 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/03_content.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/03_content.svg @@ -1,6 +1,6 @@ - + - + @@ -13,13 +13,13 @@ - {{PAGE_TITLE}} + {{PAGE_TITLE}} - {{CONTENT_AREA}} + {{CONTENT_AREA}} @@ -33,5 +33,5 @@ - {{PAGE_NUM}} + {{PAGE_NUM}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/04_ending.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/04_ending.svg index f73e908b..8c0c96d0 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/04_ending.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/04_ending.svg @@ -1,6 +1,6 @@ - + - + diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/design_spec.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/design_spec.md index b3d3d7f0..71c441cc 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/design_spec.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/design_spec.md @@ -1,6 +1,7 @@ --- layout_id: ai_ops kind: layout +native_structure_mode: template summary: Telecom AI operations architecture, IT system overviews, digital transformation proposals, smart infrastructure reports. canvas_format: ppt169 page_count: 6 diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/reference_style.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/reference_style.svg index d78328e0..93535386 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/reference_style.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/ai_ops/reference_style.svg @@ -1,6 +1,6 @@ - + - + diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/01_cover.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/01_cover.svg index 0665e9bc..80f11dcf 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/01_cover.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/01_cover.svg @@ -1,5 +1,5 @@ - + @@ -27,10 +27,10 @@ - + - + @@ -54,12 +54,12 @@ - + {{TITLE}} - + {{SUBTITLE}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/02_chapter.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/02_chapter.svg index e2693e19..fba20096 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/02_chapter.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/02_chapter.svg @@ -1,5 +1,5 @@ - + @@ -22,10 +22,10 @@ - + - + diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/02_toc.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/02_toc.svg index d29473eb..2e242f8a 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/02_toc.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/02_toc.svg @@ -1,5 +1,5 @@ - + @@ -21,7 +21,7 @@ - + @@ -102,6 +102,6 @@ - {{PAGE_NUM}} + {{PAGE_NUM}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/03_content.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/03_content.svg index 5b96e06e..a6f55240 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/03_content.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/03_content.svg @@ -1,5 +1,5 @@ - + @@ -21,7 +21,7 @@ - + @@ -32,7 +32,7 @@ {{CHAPTER_NUM}} - {{PAGE_TITLE}} + {{PAGE_TITLE}} {{ORG_SHORT}} @@ -52,7 +52,7 @@ - 流程图 --> - + {{CONTENT_AREA}} @@ -70,7 +70,7 @@ - {{PAGE_NUM}} + {{PAGE_NUM}} {{ORG_SHORT}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/04_ending.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/04_ending.svg index 8ac235cd..569b8f3a 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/04_ending.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/04_ending.svg @@ -1,5 +1,5 @@ - + @@ -17,7 +17,7 @@ - + diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/design_spec.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/design_spec.md index 3440a715..b8556fbd 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/design_spec.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_blue/design_spec.md @@ -1,6 +1,7 @@ --- layout_id: government_blue kind: layout +native_structure_mode: template summary: Key project briefings, Five-Year Plan presentations, work summaries, investment promotion, policy interpretation. canvas_format: ppt169 page_count: 5 diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/01_cover.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/01_cover.svg index a593c454..04e31365 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/01_cover.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/01_cover.svg @@ -1,5 +1,5 @@ - + @@ -17,7 +17,7 @@ - + @@ -37,12 +37,12 @@ - + {{TITLE}} - + {{SUBTITLE}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/02_chapter.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/02_chapter.svg index 6941357e..81b945da 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/02_chapter.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/02_chapter.svg @@ -1,5 +1,5 @@ - + @@ -17,7 +17,7 @@ - + diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/02_toc.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/02_toc.svg index faf6dd88..dca695ab 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/02_toc.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/02_toc.svg @@ -1,5 +1,5 @@ - + @@ -11,7 +11,7 @@ - + @@ -83,6 +83,6 @@ - {{PAGE_NUM}} + {{PAGE_NUM}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/03_content.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/03_content.svg index 5f09d858..f4eec87a 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/03_content.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/03_content.svg @@ -1,5 +1,5 @@ - + @@ -11,7 +11,7 @@ - + @@ -22,7 +22,7 @@ {{CHAPTER_NUM}} - {{PAGE_TITLE}} + {{PAGE_TITLE}} {{ORG_SHORT}} @@ -39,7 +39,7 @@ - 流程图 --> - + {{CONTENT_AREA}} @@ -58,7 +58,7 @@ - {{PAGE_NUM}} + {{PAGE_NUM}} {{ORG_SHORT}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/04_ending.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/04_ending.svg index 8097e148..7fb9f883 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/04_ending.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/04_ending.svg @@ -1,5 +1,5 @@ - + @@ -17,7 +17,7 @@ - + diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/design_spec.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/design_spec.md index 614d8e1b..e85c3155 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/design_spec.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/government_red/design_spec.md @@ -1,6 +1,7 @@ --- layout_id: government_red kind: layout +native_structure_mode: template summary: Government briefings, policy interpretation, work summaries, project introductions, investment promotion. canvas_format: ppt169 page_count: 5 diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/01_cover.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/01_cover.svg index da938449..ca1c3df5 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/01_cover.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/01_cover.svg @@ -1,7 +1,7 @@ - + - + @@ -17,20 +17,20 @@ + font-size="16" font-weight="bold" id="semantic-01_cover-logo-1" data-pptx-role="logo"> {{LOGO}} + font-size="52" font-weight="bold" id="slot-01_cover-title-1" data-pptx-placeholder="title"> {{TITLE}} + font-size="26" id="slot-01_cover-subtitle-2" data-pptx-placeholder="subtitle"> {{SUBTITLE}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/02_chapter.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/02_chapter.svg index 251091c6..13913336 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/02_chapter.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/02_chapter.svg @@ -1,7 +1,7 @@ - + - + @@ -32,7 +32,7 @@ + font-weight="bold" id="slot-02_chapter-title-1" data-pptx-placeholder="title"> {{CHAPTER_TITLE}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/02_toc.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/02_toc.svg index 6a7817df..a74ab669 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/02_toc.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/02_toc.svg @@ -1,7 +1,7 @@ - + - + @@ -21,7 +21,7 @@ + font-size="16" font-weight="bold" id="semantic-02_toc-logo-1" data-pptx-role="logo"> {{LOGO}} @@ -93,5 +93,5 @@ {{INSTITUTION}} {{PAGE_NUM}} + font-size="14" id="slot-02_toc-slide_number-1" data-pptx-placeholder="slide-number">{{PAGE_NUM}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/03_content.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/03_content.svg index f40a0dc3..fde25a1a 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/03_content.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/03_content.svg @@ -1,7 +1,7 @@ - + - + @@ -11,21 +11,21 @@ + font-weight="bold" id="slot-03_content-title-1" data-pptx-placeholder="title"> {{SECTION_NUM}} {{PAGE_TITLE}} + font-size="16" font-weight="bold" id="semantic-03_content-logo-1" data-pptx-role="logo"> {{LOGO}} - + {{KEY_MESSAGE}} @@ -72,7 +72,7 @@ {{SECTION_NAME}} + font-size="14" id="slot-03_content-slide_number-3" data-pptx-placeholder="slide-number"> {{PAGE_NUM}} \ No newline at end of file diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/04_ending.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/04_ending.svg index ee52fc98..beceadbf 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/04_ending.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/04_ending.svg @@ -1,7 +1,7 @@ - + - + @@ -13,7 +13,7 @@ + font-size="16" font-weight="bold" id="semantic-04_ending-logo-1" data-pptx-role="logo"> {{LOGO}} @@ -73,7 +73,7 @@ {{COPYRIGHT}} + font-size="14" id="slot-04_ending-slide_number-1" data-pptx-placeholder="slide-number"> {{PAGE_NUM}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/design_spec.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/design_spec.md index d9cb8d31..0302cd31 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/design_spec.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/medical_university/design_spec.md @@ -1,6 +1,7 @@ --- layout_id: medical_university kind: layout +native_structure_mode: template summary: Medical academic reports, case discussions, research presentations, hospital work reports, medical education and training. canvas_format: ppt169 page_count: 5 diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/01_cover.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/01_cover.svg index 6769b566..1f1a2e7a 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/01_cover.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/01_cover.svg @@ -1,5 +1,5 @@ - + @@ -30,7 +30,7 @@ - + @@ -71,12 +71,12 @@ - + {{TITLE}} - + {{SUBTITLE}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/02_chapter.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/02_chapter.svg index 96216446..c3c03ed1 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/02_chapter.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/02_chapter.svg @@ -1,5 +1,5 @@ - + @@ -20,7 +20,7 @@ - + @@ -124,7 +124,7 @@ - {{PAGE_NUM}}/{{TOTAL_PAGES}} + {{PAGE_NUM}}/{{TOTAL_PAGES}} LOADING... diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/02_toc.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/02_toc.svg index 8e9f2130..2049524e 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/02_toc.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/02_toc.svg @@ -1,5 +1,5 @@ - + @@ -13,14 +13,14 @@ - + - + 📍 {{PAGE_TITLE}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/03_content.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/03_content.svg index 3d132691..515b70c7 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/03_content.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/03_content.svg @@ -1,5 +1,5 @@ - + @@ -14,13 +14,13 @@ - + - + {{PAGE_TITLE}} @@ -36,7 +36,7 @@ - + {{CONTENT_AREA}} @@ -44,7 +44,7 @@ - {{PAGE_NUM}}/{{TOTAL_PAGES}} + {{PAGE_NUM}}/{{TOTAL_PAGES}} {{PAGE_SUBTITLE}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/04_ending.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/04_ending.svg index d1fcfc74..7796c713 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/04_ending.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/04_ending.svg @@ -1,5 +1,5 @@ - + @@ -20,7 +20,7 @@ - + @@ -157,7 +157,7 @@ - {{PAGE_NUM}}/{{TOTAL_PAGES}} + {{PAGE_NUM}}/{{TOTAL_PAGES}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/design_spec.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/design_spec.md index ab7d84cd..68123668 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/design_spec.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/pixel_retro/design_spec.md @@ -1,6 +1,7 @@ --- layout_id: pixel_retro kind: layout +native_structure_mode: template summary: Tech talks, programming tutorials, game introductions, geek-style showcases. canvas_format: ppt169 page_count: 5 diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/01_cover.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/01_cover.svg index 63fa3ff1..840de924 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/01_cover.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/01_cover.svg @@ -1,4 +1,4 @@ - + @@ -8,7 +8,7 @@ - + @@ -28,13 +28,13 @@ + fill="#FFFFFF" text-anchor="middle" id="slot-01_cover-title-1" data-pptx-placeholder="title"> {{TITLE}} + text-anchor="middle" id="slot-01_cover-subtitle-2" data-pptx-placeholder="subtitle"> {{SUBTITLE}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/02_chapter.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/02_chapter.svg index ce3f0364..b72298ee 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/02_chapter.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/02_chapter.svg @@ -1,4 +1,4 @@ - + @@ -7,7 +7,7 @@ - + @@ -37,7 +37,7 @@ + fill="#FFFFFF" text-anchor="middle" id="slot-02_chapter-title-1" data-pptx-placeholder="title"> {{CHAPTER_TITLE}} @@ -78,5 +78,5 @@ {{PAGE_NUM}} + text-anchor="end" id="slot-02_chapter-slide_number-2" data-pptx-placeholder="slide-number">{{PAGE_NUM}} \ No newline at end of file diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/02_toc.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/02_toc.svg index d5696fcd..b22dcc18 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/02_toc.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/02_toc.svg @@ -1,6 +1,6 @@ - + - + diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/03_content.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/03_content.svg index bd253085..eeaf7770 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/03_content.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/03_content.svg @@ -1,13 +1,13 @@ - + - + + fill="#1E293B" id="slot-03_content-title-1" data-pptx-placeholder="title"> {{PAGE_TITLE}} @@ -31,11 +31,11 @@ + font-size="14" id="slot-03_content-body-2" data-pptx-placeholder="body"> {{CONTENT_AREA}} {{PAGE_NUM}} + text-anchor="end" id="slot-03_content-slide_number-3" data-pptx-placeholder="slide-number">{{PAGE_NUM}} diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/04_ending.svg b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/04_ending.svg index 3844879b..761967b5 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/04_ending.svg +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/04_ending.svg @@ -1,4 +1,4 @@ - + @@ -8,7 +8,7 @@ - + diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/design_spec.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/design_spec.md index 18ef99e8..4760085a 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/design_spec.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/layouts/psychology_attachment/design_spec.md @@ -1,6 +1,7 @@ --- layout_id: psychology_attachment kind: layout +native_structure_mode: template summary: Psychotherapy training, academic lectures, counseling case analysis, professional sharing. canvas_format: ppt169 page_count: 5 diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/spec_lock_reference.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/spec_lock_reference.md index e282ab40..f44f25d9 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/spec_lock_reference.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/spec_lock_reference.md @@ -37,6 +37,8 @@ > Strategist: fill only colors actually used. Add extra rows as needed; delete unused rows rather than leave as `#......`. > +> **PowerPoint theme roles.** Native `baseline` / `template` export maps `bg` / `background` / `master_bg` → `lt1`, `secondary_bg` / `bg_secondary` → `lt2`, `text` / `body_text` → `dk1`, `text_secondary` → `dk2`, `primary` → `accent1`, `accent` → `accent2`, `secondary_accent` → `accent3`, and `border` → `accent4`. The first two additional non-black/non-white roles become `accent5` / `accent6`; remaining colors stay fixed. Mapping is usage-aware, so a background HEX is not automatically reused for inverse text. Preserve mode keeps the imported source theme; flat mode keeps concrete colors for diagnostics. +> > **`image_rendering` and `image_palette`** — required only when `images` section below contains `ai`-sourced files. Values MUST be valid names from `references/image-renderings/_index.md` and `references/image-palettes/_index.md`, **or** the literal string `custom`. Image_Generator reads these and applies them deck-wide. Omit both rows when the deck has no AI-generated images. > > **`custom` escape hatch.** When set to `custom`, add a sibling `*_behavior` row carrying a one-paragraph prose description. Image_Generator splices the prose into the prompt in place of the preset file's fewshot snippet. Tail-case only — see [`image-renderings/_index.md`](../references/image-renderings/_index.md) §1.5 / [`image-palettes/_index.md`](../references/image-palettes/_index.md) §2 for invocation rules. @@ -73,7 +75,7 @@ > > **Size slots are anchors, not a closed menu.** Common slots (`title` / `subtitle` / `annotation`) cover frequent cases. Add role-specific slots (e.g. `cover_title: 88`, `hero_number: 56`, `subheading: 32`, `lead: 30`, `footnote: 16`, `chart_annotation: 16`) for the roles the deck actually uses — common for cover-heavy decks, consulting-style hero numbers, dense pages. **Mandatory — scan `§IX` and declare a slot for every role that recurs across pages, not just the four defaults.** A report / `text`-mode deck almost always recurs a per-page **core-message / lead line** and **page numbers / source credits / footnotes** → declare `lead` and `footnote` for them. `subheading` and `lead` sit between `subtitle` and `body` (their bands overlap `subtitle`) — pick by role, not size — and the core-message `lead` is a **primary** line, **always ≥ `body`**, never smaller. Leaving a recurring lead / footnote undeclared forces the Executor to improvise an unlocked size (and a core line improvised below `body` inverts the hierarchy). **Structural roles (title / body / subtitle / annotation / footnote) render at their locked size on every page — one role, one size, deck-wide.** Intermediate in-band sizes are for special / feature elements only (hero number, display title, one-off emphasis); declare a recurring one as its own slot so it stays consistent too. > -> **⚠️ PPT-safe stack discipline (HARD rule).** PPTX stores concrete exported Latin / EA typefaces per run with no runtime fallback. Every stack's exported Latin / EA typefaces MUST resolve to cross-platform pre-installed fonts: `"Microsoft YaHei"` / `SimSun` / `Arial` / `"Times New Roman"` / `Consolas`. Stacks that export non-preinstalled typefaces (Inter / Google Fonts / brand typefaces) may be used only when the Design Spec notes the font-install or embedding requirement. +> **⚠️ PPT-safe stack discipline (HARD rule).** Native `baseline` / `template` export maps `title_family` to the PowerPoint theme major font and `body_family` (or `font_family`) to the theme minor font. Runs whose resolved face matches either role use `+mj-*` / `+mn-*` theme tokens; other role families remain concrete per-run typefaces. Every exported Latin / EA face MUST therefore resolve to cross-platform pre-installed fonts: `"Microsoft YaHei"` / `SimSun` / `Arial` / `"Times New Roman"` / `Consolas`. Stacks that resolve to non-preinstalled typefaces (Inter / Google Fonts / brand typefaces) may be used only when the Design Spec notes the font-install or embedding requirement. `preserve` keeps the source template theme; `flat` keeps concrete fonts for diagnostics. > > **Stack length discipline.** 3-4 fonts per stack is the sweet spot. Converter only writes the **first** Latin and **first** CJK font into PPTX — everything after is silently dropped. macOS-only families (`Songti SC`, `Menlo`, `Monaco`, `Helvetica`) are auto-mapped to Windows equivalents via `FONT_FALLBACK_WIN` (see `scripts/svg_to_pptx/drawingml/utils.py`); stacking both is redundant. Lead with Windows-preinstalled fonts (`Microsoft YaHei` / `SimSun` / `Arial` / `Georgia` / `Consolas`); keep at most **one** macOS-exclusive family (typically `"PingFang SC"`) as a browser-preview nicety. @@ -120,18 +122,57 @@ > > **Missing or empty section** → Executor falls back to `dense` for every page (legacy pre-rhythm behavior). Remove the section only for legacy decks; new decks MUST fill it. +## pptx_structure +- mode: baseline + +> One deck-wide native PowerPoint structure policy. New projects always include this section. +> +> When Step 3 loaded a deck/layout template, add exactly one of: +> ``` +> - template_adherence: adaptive +> - template_adherence: strict +> ``` +> Omit the row for free design and brand-only templates. Both values require one `page_layouts` and one `pptx_layouts` row per page. `strict` keeps the selected Layout contract; `adaptive` may create a new explicit Layout under the same template Master. +> +> - `baseline` — default for free design and brand-only routes. Preserve conservative shared Master/background/chrome behavior, then assign Cover/Agenda/Section/Closing/Content from root `data-pptx-page-role`. It may also promote exact family-wide leading structurally marked chrome into a Layout. Marker-free legacy SVGs retain filename/id fallback. Actual content stays slide-local; no placeholders or visual-similarity inference are authored. +> - `template` — required whenever Step 3 loaded a deck/layout template. Requires complete `page_layouts` and `pptx_layouts` sections plus explicit SVG structure metadata on every generated page. +> - `preserve` — legacy strict-only compatibility for an existing project that already ships `native_structure.json` + `source_template.pptx`. Do not select it for newly created templates. +> - `flat` — diagnostic escape hatch. Do not lock this in a normal project; pass it on the CLI when comparing slide-local output. +> +> Preserve example: +> ``` +> - mode: preserve +> - template_adherence: strict +> - source_template: templates/source_template.pptx +> - native_structure: templates/native_structure.json +> ``` + +## pptx_layouts + +> Emit this section only when `pptx_structure.mode` is `template` or legacy `preserve`. Include exactly one row per generated page. Value format: ` | `. Strict template use copies the selected SVG key/name; adaptive use may declare a new stable key/name. Preserve uses the legacy native contract. +> +> Example: +> ``` +> - P01: cover | Cover +> - P02: title-content | Title and Content +> - P03: title-content | Title and Content +> - P04: section | Section Header +> ``` +> +> Reuse one key only when pages share the same static layout layer and placeholder contract. Do not generate one unique key per slide merely because every page has different content. A one-off cover/section layout is valid; content differences stay slide-local. + ## page_layouts - P01: 01_cover - P03: 02a_chapter - P04: 03a_content_abstract -> One entry per page **that uses a template SVG**. Key: `P` matching §IX. Value: the template's SVG basename without extension (e.g., `01_cover`, `03a_content_image_text`) — Executor resolves it to `templates/.svg`. Modern templates ship many content-page variants (`03a_content_abstract`, `03b_content_image_text`, `03c_content_three_items` …); the page-type → single-file mapping in `executor-base.md §1` no longer covers them, so this section is the per-page truth. +> For a deck/layout template route, include one entry per page. Key: `P` matching §IX. Value: the template SVG basename without extension. Strict inherits it unchanged; adaptive uses it as the architecture reference and may output a new `pptx_layouts` key. > -> **No entry for a page** → that page is free design (no template inheritance). Mixed decks are supported: e.g., cover/chapter pages inherit a template while content pages are free. +> **No entry for a page** is valid only when the entire route is free design or brand-only. It is an error in template mode. > -> **Hard rule**: Use both `page_layouts` and `page_charts` for the same page only when the layout template is a compatible shell for the chart. Do not assign a conflicting layout just to fill every page: a waterfall chart should not inherit a timeline layout, and KPI cards should not inherit a circle-diagram layout unless that is the intended visual structure. When no compatible layout exists, omit the page from `page_layouts`. +> **Hard rule**: Use both `page_layouts` and `page_charts` only with a compatible shell. Adaptive mode may start from a neutral content template and create a new explicit Layout; strict mode must choose an existing compatible Layout or revise the outline. > -> **Whole section omitted** → entire deck is free design. Equivalent to no rows but cleaner; do this when zero pages reference a template. +> **Whole section omitted** → free design or brand-only route. > > **Strategist source**: copy the per-page SVG choices from `design_spec.md §VI Page Roster` (or §IX outline if Roster is absent). Names must match files in `templates/` exactly — typos cause silent fallback to free design. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/create-template.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/create-template.md index 1f49d867..e4f3052c 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/create-template.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/create-template.md @@ -43,7 +43,7 @@ Branch by the type of reference source the user supplied. This step produces ana | Type | What the user supplied | Tool / read path | Replication modes available | |------|-------------------------|------------------|------------------------------| -| **A** `.pptx` reference | A `.pptx` file path | `pptx_template_import.py` → `manifest.json` + `svg/master_*.svg` + `svg/layout_*.svg` + `svg/slide_*.svg` + `svg-flat/slide_*.svg` + `assets/` | `standard` / `fidelity` / `mirror` | +| **A** `.pptx` reference | A `.pptx` file path | `pptx_template_import.py` → `manifest.json` + `native_structure.json` + `source_template.pptx` + layered/flat SVGs + `assets/` | `standard` / `fidelity` / `mirror` | | **B** Existing SVG assets | `projects//svg_output/`, `templates/layouts/`, or a loose `.svg` folder | `ls` + `Read` each `*.svg`; plus `design_spec.md` / `spec_lock.md` if present | `standard` / `fidelity` (AI visual clustering) / `mirror` (direct 1:1 copy) | | **C** Image / visual references | Screenshot folder, single image, PDF pages | `ls` + `Read` each file (multimodal visual recognition) | `standard` only | | **D** No reference source | Verbal description only ("McKinsey style", "tech blue", "dark minimal") | — | `standard` only | @@ -66,6 +66,8 @@ python3 skills/ppt-master/scripts/pptx_template_import.py "/svg" --icons-dir "/icons" --inplace --id-prefix layered --min-decoration-bytes 3000 --clean-stale -# mirror creation path +# mirror visual-copy path; also run the layered command above because Master/Layout +# ownership remains required for the rebuilt explicit structure python3 skills/ppt-master/scripts/extract_svg_assets.py "/svg-flat" --icons-dir "/icons" --inplace --id-prefix flat --min-decoration-bytes 3000 --clean-stale ``` @@ -102,16 +105,18 @@ The source SVGs in `/svg/` / `/svg-flat/` ar **Read order during analysis** (read everything below before composing Step 2): 1. `manifest.json` (factual metadata: slide size, theme, assets, layouts, masters, slide page-types) -2. cleaned `svg/master_*.svg` and `svg/layout_*.svg` — read these **before** any slide SVG; they show the deck's shared visual language (background, headers, footers, decorative bars). This is what the new template's fixed structure should adapt from. -3. `svg/inheritance.json` — confirms which slide uses which layout/master -4. exported `assets/` -5. cleaned slide SVG references from `svg/` — content unique to each slide; consult after the master/layout language is understood -6. `summary.md` only as a quick orientation aid -7. user-provided screenshots or the original PPTX only for visual cross-checking +2. `native_structure.json` (stable native keys, source layout names/placeholders, relationship completeness, recommended structure strategy) +3. cleaned `svg/master_*.svg` and `svg/layout_*.svg` — read these **before** any slide SVG; they show the deck's shared visual language (background, headers, footers, decorative bars). This is what the new template's fixed structure should adapt from. +4. `svg/inheritance.json` — confirms which slide uses which layout/master +5. exported `assets/` +6. cleaned slide SVG references from `svg/` — content unique to each slide; consult after the master/layout language is understood +7. `summary.md` only as a quick orientation aid +8. user-provided screenshots or the original PPTX only for visual cross-checking Interpretation rule (carries forward into Steps 2 and 4): - `manifest.json` is the source of truth for slide size, theme colors, fonts, background inheritance, reusable asset inventory, unique layout/master structure, and slide reuse relationships +- `native_structure.json` is the source of truth for source PowerPoint identity: stable layout keys, picker names, parent masters, placeholder types/indices, and the source-package hash. Use those facts to rebuild the explicit SVG contract; never retain the source package as the new template architecture. - `summary.md` is a quick scan; never treat it as the canonical fact source — go back to `manifest.json` if anything is unclear - exported `assets/` are the canonical reusable image pool — `` references in `svg/` already point at these files directly - exported `icons/*.svg` are the canonical reusable vector illustration pool, but they are **not** part of the default read set. Read the cleaned SVGs and `*_vector_asset_inventory.json` first; open a specific icon SVG only when the cleaned page or inventory shows that the extracted asset is relevant to the current design decision. This is what makes the SVG work surface smaller. @@ -133,20 +138,18 @@ Before composing Step 2, extract the template's reusable norms from the previous | Density rhythm | title scale, content block count, whitespace balance, dense vs. breathing pages | Page-type guidance for Strategist / Executor | | Page roster semantics | cover / TOC / chapter / content / ending variants and their intended content slots | `design_spec.md §V Page Roster` rows | | Asset policy | source images / icons / textures that are part of the template vs. sample-only content | `design_spec.md §VI Assets` or omit sample-only assets | +| Native PowerPoint structure | `native_structure.json` source facts, masters/layouts, picker names, placeholder type/index, source slide mapping | Rebuild one clean Master plus semantic Layouts in complete, explicitly annotated SVG pages. | Distinguish observed facts from template rules: "`slide_07` uses a left photo crop" is a fact; "content pages may use a left photo rail for location / product / case-study pages" is the reusable rule. -**Hard read gate** (`standard` / `fidelity` modes — `mirror` differs, see below): +**Hard read gate** (all replication modes): - The agent MUST finish reading every cleaned `master_*.svg`, `layout_*.svg`, and `slide_*.svg` file from the layered `svg/` view before moving on to Step 2 - The agent MUST list the read master / layout / slide filenames inside the Step 2 brief proposal as proof of the gate Do **not** treat the imported PPTX or exported slide SVGs as direct final template assets — Step 4 reconstructs them as a clean, maintainable PPT Master template package, not a 1:1 shape translation. -> **Mirror-mode fast path** — when the user has indicated mirror replication (verbatim copy of every source slide): -> - Run the vector illustration readability pass on `svg-flat/`, then read **only** the cleaned flat `slide_*.svg` files (the self-contained, what-PowerPoint-shows view) and `manifest.json` (for theme colors, fonts, asset inventory). -> - Skip `svg/master_*.svg` / `svg/layout_*.svg` / `svg/inheritance.json` — chrome / content separation is irrelevant in mirror mode (no placeholder insertion happens). -> - Mirror is explicitly a visual-verbatim copy flow: every slide becomes a template page as-is, except that large vector illustrations may be factored into `icons/` placeholders for maintainability. The "reconstruct, don't translate" rule applies to `standard` / `fidelity` only. +> **Mirror-mode visual path** — when the user selects mirror replication, use cleaned `svg-flat/slide_*.svg` for literal page appearance, but still read cleaned `svg/master_*.svg`, `svg/layout_*.svg`, and `svg/inheritance.json` to recover layer ownership. Mirror preserves page visuals; it does not bypass the explicit Master/Layout reconstruction contract. ### 1B. Existing SVG assets @@ -204,6 +207,7 @@ Compose a single message that surfaces every Required brief item to the user, ** | Theme mode | Recommended localized mode with English ID, plus available modes such as `light` / `dark` / `mixed` with localized explanations | | Canvas format | Recommended canvas, plus other supported formats from [`canvas-formats.md`](../references/canvas-formats.md) that fit the source aspect ratio or user intent. Always show the concrete pixel size and `viewBox`; do not treat two same-ratio formats such as `ppt169` (`1280x720`) and `banner` (`1920x1080`) as interchangeable. | | Replication mode | Recommended localized mode with English ID, plus all modes available for the current input type; list forbidden modes for type C / D as unavailable with localized reasons | +| Native structure reconstruction | For type A, summarize the source master/layout graph from `native_structure.json`, then state the fixed output policy: rebuild one clean Master plus explicit semantic Layouts (`template`). The source package is analysis input only. | | Visual fidelity for fixed pages | Recommended localized choice with English ID, plus both `literal` / `adapted` options when applicable | | Asset bundling | Recommended included assets, plus excluded candidate assets with a one-line reason when reference assets exist | @@ -219,6 +223,7 @@ Items to surface: | Theme mode | Yes | A: `[fact]` from `manifest.json` background colors. B: `[fact]` from SVG `fill`. C: `[suggested]` from visual estimate. D: `[decision]` | | Canvas format and dimensions | Yes | A/B: `[fact]` from slide size or SVG `width` / `height` / `viewBox`; show `canvas_format`, `canvas_width`, `canvas_height`, `canvas_viewbox`, and `source_viewbox`. C: `[suggested]` from image aspect ratio. D: `[decision]`, default `ppt169` (`1280x720`, `0 0 1280 720`) | | Replication mode | Yes | `[decision]` — `standard` always available; `fidelity` and `mirror` available for type A (canonical, manifest-anchored) and type B (AI visual clustering / direct 1:1 copy — see Step 1 caveats); reject `fidelity` / `mirror` upfront for type C / D | +| Native structure facts | Type A only | `[fact]` from `native_structure.json.strategy`: master/layout counts, source placeholder identities, multi-master status, and reason codes. Output remains rebuilt explicit SVG structure (`template`) for every replication mode. | | Visual fidelity for fixed pages | Yes for `standard` / `fidelity` when reference exists; **N/A for `mirror`** (mirror is implicitly literal) | `[decision]` — `literal` (preserve original geometry / decoration / sprite crops as-is; for cover / chapter / ending especially) or `adapted` (use the reference for tone/structure but allow design evolution). Different page types may take different settings | | Basic template norms | Yes when reference exists | `[fact]` / `[suggested]` — layout grammar, image system, density rhythm, page roster semantics, and asset policy extracted in Step 1 | | Reference source | Optional | already known if Step 1 ran | @@ -232,10 +237,11 @@ For type A, also include in this message: - the exact cleaned `master_*.svg`, `layout_*.svg`, `slide_*.svg` filenames you read from the layered `svg/` view (proof of the hard read gate) - a one-line summary of the master / layout structure you extracted +- the source structure facts, including master/layout counts, multi-master status, and reason codes; state that the final package will rebuild one clean Master plus semantic Layouts The user replies with corrections, additions, or "all good". -> **Persist the brief into `design_spec.md`**. When the Template_Designer writes `design_spec.md` in Step 4, declare a YAML frontmatter block at the top with the confirmed brief (`template_id`, `category`, `summary`, `keywords`, `primary_color`, `canvas_format`, `canvas_width`, `canvas_height`, `canvas_viewbox`, `source_viewbox`, `replication_mode`, etc.). `register_template.py` reads this in Step 6, so the brief flows directly into the index without the AI re-deriving it from prose. See Step 6 for the recommended frontmatter shape. +> **Persist the brief into `design_spec.md`**. When the Template_Designer writes `design_spec.md` in Step 4, declare a YAML frontmatter block at the top with the confirmed brief (`template_id`, `category`, `summary`, `keywords`, `primary_color`, `canvas_format`, `canvas_width`, `canvas_height`, `canvas_viewbox`, `source_viewbox`, `replication_mode`, `native_structure_mode`, etc.). `register_template.py` reads this in Step 6, so the brief flows directly into the index without the AI re-deriving it from prose. See Step 6 for the recommended frontmatter shape. --- @@ -283,6 +289,7 @@ If the input source is type A, pass the following internal package to the role: - finalized brief from Step 3 - `manifest.json` +- `native_structure.json` and `source_template.pptx` as read-only analysis inputs - `summary.md` (orientation only) - exported `assets/` - `*_vector_asset_inventory.json`, when the vector readability pass extracted assets; do not bulk-read `icons/*.svg` @@ -295,13 +302,17 @@ For type D, pass only the finalized brief. The role uses the analysis bundle to anchor objective facts such as theme colors, fonts, reusable backgrounds, common branding assets, layout grammar, image-placement rules, density rhythm, and page-slot semantics, then rebuilds the final SVG templates in a simplified, maintainable form. +**Native structure reconstruction**: Do not copy `native_structure.json` or `source_template.pptx` into the final template directory. Use their master/layout/placeholder facts to reconstruct one clean Master plus semantic Layouts through the explicit SVG metadata contract. Every emitted SVG is a complete standalone preview: declare its output Layout, repeat the identical Master layer, repeat the selected Layout layer for pages sharing a key, and keep actual content on the Slide with semantic placeholder markers. Add `data-pptx-role` only to structural page-frame objects whose package/page-number/animation behavior is not already expressed by specialized metadata; do not classify ordinary template content. + +Downstream, both template-adherence choices stay on `pptx_structure.mode: template`. `strict` preserves the selected template Layout contract. `adaptive` still references one template SVG per page, keeps the same Master contract, and may author a new explicit Layout key when the source roster has no suitable composition. It never falls back to baseline inside a template deck. + **Apply the visual-fidelity decision from Step 3**: pages marked `literal` (typically cover / chapter / ending) must reproduce the reference's geometry, decoration, and sprite-sheet crops as-is — "simplified, maintainable form" applies only to genuinely redundant structure, not to load-bearing layout. Pages marked `adapted` may use the reference for tone and structural rhythm but evolve the design. **Sprite-sheet preservation (do NOT simplify away)**: PPTX-exported assets are often sprite sheets — a single tall/large image referenced from multiple slides, each cropping a different region via nested `` wrappers around ``. This nesting is **load-bearing geometry**, not redundant structure. When rebuilding, preserve the exact `viewBox` crop and the outer `` placement for every image; do not flatten to a single `` with direct `x/y/width/height`. Verify by sampling: if any asset's pixel dimensions don't match the on-page display aspect, it is a sprite and the wrapper must stay. **Mirror-mode override** (type A or B): when `Replication mode: mirror`, this step is a **verbatim copy** rather than a reconstruction. The Template_Designer role: -1. **Copies the cleaned source pages** into the template directory **without reconstruction** — no content placeholder insertion, no decorative simplification, no chrome/content reorganization. The SVG that ships in the template is the cleaned source page, modulo filename changes, asset path rewrites, and the vector illustration placeholders created by the readability pass. +1. **Copies the cleaned source pages** into the template directory with literal visual fidelity — no content rewriting or decorative simplification. Then annotate the complete page with root Layout identity plus direct Master/Layout/placeholder metadata derived from the layered source. The SVG remains visually identical when opened alone, while export can deterministically reconstruct native structure. - Type A: source is the cleaned flat `/svg-flat/slide_NN.svg` - Type B: source is each cleaned `*.svg` in the analysis workspace when extraction ran; otherwise each `*.svg` in the input directory (already self-contained) 2. **Renames each file** using the source-order-first convention `_.svg`, where `` is the source-order index zero-padded to 3 digits and `` is typically `cover` / `toc` / `chapter` / `content` / `ending` (fall back to `content` when the type cannot be confidently classified). Examples: `001_cover.svg`, `002_toc.svg`, `003_content.svg`, ..., `050_ending.svg`. @@ -311,15 +322,16 @@ The role uses the analysis bundle to anchor objective facts such as theme colors - Type A: assets come from `/assets/` - Type B: resolve relative paths in source `` against the source SVG location and copy each unique asset; if the source already follows PPT Master conventions (assets co-located with SVGs in the same directory), copy the whole asset set and then rewrite paths 4. **Copies `icons/` when present** and preserves every extracted `` reference. Do not inline these assets manually in the template working SVGs; the shared icon embedding path owns re-inlining before export. -5. Writes `design_spec.md` per [template-designer.md](../references/template-designer.md) §1 — the **§V Page Roster description per page is the load-bearing artifact** because mirror has no placeholders to advertise the per-page contract. Mirror is only the template-creation replication mode; downstream generation still treats the finished template package as a selectable / reusable roster, not as a forced 1:1 slide sequence. +5. Writes `design_spec.md` per [template-designer.md](../references/template-designer.md) §1. The §V Page Roster remains the content-fit index; explicit SVG metadata is the native Master/Layout contract. Mirror is only the template-creation replication mode; downstream generation still treats the finished package as a selectable / reusable roster, not as a forced 1:1 slide sequence. -Mirror mode does **not** invoke the "reconstruct into clean SVG" pathway. The sprite-sheet preservation rule still applies (because the flat SVGs already contain the original sprite wrappers — do not flatten them when copying). Vector illustration placeholders are the only allowed maintainability rewrite. +Mirror mode does not simplify the visual page, but it still reconstructs layer ownership. The sprite-sheet preservation rule applies because flat SVGs already contain the original crop wrappers; do not flatten them when annotating the page. **Expected outputs from this step** (full spec → [template-designer.md](../references/template-designer.md)): 1. `design_spec.md` — **personality only**. Required sections: Template Overview, Color Scheme, Signature Design Elements, Page Roster (matching the actual SVG files on disk). Skip Typography / Assets / Placeholder Overrides when they would just restate defaults. Declare brief frontmatter for `register_template.py`. **Do not** restate generic SVG constraints, layout pattern libraries, font-size ratio bands, the canonical placeholder table, or content methodology — those are sourced from `shared-standards.md` / `design_spec_reference.md` / `strategist.md` and are already in the downstream reader's context. Full scope rule and skeleton: [template-designer.md §1](../references/template-designer.md#1-must-generate-design_specmd). 2. Page roster — see [Page Roster](../references/template-designer.md#page-roster) for `standard` / `fidelity` / `mirror` mode rosters, variant naming, and TOC handling 3. Placeholder vocabulary — pages should adopt the conventional names (`{{TITLE}}`, `{{CONTENT_AREA}}`, ...) when they fit. Full reference: [Placeholder Reference](../references/template-designer.md#4-placeholder-reference-canonical-convention-overridable-per-template). When a template style legitimately needs different vocabulary (consulting → `{{KEY_MESSAGE}}`, branded cover → `{{BRAND_LOGO}}`), declare a `placeholders:` block in `design_spec.md` frontmatter so the registrar and quality checker treat it as the template's authoritative contract. **Avoid** one-off indexed families such as `{{CHAPTER_01_TITLE}}` — use the indexed TOC pattern instead. + - `{{...}}` placeholders are the authoring vocabulary used to generate final slide content. Each emitted SVG also carries the native reconstruction contract: root `data-pptx-layout` / `data-pptx-layout-name`, direct Master/Layout layers, and direct semantic `data-pptx-placeholder` markers. Minimal structural `data-pptx-role` hints are added only when those specialized markers cannot express required behavior. Both strict and adaptive downstream use set `mode: template` and require `page_layouts` plus `pptx_layouts` for every page. 4. Template assets (optional) — Logos / PNG / JPG / reference SVG bundled with the template package --- @@ -343,6 +355,8 @@ python3 skills/ppt-master/scripts/svg_quality_checker.py "skills/ppt-master/temp - skip `spec_lock.md` drift checks (templates do not ship a spec_lock) - enforce roster ↔ `design_spec.md` consistency as **errors** (orphan files / missing files break the target kind's index) - emit advisory **warnings** when a page lacks a conventional placeholder — these are hints, not failures. Declare a `placeholders:` block in `design_spec.md` frontmatter to silence them when your template intentionally uses a different vocabulary +- require every SVG root to declare an output Layout and every SVG to contain at least one direct Master/Layout/placeholder declaration +- validate cross-page Master equality plus same-key Layout/placeholder equality **Checklist**: @@ -354,6 +368,8 @@ python3 skills/ppt-master/scripts/svg_quality_checker.py "skills/ppt-master/temp - [ ] SVG `viewBox` matches the declared canvas dimensions, not just the aspect ratio (for `ppt169`: `0 0 1280 720`; for `banner`: `0 0 1920 1080`); `width` / `height`, if written, equal it - [ ] Placeholder names follow the canonical convention where applicable; templates with intentionally different vocabularies (e.g. `{{KEY_MESSAGE}}` instead of `{{PAGE_TITLE}}`) should declare a `placeholders:` frontmatter block to silence advisory warnings - [ ] Asset files referenced by SVGs actually exist in the template package +- [ ] `design_spec.md` frontmatter declares `native_structure_mode: template`; no `native_structure.json` or `source_template.pptx` is packaged +- [ ] Every SVG root declares `data-pptx-layout` and `data-pptx-layout-name`; direct Master/Layout elements and semantic placeholders obey the explicit paint-order contract. Structural `data-pptx-role` is used only when specialized metadata cannot express required package/page-number/animation behavior - [ ] If any SVG references an extracted vector `data-icon`, the corresponding SVG asset exists directly under the package's `icons/` directory; do not add a separate illustration embedding script - [ ] For `fidelity` mode: every sprite-sheet asset retains its nested `` crop wrapper; no image whose file aspect differs from its on-page aspect was flattened to a bare `` - [ ] For `mirror` mode: file count equals source page count (type A: `ls templates///*_*.svg | wc -l` matches the cleaned flat `/svg-flat/slide_*.svg | wc -l`; type B: matches the source SVG count); filenames follow the `_.svg` convention; **no `{{...}}` placeholder strings appear in any copied SVG** (`grep -l "{{" templates///*.svg` should return nothing — if the type B source itself contains placeholders, the user should be in `standard` mode, not `mirror`); §V Page Roster in `design_spec.md` lists every emitted file with a one-line description of what the page contains and what content slot it suits @@ -401,6 +417,9 @@ The index file is a **discovery index** — it lets the AI answer "what template > source_canvas_width: 1280 > source_canvas_height: 720 > source_viewbox: "0 0 1280 720" +> # All current deck/layout templates rebuild an explicit SVG structure. +> # Downstream strict/adaptive use is confirmed by Strategist and is not stored here. +> native_structure_mode: template > page_count: 5 > primary_color: "#005587" > --- diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/routing.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/routing.md index ca5eab8f..738e2553 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/routing.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/workflows/routing.md @@ -22,6 +22,17 @@ Route selection authority for PPT Master. Use this file before entering the main **Forbidden - route-choice prompts**: Do not present multiple implementation paths when this file already defines the route. Ordinary style choices and finite options belong at the next existing confirmation gate. +### 1.1 SVG Page-Design Scope + +| Route family | SVG contract | +|---|---| +| Main SVG pipeline and `beautify-pptx` | Every visible output-page object is authored in the completed page SVG; templates and locks guide authoring but are not export-time visual overlays. | +| `create-template` | Each reusable template SVG is a complete visual reference plus explicit PowerPoint structure metadata. | +| `template-fill-pptx` and `native-enhance-pptx` | Native PPTX editing routes. They retain their OOXML contracts and are not forced through SVG. | +| Animation, transition, speaker-note, and narration workflows | Presentation behavior/content outside the visible page-design layer; keep their dedicated sidecars and package post-processing. | + +**Hard rule**: Apply SVG page-design closure only after selecting an SVG-authoring route. Do not reroute a native PPTX operation merely to make every package-level capability pass through SVG. + --- ## 2. Main Route Matrix diff --git a/plugins/codex/plugins/shadcn/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/shadcn/THIRD_PARTY_SOURCE.json index a70cad6a..aa0fcf46 100644 --- a/plugins/codex/plugins/shadcn/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/shadcn/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "shadcn", "repo": "https://github.com/shadcn-ui/ui.git", "ref": "main", - "commit": "21e4ceb94418096e21a7f1990027741a8f9b085d", + "commit": "3cdaa6eb2f0da27aca8598cb752c32d840e06940", "adapter": "claude-skill", "sourcePath": "skills/shadcn", - "syncedAt": "2026-07-09T15:59:59Z" + "syncedAt": "2026-07-10T16:00:00Z" } diff --git a/plugins/codex/plugins/ui-ux-pro-max/README.md b/plugins/codex/plugins/ui-ux-pro-max/README.md index fc24ba48..d36907d9 100755 --- a/plugins/codex/plugins/ui-ux-pro-max/README.md +++ b/plugins/codex/plugins/ui-ux-pro-max/README.md @@ -339,22 +339,16 @@ uipro uninstall --global # Remove from global install ## Prerequisites -Python 3.x is required for the search script. +Python 3.x is required for the search script (standard library only — the scripts install nothing and make no network calls). + +Check if Python is installed: ```bash -# Check if Python is installed python3 --version - -# macOS -brew install python3 - -# Ubuntu/Debian -sudo apt update && sudo apt install python3 - -# Windows -winget install Python.Python.3.12 ``` +If it is missing, install it yourself from [python.org](https://www.python.org/downloads/) or with your OS package manager (Homebrew, apt, winget). These install steps are for **you, the human user** — AI agents using this skill should never install software on your machine; they are instructed to ask you instead. + ## Usage ### Skill Mode (Auto-activate) @@ -600,23 +594,16 @@ Or wait for the next release where this is resolved. ### `npm install -g ui-ux-pro-max-cli` fails with permission error -```bash -# macOS/Linux — use a Node version manager (recommended) or sudo -sudo npm install -g ui-ux-pro-max-cli +Use a Node version manager (recommended), or skip the global install entirely: -# Or use npx without installing globally +```bash +# npx without installing globally npx ui-ux-pro-max-cli init --ai claude ``` ### Python not found when running design system commands -The search scripts require Python 3.x. Install it for your OS: - -```bash -brew install python3 # macOS -sudo apt install python3 # Ubuntu/Debian -winget install Python.Python.3.12 # Windows -``` +The search scripts require Python 3.x. Install it manually from [python.org](https://www.python.org/downloads/) or with your OS package manager (Homebrew, apt, winget). AI agents should not install it for you — they are instructed to ask you instead. ### Design system output is cut off / fields truncated 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 8b36ed03..bd47f5fe 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": "12b486b22e67f5d887962ef8351c1ac863bfaeb9", + "commit": "3da52ff1cab1be91848072ec1be5f493d730fd5f", "adapter": "claude-skill", "sourcePath": ".claude/skills/ui-ux-pro-max", - "syncedAt": "2026-07-06T16:00:00Z" + "syncedAt": "2026-07-10T16:00:00Z" } diff --git a/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/SKILL.md b/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/SKILL.md index 0a0d1153..0f203cda 100644 --- a/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/SKILL.md +++ b/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/SKILL.md @@ -11,9 +11,9 @@ Comprehensive design guide for web and mobile applications. Contains 50+ styles, This Skill should be used when the task involves **UI structure, visual design decisions, interaction patterns, or user experience quality control**. -### Must Use +### Primary Use Cases -This Skill must be invoked in the following situations: +This Skill is most useful in the following situations: - Designing new pages (Landing Page, Dashboard, Admin, SaaS, Mobile App) - Creating or refactoring UI components (buttons, modals, forms, tables, charts, etc.) @@ -307,28 +307,15 @@ Search specific domains using the CLI tool below. ## Prerequisites -Check if Python is installed: +The bundled scripts require Python 3 (standard library only — no third-party packages, no network access). Check if it is available: ```bash python3 --version || python --version ``` -If Python is not installed, install it based on user's OS: +If Python is not installed, **do not install it yourself**. Stop and ask the user to install Python 3 using their preferred method (e.g. from [python.org](https://www.python.org/downloads/) or their OS package manager), then continue once it is available. Never run package-manager or system-modifying commands (`sudo`, `brew`, `apt`, `winget`, etc.) on the user's machine for this skill. -**macOS:** -```bash -brew install python3 -``` - -**Ubuntu/Debian:** -```bash -sudo apt update && sudo apt install python3 -``` - -**Windows:** -```powershell -winget install Python.Python.3.12 -``` +If the user prefers not to install Python, skip the CLI searches and rely on the Quick Reference sections above. > **Note:** On Windows, use `python` instead of `python3` to run scripts (e.g., `python scripts/search.py` instead of `python3 scripts/search.py`). diff --git a/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/data/_sync_all.py b/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/data/_sync_all.py deleted file mode 100644 index 37f7c3ac..00000000 --- a/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/data/_sync_all.py +++ /dev/null @@ -1,414 +0,0 @@ -#!/usr/bin/env python3 -""" -Sync colors.csv and ui-reasoning.csv with the updated products.csv (161 entries). -- Remove deleted product types -- Rename mismatched entries -- Add new entries for missing product types -- Keep colors.csv aligned 1:1 with products.csv -- Renumber everything -""" -import csv, os, json - -BASE = os.path.dirname(os.path.abspath(__file__)) - -# ─── Color derivation helpers ──────────────────────────────────────────────── -def h2r(h): - h = h.lstrip("#") - return tuple(int(h[i:i+2], 16) for i in (0, 2, 4)) - -def r2h(r, g, b): - return f"#{max(0,min(255,int(r))):02X}{max(0,min(255,int(g))):02X}{max(0,min(255,int(b))):02X}" - -def lum(h): - r, g, b = [x/255.0 for x in h2r(h)] - r, g, b = [(x/12.92 if x<=0.03928 else ((x+0.055)/1.055)**2.4) for x in (r, g, b)] - return 0.2126*r + 0.7152*g + 0.0722*b - -def is_dark(bg): - return lum(bg) < 0.18 - -def on_color(bg): - return "#FFFFFF" if lum(bg) < 0.4 else "#0F172A" - -def blend(a, b, f=0.15): - ra, ga, ba = h2r(a) - rb, gb, bb = h2r(b) - return r2h(ra+(rb-ra)*f, ga+(gb-ga)*f, ba+(bb-ba)*f) - -def shift(h, n): - r, g, b = h2r(h) - return r2h(r+n, g+n, b+n) - -def derive_row(pt, pri, sec, acc, bg, notes=""): - """Generate full 16-token color row from 4 base colors.""" - dark = is_dark(bg) - fg = "#FFFFFF" if dark else "#0F172A" - on_pri = on_color(pri) - on_sec = on_color(sec) - on_acc = on_color(acc) - card = shift(bg, 10) if dark else "#FFFFFF" - card_fg = "#FFFFFF" if dark else "#0F172A" - muted = blend(bg, pri, 0.08) if dark else blend("#FFFFFF", pri, 0.06) - muted_fg = "#94A3B8" if dark else "#64748B" - border = f"rgba(255,255,255,0.08)" if dark else blend("#FFFFFF", pri, 0.12) - destr = "#DC2626" - on_destr = "#FFFFFF" - ring = pri - return [pt, pri, on_pri, sec, on_sec, acc, on_acc, bg, fg, card, card_fg, muted, muted_fg, border, destr, on_destr, ring, notes] - -# ─── Rename maps ───────────────────────────────────────────────────────────── -COLOR_RENAMES = { - "Quantum Computing": "Quantum Computing Interface", - "Biohacking / Longevity": "Biohacking / Longevity App", - "Autonomous Systems": "Autonomous Drone Fleet Manager", - "Generative AI Art": "Generative Art Platform", - "Spatial / Vision OS": "Spatial Computing OS / App", - "Climate Tech": "Sustainable Energy / Climate Tech", -} -UI_RENAMES = { - "Architecture/Interior": "Architecture / Interior", - "Autonomous Drone Fleet": "Autonomous Drone Fleet Manager", - "B2B SaaS Enterprise": "B2B Service", - "Biohacking/Longevity App": "Biohacking / Longevity App", - "Biotech/Life Sciences": "Biotech / Life Sciences", - "Developer Tool/IDE": "Developer Tool / IDE", - "Education": "Educational App", - "Fintech (Banking)": "Fintech/Crypto", - "Government/Public": "Government/Public Service", - "Home Services": "Home Services (Plumber/Electrician)", - "Micro-Credentials/Badges": "Micro-Credentials/Badges Platform", - "Music/Entertainment": "Music Streaming", - "Quantum Computing": "Quantum Computing Interface", - "Real Estate": "Real Estate/Property", - "Remote Work/Collaboration": "Remote Work/Collaboration Tool", - "Restaurant/Food": "Restaurant/Food Service", - "SaaS Dashboard": "Analytics Dashboard", - "Space Tech/Aerospace": "Space Tech / Aerospace", - "Spatial Computing OS": "Spatial Computing OS / App", - "Startup Landing": "Micro SaaS", - "Sustainable Energy/Climate": "Sustainable Energy / Climate Tech", - "Travel/Tourism": "Travel/Tourism Agency", - "Wellness/Mental Health": "Mental Health App", -} - -REMOVE_TYPES = { - "Service Landing Page", "Sustainability/ESG Platform", - "Cleaning Service", "Coffee Shop", - "Consulting Firm", "Conference/Webinar Platform", -} - -# ─── New color definitions: (primary, secondary, accent, bg, notes) ────────── -# Grouped by category for clarity. Each tuple generates a full 16-token row. -NEW_COLORS = { - # ── Old #97-#116 that never got colors ── - "Todo & Task Manager": ("#2563EB","#3B82F6","#059669","#F8FAFC","Functional blue + progress green"), - "Personal Finance Tracker": ("#1E40AF","#3B82F6","#059669","#0F172A","Trust blue + profit green on dark"), - "Chat & Messaging App": ("#2563EB","#6366F1","#059669","#FFFFFF","Messenger blue + online green"), - "Notes & Writing App": ("#78716C","#A8A29E","#D97706","#FFFBEB","Warm ink + amber accent on cream"), - "Habit Tracker": ("#D97706","#F59E0B","#059669","#FFFBEB","Streak amber + habit green"), - "Food Delivery / On-Demand": ("#EA580C","#F97316","#2563EB","#FFF7ED","Appetizing orange + trust blue"), - "Ride Hailing / Transportation":("#1E293B","#334155","#2563EB","#0F172A","Map dark + route blue"), - "Recipe & Cooking App": ("#9A3412","#C2410C","#059669","#FFFBEB","Warm terracotta + fresh green"), - "Meditation & Mindfulness": ("#7C3AED","#8B5CF6","#059669","#FAF5FF","Calm lavender + mindful green"), - "Weather App": ("#0284C7","#0EA5E9","#F59E0B","#F0F9FF","Sky blue + sun amber"), - "Diary & Journal App": ("#92400E","#A16207","#6366F1","#FFFBEB","Warm journal brown + ink violet"), - "CRM & Client Management": ("#2563EB","#3B82F6","#059669","#F8FAFC","Professional blue + deal green"), - "Inventory & Stock Management":("#334155","#475569","#059669","#F8FAFC","Industrial slate + stock green"), - "Flashcard & Study Tool": ("#7C3AED","#8B5CF6","#059669","#FAF5FF","Study purple + correct green"), - "Booking & Appointment App": ("#0284C7","#0EA5E9","#059669","#F0F9FF","Calendar blue + available green"), - "Invoice & Billing Tool": ("#1E3A5F","#2563EB","#059669","#F8FAFC","Navy professional + paid green"), - "Grocery & Shopping List": ("#059669","#10B981","#D97706","#ECFDF5","Fresh green + food amber"), - "Timer & Pomodoro": ("#DC2626","#EF4444","#059669","#0F172A","Focus red on dark + break green"), - "Parenting & Baby Tracker": ("#EC4899","#F472B6","#0284C7","#FDF2F8","Soft pink + trust blue"), - "Scanner & Document Manager": ("#1E293B","#334155","#2563EB","#F8FAFC","Document grey + scan blue"), - # ── A. Utility / Productivity ── - "Calendar & Scheduling App": ("#2563EB","#3B82F6","#059669","#F8FAFC","Calendar blue + event green"), - "Password Manager": ("#1E3A5F","#334155","#059669","#0F172A","Vault dark blue + secure green"), - "Expense Splitter / Bill Split":("#059669","#10B981","#DC2626","#F8FAFC","Balance green + owe red"), - "Voice Recorder & Memo": ("#DC2626","#EF4444","#2563EB","#FFFFFF","Recording red + waveform blue"), - "Bookmark & Read-Later": ("#D97706","#F59E0B","#2563EB","#FFFBEB","Warm amber + link blue"), - "Translator App": ("#2563EB","#0891B2","#EA580C","#F8FAFC","Global blue + teal + accent orange"), - "Calculator & Unit Converter": ("#EA580C","#F97316","#2563EB","#1C1917","Operation orange on dark"), - "Alarm & World Clock": ("#D97706","#F59E0B","#6366F1","#0F172A","Time amber + night indigo on dark"), - "File Manager & Transfer": ("#2563EB","#3B82F6","#D97706","#F8FAFC","Folder blue + file amber"), - "Email Client": ("#2563EB","#3B82F6","#DC2626","#FFFFFF","Inbox blue + priority red"), - # ── B. Games ── - "Casual Puzzle Game": ("#EC4899","#8B5CF6","#F59E0B","#FDF2F8","Cheerful pink + reward gold"), - "Trivia & Quiz Game": ("#2563EB","#7C3AED","#F59E0B","#EFF6FF","Quiz blue + gold leaderboard"), - "Card & Board Game": ("#15803D","#166534","#D97706","#0F172A","Felt green + gold on dark"), - "Idle & Clicker Game": ("#D97706","#F59E0B","#7C3AED","#FFFBEB","Coin gold + prestige purple"), - "Word & Crossword Game": ("#15803D","#059669","#D97706","#FFFFFF","Word green + letter amber"), - "Arcade & Retro Game": ("#DC2626","#2563EB","#22C55E","#0F172A","Neon red+blue on dark + score green"), - # ── C. Creator Tools ── - "Photo Editor & Filters": ("#7C3AED","#6366F1","#0891B2","#0F172A","Editor violet + filter cyan on dark"), - "Short Video Editor": ("#EC4899","#DB2777","#2563EB","#0F172A","Video pink on dark + timeline blue"), - "Drawing & Sketching Canvas": ("#7C3AED","#8B5CF6","#0891B2","#1C1917","Canvas purple + tool teal on dark"), - "Music Creation & Beat Maker": ("#7C3AED","#6366F1","#22C55E","#0F172A","Studio purple + waveform green on dark"), - "Meme & Sticker Maker": ("#EC4899","#F59E0B","#2563EB","#FFFFFF","Viral pink + comedy yellow + share blue"), - "AI Photo & Avatar Generator": ("#7C3AED","#6366F1","#EC4899","#FAF5FF","AI purple + generation pink"), - "Link-in-Bio Page Builder": ("#2563EB","#7C3AED","#EC4899","#FFFFFF","Brand blue + creator purple"), - # ── D. Personal Life ── - "Wardrobe & Outfit Planner": ("#BE185D","#EC4899","#D97706","#FDF2F8","Fashion rose + gold accent"), - "Plant Care Tracker": ("#15803D","#059669","#D97706","#F0FDF4","Nature green + sun yellow"), - "Book & Reading Tracker": ("#78716C","#92400E","#D97706","#FFFBEB","Book brown + page amber"), - "Couple & Relationship App": ("#BE185D","#EC4899","#DC2626","#FDF2F8","Romance rose + love red"), - "Family Calendar & Chores": ("#2563EB","#059669","#D97706","#F8FAFC","Family blue + chore green"), - "Mood Tracker": ("#7C3AED","#6366F1","#D97706","#FAF5FF","Mood purple + insight amber"), - "Gift & Wishlist": ("#DC2626","#D97706","#EC4899","#FFF1F2","Gift red + gold + surprise pink"), - # ── E. Health ── - "Running & Cycling GPS": ("#EA580C","#F97316","#059669","#0F172A","Energetic orange + pace green on dark"), - "Yoga & Stretching Guide": ("#6B7280","#78716C","#0891B2","#F5F5F0","Sage neutral + calm teal"), - "Sleep Tracker": ("#4338CA","#6366F1","#7C3AED","#0F172A","Night indigo + dream violet on dark"), - "Calorie & Nutrition Counter": ("#059669","#10B981","#EA580C","#ECFDF5","Healthy green + macro orange"), - "Period & Cycle Tracker": ("#BE185D","#EC4899","#7C3AED","#FDF2F8","Blush rose + fertility lavender"), - "Medication & Pill Reminder": ("#0284C7","#0891B2","#DC2626","#F0F9FF","Medical blue + alert red"), - "Water & Hydration Reminder": ("#0284C7","#06B6D4","#0891B2","#F0F9FF","Refreshing blue + water cyan"), - "Fasting & Intermittent Timer":("#6366F1","#4338CA","#059669","#0F172A","Fasting indigo on dark + eating green"), - # ── F. Social ── - "Anonymous Community / Confession":("#475569","#334155","#0891B2","#0F172A","Protective grey + subtle teal on dark"), - "Local Events & Discovery": ("#EA580C","#F97316","#2563EB","#FFF7ED","Event orange + map blue"), - "Study Together / Virtual Coworking":("#2563EB","#3B82F6","#059669","#F8FAFC","Focus blue + session green"), - # ── G. Education ── - "Coding Challenge & Practice": ("#22C55E","#059669","#D97706","#0F172A","Code green + difficulty amber on dark"), - "Kids Learning (ABC & Math)": ("#2563EB","#F59E0B","#EC4899","#EFF6FF","Learning blue + play yellow + fun pink"), - "Music Instrument Learning": ("#DC2626","#9A3412","#D97706","#FFFBEB","Musical red + warm amber"), - # ── H. Transport ── - "Parking Finder": ("#2563EB","#059669","#DC2626","#F0F9FF","Available blue/green + occupied red"), - "Public Transit Guide": ("#2563EB","#0891B2","#EA580C","#F8FAFC","Transit blue + line colors"), - "Road Trip Planner": ("#EA580C","#0891B2","#D97706","#FFF7ED","Adventure orange + map teal"), - # ── I. Safety & Lifestyle ── - "VPN & Privacy Tool": ("#1E3A5F","#334155","#22C55E","#0F172A","Shield dark + connected green"), - "Emergency SOS & Safety": ("#DC2626","#EF4444","#2563EB","#FFF1F2","Alert red + safety blue"), - "Wallpaper & Theme App": ("#7C3AED","#EC4899","#2563EB","#FAF5FF","Aesthetic purple + trending pink"), - "White Noise & Ambient Sound": ("#475569","#334155","#4338CA","#0F172A","Ambient grey + deep indigo on dark"), - "Home Decoration & Interior Design":("#78716C","#A8A29E","#D97706","#FAF5F2","Interior warm grey + gold accent"), -} - -# ─── 1. REBUILD colors.csv ─────────────────────────────────────────────────── -def rebuild_colors(): - src = os.path.join(BASE, "colors.csv") - with open(src, newline="", encoding="utf-8") as f: - reader = csv.DictReader(f) - headers = reader.fieldnames - existing = list(reader) - - # Build lookup: Product Type -> row data - color_map = {} - for row in existing: - pt = row.get("Product Type", "").strip() - if not pt: - continue - # Remove deleted types - if pt in REMOVE_TYPES: - print(f" [colors] REMOVE: {pt}") - continue - # Rename mismatched types - if pt in COLOR_RENAMES: - new_name = COLOR_RENAMES[pt] - print(f" [colors] RENAME: {pt} → {new_name}") - row["Product Type"] = new_name - pt = new_name - color_map[pt] = row - - # Read products.csv to get the correct order - with open(os.path.join(BASE, "products.csv"), newline="", encoding="utf-8") as f: - products = list(csv.DictReader(f)) - - # Build final rows in products.csv order - final_rows = [] - added = 0 - for i, prod in enumerate(products, 1): - pt = prod["Product Type"] - if pt in color_map: - row = color_map[pt] - row["No"] = str(i) - final_rows.append(row) - elif pt in NEW_COLORS: - pri, sec, acc, bg, notes = NEW_COLORS[pt] - new_row = derive_row(pt, pri, sec, acc, bg, notes) - d = dict(zip(headers, [str(i)] + new_row)) - final_rows.append(d) - added += 1 - else: - print(f" [colors] WARNING: No color data for '{pt}' - using defaults") - new_row = derive_row(pt, "#2563EB", "#3B82F6", "#059669", "#F8FAFC", "Auto-generated default") - d = dict(zip(headers, [str(i)] + new_row)) - final_rows.append(d) - added += 1 - - # Write - with open(src, "w", newline="", encoding="utf-8") as f: - writer = csv.DictWriter(f, fieldnames=headers) - writer.writeheader() - writer.writerows(final_rows) - - product_count = len(products) - print(f"\n ✅ colors.csv: {len(final_rows)} rows ({product_count} products)") - print(f" Added: {added} new color rows") - -# ─── 2. REBUILD ui-reasoning.csv ───────────────────────────────────────────── -def derive_ui_reasoning(prod): - """Generate ui-reasoning row from products.csv row.""" - pt = prod["Product Type"] - style = prod.get("Primary Style Recommendation", "") - landing = prod.get("Landing Page Pattern", "") - color_focus = prod.get("Color Palette Focus", "") - considerations = prod.get("Key Considerations", "") - keywords = prod.get("Keywords", "") - - # Typography mood derived from style - typo_map = { - "Minimalism": "Professional + Clean hierarchy", - "Glassmorphism": "Modern + Clear hierarchy", - "Brutalism": "Bold + Oversized + Monospace", - "Claymorphism": "Playful + Rounded + Friendly", - "Dark Mode": "High contrast + Light on dark", - "Neumorphism": "Subtle + Soft + Monochromatic", - "Flat Design": "Bold + Clean + Sans-serif", - "Vibrant": "Energetic + Bold + Large", - "Aurora": "Elegant + Gradient-friendly", - "AI-Native": "Conversational + Minimal chrome", - "Organic": "Warm + Humanist + Natural", - "Motion": "Dynamic + Hierarchy-shifting", - "Accessible": "Large + High contrast + Clear", - "Soft UI": "Modern + Accessible + Balanced", - "Trust": "Professional + Serif accents", - "Swiss": "Grid-based + Mathematical + Helvetica", - "3D": "Immersive + Spatial + Variable", - "Retro": "Nostalgic + Monospace + Neon", - "Cyberpunk": "Terminal + Monospace + Neon", - "Pixel": "Retro + Blocky + 8-bit", - } - typo_mood = "Professional + Clear hierarchy" - for key, val in typo_map.items(): - if key.lower() in style.lower(): - typo_mood = val - break - - # Key effects from style - eff_map = { - "Glassmorphism": "Backdrop blur (10-20px) + Translucent overlays", - "Neumorphism": "Dual shadows (light+dark) + Soft press 150ms", - "Claymorphism": "Multi-layer shadows + Spring bounce + Soft press 200ms", - "Brutalism": "No transitions + Hard borders + Instant feedback", - "Dark Mode": "Subtle glow + Neon accents + High contrast", - "Flat Design": "Color shift hover + Fast 150ms transitions + No shadows", - "Minimalism": "Subtle hover 200ms + Smooth transitions + Clean", - "Motion-Driven": "Scroll animations + Parallax + Page transitions", - "Micro-interactions": "Haptic feedback + Small 50-100ms animations", - "Vibrant": "Large section gaps 48px+ + Color shift hover + Scroll-snap", - "Aurora": "Flowing gradients 8-12s + Color morphing", - "AI-Native": "Typing indicator + Streaming text + Context reveal", - "Organic": "Rounded 16-24px + Natural shadows + Flowing SVG", - "Soft UI": "Improved shadows + Modern 200-300ms + Focus visible", - "3D": "WebGL/Three.js + Parallax 3-5 layers + Physics 300-400ms", - "Trust": "Clear focus rings + Badge hover + Metric pulse", - "Accessible": "Focus rings 3-4px + ARIA + Reduced motion", - } - key_effects = "Subtle hover (200ms) + Smooth transitions" - for key, val in eff_map.items(): - if key.lower() in style.lower(): - key_effects = val - break - - # Decision rules - rules = {} - if "dark" in style.lower() or "oled" in style.lower(): - rules["if_light_mode_needed"] = "provide-theme-toggle" - if "glass" in style.lower(): - rules["if_low_performance"] = "fallback-to-flat" - if "conversion" in landing.lower(): - rules["if_conversion_focused"] = "add-urgency-colors" - if "social" in landing.lower(): - rules["if_trust_needed"] = "add-testimonials" - if "data" in keywords.lower() or "dashboard" in keywords.lower(): - rules["if_data_heavy"] = "prioritize-data-density" - if not rules: - rules["if_ux_focused"] = "prioritize-clarity" - rules["if_mobile"] = "optimize-touch-targets" - - # Anti-patterns - anti_patterns = [] - if "minimalism" in style.lower() or "minimal" in style.lower(): - anti_patterns.append("Excessive decoration") - if "dark" in style.lower(): - anti_patterns.append("Pure white backgrounds") - if "flat" in style.lower(): - anti_patterns.append("Complex shadows + 3D effects") - if "vibrant" in style.lower(): - anti_patterns.append("Muted colors + Low energy") - if "accessible" in style.lower(): - anti_patterns.append("Color-only indicators") - if not anti_patterns: - anti_patterns = ["Inconsistent styling", "Poor contrast ratios"] - anti_str = " + ".join(anti_patterns[:2]) - - return { - "UI_Category": pt, - "Recommended_Pattern": landing, - "Style_Priority": style, - "Color_Mood": color_focus, - "Typography_Mood": typo_mood, - "Key_Effects": key_effects, - "Decision_Rules": json.dumps(rules), - "Anti_Patterns": anti_str, - "Severity": "HIGH" - } - - -def rebuild_ui_reasoning(): - src = os.path.join(BASE, "ui-reasoning.csv") - with open(src, newline="", encoding="utf-8") as f: - reader = csv.DictReader(f) - headers = reader.fieldnames - existing = list(reader) - - # Build lookup - ui_map = {} - for row in existing: - cat = row.get("UI_Category", "").strip() - if not cat: - continue - if cat in REMOVE_TYPES: - print(f" [ui-reason] REMOVE: {cat}") - continue - if cat in UI_RENAMES: - new_name = UI_RENAMES[cat] - print(f" [ui-reason] RENAME: {cat} → {new_name}") - row["UI_Category"] = new_name - cat = new_name - ui_map[cat] = row - - with open(os.path.join(BASE, "products.csv"), newline="", encoding="utf-8") as f: - products = list(csv.DictReader(f)) - - final_rows = [] - added = 0 - for i, prod in enumerate(products, 1): - pt = prod["Product Type"] - if pt in ui_map: - row = ui_map[pt] - row["No"] = str(i) - final_rows.append(row) - else: - row = derive_ui_reasoning(prod) - row["No"] = str(i) - final_rows.append(row) - added += 1 - - with open(src, "w", newline="", encoding="utf-8") as f: - writer = csv.DictWriter(f, fieldnames=headers) - writer.writeheader() - writer.writerows(final_rows) - - print(f"\n ✅ ui-reasoning.csv: {len(final_rows)} rows") - print(f" Added: {added} new reasoning rows") - - -# ─── MAIN ──────────────────────────────────────────────────────────────────── -if __name__ == "__main__": - print("=== Rebuilding colors.csv ===") - rebuild_colors() - print("\n=== Rebuilding ui-reasoning.csv ===") - rebuild_ui_reasoning() - print("\n🎉 Done!") diff --git a/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/data/design.csv b/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/data/design.csv deleted file mode 100644 index e6775ce3..00000000 --- a/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/data/design.csv +++ /dev/null @@ -1,1776 +0,0 @@ -Bauhaus(包豪斯) -大胆的几何现代主义,包含圆形、方形和三角形。主色调(红/蓝/黄),边缘鲜明,阴影强烈。功能性与艺术性兼备,带有建构主义的不对称。 -1. 极简主义品牌官网与电商 -2. 工具类与高效率 App -3. 数字媒体与在线杂志 - - -Design Style: Bauhaus (Mobile) -1. Design Philosophy -The Bauhaus style embodies the revolutionary principle "form follows function" while celebrating pure geometric beauty and primary color theory. On mobile, this translates to tactile constructivism—the screen is a canvas where elements are physically stacked blocks. The aesthetic creates a high-impact, poster-like experience in the palm of the hand: bold, touch-centric, and unapologetically graphic. -Vibe: Tactile, Constructivist, Geometric, High-Contrast, Bold, Architectural -Core Concept: The mobile interface is a vertical geometric composition. Scroll views are not just content streams but stacked layers of colored paper and rigid shapes. The limited screen real estate forces a focus on massive typography and distinct color blocking (Red #D02020, Blue #1040C0, Yellow #F0C020). Every tap should feel mechanical and substantial. -Key Characteristics: -● Geometric Purity: UI elements are strictly circles (buttons/avatars) or squares (cards/inputs). -● Tactile Depth: Hard shadows indicate interactivity; elements look "pressable." -● Color Blocking: distinct screen sections use solid primary colors to separate content without whitespace. -● Thick Borders: 2px and 3px black borders ensure legibility and distinct separation on small screens. -● Vertical Rhythm: Strong vertical stacking with deliberate spacing, avoiding clutter. -● Constructivist Typography: Headlines are massive (text-4xl to text-5xl) relative to screen width, breaking traditional mobile scaling rules. -● Thumb-Friendly: Interactive zones are large, distinct, and geometrically defined. -2. Design Token System (The DNA) -Colors (Single Palette - Light Mode) -The palette remains strictly Bauhaus primaries, optimized for high contrast outdoors. -● background: #F0F0F0 (Off-white canvas) -● foreground: #121212 (Stark Black) -● primary-red: #D02020 (Bauhaus Red) -● primary-blue: #1040C0 (Bauhaus Blue) -● primary-yellow: #F0C020 (Bauhaus Yellow) -● border: #121212 (Thick, distinct borders) -● muted: #E0E0E0 -Typography -● Font Family: 'Outfit' (geometric sans-serif). -● Font Import: Outfit:wght@400;500;700;900 -● Scaling: Aggressive scaling adapted for vertical viewports. - ○ Display: text-4xl → text-5xl (Massive headers taking up 30-40% of screen width) - ○ Subheadings: text-xl → text-2xl - ○ Body: text-base (16px minimum for legibility) - ○ Button Text: text-lg (Large for readability) -● Weights: - ○ Headlines: font-black (900) uppercase, tight tracking (tracking-tighter) - ○ Buttons/Nav: font-bold (700) uppercase, wide tracking (tracking-wide) - ○ Body: font-medium (500) -● Line Height: Tight (leading-none) for headlines to save vertical space; leading-relaxed for body text. -Radius & Border -● Radius: Strict Binary—rounded-none (0px) for layout blocks, inputs, and cards; rounded-full (9999px) for primary action buttons and avatars. -● Border Widths: - ○ Standard Elements: border-2 (2px) - ○ Major Containers/Bottom Nav: border-t-2 or border-3 - ○ Separators: divide-y-2 -● Border Color: Always #121212 (black). -Shadows/Effects -● Hard Offset Shadows (Tactile feedback): - ○ Small elements (Tags/Icons): shadow-[2px_2px_0px_0px_black] - ○ Buttons/Cards: shadow-[4px_4px_0px_0px_black] (Reduced from web 8px to save screen width) - ○ Floating Action Button (FAB): shadow-[5px_5px_0px_0px_black] -● Touch Feedback: active:translate-x-[2px] active:translate-y-[2px] active:shadow-none (Instant mechanical depression). -● Patterns: - ○ Dot grid backgrounds on "Paper" colored sections (background-size: 16px 16px). - ○ Screen transitions: Slide-over with hard black borders. -3. Component Stylings -Buttons (Touch Targets) -● Minimum Size: Height h-12 or h-14 (48px-56px) for thumb accessibility. -● Variants: - ○ Primary (CTA): w-full bg-[#D02020] text-white border-2 border-black shadow-[4px_4px_0px_0px_black] - ○ Secondary: w-full bg-[#1040C0] text-white border-2 border-black shadow-[4px_4px_0px_0px_black] - ○ Floating Action (FAB): h-14 w-14 rounded-full bg-[#F0C020] border-2 border-black shadow-[4px_4px_0px_0px_black] flex items-center justify-center -● Shapes: Full-width rectangular buttons (rounded-none) or pill-shaped (rounded-full) for bottom-sticky actions. -● States: NO hover. Focus on active state (press down effect). -Cards (Mobile Stack) -● Base Style: White background, border-2 border-black, shadow-[4px_4px_0px_0px_black], mb-6. -● Decoration: - ○ Geometric badge in top-right: absolute top-0 right-0 h-8 w-8 bg-[#F0C020] border-l-2 border-b-2 border-black flex items-center justify-center. -● Interaction: Entire card is a touch target. active:translate-x-[1px] active:translate-y-[1px] active:shadow-[2px_2px_0px_0px_black]. -● Content: Image usually takes top half (aspect-video), bold text below. -Bottom Navigation (The Anchor) -● Container: fixed bottom-0 w-full bg-white border-t-2 border-black z-50 h-20. -● Grid: 3 to 5 items equally spaced. -● Items: - ○ Inactive: Black stroke icon, clear background. - ○ Active: Icon inside a geometric shape (Square/Circle) with primary color fill (e.g., Red Square context). - ○ Label: Tiny uppercase bold text below icon, or icon only for minimalism. -Inputs & Forms -● Field: h-12 bg-white border-2 border-black rounded-none px-4 text-black placeholder:text-gray-400 focus:bg-[#FFF9C4] focus:ring-0 focus:border-black. -● Label: Uppercase bold, mb-1 block. -● Checkbox: h-6 w-6 appearance-none border-2 border-black bg-white checked:bg-[#1040C0] rounded-none. -4. Layout & Spacing -● Container: w-full with px-5 (20px) padding. Max-width constraints removed (fluid mobile). -● Section Padding: - ○ Standard: py-8 - ○ Hero: pt-12 pb-16 -● Grid Systems: - ○ Main Layout: Single column (Stack). - ○ Micro-grids: 2-column for stats/gallery (gap-3 or gap-4). -● Safe Areas: Respect pt-safe (top notch) and pb-safe (bottom home indicator). -● Dividers: Thick section separators border-b-2 border-black usually paired with a background color change. -5. Non-Genericness (Bold Choices) -Mobile Constraints require distinct personality to avoid looking like a wireframe: -● Full-Screen Color Washes: - ○ Onboarding screens: Full solid backgrounds (Red Screen → Blue Screen → Yellow Screen). - ○ Success states: Full Yellow background (bg-[#F0C020]) with massive black centered checkmark. - ○ Error states: Full Red background (bg-[#D02020]) with white text. -● Header Identity: - ○ Instead of a standard navbar, use a "Bauhaus Header Block": A row containing a Square (Menu), a flexible space for the Title (Left aligned, massive), and a Circle (Profile/Search). All elements separated by vertical borders divide-x-2 border-b-2 border-black. -● Geometric Lists: - ○ List items aren't just text lines. They are alternating geometric bullets: Line 1 uses a ■, Line 2 uses a ●, Line 3 uses a ▲. -● Rotated "Stickers": - ○ "New", "Sale", or Notification badges are rotated -12deg or +12deg, sitting on top of corners to break the rigid grid. -● Image Filters: - ○ Thumbnails: Grayscale + High Contrast. - ○ Detail View: Full color, no rounded corners, thick black border. -6. Icons & Imagery -● Icon Library: lucide-react-native or similar (Circle, Square, Triangle, Menu, X, ArrowLeft). -● Icon Style: - ○ Stroke: stroke-[2px]. - ○ Size: w-6 h-6 (standard) or w-8 h-8 (hero actions). -● Icon Containers: - ○ Navigation icons often live inside rigid 48x48px bordered squares. - ○ Back buttons are always circular rounded-full with a border. -● Imagery: - ○ Masking: Use CSS clip-paths to crop header images into non-standard shapes (e.g., a trapezoid or a circle cropped at the bottom) within the mobile frame. -7. Responsive Strategy (Device Sizes) -● Small Phones (SE/Mini): - ○ Reduce display text to text-3xl. - ○ Stack stats vertically (1-col). - ○ Reduce padding to px-4. -● Large Phones (Max/Plus): - ○ Display text scales to text-5xl. - ○ Stats can use 2-col grid. - ○ Card images gain more height. -● Orientation: - ○ Portrait (Primary): Vertical stacking. - ○ Landscape: Avoid if possible, or split screen 50/50 (Text Left / Image Right). -8. Animation & Micro-Interactions -● Feel: Physical, snappy, zero-latency. -● Duration: duration-150 (Very fast). -● Easing: ease-in-out (Sharp stops). -● Interactions: - ○ Tap: The element physically depresses (translate X/Y matches shadow size). - ○ Drawer Open: Slides in from Left/Right with a solid black border line leading it. No soft fades—it slides like a mechanical door. - ○ Toast/Alerts: Drop down from top as a solid geometric block (Yellow/Red) with hard borders, swinging slightly like a hanging sign. - ○ Scroll: Sticky headers snap into place instantly. - - - - -"Monochrome(单色/黑白) -"一套简洁的编辑设计体系,建立在纯粹的黑白之上。没有点缀色——只有戏剧性的对比、超大衬线字体和精准的几何布局。让人联想到高端时尚专题和建筑作品集。严肃、成熟、毫不掩饰地大胆。 -"1. 极简主义电商与高端奢侈品 -2. 专业创意与文档编辑工具 -3. 数据密集型仪表盘(局部应用) -4. 实验性与先锋感网站" - - -Design Style: Minimalist Monochrome (Mobile) -Design Philosophy -Core Principle -The Pocket Editorial. On mobile, Minimalist Monochrome transforms the screen into a tactile, high-end printed manifesto. It rejects the "app-like" tendency for soft bubbles and friendly gradients. Instead, it offers a stark, vertical journey defined by pure black (#000000), pure white (#FFFFFF), and razor-sharp borders. Every tap is a deliberate decision; every scroll is a flip of a page in a luxury monograph. -Visual Vibe -Emotional Keywords: Tactile, Austere, Editorial, Direct, High-Fidelity, Uncompromising, Sharp, Rhythmic. -This is the visual language of: -● Mobile typographers and digital brutalism -● The mobile interfaces of luxury fashion houses (Balenciaga, Zara, SSENSE) -● Digital exhibitions where the content frames itself -● High-contrast e-reader aesthetics -What This Design Is NOT -● ❌ "App-y" (no cards with drop shadows, no floating bubbles) -● ❌ Native iOS/Android standard (no system blue, no rounded groups) -● ❌ Gesture-heavy (interactions are explicit taps, not vague swipes) -● ❌ Cluttered (one idea per screen view) -● ❌ Colorful (strictly grayscale) -The DNA of Minimalist Monochrome (Mobile) -1. Vertical Linearity -The mobile screen is a continuous roll of paper. Structure is created not by boxes, but by horizontal lines (rules) that span the full width of the device. Content lives between these lines. -2. Typography as Interface -Buttons are often just large words. Navigation is text-based. The serif typeface (Playfair Display) acts as the primary image on the screen. Headlines must be large enough to break words onto new lines, creating graphic shapes. -3. The "Touch" Inversion -Since there is no "hover" on mobile, interaction is communicated through inversion. When a user touches a white block, it turns black instantly. This zero-latency binary feedback replaces physical tactility. -4. Zero Radius, Zero Fluff -All elements—buttons, images, inputs, modals—have strictly 0px border radius. This sharp geometry cuts through the rounded physical corners of modern smartphones, creating a striking contrast. - -Design Token System -Colors (Strictly Monochrome) -background: #FFFFFF (Pure white) -foreground: #000000 (Pure black) -muted: #F5F5F5 (Off-white for "pressed" states or subtle blocks) -mutedForeground: #525252 (Dark gray for metadata) -border: #000000 (Black borders - heavy usage) -borderLight: #E5E5E5 (Subtle dividers) -overlay: #000000 (Full screen menu background) - -Rule: No accent colors. The "Active State" is simply the inverse of the "Default State." -Typography -Font Stack: -● Display: "Playfair Display", serif - For all headlines and large numerals. -● Body: "Source Serif 4", serif - For reading text. -● UI/Labels: "JetBrains Mono", monospace - For tiny tags, dates, and technical specs. -Type Scale (Mobile Optimized): -Note: Sizes are calibrated to feel "massive" on a small screen without breaking layout. -xs: 0.75rem (12px) - Metadata / Breadcrumbs -sm: 0.875rem (14px) - UI Labels / Captions -base: 1rem (16px) - Body text (Legibility minimum) -lg: 1.125rem (18px) - Lead text / Button text -xl: 1.5rem (24px) - Section headers -2xl: 2rem (32px) - Standard Headlines -3xl: 2.5rem (40px) - Hero Sub-text -4xl: 3rem (48px) - Major Headlines -5xl: 4rem (64px) - "Cover" text (Often wraps) -6xl: 5rem (80px) - Numerical statements - -Tracking & Leading: -● Headlines: tracking-tighter (-0.05em) and leading-[0.9]. Text should feel tightly packed. -● Body: leading-relaxed for readability. -● Button Text: tracking-widest uppercase. -Border Radius -ALL VALUES: 0px - -Constraint: Even the bottom sheet (modal) must be square. Even the active state of a tapped element is a sharp rectangle. -Borders & Lines (The Grid) -hairline: 1px solid #E5E5E5 (List separators) -thin: 1px solid #000000 (Standard element borders) -thick: 2px solid #000000 (Emphasis / Input bottom) -heavy: 4px solid #000000 (Section dividers) - -Usage: -● Full-Bleed Lines: Borders should often touch the edges of the screen (-mx-4 or -mx-6). -● Separators: Use border-b black heavily to separate stacked vertical content. -Shadows -NONE - -Depth is strictly 2D. Layers are defined by opacity (100% vs 0%) or borders, never by shadows. -Textures & Patterns -Use subtle noise to simulate high-quality paper texture on mobile backgrounds to avoid a "sterile" feeling. -Mobile Noise (CSS): -CSS - -background-image: url("data:image/svg+xml,..."); /* Same noise SVG as web */ -opacity: 0.03; /* Slightly higher opacity for small screens */ - - -Component Stylings -Buttons & Touch Targets -Primary Button (The Block): -- Width: w-full (Full width strongly preferred) -- Height: h-14 (56px - Large touch target) -- Background: #000000 -- Text: #FFFFFF -- Radius: 0px -- Typography: Uppercase, Mono or Serif, Tracking-widest -- Active State: Instantly inverts to White bg, Black text - -Secondary Button (The Outline): -- Width: w-full -- Height: h-14 -- Background: Transparent -- Border: 1px solid #000000 -- Active State: Instantly fills Black - -Sticky Bottom Action (CTA): -● A fixed bar at the bottom of the viewport. -● border-t-2 border-black. -● Contains a single primary action or a price + action pair. -● Background: #FFFFFF (or inverted #000000). -Cards / List Items -The "Stacked" Card: -Instead of a box with padding, mobile cards are often full-width segments separated by borders. -- Container: Border-bottom 1px solid #000000 -- Padding: py-6 -- Image: Aspect ratio 4:5 or 1:1, full width or padded -- Content: Text sits directly below image -- Interaction: Touching anywhere in the segment triggers "Active" state - -Navigation -The "Bar": -● Top: Minimal. Just a logo (Left) and a "MENU" text button or Hamburger (Right). -● Bottom: Optional. If used, simple text labels or thin outline icons. border-t-2 border-black. -The Menu Overlay: -● Full screen. -● Background: #000000 (Black). -● Text: #FFFFFF (White). -● List items: Massive text (text-4xl), serif, centered or left-aligned. -● Divider: Thin white lines between menu items. -Inputs -Mobile Input: -- Style: Flush with background. -- Border: Bottom only (2px solid black). -- Radius: 0px. -- Height: h-14. -- Focus: Border becomes 4px thick. No native blue glow. -- Clear Button: Simple 'X' icon in black. - - -Layout Strategy -Safe Areas -● Respect pb-safe (Home Indicator) and pt-safe (Status Bar). -● Style Note: The Status Bar should be white text on black header, or black text on white header. High contrast. -Container & Spacing -● Horizontal Padding: px-5 (20px) or px-6 (24px). -● Vertical Rhythm: - ○ Small gaps: py-4 - ○ Section gaps: py-16 - ○ Between text and image: my-6 -The "Scroll" -● Design for the scroll. Use sticky headers for section titles (e.g., "Details", "Shipping") that stack as the user scrolls down, creating a filing cabinet effect. -● Use divide-y divide-black for lists to create strong separation. - -Effects & Animation -Motion Philosophy: Cut, Don't Fade. -Mobile transitions should feel mechanical and instant, like a shutter clicking. -Interactions: -● Tap: active:bg-black active:text-white (Instant inversion). -● Page Transition: Slide in from right (100% width) with a hard edge. No distinct shadow on the sliding page, just a clean line. -● Modal: Slides up from bottom. 0px radius. Covers 100% or 90% of screen. Black border top. -● Scroll Parallax: None. Keep scrolling tied 1:1 to finger movement. -Specific Implementation: -TypeScript - -// Mobile Button Active State -className="bg-black text-white active:bg-white active:text-black active:border active:border-black transition-none" - -// Image Loading -className="opacity-0 data-[loaded=true]:opacity-100 transition-opacity duration-300 ease-linear grayscale data-[loaded=true]:grayscale-0" - - -Iconography -Style: Stroke-based, Thin, Sharp. -Library: Lucide React (or similar). -Mobile Specifics: -● Size: w-6 h-6 (Standard) -● Stroke: 1.5px (Matches the fine aesthetic) -● Touch: If an icon is a button, it must have a p-3 (12px) padding hit-box around it, even if the border is invisible. - -Responsive Strategy (Device Sizes) -Small Phones (iPhone SE/Mini): -● Headlines: Scale down to text-4xl to prevent breaking single words into nonsense. -● Padding: Reduce to px-4. -Large Phones (Max/Plus): -● Headlines: text-5xl or text-6xl. -● Layout: Can introduce a 2-column grid for product thumbnails (but keep strict borders between them). -Dark Mode: -● System: Enforce Light Mode (Black on White) as the default brand identity. -● Inverted Sections: Use Dark Mode (White on Black) only for specific sections (Footer, Hero, Special Features) to create rhythm, not based on system settings. - -Bold Choices (Non-Negotiable) -1. Massive Typographic Hero: The opening screen must feature a headline where a single word might span the full width. -2. The "Hard" Line: A 4px black line (border-b-4) must separate the Hero from the content. -3. Sticky Section Headers: As you scroll, the section title (e.g., "01. INFO") sticks to the top with a bottom border, stacking on top of the previous one. -4. No Hamburger Icon: Use the word "MENU" in JetBrains Mono instead of an icon, if space permits. -5. Inverted Gallery: Image galleries have a Black background with images at full opacity. -6. Editorial Inputs: Form labels look like subheadings (Playfair Display, Italic). -7. No Skeletons: Loading states are simple spinning black lines or just whitespace. No gray pulsing blobs. -8. Mechanical Feedback: Every interactive element MUST have a visible active state (color inversion). - -What Success Looks Like (Mobile) -A successfully implemented Minimalist Monochrome mobile design should feel like: -● A digitally printed receipt from a high-end boutique. -● A pocket edition of a brutally honest manifesto. -● Clean, fast, and remarkably legible outdoors. -● Intimidatingly simple. -It should NOT feel like: -● A scaled-down version of a desktop site. -● A standard Bootstrap/Tailwind mobile layout. -● An app trying to be "friendly." - - - - -Modern Dark(现代深色模式) -一种电影般的高精度暗模式设计,通过动画渐变斑点、鼠标跟踪聚光灯效果和精心制作的微交互实现分层环境照明,感觉就像高级软件。 -"1. 影音娱乐与流媒体平台 -2. 开发者工具与专业生产力软件 -3. 金融、科技与 AI 仪表盘 -4. 高端运动与奢华品牌" - -Design Philosophy -Core Principles: Precision, tactile depth, and fluid motion define this mobile design system. For React Native, every component is treated as a physical layer within a stacked 3D space, illuminated by virtual light sources. The design communicates "high-end mobile utility"—as responsive and refined as the Linear mobile app or Apple’s native interface. Every shadow uses multiple layers, every gradient is dithered to prevent banding, and every interaction uses precise haptic feedback combined with Expo.out easing. -Vibe: Cinematic Technical Minimalism. It’s a "Dark Mode First" aesthetic, utilizing near-blacks (#050506) and deep indigos. Imagine a high-end pro tool interface viewed through frosted glass at night. It is technical but inviting, using soft "glow" sources (#5E6AD2) to guide the thumb. It should feel like a premium native iOS/Android app, not a web-wrapper. -Differentiation: The signature is Atmospheric Depth. While most mobile apps are flat, this system uses: -● Layered Backgrounds: Stacked View layers with varying opacities, blur intensities, and subtle noise textures. -● Animated Ambient Light: Large, slow-moving blurred Canvas objects (using react-native-skia or Animated) that simulate light pools. -● Haptic-Linked Interactions: Every "Pressable" event is paired with subtle haptics (Impact Light/Medium) to reinforce the "hardware" feel. -● Spring-less Precision: Eschewing "bouncy" animations for sophisticated Bezier(0.16, 1, 0.3, 1) curves. - -Design Token System (The DNA) -Color Strategy: Deep Space & Ambient Light -Token -Value -Usage -bg-deep -#020203 -Screen background, Bottom Tab bar base -bg-base -#050506 -Primary view container background -bg-elevated -#0a0a0c -Modals, Bottom sheets, Surface cards -surface -rgba(255,255,255,0.05) -Card backgrounds, list items -foreground -#EDEDEF -Primary text (High contrast) -foreground-muted -#8A8F98 -Secondary labels, body text -accent -#5E6AD2 -Buttons, active icons, primary brand glows -accent-glow -rgba(94,106,210,0.2) -Ambient shadows and light emission -border-default -rgba(255,255,255,0.08) -Subtle hairline dividers (0.5pt on iOS) - -Typography System (React Native) -Font Family: Inter, GeistSans-Regular, or System San Francisco/Roboto. -Level -Size -Weight -Letter Spacing -Display -48 -700 --1.5 -H1 -32 -600 --0.5 -H2 -24 -600 --0.5 -H3 -18 -600 -0 -Body -16 -400 -0 -Label/Mono -12 -500 -1.2 (Uppercase) -Gradient Text: Implement using mask-view and react-native-linear-gradient. Headers should transition from #FFFFFF to rgba(255,255,255,0.7) vertically. - -Component Styling Principles -1. The Background System -Never use a single solid color for the screen. -● Base: A LinearGradient from #0a0a0f at the top to #020203 at the bottom. -● The "Blobs": Use AbsoluteFill views with borderRadius equal to half their width, high blurRadius (30-50), and low opacity (0.1). -● Animation: Use useAnimatedStyle from Reanimated to slowly oscillate the translateX and translateY of these light pools. -2. Cards & Containers -● Radius: Always borderRadius: 16. -● Border: Use borderWidth: StyleSheet.hairlineWidth with rgba(255,255,255,0.1). -● Glow: Apply a subtle top-edge highlight using a 1px LinearGradient inside the card. -● Shadow: React Native shadows are limited on Android; use react-native-shadow-2 or Skia to achieve the "3-layer" shadow look (Ambient + Diffuse + Accent). -3. Buttons (Pressables) -● Primary: Background #5E6AD2. Text #FFFFFF. -● Interaction: On onPressIn, scale to 0.97. On onPressOut, scale back to 1.0. Use Selection haptic feedback. -● Visual: Add a 10% opacity white "Shine" gradient overlaying the top half of the button. -4. Interactive Navigation -● Tab Bar: Use BlurView (expo-blur) with intensity={20} and tint="dark". -● Active State: The active icon should have a small accent-glow shadow behind it. -● Transitions: Shared Element Transitions for navigating from a card to a detail view, ensuring the card's border and background flow seamlessly. - -Layout & Motion -Spacing Scale -● Base Unit: 4pt. -● Screen Margins: 20pt (Standard) or 16pt (Tight). -● Gaps: Use gap (available in modern RN) with values of 8, 12, 16, or 24. -Animation Specs -● Easing: Easing.bezier(0.16, 1, 0.3, 1) (Expo Out). -● Duration: * Micro-interactions (Toggle/Press): 200ms. - ○ Screen Transitions: 400ms. - ○ Modals (Bottom Sheet): 500ms with custom spring (damping: 20, stiffness: 90). - -Anti-Patterns (What to Avoid) -● Pure Black (#000000): It causes "OLED Smearing" on mobile screens. Stick to #050506. -● Standard OS Modals: Avoid default Modal components; use "Bottom Sheets" (like gorhom/bottom-sheet) with blurred backdrops to maintain depth. -● Full Opacity Borders: Never use solid grey borders. Always use rgba white/black for natural blending. -● Laggy Blobs: Do not use too many blurRadius effects on the main JS thread. Always use useNativeDriver: true or Skia. - -The "Bold Factor" (Signature Elements) -1. Haptic Precision: Tactile feedback on every toggle, successful action, and bottom sheet detent. -2. Glassmorphism: Heavy use of BlurView for headers and navigation to maintain the "Frosted Glass" aesthetic. -3. Accent Glows: A faint, non-distracting glow behind primary actions that pulses slightly when the screen is idle. -4. Bento Layouts: For dashboards, use Flexbox to create asymmetric cards (e.g., one card taking 60% width, the other 40% with different heights). - - -SaaS(软件即服务) -一个大胆、极简主义的现代视觉系统,将简洁的美学与动态的执行相结合。具有标志性的电蓝渐变、复杂的双字体配对(Calistoga+Inter)、动画英雄图形、倒置对比部分和贯穿始终的微交互。专业而前卫的设计——自信而不杂乱。 -1. 业务管理与协作系统 (B2B / Operations) -2. 开发者工具与云服务平台 -3. 企业内部工具与人力资源管理 (HRM) -4. 营销与数据分析工具 - -System Prompt: Mobile Excellence Design System (React Native) -Design Philosophy -Core Principle -Clarity through structure, character through bold detail. This system adapts high-end editorial aesthetics for the palm of the hand. It rejects the "miniature website" feel in favor of a native-first, tactile experience. -Whitespace is a precision instrument for thumb-driven navigation. Motion is not decoration; it is spatial feedback. Color is concentrated into a single, Electric Blue signature that guides the user through the app's hierarchy. -The Visual Vibe -"High-Tech Boutique." Imagine a premium fintech app combined with a modern design portfolio. It feels engineered, yet artistic; minimal, yet alive. -● Confident: Bold typography and vibrant accents. -● Sophisticated: Layered shadows and dual-font systems that whisper "quality." -● Tactile: Haptic-ready interactions and fluid spring animations. -● Premium: Generous "safe areas" and elevated surfaces. - -The DNA of This Style -1. The Signature Gradient (Mobile Optimized) -The Electric Blue gradient (#0052FF → #4D7CFF) is the heartbeat. In React Native, this is implemented via react-native-linear-gradient. -● Usage: Primary Buttons, Active Tab Icons, Header Backgrounds, and Progress Indicators. -● Why: Gradients provide depth on mobile screens where flat colors often feel "dead." -2. Physical Depth & Living Elements -Mobile is a 3D space. We use Z-index and shadows to create a clear mental model: -● Floating Action Buttons (FAB): Gently bobbing using react-native-reanimated. -● Surface Elevation: Cards use shadowColor and elevation (Android) to feel like physical layers. -● Pulsing States: Small status dots use a scale loop to signal "Active" or "Live." -3. Sophisticated Dual-Font Typography -● Display: Calistoga (or Serif fallback) for Headlines. It adds human warmth to the digital interface. -● UI/Body: Inter (or System Sans-Serif). The workhorse for readability. -● Technical: JetBrains Mono for labels and data points. -4. Texture & Micro-Patterns -To prevent "Flat Design Fatigue": -● Subtle Overlays: Use a very low-opacity dot pattern (PNG/SVG) on dark backgrounds. -● Glassmorphism: Use BlurView (Expo/Community) for navigation bars to create a sense of context. - -Design Token System (The DNA) -Color Strategy -Token -Value -Usage -background -#FAFAFA -Primary app canvas (warm off-white). -foreground -#0F172A -Deep slate for primary text and dark sections. -muted -#F1F5F9 -Secondary surfaces (gray fills). -accent -#0052FF -Primary actions and brand touchpoints. -accent-sec -#4D7CFF -Gradient endpoint. -card -#FFFFFF -Pure white for elevated components. -border -#E2E8F0 -Hairline dividers (0.5pt to 1pt). -Typography Scale -● Hero (H1): 36pt - 42pt | Calistoga | Leading 1.1 -● Section (H2): 28pt - 32pt | Calistoga | Leading 1.2 -● Body: 16pt - 18pt | Inter | Leading 1.5 -● Label: 12pt | JetBrains Mono | Uppercase | Letter Spacing 1.5 - -Component Specifications (React Native) -1. Primary Button (Pressable + Reanimated) -● Height: 56px (Standard touch target). -● Radius: 16px (Rounded-2xl). -● Interaction: * On onPressIn: Scale down to 0.96. - ○ On onPressOut: Spring back to 1.0. -● Style: Linear Gradient background with shadow-accent. -2. The Section Badge -A consistent pattern for orienting the user: -JavaScript - -// Structure - - - SECTION NAME - - -3. Content Cards -● Border: 1pt hairline in Slate-200. -● Shadow: * iOS: shadowOpacity: 0.1, shadowRadius: 10, shadowOffset: {width: 0, height: 4}. - ○ Android: elevation: 4. -● Padding: 24px (Consistent gutter). - -Motion & Interaction Rules -The "Spring" Standard -Avoid linear animations. Use Spring Config for all transitions: -● mass: 1, damping: 15, stiffness: 120. -Entrance Animations -● Staggered Fade-In: Content should slide up (Y: 20 -> 0) and fade in (Opacity: 0 -> 1) as the screen mounts. -● Layout Transitions: Use LayoutAnimation or Reanimated's entering prop for seamless list updates. - -Implementation Instructions for AI -1. Strict Styling: Use StyleSheet.create or a utility-first library like NativeWind (Tailwind for RN). -2. Safe Areas: Always wrap root content in SafeAreaView. -3. Touch Targets: Ensure all interactive elements are at least 44x44px. -4. Icons: Use Lucide-React-Native or Expo Vector Icons (Feather/Ionicons). -5. Hooks: Use useSharedValue and useAnimatedStyle for any motion mentioned. -Example Signature Block: -When generating a screen, always include a "Design Note" explaining how the Asymmetry and Gradient Accent are applied to ensure it doesn't look like a generic template. - - -Terminal(终端/命令行) -一种原始的、功能性的、复古的、未来主义的命令行界面美学。高对比度、等宽精度和闪烁光标。 -1. 开发者工具与极客类应用 -2. 区块链、Web3 与 加密货币项目 -3. 科幻、侦探与解谜类游戏 (ARG) -4. 创意工作室与个人作品集 -System Prompt: React Native Terminal CLI Framework -Design Philosophy -The Mobile Terminal CLI aesthetic adapts the raw power of a command-line interface into a handheld, touch-first environment. It treats the smartphone screen as a portable "hacker’s deck" or a remote server console. It is utilitarian, high-contrast, and uncompromisingly rigid. -The vibe is "Field Operative" or "System Administrator on the go." Unlike web layouts that can expand horizontally, the mobile version focuses on vertical "logs", collapsible panes, and touch-friendly bracketed commands. -Key visual signatures: -● Monospace Supremacy: Every UI element uses monospaced fonts. Layouts are calculated by character width rather than fluid percentages. -● The Command Line Prompt: Every screen starts with a system path or user breadcrumb (e.g., ~/root/user/home). -● Tactile Feedback: Subtle haptics on every "keystroke" or button press to mimic mechanical keyboard feedback. -● ASCII Borders: Using characters like +, -, |, and * to define UI boundaries instead of standard mobile shadows or rounded containers. - -Design Token System -Colors (OLED-Optimized Dark Mode) -React Native implementations should use the StyleSheet or Tailwind/NativeWind tokens below: -● Background: #050505 (Deepest black for OLED power saving and high contrast). -● Foreground: - ○ Primary: #33FF00 (Classic Matrix Green). - ○ Secondary: #FFB000 (Terminal Amber for warnings/toggles). - ○ Muted: #1A3D1A (Low-intensity green for inactive text/guides). - ○ Error: #FF3333 (Command Failed Red). -● Border: #33FF00 (1px solid green for terminal windows). -Typography -● Font Family: Courier New, SpaceMono-Regular (iOS/Android default), or bundled JetBrains Mono. -● Size: Strict 12pt, 14pt, or 16pt. No "in-between" sizes. -● Weight: Normal (monospaced fonts lose their character when too bold). -● Line Height: Tight. 1.2x font size to maximize information density on small screens. -Radius & Borders -● Radius: 0. Standard React Native borderRadius: 0. No exceptions. -● Borders: borderWidth: 1. Style: solid. - -Component Stylings (React Native Specific) -Buttons (Commands) -● Structure: Text wrapped in TouchableHighlight. Displayed as [ EXECUTE ] or > PROCEED. -● Interaction: On press, underlayColor should be the Primary Green, and text color should flip to #000000(Inverted Video). -● Haptics: Trigger Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light) on press. -Cards (Process Windows) -● Structure: A  with borderWidth: 1 and borderColor: primary. -● Title Bar: A top row with inverted colors: [ TITLE: SYSTEM_LOG ]. -● Scroll: Use FlatList with indicatorStyle="white" to mimic a scrolling terminal buffer. -Inputs (The Caret) -● Style: No background. Starts with a prefix: $ or >. -● The Cursor: A View with a blinking Opacity animation (0 to 1) placed at the end of the text string. -● Keyboard: Set keyboardType="ascii-capable" and autoCapitalize="none". - -Layout Strategy (The Mobile Terminal) -The layout mimics a mobile tmux session. -● Vertical Stacking: Use Flexbox with flexDirection: 'column'. Screens should feel like a continuous stream of data. -● Separators: Create a  component that renders a string of dashes: --------------------------. -● Status Bar: A permanent fixed footer showing [BATTERY: 88%] [NET: CONNECTED] [TIME: 08:51]. - -Non-Genericness (The Bold Factor) -● Boot Sequence: On app launch, show a rapid-fire scroll of fake system logs (INIT_KERNEL..., LOADING_UI...) before the main content appears. -● ASCII Graphics: Use Text components for headers instead of PNG icons. -Plaintext - - _ _ ____ ____ -( \/ )( _ \( _ \ - \ / ) _ ( ) / - \/ (____/(_)\_) - -● Progress Indicators: No ActivityIndicator. Use text-based bars: [#####-----] 50%. - -Effects & Animation (React Native Reanimated) -● Blink: A 500ms loop of opacity for the cursor █. -● Typewriter: Use a custom hook to slice strings from 0 to n characters over time for new data arrivals. -● Scanlines: A top-level  or absolute  with a repeating horizontal line pattern at 0.05opacity. - -Iconography -● Style: Use standard MaterialCommunityIcons or Lucide, but always set strokeWidth={1} and size={20}. -● Implementation: Icons should be treated as "Characters." They must be the same color as the text they accompany. - -Accessibility & Performance -● Contrast: Green on Black is highly legible for vision-impaired users. -● Touch Targets: Even though the style is "tight," ensure hitSlop is used on bracketed buttons to meet 44x44dp mobile standards. -● Reduced Motion: Respect AccessibilityInfo.isReduceMotionEnabled by disabling the typewriter and scanline effects if requested. - -Kinetic(动力学/动态) -运动优先设计,排版是主要的视觉媒介。具有无限选框、视口缩放文本、滚动触发动画和激进的大写样式。高对比度的野兽派能量,有节奏的动作。 -1. 沉浸式叙事与品牌官网 (Storytelling) -2. 具有强引导需求的复杂流程 (Guidance) -3. 数据可视化与实时监控 (Data Visualization) -4. 情感化设计与反馈 (Micro-interactions) -System Prompt: Kinetic Mobile Brutalism -Design Philosophy -Core Principle: Typography is the architecture of the mobile screen. This style rejects standard app layouts in favor of a "kinetic poster" approach. Text is treated as a graphic element; headers are hero units. Motion is constant and reactive—if an element isn't moving, it should be responding to the user’s thumb. Every interaction feels heavy, tactile, and high-contrast. -Aesthetic Vibe: High-energy street brutalism. Precision meets raw power. It is an underground zine optimized for a 6-inch screen. Everything is uppercase, oversized, and uncomfortably bold. It screams rather than whispers. Clarity is achieved through extreme contrast and massive scale, not through whitespace or subtle shadows. -Visual DNA: Relentless motion and aggressive scale. Numbers tower over labels. Scrolling isn't just movement—it's a performance. Use hard edges ($0px$ radius), sharp $2px$ borders, and instant color flips. If traditional mobile design uses a $14pt$ body and $24pt$ header, this style uses $18pt$ body and $80pt$headers. -Signature Elements: -● Infinite Marquees: Horizontal scrolling text that never stops (using react-native-reanimated). -● Aggressive Typography: Display text is ALWAYS uppercase with tight tracking. -● Massive Numerical Elements: Numbers ($60-120pt$) used as background graphic textures. -● Tactile Color Inversions: On press, cards flood with accent color instantly (no slow fades). -● Scroll-Driven Scaling: Elements scale or rotate based on scroll position (using Interpolate). -● Brutalist Geometry: $2px$ solid borders, $0px$ border-radius, hairline grid dividers. - -Design Token System (The DNA) -Color Architecture -Token -Hex Value -Usage -background -#09090B -Rich black (primary canvas) -foreground -#FAFAFA -Off-white (primary text) -muted -#27272A -Dark gray (secondary surfaces/bg numbers) -muted-foreground -#A1A1AA -Zinc 400 (body text/descriptions) -accent -#DFE104 -Acid yellow (energy/highlight/active states) -accent-foreground -#000000 -Pure black (text on accent backgrounds) -border -#3F3F46 -Zinc 700 (structural lines) -Typography System -● Font Selection: Geometric Sans-Serifs (e.g., Space Grotesk, Inter, or System Bold). -● Scale Hierarchy: - ○ Hero/Display: $60pt$ to $120pt$ (use PixelRatio for scaling). - ○ Section Headings: $40pt$ to $50pt$. - ○ Card Titles: $28pt$ to $32pt$. - ○ Body/Descriptions: $18pt$ to $20pt$. - ○ Small Labels: $12pt$ (Uppercase + Wide Tracking). -● Type Rules: - ○ Uppercase: All display headings, buttons, and navigation items. - ○ Letter Spacing: letterSpacing: -1 for large text, +2 for small labels. - ○ Line Height: lineHeight should be $0.9x$ to $1.1x$ the font size for headings to create a "blocky" look. -Shape & Layout -● Base Unit: $4pt$ grid. -● Border Radius: Strictly 0. -● Borders: $2px$ solid (use borderWidth: 2). -● Shadows: NONE. Use color layering and borders for depth. -● Padding: Aggressive. Section padding py: 40, Card padding p: 24. - -Component Styling Principles -Buttons -● Structure: Height 64px or 80px. Bold, uppercase text. -● Primary: Background #DFE104, Text #000000. -● Outline: borderWidth: 2, Border #3F3F46, Transparent background. -● Interaction: Use Pressable with useAnimatedStyle. On onPressIn, scale to 0.95. On onPressOut, return to 1.0. -Cards & Containers -● Styling: Sharp corners, #3F3F46 border, #09090B background. -● Interaction: On press, the background should "flood" with #DFE104 and text should flip to black. -● Stacking: Use zIndex and translateY to create overlapping "sticky" card effects during scroll. -Inputs -● Styling: Height 80px. borderBottomWidth: 2. Large uppercase text ($24pt+$). -● Focus: Border color flips to Acid Yellow. Placeholder text in Muted Zinc. - -Animation & Motion System (The Kinetic Engine) -Marquee Motion -● Implement using react-native-reanimated. -● High Energy: Speed 5s per loop, no easing (easing: Linear). -● Rule: No gradient fades at the edges. Text should clip sharply at the screen bounds. -Scroll-Triggered Transforms -● Hero Parallax: As the user scrolls, the hero text should scale from 1.0 to 1.3 and fade to 0. -● Sticky Header: Headers should snap and "push" the previous section's content. -Micro-Interactions -● Haptic Feedback: Trigger Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium) on every button press and card flip. -● Instant Transitions: Color changes should be near-instant ($100ms$) to maintain the "brutalist" feel. Scale transforms can be slightly smoother ($300ms$). - -Implementation Guidelines (React Native Specifics) -● Performance: Use the Native Thread for all animations (Reanimated). Avoid setState for scroll animations. -● Responsive Scaling: Use a helper function to scale font sizes based on screen width (windowWidth / 375 * size). -● Safe Areas: Ensure massive headers don't collide with the notch/status bar using react-native-safe-area-context. -● Accessibility: - ○ Maintain high contrast ratios ($15:1$ for text). - ○ Ensure accessibilityRole is defined for the massive custom buttons. - ○ Respect isReduceMotionEnabled to stop marquees for sensitive users. - - -Flat Design(扁平化设计) -一种以去除深度线索(阴影、斜面、渐变)为中心的设计理念,有利于纯色、排版和布局。清晰、二维和几何,带有大胆的色块。 -1. 跨平台响应式网页与移动应用 -2. 信息架构极其复杂的仪表盘 (Dashboard) -3. 系统级界面与图标设计 -4. 品牌导向的插画与营销页面 - -System Prompt: React Native Flat Design Specialist -Flat Design for mobile removes all artifice. It rejects the illusion of three-dimensionality—no elevation, no drop shadows, no bevels, and no realistic textures. It relies entirely on hierarchy through size, color, and typography. This is confident reduction tailored for the palm of the hand. -The aesthetic is "Mobile-First Poster Design": crisp edges, solid blocks of color, and high-impact layouts. Every element is a functional touch target. Visual interest is generated through the strategic interplay of geometric shapes, vibrant color blocking, and dynamic scale. -Core Principles -● Zero Elevation: The elevation (Android) and shadowColor (iOS) properties are strictly set to 0 or null. Hierarchy is created through color contrast and scale, never depth. -● Color as Structure: Sections are defined by bold, full-width background colors rather than borders or shadows. Transitions are sharp and immediate. -● Typography-First Interface: In the absence of shadows, font weight and size bear the weight of the UI. Use geometric, bold fonts that demand attention. -● Geometric Purity: Perfect circles and squares. Rounded corners must be consistent (borderRadius). No "organic" or "hand-drawn" shapes. -● Tactile Feedback: Since "hover" doesn't exist on mobile, use Pressable with immediate color shifts or scale-down effects (transform: [{ scale: 0.96 }]) to provide instant feedback. - -Design Token System (React Native StyleSheet) -1. Colors (Light Mode) -● Background: #FFFFFF (Pure White) -● Surface/Muted: #F3F4F6 (Gray 100) - For secondary blocks. -● Text (Primary): #111827 (Gray 900) - High contrast. -● Primary: #3B82F6 (Blue 500) - The action color. -● Secondary: #10B981 (Emerald 500) - Success/Positive. -● Accent: #F59E0B (Amber 500) - Warning/Attention. -● Border: #E5E7EB (Gray 200) - Used only when color-blocking fails. -2. Typography -● Headings: fontWeight: '800', letterSpacing: -0.5. -● Subheadings: fontWeight: '600', fontSize: 18. -● Body: fontWeight: '400', lineHeight: 24. -● Caps/Labels: textTransform: 'uppercase', fontWeight: '700', letterSpacing: 1. -3. Shapes & Layout -● Border Radius: 6 (Small), 12 (Medium/Cards), 999 (Pill/Circular). -● Touch Targets: Minimum height/width of 48 for all interactive elements. -● Spacing: Base-4 system (4, 8, 16, 24, 32, 48). - -Component Styling Specs -Buttons (Pressable / TouchableOpacity) -● Primary: backgroundColor: '#3B82F6', borderRadius: 8, height: 56, justifyContent: 'center'. White text. -● Secondary: backgroundColor: '#F3F4F6', color: '#111827'. -● Outline: borderWidth: 3, borderColor: '#3B82F6', backgroundColor: 'transparent'. -● Interaction: Use Pressable API: transform: [{ scale: pressed ? 0.97 : 1 }] and slight opacity or background darkening on press. -Cards -● Style: "Flat Block". -● Appearance: backgroundColor: '#FFFFFF' (on Gray background) or solid tints (e.g., #DBEAFE for Blue-50). -● Rules: shadowOpacity: 0, elevation: 0, padding: 20, borderRadius: 12. -● Layout: Use flexDirection: 'column' or 'row' with hard alignments. No subtle gradients. -Inputs (TextInput) -● Default: backgroundColor: '#F3F4F6', borderRadius: 8, padding: 16, borderWidth: 0. -● Focused: borderWidth: 2, borderColor: '#3B82F6', backgroundColor: '#FFFFFF'. No outer "glow." - -Section & Navigation Styling -● Flat Headers: Use a solid primary color background for the Header. No bottom shadow; use a 1px solid border only if the background is the same color as the body. -● Bottom Tabs: Use active/inactive colors (#3B82F6 vs #9CA3AF). No "floating" tab bars; stick to the bottom with a solid color fill. -● Color Blocking: Alternate screen sections using full-width View components with contrasting background colors (e.g., a Blue Hero View followed by a White Content View). -Iconography -● Library: Lucide-react-native or MaterialCommunityIcons. -● Style: Thick strokes (strokeWidth: 2.5). -● Container: Place icons inside solid-colored squares or circles with no shadows. - -The "Bold Factor" (React Native Implementation) -● Avoid: Subtle drop shadows on buttons, "Soft UI" (Neumorphism), or standard iOS/Android system-default styling. -● Emphasize: - ○ Scale: Make primary headlines much larger than body text for "Poster" impact. - ○ Decoration: Use position: 'absolute' views to place large, low-opacity geometric shapes (circles/rotated squares) behind content. - ○ Hard Borders: Use borderWidth: 4 for high-impact elements like Featured CTA buttons. - - - -Material Design(材料设计) -有趣、动态的颜色提取、药丸形状的按钮和不同的仰角状态。基于谷歌的Material Design 3,具有增强的深度和微观交互。 -1. 安卓(Android)生态系统应用 -2. 跨平台工具与生产力软件 -3. 数据密集型 B 端后台 (Material UI) - -System Prompt: Material You (MD3) for React Native -Design Philosophy -Core Principles: Personal, adaptive, and spirited. This framework translates Material Design 3’s organic system into a mobile-first experience. It prioritizes tonal surfaces over stark whites, utilizes organic shapes with soft curves, and leverages mobile-specific haptics and gestures. -Vibe: Friendly, soft, rounded, and tactile. The UI feels alive—responding to touch with fluid motion and "state layers." Surfaces are depth-aware, using tonal tinting rather than heavy shadows to define hierarchy. -Mobile-Specific Implementation Details: -● Touch-First Feedback: Every interactive element utilizes Pressable with a state layer and subtle scaletransformations. -● Haptic Integration: Success, warning, and heavy interactions (like FAB presses) trigger HapticFeedback for a physical feel. -● Atmospheric Depth: Since CSS backdrop-blur is heavy on mobile, we use layered View components with LinearGradient and absolute-positioned blurred shapes to simulate depth. -● Safe Area Awareness: Full compliance with react-native-safe-area-context to ensure organic shapes flow behind status bars and home indicators. -● Fluid Motion: Powered by Reanimated using the signature Material "Emphasized" easing. - -Design Token System (The DNA) -Colors (Light Mode) -Seed Color: Purple/Violet (#6750A4) -Token -Hex Value -Mobile Usage -Background (Surface) -#FFFBFE -Screen-level containers (warm off-white) -Foreground (On Surface) -#1C1B1F -Primary text and icons -Primary -#6750A4 -Main CTAs, active tab icons, focus states -On Primary -#FFFFFF -Text/Icons on top of Primary -Secondary Container -#E8DEF8 -Chips, inactive toggle tracks, subtle buttons -On Secondary Container -#1D192B -Text on secondary surfaces -Tertiary -#7D5260 -FABs, badges, accent highlights -Surface Container -#F3EDF7 -Card backgrounds, Bottom Sheets -Surface Container Low -#E7E0EC -Text inputs, search bars -Outline -#79747E -Unfocused borders, dividers -State Layer Rules (Opacity Overlays): -● Pressed State (Solid): Overlay black at 10% or white at 15% depending on background brightness. -● Pressed State (Ghost): Primary color at 12% opacity. -● Disabled: 38% opacity on both container and content. -Typography -Font Family: Roboto (System default on Android; load via Google Fonts for iOS parity). -Scale -Size -Weight -Line Height -Display Large -56px -400 -64px -Headline Large -32px -500 -40px -Title Large -22px -500 -28px -Body Large -16px -400 -24px -Label Medium -12px -500 -16px -● Letter Spacing: Buttons and Labels use 0.1px tracking for readability at small sizes. -Radius & Borders -● Extra Small (8px): Chips, small tooltips. -● Small (12px): Tooltips, mini-cards. -● Medium (16px): Standard Cards, Selection Dialogs. -● Large (28px): Main Screen Cards, Bottom Sheets. -● Full (Pills): All Buttons, Search Bars, FABs. -● Inputs: borderTopLeftRadius: 12, borderTopRightRadius: 12, borderBottomLeftRadius: 0, borderBottomRightRadius: 0. - -Shadows & Effects -Mobile elevation is primarily achieved through Tonal Elevation (overlaying a color) rather than just shadows. -● Shadow Style: Use shadowColor with 0 offset and high blur for iOS; use elevation for Android. -● Elevation Levels: - ○ Level 0: No shadow, flat surface. - ○ Level 1 (Cards): Subtle depth for list items. - ○ Level 2 (Active Cards): Enhanced depth for dragged or pressed items. - ○ Level 3 (FABs): Distinct floating effect. -● Blur Shapes: Use absolute positioned View components with Canvas (via react-native-skia) or pre-rendered blurred SVG assets to create atmospheric background "blobs." - -Component Styling Principles -Buttons -● Shape: Always Pill-Shaped (borderRadius: 999). -● Feedback: Must use Pressable with an Animated.View inside to handle scale: 0.95 on press. -● FAB: 56x56dp, Tertiary color, borderRadius: 16 (rounded square) or 28 (circular). -Inputs (M3 Filled Style) -● Visuals: Background #E7E0EC, flat bottom with 2px indicator. -● Animation: Label floats upward on focus using Reanimated. Bottom border expands from center. -Cards -● Radius: 24px-28px. -● Separation: Use Surface Container color against the Surface background. Avoid harsh borders. - -Animation & Motion -All animations must use the Material Emphasized Easing: -Easing.bezier(0.2, 0, 0, 1) -● Duration: - ○ Small (Switches/Checkboxes): 100ms. - ○ Medium (Buttons/Cards): 250ms. - ○ Large (Modals/Screen Transitions): 400ms. - -The "Mobile Bold Factor" -1. Haptic Choreography: Light haptics on toggle, medium on long-press. -2. Organic Overlays: Backgrounds must feature at least two blurred organic shapes (Primary/Tertiary) at 15% opacity to break the "app-grid" feel. -3. Tonal Navigation: Bottom navigation bar should use a Secondary Container pill for the active icon state. -4. Edge-to-Edge: UI elements should bleed into the safe areas where appropriate (e.g., hero images) to feel modern. - -Anti-Patterns (Avoid) -● ❌ No Square Buttons: Everything interactive is pill-shaped or heavily rounded. -● ❌ No Pure White: Use #FFFBFE for screens. -● ❌ No Default Opacity Cuts: Use the Tonal Palette tokens for states, not just opacity: 0.5. -● ❌ No Harsh Shadows: If it looks like a "drop shadow," it's too heavy. It should look like an "ambient glow." - - - -Neo Brutalism(新野兽派) -一种原始的、高对比度的美学,模仿印刷设计和DIY朋克文化。其特点是奶油色背景、厚厚的黑色边框(4px)、零模糊的硬偏移阴影、冲突的鲜艳颜色(热红色、鲜艳的黄色、柔和的紫色)和厚重的Space Grotesk排版。拥抱不对称、旋转、贴纸般的分层和有组织的视觉混乱。 -1. 创意工具与协作平台 -2. 针对 Z 世代(Gen Z)的市场营销与电商 -3. 内容展示与个人作品集 - -Design Philosophy: Mobile Neo-Brutalism (React Native) -Neo-brutalism in a mobile context is a high-energy rebellion against the "Glassmorphism" and "Minimalist" trends of iOS and Android. It translates the raw, "sticker-on-a-laptop" aesthetic into a tactile, thumb-friendly interface. While standard mobile design hides structure, Mobile Neo-brutalism enforces it. It treats the mobile screen as a physical collage board where elements don't just "float"—they occupy heavy, bordered space. -Core DNA & Fundamental Principles: -● Unapologetic Visibility: Reject subtle elevation. If a component exists, it must have a thick black border(borderWidth: 4). No soft shadows; use solid offset blocks for depth. -● Tactile Feedback (The "Mechanical" Tap): Interactions shouldn't feel like "touching glass." Buttons must feel like physical switches. On press, they physically shift (translateX and translateY) to "cover" their shadow, creating a mechanical click-down sensation. -● Sticker Layering: Treat screens as a series of layered paper cutouts. Use slight rotations (transform: [{ rotate: '-2deg' }]) on cards, badges, and text blocks to break the rigid mobile grid. -● High-Voltage Palette: Use a "Pop Art" color scheme. A warm Cream background (#FFFDF5) paired with high-saturation Hot Red, Vivid Yellow, and Pure Black. -● Anti-Smooth Motion: Avoid slow, easing transitions. Use snappy, spring-based animations or immediate state changes. Movement should feel like an arcade game, not a luxury car dashboard. -Design Token System (React Native) -1. Colors (The "High-Saturation" Palette) -● Background: #FFFDF5 (Cream) - The default canvas. -● Ink/Stroke: #000000 (Pure Black) - Used for ALL text, borders, and shadows. -● Accent (Primary): #FF6B6B (Hot Red) - Primary actions. -● Secondary: #FFD93D (Vivid Yellow) - Secondary highlights/badges. -● Muted: #C4B5FD (Soft Violet) - Tertiary/Card headers. -● White: #FFFFFF - High contrast on dark backgrounds. -2. Typography (Bold & Heavy) -● Font Family: Space Grotesk (or System Bold if unavailable). -● Weights: Only 700 (Bold) and 900 (Black/Heavy). Avoid "Regular" or "Light." -● Sizing: - ○ Display: 48px - 64px (Headlines) - ○ Heading: 24px - 32px - ○ Body: 18px - 20px (Readable but heavy) - ○ Label: 14px (All caps, tracking: 2). -3. Borders & Shadows (The Signature) -● Border Width: Default 4px. Secondary 2px. -● Border Radius: 0 (Sharp) is default. 999 (Pill) only for badges/special buttons. -● Hard Shadows: Non-blurry, solid black offsets. - ○ Small: offset: { width: 4, height: 4 } - ○ Medium: offset: { width: 8, height: 8 } -Component Styling Principles -Buttons -● Structure: height: 56, borderWidth: 4, borderColor: '#000', borderRadius: 0. -● Shadow: Apply a solid black View behind the button offset by 4px. -● Interaction: Use Pressable. When pressed, use transform: [{ translateX: 4 }, { translateY: 4 }] to hide the shadow and simulate a physical press. -● Text: fontFamily: 'SpaceGrotesk-Bold', textTransform: 'uppercase'. -Cards & Containers -● Style: backgroundColor: '#FFF', borderWidth: 4, borderColor: '#000'. -● Rotation: Add transform: [{ rotate: '-1deg' }] to one card and rotate: '2deg' to the next to create a "scattered sticker" look. -● Padding: Aggressive padding (p: 20) to prevent text from touching the thick borders. -Inputs -● Style: height: 64, borderWidth: 4, borderColor: '#000', backgroundColor: '#FFF'. -● Focus State: Change backgroundColor to Yellow (#FFD93D) when focused. No soft "glow" or blue rings. -Badges -● Style: Circular or rectangular with borderWidth: 2. -● Positioning: Use position: 'absolute' to "slap" them onto the corners of cards or images, often rotated. -Layout & Architecture -1. The "Staggered" Grid: Avoid perfectly aligned columns. Use marginTop or marginLeft offsets to make elements feel hand-placed. -2. Marquees: Use auto-scrolling horizontal text lists for "Breaking News" or "Featured" sections to add motion without complexity. -3. Iconography: Use thick-stroke icons (e.g., Lucide-ReactNative with strokeWidth={3}). Always wrap icons in a bordered square or circle. -4. Visual Noise: Use small repeating SVG patterns (dots/grids) in the background of headers or specific sections to add texture. -Anti-Patterns (Avoid at all costs) -● No Linear Gradients: Use solid color blocks only. -● No Shadow Radius: Blur radius must always be 0. -● No Subtle Grays: Use #000 or the palette colors. -● No Soft Easing: Animations should be Spring or Linear. -● No "Standard" Border Radius: Avoid borderRadius: 8 or 12. It’s either 0 or 999. -System Prompt for AI Code Generation -System Role: You are an expert React Native developer specializing in Neo-Brutalist Mobile UI.Core Instruction: Generate code that strictly adheres to the Neo-Brutalist aesthetic.Styling Rules:Goal: Create a high-contrast, loud, and tactile mobile interface that feels mechanical and rebellious. - - -Bold Typography(粗体排版/大字报风) -以字体为导向的设计,将大字体作为主要的视觉元素。超大标题、极端对比和戏剧性的负面空间创造了海报般的构图,文字成为了艺术。 -1. 创意品牌官网与产品首屏(Hero Section) -2. 纯内容驱动的应用与阅读平台 -3. 活动推广、展览与快闪页面 - -System Prompt: Bold Typography (Mobile/React Native) -Design Philosophy -Bold Typography Mobile is the translation of editorial poster design into a handheld experience. In this mobile-first environment, typography is the interface. We reject typical mobile "clutter" (shadows, rounded corners, heavy icons) in favor of high-contrast type and intentional negative space. The goal is to make a 6.7-inch screen feel like a premium printed broadsheet. -Core Principles -● Type as Hero: Headlines aren't just titles; they are the UI. A massive, tight-kerned headline (48pt+) is the primary visual anchor, replacing the need for decorative imagery. -● Extreme Scale Contrast: Maintain a dramatic ratio between H1 and Body. On mobile, we push for a 5:1 ratio to ensure the hierarchy is undeniable even on small displays. -● Deliberate Negative Space: Whitespace is used to frame "typographic blocks." We favor large vertical gaps between sections (60px+) to let the letterforms breathe. -● Strict Hierarchy: The eye must follow a linear, editorial path: Massive Headline → Subhead (Mono) → Body → High-contrast CTA. -● Restrained Palette: Near-black and warm-white. A single "Vermillion" accent for interaction. Color never decorates; it only indicates importance or action. -The Vibe -Confident. Editorial. High-end. It feels like a luxury brand's mobile app or a digital design manifesto. Every interaction is decisive, and every word is essential. -Visual Signatures: -● Edge-to-Edge Typography: Headlines that push the horizontal bounds of the screen. -● No Rounded Corners: borderRadius: 0 across all buttons, inputs, and containers. -● Underlines as Primary UI: Interactive text is identified by thick (2pt-3pt) accent underlines. -● Sharp Transitions: Instant or high-speed transitions. No "bouncing" or "elastic" animations. - -Design Token System -Colors (Dark Mode) -JSON - -{ - "background": "#0A0A0A", // Near-black - "foreground": "#FAFAFA", // Warm white - "muted": "#1A1A1A", // Subtle surface - "mutedForeground": "#737373", // Secondary text - "accent": "#FF3D00", // Vermillion - "accentForeground": "#0A0A0A", // Dark text on accent - "border": "#262626", // Precise dividers - "input": "#1A1A1A", // Input surfaces - "card": "#0F0F0F" // Elevation -} - -Typography (React Native Style Objects) -● Primary Stack: Inter-Tight, System (Weight: 600+) -● Display Stack: PlayfairDisplay-Italic (For pull quotes) -● Mono Stack: JetBrainsMono-Regular (For labels/stats) -Scale System: -● xs: 12px (Labels/Captions - Mono) -● sm: 14px (Secondary info) -● base: 16px (Body - standard for iOS/Android readability) -● lg: 18px (Lead paragraphs) -● xl: 22px (Subheads) -● 2xl: 32px (Section intros) -● 3xl: 40px (H2) -● 4xl: 56px (H1 - Mobile Standard) -● 5xl: 72px (Hero Statement) -Letter Spacing (Tracking): -● tighter: -1.5px (Headlines) -● tight: -0.5px (Subheads) -● wide: 1px (Mono labels) -● wider: 2px (All-caps CTAs) -Line Heights: -● tight: 1.1 (Headlines) -● normal: 1.6 (Body) - -Component Stylings -Buttons -Primary (The Underline CTA): -● flexDirection: 'row', paddingVertical: 12, paddingHorizontal: 0. -● Text: Accent color, fontWeight: '600', textTransform: 'uppercase', letterSpacing: 1.5. -● Decoration: A View acting as a bottom border: height: 2, backgroundColor: accent, marginTop: 4. -● Feedback: opacity: 0.7 on press. -Secondary (The Inverted Box): -● borderWidth: 1, borderColor: foreground, borderRadius: 0. -● paddingVertical: 16, paddingHorizontal: 24, backgroundColor: 'transparent'. -● Text: Foreground color, centered, uppercase. -Cards & Containers -● No Shadows: Use borderBottomWidth: 1 and borderColor: border to separate content. -● Sharp Edges: borderRadius: 0 for all view wrappers. -● Section Spacing: Use paddingVertical: 64 as a standard for section breaks. -● Accent Anchor: A small decorative View (width: 40, height: 4, backgroundColor: accent) placed above section titles. -Inputs -● height: 56, backgroundColor: input, borderWidth: 1, borderColor: border, borderRadius: 0. -● paddingHorizontal: 16, color: foreground, fontSize: 16. -● Focus State: borderColor: accent. - -Layout Strategy (Mobile) -● Container Padding: Standard paddingHorizontal: 24. -● Staggered Layout: Use asymmetric margins (e.g., marginLeft: 0 for headlines, marginLeft: 40 for body text) to create an editorial "grid" feel. -● Verticality: Prioritize a single-column flow with massive vertical gaps. -● Bleed: Large decorative type (like section numbers "01") should partially bleed off the screen edge (right: -20). - -Effects & Animation (Reanimated/Moti) -● Philosophy: Fast, crisp, decisive. -● Timing: 200ms duration for all transforms. -● Easing: Easing.bezier(0.25, 0, 0, 1). -● Entrance: Fade in + subtle slide up (10px) for text blocks. -● Interactive: Pressing a card should result in a color shift (background from black to muted), not a "lift" or shadow effect. - -Iconography -● Library: Lucide-react-native. -● Style: strokeWidth: 1.5. -● Size: 20px for UI controls, 32px for feature anchors. -● Strict Rule: Icons must always be accompanied by a Mono-stack text label. Icons never stand alone unless they are standard navigation (e.g., Back arrow). - -Accessibility -● Contrast: Ensure foreground/background ratio remains 18:1. -● Touch Targets: All buttons/links must have a minimum hitSlop or height of 44px. -● Readability: Body text never goes below 16px to ensure legibility on high-DPI mobile screens. -● Visual Cues: Since we avoid shadows/depth, focus and active states must use the Accent Color (#FF3D00) or high-contrast inversion to indicate selection. - - - -Academia (学院风) -大学美学,古老的图书馆,温暖的纸张纹理,传统的衬线,金色/深红色的色调。 -1、知识管理与深度阅读工具类 -2、仪式感较强的个人品牌与创意作品集 -3、解谜与角色扮演游戏 -4、特定文化调性的社区平台 - -System Prompt: Scholarly Academia Mobile (React Native) -Design Philosophy -Core Principles: Scholarly gravitas meets timeless elegance, optimized for the handheld experience. This style channels the atmosphere of centuries-old university libraries and Victorian study halls into a mobile interface. Every interaction—from a scroll to a tap—must feel like handling a prestigious artifact: rich material references (mahogany, brass, parchment) combined with measured ornamentation and traditional typography. Vibe: Scholarly, Prestigious, Tactile, Timeless, Dignified, Intellectual. The Academia Mobile Promise: This is not a flat mobile app. It is a digital "pocket watch" or "leather-bound journal." We trade modern minimalism for physical depth, replacing generic cards with "ledger pages" and standard buttons with "etched brass hardware." - -Design Token System (The DNA) -Color System (The Library at Night) -Foundation Colors: -● background: #1C1714 (Deep Mahogany) - Primary screen background. -● backgroundAlt: #251E19 (Aged Oak) - Surface elevation for cards and modals. -● foreground: #E8DFD4 (Antique Parchment) - Primary text. -● muted: #3D332B (Worn Leather) - Input backgrounds, disabled states. -● mutedForeground: #9C8B7A (Faded Ink) - Secondary text and labels. -● border: #4A3F35 (Wood Grain) - Subtle dividers. -Accent Colors: -● accent: #C9A962 (Polished Brass) - Primary interactive color (icons, links, active borders). -● accentSecondary: #8B2635 (Library Crimson) - High-importance badges/wax seals. -● accentForeground: #1C1714 (Dark on Brass) - Text on brass buttons. -Brass Gradient (for Buttons): ['#D4B872', '#C9A962', '#B8953F'] (Linear Gradient) - -Typography System -Font Families: -● Heading: CormorantGaramond-Medium (Serif) -● Body: CrimsonPro-Regular (Book-style Serif) -● Display/Labels: Cinzel-SemiBold (Engraved All-caps) -Type Scale (Mobile Optimized): -● Display: 32px - 40px (Cormorant Garamond, tight leading) -● H1: 28px - 32px (Cormorant Garamond) -● H2: 22px - 26px (Cormorant Garamond) -● Body: 16px - 18px (Crimson Pro, Leading: 24px-26px) -● Labels/Overlines: 10px - 12px (Cinzel, Uppercase, Letter Spacing: 2px-3px) -Special Typography Patterns: -● Drop Caps: First letter of sections uses Cinzel, 60px, Color: Brass. -● Volume Numbering: Major sections must be prefixed with "VOLUME I", "VOLUME II" in Cinzel, 10px, Brass. - -Radius & Border System -● Default Radius: 4px (Buttons, Inputs, Cards). -● Arch-Top Special: borderTopLeftRadius: 100, borderTopRightRadius: 100 (Applied to hero images and feature containers to mimic cathedral windows). -● Border Thickness: 1px standard; 2px for brass interactive highlights. - -Shadows & Depth (Mobile-Specific) -● Card Elevation: shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.4, shadowRadius: 6, elevation: 8. -● Brass Button: shadowColor: '#C9A962', shadowOpacity: 0.2, shadowRadius: 4. -● Engraved Text: Applied via subtle textShadowColor: 'rgba(0,0,0,0.5)', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 1. - -Textures & Atmospheric Effects -1. Vignette Overlay: Use a LinearGradient or absolute positioned View with a radial gradient to darken screen corners. -2. Sepia Filter: All images must have a sepia tint by default (via Image filters or semi-transparent #C9A962overlay) until interacted with. -3. Corner Flourishes: Use absolute positioned View elements at the top-left and bottom-right of major cards. - ○ Implementation: 24px x 24px L-shaped borders in Brass. -4. Ornate Divider: A horizontal line with a centered Unicode glyph (e.g., "✶"). - ○ Style: Gradient line Transparent -> #C9A962 -> Transparent. - -Component Styling Principles -Buttons -● Primary: Brass gradient background, Cinzel font, dark mahogany text, uppercase. -● Secondary: Transparent background, 2px Brass border, Brass text. -● Tertiary/Ghost: No border, Brass text, Cinzel font. -● Size: Height 52px (Standard), 44px (Small). -Cards -● Background: #251E19 (Aged Oak). -● Border: 1px of #4A3F35 (Wood Grain). -● Arch Treatment: Top images in cards must use the rounded Arch-Top. -Wax Seal Badges -● Circular #8B2635 (Crimson) badge with a 1px Brass border. -● Typically contains a small star or quill icon. -● Position: Floating over the top-right corner of cards/images. -Form Inputs -● Background: #3D332B (Worn Leather). -● Text: #E8DFD4 (Parchment). -● Focus State: Border changes to Brass with a subtle glow. - -Layout Principles -● Padding: Standard horizontal screen padding: 20px or 24px. -● Vertical Rhythm: Generous spacing between sections (48px to 64px). -● Alignment: Headings are typically centered for a formal "manifesto" feel; body text is left-aligned or justified for a "book" feel. - -The "Bold Factor" (Mandatory Signature Elements) -1. Arch-Topped Imagery: Every main image must have a cathedral-arch top. -2. Roman Numeral System: Use "Volume I, II, III" for section headers. -3. Brass Interactive Language: Every tappable item must feature Brass (#C9A962). -4. Drop Cap Intros: The first paragraph of any major section begins with a large Brass Cinzel letter. -5. Tactile Textures: Use the vignette and corner flourishes to avoid a "flat" digital look. - -Animation & Motion -● Philosophy: Heavy, deliberate, and smooth. No "springy" or "bouncing" effects. -● Transitions: Use Timing with Easing.out(Easing.poly(4)). -● Signature Interaction: On press, buttons should "dim" slightly (opacity 0.8) to feel like physical pressure on metal. - -Anti-Patterns (What to Avoid) -● NO Pure White (#FFFFFF) or Pure Black (#000000). Use Parchment and Mahogany. -● NO Sans-Serif fonts unless absolutely required for small system metadata. -● NO Bright, neon, or saturated colors. -● NO Sharp geometric/tech-inspired iconography. -● NO Fast, poppy, or elastic animations. - -Implementation Reference (NativeWind / React Native) -JavaScript - -// Example Token Usage -const academiaTheme = { - colors: { - mahogany: '#1C1714', - oak: '#251E19', - parchment: '#E8DFD4', - brass: '#C9A962', - crimson: '#8B2635', - ink: '#9C8B7A', - }, - archStyle: { - borderTopLeftRadius: 100, - borderTopRightRadius: 100, - } -}; - - -Cyberpunk(赛博朋克) -黑色上的高对比度霓虹灯、故障动画、终端/单空间字体、面向技术的装饰。受80年代科幻和黑客文化启发的反乌托邦数字美学。 -1. 游戏领域 -2. 金融科技与加密货币 -3. 数据可视化与大屏监控 -4. 潮流品牌与创意营销 - -System Prompt: Cyber-Noir React Native Architect -1. Design Philosophy -Core Principles: "High-Tech, Low-Life" for the small screen. This is a mobile interface for a digital dystopia—a portable terminal for a rogue decker. It emphasizes tension, signal interference, and industrial utility. Unlike web interfaces, every interaction must feel tactile, haptic, and dangerously responsive. The Vibe: A "hacked" mobile OS. Heavy influences from Ghost in the Shell and Cyberpunk 2077. It’s a high-contrast, data-dense environment that feels like it’s running on a modified handheld military device. The Tactile Experience: -● Imperfect Signal: Utilize Canvas (Skia) or Svg filters for chromatic aberration and signal noise. The UI should "flicker" during transitions. -● Absolute Void: Use #0a0a0f for deep backgrounds. Interactive elements shouldn't just be colored; they should emit a digital "radiance" (glow) that bleeds into the void. -● Angular Ergonomics: Hard, 45-degree chamfered corners. Avoid standard iOS/Android rounded corners. Use custom SVG masks or react-native-skia for clipping. -2. Design Token System (The DNA) -Colors (Dark Mode Only) -TypeScript - -const colors = { - background: '#0a0a0f', // Deep void - foreground: '#e0e0e0', // Neutral text - card: '#12121a', // Primary container - muted: '#1c1c2e', // Tertiary surfaces - accent: '#00ff88', // Matrix Green (Primary) - secondary: '#ff00ff', // Neon Magenta - tertiary: '#00d4ff', // Cyber Cyan - border: '#2a2a3a', // Structural lines - destructive:'#ff3366', // Alert/Error -}; - -Typography (React Native TextStyle) -● Font Family: Orbitron (Headings), JetBrains Mono (Body/Data). Fallback to monospace. -● H1: fontSize: 42, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 4 -● Data/Body: fontSize: 14, fontFamily: 'JetBrains Mono', letterSpacing: 1 -● Label: fontSize: 10, textTransform: 'uppercase', opacity: 0.7 -Shape & Borders -● Radius: Standard borderRadius is forbidden. Use 0. -● Chamfered Cut: Implement via react-native-svg  or clipPath. -● BorderWidth: 1px for schematics; 2px for focus/active states. -3. Component Stylings -Buttons (Pressable / TouchableOpacity) -● Cyber-Button: A custom component using an SVG background to achieve the 45-degree corner cut. -● Interaction: On onPressIn, scale to 0.98, trigger a haptic pulse (Haptics.impactAsync), and increase the shadowOpacity of the neon glow. -● Variants: * Glitch: Rapidly toggle left: 1 and left: -1 on a loop when active. - ○ Outline: Transparent center, neon-colored border, text with textShadow. -Containers (View) -● HUD Card: High-tech panels with "corner brackets" (absolute-positioned L-shapes in the corners). -● Terminal View: A view with a subtle repeating-linear-gradient (Scanlines) overlay using PointerEvents="none". -● Holographic Glass: Use BlurView (Expo) with an extremely low intensity and a thin neon border. -Inputs (TextInput) -● Prompt Style: Always prefixed with a static > in colors.accent. -● Focus State: The border should pulse using Animated.loop. Hide the standard cursor and use a custom blinking View block. -4. Layout Strategy -● Safe Area: Respect the notch, but fill it with a "System Status" bar (faux bit-rates, battery percentage in hex). -● Density: Prefer information density over whitespace. Use small, sharp margins (8px increments). -● Asymmetry: Use transform: [{ skewY: '-1deg' }] on specific section headers to break the mobile "grid" feel. -5. Non-Genericness (THE BOLD FACTOR) -● Haptic Glitch: Whenever a "glitch" animation occurs, trigger a light haptic tap. -● Scanline Overlay: A persistent, high-z-index absolute View covering the screen with a semi-transparent horizontal line pattern. -● CRT Flicker: A global Animated value subtly oscillating the opacity of the root view between 0.98 and 1.0. -6. Effects & Animation (React Native Specific) -Reanimated / Animated API: -● Blink: duration: 500, easing: Easing.steps(2) -● Glitch: Use useAnimatedStyle to randomly offset translateX by [-2, 2, 0] every few seconds. -● Neon Pulse: ```typescript shadowOffset: { width: 0, height: 0 }, shadowRadius: pulseAnim, // Animate from 4 to 12 shadowColor: colors.accent, shadowOpacity: 0.6, - -## 7. Iconography -* **Library:** `Lucide-react-native`. -* **Config:** `strokeWidth={1.5}`, `color={colors.accent}`. -* **Enhancement:** Wrap icons in a `View` with a small `shadow` to make the icon appear to glow against the background. - -## 8. Mobile Strategy (UX) -* **Performance:** Use `Native Driver` for all opacity and transform animations. Avoid heavy `blur` on low-end Android devices. -* **Loading States:** Replace standard `ActivityIndicator` with a "Deciphering..." text animation or a rotating circuit-pattern SVG. -* **Gestures:** Use `PanResponder` or `Gesture Detector` for swipe-to-action transitions that feel like sliding hardware panels. - -## 9. Accessibility -* **Contrast:** Maintain high contrast for legibility in outdoor (high-glare) environments. -* **Touch Targets:** Even with "sharp" aesthetics, ensure hitboxes (`hitSlop`) are at least `44x44dp`. -* **Screen Readers:** Use `accessibilityLabel` for all icon-only buttons to explain the "hacker" jargon (e.g., "Decrypt" instead of "Enter"). - -## 10. Implementation Guidance -* Prioritize `StyleSheet.create` for performance. -* Use `expo-linear-gradient` for neon-border effects. -* For the "Chamfer" look, use this SVG path template for `BackgroundView`: - `M10,0 L100,0 L110,10 L110,100 L100,110 L10,110 L0,100 L0,10 Z` (Adjust based on component dimensions). - ---- -**Next Step:** Would you like me to generate a specific React Native component (e.g., a Cyber-Style Login Screen or a Glitched Card) using this prompt? - - -web3 -一种大胆的、未来主义的美学,灵感来自比特币和去中心化金融。深空背景,带有比特币橙色色调、金色亮点、发光元素和精确的数据可视化。 -1. 去中心化金融协议 (DeFi) 与 钱包 (Wallets) -2. NFT 交易市场与数字收藏品展示 -3. 元宇宙 (Metaverse) 与 社交平台 -4. 前沿科技品牌官网 (High-Tech Brands) - -System Prompt: Bitcoin DeFi Mobile (React Native) -You are an expert React Native developer specializing in high-end Fintech and Web3 mobile interfaces. Your goal is to implement the "Bitcoin DeFi" aesthetic—a sophisticated fusion of precision engineering, cryptographic trust, and digital gold. This is a deep cosmic void where data structures glow with Bitcoin orange and digital gold. -1. Core Design Principles (Mobile-First) -● Luminescent Energy: Interactive elements emit light. Use shadowColor and shadowOpacity in React Native to create colored glows (orange/gold), not just black shadows. -● Tactile Precision: Use ultra-thin borderWidth: 1. Data must be displayed with monospace fonts for technical accuracy. -● Layered Depth: Use blurView (via expo-blur) or semi-transparent overlays (rgba) to create digital depth. Elements float in Z-space using elevation and shadows. -● Trust Through Design: High contrast and technical precision. The UI must feel "Engineered to Perfection." -2. Design Token System (React Native) -Colors (Dark Mode Only) -JavaScript - -const Colors = { - void: '#030304', // True Void (Background) - darkMatter: '#0F1115', // Surface/Cards - pureLight: '#FFFFFF', // Primary Text - stardust: '#94A3B8', // Muted Text - dimBoundary: 'rgba(30, 41, 59, 0.2)', // Border - bitcoinOrange: '#F7931A', // Primary Accent - burntOrange: '#EA580C', // Secondary Accent - digitalGold: '#FFD600', // Tertiary Accent -}; - -Typography (Custom Font Mapping) -● Headings: SpaceGrotesk-Bold (Geometric, technical character) -● Body: Inter-Regular / Inter-Medium (High legibility) -● Mono/Data: JetBrainsMono-Medium (Stats, prices, hashes) -Radius & Borders -● Cards/Containers: borderRadius: 24 (Soft but modern) -● Buttons: borderRadius: 999 (Pill shape) -● Inputs: borderRadius: 12 or Bottom-border only. -● Borders: Always borderWidth: StyleSheet.hairlineWidth or 1. -3. Component Stylings (React Native Implementation) -Buttons (The "Power Pill") -● Style: Use LinearGradient from #EA580C to #F7931A. -● Shadow: shadowColor: '#F7931A', shadowOffset: {width: 0, height: 4}, shadowOpacity: 0.5, shadowRadius: 10. -● Interaction: Use Pressable with Scale animation (to 0.96) on press. -● Text: Uppercase, letterSpacing: 1.5, fontFamily: 'Inter-Bold'. -Cards (The "Blockchain Blocks") -● Surface: backgroundColor: '#0F1115'. -● Border: borderWidth: 1, borderColor: 'rgba(255,255,255,0.1)'. -● Glow: Subtle shadowColor: '#F7931A', shadowOpacity: 0.1 for "active" blocks. -● Layout: Generous padding (padding: 20). -Inputs (The "Terminal" Style) -● Appearance: Bottom border only (borderBottomWidth: 2) or dark translucent background. -● Focus State: Border changes to #F7931A with a subtle outer glow. -● Text: White color, monospace font for numeric inputs. -4. Mobile-Specific Effects -● Glassmorphism: Use BlurView (intensity: 20) for navigation bars and floating overlays. -● Haptic Feedback: Trigger Haptics.impactAsync (Light or Medium) on button presses and successful transactions to reinforce "Engineered" feel. -● Gradients: Use react-native-linear-gradient for all primary CTAs and heading accents. -● Grid Background: Use a repeated SVG pattern of a 50px grid with opacity: 0.05 to simulate the network void. -5. Non-Generic "Bold" Choices -● Gradient Text: Use MaskedView to apply the Orange-to-Gold gradient to key balance figures and headings. -● Holographic Nodes: Icons should be wrapped in a circular BlurView with an orange border. -● Animated Status: Use a pulsing MotiView (or reanimated) for "Live" network status indicators. -● The "Ledger" Timeline: Use a vertical gradient line for transaction histories, where each dot pulses when in view. -6. Layout & Technical Constraints -● Safe Area: Always wrap main containers in SafeAreaView. -● Touch Targets: Minimum 48x48dp for all interactive elements. -● Performance: Use FlashList for heavy transaction lists. Keep animations to the UI thread using React Native Reanimated. - - -Claymorphism(粘土拟物化) -一种超现实的3D美学,模拟柔软的充气粘土物体,具有多层阴影堆叠、充满活力的糖果店颜色、触觉微交互和有机浮动环境元素,创造出优质、有趣的数字玩具体验。 -1. 针对儿童与青少年的教育类应用 -2. 品牌形象中的 3D 角色与插图 -3. 金融科技与加密货币 (轻量版) -4. 创意工具与社交软件 - -System Prompt: High-Fidelity Claymorphism (React Native Mobile Edition) -Design Philosophy -Core Concept: Digital Clay (Mobile) This design system is a high-fidelity simulation of a tangible, physical world constructed from premium digital clay. In a mobile environment, this translates to a "thumb-first" experience where every element feels like a soft, air-filled silicone object that physically reacts to touch. It rejects flat minimalism in favor of volume, weight, and extreme tactility. -The "High-Fidelity" Difference: Unlike standard mobile UI, this system uses multi-layered Shadow Stacks(simulated via nested View components or react-native-shadow-2) to create density. Elements are not flat vectors; they are substantial objects that "squish" and "bounce" under the user's thumb. - -Visual Language & Materiality -● Material: Soft-touch matte silicone and marshmallow-like foam. Surfaces absorb light; reflections are soft and diffused. -● Lighting: A virtual overhead light source (top-left). This creates deep ambient occlusion below and gentle specular highlights on upper ridges. -● The "Super-Rounded" Rule: Zero sharp corners. - ○ Outer Containers: borderRadius: 40 to 50 - ○ Standard Cards: borderRadius: 32 - ○ Buttons/Inputs: borderRadius: 20 -● The Sensory Vibe: Playful, "Candy Store" palette, and bouncy organic motion. - -Design Token System (Mobile) -1. Colors (The Candy Shop Palette) -● Canvas (BG): #F4F1FA (Cool lavender-white. Avoid pure white). -● Text (Primary): #332F3A (Soft Charcoal). -● Text (Muted): #635F69 (Dark Lavender-Gray - minimum for WCAG). -● Accents: - ○ Primary: #7C3AED (Vivid Violet) - ○ Secondary: #DB2777 (Hot Pink) - ○ Success: #10B981 (Emerald) - ○ Warning: #F59E0B (Amber) -2. Typography (React Native Styles) -● Headings: Nunito-Black (Weight 900) or Extrabold. Rounded terminals are mandatory. -● Body: DMSans-Medium (Weight 500). Clean and geometric. -● Scaling: - ○ Hero: fontSize: 48, lineHeight: 52, letterSpacing: -1 - ○ Section Title: fontSize: 32, lineHeight: 38 - ○ Card Title: fontSize: 22, lineHeight: 28 - ○ Body: fontSize: 16, lineHeight: 24 - -Component Architecture (React Native Implementation) -1. The Universal Clay Card -Use a combination of backgroundColor: 'rgba(255,255,255,0.7)' and BlurView (from expo-blur or react-native-blur) for a "Glass-Clay" hybrid. -● Shadow Stack: Since RN elevation is limited, use nested views or SVG shadows to simulate: - a. Outer: offset: {12, 12}, blur: 24, color: 'rgba(160, 150, 180, 0.2)' - b. Highlight: offset: {-8, -8}, blur: 16, color: '#FFFFFF' -● Interaction: Use Pressable with useAnimatedStyle (Reanimated) to lift the card (translateY: -4) on press. -2. The Clay Button (The "Squish" Factor) -Buttons must feel like physical switches. -● Base: height: 56, borderRadius: 20, justifyContent: 'center'. -● Gradient: Use LinearGradient from #A78BFA to #7C3AED. -● Micro-Physics: * On Press In: scale: 0.92, shadowOpacity decreases. - ○ On Press Out: Spring back to scale: 1.0 with damping: 10. -● Shadow: Deep colored shadow matching the button hue. -3. The Recessed Input -Simulates a finger-press into the clay. -● Style: backgroundColor: '#EFEBF5', height: 64, paddingHorizontal: 24. -● Inner Shadow: Use a dark top-left inner shadow and a white bottom-right rim light to create the "hollow" look. - -Animation System (Powered by Reanimated) -1. Buoyancy (Float): Background blobs should use withRepeat(withTiming(...)) to drift ±20px over 8-10 seconds. -2. Clay Breathe: Stat orbs should subtly scale between 1.0 and 1.03 every 4 seconds. -3. Haptic Feedback: Every "Squish" interaction (Buttons/Cards) must trigger Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light). - -Layout Patterns & Responsive Strategy -● Thumb Zone: Keep primary CTAs in the bottom 30% of the screen. -● Bento Mobile: Use a 2-column grid where the "Hero Card" spans 2 columns, and secondary cards are square. -● Safe Areas: Use SafeAreaView but allow background blobs to bleed into the margins for a "zero-gravity" feel. - -Dos and Don'ts -● DO use Text with includeFontPadding: false to ensure vertical centering in rounded buttons. -● DO use Spring physics for all transitions; avoid linear easing. -● DO nested border radii: if a card is 32, the image inside is 24. -● DON'T use standard Android elevation. It creates flat, gray shadows that break the clay aesthetic. Use SVG-based shadows. -● DON'T use sharp corners (< 20). -● DON'T use pure black #000000 for text or shadows. - - -Enterprise(企业级/政企风) -现代SaaS美学平衡了专业性和可接近性。鲜艳的靛蓝/紫罗兰色渐变、柔和的阴影、等距深度和干净的几何无衬线字体。 -1. 后台管理系统 -2. 生产力工具与协作平台 -3. 政务与金融类严肃应用 -4. 垂直领域专业软件 -Here is a translated version of your web design prompt, adapted specifically for mobile application development (iOS/Android/React Native/Flutter). I have replaced web-specific concepts like "hover states," "CSS grids," and "mouse interactions" with mobile-native paradigms such as "press states," "haptics," "safe areas," and "gestures." - -App Design System Prompt: Modern Enterprise SaaS (Mobile) -1. Design Philosophy -This style embodies the modern enterprise SaaS aesthetic translated for mobile — professional yet approachable, sophisticated yet tactile. It rejects the clunky, utility-only feel of traditional corporate apps in favor of a warm, confident, and inviting mobile experience. -Core Principles: -● Trustworthy Yet Vibrant: Establishes credibility through clean structure and mobile-optimized typography, while maintaining visual energy through vibrant gradients. -● Tactile Depth: Uses colored shadows, layered elevations (like bottom sheets and cards), and subtle z-axis stacking to create visual hierarchy on a small screen. -● Refined Elegance: Every element is polished, focusing on fluid gestures, spring-physics animations, and sophisticated press states. -● Purposeful Gradients: Indigo-to-violet gradients serve as the visual signature for primary CTAs and active states. -● Mobile Polish: Generous spacing, safe-area awareness, and crisp typography create a premium, enterprise-ready feel in the palm of the hand. -Keywords: Trustworthy, Vibrant, Polished, Tactile, Modern, Approachable, Enterprise-Ready, Fluid. -2. Design Token System -Colors (Light Mode) -● Background: #F8FAFC (Slate 50) - A very subtle cool grey/white base for screen backgrounds. -● Surface: #FFFFFF (White) - For cards, bottom sheets, and elevated elements. -● Primary: #4F46E5 (Indigo 600) - The core brand color. Vibrant blue-purple. -● Secondary: #7C3AED (Violet 600) - For gradients, badges, and accents. -● Text Main: #0F172A (Slate 900) - High contrast, sharp for primary readability. -● Text Muted: #64748B (Slate 500) - For supporting text and inactive tab icons. -● Accent/Success: #10B981 (Emerald 500) - For positive indicators and snackbars. -● Divider/Border: #E2E8F0 (Slate 200) - Subtle separation for list items. -Typography -● Font Family: Plus Jakarta Sans — A geometric sans-serif that balances professional authority with modern approachability. High legibility on mobile screens. -● Scaling: Designed to support iOS Dynamic Type and Android font scaling. -● Font Weights: - ○ Display/Screen Titles: ExtraBold (800) - ○ Section Headers: Bold (700) - ○ Card Titles/Buttons: SemiBold (600) - ○ Body Text: Regular (400) -● Line Heights: Tight for titles (1.1-1.2), relaxed for body text (1.4-1.5) to ensure comfortable reading on narrow screens. -Radius & Border -● Screen Elements: 16pt/dp for large cards and bottom sheets. -● Interactive Elements: 8pt/dp for inputs and small chips. -● Buttons: Fully rounded (pill shape) or 12pt/dp for a modern tech feel. -● Borders: Hairline (0.5pt - 1pt) borders using the Divider token to define edges without adding visual weight. -Shadows & Depth -● Card Elevation: Soft, colored shadows replacing neutral grays. Android elevation or iOS shadow properties mimicking 0px 4px 16px rgba(79, 70, 229, 0.08). -● Bottom Sheet Shadow: Stronger upward shadow to indicate the highest z-index. -● Button Shadow (Primary): 0px 4px 12px rgba(79, 70, 229, 0.25) to make primary actions highly prominent. -3. Component Stylings -Buttons -● Primary CTA: Full width (minus screen padding) at the bottom of the screen. Background uses the Indigo-to-Violet gradient. White text. -● Secondary: White surface, slate border, slate text. -● Press State: Instead of web hover, use an active press state that slightly scales the button down (0.95x) and reduces shadow, paired with subtle haptic feedback. -Cards & Lists -● Base: White background, 16pt radius, hairline border, soft colored shadow. -● Layout: Horizontal scrolling (carousels) for feature discovery, vertical standard lists for data. -● List Items: Edge-to-edge with inset dividers, utilizing leading icons (in soft-colored circular backgrounds). -Inputs & Forms -● Style: White background, 8pt radius, light slate border. -● Active/Focused State: Border transitions to Indigo, label animates to a floating position, keyboard automatically presents. -● Error State: Red border with an immediate, subtle shake animation. -4. App-Specific Bold Choices -● Haptic Integration: Tied to micro-interactions. Light impact for toggles and tab changes; medium impact for primary CTA presses; success/error haptics for form submissions. -● Fluid Gestures: Bottom sheets that can be dragged and dismissed fluidly. Cards that support swipe-to-action (e.g., swipe left to archive) with color reveals underneath. -● Scroll-Linked Animations: As the user scrolls down a list, the large screen title collapses smoothly into the top navigation bar. -● Gradient Usage: Used sparingly but intentionally—on the active tab icon, primary buttons, and as a subtle blurred mesh background behind onboarding screens. -● Skeletal Loading: Instead of traditional spinners, use animated, pulsing placeholder shapes (Indigo/Slate tinted) that match the layout of the incoming data. -5. Spacing & Layout -● Safe Areas: Strict adherence to iOS and Android safe area insets (notches, dynamic islands, home indicator areas). -● Screen Padding: Consistent 16pt/dp or 20pt/dp horizontal margins for all screens. -● Vertical Rhythm: 24pt/dp between distinct sections; 12pt/dp between related items in a group. -● Navigation: - ○ Bottom Tab Navigation for primary destinations (3-5 items). - ○ Top App Bar for contextual actions, screen titles, and back buttons. -● Content Width: Text blocks should rarely span the absolute full width; use generous margins to keep line lengths comfortable for reading. -6. Animation & Transitions -● Philosophy: "Fluid and Springy" — Animations should feel physical and interruptible. -● Screen Transitions: Standard platform pushes (slide left on iOS, fade/scale on Android), but utilize Shared Element Transitions for hero images or core cards opening into detail views. -● Press Animations: Scale-down (scale: 0.97) on press down, spring back on release. -● Easing: Avoid linear animations. Use spring physics for layout changes and ease-out for opacities. -7. Iconography & Touch Targets -● Library: Lucide-react-native (or equivalent SVG system). -● Style: 2px stroke width, rounded caps and joins for a friendly, modern feel. -● Touch Targets: Crucial for mobile. Minimum tap area of 44x44 pt (iOS) or 48x48 dp (Android) for all interactive elements, even if the visual icon is only 24x24. -● Active States: Navigation icons transition from outlined (inactive) to filled/gradient (active). -8. Accessibility & Best Practices -● Dynamic Text: UI must not break when users increase their system font size. Text must wrap or truncate gracefully. -● Contrast: All text meets WCAG AA standards. Slate 900 on Slate 50 background is highly compliant. -● Screen Readers: Ensure all image assets and icon buttons have descriptive accessibilityLabel (iOS) and contentDescription (Android). -● Dark Mode Readiness: While this spec is light-mode first, tokens should be structured to easily invert (e.g., Slate 50 becomes Slate 900, gradients become slightly muted). -● Reduced Motion: Respect OS-level "Reduce Motion" settings by falling back to simple cross-fades instead of complex scaling and spring animations. - - - -Sketch(素描/手绘风) -有机的不稳定边框、手写排版、纸张纹理和俏皮的不完美。每个元素都是用记号笔和铅笔在纹理纸上勾勒出来的。 -1. 原型设计与早期创意提案 (Low-Fidelity Prototyping) -2. 创意品牌、艺术与独立文化项目 -3. 教育、绘本与儿童类产品 -4. 游戏化 UI 与 叙事解谜游戏 - -Design Philosophy -The Hand-Drawn mobile design style celebrates authentic imperfection and human touch within the palm of the hand. It rejects the sterile precision of typical mobile UI in favor of organic, playful irregularity that evokes a portable sketchbook, a pocket notebook, or a collection of sticky notes. -Core Principles: -● No Straight Lines: Every View and TouchableOpacity uses complex borderRadius logic or SVG masks to create wobbly edges that reject geometric perfection. -● Tactile Texture: Use background overlays or tiled images to simulate physical media like grain, paper, or notebook grids, making the screen feel like a physical surface. -● Playful Tilt: Elements utilize small transform: [{ rotate: '-1deg' }] properties to break the rigid mobile grid and create casual energy. -● Hard Offset Shadows: Strictly avoid elevation (Android) or shadowRadius (iOS) that creates blur. Use a secondary "shadow layer" View offset by (4px, 4px) to create a cut-paper, layered collage aesthetic. -● Handwritten Typography: Exclusively use handwritten fonts (Kalam, Patrick Hand). Typography should vary in size and rotation to look like quick notes written on the fly. -● Scribbled Overlays: Use absolute-positioned SVGs for flourishes like hand-drawn arrows, "tape" effects on images, and "X" marks for close buttons. -● Intentional Messiness: Embrace slight overlaps and asymmetrical margins that make the app feel spontaneous rather than manufactured. -● Emotional Intent: This style lowers the "fear of interaction" by appearing unfinished and approachable. It is perfect for journaling apps, creative tools, education, or any product that wants to feel human-centered rather than corporate. - -Design Token System -Colors (Single Palette - Light Mode) -● Background: #fdfbf7 (Warm Paper) -● Foreground: #2d2d2d (Soft Pencil Black) -● Muted: #e5e0d8 (Old Paper / Erased Pencil) -● Accent: #ff4d4d (Red Correction Marker) -● Border: #2d2d2d (Pencil Lead) -● Secondary Accent: #2d5da1 (Blue Ballpoint Pen) -Typography -● Headings: Kalam-Bold - High visual weight, felt-tip marker style. -● Body: PatrickHand-Regular - Highly legible but distinctly human. -● Scale: Large and readable for mobile. Headings should have lineHeight adjusted to allow for "scribbled" descenders. -Radius & Border -● Wobbly Borders: Since React Native doesn't support the slash / syntax for borderRadius, use unique values for each corner (e.g., borderTopLeftRadius: 15, borderTopRightRadius: 25, borderBottomLeftRadius: 20, borderBottomRightRadius: 10). -● Border Width: Bold and visible. borderWidth: 2 minimum; borderWidth: 3 for primary actions. -● Style: solid for containers; dashed for "cut-out" sections or empty states. -Shadows & Effects -● Hard Offset Shadows: Since shadowOpacity with 0 radius is inconsistent across platforms, implement as a "Shadow View" placed behind the main component. - ○ Standard: Offset { x: 4, y: 4 } with #2d2d2d. - ○ Active (Press): Offset { x: 0, y: 0 } (the button "sinks" into the paper). -● Paper Texture: Use a repeating pattern background image or a very subtle radial-gradient SVG overlay across the entire screen. - -Component Stylings -Buttons (TouchableOpacity) -● Shape: Irregular wobbly rectangle. -● Normal State: * White background, borderWidth: 3, Pencil Black text. - ○ Hard offset shadow view behind it. -● Pressed State (activeOpacity={1}): - ○ Main button transform: [{ translateX: 4 }, { translateY: 4 }]. - ○ The background "shadow view" is covered, simulating a physical press. -● Variant: "Post-it" Yellow (#fff9c4) for primary CTAs. -Cards/Containers -● Base Style: White background, wobbly border, slight rotation (-1deg or 1deg). -● Decoration Types: - ○ "Tape": A semi-transparent View (rgba(200, 200, 200, 0.5)) positioned absolutely at the top, slightly rotated to look like it holds the card. - ○ "Tack": A small red circle SVG at the top center. -● Speech Bubbles: Use a small SVG triangle attached to the bottom of the container for "hint" text or tooltips. -Inputs (TextInput) -● Style: Full box with wobbly borderRadius. -● Font: PatrickHand-Regular. -● Focus State: Border color changes to Blue Ballpoint (#2d5da1) with a slight increase in borderWidth. -● Placeholder: Muted Pencil (#2d2d2d60). - -Layout Strategy -● The "Anti-Grid": Avoid perfect alignment. Give adjacent cards slightly different rotation values. -● Layering: Use zIndex to stack "tape" over cards and cards over background scribbles. -● Visual Rhythm: Use padding: 20 for standard screens. Use large gap values between vertical elements to let the "paper" breathe. -● Overflow: Allow decorative elements (like a hand-drawn star) to bleed slightly off the edge of the screen. - -Non-Genericness (Bold Choices) -● Sketchy Loaders: Instead of a spinner, use an animation that "scribbles" a circle or toggles between three hand-drawn frames. -● Hand-Drawn Icons: Use icons that look like they were drawn with a 0.5mm fineliner (thick strokes, unclosed loops). -● Strikethrough: When a task is completed or an item is deleted, use a red "scribble-out" SVG overlay instead of a simple line. -● Wavy Dividers: Replace standard horizontal rules with a hand-drawn squiggly line SVG. - -Effects & Animation -● The "Jiggle": When a user triggers an error, use a Sequence animation that rapidly rotates the element between -2deg and 2deg. -● Snappy Transitions: Use LayoutAnimation.configureNext(LayoutAnimation.Presets.spring) for all layout changes to give a bouncy, physical feel. - -Mobile-First Strategy (React Native) -● Touch Targets: Ensure all wobbly buttons have a minimum hit area of 48x48 even if the visual "ink" is smaller. -● Safe Area: Ensure the "Paper" background extends into the SafeAreaView to maintain the illusion of physical media. -● Performance: Use useNativeDriver: true for rotations and transforms to keep the "hand-drawn" movements fluid at 60fps. -● Haptics: Add Haptics.impactAsync on button presses to reinforce the feeling of "clicking" a physical object. - - -Neumorphism(新拟物化) -通过单色背景上的双阴影拉伸和插入元素。柔软、触感好、物理接地,可接近性极佳。 -1. 极简主义硬件控制与智能家居 -2. 追求审美感的工具类 App -3. 金融与健康监测看板(非密集型) -4. 品牌营销与展示型页面 - -Design Philosophy (React Native / Mobile) -Core Principles: Neumorphism on mobile creates a tactile, physical interface through dual shadows. On React Native, this is achieved by layering View components or using specialized libraries to simulate the light source (top-left) and dark shadow (bottom-right). Every component is "molded" from the base material (#E0E5EC). Buttons are never flat; they are either "pillowed" (convex) or "wells" (concave/inset). -Vibe: Premium, ceramic-like feel. It prioritizes the "thump" of a physical button press. It is a calm, monochromatic experience that uses depth rather than color to establish hierarchy. -Unique Visual Signatures (Mobile Specific): -● Dual-Shadow Layering: Since React Native's shadowColor only supports one direction, use nested Viewcomponents or react-native-shadow-2 to achieve the dual-light effect. -● Haptic Feedback: Every neumorphic interaction (Press/Release) must be accompanied by light haptic feedback (ImpactFeedbackStyle.Light). -● Physical Press: Active states should use transform: [{ scale: 0.97 }] to simulate material compression. -● Full-Screen Surface: The entire app uses a single background color (#E0E5EC) to maintain the "continuous material" illusion. - -Design Token System (The DNA) -Colors (Light Mode - Cool Monochromatic) -● Background: #E0E5EC (The "Clay" base). -● Text Primary: #3D4852 (7.5:1 contrast). -● Text Muted: #6B7280 (4.6:1 contrast). -● Accent: #6C63FF (Used for active toggles or primary CTAs). -● Shadow Light: rgba(255, 255, 255, 0.6) (Top-Left). -● Shadow Dark: rgba(163, 177, 198, 0.7) (Bottom-Right). -Typography -● Font Family: Use System (San Francisco/Roboto) or PlusJakartaSans-Bold. -● Sizing: * Heading: 24pt-32pt, Bold. - ○ Body: 16pt, Medium. - ○ Caption: 12pt, Regular. -● Letter Spacing: -0.5 for headings to enhance the modern look. -Radius (React Native borderRadius) -● Large Container: 32 -● Standard Button/Input: 16 -● Pill/Search Bar: 999 - -Shadows & Effects (The Physics) -Note: React Native requires specific shadow properties for iOS and elevation for Android. For high-fidelity Neumorphism, use a wrapping implementation. -Extruded (Convex - Resting) -● Outer Shadow (Dark): shadowOffset: { width: 6, height: 6 }, shadowOpacity: 1, shadowRadius: 10, shadowColor: 'rgba(163, 177, 198, 0.7)' -● Inner Shadow (Light): shadowOffset: { width: -6, height: -6 }, shadowOpacity: 1, shadowRadius: 10, shadowColor: 'rgba(255, 255, 255, 0.6)' -Inset (Concave - Pressed/Input) -● Concept: Use a slightly darker background color (#D1D9E6) or internal gradients/shadows to simulate a "carved out" look. -● Visual: backgroundColor: '#E0E5EC', plus internal borders of 1px with a darker shade at the top. - -Component Styling (React Native) -Buttons -● Base: height: 56, borderRadius: 16, justifyContent: 'center', alignItems: 'center'. -● Default State: Extruded dual shadow. -● Active State (Pressable): Remove outer shadows, apply scale: 0.98, and change background slightly to indicate an "inset" state. -● Haptics: Trigger Haptics.impactAsync on onPressIn. -Cards -● Base: padding: 24, borderRadius: 32, backgroundColor: '#E0E5EC'. -● Hierarchy: Use "Nested Depth." A card (Extruded) contains an Icon Container (Inset Deep). -Inputs -● Style: height: 50, borderRadius: 16, paddingHorizontal: 16. -● Depth: Must be Inset (Concave) to look like a physical slot. -● Focus: Border color changes to #6C63FF with a thin 1.5px width. - -Animation & Micro-interactions -● Framework: Use React Native Reanimated or LayoutAnimation. -● Transitions: Duration: 250ms, Easing: Bezier(0.4, 0, 0.2, 1). -● Interaction: * Scale: Buttons shrink by 2-3% when pressed. - ○ Shadow Transition: Smoothly interpolate shadow opacity from 1 to 0.4 when an element is "pressed" into the surface. - -Layout & Accessibility -● Safe Area: Always wrap in SafeAreaView. -● Touch Targets: Minimum 48x48 for all interactive elements. -● Spacing: Use a 8pt grid system (8, 16, 24, 32, 48). -● Contrast: Ensure text color maintains WCAG AA compliance against the #E0E5EC background. - -Anti-Patterns (Do Not Do) -● No Black Shadows: Never use rgba(0,0,0,x). Shadows must be blue-grey to match the "Cool Grey" material. -● No Pure White Backgrounds: The screen background MUST be #E0E5EC. -● No Sharp Edges: Avoid borderRadius < 12. -● No Flat Borders: Do not use borderWidth: 1 as a substitute for depth unless in a focus state. \ No newline at end of file diff --git a/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/data/draft.csv b/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/data/draft.csv deleted file mode 100644 index cac862f8..00000000 --- a/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/data/draft.csv +++ /dev/null @@ -1,1779 +0,0 @@ -# NOTE: 此文件仅作为设计备份/参考文档,当前搜索引擎与 CLI 不会读取或执行本文件内容。 - - -Bauhaus(包豪斯) -大胆的几何现代主义,包含圆形、方形和三角形。主色调(红/蓝/黄),边缘鲜明,阴影强烈。功能性与艺术性兼备,带有建构主义的不对称。 -1. 极简主义品牌官网与电商 -2. 工具类与高效率 App -3. 数字媒体与在线杂志 - - -Design Style: Bauhaus (Mobile) -1. Design Philosophy -The Bauhaus style embodies the revolutionary principle "form follows function" while celebrating pure geometric beauty and primary color theory. On mobile, this translates to tactile constructivism—the screen is a canvas where elements are physically stacked blocks. The aesthetic creates a high-impact, poster-like experience in the palm of the hand: bold, touch-centric, and unapologetically graphic. -Vibe: Tactile, Constructivist, Geometric, High-Contrast, Bold, Architectural -Core Concept: The mobile interface is a vertical geometric composition. Scroll views are not just content streams but stacked layers of colored paper and rigid shapes. The limited screen real estate forces a focus on massive typography and distinct color blocking (Red #D02020, Blue #1040C0, Yellow #F0C020). Every tap should feel mechanical and substantial. -Key Characteristics: -● Geometric Purity: UI elements are strictly circles (buttons/avatars) or squares (cards/inputs). -● Tactile Depth: Hard shadows indicate interactivity; elements look "pressable." -● Color Blocking: distinct screen sections use solid primary colors to separate content without whitespace. -● Thick Borders: 2px and 3px black borders ensure legibility and distinct separation on small screens. -● Vertical Rhythm: Strong vertical stacking with deliberate spacing, avoiding clutter. -● Constructivist Typography: Headlines are massive (text-4xl to text-5xl) relative to screen width, breaking traditional mobile scaling rules. -● Thumb-Friendly: Interactive zones are large, distinct, and geometrically defined. -2. Design Token System (The DNA) -Colors (Single Palette - Light Mode) -The palette remains strictly Bauhaus primaries, optimized for high contrast outdoors. -● background: #F0F0F0 (Off-white canvas) -● foreground: #121212 (Stark Black) -● primary-red: #D02020 (Bauhaus Red) -● primary-blue: #1040C0 (Bauhaus Blue) -● primary-yellow: #F0C020 (Bauhaus Yellow) -● border: #121212 (Thick, distinct borders) -● muted: #E0E0E0 -Typography -● Font Family: 'Outfit' (geometric sans-serif). -● Font Import: Outfit:wght@400;500;700;900 -● Scaling: Aggressive scaling adapted for vertical viewports. - ○ Display: text-4xl → text-5xl (Massive headers taking up 30-40% of screen width) - ○ Subheadings: text-xl → text-2xl - ○ Body: text-base (16px minimum for legibility) - ○ Button Text: text-lg (Large for readability) -● Weights: - ○ Headlines: font-black (900) uppercase, tight tracking (tracking-tighter) - ○ Buttons/Nav: font-bold (700) uppercase, wide tracking (tracking-wide) - ○ Body: font-medium (500) -● Line Height: Tight (leading-none) for headlines to save vertical space; leading-relaxed for body text. -Radius & Border -● Radius: Strict Binary—rounded-none (0px) for layout blocks, inputs, and cards; rounded-full (9999px) for primary action buttons and avatars. -● Border Widths: - ○ Standard Elements: border-2 (2px) - ○ Major Containers/Bottom Nav: border-t-2 or border-3 - ○ Separators: divide-y-2 -● Border Color: Always #121212 (black). -Shadows/Effects -● Hard Offset Shadows (Tactile feedback): - ○ Small elements (Tags/Icons): shadow-[2px_2px_0px_0px_black] - ○ Buttons/Cards: shadow-[4px_4px_0px_0px_black] (Reduced from web 8px to save screen width) - ○ Floating Action Button (FAB): shadow-[5px_5px_0px_0px_black] -● Touch Feedback: active:translate-x-[2px] active:translate-y-[2px] active:shadow-none (Instant mechanical depression). -● Patterns: - ○ Dot grid backgrounds on "Paper" colored sections (background-size: 16px 16px). - ○ Screen transitions: Slide-over with hard black borders. -3. Component Stylings -Buttons (Touch Targets) -● Minimum Size: Height h-12 or h-14 (48px-56px) for thumb accessibility. -● Variants: - ○ Primary (CTA): w-full bg-[#D02020] text-white border-2 border-black shadow-[4px_4px_0px_0px_black] - ○ Secondary: w-full bg-[#1040C0] text-white border-2 border-black shadow-[4px_4px_0px_0px_black] - ○ Floating Action (FAB): h-14 w-14 rounded-full bg-[#F0C020] border-2 border-black shadow-[4px_4px_0px_0px_black] flex items-center justify-center -● Shapes: Full-width rectangular buttons (rounded-none) or pill-shaped (rounded-full) for bottom-sticky actions. -● States: NO hover. Focus on active state (press down effect). -Cards (Mobile Stack) -● Base Style: White background, border-2 border-black, shadow-[4px_4px_0px_0px_black], mb-6. -● Decoration: - ○ Geometric badge in top-right: absolute top-0 right-0 h-8 w-8 bg-[#F0C020] border-l-2 border-b-2 border-black flex items-center justify-center. -● Interaction: Entire card is a touch target. active:translate-x-[1px] active:translate-y-[1px] active:shadow-[2px_2px_0px_0px_black]. -● Content: Image usually takes top half (aspect-video), bold text below. -Bottom Navigation (The Anchor) -● Container: fixed bottom-0 w-full bg-white border-t-2 border-black z-50 h-20. -● Grid: 3 to 5 items equally spaced. -● Items: - ○ Inactive: Black stroke icon, clear background. - ○ Active: Icon inside a geometric shape (Square/Circle) with primary color fill (e.g., Red Square context). - ○ Label: Tiny uppercase bold text below icon, or icon only for minimalism. -Inputs & Forms -● Field: h-12 bg-white border-2 border-black rounded-none px-4 text-black placeholder:text-gray-400 focus:bg-[#FFF9C4] focus:ring-0 focus:border-black. -● Label: Uppercase bold, mb-1 block. -● Checkbox: h-6 w-6 appearance-none border-2 border-black bg-white checked:bg-[#1040C0] rounded-none. -4. Layout & Spacing -● Container: w-full with px-5 (20px) padding. Max-width constraints removed (fluid mobile). -● Section Padding: - ○ Standard: py-8 - ○ Hero: pt-12 pb-16 -● Grid Systems: - ○ Main Layout: Single column (Stack). - ○ Micro-grids: 2-column for stats/gallery (gap-3 or gap-4). -● Safe Areas: Respect pt-safe (top notch) and pb-safe (bottom home indicator). -● Dividers: Thick section separators border-b-2 border-black usually paired with a background color change. -5. Non-Genericness (Bold Choices) -Mobile Constraints require distinct personality to avoid looking like a wireframe: -● Full-Screen Color Washes: - ○ Onboarding screens: Full solid backgrounds (Red Screen → Blue Screen → Yellow Screen). - ○ Success states: Full Yellow background (bg-[#F0C020]) with massive black centered checkmark. - ○ Error states: Full Red background (bg-[#D02020]) with white text. -● Header Identity: - ○ Instead of a standard navbar, use a "Bauhaus Header Block": A row containing a Square (Menu), a flexible space for the Title (Left aligned, massive), and a Circle (Profile/Search). All elements separated by vertical borders divide-x-2 border-b-2 border-black. -● Geometric Lists: - ○ List items aren't just text lines. They are alternating geometric bullets: Line 1 uses a ■, Line 2 uses a ●, Line 3 uses a ▲. -● Rotated "Stickers": - ○ "New", "Sale", or Notification badges are rotated -12deg or +12deg, sitting on top of corners to break the rigid grid. -● Image Filters: - ○ Thumbnails: Grayscale + High Contrast. - ○ Detail View: Full color, no rounded corners, thick black border. -6. Icons & Imagery -● Icon Library: lucide-react-native or similar (Circle, Square, Triangle, Menu, X, ArrowLeft). -● Icon Style: - ○ Stroke: stroke-[2px]. - ○ Size: w-6 h-6 (standard) or w-8 h-8 (hero actions). -● Icon Containers: - ○ Navigation icons often live inside rigid 48x48px bordered squares. - ○ Back buttons are always circular rounded-full with a border. -● Imagery: - ○ Masking: Use CSS clip-paths to crop header images into non-standard shapes (e.g., a trapezoid or a circle cropped at the bottom) within the mobile frame. -7. Responsive Strategy (Device Sizes) -● Small Phones (SE/Mini): - ○ Reduce display text to text-3xl. - ○ Stack stats vertically (1-col). - ○ Reduce padding to px-4. -● Large Phones (Max/Plus): - ○ Display text scales to text-5xl. - ○ Stats can use 2-col grid. - ○ Card images gain more height. -● Orientation: - ○ Portrait (Primary): Vertical stacking. - ○ Landscape: Avoid if possible, or split screen 50/50 (Text Left / Image Right). -8. Animation & Micro-Interactions -● Feel: Physical, snappy, zero-latency. -● Duration: duration-150 (Very fast). -● Easing: ease-in-out (Sharp stops). -● Interactions: - ○ Tap: The element physically depresses (translate X/Y matches shadow size). - ○ Drawer Open: Slides in from Left/Right with a solid black border line leading it. No soft fades—it slides like a mechanical door. - ○ Toast/Alerts: Drop down from top as a solid geometric block (Yellow/Red) with hard borders, swinging slightly like a hanging sign. - ○ Scroll: Sticky headers snap into place instantly. - - - - -"Monochrome(单色/黑白) -"一套简洁的编辑设计体系,建立在纯粹的黑白之上。没有点缀色——只有戏剧性的对比、超大衬线字体和精准的几何布局。让人联想到高端时尚专题和建筑作品集。严肃、成熟、毫不掩饰地大胆。 -"1. 极简主义电商与高端奢侈品 -2. 专业创意与文档编辑工具 -3. 数据密集型仪表盘(局部应用) -4. 实验性与先锋感网站" - - -Design Style: Minimalist Monochrome (Mobile) -Design Philosophy -Core Principle -The Pocket Editorial. On mobile, Minimalist Monochrome transforms the screen into a tactile, high-end printed manifesto. It rejects the "app-like" tendency for soft bubbles and friendly gradients. Instead, it offers a stark, vertical journey defined by pure black (#000000), pure white (#FFFFFF), and razor-sharp borders. Every tap is a deliberate decision; every scroll is a flip of a page in a luxury monograph. -Visual Vibe -Emotional Keywords: Tactile, Austere, Editorial, Direct, High-Fidelity, Uncompromising, Sharp, Rhythmic. -This is the visual language of: -● Mobile typographers and digital brutalism -● The mobile interfaces of luxury fashion houses (Balenciaga, Zara, SSENSE) -● Digital exhibitions where the content frames itself -● High-contrast e-reader aesthetics -What This Design Is NOT -● ❌ "App-y" (no cards with drop shadows, no floating bubbles) -● ❌ Native iOS/Android standard (no system blue, no rounded groups) -● ❌ Gesture-heavy (interactions are explicit taps, not vague swipes) -● ❌ Cluttered (one idea per screen view) -● ❌ Colorful (strictly grayscale) -The DNA of Minimalist Monochrome (Mobile) -1. Vertical Linearity -The mobile screen is a continuous roll of paper. Structure is created not by boxes, but by horizontal lines (rules) that span the full width of the device. Content lives between these lines. -2. Typography as Interface -Buttons are often just large words. Navigation is text-based. The serif typeface (Playfair Display) acts as the primary image on the screen. Headlines must be large enough to break words onto new lines, creating graphic shapes. -3. The "Touch" Inversion -Since there is no "hover" on mobile, interaction is communicated through inversion. When a user touches a white block, it turns black instantly. This zero-latency binary feedback replaces physical tactility. -4. Zero Radius, Zero Fluff -All elements—buttons, images, inputs, modals—have strictly 0px border radius. This sharp geometry cuts through the rounded physical corners of modern smartphones, creating a striking contrast. - -Design Token System -Colors (Strictly Monochrome) -background: #FFFFFF (Pure white) -foreground: #000000 (Pure black) -muted: #F5F5F5 (Off-white for "pressed" states or subtle blocks) -mutedForeground: #525252 (Dark gray for metadata) -border: #000000 (Black borders - heavy usage) -borderLight: #E5E5E5 (Subtle dividers) -overlay: #000000 (Full screen menu background) - -Rule: No accent colors. The "Active State" is simply the inverse of the "Default State." -Typography -Font Stack: -● Display: "Playfair Display", serif - For all headlines and large numerals. -● Body: "Source Serif 4", serif - For reading text. -● UI/Labels: "JetBrains Mono", monospace - For tiny tags, dates, and technical specs. -Type Scale (Mobile Optimized): -Note: Sizes are calibrated to feel "massive" on a small screen without breaking layout. -xs: 0.75rem (12px) - Metadata / Breadcrumbs -sm: 0.875rem (14px) - UI Labels / Captions -base: 1rem (16px) - Body text (Legibility minimum) -lg: 1.125rem (18px) - Lead text / Button text -xl: 1.5rem (24px) - Section headers -2xl: 2rem (32px) - Standard Headlines -3xl: 2.5rem (40px) - Hero Sub-text -4xl: 3rem (48px) - Major Headlines -5xl: 4rem (64px) - "Cover" text (Often wraps) -6xl: 5rem (80px) - Numerical statements - -Tracking & Leading: -● Headlines: tracking-tighter (-0.05em) and leading-[0.9]. Text should feel tightly packed. -● Body: leading-relaxed for readability. -● Button Text: tracking-widest uppercase. -Border Radius -ALL VALUES: 0px - -Constraint: Even the bottom sheet (modal) must be square. Even the active state of a tapped element is a sharp rectangle. -Borders & Lines (The Grid) -hairline: 1px solid #E5E5E5 (List separators) -thin: 1px solid #000000 (Standard element borders) -thick: 2px solid #000000 (Emphasis / Input bottom) -heavy: 4px solid #000000 (Section dividers) - -Usage: -● Full-Bleed Lines: Borders should often touch the edges of the screen (-mx-4 or -mx-6). -● Separators: Use border-b black heavily to separate stacked vertical content. -Shadows -NONE - -Depth is strictly 2D. Layers are defined by opacity (100% vs 0%) or borders, never by shadows. -Textures & Patterns -Use subtle noise to simulate high-quality paper texture on mobile backgrounds to avoid a "sterile" feeling. -Mobile Noise (CSS): -CSS - -background-image: url("data:image/svg+xml,..."); /* Same noise SVG as web */ -opacity: 0.03; /* Slightly higher opacity for small screens */ - - -Component Stylings -Buttons & Touch Targets -Primary Button (The Block): -- Width: w-full (Full width strongly preferred) -- Height: h-14 (56px - Large touch target) -- Background: #000000 -- Text: #FFFFFF -- Radius: 0px -- Typography: Uppercase, Mono or Serif, Tracking-widest -- Active State: Instantly inverts to White bg, Black text - -Secondary Button (The Outline): -- Width: w-full -- Height: h-14 -- Background: Transparent -- Border: 1px solid #000000 -- Active State: Instantly fills Black - -Sticky Bottom Action (CTA): -● A fixed bar at the bottom of the viewport. -● border-t-2 border-black. -● Contains a single primary action or a price + action pair. -● Background: #FFFFFF (or inverted #000000). -Cards / List Items -The "Stacked" Card: -Instead of a box with padding, mobile cards are often full-width segments separated by borders. -- Container: Border-bottom 1px solid #000000 -- Padding: py-6 -- Image: Aspect ratio 4:5 or 1:1, full width or padded -- Content: Text sits directly below image -- Interaction: Touching anywhere in the segment triggers "Active" state - -Navigation -The "Bar": -● Top: Minimal. Just a logo (Left) and a "MENU" text button or Hamburger (Right). -● Bottom: Optional. If used, simple text labels or thin outline icons. border-t-2 border-black. -The Menu Overlay: -● Full screen. -● Background: #000000 (Black). -● Text: #FFFFFF (White). -● List items: Massive text (text-4xl), serif, centered or left-aligned. -● Divider: Thin white lines between menu items. -Inputs -Mobile Input: -- Style: Flush with background. -- Border: Bottom only (2px solid black). -- Radius: 0px. -- Height: h-14. -- Focus: Border becomes 4px thick. No native blue glow. -- Clear Button: Simple 'X' icon in black. - - -Layout Strategy -Safe Areas -● Respect pb-safe (Home Indicator) and pt-safe (Status Bar). -● Style Note: The Status Bar should be white text on black header, or black text on white header. High contrast. -Container & Spacing -● Horizontal Padding: px-5 (20px) or px-6 (24px). -● Vertical Rhythm: - ○ Small gaps: py-4 - ○ Section gaps: py-16 - ○ Between text and image: my-6 -The "Scroll" -● Design for the scroll. Use sticky headers for section titles (e.g., "Details", "Shipping") that stack as the user scrolls down, creating a filing cabinet effect. -● Use divide-y divide-black for lists to create strong separation. - -Effects & Animation -Motion Philosophy: Cut, Don't Fade. -Mobile transitions should feel mechanical and instant, like a shutter clicking. -Interactions: -● Tap: active:bg-black active:text-white (Instant inversion). -● Page Transition: Slide in from right (100% width) with a hard edge. No distinct shadow on the sliding page, just a clean line. -● Modal: Slides up from bottom. 0px radius. Covers 100% or 90% of screen. Black border top. -● Scroll Parallax: None. Keep scrolling tied 1:1 to finger movement. -Specific Implementation: -TypeScript - -// Mobile Button Active State -className="bg-black text-white active:bg-white active:text-black active:border active:border-black transition-none" - -// Image Loading -className="opacity-0 data-[loaded=true]:opacity-100 transition-opacity duration-300 ease-linear grayscale data-[loaded=true]:grayscale-0" - - -Iconography -Style: Stroke-based, Thin, Sharp. -Library: Lucide React (or similar). -Mobile Specifics: -● Size: w-6 h-6 (Standard) -● Stroke: 1.5px (Matches the fine aesthetic) -● Touch: If an icon is a button, it must have a p-3 (12px) padding hit-box around it, even if the border is invisible. - -Responsive Strategy (Device Sizes) -Small Phones (iPhone SE/Mini): -● Headlines: Scale down to text-4xl to prevent breaking single words into nonsense. -● Padding: Reduce to px-4. -Large Phones (Max/Plus): -● Headlines: text-5xl or text-6xl. -● Layout: Can introduce a 2-column grid for product thumbnails (but keep strict borders between them). -Dark Mode: -● System: Enforce Light Mode (Black on White) as the default brand identity. -● Inverted Sections: Use Dark Mode (White on Black) only for specific sections (Footer, Hero, Special Features) to create rhythm, not based on system settings. - -Bold Choices (Non-Negotiable) -1. Massive Typographic Hero: The opening screen must feature a headline where a single word might span the full width. -2. The "Hard" Line: A 4px black line (border-b-4) must separate the Hero from the content. -3. Sticky Section Headers: As you scroll, the section title (e.g., "01. INFO") sticks to the top with a bottom border, stacking on top of the previous one. -4. No Hamburger Icon: Use the word "MENU" in JetBrains Mono instead of an icon, if space permits. -5. Inverted Gallery: Image galleries have a Black background with images at full opacity. -6. Editorial Inputs: Form labels look like subheadings (Playfair Display, Italic). -7. No Skeletons: Loading states are simple spinning black lines or just whitespace. No gray pulsing blobs. -8. Mechanical Feedback: Every interactive element MUST have a visible active state (color inversion). - -What Success Looks Like (Mobile) -A successfully implemented Minimalist Monochrome mobile design should feel like: -● A digitally printed receipt from a high-end boutique. -● A pocket edition of a brutally honest manifesto. -● Clean, fast, and remarkably legible outdoors. -● Intimidatingly simple. -It should NOT feel like: -● A scaled-down version of a desktop site. -● A standard Bootstrap/Tailwind mobile layout. -● An app trying to be "friendly." - - - - -Modern Dark(现代深色模式) -一种电影般的高精度暗模式设计,通过动画渐变斑点、鼠标跟踪聚光灯效果和精心制作的微交互实现分层环境照明,感觉就像高级软件。 -"1. 影音娱乐与流媒体平台 -2. 开发者工具与专业生产力软件 -3. 金融、科技与 AI 仪表盘 -4. 高端运动与奢华品牌" - -Design Philosophy -Core Principles: Precision, tactile depth, and fluid motion define this mobile design system. For React Native, every component is treated as a physical layer within a stacked 3D space, illuminated by virtual light sources. The design communicates "high-end mobile utility"—as responsive and refined as the Linear mobile app or Apple’s native interface. Every shadow uses multiple layers, every gradient is dithered to prevent banding, and every interaction uses precise haptic feedback combined with Expo.out easing. -Vibe: Cinematic Technical Minimalism. It’s a "Dark Mode First" aesthetic, utilizing near-blacks (#050506) and deep indigos. Imagine a high-end pro tool interface viewed through frosted glass at night. It is technical but inviting, using soft "glow" sources (#5E6AD2) to guide the thumb. It should feel like a premium native iOS/Android app, not a web-wrapper. -Differentiation: The signature is Atmospheric Depth. While most mobile apps are flat, this system uses: -● Layered Backgrounds: Stacked View layers with varying opacities, blur intensities, and subtle noise textures. -● Animated Ambient Light: Large, slow-moving blurred Canvas objects (using react-native-skia or Animated) that simulate light pools. -● Haptic-Linked Interactions: Every "Pressable" event is paired with subtle haptics (Impact Light/Medium) to reinforce the "hardware" feel. -● Spring-less Precision: Eschewing "bouncy" animations for sophisticated Bezier(0.16, 1, 0.3, 1) curves. - -Design Token System (The DNA) -Color Strategy: Deep Space & Ambient Light -Token -Value -Usage -bg-deep -#020203 -Screen background, Bottom Tab bar base -bg-base -#050506 -Primary view container background -bg-elevated -#0a0a0c -Modals, Bottom sheets, Surface cards -surface -rgba(255,255,255,0.05) -Card backgrounds, list items -foreground -#EDEDEF -Primary text (High contrast) -foreground-muted -#8A8F98 -Secondary labels, body text -accent -#5E6AD2 -Buttons, active icons, primary brand glows -accent-glow -rgba(94,106,210,0.2) -Ambient shadows and light emission -border-default -rgba(255,255,255,0.08) -Subtle hairline dividers (0.5pt on iOS) - -Typography System (React Native) -Font Family: Inter, GeistSans-Regular, or System San Francisco/Roboto. -Level -Size -Weight -Letter Spacing -Display -48 -700 --1.5 -H1 -32 -600 --0.5 -H2 -24 -600 --0.5 -H3 -18 -600 -0 -Body -16 -400 -0 -Label/Mono -12 -500 -1.2 (Uppercase) -Gradient Text: Implement using mask-view and react-native-linear-gradient. Headers should transition from #FFFFFF to rgba(255,255,255,0.7) vertically. - -Component Styling Principles -1. The Background System -Never use a single solid color for the screen. -● Base: A LinearGradient from #0a0a0f at the top to #020203 at the bottom. -● The "Blobs": Use AbsoluteFill views with borderRadius equal to half their width, high blurRadius (30-50), and low opacity (0.1). -● Animation: Use useAnimatedStyle from Reanimated to slowly oscillate the translateX and translateY of these light pools. -2. Cards & Containers -● Radius: Always borderRadius: 16. -● Border: Use borderWidth: StyleSheet.hairlineWidth with rgba(255,255,255,0.1). -● Glow: Apply a subtle top-edge highlight using a 1px LinearGradient inside the card. -● Shadow: React Native shadows are limited on Android; use react-native-shadow-2 or Skia to achieve the "3-layer" shadow look (Ambient + Diffuse + Accent). -3. Buttons (Pressables) -● Primary: Background #5E6AD2. Text #FFFFFF. -● Interaction: On onPressIn, scale to 0.97. On onPressOut, scale back to 1.0. Use Selection haptic feedback. -● Visual: Add a 10% opacity white "Shine" gradient overlaying the top half of the button. -4. Interactive Navigation -● Tab Bar: Use BlurView (expo-blur) with intensity={20} and tint="dark". -● Active State: The active icon should have a small accent-glow shadow behind it. -● Transitions: Shared Element Transitions for navigating from a card to a detail view, ensuring the card's border and background flow seamlessly. - -Layout & Motion -Spacing Scale -● Base Unit: 4pt. -● Screen Margins: 20pt (Standard) or 16pt (Tight). -● Gaps: Use gap (available in modern RN) with values of 8, 12, 16, or 24. -Animation Specs -● Easing: Easing.bezier(0.16, 1, 0.3, 1) (Expo Out). -● Duration: * Micro-interactions (Toggle/Press): 200ms. - ○ Screen Transitions: 400ms. - ○ Modals (Bottom Sheet): 500ms with custom spring (damping: 20, stiffness: 90). - -Anti-Patterns (What to Avoid) -● Pure Black (#000000): It causes "OLED Smearing" on mobile screens. Stick to #050506. -● Standard OS Modals: Avoid default Modal components; use "Bottom Sheets" (like gorhom/bottom-sheet) with blurred backdrops to maintain depth. -● Full Opacity Borders: Never use solid grey borders. Always use rgba white/black for natural blending. -● Laggy Blobs: Do not use too many blurRadius effects on the main JS thread. Always use useNativeDriver: true or Skia. - -The "Bold Factor" (Signature Elements) -1. Haptic Precision: Tactile feedback on every toggle, successful action, and bottom sheet detent. -2. Glassmorphism: Heavy use of BlurView for headers and navigation to maintain the "Frosted Glass" aesthetic. -3. Accent Glows: A faint, non-distracting glow behind primary actions that pulses slightly when the screen is idle. -4. Bento Layouts: For dashboards, use Flexbox to create asymmetric cards (e.g., one card taking 60% width, the other 40% with different heights). - - -SaaS(软件即服务) -一个大胆、极简主义的现代视觉系统,将简洁的美学与动态的执行相结合。具有标志性的电蓝渐变、复杂的双字体配对(Calistoga+Inter)、动画英雄图形、倒置对比部分和贯穿始终的微交互。专业而前卫的设计——自信而不杂乱。 -1. 业务管理与协作系统 (B2B / Operations) -2. 开发者工具与云服务平台 -3. 企业内部工具与人力资源管理 (HRM) -4. 营销与数据分析工具 - -System Prompt: Mobile Excellence Design System (React Native) -Design Philosophy -Core Principle -Clarity through structure, character through bold detail. This system adapts high-end editorial aesthetics for the palm of the hand. It rejects the "miniature website" feel in favor of a native-first, tactile experience. -Whitespace is a precision instrument for thumb-driven navigation. Motion is not decoration; it is spatial feedback. Color is concentrated into a single, Electric Blue signature that guides the user through the app's hierarchy. -The Visual Vibe -"High-Tech Boutique." Imagine a premium fintech app combined with a modern design portfolio. It feels engineered, yet artistic; minimal, yet alive. -● Confident: Bold typography and vibrant accents. -● Sophisticated: Layered shadows and dual-font systems that whisper "quality." -● Tactile: Haptic-ready interactions and fluid spring animations. -● Premium: Generous "safe areas" and elevated surfaces. - -The DNA of This Style -1. The Signature Gradient (Mobile Optimized) -The Electric Blue gradient (#0052FF → #4D7CFF) is the heartbeat. In React Native, this is implemented via react-native-linear-gradient. -● Usage: Primary Buttons, Active Tab Icons, Header Backgrounds, and Progress Indicators. -● Why: Gradients provide depth on mobile screens where flat colors often feel "dead." -2. Physical Depth & Living Elements -Mobile is a 3D space. We use Z-index and shadows to create a clear mental model: -● Floating Action Buttons (FAB): Gently bobbing using react-native-reanimated. -● Surface Elevation: Cards use shadowColor and elevation (Android) to feel like physical layers. -● Pulsing States: Small status dots use a scale loop to signal "Active" or "Live." -3. Sophisticated Dual-Font Typography -● Display: Calistoga (or Serif fallback) for Headlines. It adds human warmth to the digital interface. -● UI/Body: Inter (or System Sans-Serif). The workhorse for readability. -● Technical: JetBrains Mono for labels and data points. -4. Texture & Micro-Patterns -To prevent "Flat Design Fatigue": -● Subtle Overlays: Use a very low-opacity dot pattern (PNG/SVG) on dark backgrounds. -● Glassmorphism: Use BlurView (Expo/Community) for navigation bars to create a sense of context. - -Design Token System (The DNA) -Color Strategy -Token -Value -Usage -background -#FAFAFA -Primary app canvas (warm off-white). -foreground -#0F172A -Deep slate for primary text and dark sections. -muted -#F1F5F9 -Secondary surfaces (gray fills). -accent -#0052FF -Primary actions and brand touchpoints. -accent-sec -#4D7CFF -Gradient endpoint. -card -#FFFFFF -Pure white for elevated components. -border -#E2E8F0 -Hairline dividers (0.5pt to 1pt). -Typography Scale -● Hero (H1): 36pt - 42pt | Calistoga | Leading 1.1 -● Section (H2): 28pt - 32pt | Calistoga | Leading 1.2 -● Body: 16pt - 18pt | Inter | Leading 1.5 -● Label: 12pt | JetBrains Mono | Uppercase | Letter Spacing 1.5 - -Component Specifications (React Native) -1. Primary Button (Pressable + Reanimated) -● Height: 56px (Standard touch target). -● Radius: 16px (Rounded-2xl). -● Interaction: * On onPressIn: Scale down to 0.96. - ○ On onPressOut: Spring back to 1.0. -● Style: Linear Gradient background with shadow-accent. -2. The Section Badge -A consistent pattern for orienting the user: -JavaScript - -// Structure - - - SECTION NAME - - -3. Content Cards -● Border: 1pt hairline in Slate-200. -● Shadow: * iOS: shadowOpacity: 0.1, shadowRadius: 10, shadowOffset: {width: 0, height: 4}. - ○ Android: elevation: 4. -● Padding: 24px (Consistent gutter). - -Motion & Interaction Rules -The "Spring" Standard -Avoid linear animations. Use Spring Config for all transitions: -● mass: 1, damping: 15, stiffness: 120. -Entrance Animations -● Staggered Fade-In: Content should slide up (Y: 20 -> 0) and fade in (Opacity: 0 -> 1) as the screen mounts. -● Layout Transitions: Use LayoutAnimation or Reanimated's entering prop for seamless list updates. - -Implementation Instructions for AI -1. Strict Styling: Use StyleSheet.create or a utility-first library like NativeWind (Tailwind for RN). -2. Safe Areas: Always wrap root content in SafeAreaView. -3. Touch Targets: Ensure all interactive elements are at least 44x44px. -4. Icons: Use Lucide-React-Native or Expo Vector Icons (Feather/Ionicons). -5. Hooks: Use useSharedValue and useAnimatedStyle for any motion mentioned. -Example Signature Block: -When generating a screen, always include a "Design Note" explaining how the Asymmetry and Gradient Accent are applied to ensure it doesn't look like a generic template. - - -Terminal(终端/命令行) -一种原始的、功能性的、复古的、未来主义的命令行界面美学。高对比度、等宽精度和闪烁光标。 -1. 开发者工具与极客类应用 -2. 区块链、Web3 与 加密货币项目 -3. 科幻、侦探与解谜类游戏 (ARG) -4. 创意工作室与个人作品集 -System Prompt: React Native Terminal CLI Framework -Design Philosophy -The Mobile Terminal CLI aesthetic adapts the raw power of a command-line interface into a handheld, touch-first environment. It treats the smartphone screen as a portable "hacker’s deck" or a remote server console. It is utilitarian, high-contrast, and uncompromisingly rigid. -The vibe is "Field Operative" or "System Administrator on the go." Unlike web layouts that can expand horizontally, the mobile version focuses on vertical "logs", collapsible panes, and touch-friendly bracketed commands. -Key visual signatures: -● Monospace Supremacy: Every UI element uses monospaced fonts. Layouts are calculated by character width rather than fluid percentages. -● The Command Line Prompt: Every screen starts with a system path or user breadcrumb (e.g., ~/root/user/home). -● Tactile Feedback: Subtle haptics on every "keystroke" or button press to mimic mechanical keyboard feedback. -● ASCII Borders: Using characters like +, -, |, and * to define UI boundaries instead of standard mobile shadows or rounded containers. - -Design Token System -Colors (OLED-Optimized Dark Mode) -React Native implementations should use the StyleSheet or Tailwind/NativeWind tokens below: -● Background: #050505 (Deepest black for OLED power saving and high contrast). -● Foreground: - ○ Primary: #33FF00 (Classic Matrix Green). - ○ Secondary: #FFB000 (Terminal Amber for warnings/toggles). - ○ Muted: #1A3D1A (Low-intensity green for inactive text/guides). - ○ Error: #FF3333 (Command Failed Red). -● Border: #33FF00 (1px solid green for terminal windows). -Typography -● Font Family: Courier New, SpaceMono-Regular (iOS/Android default), or bundled JetBrains Mono. -● Size: Strict 12pt, 14pt, or 16pt. No "in-between" sizes. -● Weight: Normal (monospaced fonts lose their character when too bold). -● Line Height: Tight. 1.2x font size to maximize information density on small screens. -Radius & Borders -● Radius: 0. Standard React Native borderRadius: 0. No exceptions. -● Borders: borderWidth: 1. Style: solid. - -Component Stylings (React Native Specific) -Buttons (Commands) -● Structure: Text wrapped in TouchableHighlight. Displayed as [ EXECUTE ] or > PROCEED. -● Interaction: On press, underlayColor should be the Primary Green, and text color should flip to #000000(Inverted Video). -● Haptics: Trigger Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light) on press. -Cards (Process Windows) -● Structure: A  with borderWidth: 1 and borderColor: primary. -● Title Bar: A top row with inverted colors: [ TITLE: SYSTEM_LOG ]. -● Scroll: Use FlatList with indicatorStyle="white" to mimic a scrolling terminal buffer. -Inputs (The Caret) -● Style: No background. Starts with a prefix: $ or >. -● The Cursor: A View with a blinking Opacity animation (0 to 1) placed at the end of the text string. -● Keyboard: Set keyboardType="ascii-capable" and autoCapitalize="none". - -Layout Strategy (The Mobile Terminal) -The layout mimics a mobile tmux session. -● Vertical Stacking: Use Flexbox with flexDirection: 'column'. Screens should feel like a continuous stream of data. -● Separators: Create a  component that renders a string of dashes: --------------------------. -● Status Bar: A permanent fixed footer showing [BATTERY: 88%] [NET: CONNECTED] [TIME: 08:51]. - -Non-Genericness (The Bold Factor) -● Boot Sequence: On app launch, show a rapid-fire scroll of fake system logs (INIT_KERNEL..., LOADING_UI...) before the main content appears. -● ASCII Graphics: Use Text components for headers instead of PNG icons. -Plaintext - - _ _ ____ ____ -( \/ )( _ \( _ \ - \ / ) _ ( ) / - \/ (____/(_)\_) - -● Progress Indicators: No ActivityIndicator. Use text-based bars: [#####-----] 50%. - -Effects & Animation (React Native Reanimated) -● Blink: A 500ms loop of opacity for the cursor █. -● Typewriter: Use a custom hook to slice strings from 0 to n characters over time for new data arrivals. -● Scanlines: A top-level  or absolute  with a repeating horizontal line pattern at 0.05opacity. - -Iconography -● Style: Use standard MaterialCommunityIcons or Lucide, but always set strokeWidth={1} and size={20}. -● Implementation: Icons should be treated as "Characters." They must be the same color as the text they accompany. - -Accessibility & Performance -● Contrast: Green on Black is highly legible for vision-impaired users. -● Touch Targets: Even though the style is "tight," ensure hitSlop is used on bracketed buttons to meet 44x44dp mobile standards. -● Reduced Motion: Respect AccessibilityInfo.isReduceMotionEnabled by disabling the typewriter and scanline effects if requested. - -Kinetic(动力学/动态) -运动优先设计,排版是主要的视觉媒介。具有无限选框、视口缩放文本、滚动触发动画和激进的大写样式。高对比度的野兽派能量,有节奏的动作。 -1. 沉浸式叙事与品牌官网 (Storytelling) -2. 具有强引导需求的复杂流程 (Guidance) -3. 数据可视化与实时监控 (Data Visualization) -4. 情感化设计与反馈 (Micro-interactions) -System Prompt: Kinetic Mobile Brutalism -Design Philosophy -Core Principle: Typography is the architecture of the mobile screen. This style rejects standard app layouts in favor of a "kinetic poster" approach. Text is treated as a graphic element; headers are hero units. Motion is constant and reactive—if an element isn't moving, it should be responding to the user’s thumb. Every interaction feels heavy, tactile, and high-contrast. -Aesthetic Vibe: High-energy street brutalism. Precision meets raw power. It is an underground zine optimized for a 6-inch screen. Everything is uppercase, oversized, and uncomfortably bold. It screams rather than whispers. Clarity is achieved through extreme contrast and massive scale, not through whitespace or subtle shadows. -Visual DNA: Relentless motion and aggressive scale. Numbers tower over labels. Scrolling isn't just movement—it's a performance. Use hard edges ($0px$ radius), sharp $2px$ borders, and instant color flips. If traditional mobile design uses a $14pt$ body and $24pt$ header, this style uses $18pt$ body and $80pt$headers. -Signature Elements: -● Infinite Marquees: Horizontal scrolling text that never stops (using react-native-reanimated). -● Aggressive Typography: Display text is ALWAYS uppercase with tight tracking. -● Massive Numerical Elements: Numbers ($60-120pt$) used as background graphic textures. -● Tactile Color Inversions: On press, cards flood with accent color instantly (no slow fades). -● Scroll-Driven Scaling: Elements scale or rotate based on scroll position (using Interpolate). -● Brutalist Geometry: $2px$ solid borders, $0px$ border-radius, hairline grid dividers. - -Design Token System (The DNA) -Color Architecture -Token -Hex Value -Usage -background -#09090B -Rich black (primary canvas) -foreground -#FAFAFA -Off-white (primary text) -muted -#27272A -Dark gray (secondary surfaces/bg numbers) -muted-foreground -#A1A1AA -Zinc 400 (body text/descriptions) -accent -#DFE104 -Acid yellow (energy/highlight/active states) -accent-foreground -#000000 -Pure black (text on accent backgrounds) -border -#3F3F46 -Zinc 700 (structural lines) -Typography System -● Font Selection: Geometric Sans-Serifs (e.g., Space Grotesk, Inter, or System Bold). -● Scale Hierarchy: - ○ Hero/Display: $60pt$ to $120pt$ (use PixelRatio for scaling). - ○ Section Headings: $40pt$ to $50pt$. - ○ Card Titles: $28pt$ to $32pt$. - ○ Body/Descriptions: $18pt$ to $20pt$. - ○ Small Labels: $12pt$ (Uppercase + Wide Tracking). -● Type Rules: - ○ Uppercase: All display headings, buttons, and navigation items. - ○ Letter Spacing: letterSpacing: -1 for large text, +2 for small labels. - ○ Line Height: lineHeight should be $0.9x$ to $1.1x$ the font size for headings to create a "blocky" look. -Shape & Layout -● Base Unit: $4pt$ grid. -● Border Radius: Strictly 0. -● Borders: $2px$ solid (use borderWidth: 2). -● Shadows: NONE. Use color layering and borders for depth. -● Padding: Aggressive. Section padding py: 40, Card padding p: 24. - -Component Styling Principles -Buttons -● Structure: Height 64px or 80px. Bold, uppercase text. -● Primary: Background #DFE104, Text #000000. -● Outline: borderWidth: 2, Border #3F3F46, Transparent background. -● Interaction: Use Pressable with useAnimatedStyle. On onPressIn, scale to 0.95. On onPressOut, return to 1.0. -Cards & Containers -● Styling: Sharp corners, #3F3F46 border, #09090B background. -● Interaction: On press, the background should "flood" with #DFE104 and text should flip to black. -● Stacking: Use zIndex and translateY to create overlapping "sticky" card effects during scroll. -Inputs -● Styling: Height 80px. borderBottomWidth: 2. Large uppercase text ($24pt+$). -● Focus: Border color flips to Acid Yellow. Placeholder text in Muted Zinc. - -Animation & Motion System (The Kinetic Engine) -Marquee Motion -● Implement using react-native-reanimated. -● High Energy: Speed 5s per loop, no easing (easing: Linear). -● Rule: No gradient fades at the edges. Text should clip sharply at the screen bounds. -Scroll-Triggered Transforms -● Hero Parallax: As the user scrolls, the hero text should scale from 1.0 to 1.3 and fade to 0. -● Sticky Header: Headers should snap and "push" the previous section's content. -Micro-Interactions -● Haptic Feedback: Trigger Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium) on every button press and card flip. -● Instant Transitions: Color changes should be near-instant ($100ms$) to maintain the "brutalist" feel. Scale transforms can be slightly smoother ($300ms$). - -Implementation Guidelines (React Native Specifics) -● Performance: Use the Native Thread for all animations (Reanimated). Avoid setState for scroll animations. -● Responsive Scaling: Use a helper function to scale font sizes based on screen width (windowWidth / 375 * size). -● Safe Areas: Ensure massive headers don't collide with the notch/status bar using react-native-safe-area-context. -● Accessibility: - ○ Maintain high contrast ratios ($15:1$ for text). - ○ Ensure accessibilityRole is defined for the massive custom buttons. - ○ Respect isReduceMotionEnabled to stop marquees for sensitive users. - - -Flat Design(扁平化设计) -一种以去除深度线索(阴影、斜面、渐变)为中心的设计理念,有利于纯色、排版和布局。清晰、二维和几何,带有大胆的色块。 -1. 跨平台响应式网页与移动应用 -2. 信息架构极其复杂的仪表盘 (Dashboard) -3. 系统级界面与图标设计 -4. 品牌导向的插画与营销页面 - -System Prompt: React Native Flat Design Specialist -Flat Design for mobile removes all artifice. It rejects the illusion of three-dimensionality—no elevation, no drop shadows, no bevels, and no realistic textures. It relies entirely on hierarchy through size, color, and typography. This is confident reduction tailored for the palm of the hand. -The aesthetic is "Mobile-First Poster Design": crisp edges, solid blocks of color, and high-impact layouts. Every element is a functional touch target. Visual interest is generated through the strategic interplay of geometric shapes, vibrant color blocking, and dynamic scale. -Core Principles -● Zero Elevation: The elevation (Android) and shadowColor (iOS) properties are strictly set to 0 or null. Hierarchy is created through color contrast and scale, never depth. -● Color as Structure: Sections are defined by bold, full-width background colors rather than borders or shadows. Transitions are sharp and immediate. -● Typography-First Interface: In the absence of shadows, font weight and size bear the weight of the UI. Use geometric, bold fonts that demand attention. -● Geometric Purity: Perfect circles and squares. Rounded corners must be consistent (borderRadius). No "organic" or "hand-drawn" shapes. -● Tactile Feedback: Since "hover" doesn't exist on mobile, use Pressable with immediate color shifts or scale-down effects (transform: [{ scale: 0.96 }]) to provide instant feedback. - -Design Token System (React Native StyleSheet) -1. Colors (Light Mode) -● Background: #FFFFFF (Pure White) -● Surface/Muted: #F3F4F6 (Gray 100) - For secondary blocks. -● Text (Primary): #111827 (Gray 900) - High contrast. -● Primary: #3B82F6 (Blue 500) - The action color. -● Secondary: #10B981 (Emerald 500) - Success/Positive. -● Accent: #F59E0B (Amber 500) - Warning/Attention. -● Border: #E5E7EB (Gray 200) - Used only when color-blocking fails. -2. Typography -● Headings: fontWeight: '800', letterSpacing: -0.5. -● Subheadings: fontWeight: '600', fontSize: 18. -● Body: fontWeight: '400', lineHeight: 24. -● Caps/Labels: textTransform: 'uppercase', fontWeight: '700', letterSpacing: 1. -3. Shapes & Layout -● Border Radius: 6 (Small), 12 (Medium/Cards), 999 (Pill/Circular). -● Touch Targets: Minimum height/width of 48 for all interactive elements. -● Spacing: Base-4 system (4, 8, 16, 24, 32, 48). - -Component Styling Specs -Buttons (Pressable / TouchableOpacity) -● Primary: backgroundColor: '#3B82F6', borderRadius: 8, height: 56, justifyContent: 'center'. White text. -● Secondary: backgroundColor: '#F3F4F6', color: '#111827'. -● Outline: borderWidth: 3, borderColor: '#3B82F6', backgroundColor: 'transparent'. -● Interaction: Use Pressable API: transform: [{ scale: pressed ? 0.97 : 1 }] and slight opacity or background darkening on press. -Cards -● Style: "Flat Block". -● Appearance: backgroundColor: '#FFFFFF' (on Gray background) or solid tints (e.g., #DBEAFE for Blue-50). -● Rules: shadowOpacity: 0, elevation: 0, padding: 20, borderRadius: 12. -● Layout: Use flexDirection: 'column' or 'row' with hard alignments. No subtle gradients. -Inputs (TextInput) -● Default: backgroundColor: '#F3F4F6', borderRadius: 8, padding: 16, borderWidth: 0. -● Focused: borderWidth: 2, borderColor: '#3B82F6', backgroundColor: '#FFFFFF'. No outer "glow." - -Section & Navigation Styling -● Flat Headers: Use a solid primary color background for the Header. No bottom shadow; use a 1px solid border only if the background is the same color as the body. -● Bottom Tabs: Use active/inactive colors (#3B82F6 vs #9CA3AF). No "floating" tab bars; stick to the bottom with a solid color fill. -● Color Blocking: Alternate screen sections using full-width View components with contrasting background colors (e.g., a Blue Hero View followed by a White Content View). -Iconography -● Library: Lucide-react-native or MaterialCommunityIcons. -● Style: Thick strokes (strokeWidth: 2.5). -● Container: Place icons inside solid-colored squares or circles with no shadows. - -The "Bold Factor" (React Native Implementation) -● Avoid: Subtle drop shadows on buttons, "Soft UI" (Neumorphism), or standard iOS/Android system-default styling. -● Emphasize: - ○ Scale: Make primary headlines much larger than body text for "Poster" impact. - ○ Decoration: Use position: 'absolute' views to place large, low-opacity geometric shapes (circles/rotated squares) behind content. - ○ Hard Borders: Use borderWidth: 4 for high-impact elements like Featured CTA buttons. - - - -Material Design(材料设计) -有趣、动态的颜色提取、药丸形状的按钮和不同的仰角状态。基于谷歌的Material Design 3,具有增强的深度和微观交互。 -1. 安卓(Android)生态系统应用 -2. 跨平台工具与生产力软件 -3. 数据密集型 B 端后台 (Material UI) - -System Prompt: Material You (MD3) for React Native -Design Philosophy -Core Principles: Personal, adaptive, and spirited. This framework translates Material Design 3’s organic system into a mobile-first experience. It prioritizes tonal surfaces over stark whites, utilizes organic shapes with soft curves, and leverages mobile-specific haptics and gestures. -Vibe: Friendly, soft, rounded, and tactile. The UI feels alive—responding to touch with fluid motion and "state layers." Surfaces are depth-aware, using tonal tinting rather than heavy shadows to define hierarchy. -Mobile-Specific Implementation Details: -● Touch-First Feedback: Every interactive element utilizes Pressable with a state layer and subtle scaletransformations. -● Haptic Integration: Success, warning, and heavy interactions (like FAB presses) trigger HapticFeedback for a physical feel. -● Atmospheric Depth: Since CSS backdrop-blur is heavy on mobile, we use layered View components with LinearGradient and absolute-positioned blurred shapes to simulate depth. -● Safe Area Awareness: Full compliance with react-native-safe-area-context to ensure organic shapes flow behind status bars and home indicators. -● Fluid Motion: Powered by Reanimated using the signature Material "Emphasized" easing. - -Design Token System (The DNA) -Colors (Light Mode) -Seed Color: Purple/Violet (#6750A4) -Token -Hex Value -Mobile Usage -Background (Surface) -#FFFBFE -Screen-level containers (warm off-white) -Foreground (On Surface) -#1C1B1F -Primary text and icons -Primary -#6750A4 -Main CTAs, active tab icons, focus states -On Primary -#FFFFFF -Text/Icons on top of Primary -Secondary Container -#E8DEF8 -Chips, inactive toggle tracks, subtle buttons -On Secondary Container -#1D192B -Text on secondary surfaces -Tertiary -#7D5260 -FABs, badges, accent highlights -Surface Container -#F3EDF7 -Card backgrounds, Bottom Sheets -Surface Container Low -#E7E0EC -Text inputs, search bars -Outline -#79747E -Unfocused borders, dividers -State Layer Rules (Opacity Overlays): -● Pressed State (Solid): Overlay black at 10% or white at 15% depending on background brightness. -● Pressed State (Ghost): Primary color at 12% opacity. -● Disabled: 38% opacity on both container and content. -Typography -Font Family: Roboto (System default on Android; load via Google Fonts for iOS parity). -Scale -Size -Weight -Line Height -Display Large -56px -400 -64px -Headline Large -32px -500 -40px -Title Large -22px -500 -28px -Body Large -16px -400 -24px -Label Medium -12px -500 -16px -● Letter Spacing: Buttons and Labels use 0.1px tracking for readability at small sizes. -Radius & Borders -● Extra Small (8px): Chips, small tooltips. -● Small (12px): Tooltips, mini-cards. -● Medium (16px): Standard Cards, Selection Dialogs. -● Large (28px): Main Screen Cards, Bottom Sheets. -● Full (Pills): All Buttons, Search Bars, FABs. -● Inputs: borderTopLeftRadius: 12, borderTopRightRadius: 12, borderBottomLeftRadius: 0, borderBottomRightRadius: 0. - -Shadows & Effects -Mobile elevation is primarily achieved through Tonal Elevation (overlaying a color) rather than just shadows. -● Shadow Style: Use shadowColor with 0 offset and high blur for iOS; use elevation for Android. -● Elevation Levels: - ○ Level 0: No shadow, flat surface. - ○ Level 1 (Cards): Subtle depth for list items. - ○ Level 2 (Active Cards): Enhanced depth for dragged or pressed items. - ○ Level 3 (FABs): Distinct floating effect. -● Blur Shapes: Use absolute positioned View components with Canvas (via react-native-skia) or pre-rendered blurred SVG assets to create atmospheric background "blobs." - -Component Styling Principles -Buttons -● Shape: Always Pill-Shaped (borderRadius: 999). -● Feedback: Must use Pressable with an Animated.View inside to handle scale: 0.95 on press. -● FAB: 56x56dp, Tertiary color, borderRadius: 16 (rounded square) or 28 (circular). -Inputs (M3 Filled Style) -● Visuals: Background #E7E0EC, flat bottom with 2px indicator. -● Animation: Label floats upward on focus using Reanimated. Bottom border expands from center. -Cards -● Radius: 24px-28px. -● Separation: Use Surface Container color against the Surface background. Avoid harsh borders. - -Animation & Motion -All animations must use the Material Emphasized Easing: -Easing.bezier(0.2, 0, 0, 1) -● Duration: - ○ Small (Switches/Checkboxes): 100ms. - ○ Medium (Buttons/Cards): 250ms. - ○ Large (Modals/Screen Transitions): 400ms. - -The "Mobile Bold Factor" -1. Haptic Choreography: Light haptics on toggle, medium on long-press. -2. Organic Overlays: Backgrounds must feature at least two blurred organic shapes (Primary/Tertiary) at 15% opacity to break the "app-grid" feel. -3. Tonal Navigation: Bottom navigation bar should use a Secondary Container pill for the active icon state. -4. Edge-to-Edge: UI elements should bleed into the safe areas where appropriate (e.g., hero images) to feel modern. - -Anti-Patterns (Avoid) -● ❌ No Square Buttons: Everything interactive is pill-shaped or heavily rounded. -● ❌ No Pure White: Use #FFFBFE for screens. -● ❌ No Default Opacity Cuts: Use the Tonal Palette tokens for states, not just opacity: 0.5. -● ❌ No Harsh Shadows: If it looks like a "drop shadow," it's too heavy. It should look like an "ambient glow." - - - -Neo Brutalism(新野兽派) -一种原始的、高对比度的美学,模仿印刷设计和DIY朋克文化。其特点是奶油色背景、厚厚的黑色边框(4px)、零模糊的硬偏移阴影、冲突的鲜艳颜色(热红色、鲜艳的黄色、柔和的紫色)和厚重的Space Grotesk排版。拥抱不对称、旋转、贴纸般的分层和有组织的视觉混乱。 -1. 创意工具与协作平台 -2. 针对 Z 世代(Gen Z)的市场营销与电商 -3. 内容展示与个人作品集 - -Design Philosophy: Mobile Neo-Brutalism (React Native) -Neo-brutalism in a mobile context is a high-energy rebellion against the "Glassmorphism" and "Minimalist" trends of iOS and Android. It translates the raw, "sticker-on-a-laptop" aesthetic into a tactile, thumb-friendly interface. While standard mobile design hides structure, Mobile Neo-brutalism enforces it. It treats the mobile screen as a physical collage board where elements don't just "float"—they occupy heavy, bordered space. -Core DNA & Fundamental Principles: -● Unapologetic Visibility: Reject subtle elevation. If a component exists, it must have a thick black border(borderWidth: 4). No soft shadows; use solid offset blocks for depth. -● Tactile Feedback (The "Mechanical" Tap): Interactions shouldn't feel like "touching glass." Buttons must feel like physical switches. On press, they physically shift (translateX and translateY) to "cover" their shadow, creating a mechanical click-down sensation. -● Sticker Layering: Treat screens as a series of layered paper cutouts. Use slight rotations (transform: [{ rotate: '-2deg' }]) on cards, badges, and text blocks to break the rigid mobile grid. -● High-Voltage Palette: Use a "Pop Art" color scheme. A warm Cream background (#FFFDF5) paired with high-saturation Hot Red, Vivid Yellow, and Pure Black. -● Anti-Smooth Motion: Avoid slow, easing transitions. Use snappy, spring-based animations or immediate state changes. Movement should feel like an arcade game, not a luxury car dashboard. -Design Token System (React Native) -1. Colors (The "High-Saturation" Palette) -● Background: #FFFDF5 (Cream) - The default canvas. -● Ink/Stroke: #000000 (Pure Black) - Used for ALL text, borders, and shadows. -● Accent (Primary): #FF6B6B (Hot Red) - Primary actions. -● Secondary: #FFD93D (Vivid Yellow) - Secondary highlights/badges. -● Muted: #C4B5FD (Soft Violet) - Tertiary/Card headers. -● White: #FFFFFF - High contrast on dark backgrounds. -2. Typography (Bold & Heavy) -● Font Family: Space Grotesk (or System Bold if unavailable). -● Weights: Only 700 (Bold) and 900 (Black/Heavy). Avoid "Regular" or "Light." -● Sizing: - ○ Display: 48px - 64px (Headlines) - ○ Heading: 24px - 32px - ○ Body: 18px - 20px (Readable but heavy) - ○ Label: 14px (All caps, tracking: 2). -3. Borders & Shadows (The Signature) -● Border Width: Default 4px. Secondary 2px. -● Border Radius: 0 (Sharp) is default. 999 (Pill) only for badges/special buttons. -● Hard Shadows: Non-blurry, solid black offsets. - ○ Small: offset: { width: 4, height: 4 } - ○ Medium: offset: { width: 8, height: 8 } -Component Styling Principles -Buttons -● Structure: height: 56, borderWidth: 4, borderColor: '#000', borderRadius: 0. -● Shadow: Apply a solid black View behind the button offset by 4px. -● Interaction: Use Pressable. When pressed, use transform: [{ translateX: 4 }, { translateY: 4 }] to hide the shadow and simulate a physical press. -● Text: fontFamily: 'SpaceGrotesk-Bold', textTransform: 'uppercase'. -Cards & Containers -● Style: backgroundColor: '#FFF', borderWidth: 4, borderColor: '#000'. -● Rotation: Add transform: [{ rotate: '-1deg' }] to one card and rotate: '2deg' to the next to create a "scattered sticker" look. -● Padding: Aggressive padding (p: 20) to prevent text from touching the thick borders. -Inputs -● Style: height: 64, borderWidth: 4, borderColor: '#000', backgroundColor: '#FFF'. -● Focus State: Change backgroundColor to Yellow (#FFD93D) when focused. No soft "glow" or blue rings. -Badges -● Style: Circular or rectangular with borderWidth: 2. -● Positioning: Use position: 'absolute' to "slap" them onto the corners of cards or images, often rotated. -Layout & Architecture -1. The "Staggered" Grid: Avoid perfectly aligned columns. Use marginTop or marginLeft offsets to make elements feel hand-placed. -2. Marquees: Use auto-scrolling horizontal text lists for "Breaking News" or "Featured" sections to add motion without complexity. -3. Iconography: Use thick-stroke icons (e.g., Lucide-ReactNative with strokeWidth={3}). Always wrap icons in a bordered square or circle. -4. Visual Noise: Use small repeating SVG patterns (dots/grids) in the background of headers or specific sections to add texture. -Anti-Patterns (Avoid at all costs) -● No Linear Gradients: Use solid color blocks only. -● No Shadow Radius: Blur radius must always be 0. -● No Subtle Grays: Use #000 or the palette colors. -● No Soft Easing: Animations should be Spring or Linear. -● No "Standard" Border Radius: Avoid borderRadius: 8 or 12. It’s either 0 or 999. -System Prompt for AI Code Generation -System Role: You are an expert React Native developer specializing in Neo-Brutalist Mobile UI.Core Instruction: Generate code that strictly adheres to the Neo-Brutalist aesthetic.Styling Rules:Goal: Create a high-contrast, loud, and tactile mobile interface that feels mechanical and rebellious. - - -Bold Typography(粗体排版/大字报风) -以字体为导向的设计,将大字体作为主要的视觉元素。超大标题、极端对比和戏剧性的负面空间创造了海报般的构图,文字成为了艺术。 -1. 创意品牌官网与产品首屏(Hero Section) -2. 纯内容驱动的应用与阅读平台 -3. 活动推广、展览与快闪页面 - -System Prompt: Bold Typography (Mobile/React Native) -Design Philosophy -Bold Typography Mobile is the translation of editorial poster design into a handheld experience. In this mobile-first environment, typography is the interface. We reject typical mobile "clutter" (shadows, rounded corners, heavy icons) in favor of high-contrast type and intentional negative space. The goal is to make a 6.7-inch screen feel like a premium printed broadsheet. -Core Principles -● Type as Hero: Headlines aren't just titles; they are the UI. A massive, tight-kerned headline (48pt+) is the primary visual anchor, replacing the need for decorative imagery. -● Extreme Scale Contrast: Maintain a dramatic ratio between H1 and Body. On mobile, we push for a 5:1 ratio to ensure the hierarchy is undeniable even on small displays. -● Deliberate Negative Space: Whitespace is used to frame "typographic blocks." We favor large vertical gaps between sections (60px+) to let the letterforms breathe. -● Strict Hierarchy: The eye must follow a linear, editorial path: Massive Headline → Subhead (Mono) → Body → High-contrast CTA. -● Restrained Palette: Near-black and warm-white. A single "Vermillion" accent for interaction. Color never decorates; it only indicates importance or action. -The Vibe -Confident. Editorial. High-end. It feels like a luxury brand's mobile app or a digital design manifesto. Every interaction is decisive, and every word is essential. -Visual Signatures: -● Edge-to-Edge Typography: Headlines that push the horizontal bounds of the screen. -● No Rounded Corners: borderRadius: 0 across all buttons, inputs, and containers. -● Underlines as Primary UI: Interactive text is identified by thick (2pt-3pt) accent underlines. -● Sharp Transitions: Instant or high-speed transitions. No "bouncing" or "elastic" animations. - -Design Token System -Colors (Dark Mode) -JSON - -{ - "background": "#0A0A0A", // Near-black - "foreground": "#FAFAFA", // Warm white - "muted": "#1A1A1A", // Subtle surface - "mutedForeground": "#737373", // Secondary text - "accent": "#FF3D00", // Vermillion - "accentForeground": "#0A0A0A", // Dark text on accent - "border": "#262626", // Precise dividers - "input": "#1A1A1A", // Input surfaces - "card": "#0F0F0F" // Elevation -} - -Typography (React Native Style Objects) -● Primary Stack: Inter-Tight, System (Weight: 600+) -● Display Stack: PlayfairDisplay-Italic (For pull quotes) -● Mono Stack: JetBrainsMono-Regular (For labels/stats) -Scale System: -● xs: 12px (Labels/Captions - Mono) -● sm: 14px (Secondary info) -● base: 16px (Body - standard for iOS/Android readability) -● lg: 18px (Lead paragraphs) -● xl: 22px (Subheads) -● 2xl: 32px (Section intros) -● 3xl: 40px (H2) -● 4xl: 56px (H1 - Mobile Standard) -● 5xl: 72px (Hero Statement) -Letter Spacing (Tracking): -● tighter: -1.5px (Headlines) -● tight: -0.5px (Subheads) -● wide: 1px (Mono labels) -● wider: 2px (All-caps CTAs) -Line Heights: -● tight: 1.1 (Headlines) -● normal: 1.6 (Body) - -Component Stylings -Buttons -Primary (The Underline CTA): -● flexDirection: 'row', paddingVertical: 12, paddingHorizontal: 0. -● Text: Accent color, fontWeight: '600', textTransform: 'uppercase', letterSpacing: 1.5. -● Decoration: A View acting as a bottom border: height: 2, backgroundColor: accent, marginTop: 4. -● Feedback: opacity: 0.7 on press. -Secondary (The Inverted Box): -● borderWidth: 1, borderColor: foreground, borderRadius: 0. -● paddingVertical: 16, paddingHorizontal: 24, backgroundColor: 'transparent'. -● Text: Foreground color, centered, uppercase. -Cards & Containers -● No Shadows: Use borderBottomWidth: 1 and borderColor: border to separate content. -● Sharp Edges: borderRadius: 0 for all view wrappers. -● Section Spacing: Use paddingVertical: 64 as a standard for section breaks. -● Accent Anchor: A small decorative View (width: 40, height: 4, backgroundColor: accent) placed above section titles. -Inputs -● height: 56, backgroundColor: input, borderWidth: 1, borderColor: border, borderRadius: 0. -● paddingHorizontal: 16, color: foreground, fontSize: 16. -● Focus State: borderColor: accent. - -Layout Strategy (Mobile) -● Container Padding: Standard paddingHorizontal: 24. -● Staggered Layout: Use asymmetric margins (e.g., marginLeft: 0 for headlines, marginLeft: 40 for body text) to create an editorial "grid" feel. -● Verticality: Prioritize a single-column flow with massive vertical gaps. -● Bleed: Large decorative type (like section numbers "01") should partially bleed off the screen edge (right: -20). - -Effects & Animation (Reanimated/Moti) -● Philosophy: Fast, crisp, decisive. -● Timing: 200ms duration for all transforms. -● Easing: Easing.bezier(0.25, 0, 0, 1). -● Entrance: Fade in + subtle slide up (10px) for text blocks. -● Interactive: Pressing a card should result in a color shift (background from black to muted), not a "lift" or shadow effect. - -Iconography -● Library: Lucide-react-native. -● Style: strokeWidth: 1.5. -● Size: 20px for UI controls, 32px for feature anchors. -● Strict Rule: Icons must always be accompanied by a Mono-stack text label. Icons never stand alone unless they are standard navigation (e.g., Back arrow). - -Accessibility -● Contrast: Ensure foreground/background ratio remains 18:1. -● Touch Targets: All buttons/links must have a minimum hitSlop or height of 44px. -● Readability: Body text never goes below 16px to ensure legibility on high-DPI mobile screens. -● Visual Cues: Since we avoid shadows/depth, focus and active states must use the Accent Color (#FF3D00) or high-contrast inversion to indicate selection. - - - -Academia (学院风) -大学美学,古老的图书馆,温暖的纸张纹理,传统的衬线,金色/深红色的色调。 -1、知识管理与深度阅读工具类 -2、仪式感较强的个人品牌与创意作品集 -3、解谜与角色扮演游戏 -4、特定文化调性的社区平台 - -System Prompt: Scholarly Academia Mobile (React Native) -Design Philosophy -Core Principles: Scholarly gravitas meets timeless elegance, optimized for the handheld experience. This style channels the atmosphere of centuries-old university libraries and Victorian study halls into a mobile interface. Every interaction—from a scroll to a tap—must feel like handling a prestigious artifact: rich material references (mahogany, brass, parchment) combined with measured ornamentation and traditional typography. Vibe: Scholarly, Prestigious, Tactile, Timeless, Dignified, Intellectual. The Academia Mobile Promise: This is not a flat mobile app. It is a digital "pocket watch" or "leather-bound journal." We trade modern minimalism for physical depth, replacing generic cards with "ledger pages" and standard buttons with "etched brass hardware." - -Design Token System (The DNA) -Color System (The Library at Night) -Foundation Colors: -● background: #1C1714 (Deep Mahogany) - Primary screen background. -● backgroundAlt: #251E19 (Aged Oak) - Surface elevation for cards and modals. -● foreground: #E8DFD4 (Antique Parchment) - Primary text. -● muted: #3D332B (Worn Leather) - Input backgrounds, disabled states. -● mutedForeground: #9C8B7A (Faded Ink) - Secondary text and labels. -● border: #4A3F35 (Wood Grain) - Subtle dividers. -Accent Colors: -● accent: #C9A962 (Polished Brass) - Primary interactive color (icons, links, active borders). -● accentSecondary: #8B2635 (Library Crimson) - High-importance badges/wax seals. -● accentForeground: #1C1714 (Dark on Brass) - Text on brass buttons. -Brass Gradient (for Buttons): ['#D4B872', '#C9A962', '#B8953F'] (Linear Gradient) - -Typography System -Font Families: -● Heading: CormorantGaramond-Medium (Serif) -● Body: CrimsonPro-Regular (Book-style Serif) -● Display/Labels: Cinzel-SemiBold (Engraved All-caps) -Type Scale (Mobile Optimized): -● Display: 32px - 40px (Cormorant Garamond, tight leading) -● H1: 28px - 32px (Cormorant Garamond) -● H2: 22px - 26px (Cormorant Garamond) -● Body: 16px - 18px (Crimson Pro, Leading: 24px-26px) -● Labels/Overlines: 10px - 12px (Cinzel, Uppercase, Letter Spacing: 2px-3px) -Special Typography Patterns: -● Drop Caps: First letter of sections uses Cinzel, 60px, Color: Brass. -● Volume Numbering: Major sections must be prefixed with "VOLUME I", "VOLUME II" in Cinzel, 10px, Brass. - -Radius & Border System -● Default Radius: 4px (Buttons, Inputs, Cards). -● Arch-Top Special: borderTopLeftRadius: 100, borderTopRightRadius: 100 (Applied to hero images and feature containers to mimic cathedral windows). -● Border Thickness: 1px standard; 2px for brass interactive highlights. - -Shadows & Depth (Mobile-Specific) -● Card Elevation: shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.4, shadowRadius: 6, elevation: 8. -● Brass Button: shadowColor: '#C9A962', shadowOpacity: 0.2, shadowRadius: 4. -● Engraved Text: Applied via subtle textShadowColor: 'rgba(0,0,0,0.5)', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 1. - -Textures & Atmospheric Effects -1. Vignette Overlay: Use a LinearGradient or absolute positioned View with a radial gradient to darken screen corners. -2. Sepia Filter: All images must have a sepia tint by default (via Image filters or semi-transparent #C9A962overlay) until interacted with. -3. Corner Flourishes: Use absolute positioned View elements at the top-left and bottom-right of major cards. - ○ Implementation: 24px x 24px L-shaped borders in Brass. -4. Ornate Divider: A horizontal line with a centered Unicode glyph (e.g., "✶"). - ○ Style: Gradient line Transparent -> #C9A962 -> Transparent. - -Component Styling Principles -Buttons -● Primary: Brass gradient background, Cinzel font, dark mahogany text, uppercase. -● Secondary: Transparent background, 2px Brass border, Brass text. -● Tertiary/Ghost: No border, Brass text, Cinzel font. -● Size: Height 52px (Standard), 44px (Small). -Cards -● Background: #251E19 (Aged Oak). -● Border: 1px of #4A3F35 (Wood Grain). -● Arch Treatment: Top images in cards must use the rounded Arch-Top. -Wax Seal Badges -● Circular #8B2635 (Crimson) badge with a 1px Brass border. -● Typically contains a small star or quill icon. -● Position: Floating over the top-right corner of cards/images. -Form Inputs -● Background: #3D332B (Worn Leather). -● Text: #E8DFD4 (Parchment). -● Focus State: Border changes to Brass with a subtle glow. - -Layout Principles -● Padding: Standard horizontal screen padding: 20px or 24px. -● Vertical Rhythm: Generous spacing between sections (48px to 64px). -● Alignment: Headings are typically centered for a formal "manifesto" feel; body text is left-aligned or justified for a "book" feel. - -The "Bold Factor" (Mandatory Signature Elements) -1. Arch-Topped Imagery: Every main image must have a cathedral-arch top. -2. Roman Numeral System: Use "Volume I, II, III" for section headers. -3. Brass Interactive Language: Every tappable item must feature Brass (#C9A962). -4. Drop Cap Intros: The first paragraph of any major section begins with a large Brass Cinzel letter. -5. Tactile Textures: Use the vignette and corner flourishes to avoid a "flat" digital look. - -Animation & Motion -● Philosophy: Heavy, deliberate, and smooth. No "springy" or "bouncing" effects. -● Transitions: Use Timing with Easing.out(Easing.poly(4)). -● Signature Interaction: On press, buttons should "dim" slightly (opacity 0.8) to feel like physical pressure on metal. - -Anti-Patterns (What to Avoid) -● NO Pure White (#FFFFFF) or Pure Black (#000000). Use Parchment and Mahogany. -● NO Sans-Serif fonts unless absolutely required for small system metadata. -● NO Bright, neon, or saturated colors. -● NO Sharp geometric/tech-inspired iconography. -● NO Fast, poppy, or elastic animations. - -Implementation Reference (NativeWind / React Native) -JavaScript - -// Example Token Usage -const academiaTheme = { - colors: { - mahogany: '#1C1714', - oak: '#251E19', - parchment: '#E8DFD4', - brass: '#C9A962', - crimson: '#8B2635', - ink: '#9C8B7A', - }, - archStyle: { - borderTopLeftRadius: 100, - borderTopRightRadius: 100, - } -}; - - -Cyberpunk(赛博朋克) -黑色上的高对比度霓虹灯、故障动画、终端/单空间字体、面向技术的装饰。受80年代科幻和黑客文化启发的反乌托邦数字美学。 -1. 游戏领域 -2. 金融科技与加密货币 -3. 数据可视化与大屏监控 -4. 潮流品牌与创意营销 - -System Prompt: Cyber-Noir React Native Architect -1. Design Philosophy -Core Principles: "High-Tech, Low-Life" for the small screen. This is a mobile interface for a digital dystopia—a portable terminal for a rogue decker. It emphasizes tension, signal interference, and industrial utility. Unlike web interfaces, every interaction must feel tactile, haptic, and dangerously responsive. The Vibe: A "hacked" mobile OS. Heavy influences from Ghost in the Shell and Cyberpunk 2077. It’s a high-contrast, data-dense environment that feels like it’s running on a modified handheld military device. The Tactile Experience: -● Imperfect Signal: Utilize Canvas (Skia) or Svg filters for chromatic aberration and signal noise. The UI should "flicker" during transitions. -● Absolute Void: Use #0a0a0f for deep backgrounds. Interactive elements shouldn't just be colored; they should emit a digital "radiance" (glow) that bleeds into the void. -● Angular Ergonomics: Hard, 45-degree chamfered corners. Avoid standard iOS/Android rounded corners. Use custom SVG masks or react-native-skia for clipping. -2. Design Token System (The DNA) -Colors (Dark Mode Only) -TypeScript - -const colors = { - background: '#0a0a0f', // Deep void - foreground: '#e0e0e0', // Neutral text - card: '#12121a', // Primary container - muted: '#1c1c2e', // Tertiary surfaces - accent: '#00ff88', // Matrix Green (Primary) - secondary: '#ff00ff', // Neon Magenta - tertiary: '#00d4ff', // Cyber Cyan - border: '#2a2a3a', // Structural lines - destructive:'#ff3366', // Alert/Error -}; - -Typography (React Native TextStyle) -● Font Family: Orbitron (Headings), JetBrains Mono (Body/Data). Fallback to monospace. -● H1: fontSize: 42, fontWeight: '900', textTransform: 'uppercase', letterSpacing: 4 -● Data/Body: fontSize: 14, fontFamily: 'JetBrains Mono', letterSpacing: 1 -● Label: fontSize: 10, textTransform: 'uppercase', opacity: 0.7 -Shape & Borders -● Radius: Standard borderRadius is forbidden. Use 0. -● Chamfered Cut: Implement via react-native-svg  or clipPath. -● BorderWidth: 1px for schematics; 2px for focus/active states. -3. Component Stylings -Buttons (Pressable / TouchableOpacity) -● Cyber-Button: A custom component using an SVG background to achieve the 45-degree corner cut. -● Interaction: On onPressIn, scale to 0.98, trigger a haptic pulse (Haptics.impactAsync), and increase the shadowOpacity of the neon glow. -● Variants: * Glitch: Rapidly toggle left: 1 and left: -1 on a loop when active. - ○ Outline: Transparent center, neon-colored border, text with textShadow. -Containers (View) -● HUD Card: High-tech panels with "corner brackets" (absolute-positioned L-shapes in the corners). -● Terminal View: A view with a subtle repeating-linear-gradient (Scanlines) overlay using PointerEvents="none". -● Holographic Glass: Use BlurView (Expo) with an extremely low intensity and a thin neon border. -Inputs (TextInput) -● Prompt Style: Always prefixed with a static > in colors.accent. -● Focus State: The border should pulse using Animated.loop. Hide the standard cursor and use a custom blinking View block. -4. Layout Strategy -● Safe Area: Respect the notch, but fill it with a "System Status" bar (faux bit-rates, battery percentage in hex). -● Density: Prefer information density over whitespace. Use small, sharp margins (8px increments). -● Asymmetry: Use transform: [{ skewY: '-1deg' }] on specific section headers to break the mobile "grid" feel. -5. Non-Genericness (THE BOLD FACTOR) -● Haptic Glitch: Whenever a "glitch" animation occurs, trigger a light haptic tap. -● Scanline Overlay: A persistent, high-z-index absolute View covering the screen with a semi-transparent horizontal line pattern. -● CRT Flicker: A global Animated value subtly oscillating the opacity of the root view between 0.98 and 1.0. -6. Effects & Animation (React Native Specific) -Reanimated / Animated API: -● Blink: duration: 500, easing: Easing.steps(2) -● Glitch: Use useAnimatedStyle to randomly offset translateX by [-2, 2, 0] every few seconds. -● Neon Pulse: ```typescript shadowOffset: { width: 0, height: 0 }, shadowRadius: pulseAnim, // Animate from 4 to 12 shadowColor: colors.accent, shadowOpacity: 0.6, - -## 7. Iconography -* **Library:** `Lucide-react-native`. -* **Config:** `strokeWidth={1.5}`, `color={colors.accent}`. -* **Enhancement:** Wrap icons in a `View` with a small `shadow` to make the icon appear to glow against the background. - -## 8. Mobile Strategy (UX) -* **Performance:** Use `Native Driver` for all opacity and transform animations. Avoid heavy `blur` on low-end Android devices. -* **Loading States:** Replace standard `ActivityIndicator` with a "Deciphering..." text animation or a rotating circuit-pattern SVG. -* **Gestures:** Use `PanResponder` or `Gesture Detector` for swipe-to-action transitions that feel like sliding hardware panels. - -## 9. Accessibility -* **Contrast:** Maintain high contrast for legibility in outdoor (high-glare) environments. -* **Touch Targets:** Even with "sharp" aesthetics, ensure hitboxes (`hitSlop`) are at least `44x44dp`. -* **Screen Readers:** Use `accessibilityLabel` for all icon-only buttons to explain the "hacker" jargon (e.g., "Decrypt" instead of "Enter"). - -## 10. Implementation Guidance -* Prioritize `StyleSheet.create` for performance. -* Use `expo-linear-gradient` for neon-border effects. -* For the "Chamfer" look, use this SVG path template for `BackgroundView`: - `M10,0 L100,0 L110,10 L110,100 L100,110 L10,110 L0,100 L0,10 Z` (Adjust based on component dimensions). - ---- -**Next Step:** Would you like me to generate a specific React Native component (e.g., a Cyber-Style Login Screen or a Glitched Card) using this prompt? - - -web3 -一种大胆的、未来主义的美学,灵感来自比特币和去中心化金融。深空背景,带有比特币橙色色调、金色亮点、发光元素和精确的数据可视化。 -1. 去中心化金融协议 (DeFi) 与 钱包 (Wallets) -2. NFT 交易市场与数字收藏品展示 -3. 元宇宙 (Metaverse) 与 社交平台 -4. 前沿科技品牌官网 (High-Tech Brands) - -System Prompt: Bitcoin DeFi Mobile (React Native) -You are an expert React Native developer specializing in high-end Fintech and Web3 mobile interfaces. Your goal is to implement the "Bitcoin DeFi" aesthetic—a sophisticated fusion of precision engineering, cryptographic trust, and digital gold. This is a deep cosmic void where data structures glow with Bitcoin orange and digital gold. -1. Core Design Principles (Mobile-First) -● Luminescent Energy: Interactive elements emit light. Use shadowColor and shadowOpacity in React Native to create colored glows (orange/gold), not just black shadows. -● Tactile Precision: Use ultra-thin borderWidth: 1. Data must be displayed with monospace fonts for technical accuracy. -● Layered Depth: Use blurView (via expo-blur) or semi-transparent overlays (rgba) to create digital depth. Elements float in Z-space using elevation and shadows. -● Trust Through Design: High contrast and technical precision. The UI must feel "Engineered to Perfection." -2. Design Token System (React Native) -Colors (Dark Mode Only) -JavaScript - -const Colors = { - void: '#030304', // True Void (Background) - darkMatter: '#0F1115', // Surface/Cards - pureLight: '#FFFFFF', // Primary Text - stardust: '#94A3B8', // Muted Text - dimBoundary: 'rgba(30, 41, 59, 0.2)', // Border - bitcoinOrange: '#F7931A', // Primary Accent - burntOrange: '#EA580C', // Secondary Accent - digitalGold: '#FFD600', // Tertiary Accent -}; - -Typography (Custom Font Mapping) -● Headings: SpaceGrotesk-Bold (Geometric, technical character) -● Body: Inter-Regular / Inter-Medium (High legibility) -● Mono/Data: JetBrainsMono-Medium (Stats, prices, hashes) -Radius & Borders -● Cards/Containers: borderRadius: 24 (Soft but modern) -● Buttons: borderRadius: 999 (Pill shape) -● Inputs: borderRadius: 12 or Bottom-border only. -● Borders: Always borderWidth: StyleSheet.hairlineWidth or 1. -3. Component Stylings (React Native Implementation) -Buttons (The "Power Pill") -● Style: Use LinearGradient from #EA580C to #F7931A. -● Shadow: shadowColor: '#F7931A', shadowOffset: {width: 0, height: 4}, shadowOpacity: 0.5, shadowRadius: 10. -● Interaction: Use Pressable with Scale animation (to 0.96) on press. -● Text: Uppercase, letterSpacing: 1.5, fontFamily: 'Inter-Bold'. -Cards (The "Blockchain Blocks") -● Surface: backgroundColor: '#0F1115'. -● Border: borderWidth: 1, borderColor: 'rgba(255,255,255,0.1)'. -● Glow: Subtle shadowColor: '#F7931A', shadowOpacity: 0.1 for "active" blocks. -● Layout: Generous padding (padding: 20). -Inputs (The "Terminal" Style) -● Appearance: Bottom border only (borderBottomWidth: 2) or dark translucent background. -● Focus State: Border changes to #F7931A with a subtle outer glow. -● Text: White color, monospace font for numeric inputs. -4. Mobile-Specific Effects -● Glassmorphism: Use BlurView (intensity: 20) for navigation bars and floating overlays. -● Haptic Feedback: Trigger Haptics.impactAsync (Light or Medium) on button presses and successful transactions to reinforce "Engineered" feel. -● Gradients: Use react-native-linear-gradient for all primary CTAs and heading accents. -● Grid Background: Use a repeated SVG pattern of a 50px grid with opacity: 0.05 to simulate the network void. -5. Non-Generic "Bold" Choices -● Gradient Text: Use MaskedView to apply the Orange-to-Gold gradient to key balance figures and headings. -● Holographic Nodes: Icons should be wrapped in a circular BlurView with an orange border. -● Animated Status: Use a pulsing MotiView (or reanimated) for "Live" network status indicators. -● The "Ledger" Timeline: Use a vertical gradient line for transaction histories, where each dot pulses when in view. -6. Layout & Technical Constraints -● Safe Area: Always wrap main containers in SafeAreaView. -● Touch Targets: Minimum 48x48dp for all interactive elements. -● Performance: Use FlashList for heavy transaction lists. Keep animations to the UI thread using React Native Reanimated. - - -Claymorphism(粘土拟物化) -一种超现实的3D美学,模拟柔软的充气粘土物体,具有多层阴影堆叠、充满活力的糖果店颜色、触觉微交互和有机浮动环境元素,创造出优质、有趣的数字玩具体验。 -1. 针对儿童与青少年的教育类应用 -2. 品牌形象中的 3D 角色与插图 -3. 金融科技与加密货币 (轻量版) -4. 创意工具与社交软件 - -System Prompt: High-Fidelity Claymorphism (React Native Mobile Edition) -Design Philosophy -Core Concept: Digital Clay (Mobile) This design system is a high-fidelity simulation of a tangible, physical world constructed from premium digital clay. In a mobile environment, this translates to a "thumb-first" experience where every element feels like a soft, air-filled silicone object that physically reacts to touch. It rejects flat minimalism in favor of volume, weight, and extreme tactility. -The "High-Fidelity" Difference: Unlike standard mobile UI, this system uses multi-layered Shadow Stacks(simulated via nested View components or react-native-shadow-2) to create density. Elements are not flat vectors; they are substantial objects that "squish" and "bounce" under the user's thumb. - -Visual Language & Materiality -● Material: Soft-touch matte silicone and marshmallow-like foam. Surfaces absorb light; reflections are soft and diffused. -● Lighting: A virtual overhead light source (top-left). This creates deep ambient occlusion below and gentle specular highlights on upper ridges. -● The "Super-Rounded" Rule: Zero sharp corners. - ○ Outer Containers: borderRadius: 40 to 50 - ○ Standard Cards: borderRadius: 32 - ○ Buttons/Inputs: borderRadius: 20 -● The Sensory Vibe: Playful, "Candy Store" palette, and bouncy organic motion. - -Design Token System (Mobile) -1. Colors (The Candy Shop Palette) -● Canvas (BG): #F4F1FA (Cool lavender-white. Avoid pure white). -● Text (Primary): #332F3A (Soft Charcoal). -● Text (Muted): #635F69 (Dark Lavender-Gray - minimum for WCAG). -● Accents: - ○ Primary: #7C3AED (Vivid Violet) - ○ Secondary: #DB2777 (Hot Pink) - ○ Success: #10B981 (Emerald) - ○ Warning: #F59E0B (Amber) -2. Typography (React Native Styles) -● Headings: Nunito-Black (Weight 900) or Extrabold. Rounded terminals are mandatory. -● Body: DMSans-Medium (Weight 500). Clean and geometric. -● Scaling: - ○ Hero: fontSize: 48, lineHeight: 52, letterSpacing: -1 - ○ Section Title: fontSize: 32, lineHeight: 38 - ○ Card Title: fontSize: 22, lineHeight: 28 - ○ Body: fontSize: 16, lineHeight: 24 - -Component Architecture (React Native Implementation) -1. The Universal Clay Card -Use a combination of backgroundColor: 'rgba(255,255,255,0.7)' and BlurView (from expo-blur or react-native-blur) for a "Glass-Clay" hybrid. -● Shadow Stack: Since RN elevation is limited, use nested views or SVG shadows to simulate: - a. Outer: offset: {12, 12}, blur: 24, color: 'rgba(160, 150, 180, 0.2)' - b. Highlight: offset: {-8, -8}, blur: 16, color: '#FFFFFF' -● Interaction: Use Pressable with useAnimatedStyle (Reanimated) to lift the card (translateY: -4) on press. -2. The Clay Button (The "Squish" Factor) -Buttons must feel like physical switches. -● Base: height: 56, borderRadius: 20, justifyContent: 'center'. -● Gradient: Use LinearGradient from #A78BFA to #7C3AED. -● Micro-Physics: * On Press In: scale: 0.92, shadowOpacity decreases. - ○ On Press Out: Spring back to scale: 1.0 with damping: 10. -● Shadow: Deep colored shadow matching the button hue. -3. The Recessed Input -Simulates a finger-press into the clay. -● Style: backgroundColor: '#EFEBF5', height: 64, paddingHorizontal: 24. -● Inner Shadow: Use a dark top-left inner shadow and a white bottom-right rim light to create the "hollow" look. - -Animation System (Powered by Reanimated) -1. Buoyancy (Float): Background blobs should use withRepeat(withTiming(...)) to drift ±20px over 8-10 seconds. -2. Clay Breathe: Stat orbs should subtly scale between 1.0 and 1.03 every 4 seconds. -3. Haptic Feedback: Every "Squish" interaction (Buttons/Cards) must trigger Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light). - -Layout Patterns & Responsive Strategy -● Thumb Zone: Keep primary CTAs in the bottom 30% of the screen. -● Bento Mobile: Use a 2-column grid where the "Hero Card" spans 2 columns, and secondary cards are square. -● Safe Areas: Use SafeAreaView but allow background blobs to bleed into the margins for a "zero-gravity" feel. - -Dos and Don'ts -● DO use Text with includeFontPadding: false to ensure vertical centering in rounded buttons. -● DO use Spring physics for all transitions; avoid linear easing. -● DO nested border radii: if a card is 32, the image inside is 24. -● DON'T use standard Android elevation. It creates flat, gray shadows that break the clay aesthetic. Use SVG-based shadows. -● DON'T use sharp corners (< 20). -● DON'T use pure black #000000 for text or shadows. - - -Enterprise(企业级/政企风) -现代SaaS美学平衡了专业性和可接近性。鲜艳的靛蓝/紫罗兰色渐变、柔和的阴影、等距深度和干净的几何无衬线字体。 -1. 后台管理系统 -2. 生产力工具与协作平台 -3. 政务与金融类严肃应用 -4. 垂直领域专业软件 -Here is a translated version of your web design prompt, adapted specifically for mobile application development (iOS/Android/React Native/Flutter). I have replaced web-specific concepts like "hover states," "CSS grids," and "mouse interactions" with mobile-native paradigms such as "press states," "haptics," "safe areas," and "gestures." - -App Design System Prompt: Modern Enterprise SaaS (Mobile) -1. Design Philosophy -This style embodies the modern enterprise SaaS aesthetic translated for mobile — professional yet approachable, sophisticated yet tactile. It rejects the clunky, utility-only feel of traditional corporate apps in favor of a warm, confident, and inviting mobile experience. -Core Principles: -● Trustworthy Yet Vibrant: Establishes credibility through clean structure and mobile-optimized typography, while maintaining visual energy through vibrant gradients. -● Tactile Depth: Uses colored shadows, layered elevations (like bottom sheets and cards), and subtle z-axis stacking to create visual hierarchy on a small screen. -● Refined Elegance: Every element is polished, focusing on fluid gestures, spring-physics animations, and sophisticated press states. -● Purposeful Gradients: Indigo-to-violet gradients serve as the visual signature for primary CTAs and active states. -● Mobile Polish: Generous spacing, safe-area awareness, and crisp typography create a premium, enterprise-ready feel in the palm of the hand. -Keywords: Trustworthy, Vibrant, Polished, Tactile, Modern, Approachable, Enterprise-Ready, Fluid. -2. Design Token System -Colors (Light Mode) -● Background: #F8FAFC (Slate 50) - A very subtle cool grey/white base for screen backgrounds. -● Surface: #FFFFFF (White) - For cards, bottom sheets, and elevated elements. -● Primary: #4F46E5 (Indigo 600) - The core brand color. Vibrant blue-purple. -● Secondary: #7C3AED (Violet 600) - For gradients, badges, and accents. -● Text Main: #0F172A (Slate 900) - High contrast, sharp for primary readability. -● Text Muted: #64748B (Slate 500) - For supporting text and inactive tab icons. -● Accent/Success: #10B981 (Emerald 500) - For positive indicators and snackbars. -● Divider/Border: #E2E8F0 (Slate 200) - Subtle separation for list items. -Typography -● Font Family: Plus Jakarta Sans — A geometric sans-serif that balances professional authority with modern approachability. High legibility on mobile screens. -● Scaling: Designed to support iOS Dynamic Type and Android font scaling. -● Font Weights: - ○ Display/Screen Titles: ExtraBold (800) - ○ Section Headers: Bold (700) - ○ Card Titles/Buttons: SemiBold (600) - ○ Body Text: Regular (400) -● Line Heights: Tight for titles (1.1-1.2), relaxed for body text (1.4-1.5) to ensure comfortable reading on narrow screens. -Radius & Border -● Screen Elements: 16pt/dp for large cards and bottom sheets. -● Interactive Elements: 8pt/dp for inputs and small chips. -● Buttons: Fully rounded (pill shape) or 12pt/dp for a modern tech feel. -● Borders: Hairline (0.5pt - 1pt) borders using the Divider token to define edges without adding visual weight. -Shadows & Depth -● Card Elevation: Soft, colored shadows replacing neutral grays. Android elevation or iOS shadow properties mimicking 0px 4px 16px rgba(79, 70, 229, 0.08). -● Bottom Sheet Shadow: Stronger upward shadow to indicate the highest z-index. -● Button Shadow (Primary): 0px 4px 12px rgba(79, 70, 229, 0.25) to make primary actions highly prominent. -3. Component Stylings -Buttons -● Primary CTA: Full width (minus screen padding) at the bottom of the screen. Background uses the Indigo-to-Violet gradient. White text. -● Secondary: White surface, slate border, slate text. -● Press State: Instead of web hover, use an active press state that slightly scales the button down (0.95x) and reduces shadow, paired with subtle haptic feedback. -Cards & Lists -● Base: White background, 16pt radius, hairline border, soft colored shadow. -● Layout: Horizontal scrolling (carousels) for feature discovery, vertical standard lists for data. -● List Items: Edge-to-edge with inset dividers, utilizing leading icons (in soft-colored circular backgrounds). -Inputs & Forms -● Style: White background, 8pt radius, light slate border. -● Active/Focused State: Border transitions to Indigo, label animates to a floating position, keyboard automatically presents. -● Error State: Red border with an immediate, subtle shake animation. -4. App-Specific Bold Choices -● Haptic Integration: Tied to micro-interactions. Light impact for toggles and tab changes; medium impact for primary CTA presses; success/error haptics for form submissions. -● Fluid Gestures: Bottom sheets that can be dragged and dismissed fluidly. Cards that support swipe-to-action (e.g., swipe left to archive) with color reveals underneath. -● Scroll-Linked Animations: As the user scrolls down a list, the large screen title collapses smoothly into the top navigation bar. -● Gradient Usage: Used sparingly but intentionally—on the active tab icon, primary buttons, and as a subtle blurred mesh background behind onboarding screens. -● Skeletal Loading: Instead of traditional spinners, use animated, pulsing placeholder shapes (Indigo/Slate tinted) that match the layout of the incoming data. -5. Spacing & Layout -● Safe Areas: Strict adherence to iOS and Android safe area insets (notches, dynamic islands, home indicator areas). -● Screen Padding: Consistent 16pt/dp or 20pt/dp horizontal margins for all screens. -● Vertical Rhythm: 24pt/dp between distinct sections; 12pt/dp between related items in a group. -● Navigation: - ○ Bottom Tab Navigation for primary destinations (3-5 items). - ○ Top App Bar for contextual actions, screen titles, and back buttons. -● Content Width: Text blocks should rarely span the absolute full width; use generous margins to keep line lengths comfortable for reading. -6. Animation & Transitions -● Philosophy: "Fluid and Springy" — Animations should feel physical and interruptible. -● Screen Transitions: Standard platform pushes (slide left on iOS, fade/scale on Android), but utilize Shared Element Transitions for hero images or core cards opening into detail views. -● Press Animations: Scale-down (scale: 0.97) on press down, spring back on release. -● Easing: Avoid linear animations. Use spring physics for layout changes and ease-out for opacities. -7. Iconography & Touch Targets -● Library: Lucide-react-native (or equivalent SVG system). -● Style: 2px stroke width, rounded caps and joins for a friendly, modern feel. -● Touch Targets: Crucial for mobile. Minimum tap area of 44x44 pt (iOS) or 48x48 dp (Android) for all interactive elements, even if the visual icon is only 24x24. -● Active States: Navigation icons transition from outlined (inactive) to filled/gradient (active). -8. Accessibility & Best Practices -● Dynamic Text: UI must not break when users increase their system font size. Text must wrap or truncate gracefully. -● Contrast: All text meets WCAG AA standards. Slate 900 on Slate 50 background is highly compliant. -● Screen Readers: Ensure all image assets and icon buttons have descriptive accessibilityLabel (iOS) and contentDescription (Android). -● Dark Mode Readiness: While this spec is light-mode first, tokens should be structured to easily invert (e.g., Slate 50 becomes Slate 900, gradients become slightly muted). -● Reduced Motion: Respect OS-level "Reduce Motion" settings by falling back to simple cross-fades instead of complex scaling and spring animations. - - - -Sketch(素描/手绘风) -有机的不稳定边框、手写排版、纸张纹理和俏皮的不完美。每个元素都是用记号笔和铅笔在纹理纸上勾勒出来的。 -1. 原型设计与早期创意提案 (Low-Fidelity Prototyping) -2. 创意品牌、艺术与独立文化项目 -3. 教育、绘本与儿童类产品 -4. 游戏化 UI 与 叙事解谜游戏 - -Design Philosophy -The Hand-Drawn mobile design style celebrates authentic imperfection and human touch within the palm of the hand. It rejects the sterile precision of typical mobile UI in favor of organic, playful irregularity that evokes a portable sketchbook, a pocket notebook, or a collection of sticky notes. -Core Principles: -● No Straight Lines: Every View and TouchableOpacity uses complex borderRadius logic or SVG masks to create wobbly edges that reject geometric perfection. -● Tactile Texture: Use background overlays or tiled images to simulate physical media like grain, paper, or notebook grids, making the screen feel like a physical surface. -● Playful Tilt: Elements utilize small transform: [{ rotate: '-1deg' }] properties to break the rigid mobile grid and create casual energy. -● Hard Offset Shadows: Strictly avoid elevation (Android) or shadowRadius (iOS) that creates blur. Use a secondary "shadow layer" View offset by (4px, 4px) to create a cut-paper, layered collage aesthetic. -● Handwritten Typography: Exclusively use handwritten fonts (Kalam, Patrick Hand). Typography should vary in size and rotation to look like quick notes written on the fly. -● Scribbled Overlays: Use absolute-positioned SVGs for flourishes like hand-drawn arrows, "tape" effects on images, and "X" marks for close buttons. -● Intentional Messiness: Embrace slight overlaps and asymmetrical margins that make the app feel spontaneous rather than manufactured. -● Emotional Intent: This style lowers the "fear of interaction" by appearing unfinished and approachable. It is perfect for journaling apps, creative tools, education, or any product that wants to feel human-centered rather than corporate. - -Design Token System -Colors (Single Palette - Light Mode) -● Background: #fdfbf7 (Warm Paper) -● Foreground: #2d2d2d (Soft Pencil Black) -● Muted: #e5e0d8 (Old Paper / Erased Pencil) -● Accent: #ff4d4d (Red Correction Marker) -● Border: #2d2d2d (Pencil Lead) -● Secondary Accent: #2d5da1 (Blue Ballpoint Pen) -Typography -● Headings: Kalam-Bold - High visual weight, felt-tip marker style. -● Body: PatrickHand-Regular - Highly legible but distinctly human. -● Scale: Large and readable for mobile. Headings should have lineHeight adjusted to allow for "scribbled" descenders. -Radius & Border -● Wobbly Borders: Since React Native doesn't support the slash / syntax for borderRadius, use unique values for each corner (e.g., borderTopLeftRadius: 15, borderTopRightRadius: 25, borderBottomLeftRadius: 20, borderBottomRightRadius: 10). -● Border Width: Bold and visible. borderWidth: 2 minimum; borderWidth: 3 for primary actions. -● Style: solid for containers; dashed for "cut-out" sections or empty states. -Shadows & Effects -● Hard Offset Shadows: Since shadowOpacity with 0 radius is inconsistent across platforms, implement as a "Shadow View" placed behind the main component. - ○ Standard: Offset { x: 4, y: 4 } with #2d2d2d. - ○ Active (Press): Offset { x: 0, y: 0 } (the button "sinks" into the paper). -● Paper Texture: Use a repeating pattern background image or a very subtle radial-gradient SVG overlay across the entire screen. - -Component Stylings -Buttons (TouchableOpacity) -● Shape: Irregular wobbly rectangle. -● Normal State: * White background, borderWidth: 3, Pencil Black text. - ○ Hard offset shadow view behind it. -● Pressed State (activeOpacity={1}): - ○ Main button transform: [{ translateX: 4 }, { translateY: 4 }]. - ○ The background "shadow view" is covered, simulating a physical press. -● Variant: "Post-it" Yellow (#fff9c4) for primary CTAs. -Cards/Containers -● Base Style: White background, wobbly border, slight rotation (-1deg or 1deg). -● Decoration Types: - ○ "Tape": A semi-transparent View (rgba(200, 200, 200, 0.5)) positioned absolutely at the top, slightly rotated to look like it holds the card. - ○ "Tack": A small red circle SVG at the top center. -● Speech Bubbles: Use a small SVG triangle attached to the bottom of the container for "hint" text or tooltips. -Inputs (TextInput) -● Style: Full box with wobbly borderRadius. -● Font: PatrickHand-Regular. -● Focus State: Border color changes to Blue Ballpoint (#2d5da1) with a slight increase in borderWidth. -● Placeholder: Muted Pencil (#2d2d2d60). - -Layout Strategy -● The "Anti-Grid": Avoid perfect alignment. Give adjacent cards slightly different rotation values. -● Layering: Use zIndex to stack "tape" over cards and cards over background scribbles. -● Visual Rhythm: Use padding: 20 for standard screens. Use large gap values between vertical elements to let the "paper" breathe. -● Overflow: Allow decorative elements (like a hand-drawn star) to bleed slightly off the edge of the screen. - -Non-Genericness (Bold Choices) -● Sketchy Loaders: Instead of a spinner, use an animation that "scribbles" a circle or toggles between three hand-drawn frames. -● Hand-Drawn Icons: Use icons that look like they were drawn with a 0.5mm fineliner (thick strokes, unclosed loops). -● Strikethrough: When a task is completed or an item is deleted, use a red "scribble-out" SVG overlay instead of a simple line. -● Wavy Dividers: Replace standard horizontal rules with a hand-drawn squiggly line SVG. - -Effects & Animation -● The "Jiggle": When a user triggers an error, use a Sequence animation that rapidly rotates the element between -2deg and 2deg. -● Snappy Transitions: Use LayoutAnimation.configureNext(LayoutAnimation.Presets.spring) for all layout changes to give a bouncy, physical feel. - -Mobile-First Strategy (React Native) -● Touch Targets: Ensure all wobbly buttons have a minimum hit area of 48x48 even if the visual "ink" is smaller. -● Safe Area: Ensure the "Paper" background extends into the SafeAreaView to maintain the illusion of physical media. -● Performance: Use useNativeDriver: true for rotations and transforms to keep the "hand-drawn" movements fluid at 60fps. -● Haptics: Add Haptics.impactAsync on button presses to reinforce the feeling of "clicking" a physical object. - - -Neumorphism(新拟物化) -通过单色背景上的双阴影拉伸和插入元素。柔软、触感好、物理接地,可接近性极佳。 -1. 极简主义硬件控制与智能家居 -2. 追求审美感的工具类 App -3. 金融与健康监测看板(非密集型) -4. 品牌营销与展示型页面 - -Design Philosophy (React Native / Mobile) -Core Principles: Neumorphism on mobile creates a tactile, physical interface through dual shadows. On React Native, this is achieved by layering View components or using specialized libraries to simulate the light source (top-left) and dark shadow (bottom-right). Every component is "molded" from the base material (#E0E5EC). Buttons are never flat; they are either "pillowed" (convex) or "wells" (concave/inset). -Vibe: Premium, ceramic-like feel. It prioritizes the "thump" of a physical button press. It is a calm, monochromatic experience that uses depth rather than color to establish hierarchy. -Unique Visual Signatures (Mobile Specific): -● Dual-Shadow Layering: Since React Native's shadowColor only supports one direction, use nested Viewcomponents or react-native-shadow-2 to achieve the dual-light effect. -● Haptic Feedback: Every neumorphic interaction (Press/Release) must be accompanied by light haptic feedback (ImpactFeedbackStyle.Light). -● Physical Press: Active states should use transform: [{ scale: 0.97 }] to simulate material compression. -● Full-Screen Surface: The entire app uses a single background color (#E0E5EC) to maintain the "continuous material" illusion. - -Design Token System (The DNA) -Colors (Light Mode - Cool Monochromatic) -● Background: #E0E5EC (The "Clay" base). -● Text Primary: #3D4852 (7.5:1 contrast). -● Text Muted: #6B7280 (4.6:1 contrast). -● Accent: #6C63FF (Used for active toggles or primary CTAs). -● Shadow Light: rgba(255, 255, 255, 0.6) (Top-Left). -● Shadow Dark: rgba(163, 177, 198, 0.7) (Bottom-Right). -Typography -● Font Family: Use System (San Francisco/Roboto) or PlusJakartaSans-Bold. -● Sizing: * Heading: 24pt-32pt, Bold. - ○ Body: 16pt, Medium. - ○ Caption: 12pt, Regular. -● Letter Spacing: -0.5 for headings to enhance the modern look. -Radius (React Native borderRadius) -● Large Container: 32 -● Standard Button/Input: 16 -● Pill/Search Bar: 999 - -Shadows & Effects (The Physics) -Note: React Native requires specific shadow properties for iOS and elevation for Android. For high-fidelity Neumorphism, use a wrapping implementation. -Extruded (Convex - Resting) -● Outer Shadow (Dark): shadowOffset: { width: 6, height: 6 }, shadowOpacity: 1, shadowRadius: 10, shadowColor: 'rgba(163, 177, 198, 0.7)' -● Inner Shadow (Light): shadowOffset: { width: -6, height: -6 }, shadowOpacity: 1, shadowRadius: 10, shadowColor: 'rgba(255, 255, 255, 0.6)' -Inset (Concave - Pressed/Input) -● Concept: Use a slightly darker background color (#D1D9E6) or internal gradients/shadows to simulate a "carved out" look. -● Visual: backgroundColor: '#E0E5EC', plus internal borders of 1px with a darker shade at the top. - -Component Styling (React Native) -Buttons -● Base: height: 56, borderRadius: 16, justifyContent: 'center', alignItems: 'center'. -● Default State: Extruded dual shadow. -● Active State (Pressable): Remove outer shadows, apply scale: 0.98, and change background slightly to indicate an "inset" state. -● Haptics: Trigger Haptics.impactAsync on onPressIn. -Cards -● Base: padding: 24, borderRadius: 32, backgroundColor: '#E0E5EC'. -● Hierarchy: Use "Nested Depth." A card (Extruded) contains an Icon Container (Inset Deep). -Inputs -● Style: height: 50, borderRadius: 16, paddingHorizontal: 16. -● Depth: Must be Inset (Concave) to look like a physical slot. -● Focus: Border color changes to #6C63FF with a thin 1.5px width. - -Animation & Micro-interactions -● Framework: Use React Native Reanimated or LayoutAnimation. -● Transitions: Duration: 250ms, Easing: Bezier(0.4, 0, 0.2, 1). -● Interaction: * Scale: Buttons shrink by 2-3% when pressed. - ○ Shadow Transition: Smoothly interpolate shadow opacity from 1 to 0.4 when an element is "pressed" into the surface. - -Layout & Accessibility -● Safe Area: Always wrap in SafeAreaView. -● Touch Targets: Minimum 48x48 for all interactive elements. -● Spacing: Use a 8pt grid system (8, 16, 24, 32, 48). -● Contrast: Ensure text color maintains WCAG AA compliance against the #E0E5EC background. - -Anti-Patterns (Do Not Do) -● No Black Shadows: Never use rgba(0,0,0,x). Shadows must be blue-grey to match the "Cool Grey" material. -● No Pure White Backgrounds: The screen background MUST be #E0E5EC. -● No Sharp Edges: Avoid borderRadius < 12. -● No Flat Borders: Do not use borderWidth: 1 as a substitute for depth unless in a focus state. \ No newline at end of file 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 62dfbc8a..bad22d34 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 @@ -16,6 +16,7 @@ Usage: import csv import json import os +import re import sys import io from datetime import datetime @@ -695,6 +696,17 @@ def generate_design_system(query: str, project_name: str = None, output_format: # ============ PERSISTENCE FUNCTIONS ============ +def safe_slug(name, fallback: str = "default") -> str: + """Slugify a name into a single safe path segment. + + Only [a-z0-9_-] survives; every other character (including '/', '\\' and + '.') collapses into '-'. This makes path traversal via project/page names + (e.g. "../../etc") impossible — the slug can never leave its parent dir. + """ + slug = re.sub(r'[^a-z0-9_-]+', '-', str(name).lower()).strip('-') + return slug or fallback + + def persist_design_system(design_system: dict, page: str = None, output_dir: str = None, page_query: str = None) -> dict: """ Persist design system to design-system// folder using Master + Overrides pattern. @@ -711,9 +723,8 @@ def persist_design_system(design_system: dict, page: str = None, output_dir: str base_dir = Path(output_dir) if output_dir else Path.cwd() # Use project name for project-specific folder. Coalesce falsy values - # (missing key, explicit None, or "") so the .lower() below can't crash. - project_name = design_system.get("project_name") or "default" - project_slug = project_name.lower().replace(' ', '-') + # (missing key, explicit None, or "") so slugification can't crash. + project_slug = safe_slug(design_system.get("project_name") or "default") design_system_dir = base_dir / "design-system" / project_slug pages_dir = design_system_dir / "pages" @@ -734,7 +745,7 @@ def persist_design_system(design_system: dict, page: str = None, output_dir: str # If page is specified, create page override file with intelligent content if page: - page_file = pages_dir / f"{page.lower().replace(' ', '-')}.md" + page_file = pages_dir / f"{safe_slug(page, 'page')}.md" page_content = format_page_override_md(design_system, page, page_query) with open(page_file, 'w', encoding='utf-8') as f: f.write(page_content) diff --git a/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/scripts/search.py b/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/scripts/search.py index e960ceac..6727ca56 100644 --- a/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/scripts/search.py +++ b/plugins/codex/plugins/ui-ux-pro-max/skills/ui-ux-pro-max/scripts/search.py @@ -24,7 +24,7 @@ import argparse import sys import io from core import CSV_CONFIG, AVAILABLE_STACKS, MAX_RESULTS, search, search_stack -from design_system import generate_design_system, persist_design_system +from design_system import generate_design_system, persist_design_system, safe_slug # Force UTF-8 for stdout/stderr to handle emojis on Windows (cp1252 default) if sys.stdout.encoding and sys.stdout.encoding.lower() != 'utf-8': @@ -98,12 +98,12 @@ if __name__ == "__main__": # Print persistence confirmation if args.persist: - project_slug = (args.project_name or args.query).lower().replace(' ', '-') + project_slug = safe_slug(args.project_name or args.query.upper()) print("\n" + "=" * 60) print(f"✅ Design system persisted to design-system/{project_slug}/") print(f" 📄 design-system/{project_slug}/MASTER.md (Global Source of Truth)") if args.page: - page_filename = args.page.lower().replace(' ', '-') + page_filename = safe_slug(args.page, 'page') print(f" 📄 design-system/{project_slug}/pages/{page_filename}.md (Page Overrides)") print("") print(f"📖 Usage: When building a page, check design-system/{project_slug}/pages/[page].md first.")