@mindstudio-ai/remy 0.1.34 → 0.1.35
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/dist/headless.js +578 -393
- package/dist/index.js +652 -385
- package/dist/prompt/sources/llms.txt +1618 -0
- package/dist/prompt/static/instructions.md +1 -1
- package/dist/prompt/static/team.md +1 -1
- package/dist/subagents/.notes-background-agents.md +60 -48
- package/dist/subagents/browserAutomation/prompt.md +14 -11
- package/dist/subagents/designExpert/data/sources/dev/index.html +901 -0
- package/dist/subagents/designExpert/data/sources/dev/serve.mjs +244 -0
- package/dist/subagents/designExpert/data/sources/dev/specimens-fonts.html +126 -0
- package/dist/subagents/designExpert/data/sources/dev/specimens-pairings.html +114 -0
- package/dist/subagents/designExpert/data/{fonts.json → sources/fonts.json} +0 -97
- package/dist/subagents/designExpert/data/sources/inspiration.json +392 -0
- package/dist/subagents/designExpert/prompt.md +36 -12
- package/dist/subagents/designExpert/prompts/animation.md +14 -6
- package/dist/subagents/designExpert/prompts/color.md +25 -5
- package/dist/subagents/designExpert/prompts/{icons.md → components.md} +17 -5
- package/dist/subagents/designExpert/prompts/frontend-design-notes.md +17 -122
- package/dist/subagents/designExpert/prompts/identity.md +15 -61
- package/dist/subagents/designExpert/prompts/images.md +35 -10
- package/dist/subagents/designExpert/prompts/layout.md +14 -9
- package/dist/subagents/designExpert/prompts/typography.md +39 -0
- package/package.json +2 -2
- package/dist/actions/buildFromInitialSpec.md +0 -15
- package/dist/actions/publish.md +0 -12
- package/dist/actions/sync.md +0 -19
- package/dist/compiled/README.md +0 -100
- package/dist/compiled/auth.md +0 -77
- package/dist/compiled/design.md +0 -251
- package/dist/compiled/dev-and-deploy.md +0 -69
- package/dist/compiled/interfaces.md +0 -238
- package/dist/compiled/manifest.md +0 -107
- package/dist/compiled/media-cdn.md +0 -51
- package/dist/compiled/methods.md +0 -225
- package/dist/compiled/msfm.md +0 -222
- package/dist/compiled/platform.md +0 -105
- package/dist/compiled/scenarios.md +0 -103
- package/dist/compiled/sdk-actions.md +0 -146
- package/dist/compiled/tables.md +0 -263
- package/dist/static/authoring.md +0 -101
- package/dist/static/coding.md +0 -29
- package/dist/static/identity.md +0 -1
- package/dist/static/instructions.md +0 -31
- package/dist/static/intake.md +0 -44
- package/dist/static/lsp.md +0 -4
- package/dist/static/projectContext.ts +0 -160
- package/dist/static/team.md +0 -39
- package/dist/subagents/designExpert/data/inspiration.json +0 -392
- package/dist/subagents/designExpert/prompts/instructions.md +0 -18
- /package/dist/subagents/designExpert/data/{compile-font-descriptions.sh → sources/compile-font-descriptions.sh} +0 -0
- /package/dist/subagents/designExpert/data/{compile-inspiration.sh → sources/compile-inspiration.sh} +0 -0
- /package/dist/subagents/designExpert/data/{inspiration.raw.json → sources/inspiration.raw.json} +0 -0
- /package/dist/subagents/designExpert/{prompts/tool-prompts → data/sources/prompts}/design-analysis.md +0 -0
- /package/dist/subagents/designExpert/{prompts/tool-prompts → data/sources/prompts}/font-analysis.md +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight dev server for visualizing and managing design expert data.
|
|
3
|
+
*
|
|
4
|
+
* Zero dependencies — uses Node's built-in http module.
|
|
5
|
+
* Reads/writes fonts.json and inspiration.json in the parent directory.
|
|
6
|
+
*
|
|
7
|
+
* Usage: node src/subagents/designExpert/data/dev/serve.mjs
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createServer } from 'node:http';
|
|
11
|
+
import { readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
12
|
+
import { join, dirname } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { execSync } from 'node:child_process';
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const dataDir = join(__dirname, '..');
|
|
18
|
+
const fontsPath = join(dataDir, 'fonts.json');
|
|
19
|
+
const inspirationPath = join(dataDir, 'inspiration.json');
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// JSON helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
function readJson(path) {
|
|
26
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function writeJson(path, data) {
|
|
30
|
+
const tmp = path + '.tmp';
|
|
31
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
|
|
32
|
+
renameSync(tmp, path);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseBody(req) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
let body = '';
|
|
38
|
+
req.on('data', (chunk) => (body += chunk));
|
|
39
|
+
req.on('end', () => {
|
|
40
|
+
try {
|
|
41
|
+
resolve(JSON.parse(body));
|
|
42
|
+
} catch {
|
|
43
|
+
reject(new Error('Invalid JSON'));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Route helpers
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
function json(res, data, status = 200) {
|
|
54
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
55
|
+
res.end(JSON.stringify(data));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function notFound(res) {
|
|
59
|
+
res.writeHead(404);
|
|
60
|
+
res.end('Not found');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function error(res, msg, status = 400) {
|
|
64
|
+
json(res, { error: msg }, status);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Server
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
const server = createServer(async (req, res) => {
|
|
72
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
73
|
+
const path = url.pathname;
|
|
74
|
+
const method = req.method;
|
|
75
|
+
|
|
76
|
+
// CORS for dev
|
|
77
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
78
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
79
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
80
|
+
if (method === 'OPTIONS') {
|
|
81
|
+
res.writeHead(204);
|
|
82
|
+
res.end();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
// Serve HTML
|
|
88
|
+
if (path === '/' && method === 'GET') {
|
|
89
|
+
const html = readFileSync(join(__dirname, 'index.html'), 'utf-8');
|
|
90
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
91
|
+
res.end(html);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (path === '/specimens/fonts' && method === 'GET') {
|
|
96
|
+
const html = readFileSync(join(__dirname, 'specimens-fonts.html'), 'utf-8');
|
|
97
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
98
|
+
res.end(html);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (path === '/specimens/pairings' && method === 'GET') {
|
|
103
|
+
const html = readFileSync(join(__dirname, 'specimens-pairings.html'), 'utf-8');
|
|
104
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
105
|
+
res.end(html);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --- Fonts API ---
|
|
110
|
+
|
|
111
|
+
if (path === '/api/fonts' && method === 'GET') {
|
|
112
|
+
return json(res, readJson(fontsPath));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (path === '/api/fonts' && method === 'POST') {
|
|
116
|
+
const font = await parseBody(req);
|
|
117
|
+
const data = readJson(fontsPath);
|
|
118
|
+
if (data.fonts.some((f) => f.slug === font.slug)) {
|
|
119
|
+
return error(res, `Font with slug "${font.slug}" already exists`);
|
|
120
|
+
}
|
|
121
|
+
data.fonts.push(font);
|
|
122
|
+
writeJson(fontsPath, data);
|
|
123
|
+
return json(res, { ok: true, count: data.fonts.length }, 201);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const fontDeleteMatch = path.match(/^\/api\/fonts\/(.+)$/);
|
|
127
|
+
if (fontDeleteMatch && method === 'DELETE') {
|
|
128
|
+
const slug = decodeURIComponent(fontDeleteMatch[1]);
|
|
129
|
+
const data = readJson(fontsPath);
|
|
130
|
+
const before = data.fonts.length;
|
|
131
|
+
data.fonts = data.fonts.filter((f) => f.slug !== slug);
|
|
132
|
+
// Also remove pairings that reference this font
|
|
133
|
+
data.pairings = data.pairings.filter(
|
|
134
|
+
(p) => p.heading.slug !== slug && p.body.slug !== slug,
|
|
135
|
+
);
|
|
136
|
+
if (data.fonts.length === before) {
|
|
137
|
+
return error(res, `Font "${slug}" not found`, 404);
|
|
138
|
+
}
|
|
139
|
+
writeJson(fontsPath, data);
|
|
140
|
+
return json(res, { ok: true, count: data.fonts.length });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- Pairings API ---
|
|
144
|
+
|
|
145
|
+
if (path === '/api/pairings' && method === 'POST') {
|
|
146
|
+
const pairing = await parseBody(req);
|
|
147
|
+
const data = readJson(fontsPath);
|
|
148
|
+
data.pairings.push(pairing);
|
|
149
|
+
writeJson(fontsPath, data);
|
|
150
|
+
return json(res, { ok: true, count: data.pairings.length }, 201);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const pairingDeleteMatch = path.match(/^\/api\/pairings\/(\d+)$/);
|
|
154
|
+
if (pairingDeleteMatch && method === 'DELETE') {
|
|
155
|
+
const index = parseInt(pairingDeleteMatch[1], 10);
|
|
156
|
+
const data = readJson(fontsPath);
|
|
157
|
+
if (index < 0 || index >= data.pairings.length) {
|
|
158
|
+
return error(res, 'Pairing index out of range', 404);
|
|
159
|
+
}
|
|
160
|
+
data.pairings.splice(index, 1);
|
|
161
|
+
writeJson(fontsPath, data);
|
|
162
|
+
return json(res, { ok: true, count: data.pairings.length });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- Inspiration API ---
|
|
166
|
+
|
|
167
|
+
if (path === '/api/inspiration' && method === 'GET') {
|
|
168
|
+
return json(res, readJson(inspirationPath));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (path === '/api/inspiration' && method === 'POST') {
|
|
172
|
+
const entry = await parseBody(req);
|
|
173
|
+
const data = readJson(inspirationPath);
|
|
174
|
+
data.images.push(entry);
|
|
175
|
+
writeJson(inspirationPath, data);
|
|
176
|
+
return json(res, { ok: true, count: data.images.length }, 201);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const inspirationDeleteMatch = path.match(/^\/api\/inspiration\/(\d+)$/);
|
|
180
|
+
if (inspirationDeleteMatch && method === 'DELETE') {
|
|
181
|
+
const index = parseInt(inspirationDeleteMatch[1], 10);
|
|
182
|
+
const data = readJson(inspirationPath);
|
|
183
|
+
if (index < 0 || index >= data.images.length) {
|
|
184
|
+
return error(res, 'Image index out of range', 404);
|
|
185
|
+
}
|
|
186
|
+
data.images.splice(index, 1);
|
|
187
|
+
writeJson(inspirationPath, data);
|
|
188
|
+
return json(res, { ok: true, count: data.images.length });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (path === '/api/inspiration/analyze' && method === 'POST') {
|
|
192
|
+
const { url } = await parseBody(req);
|
|
193
|
+
if (!url) return error(res, 'url is required');
|
|
194
|
+
|
|
195
|
+
const promptFile = join(dataDir, '..', 'prompts', 'tool-prompts', 'design-analysis.md');
|
|
196
|
+
const prompt = readFileSync(promptFile, 'utf-8').trim();
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const result = execSync(
|
|
200
|
+
`mindstudio analyze-image --prompt ${JSON.stringify(prompt)} --image-url ${JSON.stringify(url)} --output-key analysis --no-meta`,
|
|
201
|
+
{ encoding: 'utf-8', timeout: 120000 },
|
|
202
|
+
).trim();
|
|
203
|
+
return json(res, { url, analysis: result });
|
|
204
|
+
} catch (err) {
|
|
205
|
+
return error(res, `Analysis failed: ${err.message}`, 500);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (path === '/api/inspiration/dedup' && method === 'POST') {
|
|
210
|
+
const data = readJson(inspirationPath);
|
|
211
|
+
const seen = new Set();
|
|
212
|
+
const deduped = [];
|
|
213
|
+
for (const img of data.images) {
|
|
214
|
+
if (!seen.has(img.url)) {
|
|
215
|
+
seen.add(img.url);
|
|
216
|
+
deduped.push(img);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const removed = data.images.length - deduped.length;
|
|
220
|
+
data.images = deduped;
|
|
221
|
+
writeJson(inspirationPath, data);
|
|
222
|
+
return json(res, { ok: true, removed, count: deduped.length });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
notFound(res);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
error(res, err.message, 500);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const PORT = parseInt(process.env.PORT || '3333', 10);
|
|
232
|
+
server.listen(PORT, () => {
|
|
233
|
+
console.log(`Design data dev tool: http://localhost:${PORT}`);
|
|
234
|
+
// Try to open in browser
|
|
235
|
+
try {
|
|
236
|
+
const cmd =
|
|
237
|
+
process.platform === 'darwin'
|
|
238
|
+
? 'open'
|
|
239
|
+
: process.platform === 'win32'
|
|
240
|
+
? 'start'
|
|
241
|
+
: 'xdg-open';
|
|
242
|
+
execSync(`${cmd} http://localhost:${PORT}`, { stdio: 'ignore' });
|
|
243
|
+
} catch {}
|
|
244
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=1440" />
|
|
6
|
+
<title>Font Specimens</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
11
|
+
background: #fff;
|
|
12
|
+
color: #000;
|
|
13
|
+
width: 1440px;
|
|
14
|
+
}
|
|
15
|
+
.row {
|
|
16
|
+
height: 200px;
|
|
17
|
+
width: 1440px;
|
|
18
|
+
border-bottom: 1px solid #ddd;
|
|
19
|
+
padding: 24px 40px;
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
}
|
|
25
|
+
.name {
|
|
26
|
+
font-size: 12px;
|
|
27
|
+
font-weight: 600;
|
|
28
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
29
|
+
color: #999;
|
|
30
|
+
margin-bottom: 12px;
|
|
31
|
+
text-transform: uppercase;
|
|
32
|
+
letter-spacing: 0.05em;
|
|
33
|
+
}
|
|
34
|
+
.preview-large {
|
|
35
|
+
font-size: 42px;
|
|
36
|
+
line-height: 1.1;
|
|
37
|
+
margin-bottom: 8px;
|
|
38
|
+
white-space: nowrap;
|
|
39
|
+
overflow: hidden;
|
|
40
|
+
text-overflow: ellipsis;
|
|
41
|
+
}
|
|
42
|
+
.preview-medium {
|
|
43
|
+
font-size: 24px;
|
|
44
|
+
line-height: 1.3;
|
|
45
|
+
color: #333;
|
|
46
|
+
margin-bottom: 8px;
|
|
47
|
+
white-space: nowrap;
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
text-overflow: ellipsis;
|
|
50
|
+
}
|
|
51
|
+
.preview-small {
|
|
52
|
+
font-size: 15px;
|
|
53
|
+
line-height: 1.5;
|
|
54
|
+
color: #666;
|
|
55
|
+
white-space: nowrap;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
text-overflow: ellipsis;
|
|
58
|
+
}
|
|
59
|
+
</style>
|
|
60
|
+
</head>
|
|
61
|
+
<body>
|
|
62
|
+
<script>
|
|
63
|
+
const CSS_URL_PATTERN = 'https://api.fontshare.com/v2/css?f[]={slug}@{weights}&display=swap';
|
|
64
|
+
|
|
65
|
+
function loadFont(font) {
|
|
66
|
+
let cssUrl;
|
|
67
|
+
if (font.source === 'fontshare') {
|
|
68
|
+
cssUrl = CSS_URL_PATTERN
|
|
69
|
+
.replace('{slug}', font.slug)
|
|
70
|
+
.replace('{weights}', font.weights.join(','));
|
|
71
|
+
} else if (font.cssUrl) {
|
|
72
|
+
cssUrl = font.cssUrl;
|
|
73
|
+
} else {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const link = document.createElement('link');
|
|
77
|
+
link.rel = 'stylesheet';
|
|
78
|
+
link.href = cssUrl;
|
|
79
|
+
document.head.appendChild(link);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function init() {
|
|
83
|
+
const params = new URLSearchParams(window.location.search);
|
|
84
|
+
const page = parseInt(params.get('page') || '1', 10);
|
|
85
|
+
const perPage = 40;
|
|
86
|
+
|
|
87
|
+
const res = await fetch('/api/fonts');
|
|
88
|
+
const data = await res.json();
|
|
89
|
+
const allFonts = [...data.fonts].sort((a, b) => a.name.localeCompare(b.name));
|
|
90
|
+
const totalPages = Math.ceil(allFonts.length / perPage);
|
|
91
|
+
const fonts = allFonts.slice((page - 1) * perPage, page * perPage);
|
|
92
|
+
|
|
93
|
+
document.title = `Font Specimens — Page ${page}/${totalPages}`;
|
|
94
|
+
|
|
95
|
+
fonts.forEach(f => loadFont(f));
|
|
96
|
+
|
|
97
|
+
// Wait for fonts to load
|
|
98
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
99
|
+
|
|
100
|
+
fonts.forEach(font => {
|
|
101
|
+
const midWeight = font.weights[Math.floor(font.weights.length / 2)] || 400;
|
|
102
|
+
const lightWeight = font.weights[0] || 400;
|
|
103
|
+
const heavyWeight = font.weights[font.weights.length - 1] || 400;
|
|
104
|
+
|
|
105
|
+
const row = document.createElement('div');
|
|
106
|
+
row.className = 'row';
|
|
107
|
+
row.innerHTML = `
|
|
108
|
+
<div class="name">${font.name} · ${font.category} · ${font.source}</div>
|
|
109
|
+
<div class="preview-large" style="font-family: '${font.name}'; font-weight: ${heavyWeight}">
|
|
110
|
+
The quick brown fox jumps over the lazy dog
|
|
111
|
+
</div>
|
|
112
|
+
<div class="preview-medium" style="font-family: '${font.name}'; font-weight: ${midWeight}">
|
|
113
|
+
Pack my box with five dozen liquor jugs — 0123456789
|
|
114
|
+
</div>
|
|
115
|
+
<div class="preview-small" style="font-family: '${font.name}'; font-weight: ${lightWeight}">
|
|
116
|
+
A wizard's job is to vex chumps quickly in fog. The five boxing wizards jump quickly. How vexingly quick daft zebras jump!
|
|
117
|
+
</div>
|
|
118
|
+
`;
|
|
119
|
+
document.body.appendChild(row);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
init();
|
|
124
|
+
</script>
|
|
125
|
+
</body>
|
|
126
|
+
</html>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=1440" />
|
|
6
|
+
<title>Pairing Specimens</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
11
|
+
background: #fff;
|
|
12
|
+
color: #000;
|
|
13
|
+
width: 1440px;
|
|
14
|
+
}
|
|
15
|
+
.row {
|
|
16
|
+
height: 240px;
|
|
17
|
+
width: 1440px;
|
|
18
|
+
border-bottom: 1px solid #ddd;
|
|
19
|
+
padding: 24px 40px;
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
}
|
|
25
|
+
.meta {
|
|
26
|
+
font-size: 12px;
|
|
27
|
+
font-weight: 600;
|
|
28
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
29
|
+
color: #999;
|
|
30
|
+
margin-bottom: 16px;
|
|
31
|
+
text-transform: uppercase;
|
|
32
|
+
letter-spacing: 0.05em;
|
|
33
|
+
}
|
|
34
|
+
.heading {
|
|
35
|
+
font-size: 48px;
|
|
36
|
+
line-height: 1.1;
|
|
37
|
+
margin-bottom: 12px;
|
|
38
|
+
}
|
|
39
|
+
.body {
|
|
40
|
+
font-size: 17px;
|
|
41
|
+
line-height: 1.6;
|
|
42
|
+
color: #444;
|
|
43
|
+
max-width: 800px;
|
|
44
|
+
}
|
|
45
|
+
</style>
|
|
46
|
+
</head>
|
|
47
|
+
<body>
|
|
48
|
+
<script>
|
|
49
|
+
const CSS_URL_PATTERN = 'https://api.fontshare.com/v2/css?f[]={slug}@{weights}&display=swap';
|
|
50
|
+
const loaded = new Set();
|
|
51
|
+
|
|
52
|
+
function loadFontBySlug(slug, fonts) {
|
|
53
|
+
if (loaded.has(slug)) return;
|
|
54
|
+
loaded.add(slug);
|
|
55
|
+
const font = fonts.find(f => f.slug === slug);
|
|
56
|
+
if (!font) return;
|
|
57
|
+
let cssUrl;
|
|
58
|
+
if (font.source === 'fontshare') {
|
|
59
|
+
cssUrl = CSS_URL_PATTERN
|
|
60
|
+
.replace('{slug}', font.slug)
|
|
61
|
+
.replace('{weights}', font.weights.join(','));
|
|
62
|
+
} else if (font.cssUrl) {
|
|
63
|
+
cssUrl = font.cssUrl;
|
|
64
|
+
} else {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const link = document.createElement('link');
|
|
68
|
+
link.rel = 'stylesheet';
|
|
69
|
+
link.href = cssUrl;
|
|
70
|
+
document.head.appendChild(link);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function init() {
|
|
74
|
+
const params = new URLSearchParams(window.location.search);
|
|
75
|
+
const page = parseInt(params.get('page') || '1', 10);
|
|
76
|
+
const perPage = 33;
|
|
77
|
+
|
|
78
|
+
const res = await fetch('/api/fonts');
|
|
79
|
+
const data = await res.json();
|
|
80
|
+
const fonts = data.fonts;
|
|
81
|
+
const allPairings = data.pairings;
|
|
82
|
+
const totalPages = Math.ceil(allPairings.length / perPage);
|
|
83
|
+
const pairings = allPairings.slice((page - 1) * perPage, page * perPage);
|
|
84
|
+
|
|
85
|
+
document.title = `Pairing Specimens — Page ${page}/${totalPages}`;
|
|
86
|
+
|
|
87
|
+
pairings.forEach(p => {
|
|
88
|
+
loadFontBySlug(p.heading.slug, fonts);
|
|
89
|
+
loadFontBySlug(p.body.slug, fonts);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Wait for fonts to load
|
|
93
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
94
|
+
|
|
95
|
+
pairings.forEach(p => {
|
|
96
|
+
const row = document.createElement('div');
|
|
97
|
+
row.className = 'row';
|
|
98
|
+
row.innerHTML = `
|
|
99
|
+
<div class="meta">${p.heading.font} (${p.heading.weight}) + ${p.body.font} (${p.body.weight})</div>
|
|
100
|
+
<div class="heading" style="font-family: '${p.heading.font}'; font-weight: ${p.heading.weight}">
|
|
101
|
+
The quick brown fox jumps over the lazy dog
|
|
102
|
+
</div>
|
|
103
|
+
<div class="body" style="font-family: '${p.body.font}'; font-weight: ${p.body.weight}">
|
|
104
|
+
Good design is as little design as possible. Less, but better — because it concentrates on the essential aspects, and the products are not burdened with non-essentials. Back to purity, back to simplicity.
|
|
105
|
+
</div>
|
|
106
|
+
`;
|
|
107
|
+
document.body.appendChild(row);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
init();
|
|
112
|
+
</script>
|
|
113
|
+
</body>
|
|
114
|
+
</html>
|
|
@@ -497,41 +497,6 @@
|
|
|
497
497
|
"source": "fontshare",
|
|
498
498
|
"description": "This sans-serif displays monolinear stroke weight with no stress angle and a geometric construction evident in the circular bowls of letters like 'o' and 'd'. The typeface features a high x-height relative to cap height, regular width proportions, and moderately open apertures in characters such as 'c', 'e', and 'a'. Terminals are primarily flat-cut, the 'g' uses a double-story form, and the overall structure shows rationalized geometric forms with subtle optical corrections rather than pure geometric circles."
|
|
499
499
|
},
|
|
500
|
-
{
|
|
501
|
-
"name": "Outfit",
|
|
502
|
-
"slug": "outfit",
|
|
503
|
-
"category": "Sans",
|
|
504
|
-
"variable": true,
|
|
505
|
-
"weights": [
|
|
506
|
-
100,
|
|
507
|
-
200,
|
|
508
|
-
300,
|
|
509
|
-
400,
|
|
510
|
-
500,
|
|
511
|
-
600,
|
|
512
|
-
700,
|
|
513
|
-
800,
|
|
514
|
-
900
|
|
515
|
-
],
|
|
516
|
-
"italics": false,
|
|
517
|
-
"source": "fontshare",
|
|
518
|
-
"description": "This grotesque sans-serif displays monolinear stroke weight with no stress angle and flat-cut terminals throughout. The typeface features a high x-height relative to cap height, regular width proportions, and moderately open apertures in letters like 'e' and 'a'. Notable characteristics include a single-story 'a', geometric construction with slightly squared curves, and consistent stroke endings that create a sturdy, utilitarian appearance."
|
|
519
|
-
},
|
|
520
|
-
{
|
|
521
|
-
"name": "Lora",
|
|
522
|
-
"slug": "lora",
|
|
523
|
-
"category": "Serif",
|
|
524
|
-
"variable": true,
|
|
525
|
-
"weights": [
|
|
526
|
-
400,
|
|
527
|
-
500,
|
|
528
|
-
600,
|
|
529
|
-
700
|
|
530
|
-
],
|
|
531
|
-
"italics": true,
|
|
532
|
-
"source": "fontshare",
|
|
533
|
-
"description": "This serif typeface exhibits moderate stroke contrast with a vertical stress and features thick, unbracketed slab serifs with squared terminals. The construction shows a high x-height relative to cap height, regular width proportions, and moderately open apertures in letters like 'e' and 'a'. Notable characteristics include a double-story 'g', robust slab terminals on ascenders and descenders, and a rational, geometric construction typical of Egyptian or Clarendon-style serifs."
|
|
534
|
-
},
|
|
535
500
|
{
|
|
536
501
|
"name": "Plus Jakarta Sans",
|
|
537
502
|
"slug": "plus-jakarta-sans",
|
|
@@ -841,22 +806,6 @@
|
|
|
841
806
|
"source": "fontshare",
|
|
842
807
|
"description": "This sans-serif typeface exhibits monolinear stroke weight with no stress angle and a geometric construction characterized by circular bowls and consistent proportions. The apertures are moderately open in letters like 'c' and 'e', while terminals are flat-cut and perpendicular to stroke direction. The x-height is relatively high compared to the cap height, and the overall width is regular with a double-story 'g' and single-story 'a', typical of geometric sans-serifs with rationalized, optically-corrected forms."
|
|
843
808
|
},
|
|
844
|
-
{
|
|
845
|
-
"name": "Space Grotesk",
|
|
846
|
-
"slug": "space-grotesk",
|
|
847
|
-
"category": "Sans",
|
|
848
|
-
"variable": true,
|
|
849
|
-
"weights": [
|
|
850
|
-
300,
|
|
851
|
-
400,
|
|
852
|
-
500,
|
|
853
|
-
600,
|
|
854
|
-
700
|
|
855
|
-
],
|
|
856
|
-
"italics": false,
|
|
857
|
-
"source": "fontshare",
|
|
858
|
-
"description": "This grotesque sans-serif displays monolinear stroke weight with no stress angle and flat-cut terminals throughout. The typeface features a high x-height relative to cap height, moderate to open apertures in letters like 'c', 'e', and 'a', and regular width proportions. Notable glyphs include a single-story 'a' and 'g', with the construction showing rationalized, geometric tendencies typical of neo-grotesque design, particularly visible in the circular bowls and optically-corrected curves."
|
|
859
|
-
},
|
|
860
809
|
{
|
|
861
810
|
"name": "Pramukh Rounded",
|
|
862
811
|
"slug": "pramukh-rounded",
|
|
@@ -1344,28 +1293,6 @@
|
|
|
1344
1293
|
"cssUrl": "https://fonts.cdnfonts.com/css/reglo",
|
|
1345
1294
|
"description": "This sans-serif typeface exhibits monolinear stroke weight with no stress angle and flat-cut terminals throughout. The letterforms display a condensed width with a high x-height relative to the cap height, and apertures in c, e, and s are moderately closed. The construction is grotesque with distinctive features including a single-story 'a', closed aperture in the 'g', and compact proportions that emphasize vertical compression over horizontal space."
|
|
1346
1295
|
},
|
|
1347
|
-
{
|
|
1348
|
-
"name": "Poppins",
|
|
1349
|
-
"slug": "poppins",
|
|
1350
|
-
"category": "Sans",
|
|
1351
|
-
"source": "google-fonts",
|
|
1352
|
-
"googleFontsFamily": "Poppins",
|
|
1353
|
-
"weights": [
|
|
1354
|
-
100,
|
|
1355
|
-
200,
|
|
1356
|
-
300,
|
|
1357
|
-
400,
|
|
1358
|
-
500,
|
|
1359
|
-
600,
|
|
1360
|
-
700,
|
|
1361
|
-
800,
|
|
1362
|
-
900
|
|
1363
|
-
],
|
|
1364
|
-
"italics": true,
|
|
1365
|
-
"cssUrl": "https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800;900&display=swap",
|
|
1366
|
-
"variable": true,
|
|
1367
|
-
"description": "This sans-serif typeface exhibits monolinear stroke weight with no stress angle and a geometric construction characterized by circular bowls and optically-corrected curves. The letterforms feature closed apertures in characters like 'e' and 'a', with a single-story 'a' and 'g', and terminals are flat-cut perpendicular to stroke direction. The x-height is high relative to cap height, proportions are regular width, and distinctive features include perfectly circular dots on 'i' and 'j', and uniform stroke terminals throughout."
|
|
1368
|
-
},
|
|
1369
1296
|
{
|
|
1370
1297
|
"name": "Oswald",
|
|
1371
1298
|
"slug": "oswald",
|
|
@@ -1915,18 +1842,6 @@
|
|
|
1915
1842
|
"weight": 400
|
|
1916
1843
|
}
|
|
1917
1844
|
},
|
|
1918
|
-
{
|
|
1919
|
-
"heading": {
|
|
1920
|
-
"font": "Melodrama",
|
|
1921
|
-
"slug": "melodrama",
|
|
1922
|
-
"weight": 500
|
|
1923
|
-
},
|
|
1924
|
-
"body": {
|
|
1925
|
-
"font": "Nunito",
|
|
1926
|
-
"slug": "nunito",
|
|
1927
|
-
"weight": 400
|
|
1928
|
-
}
|
|
1929
|
-
},
|
|
1930
1845
|
{
|
|
1931
1846
|
"heading": {
|
|
1932
1847
|
"font": "Tanker",
|
|
@@ -2419,18 +2334,6 @@
|
|
|
2419
2334
|
"weight": 400
|
|
2420
2335
|
}
|
|
2421
2336
|
},
|
|
2422
|
-
{
|
|
2423
|
-
"heading": {
|
|
2424
|
-
"font": "Syne",
|
|
2425
|
-
"slug": "syne",
|
|
2426
|
-
"weight": 700
|
|
2427
|
-
},
|
|
2428
|
-
"body": {
|
|
2429
|
-
"font": "Plus Jakarta Sans",
|
|
2430
|
-
"slug": "plus-jakarta-sans",
|
|
2431
|
-
"weight": 400
|
|
2432
|
-
}
|
|
2433
|
-
},
|
|
2434
2337
|
{
|
|
2435
2338
|
"heading": {
|
|
2436
2339
|
"font": "Libre Baskerville",
|