forked from
stevedylan.dev/sequoia
A CLI for publishing standard.site documents to ATProto
1import { existsSync, readFileSync } from "fs";
2
3interface ThemeVars {
4 fgColor: string;
5 bgColor: string;
6 accentColor: string;
7 borderColor: string;
8 errorColor: string;
9 borderRadius: string;
10 fontFamily: string;
11 darkBgColor: string;
12 darkFgColor: string;
13 darkBorderColor: string;
14 darkErrorColor: string;
15}
16
17function getThemeVars(): ThemeVars {
18 return {
19 fgColor: process.env.THEME_FG_COLOR || "#2C2C2C",
20 bgColor: process.env.THEME_BG_COLOR || "#F5F3EF",
21 accentColor: process.env.THEME_ACCENT_COLOR || "#3A5A40",
22 borderColor: process.env.THEME_BORDER_COLOR || "#D5D1C8",
23 errorColor: process.env.THEME_ERROR_COLOR || "#8B3A3A",
24 borderRadius: process.env.THEME_BORDER_RADIUS || "6px",
25 fontFamily: process.env.THEME_FONT_FAMILY || "system-ui, sans-serif",
26 darkBgColor: process.env.THEME_DARK_BG_COLOR || "#1A1A1A",
27 darkFgColor: process.env.THEME_DARK_FG_COLOR || "#E5E5E5",
28 darkBorderColor: process.env.THEME_DARK_BORDER_COLOR || "#3A3A3A",
29 darkErrorColor: process.env.THEME_DARK_ERROR_COLOR || "#E57373",
30 };
31}
32
33function getCustomCss(): string {
34 const cssPath = process.env.THEME_CSS_PATH;
35 if (!cssPath) return "";
36 try {
37 if (existsSync(cssPath)) {
38 return readFileSync(cssPath, "utf-8");
39 }
40 } catch {
41 console.warn(`Failed to read custom CSS file: ${cssPath}`);
42 }
43 return "";
44}
45
46export function generateStyleBlock(): string {
47 const t = getThemeVars();
48 const customCss = getCustomCss();
49
50 return `<style>
51 :root {
52 --sequoia-fg-color: ${t.fgColor};
53 --sequoia-bg-color: ${t.bgColor};
54 --sequoia-accent-color: ${t.accentColor};
55 --sequoia-border-color: ${t.borderColor};
56 --sequoia-error-color: ${t.errorColor};
57 --sequoia-border-radius: ${t.borderRadius};
58 --sequoia-font-family: ${t.fontFamily};
59 }
60
61 @media (prefers-color-scheme: dark) {
62 :root {
63 --sequoia-fg-color: ${t.darkFgColor};
64 --sequoia-bg-color: ${t.darkBgColor};
65 --sequoia-border-color: ${t.darkBorderColor};
66 --sequoia-error-color: ${t.darkErrorColor};
67 }
68 }
69
70 * { box-sizing: border-box; margin: 0; padding: 0; }
71
72 body {
73 font-family: var(--sequoia-font-family);
74 background: var(--sequoia-bg-color);
75 color: var(--sequoia-fg-color);
76 line-height: 1.6;
77 }
78
79 .page-container {
80 max-width: 480px;
81 margin: 4rem auto;
82 padding: 0 1.25rem;
83 }
84
85 h1 {
86 font-size: 1.75rem;
87 font-weight: 700;
88 margin-bottom: 0.75rem;
89 }
90
91 p { margin-bottom: 1rem; }
92
93 a {
94 color: var(--sequoia-accent-color);
95 text-decoration: underline;
96 }
97
98 a:hover { text-decoration: none; }
99
100 form { display: flex; flex-direction: column; }
101
102 input[type="text"] {
103 padding: 0.5rem 0.75rem;
104 border: 1px solid var(--sequoia-border-color);
105 border-radius: var(--sequoia-border-radius);
106 margin-bottom: 1.25rem;
107 width: 100%;
108 font-size: 1rem;
109 font-family: inherit;
110 background: var(--sequoia-bg-color);
111 color: var(--sequoia-fg-color);
112 }
113
114 input[type="text"]:focus {
115 border-color: var(--sequoia-accent-color);
116 outline: 2px solid var(--sequoia-accent-color);
117 outline-offset: 2px;
118 }
119
120 button {
121 padding: 0.625rem 1.25rem;
122 background: var(--sequoia-accent-color);
123 color: #fff;
124 border: none;
125 border-radius: var(--sequoia-border-radius);
126 font-size: 1rem;
127 font-family: inherit;
128 font-weight: 600;
129 cursor: pointer;
130 transition: opacity 0.15s;
131 }
132
133 button:hover { opacity: 0.9; }
134
135 button:focus-visible {
136 outline: 2px solid var(--sequoia-accent-color);
137 outline-offset: 2px;
138 }
139
140 table {
141 width: 100%;
142 border-collapse: collapse;
143 table-layout: fixed;
144 margin-top: 1rem;
145 }
146
147 td {
148 padding: 0.5rem 0.75rem;
149 border-bottom: 1px solid var(--sequoia-border-color);
150 vertical-align: top;
151 }
152
153 td:first-child {
154 width: 7rem;
155 font-weight: 600;
156 }
157
158 td:last-child { overflow: hidden; }
159
160 td code {
161 font-size: 0.85rem;
162 word-break: break-all;
163 }
164
165 td div {
166 overflow-x: auto;
167 white-space: nowrap;
168 }
169
170 .error { color: var(--sequoia-error-color); }
171 ${customCss ? `\n /* Custom CSS */\n ${customCss}` : ""}
172 </style>`;
173}
174
175export function page(body: string, headExtra = ""): string {
176 return `<!DOCTYPE html>
177<html lang="en">
178<head>
179 <meta charset="UTF-8" />
180 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
181 <title>Sequoia · Subscribe</title>
182 ${generateStyleBlock()}
183 ${headExtra}
184</head>
185<body>
186 <div class="page-container">
187 ${body}
188 </div>
189</body>
190</html>`;
191}
192
193export function escapeHtml(text: string): string {
194 return text
195 .replace(/&/g, "&")
196 .replace(/</g, "<")
197 .replace(/>/g, ">")
198 .replace(/"/g, """);
199}