Bläddra i källkod

Add dark business card skill and improve title layout

bob.yuxinyang 1 månad sedan
förälder
incheckning
cadf7a9f60

+ 5 - 0
create-less-and-cosmos-business-card/README.md

@@ -0,0 +1,5 @@
+# 纯粹星辰浅色名片 Skill
+
+在 Codex 对话中直接说:
+
+> 使用 `$create-less-and-cosmos-business-card`,为「姓名 / 中文职位 / English Title / 邮箱 / 手机号」生成浅色双面名片。

+ 22 - 38
create-less-and-cosmos-business-card/SKILL.md

@@ -1,69 +1,53 @@
 ---
 name: create-less-and-cosmos-business-card
-description: Create matching minimalist, high-end, double-sided business cards for 杭州纯粹星辰人工智能科技有限公司 (Less and Cosmos) from a person's name, title, email, and phone number. Use when Codex needs to make, add, revise, preview, or export a 90×54 mm 纯粹星辰 employee business card in SVG or PNG while preserving the supplied brand logo, warm-white palette, generous whitespace, slogan, company address, and established layout.
+description: Create, revise, preview, and export minimalist warm-white double-sided employee business cards for 杭州纯粹星辰人工智能科技有限公司 (Less and Cosmos) from a Chinese name, bilingual title, email, and phone number. Use for the light 90×54 mm 纯粹星辰 business-card style in SVG or PNG while preserving the supplied brand system and automatically splitting bilingual titles onto separate lines.
 ---
 
-# Create Less and Cosmos Business Cards
+# Create Less and Cosmos Light Business Cards
 
-Generate the card from the bundled templates; do not redraw or reinterpret the visual system unless the user explicitly requests a redesign.
+Generate from the bundled templates. Preserve the layout and light visual system unless the user explicitly requests a redesign.
 
 ## Required input
 
-Collect or infer only these employee-specific fields:
+Collect or infer only:
 
 - Chinese name
-- Chinese title, optionally followed by an English abbreviation such as `联合创始人 / CMO`
+- Title, preferably `中文职位 / English Title`
 - Email
 - Mobile number
 
-Keep these brand fields fixed unless the user supplies corrections:
+Keep these fixed unless corrected by the user:
 
 - Company: `杭州纯粹星辰人工智能科技有限公司`
 - Address: `浙江省杭州市西湖区转塘街道云腾丝路中心5号楼商业208室`
 - Website: `lessncosmos.com`
 - Slogan: `构建能够持续学习与进化的企业级人工智能系统。`
 
-## Generate the card
+## Generate
 
-Run the bundled generator from the skill directory:
+Run from the skill directory:
 
 ```bash
 python3 scripts/generate_card.py \
-  --name "石志瑞" \
-  --title "联合创始人 / CMO" \
-  --email "shizr@lessncosmos.com" \
-  --phone "139 1051 5989" \
-  --output-dir ./output/shi-zhirui
+  --name "林知远" \
+  --title "AI 产品负责人 / Head of AI Product" \
+  --email "linzy@lessncosmos.com" \
+  --phone "138 0000 0000" \
+  --output-dir ./output/lin-zhiyuan
 ```
 
-The script creates:
+The generator writes `front.svg`, `back.svg`, `preview.html`, and `card-data.json`. It adds `+86` when the phone has no country code.
 
-- `front.svg`: common brand front
-- `back.svg`: personalized contact side
-- `preview.html`: side-by-side browser preview
-- `card-data.json`: normalized source data for later edits
-
-Use a short lowercase Latin slug for the output directory. Preserve spaces in phone groups; the generator adds `+86` when no country code is present.
+The title is laid out automatically: text before `/` is the primary Chinese line; text after `/` is the smaller English line. Do not manually edit positioned title text.
 
 ## Render and verify
 
