@seanyao/roll 0.5.0 → 2.602.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +717 -0
- package/LICENSE +21 -0
- package/README.md +65 -165
- package/bin/dream-test-quality-scan +110 -0
- package/bin/roll +14897 -815
- package/conventions/config.yaml +17 -1
- package/conventions/global/AGENTS.md +146 -100
- package/conventions/global/CLAUDE.md +1 -21
- package/conventions/global/GEMINI.md +8 -22
- package/conventions/global/project_rules.md +9 -0
- package/conventions/templates/backend-service/AGENTS.md +30 -81
- package/conventions/templates/backend-service/GEMINI.md +3 -3
- package/conventions/templates/backend-service/project_rules.md +16 -0
- package/conventions/templates/cli/AGENTS.md +31 -58
- package/conventions/templates/cli/CLAUDE.md +3 -5
- package/conventions/templates/cli/GEMINI.md +3 -3
- package/conventions/templates/cli/project_rules.md +16 -0
- package/conventions/templates/frontend-only/AGENTS.md +29 -64
- package/conventions/templates/frontend-only/GEMINI.md +3 -3
- package/conventions/templates/frontend-only/project_rules.md +14 -0
- package/conventions/templates/fullstack/AGENTS.md +31 -79
- package/conventions/templates/fullstack/CLAUDE.md +1 -1
- package/conventions/templates/fullstack/GEMINI.md +3 -3
- package/conventions/templates/fullstack/project_rules.md +15 -0
- package/lib/README.md +42 -0
- package/lib/__pycache__/github_sync.cpython-314.pyc +0 -0
- package/lib/__pycache__/loop-fmt.cpython-314.pyc +0 -0
- package/lib/__pycache__/loop_result_eval.cpython-314.pyc +0 -0
- package/lib/__pycache__/loop_unstick.cpython-314.pyc +0 -0
- package/lib/__pycache__/model_prices.cpython-314.pyc +0 -0
- package/lib/__pycache__/prices_fetcher.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll-home.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll_git.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll_render.cpython-314.pyc +0 -0
- package/lib/__pycache__/slides-render.cpython-314.pyc +0 -0
- package/lib/agent_usage/README.md +49 -0
- package/lib/agent_usage/__init__.py +108 -0
- package/lib/agent_usage/__pycache__/__init__.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/gemini.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/kimi.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/openai.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/pi.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/pi_emit.cpython-314.pyc +0 -0
- package/lib/agent_usage/__pycache__/qwen.cpython-314.pyc +0 -0
- package/lib/agent_usage/gemini.py +127 -0
- package/lib/agent_usage/kimi.py +278 -0
- package/lib/agent_usage/kimi_emit.py +123 -0
- package/lib/agent_usage/openai.py +126 -0
- package/lib/agent_usage/pi.py +200 -0
- package/lib/agent_usage/pi_emit.py +135 -0
- package/lib/agent_usage/qwen.py +128 -0
- package/lib/backfill-pi-usage.py +243 -0
- package/lib/changelog_audit.py +155 -0
- package/lib/changelog_generate.py +263 -0
- package/lib/context_feed_budget.sh +194 -0
- package/lib/github_sync.py +876 -0
- package/lib/i18n/README.md +54 -0
- package/lib/i18n/agent.sh +75 -0
- package/lib/i18n/alert.sh +20 -0
- package/lib/i18n/backlog.sh +96 -0
- package/lib/i18n/brief.sh +5 -0
- package/lib/i18n/changelog.sh +5 -0
- package/lib/i18n/ci.sh +15 -0
- package/lib/i18n/debug.sh +0 -0
- package/lib/i18n/doctor.sh +44 -0
- package/lib/i18n/dream.sh +0 -0
- package/lib/i18n/init.sh +91 -0
- package/lib/i18n/lang.sh +10 -0
- package/lib/i18n/loop.sh +140 -0
- package/lib/i18n/migrate.sh +74 -0
- package/lib/i18n/offboard.sh +31 -0
- package/lib/i18n/onboard.sh +0 -0
- package/lib/i18n/peer.sh +41 -0
- package/lib/i18n/peer_help.sh +25 -0
- package/lib/i18n/peer_reset.sh +7 -0
- package/lib/i18n/peer_status.sh +5 -0
- package/lib/i18n/prices.sh +3 -0
- package/lib/i18n/prices_refresh.sh +17 -0
- package/lib/i18n/prices_show.sh +7 -0
- package/lib/i18n/propose.sh +0 -0
- package/lib/i18n/release.sh +0 -0
- package/lib/i18n/research.sh +0 -0
- package/lib/i18n/review_pr.sh +0 -0
- package/lib/i18n/sentinel.sh +0 -0
- package/lib/i18n/setup.sh +3 -0
- package/lib/i18n/shared.sh +157 -0
- package/lib/i18n/skills/roll-brief.sh +47 -0
- package/lib/i18n/skills/roll-build.sh +97 -0
- package/lib/i18n/skills/roll-design.sh +18 -0
- package/lib/i18n/skills/roll-fix.sh +53 -0
- package/lib/i18n/skills/roll-loop.sh +28 -0
- package/lib/i18n/skills/roll-onboard.sh +33 -0
- package/lib/i18n/skills_catalog.sh +30 -0
- package/lib/i18n/slides.sh +3 -0
- package/lib/i18n/slides_build.sh +38 -0
- package/lib/i18n/slides_delete.sh +19 -0
- package/lib/i18n/slides_list.sh +14 -0
- package/lib/i18n/slides_logs.sh +12 -0
- package/lib/i18n/slides_new.sh +15 -0
- package/lib/i18n/slides_preview.sh +14 -0
- package/lib/i18n/slides_templates.sh +7 -0
- package/lib/i18n/status.sh +21 -0
- package/lib/i18n/update.sh +24 -0
- package/lib/i18n.sh +211 -0
- package/lib/loop-exit-summary.py +393 -0
- package/lib/loop-fmt.py +589 -0
- package/lib/loop_pick_agent.py +316 -0
- package/lib/loop_result_eval.py +469 -0
- package/lib/loop_unstick.py +180 -0
- package/lib/model_prices.py +186 -0
- package/lib/prices/README.md +35 -0
- package/lib/prices/snapshot-2026-05-22.json +22 -0
- package/lib/prices/snapshot-2026-05-23-deepseek.json +15 -0
- package/lib/prices/snapshot-2026-05-23-kimi.json +14 -0
- package/lib/prices_fetcher.py +285 -0
- package/lib/roll-backlog.py +225 -0
- package/lib/roll-brief.py +286 -0
- package/lib/roll-help.py +158 -0
- package/lib/roll-home.py +556 -0
- package/lib/roll-init.py +156 -0
- package/lib/roll-loop-status.py +1683 -0
- package/lib/roll-loop-story.py +191 -0
- package/lib/roll-onboard-render.py +378 -0
- package/lib/roll-peer.py +252 -0
- package/lib/roll-plan-validate.py +386 -0
- package/lib/roll-setup.py +102 -0
- package/lib/roll-status.py +367 -0
- package/lib/roll_git.py +41 -0
- package/lib/roll_render.py +414 -0
- package/lib/slides/components/README.md +123 -0
- package/lib/slides/components/cards-2.html +9 -0
- package/lib/slides/components/cards-3.html +9 -0
- package/lib/slides/components/cards-4.html +9 -0
- package/lib/slides/components/compare.html +22 -0
- package/lib/slides/components/highlight.html +9 -0
- package/lib/slides/components/pipeline.html +12 -0
- package/lib/slides/components/plain.html +7 -0
- package/lib/slides/components/quote.html +4 -0
- package/lib/slides/components/timeline.html +9 -0
- package/lib/slides/templates/introduction-v3.html +571 -0
- package/lib/slides/templates/pitch.html +0 -0
- package/lib/slides-render.py +778 -0
- package/lib/slides-validate.py +357 -0
- package/lib/test_quality_gate.py +143 -0
- package/package.json +8 -7
- package/skills/roll-.changelog/SKILL.md +406 -33
- package/skills/roll-.clarify/SKILL.md +5 -2
- package/skills/roll-.dream/SKILL.md +374 -0
- package/skills/roll-.echo/SKILL.md +5 -2
- package/skills/roll-.qa/SKILL.md +57 -3
- package/skills/roll-.review/SKILL.md +42 -3
- package/skills/roll-brief/SKILL.md +209 -0
- package/skills/roll-build/SKILL.md +308 -63
- package/skills/roll-debug/SKILL.md +341 -162
- package/skills/roll-debug/injectable-bb.js +263 -0
- package/skills/roll-deck/SKILL.md +296 -0
- package/skills/roll-design/ENGINEERING_CHECKLIST.md +1 -1
- package/skills/roll-design/SKILL.md +727 -94
- package/skills/roll-doc/SKILL.md +595 -0
- package/skills/roll-doctor/SKILL.md +192 -0
- package/skills/roll-fix/SKILL.md +149 -32
- package/skills/{roll-jot → roll-idea}/SKILL.md +18 -10
- package/skills/roll-loop/SKILL.md +578 -0
- package/skills/roll-notes/SKILL.md +103 -0
- package/skills/roll-onboard/SKILL.md +234 -0
- package/skills/roll-peer/SKILL.md +336 -0
- package/skills/roll-propose/SKILL.md +157 -0
- package/skills/roll-review-pr/SKILL.md +58 -0
- package/skills/roll-sentinel/SKILL.md +11 -2
- package/skills/roll-spar/SKILL.md +8 -6
- package/template/.github/workflows/ci.yml +5 -2
- package/template/AGENTS.md +20 -74
- package/skills/roll-research/SKILL.md +0 -307
- package/skills/roll-research/references/schema.json +0 -162
- package/skills/roll-research/scripts/md_to_pdf.py +0 -289
- package/tools/roll-fetch/SKILL.md +0 -182
- package/tools/roll-fetch/package.json +0 -15
- package/tools/roll-fetch/smart-web-fetch.js +0 -558
- package/tools/roll-probe/SKILL.md +0 -84
- /package/template/{BACKLOG.md → .roll/backlog.md} +0 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""
|
|
2
|
+
roll_render — shared terminal rendering primitives for roll CLI views.
|
|
3
|
+
|
|
4
|
+
Color palette, glyphs, padding/alignment, semantic deltas, and the layout
|
|
5
|
+
helpers used to print the static CLI dashboards (loop status, home, status,
|
|
6
|
+
backlog, brief, setup, init, peer). Every visible color lives in PAL;
|
|
7
|
+
NO_COLOR=1 falls through to glyph + weight + spacing only.
|
|
8
|
+
|
|
9
|
+
CJK display width is honored via strw() — CJK and fullwidth glyphs occupy
|
|
10
|
+
2 cells; this is what keeps EN/ZH paired rows aligned.
|
|
11
|
+
|
|
12
|
+
Set roll_render.USE_COLOR from the entry script after parsing flags / TTY.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
import re
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from typing import Any, Dict, List, Tuple
|
|
19
|
+
from unicodedata import east_asian_width
|
|
20
|
+
|
|
21
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
22
|
+
# ANSI / color
|
|
23
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
24
|
+
USE_COLOR = True
|
|
25
|
+
COLS = 100 # fixed 100-col grid; auto-narrow handled in caller's render
|
|
26
|
+
|
|
27
|
+
def _rgb(hexstr: str) -> str:
|
|
28
|
+
h = hexstr.lstrip("#")
|
|
29
|
+
return f"\033[38;2;{int(h[0:2],16)};{int(h[2:4],16)};{int(h[4:6],16)}m"
|
|
30
|
+
|
|
31
|
+
# Single source of truth — every visible color in the design lives here.
|
|
32
|
+
PAL = {
|
|
33
|
+
"fg": _rgb("e6edf3"),
|
|
34
|
+
"dim": _rgb("8b949e"),
|
|
35
|
+
"muted": _rgb("6e7681"),
|
|
36
|
+
"faint": _rgb("484f58"),
|
|
37
|
+
"blue": _rgb("58a6ff"),
|
|
38
|
+
"green": _rgb("3fb950"),
|
|
39
|
+
"amber": _rgb("d29922"),
|
|
40
|
+
"red": _rgb("f85149"),
|
|
41
|
+
"purple": _rgb("bc8cff"),
|
|
42
|
+
"pink": _rgb("f778ba"),
|
|
43
|
+
"yellow": _rgb("e3b341"),
|
|
44
|
+
}
|
|
45
|
+
BOLD = "\033[1m"
|
|
46
|
+
RESET = "\033[0m"
|
|
47
|
+
|
|
48
|
+
def c(color: str, s: str, *, bold: bool = False) -> str:
|
|
49
|
+
if not USE_COLOR:
|
|
50
|
+
return s
|
|
51
|
+
return f"{PAL.get(color, '')}{BOLD if bold else ''}{s}{RESET}"
|
|
52
|
+
|
|
53
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
54
|
+
# East-Asian display width — CJK and fullwidth glyphs occupy 2 cells.
|
|
55
|
+
# This is what keeps EN/ZH paired rows column-aligned in the terminal.
|
|
56
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
57
|
+
_ANSI_RE = re.compile(r"\033\[[\d;]*m")
|
|
58
|
+
|
|
59
|
+
def strip_ansi(s: str) -> str:
|
|
60
|
+
"""Strip ANSI escape sequences (CSI SGR) from a string."""
|
|
61
|
+
return _ANSI_RE.sub("", s)
|
|
62
|
+
|
|
63
|
+
def strw(s: str) -> int:
|
|
64
|
+
"""Display width of a string after stripping ANSI escapes."""
|
|
65
|
+
bare = _ANSI_RE.sub("", s)
|
|
66
|
+
w = 0
|
|
67
|
+
for ch in bare:
|
|
68
|
+
w += 2 if east_asian_width(ch) in ("F", "W") else 1
|
|
69
|
+
return w
|
|
70
|
+
|
|
71
|
+
def pad(s: str, w: int, align: str = "l") -> str:
|
|
72
|
+
sw = strw(s)
|
|
73
|
+
if sw >= w:
|
|
74
|
+
return s
|
|
75
|
+
fill = " " * (w - sw)
|
|
76
|
+
return fill + s if align == "r" else s + fill
|
|
77
|
+
|
|
78
|
+
def row(left: str, right: str, width: int = COLS) -> str:
|
|
79
|
+
"""Two-end-flush row at `width` columns."""
|
|
80
|
+
gap = max(1, width - strw(left) - strw(right))
|
|
81
|
+
return left + " " * gap + right
|
|
82
|
+
|
|
83
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
84
|
+
# Formatters
|
|
85
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
86
|
+
def fmt_dur(s: int) -> str:
|
|
87
|
+
if s < 3600:
|
|
88
|
+
return f"{s // 60}m"
|
|
89
|
+
return f"{s // 3600}h {(s % 3600) // 60}m"
|
|
90
|
+
|
|
91
|
+
# FIX-121: agent → primary model used by `roll loop` dashboard's fallback
|
|
92
|
+
# when an event stream lacks an explicit model name (non-claude agents'
|
|
93
|
+
# stdout isn't stream-json so loop-fmt can't extract model). Keeps the
|
|
94
|
+
# model column consistent with claude's "opus-4-7" style.
|
|
95
|
+
_AGENT_PRIMARY_MODEL = {
|
|
96
|
+
"pi": "deepseek-v4-pro",
|
|
97
|
+
"deepseek": "deepseek-v4-pro",
|
|
98
|
+
"kimi": "kimi-k2-0905",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def fmt_model(model) -> str:
|
|
103
|
+
"""Short label for the cycle row's model column.
|
|
104
|
+
|
|
105
|
+
`claude-opus-4-7-20251001` → `opus-4-7`
|
|
106
|
+
None / empty → `—`
|
|
107
|
+
Non-claude vendor → `?`
|
|
108
|
+
"""
|
|
109
|
+
if not model:
|
|
110
|
+
return "—"
|
|
111
|
+
if not model.startswith("claude-"):
|
|
112
|
+
return "?"
|
|
113
|
+
s = model[len("claude-"):]
|
|
114
|
+
s = re.sub(r"-\d{6,8}$", "", s)
|
|
115
|
+
return s if s else "?"
|
|
116
|
+
|
|
117
|
+
def fmt_tokens(n: int) -> str:
|
|
118
|
+
"""Format a token count with K / M / B unit scaling, 1 decimal place.
|
|
119
|
+
Uppercase suffix disambiguates from duration's lowercase m / h on the
|
|
120
|
+
same row (e.g. "19m 6.7M" reads cleanly as 19 minutes + 6.7M tokens)."""
|
|
121
|
+
if not n:
|
|
122
|
+
return "—"
|
|
123
|
+
if n < 1_000:
|
|
124
|
+
return str(int(n))
|
|
125
|
+
if n < 1_000_000:
|
|
126
|
+
return f"{n / 1_000:.1f}K".replace(".0K", "K")
|
|
127
|
+
if n < 1_000_000_000:
|
|
128
|
+
return f"{n / 1_000_000:.1f}M".replace(".0M", "M")
|
|
129
|
+
return f"{n / 1_000_000_000:.1f}B".replace(".0B", "B")
|
|
130
|
+
|
|
131
|
+
# Subtle red wash for the entire failure row — doubles up the color signal
|
|
132
|
+
# so a fail can't be missed even when scanning at 2x speed. Used by
|
|
133
|
+
# cycle_row when outcome=fail.
|
|
134
|
+
BG_FAIL = "\033[48;2;55;15;15m"
|
|
135
|
+
|
|
136
|
+
def fmt_delta(today: float, yest: float, *, kind: str, unit: str = "") -> Tuple[str, str]:
|
|
137
|
+
"""Return (delta_string, semantic_color). kind ∈ {'up_good','up_bad','any'}.
|
|
138
|
+
`unit`: '' → plain int, '$' → currency, 'm' → minutes (caller pre-converts)."""
|
|
139
|
+
if yest == 0 and today == 0:
|
|
140
|
+
return ("—", "muted")
|
|
141
|
+
if yest == 0:
|
|
142
|
+
return ("▲ new", "amber")
|
|
143
|
+
diff = today - yest
|
|
144
|
+
if abs(diff) < 1e-9:
|
|
145
|
+
return ("=", "muted")
|
|
146
|
+
arrow = "▲" if diff > 0 else "▼"
|
|
147
|
+
sign = "+" if diff > 0 else "−"
|
|
148
|
+
mag = abs(diff)
|
|
149
|
+
if unit in ("$", "¥"):
|
|
150
|
+
body = f"{sign}{unit}{mag:.2f}"
|
|
151
|
+
elif unit == "m":
|
|
152
|
+
body = f"{sign}{int(round(mag))}m"
|
|
153
|
+
else:
|
|
154
|
+
body = f"{sign}{int(round(mag))}"
|
|
155
|
+
color = {
|
|
156
|
+
"up_good": "green" if diff > 0 else "amber",
|
|
157
|
+
"up_bad": "red" if diff > 0 else "green",
|
|
158
|
+
"any": "amber",
|
|
159
|
+
}[kind]
|
|
160
|
+
return (f"{arrow} {body}", color)
|
|
161
|
+
|
|
162
|
+
def trunc(s: str, n: int) -> str:
|
|
163
|
+
if strw(s) <= n:
|
|
164
|
+
return s
|
|
165
|
+
out = ""
|
|
166
|
+
for ch in s:
|
|
167
|
+
if strw(out) + strw(ch) + 1 > n:
|
|
168
|
+
return out + "…"
|
|
169
|
+
out += ch
|
|
170
|
+
return out
|
|
171
|
+
|
|
172
|
+
def empty_rollup() -> Dict[str, Any]:
|
|
173
|
+
return {"cycles": 0, "prs": 0, "failed": 0, "duration_s": 0, "cost": 0.0,
|
|
174
|
+
"input_tokens": 0, "output_tokens": 0}
|
|
175
|
+
|
|
176
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
177
|
+
# Section / metric / cycle rows — printers used by all dashboards
|
|
178
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
179
|
+
def section_head(en: str, zh: str, hint: str) -> None:
|
|
180
|
+
left = " " + c("pink", en, bold=True) + c("muted", " · ") + c("dim", zh)
|
|
181
|
+
print(row(left, c("muted", hint)))
|
|
182
|
+
|
|
183
|
+
def metric(name: str, t: int, y: int, d2: int, kind: str, *,
|
|
184
|
+
yest_color: str = "dim", yest_suffix: str = "",
|
|
185
|
+
partial: bool = False) -> None:
|
|
186
|
+
"""Print one metric row. When `partial=True` the delta is rendered in
|
|
187
|
+
muted gray instead of green/red — today's incomplete, so a 'down −23'
|
|
188
|
+
against yesterday's full day would otherwise read as an alarm.
|
|
189
|
+
|
|
190
|
+
Column geometry (kept in lockstep with the header in roll-loop-status):
|
|
191
|
+
indent 2 · name 14 · today_value 8 · gap 2 · delta 12 · yest 10 · d2 8
|
|
192
|
+
"""
|
|
193
|
+
delta_text, delta_c = fmt_delta(float(t), float(y), kind=kind)
|
|
194
|
+
if partial and delta_c not in ("muted",):
|
|
195
|
+
delta_c = "muted"
|
|
196
|
+
yest_str = f"{y}" + (f" {yest_suffix}" if yest_suffix else "")
|
|
197
|
+
print(" " +
|
|
198
|
+
c("dim", pad(name, 14)) +
|
|
199
|
+
c("fg", pad(str(t), 8, "r"), bold=True) + " " +
|
|
200
|
+
c(delta_c, pad(delta_text, 12), bold=(delta_c != "muted")) +
|
|
201
|
+
c(yest_color, pad(yest_str, 10), bold=bool(yest_suffix)) +
|
|
202
|
+
c("muted", pad(str(d2), 8)))
|
|
203
|
+
|
|
204
|
+
def metric_dur(name: str, t: int, y: int, d2: int, *, partial: bool = False) -> None:
|
|
205
|
+
# work in whole minutes for the delta so it reads naturally (▲ +14m)
|
|
206
|
+
t_m = t // 60
|
|
207
|
+
y_m = y // 60
|
|
208
|
+
delta_text, delta_c = fmt_delta(float(t_m), float(y_m), kind="up_bad", unit="m")
|
|
209
|
+
if partial and delta_c not in ("muted",):
|
|
210
|
+
delta_c = "muted"
|
|
211
|
+
print(" " +
|
|
212
|
+
c("dim", pad(name, 14)) +
|
|
213
|
+
c("fg", pad(fmt_dur(t), 8, "r"), bold=True) + " " +
|
|
214
|
+
c(delta_c, pad(delta_text, 12), bold=(delta_c != "muted")) +
|
|
215
|
+
c("dim", pad(fmt_dur(y), 10)) +
|
|
216
|
+
c("muted", pad(fmt_dur(d2), 8)))
|
|
217
|
+
|
|
218
|
+
def metric_dollar(name: str, t: float, y: float, d2: float, *,
|
|
219
|
+
partial: bool = False, symbol: str = "$") -> None:
|
|
220
|
+
# FIX-126: currency-aware — deepseek cost is native CNY (¥), claude USD ($).
|
|
221
|
+
# We never convert; the rollup shows one row per currency with its own
|
|
222
|
+
# symbol, so a ¥-row and a $-row are never summed into a meaningless total.
|
|
223
|
+
delta_text, delta_c = fmt_delta(t, y, kind="up_bad", unit=symbol)
|
|
224
|
+
if partial and delta_c not in ("muted",):
|
|
225
|
+
delta_c = "muted"
|
|
226
|
+
print(" " +
|
|
227
|
+
c("dim", pad(name, 14)) +
|
|
228
|
+
c("fg", pad(f"{symbol}{t:.2f}", 8, "r"), bold=True) + " " +
|
|
229
|
+
c(delta_c, pad(delta_text, 12), bold=(delta_c != "muted")) +
|
|
230
|
+
c("dim", pad(f"{symbol}{y:.2f}", 10)) +
|
|
231
|
+
c("muted", pad(f"{symbol}{d2:.2f}", 8)))
|
|
232
|
+
|
|
233
|
+
def metric_tokens(name: str, t: int, y: int, d2: int, *, partial: bool = False) -> None:
|
|
234
|
+
# Compose the delta string with token-unit scaling so a 200M increase
|
|
235
|
+
# doesn't print '+200000000'.
|
|
236
|
+
if y == 0 and t == 0:
|
|
237
|
+
delta_text, delta_c = "—", "muted"
|
|
238
|
+
elif y == 0:
|
|
239
|
+
delta_text, delta_c = "▲ new", "amber"
|
|
240
|
+
elif t == y:
|
|
241
|
+
delta_text, delta_c = "=", "muted"
|
|
242
|
+
else:
|
|
243
|
+
diff = t - y
|
|
244
|
+
arrow = "▲" if diff > 0 else "▼"
|
|
245
|
+
sign = "+" if diff > 0 else "−"
|
|
246
|
+
delta_text = f"{arrow} {sign}{fmt_tokens(abs(diff))}"
|
|
247
|
+
delta_c = "red" if diff > 0 else "green"
|
|
248
|
+
if partial and delta_c not in ("muted",):
|
|
249
|
+
delta_c = "muted"
|
|
250
|
+
print(" " +
|
|
251
|
+
c("dim", pad(name, 14)) +
|
|
252
|
+
c("fg", pad(fmt_tokens(t), 8, "r"), bold=True) + " " +
|
|
253
|
+
c(delta_c, pad(delta_text, 12), bold=(delta_c != "muted")) +
|
|
254
|
+
c("dim", pad(fmt_tokens(y), 10)) +
|
|
255
|
+
c("muted", pad(fmt_tokens(d2), 8)))
|
|
256
|
+
|
|
257
|
+
def day_band(day_key: str, n_total: int, n_failed: int, now: datetime, *,
|
|
258
|
+
in_progress: bool = False) -> None:
|
|
259
|
+
from datetime import timedelta
|
|
260
|
+
today = now.strftime("%Y-%m-%d")
|
|
261
|
+
yest = (now - timedelta(days=1)).strftime("%Y-%m-%d")
|
|
262
|
+
if day_key == today:
|
|
263
|
+
label = "Today · 今日"
|
|
264
|
+
elif day_key == yest:
|
|
265
|
+
label = "Yesterday · 昨日"
|
|
266
|
+
else:
|
|
267
|
+
n = (now.date() - datetime.strptime(day_key, "%Y-%m-%d").date()).days
|
|
268
|
+
label = f"−{n} days · 前 {n} 天"
|
|
269
|
+
weekday = datetime.strptime(day_key, "%Y-%m-%d").strftime("%a")
|
|
270
|
+
weekday_zh = ["周一","周二","周三","周四","周五","周六","周日"][
|
|
271
|
+
datetime.strptime(day_key, "%Y-%m-%d").weekday()]
|
|
272
|
+
count_str = f"{n_total} cycles" + (f" · {n_failed} failed" if n_failed else " · 0 failed")
|
|
273
|
+
if in_progress:
|
|
274
|
+
count_str += " · " + "in progress"
|
|
275
|
+
left = (" " + c("faint", "─ ") +
|
|
276
|
+
c("fg", label, bold=True) +
|
|
277
|
+
c("muted", " · ") + c("dim", day_key) +
|
|
278
|
+
c("muted", " · ") + c("dim", f"{weekday} · {weekday_zh}") + " ")
|
|
279
|
+
if in_progress:
|
|
280
|
+
right_inner = (c("dim", f"{n_total} cycles") +
|
|
281
|
+
(c("dim", f" · {n_failed} failed") if n_failed
|
|
282
|
+
else c("dim", " · 0 failed")) +
|
|
283
|
+
c("muted", " · ") + c("amber", "in progress"))
|
|
284
|
+
else:
|
|
285
|
+
right_inner = c("dim", count_str)
|
|
286
|
+
right = " " + right_inner
|
|
287
|
+
dashes = max(2, COLS - strw(left) - strw(right))
|
|
288
|
+
print(left + c("faint", "─" * dashes) + right)
|
|
289
|
+
|
|
290
|
+
def cycle_row(cy: Dict[str, Any], backlog: Dict[str, str]) -> None:
|
|
291
|
+
outcome = cy.get("outcome", "done")
|
|
292
|
+
pr_outcome = cy.get("pr_outcome")
|
|
293
|
+
glyph_c, glyph = {
|
|
294
|
+
"done": ("green", "✓"),
|
|
295
|
+
"ok": ("green", "✓"),
|
|
296
|
+
"fail": ("red", "✗"),
|
|
297
|
+
"running": ("purple", "⏵"),
|
|
298
|
+
"idle": ("muted", "·"),
|
|
299
|
+
}.get(outcome, ("muted", "·"))
|
|
300
|
+
# US-VIEW-011: a completed cycle whose PR was closed without merging is
|
|
301
|
+
# a "wasted run" — flip the green ✓ to an amber ⊘ so it can't be
|
|
302
|
+
# mistaken for a real delivery when scanning the dashboard.
|
|
303
|
+
if outcome in ("done", "ok") and pr_outcome == "closed":
|
|
304
|
+
glyph_c, glyph = "amber", "⊘"
|
|
305
|
+
time_str = cy["start"].astimezone().strftime("%H:%M")
|
|
306
|
+
cr = cy.get("cron") or {}
|
|
307
|
+
# duration prefers the explicit cy["duration_s"] (computed from event
|
|
308
|
+
# timestamps or runs.jsonl) so it shows for all completed cycles, not
|
|
309
|
+
# only the one that happens to be in the latest cron.log dump.
|
|
310
|
+
# For a currently-running cycle, show wall-clock elapsed (now - start).
|
|
311
|
+
dur_s = cy.get("duration_s") or cr.get("duration_s") or 0
|
|
312
|
+
if outcome == "running" and not dur_s and cy.get("start"):
|
|
313
|
+
from datetime import datetime as _dt, timezone as _tz
|
|
314
|
+
dur_s = int((_dt.now(_tz.utc) - cy["start"]).total_seconds())
|
|
315
|
+
dur = fmt_dur(dur_s) if dur_s else "—"
|
|
316
|
+
# US-VIEW-017: show all 4 token components when cache data is available.
|
|
317
|
+
# Format: "in/cw↑ cr↓/out" (cache writes ↑, cache reads ↓).
|
|
318
|
+
# Falls back to "in/out" for cycles that predate cache tracking.
|
|
319
|
+
inp = cy.get('input_tokens') or 0
|
|
320
|
+
out_tok = cy.get('output_tokens') or 0
|
|
321
|
+
cw = cy.get('cache_creation_tokens') or 0
|
|
322
|
+
cr = cy.get('cache_read_tokens') or 0
|
|
323
|
+
if cw or cr:
|
|
324
|
+
tok = (f"{fmt_tokens(inp)}"
|
|
325
|
+
f"/{fmt_tokens(cw)}↑ {fmt_tokens(cr)}↓"
|
|
326
|
+
f"/{fmt_tokens(out_tok)}")
|
|
327
|
+
else:
|
|
328
|
+
tok = f"{fmt_tokens(inp)}/{fmt_tokens(out_tok)}"
|
|
329
|
+
# cost prefers the backfilled list-price; falls back to cron.log when
|
|
330
|
+
# the claude session log isn't available (only the latest cycle).
|
|
331
|
+
# FIX-116: use the model's native currency symbol.
|
|
332
|
+
cur = cy.get("cost_currency", "USD")
|
|
333
|
+
symbol = "¥" if cur == "CNY" else "$"
|
|
334
|
+
if cy.get("cost_list") is not None:
|
|
335
|
+
cost = f"{symbol}{cy['cost_list']:.2f}"
|
|
336
|
+
elif cr:
|
|
337
|
+
cost = f"{symbol}{cr.get('cost', 0):.2f}"
|
|
338
|
+
else:
|
|
339
|
+
cost = "—"
|
|
340
|
+
sid = cy.get("story") or "—"
|
|
341
|
+
built = cy.get("built") or ([sid] if sid != "—" else [])
|
|
342
|
+
# Join multiple stories with " | ". Drop empties and dedupe in order.
|
|
343
|
+
seen = set()
|
|
344
|
+
ids = []
|
|
345
|
+
for s in built:
|
|
346
|
+
if s and s not in seen:
|
|
347
|
+
seen.add(s)
|
|
348
|
+
ids.append(s)
|
|
349
|
+
ids_str = " | ".join(ids) if ids else sid
|
|
350
|
+
time_c = "red" if outcome == "fail" else "fg"
|
|
351
|
+
sid_c = "red" if outcome == "fail" else "blue"
|
|
352
|
+
|
|
353
|
+
model_label = fmt_model(cy.get("model"))
|
|
354
|
+
# FIX-119: fall back to cy["agent"] (from agent_used event) when model
|
|
355
|
+
# is unknown — non-claude agents (pi, deepseek, kimi) don't expose model
|
|
356
|
+
# info in stream-json, leaving a "—" or "?" on the dashboard.
|
|
357
|
+
# FIX-121: map agent → its configured primary model so the column shows
|
|
358
|
+
# the actual model name (e.g. "deepseek-v4-pro") consistently with
|
|
359
|
+
# claude's "opus-4-7", not the bare agent name ("pi").
|
|
360
|
+
if model_label in ("—", "?") and cy.get("agent"):
|
|
361
|
+
model_label = _AGENT_PRIMARY_MODEL.get(cy["agent"], cy["agent"])
|
|
362
|
+
# Auto-hide model column on narrow screens — keeps the dashboard readable
|
|
363
|
+
# when terminal is < 100 cols (cost / story IDs are higher-priority).
|
|
364
|
+
show_model = COLS >= 100
|
|
365
|
+
model_seg = c("muted", pad(model_label, 11)) + " " if show_model else ""
|
|
366
|
+
# US-VIEW-011: PR landing marker after the story id(s).
|
|
367
|
+
# merged → "#NN ✓" green
|
|
368
|
+
# closed → "#NN ↩" amber (paired with ⊘ glyph above)
|
|
369
|
+
# open → "#NN …" dim (still landing; auto-merge or human pending)
|
|
370
|
+
pr_marker = ""
|
|
371
|
+
pr_num = cy.get("pr_num")
|
|
372
|
+
if pr_num is not None and pr_outcome:
|
|
373
|
+
mark_c, mark_sym = {
|
|
374
|
+
"merged": ("green", "✓"),
|
|
375
|
+
"closed": ("amber", "↩"),
|
|
376
|
+
"open": ("dim", "…"),
|
|
377
|
+
}.get(pr_outcome, ("dim", "…"))
|
|
378
|
+
pr_marker = " " + c(mark_c, f"#{pr_num} {mark_sym}")
|
|
379
|
+
# US-VIEW-014: pre-US-VIEW-014 events (no frozen cost_list_usd at
|
|
380
|
+
# cycle_end) get a muted [legacy] suffix — the number is recomputed on
|
|
381
|
+
# the fly and can shift with future price changes, unlike the frozen
|
|
382
|
+
# values written by current loop-fmt.
|
|
383
|
+
legacy_marker = " " + c("muted", "[legacy]") if cy.get("cost_list_legacy") else ""
|
|
384
|
+
inner = (
|
|
385
|
+
" " + c(glyph_c, glyph, bold=True) + " " +
|
|
386
|
+
c(time_c, pad(time_str, 5), bold=(outcome == "fail")) + " " +
|
|
387
|
+
c("muted", pad(dur, 4, "r")) + " " +
|
|
388
|
+
c("muted", pad(tok, 26)) + " " +
|
|
389
|
+
model_seg +
|
|
390
|
+
c("muted", pad(cost, 7, "r")) + " " +
|
|
391
|
+
c(sid_c, ids_str, bold=True) + pr_marker + legacy_marker
|
|
392
|
+
)
|
|
393
|
+
# Subtle red bg on failure rows so a fail can't be missed at a glance.
|
|
394
|
+
if outcome == "fail" and USE_COLOR:
|
|
395
|
+
# Every inner c(...) span ends with \033[0m which terminates the bg
|
|
396
|
+
# too. Re-paint the bg after every internal reset so the wash spans
|
|
397
|
+
# the whole row, not just the first colored cell. Then pad to full
|
|
398
|
+
# width so the bg extends edge-to-edge before the final reset.
|
|
399
|
+
line_pad = max(0, COLS - strw(inner))
|
|
400
|
+
inner_padded = inner + " " * line_pad
|
|
401
|
+
print(BG_FAIL + inner_padded.replace(RESET, RESET + BG_FAIL) + RESET)
|
|
402
|
+
# Always emit the drill hint for fails — fail_detail is often missing
|
|
403
|
+
# because not every fail path goes through the test / build stages.
|
|
404
|
+
hint = " " * 8 + c("dim", "→ ") + c("amber", f"roll loop show {cy['label']}")
|
|
405
|
+
if cy.get("fail_detail"):
|
|
406
|
+
hint += c("muted", " ") + c("dim", cy["fail_detail"])
|
|
407
|
+
hint_pad = max(0, COLS - strw(hint))
|
|
408
|
+
hint_padded = hint + " " * hint_pad
|
|
409
|
+
print(BG_FAIL + hint_padded.replace(RESET, RESET + BG_FAIL) + RESET)
|
|
410
|
+
else:
|
|
411
|
+
print(inner)
|
|
412
|
+
if outcome == "fail" and cy.get("fail_detail"):
|
|
413
|
+
# NO_COLOR path: drill hint still useful for diagnosis.
|
|
414
|
+
print(" " * 8 + "→ " + f"roll loop show {cy['label']}")
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Slide Components Library
|
|
2
|
+
|
|
3
|
+
Reusable Mustache partials for slide layouts. Each partial is a self-contained
|
|
4
|
+
HTML fragment consumed by `lib/slides-render.py`. Partials use CSS classes from
|
|
5
|
+
the introduction-v3 template and require no additional stylesheets.
|
|
6
|
+
|
|
7
|
+
## Layout Reference
|
|
8
|
+
|
|
9
|
+
| Layout | Partial File | Use When | Avoid When |
|
|
10
|
+
|-------------|--------------------|--------------------------------------------|-------------------------------------|
|
|
11
|
+
| `plain` | `plain.html` | Free-form text, no structure needed | Data has compare / flow / timeline |
|
|
12
|
+
| `cards-2` | `cards-2.html` | 2 parallel concepts, side-by-side feature | 3+ items (use cards-3/cards-4) |
|
|
13
|
+
| `cards-3` | `cards-3.html` | 3 pillars, triple option, 3-step summary | 2 items (use cards-2) |
|
|
14
|
+
| `cards-4` | `cards-4.html` | 4 quadrants, pricing tiers, team roles | <4 items (too sparse) |
|
|
15
|
+
| `compare` | `compare.html` | Before/after, problem/solution, old/new | Unrelated items (use cards) |
|
|
16
|
+
| `pipeline` | `pipeline.html` | Sequential flow, CI/CD, process steps | Unordered items (use cards) |
|
|
17
|
+
| `timeline` | `timeline.html` | Chronological events, history, roadmap | Single event (use highlight) |
|
|
18
|
+
| `quote` | `quote.html` | Testimonial, key takeaway, memorable line | Multi-paragraph prose (use plain) |
|
|
19
|
+
| `highlight` | `highlight.html` | Callout, warning, important note | Normal body text (use plain) |
|
|
20
|
+
|
|
21
|
+
## Field Tables
|
|
22
|
+
|
|
23
|
+
### cards-2 / cards-3 / cards-4
|
|
24
|
+
|
|
25
|
+
| Variable | Required | Type | Description |
|
|
26
|
+
|-----------------|----------|--------|---------------------------------|
|
|
27
|
+
| `cards` | yes | array | Array of card objects |
|
|
28
|
+
| `cards[].title_en` | yes | string | Card title (English) |
|
|
29
|
+
| `cards[].title_zh` | yes | string | Card title (Chinese) |
|
|
30
|
+
| `cards[].body_en` | yes | string | Card body HTML (English, raw) |
|
|
31
|
+
| `cards[].body_zh` | yes | string | Card body HTML (Chinese, raw) |
|
|
32
|
+
| `accent_color` | no | string | Unused — reserved for future |
|
|
33
|
+
|
|
34
|
+
### compare
|
|
35
|
+
|
|
36
|
+
| Variable | Required | Type | Description |
|
|
37
|
+
|----------------------|----------|--------|---------------------------------|
|
|
38
|
+
| `left_title_en` | yes | string | Left column heading (EN) |
|
|
39
|
+
| `left_title_zh` | yes | string | Left column heading (ZH) |
|
|
40
|
+
| `right_title_en` | yes | string | Right column heading (EN) |
|
|
41
|
+
| `right_title_zh` | yes | string | Right column heading (ZH) |
|
|
42
|
+
| `left_items` | yes | array | Left column items |
|
|
43
|
+
| `left_items[].text_en` | yes | string | Item text (EN) |
|
|
44
|
+
| `left_items[].text_zh` | yes | string | Item text (ZH) |
|
|
45
|
+
| `right_items` | yes | array | Right column items |
|
|
46
|
+
| `right_items[].text_en` | yes | string | Item text (EN) |
|
|
47
|
+
| `right_items[].text_zh` | yes | string | Item text (ZH) |
|
|
48
|
+
|
|
49
|
+
### pipeline
|
|
50
|
+
|
|
51
|
+
| Variable | Required | Type | Description |
|
|
52
|
+
|---------------------|----------|--------|---------------------------------|
|
|
53
|
+
| `stages` | yes | array | Pipeline stages in order |
|
|
54
|
+
| `stages[].title_en` | yes | string | Stage title (EN) |
|
|
55
|
+
| `stages[].title_zh` | yes | string | Stage title (ZH) |
|
|
56
|
+
| `stages[].desc_en` | yes | string | Stage description (EN) |
|
|
57
|
+
| `stages[].desc_zh` | yes | string | Stage description (ZH) |
|
|
58
|
+
| `stages[].css_class` | yes | string | CSS class: `pipe-idea`, `pipe-backlog`, `pipe-build`, `pipe-verify`, or `pipe-release` |
|
|
59
|
+
|
|
60
|
+
### timeline
|
|
61
|
+
|
|
62
|
+
| Variable | Required | Type | Description |
|
|
63
|
+
|---------------------|----------|--------|---------------------------------|
|
|
64
|
+
| `items` | yes | array | Timeline entries (chronological)|
|
|
65
|
+
| `items[].title_en` | yes | string | Entry title (EN) |
|
|
66
|
+
| `items[].title_zh` | yes | string | Entry title (ZH) |
|
|
67
|
+
| `items[].body_en` | yes | string | Entry body HTML (EN, raw) |
|
|
68
|
+
| `items[].body_zh` | yes | string | Entry body HTML (ZH, raw) |
|
|
69
|
+
|
|
70
|
+
### quote
|
|
71
|
+
|
|
72
|
+
| Variable | Required | Type | Description |
|
|
73
|
+
|-----------|----------|--------|---------------------------------|
|
|
74
|
+
| `text_en` | yes | string | Quote text (EN) |
|
|
75
|
+
| `text_zh` | yes | string | Quote text (ZH) |
|
|
76
|
+
|
|
77
|
+
### highlight
|
|
78
|
+
|
|
79
|
+
| Variable | Required | Type | Description |
|
|
80
|
+
|-----------|----------|--------|---------------------------------|
|
|
81
|
+
| `body_en` | yes | string | Body HTML (EN, raw) |
|
|
82
|
+
| `body_zh` | yes | string | Body HTML (ZH, raw) |
|
|
83
|
+
|
|
84
|
+
### plain
|
|
85
|
+
|
|
86
|
+
| Variable | Required | Type | Description |
|
|
87
|
+
|-----------|----------|--------|---------------------------------|
|
|
88
|
+
| `body_en` | yes | string | Body HTML (EN, raw) |
|
|
89
|
+
| `body_zh` | yes | string | Body HTML (ZH, raw) |
|
|
90
|
+
|
|
91
|
+
## CSS Classes
|
|
92
|
+
|
|
93
|
+
Every class name in these partials is copied verbatim from the introduction-v3
|
|
94
|
+
template (`lib/slides/templates/introduction-v3.html`). Do **not** introduce
|
|
95
|
+
new class names — the template's CSS is the single source of truth.
|
|
96
|
+
|
|
97
|
+
## Usage
|
|
98
|
+
|
|
99
|
+
Partials are consumed by `lib/slides-render.py` when a `deck.md` slide declares
|
|
100
|
+
a `layout` field. A slide that omits `layout` renders as `plain`.
|
|
101
|
+
|
|
102
|
+
For the user-facing walkthrough — per-layout `deck.md` examples, rendered
|
|
103
|
+
screenshots, and how `$roll-deck` picks a layout — see the Layouts section of
|
|
104
|
+
the slides guide: [`guide/en/slides.md`](../../../guide/en/slides.md#layouts)
|
|
105
|
+
([中文](../../../guide/zh/slides.md#layouts布局)). Field names in this file,
|
|
106
|
+
that guide, and `skills/roll-deck/SKILL.md` are kept in sync — change one,
|
|
107
|
+
change all three.
|
|
108
|
+
|
|
109
|
+
```markdown
|
|
110
|
+
### Slide 3: Architecture Overview
|
|
111
|
+
|
|
112
|
+
layout: cards-3
|
|
113
|
+
title_en: Three Layers
|
|
114
|
+
title_zh: 三层架构
|
|
115
|
+
|
|
116
|
+
body_en: |
|
|
117
|
+
1. {{#cards}}...{{/cards}}
|
|
118
|
+
body_zh: |
|
|
119
|
+
1. {{#cards}}...{{/cards}}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The renderer inlines the partial's HTML into the template and resolves
|
|
123
|
+
Mustache variables and sections from the slide context.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<!-- cards-2: requires cards[].{title_en,title_zh,body_en,body_zh}, optional accent_color -->
|
|
2
|
+
<div class="cards cards-2">
|
|
3
|
+
{{#cards}}
|
|
4
|
+
<div class="card">
|
|
5
|
+
<h3><span class="lang-en">{{title_en}}</span><span class="lang-zh">{{title_zh}}</span></h3>
|
|
6
|
+
<p><span class="lang-en">{{{body_en}}}</span><span class="lang-zh">{{{body_zh}}}</span></p>
|
|
7
|
+
</div>
|
|
8
|
+
{{/cards}}
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<!-- cards-3: requires cards[].{title_en,title_zh,body_en,body_zh}, optional accent_color -->
|
|
2
|
+
<div class="cards cards-3">
|
|
3
|
+
{{#cards}}
|
|
4
|
+
<div class="card">
|
|
5
|
+
<h3><span class="lang-en">{{title_en}}</span><span class="lang-zh">{{title_zh}}</span></h3>
|
|
6
|
+
<p><span class="lang-en">{{{body_en}}}</span><span class="lang-zh">{{{body_zh}}}</span></p>
|
|
7
|
+
</div>
|
|
8
|
+
{{/cards}}
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<!-- cards-4: requires cards[].{title_en,title_zh,body_en,body_zh}, optional accent_color -->
|
|
2
|
+
<div class="cards cards-4">
|
|
3
|
+
{{#cards}}
|
|
4
|
+
<div class="card">
|
|
5
|
+
<h3><span class="lang-en">{{title_en}}</span><span class="lang-zh">{{title_zh}}</span></h3>
|
|
6
|
+
<p><span class="lang-en">{{{body_en}}}</span><span class="lang-zh">{{{body_zh}}}</span></p>
|
|
7
|
+
</div>
|
|
8
|
+
{{/cards}}
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!-- compare: requires left_title_en,left_title_zh,right_title_en,right_title_zh, left_items[].{text_en,text_zh}, right_items[].{text_en,text_zh} -->
|
|
2
|
+
<div class="compare">
|
|
3
|
+
<div class="compare-col compare-before">
|
|
4
|
+
<h3><span class="lang-en">{{left_title_en}}</span><span class="lang-zh">{{left_title_zh}}</span></h3>
|
|
5
|
+
{{#left_items}}
|
|
6
|
+
<div class="compare-item">
|
|
7
|
+
<span class="icon">✗</span>
|
|
8
|
+
<span class="lang-en">{{text_en}}</span><span class="lang-zh">{{text_zh}}</span>
|
|
9
|
+
</div>
|
|
10
|
+
{{/left_items}}
|
|
11
|
+
</div>
|
|
12
|
+
<div class="compare-arrow">→</div>
|
|
13
|
+
<div class="compare-col compare-after">
|
|
14
|
+
<h3><span class="lang-en">{{right_title_en}}</span><span class="lang-zh">{{right_title_zh}}</span></h3>
|
|
15
|
+
{{#right_items}}
|
|
16
|
+
<div class="compare-item">
|
|
17
|
+
<span class="icon">✓</span>
|
|
18
|
+
<span class="lang-en">{{text_en}}</span><span class="lang-zh">{{text_zh}}</span>
|
|
19
|
+
</div>
|
|
20
|
+
{{/right_items}}
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!-- pipeline: requires stages[].{title_en,title_zh,desc_en,desc_zh,css_class} -->
|
|
2
|
+
<div class="pipeline-bar">
|
|
3
|
+
{{#stages}}
|
|
4
|
+
<div class="pipe-stage {{css_class}}">
|
|
5
|
+
<h4><span class="lang-en">{{title_en}}</span><span class="lang-zh">{{title_zh}}</span></h4>
|
|
6
|
+
<p><span class="lang-en">{{desc_en}}</span><span class="lang-zh">{{desc_zh}}</span></p>
|
|
7
|
+
</div>
|
|
8
|
+
{{^last}}
|
|
9
|
+
<div class="pipe-arrow">→</div>
|
|
10
|
+
{{/last}}
|
|
11
|
+
{{/stages}}
|
|
12
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<!-- timeline: requires items[].{title_en,title_zh,body_en,body_zh} -->
|
|
2
|
+
<div class="timeline">
|
|
3
|
+
{{#items}}
|
|
4
|
+
<div class="timeline-item">
|
|
5
|
+
<h4><span class="lang-en">{{title_en}}</span><span class="lang-zh">{{title_zh}}</span></h4>
|
|
6
|
+
<p><span class="lang-en">{{{body_en}}}</span><span class="lang-zh">{{{body_zh}}}</span></p>
|
|
7
|
+
</div>
|
|
8
|
+
{{/items}}
|
|
9
|
+
</div>
|