## Geometric Decoration System — Pure docx-js Decorations

### Design Philosophy

Uses only docx-js native capabilities for visual decoration — no external tools (like Playwright screenshots). Suitable for covers, chapter separators, page background enhancement.

**When to fall back to Playwright?**  
Only when gradients, complex illustrations, or brand visuals are needed that pure OOXML cannot express. Default: prefer native solutions below.

### Decoration Element Library

#### 1. Color Strip — Table Simulation

Single-row single-column borderless table + background color to create horizontal color strips.

```js
function colorStrip(color, height = 80) {
  return new Table({
    width: { size: 100, type: WidthType.PERCENTAGE },
    borders: { top: NB, bottom: NB, left: NB, right: NB,
               insideHorizontal: NB, insideVertical: NB },
    rows: [new TableRow({
      height: { value: height, rule: "exact" },
      children: [new TableCell({
        shading: { type: ShadingType.CLEAR, fill: color.replace("#", "") },
        borders: { top: NB, bottom: NB, left: NB, right: NB },
        children: [new Paragraph({ children: [] })],
      })],
    })],
  });
}

// ══════════════════════════════════════════════════════════════
// R6 — Editorial Warm (minimal, warm white bg, no decorations)
// ══════════════════════════════════════════════════════════════
// Suitable for: lesson plans (non-STEM), cultural/creative, newsletters,
//   event planning, internal reports, light-weight documents
// NOT for: formal business, consulting, finance, government, academic
// Title constraint: single line only (≤20 chars). Longer titles → route to R1.
//
// Structure: 2-row wrapper table (no border, warm bg shading)
//   Row 1 (content): category → title → subtitle → fields
//   Row 2 (footer):  left English title + right label
// All spacing via paragraph indent (WPS safe, no cell margins).

function buildCoverR6(config) {
  const P = config.palette;
  const PAD_L = 1300, PAD_R = 1100;
  const ind = { left: PAD_L, right: PAD_R };
  const FOOTER_H = 900;
  const CONTENT_H = 16838 - FOOTER_H;
  const shading = { fill: P.bg || "F7F7F5", type: ShadingType.CLEAR };

  // ⚠️ R6 uses a simplified title layout: prefer single line, shrink font to fit
  const availW = 11906 - PAD_L - PAD_R;
  const { titlePt, titleLines } = calcTitleLayoutR6(config.title, availW, 36, 22);
  const titleSize = titlePt * 2;
  const lineH = Math.ceil(titlePt * 23 * 1.3);

  // Dynamic top spacing
  const titleH = titleLines.length * (titleSize * 10 + 200);
  const categoryH = 22 * 10 + 900;
  const subtitleH = config.subtitle ? (28 * 10 + 1200) : 0;
  const fieldsH = (config.metaLines || []).length * (24 * 10 + 100);
  const contentH = categoryH + titleH + subtitleH + fieldsH;
  const remaining = Math.max(CONTENT_H - 1200 - contentH, 400);
  const topSpacing = Math.floor(remaining * 0.55);

  const children = [];

  // 1. Top spacer (dynamic)
  children.push(new Paragraph({ indent: ind, spacing: { before: topSpacing } }));

  // 2. Category label (small, wide letter-spacing)
  if (config.englishLabel) {
    children.push(new Paragraph({
      indent: ind, spacing: { after: 900 },
      children: [new TextRun({
        text: config.englishLabel, size: 22,
        color: P.cover.metaColor || "9A9A9A",
        font: { ascii: "Calibri", eastAsia: "Microsoft YaHei" },
        characterSpacing: 60,
      })],
    }));
  }

  // 3. Title (single line preferred, dynamic font size)
  for (let i = 0; i < titleLines.length; i++) {
    children.push(new Paragraph({
      indent: ind,
      spacing: { after: i < titleLines.length - 1 ? 60 : 300, line: lineH, lineRule: "atLeast" },
      children: [new TextRun({
        text: titleLines[i], size: titleSize,
        color: P.cover.titleColor || "2C2C2C",
        font: { ascii: "Calibri", eastAsia: "Microsoft YaHei" },
        characterSpacing: 30,
      })],
    }));
  }

  // 4. Subtitle
  if (config.subtitle) {
    children.push(new Paragraph({
      indent: ind, spacing: { after: 1200 },
      children: [new TextRun({
        text: config.subtitle, size: 28,
        color: P.cover.subtitleColor || "6B6B6B",
        font: { ascii: "Calibri", eastAsia: "Microsoft YaHei" },
        characterSpacing: 15,
      })],
    }));
  }

  // 5. Meta fields (tab-aligned label + value)
  for (const line of (config.metaLines || [])) {
    // Expect "label：value" format or plain text
    const sep = line.indexOf("：") !== -1 ? "：" : (line.indexOf(":") !== -1 ? ":" : null);
    const label = sep ? line.split(sep)[0].trim() : line;
    const value = sep ? line.split(sep).slice(1).join(sep).trim() : "";
    children.push(new Paragraph({
      indent: ind, spacing: { after: 100 },
      tabStops: [{ type: TabStopType.LEFT, position: PAD_L + 1600 }],
      children: [
        new TextRun({ text: label, size: 22, color: P.cover.metaColor || "9A9A9A",
          font: { ascii: "Calibri", eastAsia: "Microsoft YaHei" }, characterSpacing: 20 }),
        ...(value ? [
          new TextRun({ text: "\t" }),
          new TextRun({ text: value, size: 24, color: P.cover.subtitleColor || "6B6B6B",
            font: { ascii: "Calibri", eastAsia: "Microsoft YaHei" }, characterSpacing: 8 }),
        ] : []),
      ],
    }));
  }

  // 6. Footer (2-column borderless table)
  const footerLeft = config.footerLeft || "";
  const footerRight = config.footerRight || "";
  // Adaptive font size for long English footer text
  const flSize = footerLeft.length > 60 ? 14 : (footerLeft.length > 40 ? 16 : 18);
  const flSpacing = footerLeft.length > 60 ? 5 : (footerLeft.length > 40 ? 10 : 20);

  const footerTable = new Table({
    width: { size: 100, type: WidthType.PERCENTAGE },
    layout: TableLayoutType.FIXED, borders: allNoBorders,
    rows: [new TableRow({
      children: [
        new TableCell({
          width: { size: 70, type: WidthType.PERCENTAGE }, borders: noBorders, shading,
          children: [new Paragraph({
            indent: { left: PAD_L },
            children: [new TextRun({ text: footerLeft, size: flSize,
              color: P.cover.footerColor || "9A9A9A",
              font: { ascii: "Calibri" }, characterSpacing: flSpacing })],
          })],
        }),
        new TableCell({
          width: { size: 30, type: WidthType.PERCENTAGE }, borders: noBorders, shading,
          children: [new Paragraph({
            alignment: AlignmentType.RIGHT, indent: { right: PAD_R },
            children: [new TextRun({ text: footerRight, size: 18,
              color: P.cover.footerColor || "9A9A9A",
              font: { ascii: "Calibri" }, characterSpacing: 20 })],
          })],
        }),
      ],
    })],
  });

  // 7. 2-row wrapper (content + footer)
  return [new Table({
    width: { size: 100, type: WidthType.PERCENTAGE },
    layout: TableLayoutType.FIXED, borders: allNoBorders,
    rows: [
      new TableRow({
        height: { value: CONTENT_H, rule: "exact" },
        children: [new TableCell({
          shading, borders: noBorders,
          margins: { top: 0, bottom: 0, left: 0, right: 0 },
          verticalAlign: VerticalAlign.TOP,
          children,
        })],
      }),
      new TableRow({
        height: { value: FOOTER_H, rule: "exact" },
        children: [new TableCell({
          shading, borders: noBorders,
          margins: { top: 0, bottom: 0, left: 0, right: 0 },
          verticalAlign: VerticalAlign.CENTER,
          children: [footerTable],
        })],
      }),
    ],
  })];
}

// R6 title layout: prefer FEWER lines over larger font size (single line best)
function calcTitleLayoutR6(title, availableWidthTw, preferredPt, minPt) {
  const step = 2;
  // Try to fit in 1 line (shrink font if needed)
  for (let pt = preferredPt; pt >= minPt; pt -= step) {
    const charWidthTw = pt * 23 * 0.5; // CJK ~50% em width
    const charsPerLine = Math.floor(availableWidthTw / charWidthTw);
    if (title.length <= charsPerLine) return { titlePt: pt, titleLines: [title] };
  }
  // Can't fit in 1 line, try 2 lines at largest possible font
  for (let pt = preferredPt; pt >= minPt; pt -= step) {
    const charWidthTw = pt * 23 * 0.5;
    const charsPerLine = Math.floor(availableWidthTw / charWidthTw);
    const lines = splitTitleLines(title, charsPerLine);
    if (lines.length <= 2) return { titlePt: pt, titleLines: lines };
  }
  // Fallback: minPt, up to 3 lines
  const charWidthTw = minPt * 23 * 0.5;
  const charsPerLine = Math.floor(availableWidthTw / charWidthTw);
  return { titlePt: minPt, titleLines: splitTitleLines(title, charsPerLine) };
}

// Usage: cover top decoration
// children: [colorStrip(P.accent, 120), ...]
```

