Skip to content

Commit 44ea114

Browse files
committed
docs(design): add www redesign proposal and homepage mockup\n\n- Add docs/design/www-redesign-proposal.md with full redesign direction,\n tokens, components, page wireframes, and implementation phases.\n- Add docs/design/www-tokens.css as the reference token sheet.\n- Add docs/design/homepage-mockup.png generated from the proposal.\n- Include the generator script for reproducibility.
1 parent 8b9d1fd commit 44ea114

4 files changed

Lines changed: 808 additions & 0 deletions

File tree

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
#!/usr/bin/env python3
2+
"""Generate a homepage mockup PNG for the openElement redesign proposal."""
3+
from PIL import Image, ImageDraw, ImageFont
4+
import os
5+
6+
WIDTH = 1440
7+
HEIGHT = 1900
8+
9+
# Palette
10+
BG_BASE = (11, 12, 15)
11+
BG_SURFACE = (19, 21, 26)
12+
BG_ELEVATED = (26, 29, 36)
13+
BG_CODE = (13, 14, 18)
14+
BG_HOVER = (255, 255, 255, 10)
15+
TEXT_PRIMARY = (242, 243, 245)
16+
TEXT_SECONDARY = (156, 163, 175)
17+
TEXT_MUTED = (107, 114, 128)
18+
BRAND = (99, 102, 241)
19+
BRAND_HOVER = (129, 140, 248)
20+
BORDER = (255, 255, 255, 20)
21+
BORDER_STRONG = (255, 255, 255, 36)
22+
ACCENTS = {
23+
"Elements": (34, 211, 238),
24+
"UI": (167, 139, 250),
25+
"Framework": (52, 211, 153),
26+
"Protocols": (251, 191, 36),
27+
}
28+
29+
FONT_SANS = "C:/Windows/Fonts/segoeui.ttf"
30+
FONT_SANS_BOLD = "C:/Windows/Fonts/segoeuib.ttf"
31+
FONT_MONO = "C:/Windows/Fonts/consola.ttf"
32+
FONT_CN = "C:/Windows/Fonts/msyh.ttc"
33+
FONT_CN_BOLD = "C:/Windows/Fonts/msyhbd.ttc"
34+
35+
36+
def load_fonts():
37+
fonts = {}
38+
try:
39+
fonts["display"] = ImageFont.truetype(FONT_CN_BOLD, 68)
40+
fonts["title"] = ImageFont.truetype(FONT_CN_BOLD, 44)
41+
fonts["h2"] = ImageFont.truetype(FONT_CN_BOLD, 34)
42+
fonts["h3"] = ImageFont.truetype(FONT_CN_BOLD, 22)
43+
fonts["body"] = ImageFont.truetype(FONT_CN, 20)
44+
fonts["body-sm"] = ImageFont.truetype(FONT_CN, 17)
45+
fonts["caption"] = ImageFont.truetype(FONT_CN, 15)
46+
fonts["mono"] = ImageFont.truetype(FONT_MONO, 17)
47+
except Exception as e:
48+
print("Font load error:", e)
49+
fonts["display"] = ImageFont.load_default()
50+
fonts["title"] = ImageFont.load_default()
51+
fonts["h2"] = ImageFont.load_default()
52+
fonts["h3"] = ImageFont.load_default()
53+
fonts["body"] = ImageFont.load_default()
54+
fonts["body-sm"] = ImageFont.load_default()
55+
fonts["caption"] = ImageFont.load_default()
56+
fonts["mono"] = ImageFont.load_default()
57+
return fonts
58+
59+
60+
def draw_text(draw, text, pos, font, fill, anchor="lt"):
61+
draw.text(pos, text, font=font, fill=fill, anchor=anchor)
62+
63+
64+
def text_size(draw, text, font):
65+
bbox = draw.textbbox((0, 0), text, font=font)
66+
return bbox[2] - bbox[0], bbox[3] - bbox[1]
67+
68+
69+
def wrap_text(draw, text, font, max_width):
70+
"""Simple greedy word wrap for Chinese/English mixed text."""
71+
words = []
72+
for ch in text:
73+
words.append(ch)
74+
lines = []
75+
current = ""
76+
for word in words:
77+
test = current + word
78+
w, _ = text_size(draw, test, font)
79+
if w <= max_width:
80+
current = test
81+
else:
82+
if current:
83+
lines.append(current)
84+
current = word
85+
if current:
86+
lines.append(current)
87+
return lines
88+
89+
90+
def main():
91+
img = Image.new("RGBA", (WIDTH, HEIGHT), BG_BASE)
92+
draw = ImageDraw.Draw(img)
93+
fonts = load_fonts()
94+
95+
# Nav
96+
nav_h = 72
97+
draw.rectangle((0, 0, WIDTH, nav_h), fill=BG_BASE)
98+
draw.line((0, nav_h, WIDTH, nav_h), fill=BORDER_STRONG, width=1)
99+
100+
# Logo mark (colored quadrants)
101+
logo_x, logo_y = 64, 26
102+
mark_size = 22
103+
gap = 3
104+
cell = (mark_size - gap) // 2
105+
quadrants = ["Elements", "Framework", "UI", "Protocols"]
106+
for idx in range(4):
107+
i = idx % 2
108+
j = idx // 2
109+
x = logo_x + i * (cell + gap)
110+
y = logo_y + j * (cell + gap)
111+
color = ACCENTS[quadrants[idx]]
112+
draw.rectangle((x, y, x + cell, y + cell), outline=color, width=2)
113+
114+
draw_text(draw, "openElement", (logo_x + mark_size + 14, logo_y + 1), fonts["body"], TEXT_PRIMARY)
115+
116+
# Nav links right aligned
117+
nav_links = ["Docs", "Architecture", "Roadmap", "Blog"]
118+
link_spacing = 72
119+
total_links_width = sum(text_size(draw, link, fonts["body-sm"])[0] for link in nav_links) + link_spacing * (len(nav_links) - 1)
120+
nx = WIDTH - 260 - total_links_width
121+
for link in nav_links:
122+
draw_text(draw, link, (nx, 28), fonts["body-sm"], TEXT_SECONDARY)
123+
w, _ = text_size(draw, link, fonts["body-sm"])
124+
nx += w + link_spacing
125+
126+
draw_text(draw, "v0.40.7", (WIDTH - 210, 28), fonts["caption"], TEXT_MUTED)
127+
draw_text(draw, "GitHub", (WIDTH - 130, 28), fonts["body-sm"], TEXT_SECONDARY)
128+
129+
# Hero
130+
y = nav_h + 130
131+
draw_text(draw, "v0.40.7 已发布", (80, y), fonts["caption"], BRAND)
132+
133+
y += 42
134+
headline = "用 JSX 构建 Web Components"
135+
draw_text(draw, headline, (80, y), fonts["display"], TEXT_PRIMARY)
136+
y += 84
137+
draw_text(draw, "无需框架锁定。", (80, y), fonts["display"], TEXT_PRIMARY)
138+
139+
y += 100
140+
sub = "Static-first · DSD 默认渲染 · 单一 VNode 管线 · Preact islands · Vite + Nitro"
141+
draw_text(draw, sub, (80, y), fonts["body"], TEXT_SECONDARY)
142+
143+
y += 70
144+
# Primary button
145+
btn_h = 48
146+
draw.rounded_rectangle((80, y, 220, y + btn_h), radius=8, fill=BRAND)
147+
draw_text(draw, "开始构建", (150, y + 13), fonts["body-sm"], (255, 255, 255), anchor="mt")
148+
# Secondary button
149+
draw.rounded_rectangle((240, y, 420, y + btn_h), radius=8, fill=BG_SURFACE, outline=BORDER_STRONG, width=1)
150+
draw_text(draw, "查看 GitHub", (330, y + 13), fonts["body-sm"], TEXT_PRIMARY, anchor="mt")
151+
152+
y += 100
153+
# Code block
154+
code_x, code_y = 80, y
155+
code_w = 720
156+
code_h = 170
157+
draw.rounded_rectangle((code_x, code_y, code_x + code_w, code_y + code_h), radius=12, fill=BG_CODE, outline=BORDER, width=1)
158+
# Header
159+
draw.rectangle((code_x, code_y, code_x + code_w, code_y + 40), fill=BG_SURFACE)
160+
draw.line((code_x, code_y + 40, code_x + code_w, code_y + 40), fill=BORDER, width=1)
161+
draw.ellipse((code_x + 18, code_y + 14, code_x + 28, code_y + 24), fill=(248, 113, 113))
162+
draw.ellipse((code_x + 36, code_y + 14, code_x + 46, code_y + 24), fill=(251, 191, 36))
163+
draw.ellipse((code_x + 54, code_y + 14, code_x + 64, code_y + 24), fill=(52, 211, 153))
164+
draw_text(draw, "bash", (code_x + code_w - 60, code_y + 11), fonts["caption"], TEXT_MUTED)
165+
# Body
166+
lines = [
167+
("$ ", "deno task create my-app", TEXT_MUTED, TEXT_PRIMARY),
168+
("$ ", "cd my-app && deno task dev", TEXT_MUTED, TEXT_PRIMARY),
169+
("", "Server ready at http://localhost:3000", TEXT_SECONDARY, TEXT_SECONDARY),
170+
]
171+
ly = code_y + 60
172+
for prefix, content, pc, cc in lines:
173+
draw_text(draw, prefix, (code_x + 24, ly), fonts["mono"], pc)
174+
pw, _ = text_size(draw, prefix, fonts["mono"])
175+
draw_text(draw, content, (code_x + 24 + pw, ly), fonts["mono"], cc)
176+
ly += 30
177+
178+
# Four products
179+
y = code_y + code_h + 140
180+
draw_text(draw, "四大产品", (80, y), fonts["caption"], BRAND)
181+
y += 32
182+
draw_text(draw, "一个框架,四层抽象", (80, y), fonts["h2"], TEXT_PRIMARY)
183+
184+
y += 80
185+
cards = [
186+
("Elements", "Shadow/DSD 组件", "用 JSX 编写原生 Web Components,默认 Shadow DOM 渲染。"),
187+
("UI", "open-* 组件库", "基于 Elements 的第一方组件:button、modal、tabs、dropdown。"),
188+
("Framework", "应用框架", "Vite + Nitro 驱动的路由、SSR、API routes、构建管线。"),
189+
("Protocols", "协议边界", "渲染器、路由、islands、signals 的运行时无关契约。"),
190+
]
191+
card_w = 310
192+
card_h = 200
193+
gap_x = 24
194+
start_x = (WIDTH - (4 * card_w + 3 * gap_x)) // 2
195+
for i, (title, label, desc) in enumerate(cards):
196+
cx = start_x + i * (card_w + gap_x)
197+
accent = ACCENTS[title]
198+
draw.rounded_rectangle((cx, y, cx + card_w, y + card_h), radius=12, fill=BG_SURFACE, outline=BORDER, width=1)
199+
# Accent top line
200+
draw.rectangle((cx + 1, y, cx + card_w - 1, y + 4), fill=accent)
201+
draw_text(draw, title, (cx + 22, y + 26), fonts["h3"], TEXT_PRIMARY)
202+
draw_text(draw, label, (cx + 22, y + 58), fonts["caption"], accent)
203+
wrapped = wrap_text(draw, desc, fonts["body-sm"], card_w - 44)
204+
line_y = y + 96
205+
for line in wrapped:
206+
draw_text(draw, line, (cx + 22, line_y), fonts["body-sm"], TEXT_SECONDARY)
207+
line_y += 26
208+
209+
# Why section
210+
y += card_h + 140
211+
draw_text(draw, "为什么选 openElement", (80, y), fonts["caption"], BRAND)
212+
y += 32
213+
draw_text(draw, "一条渲染路径,而不是 N 条", (80, y), fonts["h2"], TEXT_PRIMARY)
214+
215+
y += 80
216+
why = [
217+
("单一渲染器", "VNode 直接输出 DSD 或 DOM,一套事件模型,没有适配器混战。"),
218+
("按需 Hydrate", "Preact islands 只在需要的地方升级,其余页面保持纯静态。"),
219+
("Web 标准", "输出真正的 Web Components,不绑定特定前端框架。"),
220+
]
221+
why_w = 420
222+
why_h = 170
223+
why_gap = 30
224+
why_start = (WIDTH - (3 * why_w + 2 * why_gap)) // 2
225+
for i, (title, desc) in enumerate(why):
226+
wx = why_start + i * (why_w + why_gap)
227+
draw.rounded_rectangle((wx, y, wx + why_w, y + why_h), radius=12, fill=BG_SURFACE, outline=BORDER, width=1)
228+
draw_text(draw, title, (wx + 24, y + 26), fonts["h3"], TEXT_PRIMARY)
229+
wrapped = wrap_text(draw, desc, fonts["body-sm"], why_w - 48)
230+
line_y = y + 64
231+
for line in wrapped:
232+
draw_text(draw, line, (wx + 24, line_y), fonts["body-sm"], TEXT_SECONDARY)
233+
line_y += 26
234+
235+
# Footer
236+
footer_y = HEIGHT - 150
237+
draw.rectangle((0, footer_y, WIDTH, HEIGHT), fill=BG_SURFACE)
238+
draw.line((0, footer_y, WIDTH, footer_y), fill=BORDER_STRONG, width=1)
239+
draw_text(draw, "openElement", (80, footer_y + 40), fonts["h3"], TEXT_PRIMARY)
240+
draw_text(draw, "JSX-first Web Components platform.", (80, footer_y + 76), fonts["body-sm"], TEXT_SECONDARY)
241+
footer_links = ["Docs", "Architecture", "Roadmap", "Blog", "GitHub"]
242+
fx = WIDTH - 80
243+
for link in reversed(footer_links):
244+
w, _ = text_size(draw, link, fonts["body-sm"])
245+
fx -= w
246+
draw_text(draw, link, (fx, footer_y + 44), fonts["body-sm"], TEXT_SECONDARY)
247+
fx -= 40
248+
draw_text(draw, "© 2026 openElement contributors · MIT", (80, footer_y + 116), fonts["caption"], TEXT_MUTED)
249+
250+
out_path = os.path.join(os.path.dirname(__file__), "homepage-mockup.png")
251+
img.save(out_path)
252+
print("Saved:", out_path)
253+
254+
255+
if __name__ == "__main__":
256+
main()

docs/design/homepage-mockup.png

169 KB
Loading

0 commit comments

Comments
 (0)