-1. Render both SVG files or open `preview.html` in a browser.
-2. Produce 1800×1080 PNG files when the user requests images or a downloadable bundle. Prefer an installed SVG renderer such as Sharp, a browser screenshot, Inkscape, or `rsvg-convert`.
-3. Visually check the preview at full size. Confirm that the email does not overflow and all Chinese glyphs render correctly.
-4. Compare every output field with `card-data.json`; never silently correct a person's name or email.
-5. Package the two SVG files, PNG files when rendered, preview, and JSON into a ZIP for sharing.
-
-## Preserve the visual system
-
-- Keep the canvas at 90×54 mm with `viewBox="0 0 1800 1080"`.
-- Keep the warm-white background `#F7F7F5`, near-black text, cool gray secondary text, and restrained gold accent `#B89652`.
-- Keep the front common across all employees.
-- Keep the employee name and title on the left of the back, contact information on the right, and company/address below the divider.
-- Maintain the generous margins and avoid adding icons, QR codes, borders, gradients, shadows, or decorative imagery unless requested.
-- Use the font fallbacks already embedded in the SVG. Do not outline text unless a print vendor requires it.
-
-For production details and delivery checks, read [references/print-specifications.md](references/print-specifications.md).
+1. Render both SVG files or open `preview.html`.
+2. When delivering images, render 1800×1080 PNG files.
+3. Check Chinese glyphs, title wrapping, and email overflow visually.
+4. Compare every field with `card-data.json`.
+5. Package SVG, PNG, preview, and JSON files into a ZIP.
 
-## Revise an existing card
+Keep the 90×54 mm canvas, warm-white `#F7F7F5` background, cool gray text, gold accent, generous margins, and existing layout. Read [references/print-specifications.md](references/print-specifications.md) for print delivery.
 
-Regenerate from the corrected fields instead of editing positioned text manually. Re-render and re-check the result after every data change.
+Regenerate after data changes; do not edit personalized text manually.

+ 3 - 3
create-less-and-cosmos-business-card/agents/openai.yaml

@@ -1,4 +1,4 @@
 interface:
-  display_name: "纯粹星辰名片制作"
-  short_description: "根据人员信息生成纯粹星辰品牌的极简双面商务名片及预览文件"
-  default_prompt: "使用 $create-less-and-cosmos-business-card 根据姓名、职位、邮箱和电话生成一套双面名片。"
+  display_name: "纯粹星辰浅色名片制作"
+  short_description: "根据人员信息生成纯粹星辰品牌的浅色极简双面商务名片"
+  default_prompt: "使用 $create-less-and-cosmos-business-card 根据姓名、职位、邮箱和电话生成一套浅色双面名片。"

+ 4 - 1
create-less-and-cosmos-business-card/assets/back-template.svg

@@ -4,7 +4,10 @@
   <g opacity="0.11" transform="translate(1170 52) scale(2.1)"><circle cx="12" cy="36" r="4" fill="#B89652"/><path d="M38 43C76 11 142 1 204 15C228 21 249 31 266 44" fill="none" stroke="#4E5862" stroke-width="1.15" stroke-linecap="round"/><path d="M66 49C117 31 187 28 246 45" fill="none" stroke="#0B0C10" stroke-width="0.62" stroke-linecap="round"/><circle cx="268" cy="45" r="1.35" fill="#0B0C10"/></g>
   <circle cx="150" cy="152" r="6" fill="#B89652"/>
   <text x="150" y="350" fill="#111318" font-family="'PingFang SC','Noto Sans SC','Source Han Sans SC','Microsoft YaHei',sans-serif" font-size="130" font-weight="300" letter-spacing="13">{{NAME}}</text>
-  <text x="156" y="435" fill="#656B72" font-family="'PingFang SC','Noto Sans SC','Source Han Sans SC','Microsoft YaHei',sans-serif" font-size="42" font-weight="400" letter-spacing="5">{{TITLE}}</text>
+  <text x="156" y="425" fill="#656B72" font-family="'PingFang SC','Noto Sans SC','Source Han Sans SC','Microsoft YaHei',sans-serif" font-size="42" font-weight="400" letter-spacing="5">
+    <tspan x="156" dy="0">{{TITLE_ZH}}</tspan>
+    <tspan x="156" dy="58" fill="#8B9096" font-family="Inter,'Helvetica Neue',Arial,sans-serif" font-size="28" font-weight="400" letter-spacing="2.2">{{TITLE_EN}}</tspan>
+  </text>
   <g transform="translate(912 337)" font-family="Inter,'Helvetica Neue',Arial,sans-serif">
     <text x="0" y="0" fill="#999DA2" font-size="22" font-weight="600" letter-spacing="4">EMAIL</text><text x="0" y="53" fill="#20242A" font-size="38" font-weight="400" letter-spacing="0.2">{{EMAIL}}</text>
     <text x="0" y="145" fill="#999DA2" font-size="22" font-weight="600" letter-spacing="4">MOBILE</text><text x="0" y="198" fill="#20242A" font-size="38" font-weight="400" letter-spacing="1.2">{{PHONE}}</text>