#### 2. Side Ribbon

Uses left border to create vertical ribbon effect.

```js
function sideRibbon(content, color, width = 14) {
  return new Paragraph({
    border: {
      left: { style: BorderStyle.SINGLE, size: width, color: color.replace("#", ""), space: 12 },
    },
    indent: { left: 240 },
    spacing: { before: 100, after: 100 },
    children: content,
  });
}

// Usage: emphasis quotes, chapter tips
// sideRibbon([new TextRun({ text: "Key Insight", bold: true })], P.accent)
```

#### 3. Border Compositions

```js
// Top thick line + bottom thin line — title area frame
function frameTitle(titleRuns) {
  return new Paragraph({
    border: {
      top: { style: BorderStyle.SINGLE, size: 18, color: c(P.accent) },
      bottom: { style: BorderStyle.SINGLE, size: 4, color: c(P.accent) },
    },
    spacing: { before: 400, after: 200 },
    alignment: AlignmentType.CENTER,
    children: titleRuns,
  });
}

// L-shape border — left + bottom
function lShapeBorder(content) {
  return new Paragraph({
    border: {
      left: { style: BorderStyle.SINGLE, size: 12, color: c(P.accent), space: 10 },
      bottom: { style: BorderStyle.SINGLE, size: 12, color: c(P.accent) },
    },
    indent: { left: 300 },
    spacing: { before: 200, after: 300 },
    children: content,
  });
}

// Double-line frame — top and bottom double lines
function doubleLine(content) {
  return new Paragraph({
    border: {
      top: { style: BorderStyle.DOUBLE, size: 6, color: c(P.accent) },
      bottom: { style: BorderStyle.DOUBLE, size: 6, color: c(P.accent) },
    },
    spacing: { before: 200, after: 200 },
    alignment: AlignmentType.CENTER,
    children: content,
  });
}
```

