fix(tier2-g): warmer Gate copy, generic example, plain-language advanced controls
Round-three pass on the Gate card based on the latest UX feedback. - Tagline: "Keeps your guest list yours" -> "Only your guests get in" - Walk-through example now uses a neutral placeholder name (Sam) instead of "Aunty Patience". GuestGuard's audience is wedding couples, corporate planners, and party hosts as much as family organisers, and Sam is at home in any of those contexts. - The four-step "How does the Gate work?" expander was lightly rewritten for warmth and to remove em-dashes; the mechanism it describes is unchanged. - Advanced strictness controls now open with a real explanation rather than a one-liner about "band thresholds". The intro maps the three sliders to the three reactions (watch / flag / refuse), explains what the 0-100 numbers mean, and reassures hosts that the presets above are still fine for almost everyone. Each slider also gets a one-line caption tying its level dot (green / amber / red) to the user-visible effect. - The trusted-networks "What this is and isn't" panel had its quotes unwound into natural sentences; the empty-state copy was rewritten in the second person; "No decisions to review yet" reworded. - Pass through every user-visible string and replaced em-dashes with periods, commas, or natural rephrasing. Em-dashes only remain in code comments now (developer-facing, not on screen). No behaviour changes; no backend changes; no API changes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -53,7 +53,7 @@ const PRESETS: Preset[] = [
|
|||||||
{
|
{
|
||||||
id: 'relaxed',
|
id: 'relaxed',
|
||||||
label: 'Relaxed',
|
label: 'Relaxed',
|
||||||
description: "Casual party — fine if friends share their link with each other.",
|
description: "Casual party. It's fine if a few friends share their link with each other.",
|
||||||
medium: 50, high: 80, block: 95,
|
medium: 50, high: 80, block: 95,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -65,7 +65,7 @@ const PRESETS: Preset[] = [
|
|||||||
{
|
{
|
||||||
id: 'strict',
|
id: 'strict',
|
||||||
label: 'Strict',
|
label: 'Strict',
|
||||||
description: "Wedding or private event — uninvited plus-ones are a problem and the guest list matters.",
|
description: "Wedding or private event. Uninvited plus-ones are a problem and the guest list really matters.",
|
||||||
medium: 25, high: 50, block: 70,
|
medium: 25, high: 50, block: 70,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -270,30 +270,30 @@ function verdictLabel(v: string) {
|
|||||||
<header class="mb-3 flex items-center justify-between">
|
<header class="mb-3 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-lg font-semibold">Gate</h2>
|
<h2 class="text-lg font-semibold">Gate</h2>
|
||||||
<p class="text-xs text-zinc-500">Keeps your guest list yours.</p>
|
<p class="text-xs text-zinc-500">Only your guests get in.</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Value-prop paragraph. This is the *point* of the feature in plain
|
<!-- Value-prop paragraph. Plain language, no em-dashes, no example
|
||||||
words. The user named the actual pain ("preventing multiple sharing
|
names. Hits the actual pain (forwarded / shared invitation
|
||||||
of events to uninvited guests") — that phrase lives here verbatim. -->
|
links) head-on. -->
|
||||||
<div class="mb-4 rounded-lg border border-brand-700/40 bg-brand-500/[0.04] p-4 text-sm leading-relaxed">
|
<div class="mb-4 rounded-lg border border-brand-700/40 bg-brand-500/[0.04] p-4 text-sm leading-relaxed">
|
||||||
<p class="text-zinc-200">
|
<p class="text-zinc-200">
|
||||||
Every guest gets their own personal invitation link — only meant for them.
|
Every guest gets their own personal invitation link. Only one person was meant to use it.
|
||||||
The <strong class="text-brand-300">Gate</strong> watches each link in the
|
The <strong class="text-brand-300">Gate</strong> quietly watches each link, so when
|
||||||
background and stops <strong>forwarded or shared invitations</strong>
|
someone forwards or shares an invitation, the people it ends up with can't actually use it.
|
||||||
from being used by people who weren't on your list.
|
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2 text-xs text-zinc-400">
|
<p class="mt-2 text-xs text-zinc-400">
|
||||||
Real invited guests don't notice it. You don't need to set anything up
|
Your invited guests won't notice a thing. There's nothing you need to set up for this
|
||||||
for it to work — the Gate is on by default with sensible settings.
|
to work; the Gate runs in the background with sensible defaults from the moment your
|
||||||
|
event is live.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- "How does this work?" — collapsed by default so the page stays
|
<!-- "How does this work?" — collapsed by default so the page stays
|
||||||
scannable. The expander walks through the actual mechanism in
|
scannable. Plain-language walkthrough; uses a generic placeholder
|
||||||
plain language so curious / sceptical hosts can see what we're
|
name (Sam) rather than family vocabulary so the example fits
|
||||||
doing without reading docs. -->
|
wedding couples, party hosts, and corporate planners equally. -->
|
||||||
<details class="group mb-6 rounded-lg border border-zinc-800 bg-zinc-950">
|
<details class="group mb-6 rounded-lg border border-zinc-800 bg-zinc-950">
|
||||||
<summary class="flex cursor-pointer items-center justify-between p-3 text-sm font-medium text-zinc-200">
|
<summary class="flex cursor-pointer items-center justify-between p-3 text-sm font-medium text-zinc-200">
|
||||||
<span class="flex items-center gap-2">
|
<span class="flex items-center gap-2">
|
||||||
@@ -307,46 +307,49 @@ function verdictLabel(v: string) {
|
|||||||
</svg>
|
</svg>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="space-y-3 border-t border-zinc-900 p-4 text-sm text-zinc-300">
|
<div class="space-y-3 border-t border-zinc-900 p-4 text-sm text-zinc-300">
|
||||||
|
<p class="text-zinc-400">
|
||||||
|
Here's the short version, using "Sam" as a stand-in for any one of your invited guests.
|
||||||
|
</p>
|
||||||
<ol class="space-y-3">
|
<ol class="space-y-3">
|
||||||
<li class="flex gap-3">
|
<li class="flex gap-3">
|
||||||
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-brand-500/15 text-xs font-semibold text-brand-300">1</span>
|
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-brand-500/15 text-xs font-semibold text-brand-300">1</span>
|
||||||
<p>
|
<p>
|
||||||
When you send Aunty Patience her invitation, she gets her own personal link.
|
When you send Sam an invitation, the link inside it is just for Sam. Nobody else
|
||||||
That link only belongs to her.
|
has the same link.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex gap-3">
|
<li class="flex gap-3">
|
||||||
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-brand-500/15 text-xs font-semibold text-brand-300">2</span>
|
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-brand-500/15 text-xs font-semibold text-brand-300">2</span>
|
||||||
<p>
|
<p>
|
||||||
The first time Aunty opens her link, the Gate quietly remembers a few details:
|
The first time Sam opens it, the Gate quietly notes a few details about the visit:
|
||||||
her phone or browser, the general area she connected from, and the network shape.
|
the phone or browser Sam is on, roughly where Sam is in the world, and the kind of
|
||||||
That becomes her "I'm me" signature.
|
network Sam is connected to. Together those become Sam's "this is really me" picture.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex gap-3">
|
<li class="flex gap-3">
|
||||||
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-brand-500/15 text-xs font-semibold text-brand-300">3</span>
|
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-brand-500/15 text-xs font-semibold text-brand-300">3</span>
|
||||||
<p>
|
<p>
|
||||||
Every time someone clicks Aunty's link after that — including Aunty herself —
|
Every later click on Sam's link, including Sam coming back to update their reply,
|
||||||
the Gate compares them to her signature. Same phone, similar location?
|
gets compared to that first visit. If it looks like the same person, they go straight
|
||||||
They sail through. No signature yet? They become Aunty's signature on the first visit.
|
through with no friction at all.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex gap-3">
|
<li class="flex gap-3">
|
||||||
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-brand-500/15 text-xs font-semibold text-brand-300">4</span>
|
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-brand-500/15 text-xs font-semibold text-brand-300">4</span>
|
||||||
<p>
|
<p>
|
||||||
If someone with a totally different phone on a totally different network tries to
|
If a completely different device on a completely different network tries to use Sam's
|
||||||
use Aunty's link — like a friend Aunty forwarded it to — the Gate notices. Depending
|
link, say because Sam forwarded it to a friend, the Gate notices. Depending on how
|
||||||
on your strictness setting it'll either flag them for your review, or refuse them
|
strict you've set it, the Gate will either flag that click for you to review, or stop
|
||||||
outright.
|
the RSVP from going through at all.
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<p class="rounded-md border border-zinc-800 bg-zinc-900/60 p-3 text-xs text-zinc-400">
|
<p class="rounded-md border border-zinc-800 bg-zinc-900/60 p-3 text-xs text-zinc-400">
|
||||||
<strong class="text-zinc-300">Worth knowing:</strong> guests can switch from mobile data
|
<strong class="text-zinc-300">Worth knowing:</strong> guests can switch between Wi-Fi
|
||||||
to Wi-Fi or change rooms without being flagged — the Gate only cares about
|
and mobile data, change rooms, or open their link a few days later without being
|
||||||
<em>meaningful</em> differences from the original visit, not normal day-to-day variation.
|
flagged. The Gate only cares about <em>meaningful</em> differences from that first
|
||||||
If a real guest does ever get flagged (it happens), you can clear them with one click
|
visit, not normal day-to-day variation. And if a real guest of yours ever does end up
|
||||||
in <em>Recent reviews</em> below.
|
flagged, you can clear them with one click under <em>Recent reviews</em> below.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@@ -393,7 +396,11 @@ function verdictLabel(v: string) {
|
|||||||
You're running with custom strictness settings (set under Advanced).
|
You're running with custom strictness settings (set under Advanced).
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Advanced — sliders for power users who want to dial individual bands. -->
|
<!-- Advanced — three sliders that map directly to the engine's
|
||||||
|
band thresholds. Heavy intro copy is intentional: most
|
||||||
|
hosts who open this are curious rather than expert, and a
|
||||||
|
short plain-language explanation here keeps them from
|
||||||
|
feeling lost. -->
|
||||||
<details class="group mt-4 rounded-lg border border-zinc-800 bg-zinc-950">
|
<details class="group mt-4 rounded-lg border border-zinc-800 bg-zinc-950">
|
||||||
<summary class="flex cursor-pointer items-center justify-between p-3 text-sm text-zinc-300">
|
<summary class="flex cursor-pointer items-center justify-between p-3 text-sm text-zinc-300">
|
||||||
<span>Advanced strictness controls</span>
|
<span>Advanced strictness controls</span>
|
||||||
@@ -402,40 +409,70 @@ function verdictLabel(v: string) {
|
|||||||
</svg>
|
</svg>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="space-y-4 border-t border-zinc-900 p-4">
|
<div class="space-y-4 border-t border-zinc-900 p-4">
|
||||||
<p class="text-xs text-zinc-500">
|
<div class="space-y-2 text-xs text-zinc-400">
|
||||||
These directly drive the band thresholds (0–100). The presets above just write
|
<p class="text-zinc-300">
|
||||||
sensible triples for you.
|
For most hosts, the four presets above are all you need. These sliders are
|
||||||
|
here if you want to fine-tune exactly when each reaction kicks in.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
As a click on a guest's link looks more and more different from their first
|
||||||
|
visit, the Gate moves through three reactions: watch silently, flag for your
|
||||||
|
review, or refuse the RSVP. The numbers below (from 0 to 100) are how much
|
||||||
|
difference is enough to trigger each one. Lower numbers make the Gate more
|
||||||
|
sensitive; higher numbers make it more forgiving.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
<label class="block text-sm">
|
<label class="block text-sm">
|
||||||
<span class="mb-1 flex items-center justify-between text-xs text-zinc-400">
|
<span class="mb-1 flex items-center justify-between text-xs text-zinc-400">
|
||||||
<span>Watch from</span>
|
<span class="flex items-center gap-1.5">
|
||||||
|
<span class="inline-block h-2 w-2 rounded-full bg-brand-500"></span>
|
||||||
|
Watch from
|
||||||
|
</span>
|
||||||
<span class="font-mono tabular-nums text-zinc-300">{{ thresholds.medium }}</span>
|
<span class="font-mono tabular-nums text-zinc-300">{{ thresholds.medium }}</span>
|
||||||
</span>
|
</span>
|
||||||
<input v-model.number="thresholds.medium" type="range" min="0" max="100"
|
<input v-model.number="thresholds.medium" type="range" min="0" max="100"
|
||||||
:disabled="!canEdit" class="w-full accent-brand-500"
|
:disabled="!canEdit" class="w-full accent-brand-500"
|
||||||
@change="saveThresholds()"
|
@change="saveThresholds()"
|
||||||
@input="clampOrder('medium')" />
|
@input="clampOrder('medium')" />
|
||||||
|
<p class="mt-1 text-[11px] leading-snug text-zinc-500">
|
||||||
|
When the Gate starts paying closer attention. Quiet for guests; visible only
|
||||||
|
to you in activity logs.
|
||||||
|
</p>
|
||||||
</label>
|
</label>
|
||||||
<label class="block text-sm">
|
<label class="block text-sm">
|
||||||
<span class="mb-1 flex items-center justify-between text-xs text-zinc-400">
|
<span class="mb-1 flex items-center justify-between text-xs text-zinc-400">
|
||||||
<span>Flag from</span>
|
<span class="flex items-center gap-1.5">
|
||||||
|
<span class="inline-block h-2 w-2 rounded-full bg-amber-500"></span>
|
||||||
|
Flag from
|
||||||
|
</span>
|
||||||
<span class="font-mono tabular-nums text-zinc-300">{{ thresholds.high }}</span>
|
<span class="font-mono tabular-nums text-zinc-300">{{ thresholds.high }}</span>
|
||||||
</span>
|
</span>
|
||||||
<input v-model.number="thresholds.high" type="range" min="0" max="100"
|
<input v-model.number="thresholds.high" type="range" min="0" max="100"
|
||||||
:disabled="!canEdit" class="w-full accent-amber-500"
|
:disabled="!canEdit" class="w-full accent-amber-500"
|
||||||
@change="saveThresholds()"
|
@change="saveThresholds()"
|
||||||
@input="clampOrder('high')" />
|
@input="clampOrder('high')" />
|
||||||
|
<p class="mt-1 text-[11px] leading-snug text-zinc-500">
|
||||||
|
When the Gate marks the click for your review. The RSVP still goes through;
|
||||||
|
you decide whether to clear it or flag the guest.
|
||||||
|
</p>
|
||||||
</label>
|
</label>
|
||||||
<label class="block text-sm">
|
<label class="block text-sm">
|
||||||
<span class="mb-1 flex items-center justify-between text-xs text-zinc-400">
|
<span class="mb-1 flex items-center justify-between text-xs text-zinc-400">
|
||||||
<span>Refuse from</span>
|
<span class="flex items-center gap-1.5">
|
||||||
|
<span class="inline-block h-2 w-2 rounded-full bg-red-500"></span>
|
||||||
|
Refuse from
|
||||||
|
</span>
|
||||||
<span class="font-mono tabular-nums text-zinc-300">{{ thresholds.block }}</span>
|
<span class="font-mono tabular-nums text-zinc-300">{{ thresholds.block }}</span>
|
||||||
</span>
|
</span>
|
||||||
<input v-model.number="thresholds.block" type="range" min="0" max="100"
|
<input v-model.number="thresholds.block" type="range" min="0" max="100"
|
||||||
:disabled="!canEdit" class="w-full accent-red-500"
|
:disabled="!canEdit" class="w-full accent-red-500"
|
||||||
@change="saveThresholds()"
|
@change="saveThresholds()"
|
||||||
@input="clampOrder('block')" />
|
@input="clampOrder('block')" />
|
||||||
|
<p class="mt-1 text-[11px] leading-snug text-zinc-500">
|
||||||
|
Where the Gate refuses the RSVP outright. The visitor sees a polite "this
|
||||||
|
invitation can't be used" and you get a notification.
|
||||||
|
</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -459,15 +496,17 @@ function verdictLabel(v: string) {
|
|||||||
<strong>What this is and isn't:</strong>
|
<strong>What this is and isn't:</strong>
|
||||||
</p>
|
</p>
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
Adding a network here tells the Gate "always wave through clicks coming from
|
Adding a network here tells the Gate to always wave through clicks coming from
|
||||||
this Wi-Fi — they're already with me." It's useful when you're hosting at home
|
that Wi-Fi, because the people on it are already with you. It's useful when
|
||||||
or the office and your guests will connect through your network.
|
you're hosting at home or in the office and your guests will be on your network
|
||||||
|
when they reply.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong class="text-zinc-300">It doesn't change how anyone else is treated.</strong>
|
<strong class="text-zinc-300">It doesn't change how anyone else is treated.</strong>
|
||||||
Guests connecting from their own homes, mobile data, or anywhere else still get
|
Guests connecting from their own homes, from mobile data, or from anywhere else
|
||||||
the regular check — they're not suspected of anything by default. The Gate works
|
still get the regular check. They aren't suspected of anything just because they
|
||||||
perfectly well with this list empty.
|
aren't on your network. The Gate works perfectly well with this list empty, and
|
||||||
|
most events stay that way.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -547,7 +586,7 @@ function verdictLabel(v: string) {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p v-else class="text-sm text-zinc-500">
|
<p v-else class="text-sm text-zinc-500">
|
||||||
No trusted networks — and that's fine. The Gate is doing its job.
|
You haven't added any trusted networks, and that's fine. The Gate is still doing its job.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -577,7 +616,7 @@ function verdictLabel(v: string) {
|
|||||||
and {{ feedback.length - 10 }} more.
|
and {{ feedback.length - 10 }} more.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p v-else class="text-sm text-zinc-500">No decisions to review yet — the gate hasn't flagged anyone.</p>
|
<p v-else class="text-sm text-zinc-500">Nothing to review yet. The Gate hasn't flagged anyone.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user