+ 9 - 0
create-less-and-cosmos-business-card/scripts/generate_card.py

@@ -31,6 +31,13 @@ def normalize_phone(value: str) -> str:
     return "+86 " + value
 
 
+def split_title(value: str) -> tuple[str, str]:
+    parts = re.split(r"\s*[//]\s*", value, maxsplit=1)
+    if len(parts) == 1:
+        return parts[0], ""
+    return parts[0], parts[1]
+
+
 def render_template(template: str, values: dict[str, str]) -> str:
     for key, value in values.items():
         template = template.replace("{{" + key + "}}", html.escape(value))
@@ -66,6 +73,7 @@ def main() -> None:
 
     name = clean(args.name, "name")
     title = clean(args.title, "title")
+    title_zh, title_en = split_title(title)
     email = clean(args.email, "email")
     if "@" not in email or email.startswith("@") or email.endswith("@"):
         raise ValueError("email does not look valid")
@@ -88,6 +96,7 @@ def main() -> None:
     )
     template = (ASSETS_DIR / "back-template.svg").read_text(encoding="utf-8")
     replacements = {key.upper(): value for key, value in data.items()}
+    replacements.update({"TITLE_ZH": title_zh, "TITLE_EN": title_en})
     (args.output_dir / "back.svg").write_text(
         render_template(template, replacements), encoding="utf-8"
     )

+ 5 - 0
create-less-and-cosmos-dark-business-card/README.md

@@ -0,0 +1,5 @@
+# 纯粹星辰暗色名片 Skill
+
+在 Codex 对话中直接说:
+
+> 使用 `$create-less-and-cosmos-dark-business-card`,为「姓名 / 中文职位 / English Title / 邮箱 / 手机号」生成暗色双面名片。

+ 53 - 0
create-less-and-cosmos-dark-business-card/SKILL.md

@@ -0,0 +1,53 @@
+---
+name: create-less-and-cosmos-dark-business-card
+description: Create, revise, preview, and export minimalist dark double-sided employee business cards for 杭州纯粹星辰人工智能科技有限公司 (Less and Cosmos) from a Chinese name, bilingual title, email, and phone number. Use for the black 90×54 mm 纯粹星辰 business-card style in SVG or PNG while preserving the supplied dark brand system and automatically splitting bilingual titles onto separate lines.
+---
+
+# Create Less and Cosmos Dark Business Cards
+
+Generate from this skill's bundled dark templates. Do not depend on the light-card skill. Preserve the layout and dark visual system unless the user explicitly requests a redesign.
+
+## Required input
+
+Collect or infer only:
+
+- Chinese name
+- Title, preferably `中文职位 / English Title`
+- Email
+- Mobile number
+
+Keep these fixed unless corrected by the user:
+
+- Company: `杭州纯粹星辰人工智能科技有限公司`
+- Address: `浙江省杭州市西湖区转塘街道云腾丝路中心5号楼商业208室`
+- Website: `lessncosmos.com`
+- Slogan: `构建能够持续学习与进化的企业级人工智能系统。`
+
+## Generate
+
+Run from the skill directory:
+
+```bash
+python3 scripts/generate_card.py \
+  --name "林知远" \
+  --title "AI 产品负责人 / Head of AI Product" \
+  --email "linzy@lessncosmos.com" \
+  --phone "138 0000 0000" \
+  --output-dir ./output/lin-zhiyuan
+```
+
+The generator writes `front.svg`, `back.svg`, `preview.html`, and `card-data.json`. It adds `+86` when the phone has no country code.
+
+The title is laid out automatically: text before `/` is the primary Chinese line; text after `/` is the smaller English line. Do not manually edit positioned title text.
+
+## Render and verify
+
+1. Render both SVG files or open `preview.html`.
+2. When delivering images, render 1800×1080 PNG files.
+3. Check Chinese glyphs, title wrapping, and email overflow visually.
+4. Compare every field with `card-data.json`.
+5. Package SVG, PNG, preview, and JSON files into a ZIP.
+
+Keep the 90×54 mm canvas, near-black `#0B0C10` background, soft-white text, cool gray secondary text, pale-gold accent, generous margins, and existing layout. Read [references/print-specifications.md](references/print-specifications.md) for print delivery.
+
+Regenerate after data changes; do not edit personalized text manually.