#### 4. Gradient Simulation

Multiple narrow color strips to simulate gradient effect.

```js
function gradientStrip(startColor, endColor, steps = 5, totalHeight = 200) {
  const rows = [];
  const h = Math.floor(totalHeight / steps);
  for (let i = 0; i < steps; i++) {
    const ratio = i / (steps - 1);
    const blended = blendColors(startColor, endColor, ratio);
    rows.push(new TableRow({
      height: { value: h, rule: "exact" },
      children: [new TableCell({
        shading: { type: ShadingType.CLEAR, fill: blended },
        borders: { top: NB, bottom: NB, left: NB, right: NB },
        children: [new Paragraph({ children: [] })],
      })],
    }));
  }
  return new Table({
    width: { size: 100, type: WidthType.PERCENTAGE },
    borders: { top: NB, bottom: NB, left: NB, right: NB,
               insideHorizontal: NB, insideVertical: NB },
    rows,
  });
}

function blendColors(hex1, hex2, ratio) {
  const r1 = parseInt(hex1.slice(1, 3), 16), g1 = parseInt(hex1.slice(3, 5), 16), b1 = parseInt(hex1.slice(5, 7), 16);
  const r2 = parseInt(hex2.slice(1, 3), 16), g2 = parseInt(hex2.slice(3, 5), 16), b2 = parseInt(hex2.slice(5, 7), 16);
  const r = Math.round(r1 + (r2 - r1) * ratio), g = Math.round(g1 + (g2 - g1) * ratio), b = Math.round(b1 + (b2 - b1) * ratio);
  return `${r.toString(16).padStart(2,"0")}${g.toString(16).padStart(2,"0")}${b.toString(16).padStart(2,"0")}`;
}
```

#### 5. Symbol Ornaments

