diff --git a/docs/ledger/rows/LIVE-2102.json b/docs/ledger/rows/LIVE-2102.json new file mode 100644 index 000000000..e7f9b5e8d --- /dev/null +++ b/docs/ledger/rows/LIVE-2102.json @@ -0,0 +1,18 @@ +{ + "id": "LIVE-2102", + "title": "Create a Space builder: the Free Human and Services previews wear their own shape and pictures; SHAPE and KIT_IMAGERY are total", + "lane": "live", + "priority": "P1", + "size": "S", + "status": "done", + "source": { + "file": "lib/create-space/preview-site.ts", + "ref": "Promised to Daniel 2026-11-03 05:52Z: the signed-out builder previews for Free Human and Services wear their own accents, shape and photos, like the Temple kit (LIVE-2066) and Nocturne (LIVE-2082)" + }, + "detail": "Free Human had accents of its own (LIVE-2066) but no shape and no pictures in the builder preview: previewTokens fell back to Temple's shape and kitImagery to the gathering photos. The preview now wears the kit's own faces (Cormorant Garamond display with the clay on the emphasised word, Hanken Grotesk body, .2em labels), its 16px controls, 40px cards, the arch on its pictures and its depth-2 shadow, and its own photo library (the walking virgin hero, the founder portrait, the daisies, sun and floating candles). The font sheet loads Cormorant Garamond at 400 too. free-human joins the preview kit order behind New World and Services, gated like them on the one hidden flag (RETIRED_FROM_PICKERS), so it reaches the builder the moment it is un-hidden. Services already had its shape (Bricolage and Lora italic, 20px cards, pill buttons) and the New World pictures from LIVE-2067 and its six accents from LIVE-2066; it has no photo library of its own, so nothing more to give it until one lands. SHAPE and KIT_IMAGERY are now plain Records over every kit id and the two fallbacks are gone. Probe: node scripts/probes/create-space-kit-previews.mjs.", + "verify": { + "kind": "cmd", + "cmd": "node scripts/probes/create-space-kit-previews.mjs" + }, + "closed": "2026-11-03" +} diff --git a/lib/create-space/preview-site.test.ts b/lib/create-space/preview-site.test.ts index 028e5e871..2099c74d6 100644 --- a/lib/create-space/preview-site.test.ts +++ b/lib/create-space/preview-site.test.ts @@ -36,6 +36,15 @@ describe('preview site', () => { expect(kitImagery('temple-kit').hero).toMatch(/^\/sites\/temple-kit\/photos\//) }) + it('Free Human wears its own library; Services wears New World\'s until it has one (LIVE-2102)', () => { + const fh = kitImagery('free-human') + expect(fh.hero).toBe('/sites/free-human/photos/heritage-a-virgin.webp') + expect(fh.portrait).toMatch(/^\/sites\/free-human\/photos\//) + for (const src of [...fh.strip, ...fh.covers, ...fh.portraits]) expect(src).toMatch(/^\/sites\/free-human\/photos\//) + expect(fh).not.toEqual(kitImagery('frequency')) + expect(kitImagery('services').hero).toMatch(/^\/sites\/new-world\/photos\//) + }) + it('reads as a sample Space when nothing is filled in', () => { const s = deriveSite(base) expect(s.name).toBe('Your Space') diff --git a/lib/create-space/preview-site.ts b/lib/create-space/preview-site.ts index f4b513888..1dceaadd8 100644 --- a/lib/create-space/preview-site.ts +++ b/lib/create-space/preview-site.ts @@ -63,6 +63,7 @@ const NETWORK_PHOTO = (n: string) => `/sites/network-kit/photos/photo-${n}.jpg` const TEMPLE_KIT_PHOTO = (n: string) => `/sites/temple-kit/photos/photo-${n}.jpg` const NOCTURNE_PHOTO = (name: string) => `/sites/midnight/photos/${name}.jpg` const NEW_WORLD_PHOTO = (name: string) => `/sites/new-world/photos/${name}.webp` +const FREE_HUMAN_PHOTO = (name: string) => `/sites/free-human/photos/${name}.webp` const FREQUENCY_IMAGERY: KitImagery = { hero: FREQUENCY_PHOTO(2), heroPos: 'center', portrait: FREQUENCY_PHOTO(1), portraitPos: 'center', @@ -70,9 +71,9 @@ const FREQUENCY_IMAGERY: KitImagery = { covers: [FREQUENCY_PHOTO(2), FREQUENCY_PHOTO(1), FREQUENCY_PHOTO(3)], portraits: [FREQUENCY_PHOTO(1)], } -/** Each kit's sample pictures. Designer wears its own library; every other kit wears the gathering photography - * until it has a library of its own. A Space's uploads replace these in the setup walk. */ -const KIT_IMAGERY: Partial> & Record, KitImagery> = { +/** Each kit's sample pictures. A kit with a library of its own wears it; the rest wear the gathering photography + * until theirs arrives. A Space's uploads replace these in the setup walk. */ +const KIT_IMAGERY: Record = { // New World's own gathering library (lib/sites/new-world-photos.json, LIVE-2060): the beach is its hero. 'new-world': { hero: NEW_WORLD_PHOTO('beach-joy'), heroPos: 'center 40%', portrait: NEW_WORLD_PHOTO('group-hug'), portraitPos: 'center 30%', @@ -87,6 +88,14 @@ const KIT_IMAGERY: Partial> & Record> & Record KIT_IMAGERY[kit] ?? FREQUENCY_IMAGERY +/** Every kit's pictures: a kit with its own library never borrows another's. */ +export const kitImagery = (kit: MegaThemeId): KitImagery => KIT_IMAGERY[kit] /** Every kit's accents are its own: no kit falls back to another kit's family (Daniel, 2026-11-03). */ export const kitAccents = (kit: MegaThemeId): readonly PreviewAccent[] => KIT_ACCENTS[kit] diff --git a/lib/theme/preview-tokens.test.ts b/lib/theme/preview-tokens.test.ts index d7bc0cb5a..2a31f07fb 100644 --- a/lib/theme/preview-tokens.test.ts +++ b/lib/theme/preview-tokens.test.ts @@ -14,6 +14,19 @@ describe('preview tokens', () => { expect(PREVIEW_KIT_ORDER[0]).toBe(SITE_THEMES.some((t) => t.id === 'new-world') ? 'new-world' : 'designer') }) + it('Free Human and Services wear shapes of their own, never Temple\'s (LIVE-2102)', () => { + const fh = previewTokens('free-human') + expect(fh['--pv-font-display']).toMatch(/^'Cormorant Garamond'/) + expect(fh['--pv-radius-card']).toBe('40px') + expect(fh['--pv-radius-photo']).toBe('999px 999px 120px 120px') + expect(fh['--pv-em-ink']).toBe('#B3361F') + expect(fh['--pv-font-display']).not.toBe(previewTokens('temple')['--pv-font-display']) + const sv = previewTokens('services') + expect(sv['--pv-font-display']).toMatch(/^'Bricolage Grotesque'/) + expect(sv['--pv-font-em']).toMatch(/^'Lora'/) + expect(sv['--pv-radius-card']).toBe('20px') + }) + it('Designer reads its own palette, with a charcoal primary', () => { const t = previewTokens('designer') expect(t['--pv-bg']).toBe('#F7F3EC') diff --git a/lib/theme/preview-tokens.ts b/lib/theme/preview-tokens.ts index b92e85ff4..f1630121c 100644 --- a/lib/theme/preview-tokens.ts +++ b/lib/theme/preview-tokens.ts @@ -29,7 +29,7 @@ type PreviewTokens = Record * lib/theme/site-style.ts, the one hidden flag) is left out here too, so New World and Services reach the builder the * moment that flag lets them go and never before. */ const PREVIEW_KIT_PREFERENCE: readonly MegaThemeId[] = [ - 'new-world', 'services', 'designer', 'midnight', 'network-kit', 'temple-kit', 'bold', 'wildheart', 'frequency', + 'new-world', 'services', 'free-human', 'designer', 'midnight', 'network-kit', 'temple-kit', 'bold', 'wildheart', 'frequency', ] export const PREVIEW_KIT_ORDER: readonly MegaThemeId[] = PREVIEW_KIT_PREFERENCE.filter((id) => SITE_THEMES.some((t) => t.id === id)) @@ -37,7 +37,7 @@ export const PREVIEW_KIT_ORDER: readonly MegaThemeId[] = PREVIEW_KIT_PREFERENCE. * allows it), the way the Space page does for a themed Space (lib/theme/site-faces.ts), so they register on no * other route. Unbounded, Hanken Grotesk and Instrument Serif are the site's own. */ export const PREVIEW_FONT_SHEET = - 'https://fonts.googleapis.com/css2?family=Anton&family=Bodoni+Moda:wght@400;600&family=Bricolage+Grotesque:opsz,wght@12..96,700&family=Cormorant:ital,wght@0,400;1,400&family=Cormorant+Garamond:ital,wght@0,600;1,600&family=Geist:wght@300..600&family=Instrument+Serif:ital@1&family=Jost:wght@300..500&family=Lora:ital@1&family=Marcellus&family=Outfit:wght@300..500&display=swap' + 'https://fonts.googleapis.com/css2?family=Anton&family=Bodoni+Moda:wght@400;600&family=Bricolage+Grotesque:opsz,wght@12..96,700&family=Cormorant:ital,wght@0,400;1,400&family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400;1,600&family=Geist:wght@300..600&family=Instrument+Serif:ital@1&family=Jost:wght@300..500&family=Lora:ital@1&family=Marcellus&family=Outfit:wght@300..500&display=swap' interface Shape { display: string; body: string; weight: string; textCase: string; track: string; label: string @@ -56,7 +56,7 @@ interface Shape { const hairline = (line: string) => `inset 0 0 0 1px ${line}` -const SHAPE: Partial> & Record, Shape> = { +const SHAPE: Record = { // New World (lib/theme/new-world.ts): Bricolage display and body, Lora italic for the one-word voice, the kit's 20px // cards and pill buttons, on the Night register. Services (lib/theme/services.ts) shares the type system and the radii. 'new-world': { @@ -71,6 +71,15 @@ const SHAPE: Partial> & Record '0 2px 4px -2px rgba(90,30,12,.10),0 12px 24px -14px rgba(90,30,12,.22),0 34px 64px -34px rgba(120,50,20,.30)', rule: '0', ring: '0', + em: '#B3361F', // token-ok: Free Human clay 500 (--text-lyric), lib/theme/free-human.ts + }, designer: { display: "'Cormorant', 'Cormorant Garamond', Georgia, serif", body: "'Jost', system-ui, sans-serif", weight: '400', textCase: 'none', track: '-0.01em', label: '.18em', @@ -146,7 +155,7 @@ function surfaceOf(id: MegaThemeId): ThemeSurface { /** The --pv-* tokens of one theme, generated from its own palette and the SHAPE table. */ export function previewTokens(id: MegaThemeId): PreviewTokens { const s = surfaceOf(id) - const p = SHAPE[id] ?? SHAPE.temple + const p = SHAPE[id] return { '--pv-bg': s.canvas, '--pv-ink': s.text, diff --git a/scripts/probes/create-space-kit-previews.mjs b/scripts/probes/create-space-kit-previews.mjs new file mode 100644 index 000000000..90e73dca2 --- /dev/null +++ b/scripts/probes/create-space-kit-previews.mjs @@ -0,0 +1,31 @@ +// LIVE-2102: the Free Human and Services builder previews wear their own shape, surface and pictures; SHAPE and +// KIT_IMAGERY are total over every kit id, so no kit falls back to Temple's shape or the gathering photos. Reads the +// files under node, never a test runner (LIVE-034). Exit 1 on the first miss. +import { readFileSync, existsSync } from 'node:fs' + +const bad = [] +const read = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '') + +const tokens = read('lib/theme/preview-tokens.ts') +if (!/const SHAPE: Record = \{/.test(tokens)) bad.push('SHAPE is not total over every kit id') +if (/SHAPE\[id\] \?\? SHAPE\.temple/.test(tokens)) bad.push('previewTokens still falls back to Temple\'s shape') +if (!/'free-human': \{\s*display: "'Cormorant Garamond'/.test(tokens)) bad.push('the Free Human preview has no shape of its own (Cormorant Garamond display)') +if (!/services: \{\s*display: "'Bricolage Grotesque'/.test(tokens)) bad.push('the Services preview has no shape of its own (Bricolage display)') +if (!/Cormorant\+Garamond:ital,wght@0,400;0,600;1,400;1,600/.test(tokens)) bad.push('the preview font sheet does not load Cormorant Garamond at 400 for Free Human\'s body copy') +if (!/'new-world', 'services', 'free-human',/.test(tokens)) bad.push('free-human is not in the preview kit order, so it cannot reach the builder when it is un-hidden') + +const site = read('lib/create-space/preview-site.ts') +if (!/const KIT_IMAGERY: Record = \{/.test(site)) bad.push('KIT_IMAGERY is not total over every kit id') +if (/KIT_IMAGERY\[kit\] \?\? FREQUENCY_IMAGERY/.test(site)) bad.push('kitImagery still falls back to the gathering photos') +if (!/'free-human': \{\s*hero: FREE_HUMAN_PHOTO\('heritage-a-virgin'\)/.test(site)) bad.push('the Free Human preview does not wear the kit\'s own photo library') +if (!/services: \{\s*hero: NEW_WORLD_PHOTO/.test(site)) bad.push('the Services preview has no pictures of its own') + +for (const n of ['heritage-a-virgin', 'founder-audrey', 'place-daisies', 'light-sun', 'light-floating-candles', 'light-lanterns', 'light-honey-dipper']) { + if (!existsSync(`public/sites/free-human/photos/${n}.webp`)) bad.push(`Free Human photo ${n} is missing`) +} + +if (bad.length) { + for (const b of bad) console.error(`create-space-kit-previews: ${b}`) + process.exit(1) +} +console.log('create-space-kit-previews: Free Human and Services builder previews wear their own shape and pictures')