+ 4 - 0
create-less-and-cosmos-dark-business-card/agents/openai.yaml

@@ -0,0 +1,4 @@
+interface:
+  display_name: "纯粹星辰暗色名片制作"
+  short_description: "根据人员信息生成纯粹星辰品牌的暗色极简双面商务名片"
+  default_prompt: "使用 $create-less-and-cosmos-dark-business-card 根据姓名、职位、邮箱和电话生成一套暗色双面名片。"

+ 19 - 0
create-less-and-cosmos-dark-business-card/assets/back-template.svg

@@ -0,0 +1,19 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="90mm" height="54mm" viewBox="0 0 1800 1080" role="img" aria-labelledby="title desc">
+  <title id="title">{{NAME}}名片背面</title><desc id="desc">{{NAME}},{{TITLE}}的联系方式与公司地址。</desc>
+  <rect width="1800" height="1080" fill="#0B0C10"/>
+  <g opacity="0.15" transform="translate(1170 52) scale(2.1)"><circle cx="12" cy="36" r="4" fill="#E6C687"/><path d="M38 43C76 11 142 1 204 15C228 21 249 31 266 44" fill="none" stroke="#C5C6C7" stroke-width="1.15" stroke-linecap="round"/><path d="M66 49C117 31 187 28 246 45" fill="none" stroke="#F5F5F7" stroke-width="0.62" stroke-linecap="round"/><circle cx="268" cy="45" r="1.35" fill="#F5F5F7"/></g>
+  <circle cx="150" cy="152" r="6" fill="#E6C687"/>
+  <text x="150" y="350" fill="#F5F5F7" font-family="'PingFang SC','Noto Sans SC','Source Han Sans SC','Microsoft YaHei',sans-serif" font-size="130" font-weight="300" letter-spacing="13">{{NAME}}</text>
+  <text x="156" y="425" fill="#C5C6C7" font-family="'PingFang SC','Noto Sans SC','Source Han Sans SC','Microsoft YaHei',sans-serif" font-size="42" font-weight="400" letter-spacing="5">
+    <tspan x="156" dy="0">{{TITLE_ZH}}</tspan>
+    <tspan x="156" dy="58" fill="#8F969E" font-family="Inter,'Helvetica Neue',Arial,sans-serif" font-size="28" font-weight="400" letter-spacing="2.2">{{TITLE_EN}}</tspan>
+  </text>
+  <g transform="translate(912 337)" font-family="Inter,'Helvetica Neue',Arial,sans-serif">
+    <text x="0" y="0" fill="#8F969E" font-size="22" font-weight="600" letter-spacing="4">EMAIL</text><text x="0" y="53" fill="#F5F5F7" font-size="38" font-weight="400" letter-spacing="0.2">{{EMAIL}}</text>
+    <text x="0" y="145" fill="#8F969E" font-size="22" font-weight="600" letter-spacing="4">MOBILE</text><text x="0" y="198" fill="#F5F5F7" font-size="38" font-weight="400" letter-spacing="1.2">{{PHONE}}</text>
+    <text x="0" y="290" fill="#8F969E" font-size="22" font-weight="600" letter-spacing="4">WEB</text><text x="0" y="343" fill="#F5F5F7" font-size="38" font-weight="400" letter-spacing="0.5">{{WEBSITE}}</text>
+  </g>
+  <path d="M150 740H1650" stroke="#1F2833" stroke-width="1"/>
+  <g font-family="'PingFang SC','Noto Sans SC','Source Han Sans SC','Microsoft YaHei',sans-serif"><text x="150" y="817" fill="#E2E4E7" font-size="36" font-weight="400" letter-spacing="1.3">{{COMPANY}}</text><text x="150" y="887" fill="#8F969E" font-size="32" font-weight="300" letter-spacing="0.4">{{ADDRESS}}</text></g>
+  <text x="1650" y="954" text-anchor="end" fill="#707780" font-family="Inter,'Helvetica Neue',Arial,sans-serif" font-size="20" letter-spacing="4.3">LESS AND COSMOS · HANGZHOU</text>
+</svg>

+ 17 - 0
create-less-and-cosmos-dark-business-card/assets/common-front.svg