```js
// Section divider line — for chapter separation
function ornamentDivider(symbol = "◆", count = 3) {
  const ornament = Array(count).fill(symbol).join("   ");
  return new Paragraph({
    alignment: AlignmentType.CENTER,
    spacing: { before: 400, after: 400 },
    children: [new TextRun({ text: ornament, size: 20, color: c(P.accent) })],
  });
}

// Common decoration symbols
// ◆ ◇ ● ○ ★ ☆ ■ □ ▲ △ ─ ━ ═ ║ ╔ ╗ ╚ ╝
// Ornamental: ❧ ❦ ✦ ✧ ✿ ❀ ❁ ※
```

#### 6. Info Card — Table Implementation

```js
function infoCard(title, items, accentColor) {
  const ac = accentColor.replace("#", "");
  const headerRow = new TableRow({
    children: [new TableCell({
      columnSpan: 2,
      shading: { type: ShadingType.CLEAR, fill: ac },
      margins: { top: 80, bottom: 80, left: 160, right: 160 },
      borders: { top: NB, bottom: NB, left: NB, right: NB },
      children: [new Paragraph({
        children: [new TextRun({ text: title, bold: true, size: 24, color: "FFFFFF" })],
      })],
    })],
  });

  const dataRows = items.map(([label, value]) => new TableRow({
    children: [
      new TableCell({
        width: { size: 30, type: WidthType.PERCENTAGE },
        margins: { top: 60, bottom: 60, left: 160, right: 80 },
        shading: { type: ShadingType.CLEAR, fill: "F8F9FA" },
        borders: { bottom: { style: BorderStyle.SINGLE, size: 1, color: "E0E0E0" },
                   top: NB, left: NB, right: NB },
        children: [new Paragraph({ children: [new TextRun({ text: label, size: 21, color: "666666" })] })],
      }),
      new TableCell({
        margins: { top: 60, bottom: 60, left: 80, right: 160 },
        borders: { bottom: { style: BorderStyle.SINGLE, size: 1, color: "E0E0E0" },
                   top: NB, left: NB, right: NB },
        children: [new Paragraph({ children: [new TextRun({ text: value, size: 21 })] })],
      }),
    ],
  }));

  return new Table({
    width: { size: 80, type: WidthType.PERCENTAGE },
    alignment: AlignmentType.CENTER,
    borders: { top: NB, bottom: NB, left: NB, right: NB,
               insideHorizontal: NB, insideVertical: NB },
    rows: [headerRow, ...dataRows],
  });
}
```


// R7 — Swiss Tech Minimalist (slate grey bg, Klein blue accent, asymmetric layout)
// Suitable for: cultural/creative research, trend reports, brand strategy, design deliverables
// Palette: ST-1 (exclusive)
// Layout: left-aligned title (upper 20%), right-shifted subtitle with top rule,
//         right-aligned info block with accent right border, Swiss cross anchor
// Key features: ■ square accent dot, open-frame tables, large whitespace
//
// ⚠️ MANDATORY: All cover non-negotiables apply (margin=0, 16838 exact, allNoBorders)
// ⚠️ Title uses calcTitleLayout() with maxPt=36 (not 40 — R7 uses lighter visual weight)

