mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-20 01:56:37 +03:00
update
This commit is contained in:
@@ -378,13 +378,15 @@ Compose six distinct territories, then resolve each into one heading and body pa
|
||||
- Apply [new-work.md](new-work.md)'s `rule:skill-typo-reflex-faces` as the canonical denylist and subject-world test. A family the user named in the interview or supplied assets is the only exception.
|
||||
- Follow [typeset.md](typeset.md)'s workhorse discipline. Give the heading a point of view; give the body a real text face that stays legible at 15px and provides regular and bold weights. A display face in the body slot fails the pair.
|
||||
- Verify every family exists on Google Fonts under the exact current name. Spelling is part of correctness; use `Source Sans 3`, never a retired family name.
|
||||
- Write one sentence in `why` that names the Q4 reference, Users fact, positioning line, commitment, or letterform observation the pair serves. Replace a sentence that could describe any brand.
|
||||
- Write `why` as three to five words naming the pair's voice, not a sentence about the brand. The picker sets it in tracked caps under the two family names, so anything longer wraps and stops scanning. `Considered and editorial`, not `Source Serif 4 gives the questionnaire an editorial voice while Source Sans 3 keeps guidance easy to scan`.
|
||||
- Order the pairs best-first. `pairs[0]` is the recommendation and reaches the picker pre-selected.
|
||||
|
||||
Choose specimen strings and wireframe copy from the product's own world. Do not invent claims or use placeholder prose that could describe any brand.
|
||||
Choose the headline and every wireframe label from the product's own world. Do not invent claims or use placeholder prose that could describe any brand.
|
||||
|
||||
- **Hero**: a headline of at most six words and one honest body sentence (`specimen.headline`, `specimen.body`).
|
||||
- **Wireframe**: every other label the type-preview artboard shows (`preview`): a short brand mark, four nav labels, nav and menu actions, two CTA labels, four proof chips, a section title, two section body lines, one section link, four gallery cards (`title` + `meta`), four footer links, and a footer mark. Pull each string from PRODUCT.md, the interview, or supplied assets. Keep labels short enough to fit the artboard.
|
||||
- **Hero**: a headline of at most six words (`specimen.headline`).
|
||||
- **Wireframe**: every other label the type-preview artboard shows (`preview`): a short brand mark, four nav labels, nav and menu actions, two CTA labels, four proof chips, a section title, one section link, three gallery cards (`title` + `meta`), four footer links, and a footer mark. Pull each string from PRODUCT.md, the interview, or supplied assets. Keep labels short enough to fit the artboard.
|
||||
|
||||
**Running text is not yours to write.** The picker sets every paragraph in lorem, because a body face is judged on texture and real prose pulls the eye into reading it instead. Leave `specimen.body` and `preview.sectionBody` out of the file.
|
||||
|
||||
Write `.impeccable/visual-cues/fonts.json` with this shape:
|
||||
|
||||
@@ -392,8 +394,7 @@ Write `.impeccable/visual-cues/fonts.json` with this shape:
|
||||
{
|
||||
"version": 1,
|
||||
"specimen": {
|
||||
"headline": "Six words from the product's world",
|
||||
"body": "One honest sentence in the product's voice for the body face."
|
||||
"headline": "Six words from the product's world"
|
||||
},
|
||||
"preview": {
|
||||
"brand": "Ab",
|
||||
@@ -404,13 +405,11 @@ Write `.impeccable/visual-cues/fonts.json` with this shape:
|
||||
"ctaSecondary": "Secondary action",
|
||||
"proof": ["Proof one", "Proof two", "Proof three", "Proof four"],
|
||||
"sectionTitle": "Section title",
|
||||
"sectionBody": ["First body line.", "Second body line."],
|
||||
"sectionLink": "Section link",
|
||||
"gallery": [
|
||||
{ "title": "Card one", "meta": "Detail one" },
|
||||
{ "title": "Card two", "meta": "Detail two" },
|
||||
{ "title": "Card three", "meta": "Detail three" },
|
||||
{ "title": "Card four", "meta": "Detail four" }
|
||||
{ "title": "Card three", "meta": "Detail three" }
|
||||
],
|
||||
"footerLinks": ["Link one", "Link two", "Link three", "Link four"],
|
||||
"footerMark": "© Brand"
|
||||
@@ -431,7 +430,7 @@ Write exactly six pair entries. Each role carries the single weight it needs; th
|
||||
|
||||
If Q2 is missing because the interview was skipped, say in one line that the typography set is direction-neutral, then compose all six from the four allowed PRODUCT.md sections alone. Still write the file.
|
||||
|
||||
Parse the finished file as JSON and verify its version, specimen, preview (every field above), six unique ids, six unique heading families, role names, weights, and one-sentence `why` fields before continuing.
|
||||
Parse the finished file as JSON and verify its version, specimen, preview (every field above), six unique ids, six unique heading families, role names, weights, and short `why` fields before continuing.
|
||||
|
||||
Done when: `fonts.json` is parseable, contains exactly six ranked pairs, every family name has been checked against Google Fonts, and the preview copy reads as this product, not generic SaaS filler.
|
||||
|
||||
|
||||
@@ -14,7 +14,9 @@ import { SEEDS } from './palette.mjs';
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const pickerDir = path.join(scriptDir, 'picker');
|
||||
const answersPath = path.resolve(process.cwd(), '.impeccable/design-interview/answers.json');
|
||||
const fontsDir = path.resolve(process.cwd(), '.impeccable/design-interview/fonts');
|
||||
const MAX_BODY_BYTES = 1024 * 1024;
|
||||
const FONT_EXTENSIONS = new Set(['.woff2', '.woff', '.ttf', '.otf']);
|
||||
const MIME = new Map([
|
||||
['.html', 'text/html; charset=utf-8'],
|
||||
['.css', 'text/css; charset=utf-8'],
|
||||
@@ -23,6 +25,10 @@ const MIME = new Map([
|
||||
['.png', 'image/png'],
|
||||
['.svg', 'image/svg+xml'],
|
||||
['.json', 'application/json; charset=utf-8'],
|
||||
['.woff2', 'font/woff2'],
|
||||
['.woff', 'font/woff'],
|
||||
['.ttf', 'font/ttf'],
|
||||
['.otf', 'font/otf'],
|
||||
]);
|
||||
function printHelp() {
|
||||
console.log(`Usage: node picker-server.mjs [options]
|
||||
@@ -205,6 +211,28 @@ async function handleRequest(request, response) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Uploaded faces are stored, not parsed: the questionnaire defers validation
|
||||
// to the end, so the server only needs to put the bytes where the agent can
|
||||
// reach them and hand back the path the answers will carry.
|
||||
if (request.method === 'POST' && requestPath === '/font-upload') {
|
||||
const name = path.basename(request.headers['x-font-filename'] || '');
|
||||
if (!name || !FONT_EXTENSIONS.has(path.extname(name).toLowerCase())) {
|
||||
sendJson(response, 400, { error: 'Expected a .woff2, .woff, .ttf, or .otf filename' });
|
||||
return;
|
||||
}
|
||||
const chunks = [];
|
||||
let size = 0;
|
||||
for await (const chunk of request) {
|
||||
size += chunk.length;
|
||||
if (size > MAX_BODY_BYTES) throw httpError(413, 'Font exceeds 1 MB');
|
||||
chunks.push(chunk);
|
||||
}
|
||||
await mkdir(fontsDir, { recursive: true });
|
||||
await writeFile(path.join(fontsDir, name), Buffer.concat(chunks));
|
||||
sendJson(response, 200, { path: path.join('.impeccable/design-interview/fonts', name) });
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method !== 'GET') {
|
||||
sendJson(response, 405, { error: 'Method not allowed' });
|
||||
return;
|
||||
@@ -232,6 +260,16 @@ async function handleRequest(request, response) {
|
||||
await serveFile(response, options.cuesDir, cueName, ['.png']);
|
||||
return;
|
||||
}
|
||||
// Uploaded faces are read back so the specimen can render in them.
|
||||
if (requestPath.startsWith('/fonts/')) {
|
||||
const fontName = requestPath.slice('/fonts/'.length);
|
||||
if (!fontName || fontName.includes('/')) {
|
||||
sendJson(response, 404, { error: 'Not found' });
|
||||
return;
|
||||
}
|
||||
await serveFile(response, fontsDir, fontName, [...FONT_EXTENSIONS]);
|
||||
return;
|
||||
}
|
||||
|
||||
const assetPath = requestPath === '/' ? 'index.html' : requestPath.slice(1);
|
||||
await serveFile(response, pickerDir, assetPath);
|
||||
|
||||
Reference in New Issue
Block a user