@@ -0,0 +1,17 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="90mm" height="54mm" viewBox="0 0 1800 1080" role="img" aria-labelledby="title desc">
+  <title id="title">纯粹星辰名片正面</title><desc id="desc">品牌、企业理念和网站。</desc>
+  <rect width="1800" height="1080" fill="#0B0C10"/>
+  <g transform="translate(146 130)"><g transform="translate(0 12) scale(0.78)">
+    <circle cx="12" cy="36" r="6" fill="#E6C687"/>
+    <path d="M38 43C76 11 142 1 204 15C228 21 249 31 266 44" fill="none" stroke="#C5C6C7" stroke-width="3.5" stroke-linecap="round" opacity="0.82"/>
+    <path d="M66 49C117 31 187 28 246 45" fill="none" stroke="#F5F5F7" stroke-width="2" stroke-linecap="round" opacity="0.28"/>
+    <circle cx="268" cy="45" r="3" fill="#F5F5F7" opacity="0.8"/>
+  </g><text x="264" y="78" fill="#F5F5F7" font-family="'PingFang SC','Noto Sans SC','Source Han Sans SC','Microsoft YaHei',sans-serif" font-size="72" font-weight="300" letter-spacing="20">纯粹星辰</text>
+  <text x="269" y="132" fill="#C5C6C7" font-family="Inter,'Helvetica Neue',Arial,sans-serif" font-size="25" font-weight="300" letter-spacing="7">less and cosmos.</text></g>
+  <g transform="translate(900 386)"><circle cx="0" cy="11" r="6" fill="#E6C687"/>
+    <text x="38" y="0" fill="#F5F5F7" font-family="'PingFang SC','Noto Sans SC','Source Han Sans SC','Microsoft YaHei',sans-serif" font-size="58" font-weight="300" letter-spacing="2"><tspan x="38" dy="0">构建能够持续学习与进化的</tspan><tspan x="38" dy="92">企业级人工智能系统。</tspan></text>
+    <path d="M38 191H580" stroke="#1F2833" stroke-width="1"/><text x="38" y="235" fill="#8F969E" font-family="Inter,'Helvetica Neue',Arial,sans-serif" font-size="22" font-weight="400" letter-spacing="4.6">CONTINUOUSLY LEARNING AI</text>
+  </g>
+  <text x="146" y="935" fill="#C5C6C7" font-family="Inter,'Helvetica Neue',Arial,sans-serif" font-size="42" font-weight="400" letter-spacing="1.2">lessncosmos.com</text>
+  <path d="M146 986H1654" stroke="#1F2833" stroke-width="1"/><circle cx="1654" cy="986" r="4" fill="#E6C687"/>
+</svg>

+ 26 - 0
create-less-and-cosmos-dark-business-card/references/print-specifications.md

@@ -0,0 +1,26 @@
+# Dark-card print specifications
+
+## Digital master
+
+- Trim size: 90×54 mm, landscape
+- SVG viewBox: 1800×1080
+- Recommended PNG preview: 1800×1080 pixels
+- Working color space: RGB
+- Safe area: keep essential content at least 7.5 mm from the trimmed edge
+
+## Commercial printing
+
+Ask the printer to:
+
+- add 3 mm bleed on all sides, yielding a 96×60 mm production page;
+- convert the artwork to the printer's requested CMYK profile;
+- use the printer's recommended rich-black build for the full background;
+- embed or outline fonts if required;
+- proof the soft-white and cool-gray text for sufficient contrast;
+- optionally print the gold accents as a restrained spot gold.
+
+Recommended finish: 350 g/m² smooth uncoated stock with matte lamination. If using black stock instead of printing the black background, confirm white-ink or foil capability with the printer.
+
+## Final content check
+
+Verify character by character: name, bilingual title, email, phone and country code, company name, address, website, and slogan.

+ 111 - 0
create-less-and-cosmos-dark-business-card/scripts/generate_card.py