function buildCoverR7(config) {
  const P = palettes[config.palette || "ST-1"];
  const C = P.cover;
  const padL = 600;

  // Title layout — R7 uses 36pt max (lighter than R1-R4's 40pt)
  const availW = 11906 - padL - 600;
  const { titlePt, titleLines } = calcTitleLayout(config.title, availW, 36, 24);
  const titleSize = titlePt * 2;
  const lineH = Math.ceil(titlePt * 23);

  // Dynamic spacing based on title lines
  const topSpacer = titleLines.length <= 2 ? 1200 : 800;
  const subtitleSpacer = titleLines.length <= 2 ? 1400 : 800;
  const infoSpacer = titleLines.length <= 2 ? 2200 : 1200;

  const children = [];

  // 1. Swiss cross anchor — top-left decorative element
  children.push(new Paragraph({
    spacing: { before: 600 },
    indent: { left: padL },
    children: [new TextRun({
      text: "\uFF0B",  // ＋ fullwidth plus
      size: 40, bold: true, color: C.titleColor,
      font: { ascii: "Arial", eastAsia: "SimHei" },
    })],
  }));

  // 2. Top spacer
  children.push(new Paragraph({ spacing: { before: topSpacer } }));

  // 3. Title lines — left-aligned, last line has accent ■
  titleLines.forEach((line, i) => {
    const isLast = i === titleLines.length - 1;
    const runs = [new TextRun({
      text: line, size: titleSize, color: C.titleColor,
      font: { ascii: "Arial", eastAsia: "Noto Sans SC" },
    })];
    if (isLast) {
      runs.push(new TextRun({
        text: " \u25A0",  // ■ black square
        size: 24, color: P.accent,
        font: { ascii: "Arial" },
      }));
    }
    children.push(new Paragraph({
      indent: { left: padL },
      spacing: { after: isLast ? 200 : 80, line: lineH, lineRule: "atLeast" },
      children: runs,
    }));
  });

  // 4. Subtitle spacer
  children.push(new Paragraph({ spacing: { before: subtitleSpacer } }));

  // 5. Subtitle — right-shifted, top border rule, wide character spacing
  if (config.subtitle) {
    children.push(new Paragraph({
      indent: { left: 3800, right: 600 },
      border: { top: { style: BorderStyle.SINGLE, size: 2, color: C.titleColor, space: 14 } },
      spacing: { after: 200 },
      children: [new TextRun({
        text: config.subtitle, size: 26, color: C.subtitleColor,
        font: { ascii: "Arial", eastAsia: "Noto Sans SC" },
        characterSpacing: 40,
      })],
    }));
  }

  // 6. Decorative horizontal line
  children.push(new Paragraph({
    spacing: { before: 600 },
    border: { bottom: { style: BorderStyle.SINGLE, size: 1, color: "C8D0DC", space: 0 } },
  }));

  // 7. Info spacer
  children.push(new Paragraph({ spacing: { before: infoSpacer } }));

  // 8. Info footer — right-aligned, 4 label+value pairs, accent right border
  // Standard fields: ORGANIZATION, RESPONSIBILITY, REPORT NUMBER, DATE & EDITION
  const metaEntries = config.metaEntries || [
    { label: "ORGANIZATION", value: config.organization || "" },
    { label: "RESPONSIBILITY", value: config.responsibility || "" },
    { label: "REPORT NUMBER", value: config.reportNumber || "" },
    { label: "DATE & EDITION", value: config.dateEdition || "" },
  ];

  for (const entry of metaEntries) {
    // Label — 7pt uppercase English
    children.push(new Paragraph({
      alignment: AlignmentType.RIGHT,
      indent: { right: 800 },
      border: { right: { style: BorderStyle.SINGLE, size: 12, color: P.accent, space: 16 } },
      spacing: { after: 20 },
      children: [new TextRun({
        text: entry.label, size: 14, color: C.metaColor,
        font: { ascii: "Arial" },
        characterSpacing: 20,
      })],
    }));
    // Value — 11pt bold
    children.push(new Paragraph({
      alignment: AlignmentType.RIGHT,
      indent: { right: 800 },
      border: { right: { style: BorderStyle.SINGLE, size: 12, color: P.accent, space: 16 } },
      spacing: { after: 280 },
      children: [new TextRun({
        text: entry.value, size: 22, bold: true, color: C.titleColor,
        font: { ascii: "Arial", eastAsia: "Noto Sans SC" },
      })],
    }));
  }

  // Wrap in 16838 exact wrapper table
  return [new Table({
    width: { size: 100, type: WidthType.PERCENTAGE },
    layout: TableLayoutType.FIXED,
    borders: allNoBorders,
    rows: [new TableRow({
      height: { value: 16838, rule: "exact" },
      children: [new TableCell({
        shading: { type: ShadingType.CLEAR, fill: P.bg },
        borders: noBorders,
        verticalAlign: VerticalAlign.TOP,
        children,
      })],
    })],
  })];
}

### Decoration Usage Scenarios

| Scenario | Recommended Decoration | Combination |
|------|----------|----------|
| Report cover | Color strip + L-frame border | Top strip → Title area → L-frame author info |
| Proposal cover | Gradient simulation + double-line frame | Gradient bg → Double-line title |
| Chapter separator | Symbol ornament + side ribbon | Symbol divider → New chapter title with ribbon |
| Summary card | Info card | Standalone card displaying key metrics |
| Academic cover | Color strip + info table | Top strip → School name → Title → Info table |

---

