From c63375cb04fc80088846341d2113d3c09bcf527c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 18:49:29 +0000 Subject: [PATCH 01/26] LIVE-1963: intake the Create a Space v3 handoff, add the phase rows and ADR-1933 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012BMmZXp78ds6DqD6ywCjf6 --- .../create-a-space/baseline.json | 133 ++++++++++++------ docs/design-systems/create-a-space/copy.json | 128 +++++++++-------- .../create-a-space/inventory.json | 87 +++++------- docs/design-systems/create-a-space/rules.json | 57 +++----- docs/ledger/adr/ADR-1933.md | 9 ++ docs/ledger/rows/LIVE-1963.json | 20 +++ docs/ledger/rows/LIVE-1964.json | 20 +++ docs/ledger/rows/LIVE-1965.json | 20 +++ docs/ledger/rows/LIVE-1966.json | 20 +++ docs/ledger/rows/LIVE-1967.json | 20 +++ docs/ledger/rows/LIVE-1968.json | 20 +++ docs/ledger/rows/LIVE-1969.json | 20 +++ docs/ledger/rows/LIVE-1970.json | 20 +++ docs/ledger/rows/LIVE-1971.json | 20 +++ 14 files changed, 400 insertions(+), 194 deletions(-) create mode 100644 docs/ledger/adr/ADR-1933.md create mode 100644 docs/ledger/rows/LIVE-1963.json create mode 100644 docs/ledger/rows/LIVE-1964.json create mode 100644 docs/ledger/rows/LIVE-1965.json create mode 100644 docs/ledger/rows/LIVE-1966.json create mode 100644 docs/ledger/rows/LIVE-1967.json create mode 100644 docs/ledger/rows/LIVE-1968.json create mode 100644 docs/ledger/rows/LIVE-1969.json create mode 100644 docs/ledger/rows/LIVE-1970.json create mode 100644 docs/ledger/rows/LIVE-1971.json diff --git a/docs/design-systems/create-a-space/baseline.json b/docs/design-systems/create-a-space/baseline.json index 6bf633c3f..eecdc7224 100644 --- a/docs/design-systems/create-a-space/baseline.json +++ b/docs/design-systems/create-a-space/baseline.json @@ -1,50 +1,91 @@ { - "ledger": "LIVE-1766", + "ledger": "LIVE-1965", "ids": [ - "copy:reference/Create a Space.dc.html#022f23d7", - "copy:reference/Create a Space.dc.html#02f3b07c", - "copy:reference/Create a Space.dc.html#0c7f75d2", - "copy:reference/Create a Space.dc.html#0de16cbb", - "copy:reference/Create a Space.dc.html#102fde7c", - "copy:reference/Create a Space.dc.html#204f3b1d", - "copy:reference/Create a Space.dc.html#234c2bc9", - "copy:reference/Create a Space.dc.html#31e6dcde", - "copy:reference/Create a Space.dc.html#37b36826", - "copy:reference/Create a Space.dc.html#416bcc7b", - "copy:reference/Create a Space.dc.html#49282117", - "copy:reference/Create a Space.dc.html#4a7c2c55", - "copy:reference/Create a Space.dc.html#50bac4fb", - "copy:reference/Create a Space.dc.html#58977e4e", - "copy:reference/Create a Space.dc.html#5b58bb6c", - "copy:reference/Create a Space.dc.html#672af675", - "copy:reference/Create a Space.dc.html#6f727392", - "copy:reference/Create a Space.dc.html#71c96855", - "copy:reference/Create a Space.dc.html#71da0f20", - "copy:reference/Create a Space.dc.html#771352c6", - "copy:reference/Create a Space.dc.html#7c5ff581", - "copy:reference/Create a Space.dc.html#7c7e14f8", - "copy:reference/Create a Space.dc.html#7ce9a661", - "copy:reference/Create a Space.dc.html#7ea3e57c", - "copy:reference/Create a Space.dc.html#810e679f", - "copy:reference/Create a Space.dc.html#9368a898", - "copy:reference/Create a Space.dc.html#94aa3e19", - "copy:reference/Create a Space.dc.html#9c00a861", - "copy:reference/Create a Space.dc.html#9ce008ff", - "copy:reference/Create a Space.dc.html#a23c4571", - "copy:reference/Create a Space.dc.html#ac4bcad9", - "copy:reference/Create a Space.dc.html#ada45f51", - "copy:reference/Create a Space.dc.html#b2b1b29d", - "copy:reference/Create a Space.dc.html#ba9f3b87", - "copy:reference/Create a Space.dc.html#bdc1d13d", - "copy:reference/Create a Space.dc.html#bee555b7", - "copy:reference/Create a Space.dc.html#c3b38219", - "copy:reference/Create a Space.dc.html#cc9405e3", - "copy:reference/Create a Space.dc.html#d5c1ee90", - "copy:reference/Create a Space.dc.html#d5c68e9b", - "copy:reference/Create a Space.dc.html#d98fb5c0", - "copy:reference/Create a Space.dc.html#df3a3fca", - "copy:reference/Create a Space.dc.html#f06022a9", - "copy:reference/Create a Space.dc.html#f7417e0a", - "copy:reference/Create a Space.dc.html#fb54b1a3" + "copy:Create a Space.dc.html#022f23d7", + "copy:Create a Space.dc.html#07fed87a", + "copy:Create a Space.dc.html#08f59e8c", + "copy:Create a Space.dc.html#0c0d12db", + "copy:Create a Space.dc.html#0c61f2fc", + "copy:Create a Space.dc.html#11831400", + "copy:Create a Space.dc.html#121a8c17", + "copy:Create a Space.dc.html#1f279624", + "copy:Create a Space.dc.html#210be855", + "copy:Create a Space.dc.html#234c2bc9", + "copy:Create a Space.dc.html#25d59bc0", + "copy:Create a Space.dc.html#2ad8a480", + "copy:Create a Space.dc.html#2c69e4d7", + "copy:Create a Space.dc.html#2d2d8006", + "copy:Create a Space.dc.html#31e6dcde", + "copy:Create a Space.dc.html#3303eeef", + "copy:Create a Space.dc.html#355f3667", + "copy:Create a Space.dc.html#3f44a8b4", + "copy:Create a Space.dc.html#43b71c74", + "copy:Create a Space.dc.html#45b7e5e4", + "copy:Create a Space.dc.html#49282117", + "copy:Create a Space.dc.html#4dde91fb", + "copy:Create a Space.dc.html#4fe294c7", + "copy:Create a Space.dc.html#50bac4fb", + "copy:Create a Space.dc.html#5b58bb6c", + "copy:Create a Space.dc.html#672af675", + "copy:Create a Space.dc.html#6f727392", + "copy:Create a Space.dc.html#713ab484", + "copy:Create a Space.dc.html#71c96855", + "copy:Create a Space.dc.html#71fd9c58", + "copy:Create a Space.dc.html#761c2640", + "copy:Create a Space.dc.html#771352c6", + "copy:Create a Space.dc.html#7b84a661", + "copy:Create a Space.dc.html#7c5ff581", + "copy:Create a Space.dc.html#7c7e14f8", + "copy:Create a Space.dc.html#7ce9a661", + "copy:Create a Space.dc.html#7e09bca9", + "copy:Create a Space.dc.html#7ea3e57c", + "copy:Create a Space.dc.html#7fd275a1", + "copy:Create a Space.dc.html#8aa6f66a", + "copy:Create a Space.dc.html#8ce5112c", + "copy:Create a Space.dc.html#8f182272", + "copy:Create a Space.dc.html#90b0683d", + "copy:Create a Space.dc.html#9368a898", + "copy:Create a Space.dc.html#94328140", + "copy:Create a Space.dc.html#9629a93b", + "copy:Create a Space.dc.html#9c00a861", + "copy:Create a Space.dc.html#9c55b089", + "copy:Create a Space.dc.html#9d0c3ae4", + "copy:Create a Space.dc.html#a23c4571", + "copy:Create a Space.dc.html#a3d1ae38", + "copy:Create a Space.dc.html#a7cb7cd5", + "copy:Create a Space.dc.html#a8c8b909", + "copy:Create a Space.dc.html#ab10a6be", + "copy:Create a Space.dc.html#ac4bcad9", + "copy:Create a Space.dc.html#ae4bae4b", + "copy:Create a Space.dc.html#b292ebca", + "copy:Create a Space.dc.html#b2af515e", + "copy:Create a Space.dc.html#b3cf0316", + "copy:Create a Space.dc.html#b6b5a48f", + "copy:Create a Space.dc.html#b96f6c09", + "copy:Create a Space.dc.html#ba2a0180", + "copy:Create a Space.dc.html#ba9f3b87", + "copy:Create a Space.dc.html#bcba8436", + "copy:Create a Space.dc.html#bdc1d13d", + "copy:Create a Space.dc.html#c403d2d9", + "copy:Create a Space.dc.html#c41842e5", + "copy:Create a Space.dc.html#c9027ab5", + "copy:Create a Space.dc.html#c911c9a0", + "copy:Create a Space.dc.html#cb657469", + "copy:Create a Space.dc.html#cc9405e3", + "copy:Create a Space.dc.html#cfb15b98", + "copy:Create a Space.dc.html#d064ec25", + "copy:Create a Space.dc.html#d1c78f40", + "copy:Create a Space.dc.html#d5c68e9b", + "copy:Create a Space.dc.html#d8395751", + "copy:Create a Space.dc.html#d83baba9", + "copy:Create a Space.dc.html#d96fd4f8", + "copy:Create a Space.dc.html#dcee4e1f", + "copy:Create a Space.dc.html#e2f6c998", + "copy:Create a Space.dc.html#e9f37484", + "copy:Create a Space.dc.html#ef8dffe4", + "copy:Create a Space.dc.html#f06022a9", + "copy:Create a Space.dc.html#f9959fdf", + "copy:Create a Space.dc.html#fadc007f", + "copy:Create a Space.dc.html#fb7bcc92" ] } diff --git a/docs/design-systems/create-a-space/copy.json b/docs/design-systems/create-a-space/copy.json index a2e74cdc5..fa25027d9 100644 --- a/docs/design-systems/create-a-space/copy.json +++ b/docs/design-systems/create-a-space/copy.json @@ -1,5 +1,5 @@ { - "reference/Create a Space.dc.html": [ + "Create a Space.dc.html": [ "'Anton', Impact, sans-serif", "'Bodoni Moda', Georgia, serif", "'Bricolage Grotesque', system-ui, sans-serif", @@ -11,20 +11,20 @@ "'Marcellus', Georgia, serif", "'Outfit', system-ui, sans-serif", "'Unbounded', system-ui, sans-serif", - ", with a time and a price.", - ". Open it whenever you like and your Space goes live. You don't have to wait. Keep building now.", - "02 Let's get started", + "01 Build your Space", + "02 What you offer", "03 Sound familiar", "05 Free to operate", + "1:1 coaching for first-time managers.", "1:1 sessions and appointments", - "3 of 5 · tap any", - "5 of 5 · last one", "A Space holds your page, your bookings, your payments and your people in one place.", "A Space keeps the guest list, so you can welcome people back by name.", "A Space on Frequency", "A catalog and a storefront", + "A city, or Online", "A door from the real room to your page.", "A few lines on who you are and who it is for.", + "A few words is enough. What do people come to you for?", "A great night, then everyone disappears.", "A lime-washed house. Warm and unhurried.", "A nonprofit with programs", @@ -34,129 +34,135 @@ "A small group that keeps meeting.", "A small group that meets", "A studio, gym, or recurring group", - "About 12 minutes for most people. A couple of minutes here builds your Space, and the setup walk covers your story, photos and first offering. Have a few photos handy and you will finish in one sitting. If you take payments, connecting payouts adds a few minutes.", - "Add the name people will know you by.", + "A way to stay close", + "About 12 minutes for most people. A few minutes here builds your Space, and the setup walk covers your story, prices and first offering.", + "Activate my Space", + "Activate when you are ready", + "Add an email so your draft is safe.", + "Add the name clients know you by.", "After dark. One violet light on true black.", - "Already a member? Use the same email or Google account you joined with.", + "Arranging three offerings", "Availability, deposits and reminders from one button.", "Build a community", "Build circles, not walls. A community network where people find groups near them and keep coming back, and Spaces host experiences people come back to.", - "Build it first. You make a free account at the last step.", - "Built · keep going", + "Build your website and Spotlight while you watch. Name it, say what you offer, add a few photos. Free, and a private draft until you activate it.", "Business Spaces directory", "Calendar and gatherings", "Can I bring my own domain?", "Can I change the look later?", - "Choose a Theme Kit", + "Can I come back later?", "Circles for your people", "Coaches and healers", - "Continue with Google", + "Contact section added. Draft saved to your email.", "Courses and programs you run", "Create a Space · Frequency", "Dark, graphic and direct. Say it loud.", - "Do I have to wait for the email?", - "Do I need a website already?", "Does it cost anything to start?", "Email and contacts", - "Email confirmed. Your Space page, website and Spotlight are public, and your host profile is ready.", - "Enter a full email address.", "Every tool, ready to switch", "Features for Spaces", "Features for people", "Find your Circle", - "Five quick moves build your Space, website and Spotlight. Then the setup walk adds your story, photos and first offering, and you're open.", + "Finish my launch", + "Five steps to live", "Free to start. You can change everything later.", + "Free, no card. Build it first, save it last. It stays a private draft until you activate it.", "Frequency Community Collective", "Frequency vs the rest", "Friends as an adult", - "Gatherings, sessions and your people, all in one place.", + "From one sentence", "Get people together. Do things on", - "Give your Space a name. You can change it any time.", + "Give it a name. You can change it any time.", "Good with people, buried in logistics.", "Good with people. Tired of the", - "Google confirmed your email, so there is nothing to wait for. Your Space page, website and Spotlight are public.", "Guided paths and routines people come back to.", - "Host profile made", + "How clients find you", "How long does it really take?", - "How people reach you", + "Is my site public while I build?", "Journeys and practices", + "Keep the photos picked for your theme. Swap in your own whenever you have them.", "Kind words, shown where people decide.", + "Kitchen and home design for families who love to cook.", + "Launch your business in", + "Laying out your headline", "Let people sign up and pay without the chasing.", - "Let's get started", "Limewash and daylight. Plaster, walnut and one ochre accent.", - "Live · finish the setup walk", - "Live, and all yours.", "Lonely but not alone", "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z", - "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z", - "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z", + "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.150-3.150C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z", + "M22.56 12.25c0-.78-.07-1.530-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.920 3.28-4.740 3.28-8.090z", "M3 4.5 6 7.5 9 4.5", "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z", - "Make it look like", - "Make it look like you", "Members and drop-ins", + "Mocked up for you", + "Mocking up your site", + "Name it and pick an accent", "Newsletters and follow ups", - "No. Every Space starts on the free plan, with no card. Plans and prices live on the pricing page, and you can change plan from your Space’s billing page.", - "No. Your Space comes with a website and a Spotlight page in the look you pick.", - "No. Your Space is saved the moment you add your email, and you can keep building. Opening the link makes it public. Continue with Google and it goes live straight away.", - "One Theme Kit dresses your website, Spotlight and email. Then pick your accent.", - "One step keeps everything you just built. No password, no card.", - "Open the email to go live", + "No password, no card. Saving makes your free account and host profile. You agree to the", + "No photos yet? Keep these", + "No. Every Space starts on the free plan, with no card. Plans and prices live on the pricing page.", + "No. Your website, Spotlight and host profile stay a private draft that only you can see. They go public when you activate, from the email link or with one click after saving with Google.", + "One sentence is plenty. We mock up your headline, offerings and Spotlight from it.", + "Only you can see this", "Paper, ink and one solid color. Thirteen ways in.", "Parchment, violet and a sky that turns with the year.", "People gathered under string lights at night", "Pick a handle that is free to use.", + "Pick an accent above to keep going.", + "Pick an accent and watch your buttons change, then hit Next.", + "Pick an accent first", + "Pick an accent to continue", "Plans people can join in a minute, and pause any time.", - "Press Enter to keep going", - "Preview: open the email link", - "Previewing the default look.", + "Preview: open the link", "Prototype only: acts as if the email link was opened", + "Reading what you offer", "Run classes or memberships", - "Save it and claim your handle", - "Saved · one click from live", - "Saving makes your free account and host profile. You agree to the", + "Save it and claim your handle. Nothing is public until you activate it.", + "Save with Google", + "Saved to your Google account. It stays a private draft until you activate it.", + "Saved with Google", + "Saved · activate when ready", "Sell what you make, with the same look as the rest.", - "Send my magic link", "Share your Spotlight", "Six tools and no home.", - "Space page and Spotlight started", "Start my Free Space", "Start the setup walk", - "Starting simple. Add tools later.", "Stay in touch by email", + "Stuck? Start from one of these", "Support the work", + "Switched on for you", + "Switching on the right tools", "Teach programs or courses", "The whole system", - "Want to see them full size?", + "This fills in your contact section and Spotlight. Your draft saves to this email too.", "Warm, bright and easy. The house look.", - "Watch your website and Spotlight change as you type.", - "We sent a link to", "We want every business on Frequency to succeed, so every tool is yours from day one: your website, Spotlight, bookings, memberships, email and more. No card, no trial clock. You only pay when you start making money.", - "Website + Spotlight", - "What will happen", - "What will happen there", + "Weekly pottery classes and open studio nights.", + "What does your business", + "What if I do not have photos yet?", + "What people book", "What you get on day one", - "What's your Space", + "What your business offers", + "What's your business called?", "What’s happening near you", "What’s happening, how to join and who is coming.", - "Where you meet, your hours, your links.", - "Who can find my Space?", "Workshops, gatherings, ticketed nights", + "Yes. Connect a domain you own from your Space settings once you are set up.", "Yes. Switch Theme Kits or your accent any time. Your website, Spotlight and email change together.", - "Yes. Your website can run on a domain you own. You connect it from your Space’s settings once you are set up.", - "You choose. A listed Space shows in the Spaces directory. A private Space is only open to you and your members.", + "Yes. Your draft saves as you go. Once you add your email, we keep it there too, so you can pick up on any device.", "You have a practice, a studio, a gathering or a group, and the tools are scattered. Build one home for all of it: your website, Spotlight, bookings and people.", - "You're not picking a niche. We switch on the tools that fit, and you can add more any time.", - "Your Space, as you build it.", + "Your Space is public", "Your Space, built", + "Your draft is saved to your Google account. It stays private until you activate it, now or after the setup walk.", "Your first gathering", "Your first offering", "Your list, your newsletters, your receipts.", - "Your name goes on your Spotlight and your host profile. Add a photo later.", + "Your one-line description goes here, mocked up from what you offer.", "Your people stay yours.", "Your setup starts with two or three. Turn on the rest when you need them. Each one wears the look you picked.", - "Your whole business on Frequency, live in about 12 minutes. Name it, pick a look, and watch your website and Spotlight come together. Free to start.", - "Your whole business, live in", + "Your website, Spotlight and host profile are public now. The setup walk adds your story, prices and first offering.", + "Your whole business,", + "live in 12 minutes.", "start to finish.", "with your people.", "© 2026 Frequency Community Collective" diff --git a/docs/design-systems/create-a-space/inventory.json b/docs/design-systems/create-a-space/inventory.json index 8b33a2dd4..3e8884a10 100644 --- a/docs/design-systems/create-a-space/inventory.json +++ b/docs/design-systems/create-a-space/inventory.json @@ -1,146 +1,136 @@ [ { - "path": "COMPONENTS.md", - "bytes": 6625, - "sha256": "eb800792c1731086449c7e2c90c2ccc2c0490aaacaf8ab164d39d80834aa959f" + "path": "Create a Space.dc.html", + "bytes": 116860, + "sha256": "5b4f2f4a1b29092c9e11aca318897a375fd98477b88b396ee3aa2a0695296da5" }, { - "path": "LINKS.md", - "bytes": 4497, - "sha256": "8cf7c0b1872d9495802446b0ed76a77316561683c819ab610ce29891364ab3df" - }, - { - "path": "PREVIEW-MODE.md", - "bytes": 6400, - "sha256": "c8fe7709af938b1f39b8e463d0897c5a34ee1f83b55fa527ffcb8b8f880d91e7" + "path": "DRAFT-RECOVERY-EMAILS.md", + "bytes": 2756, + "sha256": "43cf141561b3cd55ba871e38523f13501fd5d613a692909baf88ff290862b1fe" }, { "path": "README.md", - "bytes": 5172, - "sha256": "2a11e08b455000440fe92e1465f67cd2dd316ded9f2017f1981af0b58de2f3b0" - }, - { - "path": "reference/Create a Space.dc.html", - "bytes": 117011, - "sha256": "f9b26801ab18dd2da0b10f11b659c168cd3bbe02d8233d5ad90484553f4f6e55" - }, - { - "path": "reference/README.md", - "bytes": 15362, - "sha256": "378f081f6c4a5560304887be03466a65dbf53105fb9d16df75d3ef470dba6f7c" + "bytes": 25212, + "sha256": "b224796e6cc7289a2948b323a8c170b9a63d3c9660dc027b512e34e33b060d91" }, { - "path": "reference/_ds/frequency-design-system-5ecee10f-9078-41bd-8de4-38ad33320f4e/_ds_bundle.js", + "path": "_ds/frequency-design-system-5ecee10f-9078-41bd-8de4-38ad33320f4e/_ds_bundle.js", "bytes": 249747, "sha256": "48395e2fc13bf7a72c5be5c8428b17f52537e3f20724ff0370dcfc669fd01bc8" }, { - "path": "reference/_ds/frequency-design-system-5ecee10f-9078-41bd-8de4-38ad33320f4e/styles.css", + "path": "_ds/frequency-design-system-5ecee10f-9078-41bd-8de4-38ad33320f4e/styles.css", "bytes": 360, "sha256": "8e3845c3efe659531087758f88ef44ed6b63e39c5f2df89abb714923bf7e6019" }, { - "path": "reference/_ds/frequency-design-system-5ecee10f-9078-41bd-8de4-38ad33320f4e/tokens/base.css", + "path": "_ds/frequency-design-system-5ecee10f-9078-41bd-8de4-38ad33320f4e/tokens/base.css", "bytes": 8367, "sha256": "0addf8039184cfee4ec4dbbebc75896e1adfaba938d553a46845f6b6c2ef2ea1" }, { - "path": "reference/_ds/frequency-design-system-5ecee10f-9078-41bd-8de4-38ad33320f4e/tokens/colors.css", + "path": "_ds/frequency-design-system-5ecee10f-9078-41bd-8de4-38ad33320f4e/tokens/colors.css", "bytes": 6286, "sha256": "3a342f5cfa802e7eb3f806348ba3a93d4816225aff7e63a14cc114f1170ba67a" }, { - "path": "reference/_ds/frequency-design-system-5ecee10f-9078-41bd-8de4-38ad33320f4e/tokens/features.css", + "path": "_ds/frequency-design-system-5ecee10f-9078-41bd-8de4-38ad33320f4e/tokens/features.css", "bytes": 7359, "sha256": "c643eb9f4f6dd52ad57f66a8d13c050468061869c1c9154e669b72afc4dd899b" }, { - "path": "reference/_ds/frequency-design-system-5ecee10f-9078-41bd-8de4-38ad33320f4e/tokens/infographics.css", + "path": "_ds/frequency-design-system-5ecee10f-9078-41bd-8de4-38ad33320f4e/tokens/infographics.css", "bytes": 30200, "sha256": "991b878fae483b9043654aeba294f2e84ca74842e8fbca290ae81a559609c505" }, { - "path": "reference/_ds/frequency-design-system-5ecee10f-9078-41bd-8de4-38ad33320f4e/tokens/space-motion.css", + "path": "_ds/frequency-design-system-5ecee10f-9078-41bd-8de4-38ad33320f4e/tokens/space-motion.css", "bytes": 1583, "sha256": "a9884f4175801d0e4f318466c92c7c29f0dfe09449e15dafac050d56d16780d9" }, { - "path": "reference/_ds/frequency-design-system-5ecee10f-9078-41bd-8de4-38ad33320f4e/tokens/themes.css", + "path": "_ds/frequency-design-system-5ecee10f-9078-41bd-8de4-38ad33320f4e/tokens/themes.css", "bytes": 5803, "sha256": "aa2f5473b1859da30f5f7d43a9c8e75ac06ca4929c95e88ee05b7985e3a19709" }, { - "path": "reference/_ds/frequency-design-system-5ecee10f-9078-41bd-8de4-38ad33320f4e/tokens/typography.css", + "path": "_ds/frequency-design-system-5ecee10f-9078-41bd-8de4-38ad33320f4e/tokens/typography.css", "bytes": 1667, "sha256": "bfb2dd92bab96b6321e5a45b4a3b474edd0a81f5f06381b3d37a9ccc9767f9a0" }, { - "path": "reference/assets/designer/courtyard-arch.webp", + "path": "assets/designer/courtyard-arch.webp", "bytes": 115542, "sha256": "924a25edf9122758c048773637885cc31f32cffdfe09e24b14e42ad09eb7b8eb" }, { - "path": "reference/assets/designer/house-front.webp", + "path": "assets/designer/house-front.webp", "bytes": 175652, "sha256": "0e42355fca75a2e5e97a6b2d673c92025e12256372b953383272229eff7be017" }, { - "path": "reference/assets/designer/kitchen-hero.webp", + "path": "assets/designer/kitchen-hero.webp", "bytes": 73604, "sha256": "71dcafdf4d93bb44eef63ab3c3b0a077d231cb60a299b35dd6bc587a116ddd2e" }, { - "path": "reference/assets/designer/kitchen-island.webp", + "path": "assets/designer/kitchen-island.webp", "bytes": 81852, "sha256": "5caa2be7193f326353968dc0c58e7c6d0de325e6052fcc06cd0f0599bc636c53" }, { - "path": "reference/assets/designer/portrait-01.webp", + "path": "assets/designer/portrait-01.webp", "bytes": 67160, "sha256": "e50c78ac9b8385d3a65333a740f6753011ec9b3a9a0f6fa4fe57916f6dec8c47" }, { - "path": "reference/assets/designer/portrait-02.webp", + "path": "assets/designer/portrait-02.webp", "bytes": 74752, "sha256": "96e5a72b5fdb29d925afb4a763ae116bcceddba26c7b86fb58569f0f0cf5af64" }, { - "path": "reference/assets/designer/sofa.webp", + "path": "assets/designer/sofa.webp", "bytes": 89654, "sha256": "7b394db26a03187dfe9d49241a34d46181764f8a9a81065671bdaffe5686c7e8" }, { - "path": "reference/assets/icons/frequency-icons.svg", + "path": "assets/icons/frequency-icons.svg", "bytes": 29299, "sha256": "75d119bd9b0e5e523d3775b19a4f95280fba7586f6ebb9ac55497424d1b6146f" }, { - "path": "reference/assets/logo/frequency-icon-round.png", + "path": "assets/icons/inline.js", + "bytes": 1509, + "sha256": "30d4ad7fb10c34373de5f1175676629c06ebf2e6ac99cffd93706d69bc4c574d" + }, + { + "path": "assets/logo/frequency-icon-round.png", "bytes": 23140, "sha256": "0115c43fdbbd97c018b418d56d9a51e328a88244b647bf98246b37f455cf5d0f" }, { - "path": "reference/assets/logo/frequency-wordmark.png", + "path": "assets/logo/frequency-wordmark.png", "bytes": 35300, "sha256": "fd6aca4bcf54b0f988a28ec405b26996d1f12b9117d10037389c891b11dbec0c" }, { - "path": "reference/assets/photos/pain-1.jpg", + "path": "assets/photos/pain-1.jpg", "bytes": 257093, "sha256": "bfe8057a7ea44c63f62de08e2067b1dde42a8fb979f84861aaf9b8ba6702555f" }, { - "path": "reference/assets/photos/pain-2.jpg", + "path": "assets/photos/pain-2.jpg", "bytes": 168402, "sha256": "f1b89e5ef2f528c08bb7cfceb8f31fa7c5bd40467195604fe1433e714b18b35c" }, { - "path": "reference/assets/photos/pain-3.jpg", + "path": "assets/photos/pain-3.jpg", "bytes": 226967, "sha256": "a8d65fd76425832f6ee3941eae3b65ffa922565d66fd273caab6a7da8b1bf6c8" }, { - "path": "reference/support.js", + "path": "support.js", "bytes": 69150, "sha256": "8fe7df74405f3c55f49b7249c74ea1397e65d07dea2b1bd3b4a489bec2e28cbe" }, @@ -153,10 +143,5 @@ "path": "tokens/preview-themes.json", "bytes": 18737, "sha256": "c2bd2d7ab1abfda8dab124ded5a28cad0a10eb42b341e0274bd140d4645a523a" - }, - { - "path": "tokens/stage.css", - "bytes": 2140, - "sha256": "8c502a2912a553196d342abf9ca163588fac889d578dfd4e81f62ac92719d97d" } ] diff --git a/docs/design-systems/create-a-space/rules.json b/docs/design-systems/create-a-space/rules.json index 36cc202d9..f119b0d11 100644 --- a/docs/design-systems/create-a-space/rules.json +++ b/docs/design-systems/create-a-space/rules.json @@ -12,90 +12,75 @@ "reason": "Handoff text for the whole page; every value in it is checked through the files it describes." }, { - "glob": "COMPONENTS.md", + "glob": "DRAFT-RECOVERY-EMAILS.md", "kind": "reference", - "reason": "Component notes for the page; each component is checked through the rule for the file that builds it." + "reason": "Copy and rules for the Track A draft-recovery emails; built in lib/create-space/draft-recovery*.ts and checked there (Phase 5, ledger fragment)." }, { - "glob": "LINKS.md", - "kind": "reference", - "reason": "Route and link map; the routes are checked by site-nav.test.ts." - }, - { - "glob": "PREVIEW-MODE.md", - "kind": "reference", - "reason": "The --pv-* contract; built in lib/theme/preview-tokens.ts and pinned by preview-tokens.test.ts." - }, - { - "glob": "reference/README.md", - "kind": "reference", - "reason": "Screen-level spec from the earlier pass; the system docs supersede it where they differ." - }, - { - "glob": "reference/support.js", + "glob": "support.js", "kind": "skip", "reason": "Prototype runtime only; never shipped." }, { - "glob": "reference/_ds/**", + "glob": "_ds/**", "kind": "reference", "reason": "A copy of the Frequency design-system tokens and bundle; the repo's own files (app/themes/frequency) are the authority." }, { - "glob": "reference/assets/logo/*", + "glob": "assets/icons/inline.js", + "kind": "skip", + "reason": "Prototype runtime that inlines the icon sprite; never shipped." + }, + { + "glob": "assets/logo/*", "kind": "reference", "reason": "Frequency's wordmark and icon; the repo uses its own copies." }, { - "glob": "reference/assets/icons/frequency-icons.svg", + "glob": "assets/icons/frequency-icons.svg", "kind": "reference", "reason": "The in-house icon sprite; the repo has its own (components/frequency icons)." }, { - "glob": "reference/assets/designer/courtyard-arch.webp", + "glob": "assets/designer/courtyard-arch.webp", "kind": "asset", "target": "public/sites/designer/photos/dz-courtyard-arch.webp" }, { - "glob": "reference/assets/designer/house-front.webp", + "glob": "assets/designer/house-front.webp", "kind": "asset", "target": "public/sites/designer/photos/dz-house-front.webp" }, { - "glob": "reference/assets/designer/kitchen-hero.webp", + "glob": "assets/designer/kitchen-hero.webp", "kind": "asset", "target": "public/sites/designer/photos/dz-designer-kitchen-hero.webp" }, { - "glob": "reference/assets/designer/kitchen-island.webp", + "glob": "assets/designer/kitchen-island.webp", "kind": "asset", "target": "public/sites/designer/photos/dz-kitchen-island.webp" }, { - "glob": "reference/assets/designer/portrait-01.webp", + "glob": "assets/designer/portrait-01.webp", "kind": "asset", "target": "public/sites/designer/photos/dz-portrait-01.webp" }, { - "glob": "reference/assets/designer/portrait-02.webp", + "glob": "assets/designer/portrait-02.webp", "kind": "asset", "target": "public/sites/designer/photos/dz-portrait-02.webp" }, { - "glob": "reference/assets/designer/sofa.webp", + "glob": "assets/designer/sofa.webp", "kind": "asset", "target": "public/sites/designer/photos/dz-designer-sofa.webp" }, { - "glob": "reference/assets/photos/*.jpg", + "glob": "assets/photos/*.jpg", "kind": "asset", "target": "public/create-a-space/photos/{base}" }, - { - "glob": "tokens/stage.css", - "kind": "reference", - "reason": "Mirrors the live Stage values in app/themes/frequency; the repo's file is the authority." - }, { "glob": "tokens/preview-themes.*", "kind": "built", @@ -103,7 +88,7 @@ "reason": "Prototype stand-ins; the real tokens are generated from each theme's own palette." }, { - "glob": "reference/Create a Space.dc.html", + "glob": "Create a Space.dc.html", "kind": "copy", "targets": [ "components/frequency/site/pages/create-space", @@ -115,4 +100,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/docs/ledger/adr/ADR-1933.md b/docs/ledger/adr/ADR-1933.md new file mode 100644 index 000000000..9b59a5667 --- /dev/null +++ b/docs/ledger/adr/ADR-1933.md @@ -0,0 +1,9 @@ +## ADR-1933: Create a Space saves a private draft to the visitor's email at step 3, before there is an account + +**Status:** Accepted 2026-11-02 (owner asked for the signed-out wizard with lead capture and abandon recovery, and took every recommendation). Amends the v1 rule written in `lib/create-space/handoff.ts` that the draft leaves the browser once, at the last move. Builds on [ADR-125](DECISIONS.md) (lead flows) and the come-back rule of VLS-26 and VLS-27. + +**Context.** The v3 handoff asks for an email at step 3 ("How do clients find you?"), before photos and before the account, because photos is where people stop and an address is the only way to bring them back. Under v1 nothing left the browser until the visitor saved, so an abandoned draft left no trace and no way back. + +**Decision.** At step 3 the draft is sent once to `saveDraftLead` and kept server-side in `create_space_drafts` (token, contact, draft JSON capped at 8KB, time zone, step reached; row-level security on with no policies, admin client only). The contact goes through `captureLead` (root-scoped, suppression-checked). The draft is private: nothing is public until the Space is launched on /launch. Recovery is a dedicated cron that mirrors the come-back emails (1 hour, 1 day, 3 days after the last save, 8am to 8pm in the browser's zone, at most three, ended by saving, launching or unsubscribing), not a persona nurture sequence, because nurture has no local-hours window and no per-email log. The emails are transactional messages about the visitor's own draft, carry an unsubscribe link and pass the lifecycle send gate; contacts stay `consent_state: unknown` and newsletters stay opt-in. No SMS without explicit opt-in. Drafts and unconverted leads are purged after 90 days. Photo files never leave the browser. Funnel counts are first-party only, with no typed text and nothing sent to a pixel. + +**Consequences.** A visitor who gives an address can be reminded three times at most. The privacy page and the step-3 fine print must say the draft saves to their email; legal review of phone and location capture and of the recovery emails stays open. Signed-out operator doors now open this wizard instead of /sign-in. diff --git a/docs/ledger/rows/LIVE-1963.json b/docs/ledger/rows/LIVE-1963.json new file mode 100644 index 000000000..df2d4dd11 --- /dev/null +++ b/docs/ledger/rows/LIVE-1963.json @@ -0,0 +1,20 @@ +{ + "id": "LIVE-1963", + "title": "Intake the Create a Space v3 handoff and record the pre-account lead rule", + "lane": "live", + "priority": "P1", + "size": "S", + "status": "open", + "source": { + "file": "docs/design-systems/create-a-space/rules.json", + "ref": "Daniel asked for the signed-out Space creation wizard 2026-11-02; plan /mnt/project-files/create-a-space-wizard/PLAN.md" + }, + "detail": "Re-point the design-system rules to the flat v3 folder, re-snapshot, re-freeze the baseline against the build rows, and write the ADR that amends the v1 rule (the draft leaves the browser once) for step-3 lead capture.", + "verify": { + "kind": "cmd", + "cmd": "node -e \"const f=require('fs');process.exit(f.readFileSync('docs/design-systems/create-a-space/rules.json','utf8').includes('DRAFT-RECOVERY-EMAILS.md')&&f.existsSync('docs/ledger/adr/ADR-1933.md')?0:1)\"", + "checked": "2026-11-02", + "evidence": "Fails today: the file or change the probe names is not on main." + }, + "wave": "W7" +} diff --git a/docs/ledger/rows/LIVE-1964.json b/docs/ledger/rows/LIVE-1964.json new file mode 100644 index 000000000..d6a725d78 --- /dev/null +++ b/docs/ledger/rows/LIVE-1964.json @@ -0,0 +1,20 @@ +{ + "id": "LIVE-1964", + "title": "Create a Space v3 pure core: draft v2, offer to intents, carry keys, preview inputs", + "lane": "live", + "priority": "P1", + "size": "M", + "status": "open", + "source": { + "file": "docs/design-systems/create-a-space/rules.json", + "ref": "design_handoff create-a-space v3 README sections 3, 5, 6; Daniel asked for the signed-out Space creation wizard 2026-11-02; plan /mnt/project-files/create-a-space-wizard/PLAN.md" + }, + "detail": "Draft shape v2 with v1 upgrade, intentsFromOffer, progressOf weights, deriveSite offer and reach, kitImagery covers and portraits, carry keys line phone where cover logo profile with validators, holdFor mapping. All tested.", + "verify": { + "kind": "cmd", + "cmd": "node -e \"const f=require('fs');process.exit(f.existsSync('lib/create-space/intents-from-offer.ts')&&f.readFileSync('lib/launch/carry.ts','utf8').includes('phone?')?0:1)\"", + "checked": "2026-11-02", + "evidence": "Fails today: the file or change the probe names is not on main." + }, + "wave": "W7" +} diff --git a/docs/ledger/rows/LIVE-1965.json b/docs/ledger/rows/LIVE-1965.json new file mode 100644 index 000000000..d719a8735 --- /dev/null +++ b/docs/ledger/rows/LIVE-1965.json @@ -0,0 +1,20 @@ +{ + "id": "LIVE-1965", + "title": "Create a Space v3 hero builder: intro and building states, name, accent lock, offer mock-up", + "lane": "live", + "priority": "P1", + "size": "L", + "status": "open", + "source": { + "file": "docs/design-systems/create-a-space/rules.json", + "ref": "design_handoff create-a-space v3 README sections 2, 3 steps 1 and 2, 4; Daniel asked for the signed-out Space creation wizard 2026-11-02; plan /mnt/project-files/create-a-space-wizard/PLAN.md" + }, + "detail": "One hero holds the builder: FLIP morph, clock in the H1, saved-step pills, accent-locked Next, offer step with the mock-up moment, preview v2 with ghost state.", + "verify": { + "kind": "cmd", + "cmd": "node -e \"const f=require('fs');process.exit(f.readFileSync('components/frequency/site/pages/create-space/create-space-studio.tsx','utf8').includes('cs-hero-building')?0:1)\"", + "checked": "2026-11-02", + "evidence": "Fails today: the file or change the probe names is not on main." + }, + "wave": "W7" +} diff --git a/docs/ledger/rows/LIVE-1966.json b/docs/ledger/rows/LIVE-1966.json new file mode 100644 index 000000000..c10e8a9e6 --- /dev/null +++ b/docs/ledger/rows/LIVE-1966.json @@ -0,0 +1,20 @@ +{ + "id": "LIVE-1966", + "title": "Create a Space v3 steps 3 to 5: contact, photos, save, saved state, finish dock, FAQ", + "lane": "live", + "priority": "P1", + "size": "L", + "status": "open", + "source": { + "file": "docs/design-systems/create-a-space/rules.json", + "ref": "design_handoff create-a-space v3 README sections 3 steps 3 to 5, 8; Daniel asked for the signed-out Space creation wizard 2026-11-02; plan /mnt/project-files/create-a-space-wizard/PLAN.md" + }, + "detail": "Contact step, photo step with browser-only uploads, save step on the existing claim actions, saved state, finish dock, closing CTA and FAQ copy.", + "verify": { + "kind": "cmd", + "cmd": "node -e \"const f=require('fs');process.exit(f.existsSync('components/frequency/site/pages/create-space/create-space-photos.tsx')?0:1)\"", + "checked": "2026-11-02", + "evidence": "Fails today: the file or change the probe names is not on main." + }, + "wave": "W7" +} diff --git a/docs/ledger/rows/LIVE-1967.json b/docs/ledger/rows/LIVE-1967.json new file mode 100644 index 000000000..466e7ff95 --- /dev/null +++ b/docs/ledger/rows/LIVE-1967.json @@ -0,0 +1,20 @@ +{ + "id": "LIVE-1967", + "title": "Create a Space lead capture: saveDraftLead, create_space_drafts, autosave and resume by token", + "lane": "live", + "priority": "P1", + "size": "L", + "status": "open", + "source": { + "file": "docs/design-systems/create-a-space/rules.json", + "ref": "design_handoff create-a-space v3 README section 5; Daniel asked for the signed-out Space creation wizard 2026-11-02; plan /mnt/project-files/create-a-space-wizard/PLAN.md" + }, + "detail": "Migration for create_space_drafts (RLS deny-all), saveDraftLead with throttles and honeypot through captureLead, debounced autosave and pagehide beacon, ?draft= restore.", + "verify": { + "kind": "cmd", + "cmd": "node -e \"const f=require('fs');process.exit(f.readdirSync('supabase/migrations').some(n=>n.includes('create_space_drafts'))?0:1)\"", + "checked": "2026-11-02", + "evidence": "Fails today: the file or change the probe names is not on main." + }, + "wave": "W7" +} diff --git a/docs/ledger/rows/LIVE-1968.json b/docs/ledger/rows/LIVE-1968.json new file mode 100644 index 000000000..33267073b --- /dev/null +++ b/docs/ledger/rows/LIVE-1968.json @@ -0,0 +1,20 @@ +{ + "id": "LIVE-1968", + "title": "Create a Space abandon recovery: three draft emails on a dedicated cron", + "lane": "live", + "priority": "P1", + "size": "L", + "status": "open", + "source": { + "file": "docs/design-systems/create-a-space/rules.json", + "ref": "design_handoff create-a-space v3 DRAFT-RECOVERY-EMAILS.md; Daniel asked for the signed-out Space creation wizard 2026-11-02; plan /mnt/project-files/create-a-space-wizard/PLAN.md" + }, + "detail": "Pure timing rule, email log, runner, cron route with heartbeat and vercel.json entry, runbook gate, conversion hook that ends Track A.", + "verify": { + "kind": "cmd", + "cmd": "node -e \"const f=require('fs');process.exit(f.existsSync('app/api/cron/create-space-draft-reminders/route.ts')?0:1)\"", + "checked": "2026-11-02", + "evidence": "Fails today: the file or change the probe names is not on main." + }, + "wave": "W7" +} diff --git a/docs/ledger/rows/LIVE-1969.json b/docs/ledger/rows/LIVE-1969.json new file mode 100644 index 000000000..805ebe710 --- /dev/null +++ b/docs/ledger/rows/LIVE-1969.json @@ -0,0 +1,20 @@ +{ + "id": "LIVE-1969", + "title": "Every signed-out way in opens the Create a Space wizard", + "lane": "live", + "priority": "P1", + "size": "M", + "status": "open", + "source": { + "file": "docs/design-systems/create-a-space/rules.json", + "ref": "Daniel 2026-11-02: this should launch for anybody not logged in; Daniel asked for the signed-out Space creation wizard 2026-11-02; plan /mnt/project-files/create-a-space-wizard/PLAN.md" + }, + "detail": "Signed-out /spaces/new, the operator CTA, /go and /for doors and theme-kit CTAs go to /create-a-space with kit, mode and niche hints. Signed-in stays on /launch.", + "verify": { + "kind": "cmd", + "cmd": "node -e \"const f=require('fs');process.exit(f.readFileSync('app/(main)/spaces/new/page.tsx','utf8').includes('/create-a-space')?0:1)\"", + "checked": "2026-11-02", + "evidence": "Fails today: the file or change the probe names is not on main." + }, + "wave": "W7" +} diff --git a/docs/ledger/rows/LIVE-1970.json b/docs/ledger/rows/LIVE-1970.json new file mode 100644 index 000000000..914da6643 --- /dev/null +++ b/docs/ledger/rows/LIVE-1970.json @@ -0,0 +1,20 @@ +{ + "id": "LIVE-1970", + "title": "Create a Space funnel: anonymous signals, lead score, median name-to-saved time", + "lane": "live", + "priority": "P2", + "size": "M", + "status": "open", + "source": { + "file": "docs/design-systems/create-a-space/rules.json", + "ref": "design_handoff create-a-space v3 README section 5 analytics; Daniel asked for the signed-out Space creation wizard 2026-11-02; plan /mnt/project-files/create-a-space-wizard/PLAN.md" + }, + "detail": "Anonymous first-party signal route and taxonomy entries, no typed text and no pixel, lead score in contacts.meta, a funnel read in admin analytics.", + "verify": { + "kind": "cmd", + "cmd": "node -e \"const f=require('fs');process.exit(f.existsSync('app/api/create-space/signal/route.ts')?0:1)\"", + "checked": "2026-11-02", + "evidence": "Fails today: the file or change the probe names is not on main." + }, + "wave": "W7" +} diff --git a/docs/ledger/rows/LIVE-1971.json b/docs/ledger/rows/LIVE-1971.json new file mode 100644 index 000000000..ada68f164 --- /dev/null +++ b/docs/ledger/rows/LIVE-1971.json @@ -0,0 +1,20 @@ +{ + "id": "LIVE-1971", + "title": "Create a Space v3 hardening: walking tests, e2e, renders at five widths, budget gates", + "lane": "live", + "priority": "P2", + "size": "M", + "status": "open", + "source": { + "file": "docs/design-systems/create-a-space/rules.json", + "ref": "design_handoff create-a-space v3 README section 9; Daniel asked for the signed-out Space creation wizard 2026-11-02; plan /mnt/project-files/create-a-space-wizard/PLAN.md" + }, + "detail": "Every button and form walked, e2e from name to save, signed in and out renders at 390 1280 1440 1920 2000, theme fence and the postbuild gates green.", + "verify": { + "kind": "cmd", + "cmd": "node -e \"const f=require('fs');process.exit(f.existsSync('test/e2e/create-space.spec.ts')?0:1)\"", + "checked": "2026-11-02", + "evidence": "Fails today: the file or change the probe names is not on main." + }, + "wave": "W7" +} From 342fe08d0d1a20cc99b0cea4575d623eb6e7af92 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 18:50:33 +0000 Subject: [PATCH 02/26] LIVE-1963: close the intake row with its probe Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012BMmZXp78ds6DqD6ywCjf6 --- docs/ledger/rows/LIVE-1963.json | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/ledger/rows/LIVE-1963.json b/docs/ledger/rows/LIVE-1963.json index df2d4dd11..a1257db82 100644 --- a/docs/ledger/rows/LIVE-1963.json +++ b/docs/ledger/rows/LIVE-1963.json @@ -4,17 +4,16 @@ "lane": "live", "priority": "P1", "size": "S", - "status": "open", + "status": "done", "source": { "file": "docs/design-systems/create-a-space/rules.json", "ref": "Daniel asked for the signed-out Space creation wizard 2026-11-02; plan /mnt/project-files/create-a-space-wizard/PLAN.md" }, - "detail": "Re-point the design-system rules to the flat v3 folder, re-snapshot, re-freeze the baseline against the build rows, and write the ADR that amends the v1 rule (the draft leaves the browser once) for step-3 lead capture.", + "detail": "Re-point the design-system rules to the flat v3 folder, re-snapshot, re-freeze the baseline against the build rows, and write the ADR that amends the v1 rule (the draft leaves the browser once) for step-3 lead capture.\n\nRules re-pointed to the flat v3 folder, snapshot and baseline re-frozen against LIVE-1965, ADR-1933 written, nine phase rows added.", "verify": { "kind": "cmd", - "cmd": "node -e \"const f=require('fs');process.exit(f.readFileSync('docs/design-systems/create-a-space/rules.json','utf8').includes('DRAFT-RECOVERY-EMAILS.md')&&f.existsSync('docs/ledger/adr/ADR-1933.md')?0:1)\"", - "checked": "2026-11-02", - "evidence": "Fails today: the file or change the probe names is not on main." + "cmd": "node -e \"const f=require('fs');process.exit(f.readFileSync('docs/design-systems/create-a-space/rules.json','utf8').includes('DRAFT-RECOVERY-EMAILS.md')&&f.existsSync('docs/ledger/adr/ADR-1933.md')?0:1)\"" }, + "closed": "2026-11-02", "wave": "W7" } From ff78b230e0f61b58e20003cdbd8b246d1dfa00f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 18:58:38 +0000 Subject: [PATCH 03/26] LIVE-1964: Create a Space v3 pure core: offer to intents, carry keys, preview contact strip and photo lists Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012BMmZXP78ds6DqD6ywCjf6 --- docs/ledger/rows/LIVE-1964.json | 9 ++-- lib/create-space/intents-from-offer.test.ts | 29 +++++++++++++ lib/create-space/intents-from-offer.ts | 43 +++++++++++++++++++ lib/create-space/preview-site.test.ts | 31 +++++++++++++- lib/create-space/preview-site.ts | 46 +++++++++++++++++++-- lib/launch/carry.test.ts | 14 +++++++ lib/launch/carry.ts | 39 ++++++++++++++++- lib/launch/clock.test.ts | 11 ++++- lib/launch/clock.ts | 8 ++-- 9 files changed, 214 insertions(+), 16 deletions(-) create mode 100644 lib/create-space/intents-from-offer.test.ts create mode 100644 lib/create-space/intents-from-offer.ts diff --git a/docs/ledger/rows/LIVE-1964.json b/docs/ledger/rows/LIVE-1964.json index d6a725d78..5c5a28ecb 100644 --- a/docs/ledger/rows/LIVE-1964.json +++ b/docs/ledger/rows/LIVE-1964.json @@ -4,17 +4,16 @@ "lane": "live", "priority": "P1", "size": "M", - "status": "open", + "status": "done", "source": { "file": "docs/design-systems/create-a-space/rules.json", "ref": "design_handoff create-a-space v3 README sections 3, 5, 6; Daniel asked for the signed-out Space creation wizard 2026-11-02; plan /mnt/project-files/create-a-space-wizard/PLAN.md" }, - "detail": "Draft shape v2 with v1 upgrade, intentsFromOffer, progressOf weights, deriveSite offer and reach, kitImagery covers and portraits, carry keys line phone where cover logo profile with validators, holdFor mapping. All tested.", + "detail": "Draft shape v2 with v1 upgrade, intentsFromOffer, progressOf weights, deriveSite offer and reach, kitImagery covers and portraits, carry keys line phone where cover logo profile with validators, holdFor mapping. All tested.\n\nPure core landed: intentsFromOffer, carry keys line phone where cover logo profile, deriveSite offer and reach, kitImagery covers and portraits, formatClock shared. The draft v2 shape and holdFor mapping ship with the hero (LIVE-1965), since the v1 builder component reads the v1 shape until then.", "verify": { "kind": "cmd", - "cmd": "node -e \"const f=require('fs');process.exit(f.existsSync('lib/create-space/intents-from-offer.ts')&&f.readFileSync('lib/launch/carry.ts','utf8').includes('phone?')?0:1)\"", - "checked": "2026-11-02", - "evidence": "Fails today: the file or change the probe names is not on main." + "cmd": "node -e \"const f=require('fs');process.exit(f.existsSync('lib/create-space/intents-from-offer.ts')&&f.readFileSync('lib/launch/carry.ts','utf8').includes('phone?')?0:1)\"" }, + "closed": "2026-11-02", "wave": "W7" } diff --git a/lib/create-space/intents-from-offer.test.ts b/lib/create-space/intents-from-offer.test.ts new file mode 100644 index 000000000..5d11b49c5 --- /dev/null +++ b/lib/create-space/intents-from-offer.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest' +import { WIZARD_INTENTS } from '@/lib/spaces/create-wizard' +import { OFFER_FALLBACK, OFFER_MAX, intentsFromOffer, offerReadsAsVenue } from '@/lib/create-space/intents-from-offer' + +describe('what one sentence says a business does', () => { + it('maps the handoff examples to goals the wizard knows', () => { + expect(intentsFromOffer('Kitchen and home design for families who love to cook.')).toEqual(['bookings']) + expect(intentsFromOffer('Weekly pottery classes and open studio nights.')).toEqual(['classes', 'events']) + expect(intentsFromOffer('1:1 coaching for first-time managers.')).toEqual(['bookings']) + for (const id of intentsFromOffer('classes, workshops, a shop, a circle and donations')) expect(WIZARD_INTENTS.some((i) => i.id === id)).toBe(true) + }) + + it('returns at most three, in catalog order, and never the goal "email" on its own', () => { + expect(intentsFromOffer('We sell ceramics, run classes, host workshops and have a club.')).toEqual(['classes', 'events', 'products']) + expect(intentsFromOffer('a newsletter')).toEqual([...OFFER_FALLBACK]) + }) + + it('falls back to the sample three when nothing matches, and ignores text past the cap', () => { + expect(intentsFromOffer('')).toEqual([...OFFER_FALLBACK]) + expect(intentsFromOffer('Something quite plain.')).toEqual([...OFFER_FALLBACK]) + expect(intentsFromOffer(`${'x'.repeat(OFFER_MAX)} workshop`)).toEqual([...OFFER_FALLBACK]) + }) + + it('tells a venue from a practitioner for the lead persona', () => { + expect(offerReadsAsVenue('A venue for weddings and gatherings in Carlsbad.')).toBe(true) + expect(offerReadsAsVenue('We rent our hall to groups.')).toBe(true) + expect(offerReadsAsVenue('1:1 coaching for first-time managers.')).toBe(false) + }) +}) diff --git a/lib/create-space/intents-from-offer.ts b/lib/create-space/intents-from-offer.ts new file mode 100644 index 000000000..bd2aa5bc5 --- /dev/null +++ b/lib/create-space/intents-from-offer.ts @@ -0,0 +1,43 @@ +// WHAT ONE SENTENCE SAYS A BUSINESS DOES. The Create a Space builder asks "What does your business offer?" and +// mocks up the site from the answer with no model: a keyword table maps the sentence to the wizard's own goals +// (WIZARD_INTENTS), and lib/create-space/preview-site.ts already turns goals into the eyebrow, the italic line, the +// button and the three offer cards. The mock-up is never presented as AI; it is a mock-up built from their words. +// PURE and client-safe, so it runs signed out and a probe can run it under node. +// +// The table is the v3 handoff's starting list (README section 3, step 2). It is reviewed against real offer +// sentences once the lead data comes in (README section 11), so a keyword here is a guess that gets replaced by +// evidence, not a rule. + +import { WIZARD_INTENTS } from '@/lib/spaces/create-wizard' + +type IntentId = (typeof WIZARD_INTENTS)[number]['id'] + +/** What a sentence can say, in the catalog's order, so two matches always come back in the same order. */ +const OFFER_WORDS: readonly [IntentId, RegExp][] = [ + ['bookings', /\b(book|session|appoint|1:1|one.on.one|coach|therap|consult|massage|heal|design|client|lesson|tutor)/i], + ['classes', /\b(class|studio|member|gym|pilates|dance|fitness|weekly)/i], + ['programs', /\b(course|program|teach|cohort|training|school)/i], + ['events', /\b(event|workshop|gathering|night|retreat|ticket|supper|party|concert|festival)/i], + ['products', /\b(sell|shop|product|handmade|ceramic|goods|store|bake|print)/i], + ['community', /\b(community|circle|group|club|meetup|neighbo)/i], + ['donations', /\b(nonprofit|non-profit|donat|charity|fundrais|volunteer)/i], +] + +/** The goals the preview falls back to when a sentence names none: the same three the empty preview shows. */ +export const OFFER_FALLBACK: readonly IntentId[] = ['events', 'bookings', 'community'] + +/** The sentence's length cap, shared with the builder's textarea. */ +export const OFFER_MAX = 160 + +/** Up to three goals a sentence names, in catalog order; the fallback three when it names none. Total. */ +export function intentsFromOffer(sentence: string): IntentId[] { + const text = sentence.slice(0, OFFER_MAX) + const found = OFFER_WORDS.filter(([, rx]) => rx.test(text)).map(([id]) => id) + return (found.length ? found : [...OFFER_FALLBACK]).slice(0, 3) +} + +/** Whether the sentence reads as a venue or a place that hosts others rather than a practitioner. Used to pick the + * lead's persona (partner, not practitioner) when the draft is saved. Total. */ +export function offerReadsAsVenue(sentence: string): boolean { + return /\b(venue|hall|space for hire|rent(al)? space|host(s|ing)? (events|groups|classes)|bar|cafe|café|restaurant|gallery|theat(re|er))\b/i.test(sentence) +} diff --git a/lib/create-space/preview-site.test.ts b/lib/create-space/preview-site.test.ts index d49fc47a2..e5c90556a 100644 --- a/lib/create-space/preview-site.test.ts +++ b/lib/create-space/preview-site.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import { WIZARD_INTENTS } from '@/lib/spaces/create-wizard' import { MEGA_THEMES } from '@/lib/theme/mega-theme' -import { INTENT_LOOK, PREVIEW_KITS, deriveSite, kitAccents, kitImagery } from '@/lib/create-space/preview-site' +import { INTENT_LOOK, PREVIEW_CLIP, PREVIEW_KITS, asSentence, clipPreview, deriveSite, kitAccents, kitImagery } from '@/lib/create-space/preview-site' const base = { name: '', handle: '', hostName: '', intents: [] as string[], kit: 'designer' as const } @@ -11,7 +11,12 @@ describe('preview site', () => { }) it('every kit has pictures; every offered kit has accents', () => { - for (const t of MEGA_THEMES) expect(kitImagery(t.id).strip).toHaveLength(3) + for (const t of MEGA_THEMES) { + expect(kitImagery(t.id).strip).toHaveLength(3) + expect(kitImagery(t.id).covers.length, t.id).toBeGreaterThan(0) + expect(kitImagery(t.id).portraits.length, t.id).toBeGreaterThan(0) + } + expect(kitImagery('designer').covers).toHaveLength(5) for (const k of PREVIEW_KITS) expect(kitAccents(k.id).length, k.id).toBeGreaterThan(0) }) @@ -33,4 +38,26 @@ describe('preview site', () => { expect(s.links[2]).toBe('About Sam') expect(s.offers.map((o) => o.n)).toEqual(['I', 'II', 'III']) }) + + it('is a ghost until step 2, then their own sentence is the tagline', () => { + expect(deriveSite(base).fromOffer).toBe(false) + const s = deriveSite({ ...base, intents: ['classes'], offer: 'weekly pottery classes and open studio nights' }) + expect(s.fromOffer).toBe(true) + expect(s.tagline).toBe('Weekly pottery classes and open studio nights.') + expect(deriveSite({ ...base, offer: 'x'.repeat(200) }).tagline).toHaveLength(PREVIEW_CLIP.tagline) + }) + + it('fills the contact strip from step 3 and leaves it empty before', () => { + expect(deriveSite(base).contact).toEqual({ visit: '', write: '', call: '' }) + const s = deriveSite({ ...base, reach: { email: 'sam@example.com', phone: '555 0100', where: 'Carlsbad' } }) + expect(s.contact).toEqual({ visit: 'Carlsbad', write: 'sam@example.com', call: '555 0100' }) + }) + + it('clips and cleans what it shows', () => { + expect(clipPreview(' **Bold** `code` — and more ', 40)).toBe('Bold code , and more') + expect(asSentence('hello there')).toBe('Hello there.') + expect(asSentence('Done!')).toBe('Done!') + expect(asSentence(' ')).toBe('') + for (const o of deriveSite(base).offers) expect(o.t.length).toBeLessThanOrEqual(PREVIEW_CLIP.offerTitle) + }) }) diff --git a/lib/create-space/preview-site.ts b/lib/create-space/preview-site.ts index d1ef6b542..67290b853 100644 --- a/lib/create-space/preview-site.ts +++ b/lib/create-space/preview-site.ts @@ -46,6 +46,10 @@ export interface KitImagery { strip: readonly [string, string, string] nav1: string cta2: string + /** Cover pictures the Photos step offers (slot s0 is the hero). A kit without its own library offers the gathering photos. */ + covers: readonly string[] + /** Host portraits the Photos step offers (slot s0 is the portrait). */ + portraits: readonly string[] } const DESIGNER_PHOTO = (name: string) => `/sites/designer/photos/${name}.webp` @@ -54,6 +58,7 @@ const FREQUENCY_PHOTO = (n: 1 | 2 | 3) => `/create-a-space/photos/pain-${n}.jpg` const FREQUENCY_IMAGERY: KitImagery = { hero: FREQUENCY_PHOTO(2), heroPos: 'center', portrait: FREQUENCY_PHOTO(1), portraitPos: 'center', strip: [FREQUENCY_PHOTO(1), FREQUENCY_PHOTO(3), FREQUENCY_PHOTO(2)], nav1: 'Offerings', cta2: 'Learn more', + 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 @@ -64,6 +69,8 @@ const KIT_IMAGERY: Partial> & Record (a.length < 2 ? (a[0] ?? '') : `${a.slice(0, -1).join(', ')} and ${a[a.length - 1]}`) @@ -173,19 +204,26 @@ export function deriveSite(input: SiteInput): PreviewSite { const handle = input.handle || 'yourspace' const host = input.hostName.trim() const nouns = picks.map((p) => INTENT_LOOK[p.id].noun) + const sentence = input.offer ? clipPreview(asSentence(input.offer), PREVIEW_CLIP.tagline) : '' return { name, - em: first?.em ?? FALLBACK_EM, + em: clipPreview(first?.em ?? FALLBACK_EM, PREVIEW_CLIP.em), nav1: imagery.nav1, cta: first?.cta ?? 'Join', ctaLong: first ? `${first.cta} now` : 'Join us', cta2: imagery.cta2, - eyebrow: picks.length ? picks.slice(0, 2).map((p) => cap(p.label.split(' ').slice(-1)[0])).join(' · ') : FALLBACK_EYEBROW, - tagline: nouns.length ? `${cap(joinList(nouns))}, all in one place.` : FALLBACK_TAGLINE, + eyebrow: clipPreview(picks.length ? picks.slice(0, 2).map((p) => cap(p.label.split(' ').slice(-1)[0])).join(' · ') : FALLBACK_EYEBROW, PREVIEW_CLIP.eyebrow), + tagline: sentence || (nouns.length ? `${cap(joinList(nouns))}, all in one place.` : FALLBACK_TAGLINE), handle, domain: `frequency.community/${handle}`, host: host || 'you', - offers, + offers: offers.map((o) => ({ ...o, t: clipPreview(o.t, PREVIEW_CLIP.offerTitle), s: clipPreview(o.s, PREVIEW_CLIP.offerDetail) })), links: [offers[1].t, 'Join the list', `About ${host ? host.split(' ')[0] : 'the host'}`], + contact: { + visit: clipPreview(input.reach?.where ?? '', 80), + write: clipPreview(input.reach?.email ?? '', 254), + call: clipPreview(input.reach?.phone ?? '', 30), + }, + fromOffer: sentence.length > 0, } } diff --git a/lib/launch/carry.test.ts b/lib/launch/carry.test.ts index 5c004401d..3b3ce9e05 100644 --- a/lib/launch/carry.test.ts +++ b/lib/launch/carry.test.ts @@ -14,6 +14,20 @@ describe('what a visitor brings to the launch', () => { expect(readCarry(undefined)).toEqual({}) }) + it('reads the v3 builder keys and drops a phone, place or photo token off-shape', () => { + const full = { line: ' Weekly pottery classes. ', phone: '+1 (760) 555-0100', where: 'Carlsbad', cover: 's2', logo: 'mono', profile: 'initials' } + expect(readCarry(full)).toEqual({ ...full, line: 'Weekly pottery classes.' }) + expect(readCarry({ phone: 'call me', where: ' ', cover: 'https://x', logo: 'drop table', profile: 's' })).toEqual({}) + expect(readCarry({ line: 'x'.repeat(200) }).line).toHaveLength(160) + expect(readCarry({ where: 'y'.repeat(100) }).where).toHaveLength(80) + }) + + it('round-trips every key and keeps the Spotlight step inside the twelve keys the hold allows', () => { + const carry = { handle: 'saltwater', theme: 'designer', accent: '2', host: 'Sam', line: 'Pottery.', phone: '555 0100', where: 'Online', cover: 'up', logo: 'word', profile: 's1' } + expect(readCarry(carryFields(carry))).toEqual(carry) + expect(Object.keys(carryFields(carry)).length + ['name', 'link'].length).toBeLessThanOrEqual(12) + }) + it('writes only the keys that have a value, and what it writes reads back whole', () => { const carry = { handle: 'saltwater', theme: 'designer', accent: '2', host: 'Sam' } expect(readCarry(carryFields(carry))).toEqual(carry) diff --git a/lib/launch/carry.ts b/lib/launch/carry.ts index 46f2f9cd8..94529cf84 100644 --- a/lib/launch/carry.ts +++ b/lib/launch/carry.ts @@ -4,7 +4,9 @@ // PURE and client-safe: no imports from the app, so a probe can run it under node. // // The Spotlight form itself reads `name`, `line` and `link`. The keys below ride beside them in the same hold: -// the handle the visitor claimed, the Theme Kit they picked, their accent, and the host's name. +// the handle the visitor claimed, the Theme Kit they picked, their accent, the host's name, and (from the v3 builder, +// ADR-1933) their one offer sentence, phone, place and photo choices. The hold keeps twelve keys per step +// (lib/launch/pending.ts), and name + link + the ten here are exactly twelve: a new key needs that cap raised first. export interface LaunchCarry { /** A Space handle, lowercase letters and digits with single hyphens inside, 2 to 40 characters. */ @@ -15,8 +17,27 @@ export interface LaunchCarry { accent?: string /** The host's name as the visitor typed it. */ host?: string + /** The one sentence they wrote about what they offer (/create-a-space step 2). The Spotlight form reads `line`. */ + line?: string + /** A phone number for the contact section: digits, spaces and + ( ) - only, up to 30 characters. */ + phone?: string + /** Where they work, as typed ("A city, or Online"), up to 80 characters. */ + where?: string + /** Which cover picture they chose on /create-a-space: a theme photo slot (s0, s1...) or their own upload (up). */ + cover?: string + /** The logo choice: the wordmark, a monogram or an upload. */ + logo?: string + /** The host photo choice: a theme portrait slot, their initials or an upload. */ + profile?: string } +/** The photo tokens the hold may carry: a slot, an upload marker, or the drawn choices. Never a file, never a URL + * (lib/launch/pending.ts holds short text only; the files stay in the visitor's browser until there is an account). */ +const PHOTO_TOKEN = /^(s\d|up|word|mono|initials)$/ +const PHONE = /^[\d\s+()-]{3,30}$/ +const LINE_MAX = 160 +const WHERE_MAX = 80 + const HANDLE = /^[a-z0-9]+(-[a-z0-9]+)*$/ const THEME = /^[a-z0-9-]{1,40}$/ @@ -34,6 +55,16 @@ export function readCarry(answers: Record | undefined | null): L if (typeof answers?.accent === 'string' && /^\d{1,2}$/.test(answers.accent)) carry.accent = answers.accent const host = typeof answers?.host === 'string' ? answers.host.replace(/\s+/g, ' ').trim().slice(0, 80) : '' if (host) carry.host = host + const line = typeof answers?.line === 'string' ? answers.line.replace(/\s+/g, ' ').trim().slice(0, LINE_MAX) : '' + if (line) carry.line = line + const phone = typeof answers?.phone === 'string' ? answers.phone.trim() : '' + if (phone && PHONE.test(phone)) carry.phone = phone + const where = typeof answers?.where === 'string' ? answers.where.replace(/\s+/g, ' ').trim().slice(0, WHERE_MAX) : '' + if (where) carry.where = where + for (const key of ['cover', 'logo', 'profile'] as const) { + const v = answers?.[key] + if (typeof v === 'string' && PHOTO_TOKEN.test(v)) carry[key] = v + } return carry } @@ -44,5 +75,11 @@ export function carryFields(carry: LaunchCarry): Record { if (carry.theme) out.theme = carry.theme if (carry.accent) out.accent = carry.accent if (carry.host) out.host = carry.host + if (carry.line) out.line = carry.line + if (carry.phone) out.phone = carry.phone + if (carry.where) out.where = carry.where + if (carry.cover) out.cover = carry.cover + if (carry.logo) out.logo = carry.logo + if (carry.profile) out.profile = carry.profile return out } diff --git a/lib/launch/clock.test.ts b/lib/launch/clock.test.ts index 30be17a50..047ec82dc 100644 --- a/lib/launch/clock.test.ts +++ b/lib/launch/clock.test.ts @@ -1,5 +1,14 @@ import { describe, expect, it } from 'vitest' -import { applyBeat, BEAT_EVERY_MS, clockView } from './clock' +import { applyBeat, BEAT_EVERY_MS, CLOCK_BUDGET_MS, clockView, formatClock } from './clock' + +describe('formatClock', () => { + it('reads m:ss and rounds a part second up, from the twelve minute budget', () => { + expect(formatClock(CLOCK_BUDGET_MS)).toBe('12:00') + expect(formatClock(59_001)).toBe('1:00') + expect(formatClock(0)).toBe('0:00') + expect(formatClock(-5)).toBe('0:00') + }) +}) const T = new Date('2026-10-29T12:00:00Z') const ago = (ms: number) => new Date(T.getTime() - ms).toISOString() diff --git a/lib/launch/clock.ts b/lib/launch/clock.ts index df2829f36..933d44b1d 100644 --- a/lib/launch/clock.ts +++ b/lib/launch/clock.ts @@ -5,8 +5,6 @@ // never more than thirty seconds, so a closed tab cannot run the clock. It never shames: past twelve minutes the clock // keeps counting as bonus time and the person keeps going. PURE (no app imports), so a probe can run it under node. -const CLOCK_BUDGET_MS = 12 * 60 * 1000 - /** How often the open page reports in while the person is working. */ export const BEAT_EVERY_MS = 15_000 /** The most one heartbeat may add. Twice the beat, so one late beat is forgiven and a gap is not. */ @@ -54,7 +52,11 @@ export interface ClockView { running: boolean } -function formatClock(ms: number): string { +/** The twelve minutes the clock counts down from. /create-a-space shows the same number in its H1 (ADR-1933). */ +export const CLOCK_BUDGET_MS = 12 * 60 * 1000 + +/** m:ss from milliseconds, rounded up to the next second. Shared with the /create-a-space hero so both clocks agree. */ +export function formatClock(ms: number): string { const total = Math.max(0, Math.ceil(ms / 1000)) return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, '0')}` } From 29720eaea5a829f302551b7585d444097d1f74ea Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 20:09:00 +0000 Subject: [PATCH 04/26] Shrink the create-a-space design-system baseline: four handoff lines now live in the repo The intake test reports a baseline entry that no longer fires as healed, and the baseline may only shrink, so the four copy ids the v3 page now carries leave it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012BMmZXP78ds6DqD6ywCjf6 --- docs/design-systems/create-a-space/baseline.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/design-systems/create-a-space/baseline.json b/docs/design-systems/create-a-space/baseline.json index eecdc7224..d9d3870be 100644 --- a/docs/design-systems/create-a-space/baseline.json +++ b/docs/design-systems/create-a-space/baseline.json @@ -6,7 +6,6 @@ "copy:Create a Space.dc.html#08f59e8c", "copy:Create a Space.dc.html#0c0d12db", "copy:Create a Space.dc.html#0c61f2fc", - "copy:Create a Space.dc.html#11831400", "copy:Create a Space.dc.html#121a8c17", "copy:Create a Space.dc.html#1f279624", "copy:Create a Space.dc.html#210be855", @@ -37,15 +36,12 @@ "copy:Create a Space.dc.html#7c5ff581", "copy:Create a Space.dc.html#7c7e14f8", "copy:Create a Space.dc.html#7ce9a661", - "copy:Create a Space.dc.html#7e09bca9", "copy:Create a Space.dc.html#7ea3e57c", "copy:Create a Space.dc.html#7fd275a1", "copy:Create a Space.dc.html#8aa6f66a", - "copy:Create a Space.dc.html#8ce5112c", "copy:Create a Space.dc.html#8f182272", "copy:Create a Space.dc.html#90b0683d", "copy:Create a Space.dc.html#9368a898", - "copy:Create a Space.dc.html#94328140", "copy:Create a Space.dc.html#9629a93b", "copy:Create a Space.dc.html#9c00a861", "copy:Create a Space.dc.html#9c55b089", From 79195daad231b6e47623444fc045a39fe690a373 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 19:59:09 +0000 Subject: [PATCH 05/26] LIVE-1965: Create a Space v3 hero builder: intro and building modes, five step cards, saved state, steps bar and dock The hero is the whole builder (docs/design-systems/create-a-space v3). The H1 becomes the clock, step cards come and go in the left column while the preview fills in, saved steps collapse to pills, the steps bar reads built never live. Draft v2 shape with v1 upgrade, holdFor carries the new keys, deriveSite has a ghost state, FAQ follows the handoff. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012BMmZXP78ds6DqD6ywCjf6 (cherry picked from commit 8ecfcd98224bef5b28e12e772bbdeed1553e96c2) --- .../create-a-space/actions.test.ts | 2 +- .../create-space/create-space-builder.tsx | 380 --------------- .../pages/create-space/create-space-claim.tsx | 195 ++++---- .../create-space/create-space-closing-cta.tsx | 28 ++ .../create-space/create-space-sections.tsx | 31 +- .../pages/create-space/create-space-steps.tsx | 274 +++++++++++ .../create-space/create-space-studio.tsx | 457 ++++++++++++++---- .../site/pages/create-space/create-space.css | 389 ++++++++------- .../site/pages/create-space/space-preview.tsx | 67 ++- .../create-a-space/baseline.json | 45 +- docs/ledger/rows/LIVE-1965.json | 9 +- lib/create-space/builder.test.ts | 77 ++- lib/create-space/builder.ts | 148 ++++-- lib/create-space/handoff.test.ts | 18 +- lib/create-space/handoff.ts | 27 +- lib/create-space/preview-site.test.ts | 6 + lib/create-space/preview-site.ts | 26 +- 17 files changed, 1267 insertions(+), 912 deletions(-) delete mode 100644 components/frequency/site/pages/create-space/create-space-builder.tsx create mode 100644 components/frequency/site/pages/create-space/create-space-closing-cta.tsx create mode 100644 components/frequency/site/pages/create-space/create-space-steps.tsx diff --git a/app/(marketing)/create-a-space/actions.test.ts b/app/(marketing)/create-a-space/actions.test.ts index 6850842a5..1922a1189 100644 --- a/app/(marketing)/create-a-space/actions.test.ts +++ b/app/(marketing)/create-a-space/actions.test.ts @@ -49,7 +49,7 @@ vi.mock('@/lib/spaces/store', () => ({ import { claimWithEmail, claimWithGoogle, continueSignedIn } from './actions' const draft = (over: Record = {}) => - serializeDraft({ ...emptyDraft('designer', true), name: 'Saltwater Collective', slug: '', goals: ['events'], hostName: 'Sam Rivera', done: { name: true, look: true, goals: true, host: true }, ...over }) + serializeDraft({ ...emptyDraft('designer', true), name: 'Saltwater Collective', slug: '', goals: ['events'], hostName: 'Sam Rivera', done: { name: true, offer: true, contact: true, photos: false }, ...over }) const form = (fields: Record) => { const fd = new FormData() diff --git a/components/frequency/site/pages/create-space/create-space-builder.tsx b/components/frequency/site/pages/create-space/create-space-builder.tsx deleted file mode 100644 index b7daf4231..000000000 --- a/components/frequency/site/pages/create-space/create-space-builder.tsx +++ /dev/null @@ -1,380 +0,0 @@ -'use client' - -// /create-a-space, the builder: four moves (name, look, tools, host) beside the live preview, under a progress meter. -// A move is a card when open, a one-line row once done (tap to edit), and a dim number until its turn. The state -// lives in the island above (create-space-studio.tsx) so the hero and this section show one Space. The handle is -// checked as it is typed (use-handle-status.ts). Saving it with an account is the fifth move (create-space-claim.tsx). -// Styles: create-space.css (cs-bld-*). -import Link from 'next/link' -import { useEffect, useState } from 'react' -import { Input } from '@/components/ui/field' -import type { CSSProperties, KeyboardEvent, ReactNode } from 'react' -import { BookOpen, Check, Clock, Globe, Heart, Mail, ShoppingBag, Ticket, Users, UsersRound } from 'lucide-react' -import { ClaimCard, SavedCard } from '@/components/frequency/site/pages/create-space/create-space-claim' -import type { Saved } from '@/components/frequency/site/pages/create-space/create-space-claim' -import { SpacePreview } from '@/components/frequency/site/pages/create-space/space-preview' -import { useHandleStatus } from '@/components/frequency/site/pages/create-space/use-handle-status' -import { - BUILDER_MOVES, claimedHandle, cleanHandleInput, handleFromName, handleOf, hasProgress, isValidHandle, meterLabel, meterOf, nextOpenMove, - progressOf, startingModeFor, trackFraction, -} from '@/lib/create-space/builder' -import type { BuilderDraft, BuilderMove } from '@/lib/create-space/builder' -import { suggestHandle } from '@/lib/create-space/handle' -import { PREVIEW_KITS, deriveSite, kitAccents, kitImagery } from '@/lib/create-space/preview-site' -import { WIZARD_INTENTS } from '@/lib/spaces/create-wizard' - -const GOAL_ICON = { bookings: Clock, classes: UsersRound, programs: BookOpen, events: Ticket, products: ShoppingBag, community: Users, email: Mail, donations: Heart } as const - -const TITLES: Record = { - name: { todo: 'Name your Space', label: 'Name' }, - look: { todo: 'Make it look like you', label: 'Look' }, - goals: { todo: 'What will happen there', label: 'Tools' }, - host: { todo: "Who's hosting", label: 'Host' }, -} - -const lower = (t: string, i: number) => (i === 0 ? t : t.charAt(0).toLowerCase() + t.slice(1)) -const joinList = (items: readonly string[]) => { - const a = items.map(lower) - return a.length < 2 ? (a[0] ?? '') : `${a.slice(0, -1).join(', ')} and ${a[a.length - 1]}` -} - -interface BuilderProps { - draft: BuilderDraft - update: (patch: Partial) => void - markDone: (move: BuilderMove, patch?: Partial) => void - reset: () => void - /** The draft has left the browser (the link was sent, or the visitor is on their way to Google): stop keeping it. */ - onHandOver: () => void - /** The visitor went back to change something after saving: keep the draft again. */ - onReopen: () => void - vars: Record - accentFill: string - /** The visitor came without a kit: offer the full-size Theme Kit pages. */ - cold: boolean -} - -function Rows({ children }: { children: ReactNode }) { - return
{children}
-} - -function DoneRow({ move, value, onEdit }: { move: BuilderMove; value: string; onEdit: () => void }) { - return ( - - ) -} - -function TodoRow({ n, label }: { n: number; label: string }) { - return ( -
- {n} - {label} -
- ) -} - -function Card({ n, title, em, lead, children }: { n: string; title: string; em: string; lead: string; children: ReactNode }) { - return ( -
-
- {n} -

{title} {em}

-

{lead}

-
- {children} -
- ) -} - -export function CreateSpaceBuilder({ draft, update, markDone, reset, onHandOver, onReopen, vars, accentFill, cold }: BuilderProps) { - const [open, setOpen] = useState(() => nextOpenMove(draft.done, -1) ?? BUILDER_MOVES.length) - const [handleOpen, setHandleOpen] = useState(false) - const [nameErr, setNameErr] = useState('') - const [hostErr, setHostErr] = useState(false) - const [toast, setToast] = useState('') - const [moved, setMoved] = useState(false) - const [saved, setSaved] = useState(null) - - useEffect(() => { - if (!toast) return - const t = setTimeout(() => setToast(''), 2600) - return () => clearTimeout(t) - }, [toast]) - - const go = (i: number) => { - setMoved(true); setNameErr(''); setHostErr(false); setOpen(i) - // Going back to change something after saving: the draft is the visitor's again, and saving again re-sends it. - if (saved) { setSaved(null); onReopen() } - } - const advance = (done: Record, from: number) => go(nextOpenMove(done, from) ?? BUILDER_MOVES.length) - - const accents = kitAccents(draft.kit) - const accent = accents[draft.accentAt] ?? accents[0] - const kitLabel = PREVIEW_KITS.find((k) => k.id === draft.kit)?.label ?? draft.kit - const kitHint = PREVIEW_KITS.find((k) => k.id === draft.kit)?.hint ?? '' - const handle = handleOf(draft) - const claimed = claimedHandle(draft) - const handleOk = isValidHandle(claimed) - const handleStatus = useHandleStatus(claimed, claimed !== '') - const siteName = draft.name.trim() || 'Your Space' - const mode = startingModeFor(draft.goals) - const site = deriveSite({ name: draft.name, handle, hostName: draft.hostName, intents: draft.goals, kit: draft.kit }) - const imagery = kitImagery(draft.kit) - const pct = progressOf(draft, saved !== null) - const meter = meterOf(draft, true) - const building = BUILDER_MOVES.some((m) => !draft.done[m]) - const finish = (move: BuilderMove, patch?: Partial) => { - markDone(move, patch) - return { ...draft.done, [move]: true } - } - - const saveName = () => { - if (!draft.name.trim()) { setNameErr('Give your Space a name first.'); return } - if (!handleOk) { setNameErr('Handles use letters, numbers and single hyphens, at least two characters.'); setHandleOpen(true); return } - if (handleStatus === 'taken') { setNameErr('Pick a handle that is free to use.'); setHandleOpen(true); return } - advance(finish('name'), 0) - setToast('Name saved') - } - const saveLook = () => { advance(finish('look', { kitChosen: true }), 1); setToast(`${kitLabel} kit`) } - const saveGoals = (later: boolean) => { - const patch = later ? { goals: [] } : undefined - advance(finish('goals', patch), 2) - setToast(later || !draft.goals.length ? 'Starting simple' : `${mode.tools.length} tools switched on`) - } - const saveHost = () => { - if (!draft.hostName.trim()) { setHostErr(true); return } - advance(finish('host'), 3) - setToast('Host added') - } - const onEnter = (fn: () => void) => (e: KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); fn() } } - - const value: Record = { - name: `${siteName} · /${handle}`, - look: `${kitLabel} · ${accent.name}`, - goals: draft.goals.length ? joinList(WIZARD_INTENTS.filter((i) => draft.goals.includes(i.id)).map((i) => i.label)) : 'Starting simple', - host: draft.hostName.trim(), - } - const caption = pct >= 95 ? 'Built, and all yours.' : draft.name.trim() ? `${siteName}, coming together.` : 'Your Space, as you build it.' - - const stepFor = (move: BuilderMove, i: number): ReactNode => { - const done = draft.done[move] - if (open === i) { - if (move === 'name') { - return ( - - { setNameErr(''); update({ name: e.target.value }) }} - onKeyDown={onEnter(saveName)} - placeholder="Saltwater Collective" - maxLength={80} - autoComplete="organization" - aria-label="Space name" - /> -
- frequency.community/{handle} - - {handleStatus === 'free' ? : null} - {handleStatus === 'taken' ? Taken. Try {suggestHandle(claimed)} : null} - - -
- {handleOpen ? ( - - /spaces/ - { setNameErr(''); update({ slug: cleanHandleInput(e.target.value) }) }} - onKeyDown={onEnter(saveName)} - aria-label="Handle" - placeholder="yourhandle" - maxLength={40} - /> - - ) : null} - {nameErr ?

{nameErr}

: null} -
- Press Enter to keep going - -
-
- ) - } - if (move === 'look') { - return ( - -
- {PREVIEW_KITS.map((k) => { - const on = k.id === draft.kit - const first = kitAccents(k.id)[0] - return ( - - ) - })} -
-

{kitHint}

-
- Accent · {accent.name} -
- {accents.map((a, ai) => ( -
-
- {cold && !draft.kitChosen ?

Want to see them full size? Choose a Theme Kit

: null} -
- -
-
- ) - } - if (move === 'goals') { - return ( - -
- {WIZARD_INTENTS.map((g) => { - const on = draft.goals.includes(g.id) - const Icon = GOAL_ICON[g.id] - return ( - - ) - })} -
-
- Switching on · {mode.label} setup -
{mode.tools.map((t) => {t})}
-
-
- - -
-
- ) - } - return ( - -
- {(draft.hostName.trim()[0] ?? '?').toUpperCase()} - { setHostErr(false); update({ hostName: e.target.value }) }} - onKeyDown={onEnter(saveHost)} - placeholder="Your name" - maxLength={80} - autoComplete="name" - aria-label="Your name" - /> -
- {hostErr ?

Add the name people will know you by.

: null} -
- Press Enter to keep going - -
-
- ) - } - return done ? go(i)} /> : - } - - - return ( -
-
-
-
- Let's get started -

Start with a name.

-

Five quick moves build your Space, website and Spotlight. Then the setup walk adds your story, photos and first offering, and you're open.

-
- -
-
- {pct}% -
-
-
- {saved ? 'Saved' : meterLabel(draft)} - {toast ? {toast} : null} - {hasProgress(draft) ? : null} -
-
-
-
- {saved ? ( - Check your email - ) : building ? ( - - ) : ( - - )} -
- - - {BUILDER_MOVES.map((m, i) => stepFor(m, i))} - {saved ? ( - - ) : !building && open === BUILDER_MOVES.length ? ( -
- ) : ( - - )} -
-
- -
-
- -

{caption}

-
-
-
- ) -} diff --git a/components/frequency/site/pages/create-space/create-space-claim.tsx b/components/frequency/site/pages/create-space/create-space-claim.tsx index 19eeb7b48..67f587dc9 100644 --- a/components/frequency/site/pages/create-space/create-space-claim.tsx +++ b/components/frequency/site/pages/create-space/create-space-claim.tsx @@ -1,80 +1,72 @@ 'use client' -// /create-a-space, the fifth move: save what was built and claim the handle. A card of its own under the four moves. -// Signed out it asks for an email (a magic link) or Google, the same two doors /sign-in has, through the actions in -// app/(marketing)/create-a-space/actions.ts. Signed in there is no account to make: one button takes the answers to -// the launch page. After the link is sent the card says so ("Saved") and offers the launch page to keep going on. -// Nothing is promised that has not happened: the Space is made on the launch page, when the Spotlight is published. -// Styles: create-space.css (cs-claim-*). +// /create-a-space, the fifth step: save what was built and claim the handle (README section 3, step 5), and the +// saved state after it. Signed out there are two doors, the same two /sign-in has: Google, or a link emailed to the +// address from step 3, through the actions in app/(marketing)/create-a-space/actions.ts. Signed in there is no +// account to make: one button takes the answers to the launch page. Nothing is promised that has not happened: the +// Space is made on the launch page, when the Spotlight is published. Styles: create-space.css (cs-sum, cs-google...). import Image from 'next/image' import Link from 'next/link' import { useEffect, useState, useTransition } from 'react' -import { Input } from '@/components/ui/field' +import { FqIcon } from '@/components/frequency/icon' import { claimWithEmail, claimWithGoogle, continueSignedIn } from '@/app/(marketing)/create-a-space/actions' import { serializeDraft } from '@/lib/create-space/builder' import type { BuilderDraft } from '@/lib/create-space/builder' -import type { KitImagery } from '@/lib/create-space/preview-site' +import { joinList } from '@/lib/create-space/preview-site' export interface Saved { email: string keepBuildingHref: string | null } -interface ClaimProps { +/** Who is signed in, if anyone. The page is static, so the browser asks once the card is on screen. */ +interface Viewer { signedIn: boolean; name: string } + +function useViewer(): Viewer | null { + const [viewer, setViewer] = useState(null) + useEffect(() => { + let live = true + fetch('/api/viewer', { credentials: 'same-origin', cache: 'no-store' }) + .then((res) => (res.ok ? res.json() : null)) + .then((data) => { if (live) setViewer({ signedIn: data?.signedIn === true, name: String(data?.profile?.display_name ?? '').trim() }) }) + .catch(() => { if (live) setViewer({ signedIn: false, name: '' }) }) + return () => { live = false } + }, []) + return viewer +} + +interface SaveCardProps { draft: BuilderDraft handle: string siteName: string kitLabel: string - imagery: KitImagery + /** The cover the preview wears, for the summary row. */ + coverSrc: string + /** The honeypot was filled: nothing leaves the browser, the card only looks like it worked. */ + trapped: boolean /** The local draft has left the browser: stop keeping it. */ onHandOver: () => void onSaved: (saved: Saved) => void + /** The email from step 3 is missing or malformed: go back and fix it. */ + onNeedEmail: () => void } const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ -function Summary({ draft, handle, siteName, kitLabel, imagery }: Pick) { - return ( -
- - - - - {siteName} - frequency.community/{handle} · {kitLabel} kit · hosted by {draft.hostName.trim()} - - Ready -
- ) -} - -/** Whether the visitor already has a session. The page is static, so the browser asks once the card is on screen. */ -function useSignedIn(): boolean | null { - const [signedIn, setSignedIn] = useState(null) - useEffect(() => { - let live = true - fetch('/api/viewer', { credentials: 'same-origin', cache: 'no-store' }) - .then((res) => (res.ok ? res.json() : null)) - .then((data) => { if (live) setSignedIn(data?.signedIn === true) }) - .catch(() => { if (live) setSignedIn(false) }) - return () => { live = false } - }, []) - return signedIn -} - -export function ClaimCard(props: ClaimProps) { - const { draft, handle, onHandOver, onSaved } = props - const signedIn = useSignedIn() - const [email, setEmail] = useState('') +export function SaveCard({ draft, handle, siteName, kitLabel, coverSrc, trapped, onHandOver, onSaved, onNeedEmail }: SaveCardProps) { + const viewer = useViewer() const [err, setErr] = useState('') const [pending, start] = useTransition() + const email = draft.email.trim() + const hostName = draft.hostName.trim() || 'you' - const send = () => { - if (!EMAIL.test(email.trim())) { setErr('Enter a full email address.'); return } + const sendLink = () => { + if (!EMAIL.test(email)) { onNeedEmail(); return } setErr('') + if (trapped) { onSaved({ email, keepBuildingHref: null }); return } start(async () => { const data = new FormData() - data.set('email', email.trim()) + data.set('email', email) data.set('draft', serializeDraft(draft)) const res = await claimWithEmail(data) if (!res.ok) { setErr(res.error); return } @@ -96,37 +88,27 @@ export function ClaimCard(props: ClaimProps) { } return ( -
-
- 5 of 5 · last one -

- {signedIn ? <>Take it to launch. : <>Save it. Claim /{handle}.} -

-

- {signedIn ? 'You already have an account, so there is nothing to make. Your name, handle, look and tools come with you.' : 'One step keeps everything you just built. No password, no card.'} -

+
+

Save it. Claim /{handle}.

+
+ + + {siteName} + {kitLabel} theme · hosted by {hostName} + + Draft
- - - {signedIn ? ( -
- {err ?

{err}

: null} - -
+ {err ?

{err}

: null} + {viewer?.signedIn ? ( + <> + +

You are signed in, so there is no account to make. Your name, handle, look and tools come with you to the launch page.

+ ) : ( <> -
{ e.preventDefault(); send() }}> - - {err ?

{err}

: null} - -
- -
+ { if (trapped) { e.preventDefault(); onSaved({ email, keepBuildingHref: null }); return } onHandOver() }}> -
-
-

Already a member? Use the same email or Google account you joined with.

-

Saving makes your free account. You agree to the Terms and Privacy.

-
+ +

No password, no card. Saving makes your free account and host profile. You agree to the Terms and Privacy.

)} -
+ ) } -interface SavedProps { +interface SavedCardProps { draft: BuilderDraft - siteName: string - kitLabel: string - toolsLabel: string + /** The first offering's title, from the preview. */ + firstOffer: string + tools: readonly string[] saved: Saved + trapped: boolean } -/** After the link is sent: what was kept, and the one page a visitor can keep going on while the email travels. */ -export function SavedCard({ draft, siteName, kitLabel, toolsLabel, saved }: SavedProps) { +/** After the link is sent: what the launch page adds next, the one page to keep going on, and a resend. */ +export function SavedCard({ draft, firstOffer, tools, saved, trapped }: SavedCardProps) { const [again, setAgain] = useState<'idle' | 'sent' | 'failed'>('idle') const [pending, start] = useTransition() const resend = () => { + if (trapped) { setAgain('sent'); return } start(async () => { const data = new FormData() data.set('email', saved.email) @@ -169,33 +151,28 @@ export function SavedCard({ draft, siteName, kitLabel, toolsLabel, saved }: Save }) } - const kept = [ - { k: 'Spotlight', v: `${siteName}, ready for one line about what you do.` }, - { k: 'Website', v: `Dressed in the ${kitLabel} kit.` }, - { k: 'Tools', v: toolsLabel }, - ] + const walk = [ + { icon: 'edit', label: 'Your story', sub: 'A few lines on who you are and who it is for.' }, + { icon: 'ticket', label: 'Your first offering', sub: `${firstOffer}, with a time and a price.` }, + { icon: 'check', label: 'Switched on for you', sub: joinList(tools) }, + ] as const return ( -
-
- Saved -

{siteName} is saved.

-

- We sent a link to {saved.email}. Open it when you are ready and your answers are waiting for you. Nothing goes public until you say so. -

-
-
    - {kept.map((row) => ( -
  • {row.k}{row.v}
  • +
    + Draft saved +
    + {walk.map((w) => ( +
    + + {w.label}{w.sub} +
    ))} -
-
- {saved.keepBuildingHref ? Keep building : null} - {again === 'sent' ? Sent again : ( - - )} - {again === 'failed' ? That did not send. Try again in a few minutes. : null}
-
+
+ {saved.keepBuildingHref ? Keep building : null} + {again === 'sent' ? Sent again : } + {again === 'failed' ? That did not send. Try again in a few minutes. : null} +
+ ) } diff --git a/components/frequency/site/pages/create-space/create-space-closing-cta.tsx b/components/frequency/site/pages/create-space/create-space-closing-cta.tsx new file mode 100644 index 000000000..80386d52e --- /dev/null +++ b/components/frequency/site/pages/create-space/create-space-closing-cta.tsx @@ -0,0 +1,28 @@ +'use client' + +// The closing call on /create-a-space: "Start my Free Space" for a new visitor, "Finish my launch" when this browser +// holds a draft. Scrolls back to the builder; the builder's own island takes it from there. A client component so +// the label can read the draft, after mount, without the server markup ever differing from the cold render. +import { useEffect, useState } from 'react' +import { FqIcon } from '@/components/frequency/icon' +import { hasProgress, readStoredDraft } from '@/lib/create-space/builder' + +export function ClosingCta() { + const [resumes, setResumes] = useState(false) + useEffect(() => { + try { + const d = readStoredDraft((k) => window.localStorage.getItem(k), null) + // eslint-disable-next-line react-hooks/set-state-in-effect -- reading the browser's own storage once, after hydration + if (d && hasProgress(d)) setResumes(true) + } catch { /* storage blocked: the cold label stands */ } + }, []) + return ( + { e.preventDefault(); window.scrollTo({ top: 0, behavior: 'smooth' }); window.setTimeout(() => document.getElementById('cs-name')?.focus({ preventScroll: true }), 420) }} + > + {resumes ? 'Finish my launch' : 'Start my Free Space'} + + ) +} diff --git a/components/frequency/site/pages/create-space/create-space-sections.tsx b/components/frequency/site/pages/create-space/create-space-sections.tsx index 55475cf9f..bff6b8700 100644 --- a/components/frequency/site/pages/create-space/create-space-sections.tsx +++ b/components/frequency/site/pages/create-space/create-space-sections.tsx @@ -6,6 +6,7 @@ import Image from 'next/image' import Link from 'next/link' import { FqIcon } from '@/components/frequency/icon' import type { FqIconName } from '@/components/frequency/icon' +import { ClosingCta } from '@/components/frequency/site/pages/create-space/create-space-closing-cta' const PAINS = [ { @@ -43,32 +44,32 @@ const TOOLS: { name: string; body: string; icon: FqIconName }[] = [ const FAQ = [ { - q: 'How long does it really take?', - a: 'The target is about 12 minutes. A couple of minutes here builds your Space, and the setup walk covers your story, photos and first offering. Have a few photos handy and you can finish in one sitting. If you take payments, connecting payouts adds a few minutes.', + q: 'Is my site public while I build?', + a: 'No. Your website, Spotlight and host profile stay a private draft that only you can see. They go public when you publish from the launch page, after you open the link or continue with Google.', }, { - q: 'Does it cost anything to start?', - a: 'No. Every Space starts on the free plan, with no card. Plans and prices live on the pricing page, and you can change plan from your Space’s billing page.', + q: 'How long does it really take?', + a: 'About 12 minutes for most people. A few minutes here builds your Space, and the launch page covers your story, prices and first offering.', }, { - q: 'Do I have to wait for the email?', - a: 'No. Your Space is saved the moment you add your email, and you can keep building. Opening the link makes it public. Continue with Google and it goes live straight away.', + q: 'What if I do not have photos yet?', + a: 'Keep the photos picked for your theme. Swap in your own whenever you have them.', }, { - q: 'Can I change the look later?', - a: 'Yes. Switch Theme Kits or your accent any time. Your website, Spotlight and email change together.', + q: 'Does it cost anything to start?', + a: 'No. Every Space starts on the free plan, with no card. Plans and prices live on the pricing page.', }, { - q: 'Who can find my Space?', - a: 'You choose. A listed Space shows in the Spaces directory. A private Space is only open to you and your members.', + q: 'Can I come back later?', + a: 'Yes. Your draft saves in this browser as you go. Once you save it with your email, the link brings you back to it on any device.', }, { - q: 'Do I need a website already?', - a: 'No. Your Space comes with a website and a Spotlight page in the look you pick.', + q: 'Can I change the look later?', + a: 'Yes. Switch Theme Kits or your accent any time. Your website, Spotlight and email change together.', }, { q: 'Can I bring my own domain?', - a: 'Yes. Your website can run on a domain you own. You connect it from your Space’s settings once you are set up.', + a: 'Yes. Connect a domain you own from your Space settings once you are set up.', }, ] @@ -170,8 +171,8 @@ export function Closing() {