@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+"""Generate a dark Less and Cosmos double-sided employee business card."""
+
+from __future__ import annotations
+
+import argparse
+import html
+import json
+import re
+from pathlib import Path
+
+
+SKILL_DIR = Path(__file__).resolve().parent.parent
+ASSETS_DIR = SKILL_DIR / "assets"
+
+
+def clean(value: str, field: str) -> str:
+    value = " ".join(value.strip().split())
+    if not value:
+        raise ValueError(f"{field} must not be empty")
+    return value
+
+
+def normalize_phone(value: str) -> str:
+    value = clean(value, "phone")
+    value = re.sub(r"\s+", " ", value)
+    if value.startswith("+86"):
+        return value
+    if value.startswith("86 "):
+        return "+" + value
+    return "+86 " + value
+
+
+def split_title(value: str) -> tuple[str, str]:
+    parts = re.split(r"\s*[//]\s*", value, maxsplit=1)
+    if len(parts) == 1:
+        return parts[0], ""
+    return parts[0], parts[1]
+
+
+def render_template(template: str, values: dict[str, str]) -> str:
+    for key, value in values.items():
+        template = template.replace("{{" + key + "}}", html.escape(value))
+    unresolved = sorted(set(re.findall(r"\{\{([A-Z_]+)\}\}", template)))
+    if unresolved:
+        raise ValueError("Unresolved template fields: " + ", ".join(unresolved))
+    return template
+
+
+def build_preview(name: str) -> str:
+    safe_name = html.escape(name)
+    return f"""<!doctype html>
+<html lang=\"zh-CN\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">
+<title>{safe_name} · 纯粹星辰名片</title><style>
+*{{box-sizing:border-box}}body{{margin:0;background:#15171d;font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif}}
+main{{width:min(1480px,94vw);margin:0 auto;padding:5vw 0}}h1{{margin:0 0 28px;color:#c5c6c7;font-size:18px;font-weight:500;letter-spacing:2px}}
+.pair{{display:grid;grid-template-columns:1fr 1fr;gap:30px}}figure{{margin:0}}img{{display:block;width:100%;box-shadow:0 16px 46px rgba(25,28,31,.12)}}
+figcaption{{margin-top:12px;color:#8d9297;font-size:11px;letter-spacing:2px;text-transform:uppercase}}@media(max-width:800px){{.pair{{grid-template-columns:1fr}}}}
+</style></head><body><main><h1>{safe_name} · 纯粹星辰</h1><div class=\"pair\">
+<figure><img src=\"front.svg\" alt=\"名片正面\"><figcaption>Front</figcaption></figure>
+<figure><img src=\"back.svg\" alt=\"名片背面\"><figcaption>Back</figcaption></figure>
+</div></main></body></html>"""
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument("--name", required=True, help="Chinese name")
+    parser.add_argument("--title", required=True, help="Role, e.g. 联合创始人 / CMO")
+    parser.add_argument("--email", required=True)
+    parser.add_argument("--phone", required=True)
+    parser.add_argument("--output-dir", required=True, type=Path)
+    args = parser.parse_args()
+
+    name = clean(args.name, "name")
+    title = clean(args.title, "title")
+    title_zh, title_en = split_title(title)
+    email = clean(args.email, "email")
+    if "@" not in email or email.startswith("@") or email.endswith("@"):
+        raise ValueError("email does not look valid")
+    phone = normalize_phone(args.phone)
+
+    data = {
+        "name": name,
+        "title": title,
+        "email": email,
+        "phone": phone,
+        "company": "杭州纯粹星辰人工智能科技有限公司",
+        "address": "浙江省杭州市西湖区转塘街道云腾丝路中心5号楼商业208室",
+        "website": "lessncosmos.com",
+        "slogan": "构建能够持续学习与进化的企业级人工智能系统。",
+    }
+
+    args.output_dir.mkdir(parents=True, exist_ok=True)
+    (args.output_dir / "front.svg").write_text(
+        (ASSETS_DIR / "common-front.svg").read_text(encoding="utf-8"), encoding="utf-8"
+    )
+    template = (ASSETS_DIR / "back-template.svg").read_text(encoding="utf-8")
+    replacements = {key.upper(): value for key, value in data.items()}
+    replacements.update({"TITLE_ZH": title_zh, "TITLE_EN": title_en})
+    (args.output_dir / "back.svg").write_text(
+        render_template(template, replacements), encoding="utf-8"
+    )
+    (args.output_dir / "preview.html").write_text(build_preview(name), encoding="utf-8")
+    (args.output_dir / "card-data.json").write_text(
+        json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
+    )
+    print(f"Created business card in {args.output_dir.resolve()}")
+
+
+if __name__ == "__main__":
+    main()