oficina / memoria Public

Sign-in door: take the door's copy from estilo

The door's words now come from estilo (chrome.English, feat/door-copy):
the submit reads "Sign in" (Paul, 2026-10-01; never "Email me a link"),
the help line states the link's 15-minute lifetime, the sent state says
"If that address can sign in here…" with "Use a different address", and
Home's signed-out page is the door's "You're signed out" rather than
connect's copy, which named Home (Paul, 2026-09-23: never name the hub).
Tab titles are "Sign in · Memoria", "Check your email · Memoria" and
"Signed out · Memoria"; the email subject is "Sign in to Memoria".

Deleted memoria's own copy: the marketing lede, the heading, label and
submit overrides, the sent heading/body/back link, and signinOutcome's
sent sentence (the door says it). The ?reauth=1 lede stays: it is a
state, not an introduction.

Pinned to estilo fbd14e7 (feat/door-copy); re-pin to estilo main after
that branch lands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev d42e2f572fe569194b6af1dfad85ce841f327c66 parent 97b8a23
9 files changed, +48 −36
  • go.mod +1 −1
  • go.sum +2 −2
  • internal/memoria/auth.go +1 −1
  • internal/memoria/handlers.go +10 −18
  • internal/memoria/home_render.go +5 −7
  • internal/memoria/home_router_test.go +9 −3
  • internal/memoria/templates/signin.html +1 −1
  • internal/memoriatest/signin_door_test.go +12 −2
  • internal/memoriatest/signinoutcome_test.go +7 −1

CI passed — run details

