Group the message features by what somebody is trying to do (relay#44)
The landing page already says what the product IS in three cards and sends the detail to /messages, /files and /wallet -- that half of the issue was solved when the product pages were split out. What it never came with was anything that KEEPS it simple, and /messages had quietly reached thirteen cards in one flat list with 52 unstarted ideas still to land. Thirteen becomes three: "Saying something", "On your own time", "Knowing it stays yours". Every card is kept, none reworded -- the writing was never the problem, the quantity was. A card's title drops to an h4 now that the group owns the h3, and the two are styled on adjacent lines so they cannot drift. The test is the part that matters. A page over eight cards with no groups fails, naming the issue and saying to group rather than pile on; more than five groups fails too, because a group per feature is the same problem wearing a hat. Checked by flattening the page back: it fails and says why. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
96300b1ae7
commit
485164be26
@@ -267,6 +267,21 @@ a { color: inherit; }
|
||||
.cards-3 .card:hover { box-shadow: var(--shadow-lg); }
|
||||
.cards-3 .card-icon { margin-left: 0; }
|
||||
.card h3 { margin: 0 0 10px; font-size: 17.5px; letter-spacing: -.015em; font-weight: 700; }
|
||||
/* A card inside a group: the group owns the h3, so the card's title is an h4
|
||||
and has to keep the same look. Styled together so the two cannot drift. */
|
||||
.card h4 { margin: 0 0 10px; font-size: 17.5px; letter-spacing: -.015em; font-weight: 700; }
|
||||
/* Groups exist so the page can gain features without gaining top-level
|
||||
sections (relay#44). Quieter than an h2 band head, louder than a card. */
|
||||
.group-head {
|
||||
margin: 46px 0 22px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-2);
|
||||
text-align: center;
|
||||
}
|
||||
.group-head:first-of-type { margin-top: 30px; }
|
||||
.card p { margin: 0; color: var(--ink-2); font-size: 15px; line-height: 1.6; }
|
||||
.card-icon {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
|
||||
@@ -26,10 +26,11 @@
|
||||
accidentally end up in.
|
||||
</p>
|
||||
|
||||
<h3 class="group-head">Saying something</h3>
|
||||
<div class="cards">
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h16v10H9l-5 4z"/></svg></span>
|
||||
<h3>One-to-one and groups</h3>
|
||||
<h4>One-to-one and groups</h4>
|
||||
<p>
|
||||
Text, photos and voice notes, on your own or with everybody. Mention
|
||||
someone and they'll hear about it even if they've muted the group —
|
||||
@@ -38,7 +39,7 @@
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4l4 1 1 4-2 2a12 12 0 0 0 5 5l2-2 4 1 1 4a2 2 0 0 1-2 2A17 17 0 0 1 3 6a2 2 0 0 1 2-2"/></svg></span>
|
||||
<h3>Voice calls</h3>
|
||||
<h4>Voice calls</h4>
|
||||
<p>
|
||||
Ring someone directly. The audio is encrypted between the two phones,
|
||||
and the check that you're talking to the right person rides inside the
|
||||
@@ -47,60 +48,28 @@
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9" stroke-dasharray="3 3"/></svg></span>
|
||||
<h3>Stories</h3>
|
||||
<h4>Stories</h4>
|
||||
<p>
|
||||
Post a picture, a few words or a link, for as long as you choose —
|
||||
an hour, or a week. Only your contacts see it, and the server is never
|
||||
told how long it's meant to last.
|
||||
</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg></span>
|
||||
<h3>Chats that clear themselves</h3>
|
||||
<p>
|
||||
Set a conversation to tidy up after an hour, a day or a week. It
|
||||
applies to <strong>both</strong> phones, and the server isn't told a
|
||||
conversation is timed, let alone for how long.
|
||||
</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20v-1a6 6 0 0 1 6-6h1"/><circle cx="10.5" cy="7" r="3.5"/><path d="M17 14v4"/><path d="M17 21h.01"/></svg></span>
|
||||
<h3>No phone number</h3>
|
||||
<p>
|
||||
Sign up with a username and an email. People find you by the name they
|
||||
already call you, and your number stays out of it entirely.
|
||||
</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l7 4v5c0 4-3 7-7 9-4-2-7-5-7-9V7z"/><path d="M9 12l2 2 4-4"/></svg></span>
|
||||
<h3>You'll know if a key changes</h3>
|
||||
<p>
|
||||
If someone's phone is replaced — or somebody tries to stand in for
|
||||
them — the app stops and tells you, with one thing you can do about
|
||||
it. There's no way to wave it away, because there shouldn't be.
|
||||
</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h4l10-10a2.8 2.8 0 0 0-4-4L4 16z"/><path d="M13.5 6.5l4 4"/></svg></span>
|
||||
<h3>Fix the typo</h3>
|
||||
<h4>Fix the typo</h4>
|
||||
<p>
|
||||
Change a message after you've sent it. It turns up on the other phone
|
||||
as the same message, marked edited — not as a second one nobody can
|
||||
tell apart from the first.
|
||||
</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="6"/><path d="M20 20l-4.5-4.5"/></svg></span>
|
||||
<h3>Find what you said</h3>
|
||||
<p>
|
||||
Search across every conversation you have. It happens on your phone,
|
||||
because the server has never been able to read a word of it — which
|
||||
is also why nobody can search it but you.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
<h3 class="group-head">On your own time</h3>
|
||||
<div class="cards">
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M8 3v4M16 3v4M3 10h18"/><path d="M12 14v3l2 1"/></svg></span>
|
||||
<h3>Write now, send later</h3>
|
||||
<h4>Write now, send later</h4>
|
||||
<p>
|
||||
Say it while you're thinking of it and let it arrive at a decent
|
||||
hour. The message waits <strong>on your phone</strong>, not on the
|
||||
@@ -109,7 +78,7 @@
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6"/><path d="M10 19a2 2 0 0 0 4 0"/></svg></span>
|
||||
<h3>Nudge yourself, quiet a thread</h3>
|
||||
<h4>Nudge yourself, quiet a thread</h4>
|
||||
<p>
|
||||
Ask to be reminded to reply, or put a conversation to sleep for the
|
||||
afternoon. Both are kept on your phone — the server has no idea a
|
||||
@@ -117,17 +86,46 @@
|
||||
</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a4 4 0 0 0 6 .5l2-2a4 4 0 0 0-6-6l-1 1"/><path d="M14 11a4 4 0 0 0-6-.5l-2 2a4 4 0 0 0 6 6l1-1"/></svg></span>
|
||||
<h3>Link previews, made at your end</h3>
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg></span>
|
||||
<h4>Chats that clear themselves</h4>
|
||||
<p>
|
||||
When you share a link, <strong>your</strong> phone builds the little
|
||||
card — so the site never hears from the person you sent it to. It
|
||||
does hear from you, which is why there's a switch to turn it off.
|
||||
Set a conversation to tidy up after an hour, a day or a week. It
|
||||
applies to <strong>both</strong> phones, and the server isn't told a
|
||||
conversation is timed, let alone for how long.
|
||||
</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="6"/><path d="M20 20l-4.5-4.5"/></svg></span>
|
||||
<h4>Find what you said</h4>
|
||||
<p>
|
||||
Search across every conversation you have. It happens on your phone,
|
||||
because the server has never been able to read a word of it — which
|
||||
is also why nobody can search it but you.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
<h3 class="group-head">Knowing it stays yours</h3>
|
||||
<div class="cards">
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20v-1a6 6 0 0 1 6-6h1"/><circle cx="10.5" cy="7" r="3.5"/><path d="M17 14v4"/><path d="M17 21h.01"/></svg></span>
|
||||
<h4>No phone number</h4>
|
||||
<p>
|
||||
Sign up with a username and an email. People find you by the name they
|
||||
already call you, and your number stays out of it entirely.
|
||||
</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l7 4v5c0 4-3 7-7 9-4-2-7-5-7-9V7z"/><path d="M9 12l2 2 4-4"/></svg></span>
|
||||
<h4>You'll know if a key changes</h4>
|
||||
<p>
|
||||
If someone's phone is replaced — or somebody tries to stand in for
|
||||
them — the app stops and tells you, with one thing you can do about
|
||||
it. There's no way to wave it away, because there shouldn't be.
|
||||
</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21s-6-5.2-6-10a6 6 0 0 1 12 0c0 1.4-.5 2.8-1.3 4.2"/><circle cx="12" cy="11" r="2"/><path d="M4 4l16 16"/></svg></span>
|
||||
<h3>Photos don't say where you were</h3>
|
||||
<h4>Photos don't say where you were</h4>
|
||||
<p>
|
||||
Pictures and video you send from the photo button are rebuilt on the
|
||||
way out, so the coordinates your camera wrote in don't travel with
|
||||
@@ -136,9 +134,18 @@
|
||||
which is rather the point of a file, so check what's in it first.
|
||||
</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a4 4 0 0 0 6 .5l2-2a4 4 0 0 0-6-6l-1 1"/><path d="M14 11a4 4 0 0 0-6-.5l-2 2a4 4 0 0 0 6 6l1-1"/></svg></span>
|
||||
<h4>Link previews, made at your end</h4>
|
||||
<p>
|
||||
When you share a link, <strong>your</strong> phone builds the little
|
||||
card — so the site never hears from the person you sent it to. It
|
||||
does hear from you, which is why there's a switch to turn it off.
|
||||
</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="10" width="16" height="11" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/><path d="M12 14v3"/></svg></span>
|
||||
<h3>Lock it behind your face</h3>
|
||||
<h4>Lock it behind your face</h4>
|
||||
<p>
|
||||
Your phone is the one place your messages are readable, so it's the
|
||||
one place worth locking. Face, fingerprint or your usual PIN, and it
|
||||
@@ -147,6 +154,7 @@
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -228,6 +228,45 @@ final class PublicPagesTests: XCTestCase {
|
||||
"the page no longer states that the size it keeps is exact")
|
||||
}
|
||||
|
||||
/// 🔴 **The shape has to survive the next fifty features (relay#44).**
|
||||
///
|
||||
/// The landing page says what the product IS in three cards and sends the
|
||||
/// detail to `/messages`, `/files`, `/wallet`. That split already happened.
|
||||
/// What it did not come with was anything that KEEPS it — and `/messages`
|
||||
/// had quietly reached thirteen cards in one flat list, with 52 unstarted
|
||||
/// ideas still to land. A flat list of a hundred is a specification, and
|
||||
/// nobody reads it.
|
||||
///
|
||||
/// So features group by what a person is trying to do: the count inside a
|
||||
/// group grows freely, the number of groups does not. This test fails when
|
||||
/// a page goes back to a flat list long enough to stop being skimmable,
|
||||
/// which is the moment to add a group rather than a fourteenth card.
|
||||
func testFeaturePagesStayGroupedRatherThanFlat() throws {
|
||||
// Below this, a flat list is still fine — grouping four cards is noise.
|
||||
let flatCeiling = 8
|
||||
|
||||
for name in ["landing", "messages", "files", "wallet"] {
|
||||
let page = try String(contentsOf: views.appendingPathComponent("\(name).leaf"),
|
||||
encoding: .utf8)
|
||||
let cards = page.components(separatedBy: "<article class=\"card\">").count - 1
|
||||
let groups = page.components(separatedBy: "class=\"group-head\"").count - 1
|
||||
if cards > flatCeiling {
|
||||
XCTAssertGreaterThan(
|
||||
groups, 0,
|
||||
"\(name).leaf has \(cards) cards in a flat list. Group them by what "
|
||||
+ "somebody is trying to do — see relay#44 — rather than adding "
|
||||
+ "another card to the pile.")
|
||||
}
|
||||
// A group per card is the same problem wearing a hat.
|
||||
if groups > 0 {
|
||||
XCTAssertLessThanOrEqual(
|
||||
groups, 5,
|
||||
"\(name).leaf has \(groups) groups. The point is that the TOP level "
|
||||
+ "stays small; put the feature inside an existing group.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testTheSafetyPageCarriesAReachableContact() throws {
|
||||
let page = try String(contentsOf: views.appendingPathComponent("safety.leaf"),
|
||||
encoding: .utf8)
|
||||
|
||||
Reference in New Issue
Block a user