# XLSX Design System **The single authoritative style reference. All table styles must be derived from this file — custom color values are prohibited.** --- ## 1. Design Philosophy ### Borderless-first Visual elements exist only where data exists; blank areas remain **completely clean** — no borders, no fills, no visual noise. - **Has content → has style**: Data regions use alternating row fills to distinguish rows - **No content → no trace**: Cells outside the data range receive no formatting whatsoever - **Minimal borders**: Only a single line at the header bottom and totals top; everything else relies on whitespace and alternating fills ### Typography First Establish hierarchy through **font size, font weight, and color value**, not lines and frames. ### Three-Color Discipline The entire table uses **at most 3 color roles**: primary, secondary, and accent. Everything else is black/white/gray warm tones. --- ## 2. Color System (Three-Color Rule) ### 2.1 Color Role Definitions Each table allows only 3 color roles + neutral base: | Role | Token | Responsibility | Area Ratio | |------|-------|----------------|------------| | **Primary** | `primary` | Header background, title text | ~5-8% | | **Secondary** | `secondary` | Section title background, totals row | ~2-3% | | **Accent** | `accent` | Status indicators (positive/negative/warning) | ≤2% | | **Neutral** | `neutral-*` | Body text, background, alternating rows | ~90% | ### 2.2 Default Palette ```python # === Primary (deep blue family) === PRIMARY = "1B2A4A" # Header background, title text color PRIMARY_LIGHT = "D6E4F0" # Light variant of primary → secondary (section titles, totals row) # === Secondary (derived from primary) === SECONDARY = PRIMARY_LIGHT # Secondary = light version of primary # === Accent (semantic, used on demand) === ACCENT_POSITIVE = "1B7D46" # Positive: growth, completed, on-target (deep green) ACCENT_NEGATIVE = "C0392B" # Negative: decline, overdue, off-target (deep red) ACCENT_WARNING = "D4820A" # Warning: approaching, needs attention (deep amber) # === Neutral palette (warm gray) === NEUTRAL_900 = "37352F" # Body text NEUTRAL_600 = "8C8A84" # Secondary text, annotations NEUTRAL_200 = "E9E9E8" # Divider lines, header bottom line NEUTRAL_100 = "F7F7F5" # Alternating row fill (odd rows) NEUTRAL_50 = "FAFAF9" # Very light base color (optional) NEUTRAL_0 = "FFFFFF" # White (even rows) ``` ### 2.3 Style Palette System (Style-First Palette Engine) Palettes are implemented via `templates/palettes.py`, **purely style-driven, not bound to domains**. Domains (finance/education/sales…) only affect data formats and header conventions, not colors. **12 style palettes:** All theme headers use PRIMARY background + white text. | # | Style | Keyword Triggers | PRIMARY | Positioning | |---|------|-----------|---------|------| | 01 | **professional** | 正式/商务/汇报/默认 | `1B2A4A` deep blue | Universal default | | 02 | **warm** | 温暖/活力/热情 | `B85C1E` warm orange | Vibrant and impactful | | 03 | **elegant** | 极简/简约 | `2C2C2C` charcoal | High-end minimalist | | 04 | **creative** | 文艺/莫兰迪/设计感 | `6C5B7B` purple-gray | Artistic distinction | | 05 | **muji** | 无印/呼吸感/素净 | `2C2C2C` warm black | MUJI pencil-on-paper | | 06 | **aesop** | 沙岩/大地色/护肤 | `3D3229` earth brown | Premium skincare packaging | | 07 | **kinfolk** | 奶油/刊物/杂志/拿铁 | `5C524C` cocoa | Independent magazine aesthetic | | 08 | **celine** | 黑白/时装/冷冽/mono | `000000` pure black | Fashion house coldness | | 09 | **bottega** | 墨绿/深绿/森林/贵气 | `2D4A3E` dark green | Italian luxury restraint | | 10 | **chanel** | 米金/香奈儿/奶茶/高级 | `1C1917` ink | Champagne gold elegance | | 11 | **bloomberg** | 终端/深蓝/金融终端/工业/包豪斯 | `0D1B2A` deep space | Financial data aesthetic | | 12 | **original_blue** | 原始/经典蓝/传统蓝 | `1B2A4A` classic blue | Original blue-black scheme | **Three-step matching logic (priority from high to low):** 1. **Explicit style keywords** → direct match ("make a warm table" → warm) 2. **Scene keyword inference** → indirect match ("sales monthly report" → warm, "student grades" → muji) 3. **No match** → professional (safe default, no guessing) **Usage:** ```python import base base.use_palette("help me make a warm sales monthly report") # → warm base.use_palette_explicit("warm") # → warm base.get_active_style() # → 'warm' ``` Each palette is a complete color set (PRIMARY + SECONDARY + ACCENT × 3 + NEUTRAL × 6 + HEADER_TEXT + CHART_COLORS + CF backgrounds). When `use_palette` is not called, the default behavior is identical to before (professional = deep blue). ### 2.4 Special Color Rules for Finance Scenarios Only when the scene is Finance, add the following text color encoding (IB industry convention, overrides default NEUTRAL_900): | Text Color | Hex | Meaning | |------------|-----|--------| | Blue `0000FF` | Manual input values (user-modifiable) | | Black `000000` | Formula/calculated values | | Green `008000` | Cross-sheet references | | Red `FF0000` | External file references | ### 2.5 Color Prohibitions - ❌ Do not introduce any new hues outside of `ACCENT_*` - ❌ Do not use color for decoration (primary color is sufficient for colored headers) - ❌ No gradient fills - ❌ Do not mix two different PRIMARY colors in the same table --- ## 3. Font System ### 3.1 Font Hierarchy | Token | Size | Weight | Color | Usage | |-------|------|--------|-------|-------| | `font-title` | 16pt | `HEADER_BOLD`* | `PRIMARY` | Table title (B2) | | `font-header` | 11pt | `HEADER_BOLD`* | `#FFFFFF` | Column headers (white text on primary background) | | `font-subheader` | 11pt/12pt | `HEADER_BOLD`* | `PRIMARY` | Section titles, totals row | | `font-body` | 11pt | Normal | `NEUTRAL_900` | Body data | | `font-caption` | 9pt | Normal | `NEUTRAL_600` | Annotations, sources, footnotes | | `font-kpi` | 22pt | `HEADER_BOLD`* | `PRIMARY` | KPI large numbers (analysis scenes only) | | `font-kpi-label` | 9pt | Normal | `NEUTRAL_600` | KPI labels | > \* `HEADER_BOLD` is determined at runtime by §3.3. Heavy-stroke fonts (SimHei/YaHei/PingFang, etc.) → False, thin-stroke fonts → True. ### 3.2 Font Selection (Cross-Platform Fallback Chain) openpyxl's `Font(name=...)` can only specify a single font name and does not support CSS-style fallback chains. Therefore, **runtime platform detection** is needed to select the first available font from the fallback sequence: ```python import platform, os from openpyxl.styles import Font def _resolve_font(candidates: list[str]) -> str: """Return the first font name likely available on this OS.""" system = platform.system() # Quick lookup: match common fonts by platform _platform_hints = { "Darwin": {"PingFang SC", "Hiragino Sans GB", ".AppleSystemUIFont"}, "Windows": {"Microsoft YaHei", "SimHei", "SimSun"}, "Linux": {"Noto Sans CJK SC", "WenQuanYi Micro Hei", "Source Han Sans SC"}, } available_hints = _platform_hints.get(system, set()) for name in candidates: if name in available_hints: return name # Fallback: return the first in sequence (Excel will fallback on its own when opened) return candidates[0] # === Font fallback sequences === # CJK body text (CJK Sans): prefer platform-native sans-serif fonts CJK_BODY_CHAIN = [ "PingFang SC", # macOS native, best rendering "Microsoft YaHei", # Windows native, screen-optimized "Noto Sans CJK SC", # Linux / Android universal "Hiragino Sans GB", # macOS alternative "Source Han Sans SC", # Adobe Source Han Sans, cross-platform "SimHei", # Classic fallback ] # Latin/numbers: serif (for formal reports) LATIN_BODY_CHAIN = [ "Times New Roman", # Available on virtually all platforms "Georgia", "serif", ] # Runtime resolution FONT_CJK = _resolve_font(CJK_BODY_CHAIN) FONT_LATIN = _resolve_font(LATIN_BODY_CHAIN) # openpyxl can only set one name; use the CJK font for Chinese tables (it also covers ASCII characters) FONT_NAME = FONT_CJK ``` **Rules**: - Use `FONT_NAME` uniformly across the entire table — do not mix fonts - All `Font(name=...)` in code must use the `FONT_NAME` variable — **hardcoding font names is prohibited** - If the user explicitly specifies a font, respect the user's choice ### 3.3 Header Bold Strategy Not all fonts are suitable for bold. Heavy-stroke fonts (like SimHei, YaHei) become blurry when bolded — hierarchy should be established through **font size differences or color contrast**, not font weight: ```python # Determine whether the font is suitable for bold based on font name _HEAVY_FONTS = { "SimHei", "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", "Source Han Sans SC", "Hiragino Sans GB", "WenQuanYi Micro Hei", } HEADER_BOLD = FONT_NAME not in _HEAVY_FONTS # → Heavy fonts (SimHei/YaHei/PingFang, etc.): headers not bolded, rely on background color + white text # → Thin fonts (SimSun/Times New Roman, etc.): headers bolded ``` **Hierarchy alternatives when `HEADER_BOLD = False`**: - Headers: no bold, rely on **primary background + white text** for distinction - Titles: no bold, use **larger font size (16pt vs 11pt)** for hierarchy - Totals row: no bold, use **secondary background + primary text** for distinction - Section titles: no bold, use **primary text + slightly larger size (12pt)** for distinction ### 3.4 Alignment Rules | Data Type | Horizontal Alignment | Notes | |-----------|---------------------|-------| | Numbers/amounts/percentages | Right-aligned | Ensures decimal point alignment | | Dates | Center-aligned | | | Text | Left-aligned | | | Headers | Center-aligned | | | Titles | Left-aligned | ❌ Not centered | --- ## 4. Layout System ### 4.1 Starting Position and Margins ``` A B C D E ... 1 [blank] [blank] [blank] [blank] [blank] ← Top margin 2 [blank] Title ───────────────────────── ← Title row (starts at B2, merged to data width) 3 [blank] [blank] [blank] [blank] [blank] ← Spacing row (optional: subtitle/date) 4 [blank] Header1 Header2 Header3 Header4 ← Header row 5 [blank] Data Data Data Data ← Data area start ``` - **Canvas Origin**: `B2` (left margin Column A + top margin Row 1) - **Column A width**: 3 (pure whitespace for visual breathing room) - **Row 1 height**: 15pt (top margin) ### 4.2 Row Height Standards | Row Type | Height | Notes | |----------|--------|-------| | Title row (Row 2) | 32pt | 16pt font + top/bottom breathing room | | Spacing row (Row 3) | 8pt | Gap between title and header | | Header row (Row 4) | 28pt | 11pt font + wrap_text space | | Data rows | 22pt | 11pt font + comfortable reading | | Totals row | 26pt | Slightly taller than data rows for emphasis | ### 4.3 Column Width Guidelines ```python COLUMN_WIDTHS = { 'margin': 3, # Column A whitespace 'id_short': 8, # Serial number, ID 'name_cn': 16, # Chinese name (2-4 chars) 'name_en': 22, # English name 'description': 32, # Long text 'number': 14, # Amounts, quantities 'percentage': 12, # Percentages 'date': 14, # Dates 'status': 12, # Status labels } # CJK character ≈ 2.5 units, Latin ≈ 1.2 units # Minimum 8, maximum 40 ``` ### 4.4 Auto-Fit Column Widths (Recommended) After populating data, call `auto_fit_columns(ws)` from `templates/base.py` to automatically size columns based on **data content** (not headers). Headers that exceed the computed width get `wrap_text=True` instead of stretching the column. ```python from templates.base import auto_fit_columns # After all data is written: auto_fit_columns(ws, min_width=8, max_width=28, header_row=4, data_start_row=5) ``` **Rules**: - Column width is determined by the widest **data cell**, not the header - CJK characters are counted as 1.7x width (via `unicodedata.east_asian_width`) - Headers wider than the column automatically get `wrap_text=True` - This prevents the common problem of headers being wider than data content --- ## 5. Border System (Borderless-first) ### 5.1 Allowed Borders | Position | Style | Color | Purpose | |------|------|------|------| | Header bottom | `thin` | `NEUTRAL_200` | Separate header from data | | Totals top | `medium` | `NEUTRAL_200` | Mark summary row | ### 5.2 Prohibited Borders - ❌ Full grid (all-sides thin border) - ❌ Colored borders - ❌ Double-line borders - ❌ Thick borders (medium/thick) for decoration ### 5.3 Row Separation Alternative Use **alternating row fills** instead of grid lines: - Even rows: `NEUTRAL_0` (white) - Odd rows: `NEUTRAL_100` (warm white `#F7F7F5`) ### 5.4 Finance Scene Exception Finance scene retains section dividers (`PRIMARY` color), following IB industry convention. --- ## 6. Title Row Design ### 6.1 Title Style ```python # Title: plain text, no background fill title_font = Font(name=FONT_NAME, size=16, bold=HEADER_BOLD, color=PRIMARY) title_align = Alignment(horizontal='left', vertical='center') # Position: B2, merged to the last data column ws.merge_cells(start_row=2, start_column=2, end_row=2, end_column=last_col) ws['B2'].font = title_font ws['B2'].alignment = title_align ws.row_dimensions[2].height = 32 ``` ### 6.2 Header Style ```python # Header: primary color background + white text header_fill = PatternFill('solid', fgColor=PRIMARY) header_font = Font(name=FONT_NAME, size=11, bold=HEADER_BOLD, color="FFFFFF") header_align = Alignment(horizontal='center', vertical='center', wrap_text=True) header_border = Border(bottom=Side(style='thin', color=NEUTRAL_200)) for cell in header_row: cell.fill = header_fill cell.font = header_font cell.alignment = header_align cell.border = header_border ws.row_dimensions[header_row_num].height = 28 ``` ### 6.3 Totals Row ```python total_fill = PatternFill('solid', fgColor=SECONDARY) # PRIMARY_LIGHT total_font = Font(name=FONT_NAME, size=11, bold=HEADER_BOLD, color=PRIMARY) total_border = Border(top=Side(style='medium', color=NEUTRAL_200)) for cell in total_row: cell.fill = total_fill cell.font = total_font cell.border = total_border ``` --- ## 7. Data Area Styles ### 7.1 Alternating Row Fill ```python for i, row in enumerate(ws.iter_rows(min_row=data_start, max_row=data_end)): fill_color = NEUTRAL_0 if i % 2 == 0 else NEUTRAL_100 for cell in row: cell.fill = PatternFill('solid', fgColor=fill_color) cell.font = Font(name=FONT_NAME, size=11, color=NEUTRAL_900) # ❌ No borders ``` ### 7.2 Empty Data Area Cells outside the data range receive **no formatting** — no fill, no borders, no font settings. Keep Excel defaults. ### 7.3 Grid Lines ```python ws.sheet_view.showGridLines = False # Disable Excel default grid lines ``` --- ## 8. Conditional Formatting ### 8.1 When to Use | ✅ Use | ❌ Don't Use | |---------|----------| | Data has comparison/ranking semantics (scores, KPIs, growth rates) | Simple entry forms, reference tables | | Financial data with positive/negative values (profit/loss, increase/decrease) | Data rows ≤5 | | User explicitly requests | User requests minimalist style | ### 8.2 Color Rules Conditional formatting **uses only accent colors**: ```python # Positive → green background + green text POSITIVE_FILL = PatternFill(bgColor='E8F5E9') POSITIVE_FONT = Font(color=ACCENT_POSITIVE) # "1B7D46" # Negative → red background + red text NEGATIVE_FILL = PatternFill(bgColor='FDEDEC') NEGATIVE_FONT = Font(color=ACCENT_NEGATIVE) # "C0392B" # Warning → amber background + amber text WARNING_FILL = PatternFill(bgColor='FEF9E7') WARNING_FONT = Font(color=ACCENT_WARNING) # "D4820A" ``` ### 8.3 Color Scale ```python from openpyxl.formatting.rule import ColorScaleRule # Red → Yellow → Green (low → mid → high) ws.conditional_formatting.add('B5:B100', ColorScaleRule( start_type='min', start_color='F8696B', mid_type='percentile', mid_value=50, mid_color='FFEB84', end_type='max', end_color='63BE7B')) ``` ### 8.4 Data Bar ```python from openpyxl.formatting.rule import DataBarRule ws.conditional_formatting.add('D5:D100', DataBarRule(start_type='min', end_type='max', color=PRIMARY, showValue=True)) # Data Bar color uses primary, maintaining color discipline ``` --- ## 9. Chart Colors Chart colors are **derived from the design system**, not maintained separately: ```python CHART_COLORS = [ PRIMARY, # 1st data series = primary ACCENT_POSITIVE, # 2nd series ACCENT_WARNING, # 3rd series ACCENT_NEGATIVE, # 4th series NEUTRAL_600, # 5th series (gray) ] ``` - Single series chart → use only `PRIMARY` - Two series → `PRIMARY` + `ACCENT_POSITIVE` - Multiple series → pick colors in order from the table above - **Never exceed 5 colors** --- ## 10. Number Formats ### 10.1 General Formats ```python FORMATS = { 'integer': '#,##0', 'decimal_1': '#,##0.0', 'decimal_2': '#,##0.00', 'percentage': '0.0%', 'currency_cny': '¥#,##0.00', 'currency_usd': '$#,##0.00', 'date': 'YYYY-MM-DD', } ``` ### 10.2 Financial Formats → Full financial number format definitions are in **`scenes/finance.md §Number Formatting`**, not repeated here. Brief rules: zero values `"-"`, negatives in parentheses `($123)`, headers indicate units `"Revenue ($mm)"`. --- ## 11. Code Templates All design tokens, font resolution, and style factory functions have been extracted into **`templates/base.py`**. > `templates/base.py` is the single code-level implementation. This file (design.md) is the design specification document; `base.py` is the corresponding executable code. ### Usage ```python # In all scene/engine code, import base.py directly import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'templates')) from base import * # Then use directly: from openpyxl import Workbook wb = Workbook() ws = wb.active ws.title = "Sheet1" headers = ["Column1", "Column2", "Column3"] last_col = len(headers) + 1 # Starting from B=2 # One call to set up sheet basics + title setup_sheet(ws, title="Table Title", last_col=last_col) # Write headers for col_idx, header in enumerate(headers, start=2): ws.cell(row=4, column=col_idx, value=header) style_header_row(ws, row_num=4, col_start=2, col_end=last_col) # Write data rows for i, row_data in enumerate(data): row_num = 5 + i for col_idx, value in enumerate(row_data, start=2): ws.cell(row=row_num, column=col_idx, value=value) style_data_row(ws, row_num=row_num, col_start=2, col_end=last_col, row_index=i) # Write totals row total_row_num = 5 + len(data) style_total_row(ws, row_num=total_row_num, col_start=2, col_end=last_col) wb.properties.creator = "Z.ai" wb.save("output.xlsx") ``` ### Complete API provided by base.py | Category | Exports | |------|------| | **Constants** | `FONT_NAME`, `HEADER_BOLD`, `PRIMARY`, `PRIMARY_LIGHT`, `SECONDARY`, `ACCENT_*`, `NEUTRAL_*`, `CHART_COLORS`, `COLUMN_WIDTHS`, `FORMATS`, `ROW_HEIGHTS` | | **Conditional Formatting** | `CF_POSITIVE_FILL/FONT`, `CF_NEGATIVE_FILL/FONT`, `CF_WARNING_FILL/FONT` | | **Font Factories** | `font_title()`, `font_header()`, `font_subheader()`, `font_body()`, `font_caption()`, `font_kpi()`, `font_kpi_label()` | | **Fill Factories** | `fill_header()`, `fill_total()`, `fill_data_row(row_index)` | | **Border Factories** | `border_header()`, `border_total()` | | **Alignment Factories** | `align_title()`, `align_header()`, `align_number()`, `align_text()`, `align_date()` | | **Sheet Helpers** | `setup_sheet(ws, title, last_col)`, `style_header_row(...)`, `style_data_row(...)`, `style_total_row(...)` | | **Utility Functions** | `normalize_cell_value(value)`, `copy_style(source, target)` | --- ## 12. Design Checklist Verify each item before delivering a table: - [ ] Colors ≤ 3 hues (primary + accents, excluding neutrals) - [ ] No formatting outside data area - [ ] No full-grid borders (only header bottom line + totals top line) - [ ] Alternating row fill applied - [ ] Title has no background fill, left-aligned - [ ] Numbers right-aligned, text left-aligned - [ ] Grid lines disabled (`showGridLines = False`) - [ ] Starting position is B2, column A is margin - [ ] Body text color is `NEUTRAL_900` (`#37352F`), not pure black - [ ] Chart colors come from Design Tokens, no new colors introduced