CI log
=== vet ===
go vet ./...
go: downloading amadan.net/oficina/estilo.git v0.0.0-20261001075355-fbd14e759027
=== fmt ===
=== test ===
go test ./...
?   	amadan.net/oficina/memoria/cmd/memoria	[no test files]
ok  	amadan.net/oficina/memoria/internal/memoria	3.442s
ok  	amadan.net/oficina/memoria/internal/memoria/chunk	0.004s
ok  	amadan.net/oficina/memoria/internal/memoria/fact	0.529s
?   	amadan.net/oficina/memoria/internal/memoria/icons	[no test files]
ok  	amadan.net/oficina/memoria/internal/memoria/index	0.034s
ok  	amadan.net/oficina/memoria/internal/memoria/notion	0.020s
ok  	amadan.net/oficina/memoria/internal/memoria/provider	1.127s
ok  	amadan.net/oficina/memoria/internal/memoria/query	0.003s
ok  	amadan.net/oficina/memoria/internal/memoria/seal	0.003s
ok  	amadan.net/oficina/memoria/internal/memoria/sibling	3.022s
ok  	amadan.net/oficina/memoria/internal/memoriatest	1.736s
?   	amadan.net/oficina/memoria/scripts/writeestilo	[no test files]
?   	amadan.net/oficina/memoria/tools/bench	[no test files]
?   	amadan.net/oficina/memoria/tools/drive	[no test files]
?   	amadan.net/oficina/memoria/tools/seed	[no test files]
?   	amadan.net/oficina/memoria/tools/vendorassets	[no test files]
=== migration-check ===
go run github.com/carlosframework/rastrillo/cmd/rastrillo migration check
rastrillo migration check: models and migrations agree
diff --git a/go.mod b/go.mod
index 3ec360d..7f35e10 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.26
toolchain go1.26.5
require (
- amadan.net/oficina/estilo.git v0.0.0-20260929231226-cce11010ece0
+ amadan.net/oficina/estilo.git v0.0.0-20261001075355-fbd14e759027
amadan.net/oficina/home/connect v0.0.0-20260910000424-5b8d8410775f
amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489
amadan.net/oficina/suite v0.0.0-20260905215529-60b01a8a0d6d
diff --git a/go.sum b/go.sum
index a4586d6..771ea81 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-amadan.net/oficina/estilo.git v0.0.0-20260929231226-cce11010ece0 h1:Qww+xGIYJaj+6TIX4Knr71ghxeYJjtgFm6WukX6NkGg=
-amadan.net/oficina/estilo.git v0.0.0-20260929231226-cce11010ece0/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw=
+amadan.net/oficina/estilo.git v0.0.0-20261001075355-fbd14e759027 h1:+NFDWW5ROkLaFIsT9flDuAjNeQJJxj0Safk32UcRyn0=
+amadan.net/oficina/estilo.git v0.0.0-20261001075355-fbd14e759027/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw=
amadan.net/oficina/home/connect v0.0.0-20260910000424-5b8d8410775f h1:lkw3tjzHfZxe74pNfo+tGDz4mkZLg+D6R+FTkmsym6M=
amadan.net/oficina/home/connect v0.0.0-20260910000424-5b8d8410775f/go.mod h1:YroavXe43Wietec+po8mtSk4a9Bir2h31Q3iAf4MWxI=
amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489 h1:RPKtyry/71i06o8L+gjAaUyexbmG56kO1YPs601URAM=
diff --git a/internal/memoria/auth.go b/internal/memoria/auth.go
index 3ea2aa1..03241a8 100644
--- a/internal/memoria/auth.go
+++ b/internal/memoria/auth.go
@@ -214,7 +214,7 @@ func NewAuth(sqlDB *sql.DB, env AuthEnv, logger *slog.Logger, authorize func(str
Authorize: authorize,
SigninPath: signinPath,
SignedInPath: landing,
- Subject: "Your memoria sign-in link",
+ Subject: chrome.EmailSubject("Memoria"),
Logger: logger,
})
if err != nil {
diff --git a/internal/memoria/handlers.go b/internal/memoria/handlers.go
index 164b9d8..238ac4a 100644
--- a/internal/memoria/handlers.go
+++ b/internal/memoria/handlers.go
@@ -1135,9 +1135,6 @@ type signinPage struct {
SignedOut *chrome.SignInNotice
}
-// signinLede is what the door says under its heading on a plain visit.
-const signinLede = "Memoria holds your team's work in one place. Sign in with your email and we'll send you a link."
-
func (a *app) localSignin(w http.ResponseWriter, r *http.Request) {
p := signinPage{page: page{Chunks: a.chunkCount(), ModelOn: a.modelOn()}}
p.ForceEmail = a.home != nil && r.URL.Query().Get("force") == "1"
@@ -1152,23 +1149,16 @@ func (a *app) localSignin(w http.ResponseWriter, r *http.Request) {
if p.ForceEmail {
back += "?force=1"
}
- // signinOutcome's sentence, in the notice's two parts.
- p.Sent = &chrome.SignInNotice{
- Icon: icon, App: "Memoria", Sky: view,
- Heading: "Check your email", Body: "We've sent you a link to sign in.",
- Link: &chrome.Link{Href: back, Label: "Back to sign in"},
- }
+ // The door's words; back is where "Use a different address" goes.
+ sent := chrome.SignInNotice{Icon: icon, App: "Memoria", Sky: view}.Sent(back)
+ p.Sent = &sent
render(w, "signin", p)
return
}
- door := &chrome.SignIn{
- Icon: icon, App: "Memoria", Sky: view,
- Heading: "Sign in", Lede: signinLede,
- Action: "/auth/begin",
- AddressLabel: "Email address",
- Submit: "Email me a link",
- }
+ // The door supplies its own heading, label, help and submit; a plain
+ // visit carries no lede.
+ door := &chrome.SignIn{Icon: icon, App: "Memoria", Sky: view, Action: "/auth/begin"}
if p.ForceEmail {
door.Hidden = []chrome.Hidden{{Name: "force", Value: "1"}}
}
@@ -1176,7 +1166,8 @@ func (a *app) localSignin(w http.ResponseWriter, r *http.Request) {
case "":
case "warn":
// ?reauth=1. Not an error, so not the door's negative callout:
- // it stands where the lede does, and says why the form is back.
+ // it stands in the lede, and says why the form is back. The one
+ // lede this door carries: a state, never an introduction.
door.Lede = msg
default:
door.Error = msg
@@ -1199,7 +1190,8 @@ func (a *app) localSignin(w http.ResponseWriter, r *http.Request) {
// the owners are.
func signinOutcome(q url.Values) (msg, kind string) {
if q.Get("sent") != "" {
- return "Check your email. We've sent you a link to sign in.", "good"
+ // No sentence: the door's check-your-email state says it.
+ return "", "good"
}
switch q.Get("err") {
case "":
diff --git a/internal/memoria/home_render.go b/internal/memoria/home_render.go
index c71ace2..61142a6 100644
--- a/internal/memoria/home_render.go
+++ b/internal/memoria/home_render.go
@@ -18,14 +18,12 @@ func (a *app) renderHome(w http.ResponseWriter, r *http.Request, state, label st
pg := page{Chunks: a.chunkCount(), ModelOn: a.modelOn()}
data := connect.Page(state, label)
tree, view := pages["home"], any(homePage{page: pg, Home: data})
- // Signed out of Home is the door: Home's heading, sentence and link,
- // on Memoria's card and sky, like the sign-in page it sits beside.
+ // Signed out of Home is the door's signed-out page, on Memoria's card
+ // and sky like the sign-in page it sits beside: the door's words, not
+ // connect's (which name Home), and connect's link.
if state == "signedout" {
- tree, view = pages["signin"], signinPage{page: pg, SignedOut: &chrome.SignInNotice{
- Icon: assets.Path("static/brand/icon.svg"), App: "Memoria", Sky: sky.For(r.Host),
- Heading: data.Title, Body: data.Body,
- Link: &chrome.Link{Href: data.Action, Label: data.ActionLabel},
- }}
+ out := chrome.SignInNotice{Icon: assets.Path("static/brand/icon.svg"), App: "Memoria", Sky: sky.For(r.Host)}.SignedOut(data.Action)
+ tree, view = pages["signin"], signinPage{page: pg, SignedOut: &out}
}
var body bytes.Buffer
if err := tree.ExecuteTemplate(&body, "layout", view); err != nil {
diff --git a/internal/memoria/home_router_test.go b/internal/memoria/home_router_test.go
index 6ade219..be94fe1 100644
--- a/internal/memoria/home_router_test.go
+++ b/internal/memoria/home_router_test.go
@@ -378,7 +378,8 @@ func TestHomePagesStateTheModelSwitch(t *testing.T) {
}
// Where Home sign-out lands is the door, like the sign-in page beside it:
-// Memoria's card on the sky, with Home's words and link, and the door's
+// Memoria's card on the sky, with the door's own signed-out words (not
+// connect's, which name Home) and connect's link, and the door's
// stylesheets linked so the card is drawn.
func TestSignedOutIsTheDoor(t *testing.T) {
f := newHomeRouterFixture(t)
@@ -390,11 +391,16 @@ func TestSignedOutIsTheDoor(t *testing.T) {
b := html.UnescapeString(w.Body.String())
for _, want := range []string{
"<div estilo-door", "estilo-sky-layer", "<section estilo-signin", "<span>Memoria</span>",
- "/static/chrome.", "/static/sky.", "<title>" + data.Title + " · Memoria</title>",
- data.Title, data.Body, `href="` + data.Action + `"`, data.ActionLabel,
+ "/static/chrome.", "/static/sky.", "<title>Signed out · Memoria</title>",
+ "<h1>You’re signed out</h1>", "You’ve signed out of Memoria on this device.",
+ `href="` + data.Action + `">Sign in</a>`,
} {
if !strings.Contains(b, want) {
t.Errorf("/signed-out lacks %q", want)
}
}
+ // Home is never named to people (Paul, 2026-09-23).
+ if strings.Contains(b, "Home") {
+ t.Errorf("/signed-out names Home:\n%s", b)
+ }
}
diff --git a/internal/memoria/templates/signin.html b/internal/memoria/templates/signin.html
index 7e3a5da..0040598 100644
--- a/internal/memoria/templates/signin.html
+++ b/internal/memoria/templates/signin.html
@@ -1,4 +1,4 @@
-{{define "title"}}{{with .SignedOut}}{{.Heading}} · Memoria{{else}}Sign in — Memoria{{end}}{{end}}
+{{define "title"}}{{with .Sent}}{{.Title}}{{else with .SignedOut}}{{.Title}}{{else}}{{.Door.Title}}{{end}}{{end}}
{{/* The suite's door (estilo chrome): one card on the team's sky, the
app's mark and name at its head. chrome.css draws the card, sky.css
the sky; both are vendored from estilo (go run ./scripts/writeestilo).
diff --git a/internal/memoriatest/signin_door_test.go b/internal/memoriatest/signin_door_test.go
index 35f865f..2dd2c0b 100644
--- a/internal/memoriatest/signin_door_test.go
+++ b/internal/memoriatest/signin_door_test.go
@@ -40,9 +40,12 @@ func TestSigninPageIsTheDoor(t *testing.T) {
"estilo-door",
`<span>Memoria</span>`,
`<h1>Sign in</h1>`,
- "Memoria holds your team&#39;s work in one place.",
`action="/auth/begin"`,
- `>Email me a link</button>`,
+ // The door's copy, not memoria's (estilo chrome.English; the
+ // submit is "Sign in", Paul 2026-10-01).
+ `<title>Sign in · Memoria</title>`,
+ `>Sign in</button>`,
+ "We’ll email you a link that signs you in. It works once and expires in 15 minutes.",
// The layout's footer, and its egress claim, stays under the door.
"<footer",
"leaves this instance",
@@ -52,6 +55,13 @@ func TestSigninPageIsTheDoor(t *testing.T) {
}
}
+ // The retired copy is gone: the old submit and memoria's marketing lede.
+ for _, gone := range []string{"Email me a link", "holds your team", "Sign in — Memoria"} {
+ if strings.Contains(body, gone) {
+ t.Errorf("the sign-in page still says %q", gone)
+ }
+ }
+
// Every sky layer the door links: the two inline placeholders (which
// is why CSP must allow img-src data:) and the served ones.
layers := regexp.MustCompile(`<img estilo-sky-layer="([a-z]+)"[^>]* src="([^"]+)"`).FindAllStringSubmatch(body, -1)
diff --git a/internal/memoriatest/signinoutcome_test.go b/internal/memoriatest/signinoutcome_test.go
index 9b32145..4037822 100644
--- a/internal/memoriatest/signinoutcome_test.go
+++ b/internal/memoriatest/signinoutcome_test.go
@@ -76,12 +76,18 @@ func TestSigninPageSaysWhatHappened(t *testing.T) {
h, _ := signinApp(t)
rr := beginSignin(t, h, address)
body := followToSignin(t, h, rr, "sent=1")
- if !strings.Contains(body, `<p estilo-signin-body role="status">We&#39;ve sent you a link to sign in.</p>`) {
+ if !strings.Contains(body, `<p estilo-signin-body role="status">If that address can sign in here, we’ve sent it a link. It works once and expires in 15 minutes.</p>`) {
t.Errorf("the sent page is not the door's check-your-email notice:\n%s", body)
}
if !strings.Contains(body, "<h1>Check your email</h1>") {
t.Error("the sent notice does not say check your email")
}
+ if !strings.Contains(body, "<title>Check your email · Memoria</title>") {
+ t.Error("the sent page's tab title is not \"Check your email · Memoria\"")
+ }
+ if !strings.Contains(body, `<a href="/signin">Use a different address</a>`) {
+ t.Errorf("the sent notice does not link back to the form as \"Use a different address\":\n%s", body)
+ }
if strings.Contains(body, `action="/auth/begin"`) {
t.Error("the sign-in form is still on the page after a link was sent")
}