eleven / ocho Public

Match the web and native composer height and white background

Measure the Mac editor from its line height, align the send button, and verify web geometry in Chromium/WebKit light and dark modes.
Co-Authored-By: Codex <noreply@openai.com>
Paul Campbell pushed by codex agent, operated by paul@keymail.dev 64d670315094b3985fa98121f9778cdb824ee4a9 parent 2539a68
8 files changed, +61 −11
  • apple/Infinity/MacMessageField.swift +3 −2
  • apple/Infinity/NativeConversationView.swift +4 −4
  • apple/InfinityMacUITests/NativeChatTests.swift +3 −0
  • apple/project.yml +1 −1
  • internal/infinity/static/style.css +6 −3
  • test/infinity/composer-layout.mjs +39 −0
  • test/infinity/outgoing-browser.mjs +4 −1
  • tools/infinity/check +1 −0
diff --git a/apple/Infinity/MacMessageField.swift b/apple/Infinity/MacMessageField.swift
index 76b790b..5351284 100644
--- a/apple/Infinity/MacMessageField.swift
+++ b/apple/Infinity/MacMessageField.swift
@@ -17,7 +17,7 @@ struct MacMessageField: NSViewRepresentable {
editor.isRichText = false
editor.drawsBackground = false
editor.font = .systemFont(ofSize: NSFont.systemFontSize)
- editor.textColor = .labelColor
+ editor.textColor = .black
editor.textContainerInset = .zero
editor.textContainer?.lineFragmentPadding = 0
editor.isVerticallyResizable = true
@@ -48,7 +48,8 @@ struct MacMessageField: NSViewRepresentable {
func measure(_ editor: NSTextView) {
guard let layout = editor.layoutManager, let container = editor.textContainer else { return }
layout.ensureLayout(for: container)
- let height = min(132, max(22, ceil(layout.usedRect(for: container).height)))
+ let line = ceil(layout.defaultLineHeight(for: editor.font ?? .systemFont(ofSize: NSFont.systemFontSize)))
+ let height = min(line * 6, max(line, ceil(layout.usedRect(for: container).height)))
DispatchQueue.main.async { [weak self] in
guard let self, self.parent.height != height else { return }
self.parent.height = height
diff --git a/apple/Infinity/NativeConversationView.swift b/apple/Infinity/NativeConversationView.swift
index ffa8916..dbae25d 100644
--- a/apple/Infinity/NativeConversationView.swift
+++ b/apple/Infinity/NativeConversationView.swift
@@ -40,14 +40,14 @@ struct NativeConversationView: View {
}
if thread.left { Text("You’re no longer in this group.").foregroundStyle(.secondary).padding() }
else {
- HStack(alignment: .bottom, spacing: 12) {
+ HStack(alignment: .center, spacing: 12) {
#if os(macOS)
MacMessageField(text: $draft, height: $composerHeight, onSend: send)
.frame(height: composerHeight)
- .overlay(alignment: .topLeading) { if draft.isEmpty { Text("Message").foregroundStyle(.placeholder).allowsHitTesting(false) } }
- .padding(12).background(.quaternary, in: RoundedRectangle(cornerRadius: 18))
+ .overlay(alignment: .leading) { if draft.isEmpty { Text("Message").foregroundStyle(Color.gray).allowsHitTesting(false) } }
+ .padding(.horizontal, 14).padding(.vertical, 9).background(Color.white, in: RoundedRectangle(cornerRadius: 18)).overlay(RoundedRectangle(cornerRadius: 18).strokeBorder(Color.gray.opacity(0.25), lineWidth: 1))
#else
- TextField("Message", text: $draft, axis: .vertical).lineLimit(1...6).textFieldStyle(.plain).padding(12).background(.quaternary, in: RoundedRectangle(cornerRadius: 18)).focused($composerFocused)
+ TextField("Message", text: $draft, axis: .vertical).foregroundStyle(Color.black).lineLimit(1...6).textFieldStyle(.plain).padding(.horizontal, 14).padding(.vertical, 9).background(Color.white, in: RoundedRectangle(cornerRadius: 18)).overlay(RoundedRectangle(cornerRadius: 18).strokeBorder(Color.gray.opacity(0.25), lineWidth: 1)).focused($composerFocused)
#endif
if !draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
Button(action: send) { Image(systemName: "arrow.up.circle.fill").font(.system(size: 32)) }
diff --git a/apple/InfinityMacUITests/NativeChatTests.swift b/apple/InfinityMacUITests/NativeChatTests.swift
index 091d6c4..75966bf 100644
--- a/apple/InfinityMacUITests/NativeChatTests.swift
+++ b/apple/InfinityMacUITests/NativeChatTests.swift
@@ -52,6 +52,9 @@ import OchoCore
XCTAssertTrue(sidebar.exists)
XCTAssertGreaterThanOrEqual(sidebar.frame.width, 260)
XCTAssertEqual(app.webViews.count, 0)
+ field.click(); field.typeText("Composer alignment")
+ let sendFrame = app.buttons["Send message"].frame
+ XCTAssertLessThanOrEqual(abs(field.frame.midY - sendFrame.midY), 2)
let attachment = XCTAttachment(screenshot: app.screenshot()); attachment.lifetime = .keepAlways; add(attachment)
}
}
diff --git a/apple/project.yml b/apple/project.yml
index 2f64bfe..cd37537 100644
--- a/apple/project.yml
+++ b/apple/project.yml
@@ -8,7 +8,7 @@ settings:
base:
SWIFT_VERSION: '5.0'
MARKETING_VERSION: '0.2.0'
- CURRENT_PROJECT_VERSION: '8'
+ CURRENT_PROJECT_VERSION: '9'
GENERATE_INFOPLIST_FILE: YES
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
CODE_SIGN_STYLE: Automatic
diff --git a/internal/infinity/static/style.css b/internal/infinity/static/style.css
index b846ae9..bddaa3d 100644
--- a/internal/infinity/static/style.css
+++ b/internal/infinity/static/style.css
@@ -1,6 +1,6 @@
/* 🤖 ∞: calm purple, readable text, one conversation list. */
:root{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#252332;background:#faf9fd;font-synthesis:none;--accent:#6550ce;--ink:#252332;--muted:#706c7f;--line:#eae7f0;--surface:#fff;--soft:#f1eef9;color-scheme:light}
-*{box-sizing:border-box}body{margin:0}button,input,textarea{font:inherit}button,a,input,textarea,summary{-webkit-tap-highlight-color:transparent}button{cursor:pointer;border:0;border-radius:12px;padding:11px 17px;background:var(--soft);color:var(--ink);font-weight:600}button:hover{filter:brightness(.96)}button:disabled{cursor:default;opacity:.55}button:focus-visible,input:focus-visible,textarea:focus-visible,summary:focus-visible{outline:3px solid #a997f0;outline-offset:3px}.primary{background:var(--accent);color:#fff}.quiet{background:transparent;color:var(--accent)}.small{padding:8px 13px;font-size:13px}.danger{color:#a83248}p{line-height:1.6}h1,h2,h3,p{margin-top:0}.mark{display:block;flex:none}.muted,.footnote{color:var(--muted)}.footnote{font-size:13px}.loading{display:grid;place-items:center;min-height:100dvh;color:var(--muted)}.shell{height:100dvh;display:grid;grid-template-columns:340px 1fr;max-width:1600px;margin:auto;background:var(--surface)}.sidebar{min-width:0;border-right:1px solid var(--line);display:flex;flex-direction:column;background:var(--surface)}.sidebar-header{display:flex;align-items:center;justify-content:space-between;padding:28px 22px 18px;gap:12px}.brand{display:flex;gap:12px;align-items:center;font-size:20px;font-weight:700;letter-spacing:-.5px}.brand .mark{width:35px;height:35px}.search-wrap{padding:0 20px 20px}input[type=search]{width:100%;border:0;background:#f4f2f7;border-radius:12px;padding:11px 14px;font-size:14px;color:var(--ink)}.thread-list{flex:1;overflow:auto;padding:0 10px}.thread-row{display:flex;align-items:center;gap:12px;width:100%;text-align:left;background:transparent;padding:14px 12px;border-radius:13px;margin:3px 0;font-weight:400}.thread-row.active{background:#eee9fc}.avatar{width:44px;height:44px;border-radius:50%;display:inline-grid;place-items:center;flex:none;font-size:18px;font-weight:600;background:#e8e1fa;color:#6550a7}.thread-summary{min-width:0;flex:1}.thread-top{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:6px}.thread-top strong{font-size:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.thread-top time{font-size:11px;color:var(--muted);flex:none}.preview{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:13px;color:var(--muted)}.list-empty{padding:25px 10px;text-align:center;color:var(--muted);font-size:14px}.account-menu{padding:15px 22px;position:relative}.account-menu summary{display:flex;gap:12px;align-items:center;cursor:pointer;font-size:14px}.account-menu summary .muted{margin-left:auto}.account-menu .avatar{width:32px;height:32px;font-size:14px}.menu-items{position:absolute;bottom:75px;left:16px;right:16px;background:var(--surface);border:1px solid var(--line);border-radius:15px;padding:8px;box-shadow:0 12px 36px #231d4315;z-index:2}.menu-items button{display:block;text-align:left;width:100%;background:transparent;font-size:14px}.connection{font-size:12px;color:var(--muted);margin:0;padding:0 15px}.connection:not(:empty){padding:10px 15px}.empty-pane{display:flex;align-items:center;justify-content:center;flex-direction:column;background:radial-gradient(ellipse at center,#f2eefc 0%,#faf9fd 70%);padding:30px;text-align:center}.empty-pane .mark{width:88px;height:88px;margin-bottom:30px}.empty-pane h1{font-size:34px;letter-spacing:-1.1px;margin-bottom:12px}.empty-pane p{color:var(--muted);margin-bottom:27px}.encryption-note{font-size:12px;color:var(--muted);margin-top:25px}.conversation{display:flex;flex-direction:column;min-width:0;min-height:0;background:#fbfafd}.chat-header{height:90px;display:flex;align-items:center;gap:14px;padding:18px 30px;background:var(--surface);border-bottom:1px solid var(--line)}.chat-title{flex:1;min-width:0}.chat-title h1{font-size:17px;margin:0 0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.chat-title p{font-size:12px;color:var(--muted);margin:0}.back{display:none}.messages{flex:1;overflow:auto;padding:25px 40px;display:flex;flex-direction:column;gap:8px;overscroll-behavior:contain}.date-label{text-align:center;color:var(--muted);font-size:11px;margin:14px 0}.message{max-width:min(76%,560px);padding:11px 15px 8px;border-radius:19px;align-self:flex-start;background:var(--surface);box-shadow:0 1px 3px #21113b0b;min-width:74px;overflow-wrap:anywhere}.message.mine{align-self:flex-end;background:var(--accent);color:#fff;border-bottom-right-radius:5px}.message.theirs{border-bottom-left-radius:5px;border:1px solid #efebf4}.message p{margin:0;white-space:pre-wrap;font-size:15px;line-height:1.5}.message time{display:block;text-align:right;margin-top:4px;font-size:10px;opacity:.8}.sender-name{font-size:11px;display:block;margin-bottom:4px;color:var(--accent)}.composer{display:flex;align-items:flex-end;gap:12px;padding:10px 18px 14px;background:transparent}.composer textarea{resize:none;flex:1;min-width:0;border:0;background:transparent;border-radius:24px;padding:13px 19px;line-height:22px;min-height:48px;max-height:140px;color:var(--ink)}.send{border-radius:50%;height:46px;width:46px;padding:0;font-size:26px}.composer-note{background:transparent;padding:0 28px}.start-note{margin:auto;text-align:center;color:var(--muted);font-size:13px}.request{background:var(--soft);border-radius:14px;margin:8px 2px 14px;padding:15px}.request strong{font-size:14px}.request p{font-size:12px;margin:5px 0 10px}.actions{display:flex;gap:8px}.welcome-wrap{min-height:100dvh;display:grid;place-items:center;padding:32px 20px;background:radial-gradient(ellipse at 50% 25%,#eee8ff,#faf9fd 65%)}.welcome{width:min(100%,440px)}.welcome>.mark{width:72px;height:72px;margin-bottom:34px}.welcome h1{font-size:42px;line-height:1.12;letter-spacing:-1.7px;max-width:400px;margin-bottom:20px}.lead{font-size:20px;color:#58516f}.welcome form{margin:25px 0 16px}.welcome form>.primary{width:100%}.welcome>.quiet{padding-left:0}.field{display:flex;flex-direction:column;gap:9px;font-size:13px;font-weight:600;margin:18px 0}.field input{width:100%;padding:13px 14px;border:1px solid #d9d3e6;border-radius:11px;background:var(--surface);color:var(--ink);font-size:16px}.notice{font-size:13px;color:var(--muted);padding:9px 0;margin:0}.error{color:#a83248}.onboard-form .field{margin-top:0}dialog{width:min(92vw,480px);max-height:85dvh;overflow:auto;border:1px solid var(--line);border-radius:22px;padding:25px;color:var(--ink);background:var(--surface);box-shadow:0 25px 100px #20103d33}dialog::backdrop{background:#21182d55;backdrop-filter:blur(3px)}dialog header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:20px}dialog h2{font-size:20px;margin:0}dialog header button{font-size:12px;padding:7px}dialog p{font-size:14px;color:var(--muted)}dialog textarea{width:100%;padding:12px;border:1px solid var(--line);border-radius:10px;margin:10px 0;resize:vertical;font-size:12px;color:var(--ink);background:var(--soft)}dialog form>.primary{width:100%;margin-top:12px}.choices label{display:flex;align-items:center;gap:12px;padding:12px 0}.choices input{width:18px;height:18px;accent-color:var(--accent)}.member{display:flex;align-items:center;justify-content:space-between;padding:10px 0}.safety{font-size:15px;line-height:1.8;white-space:pre-wrap;text-align:center;background:var(--soft);padding:18px;border-radius:14px}
+*{box-sizing:border-box}body{margin:0}button,input,textarea{font:inherit}button,a,input,textarea,summary{-webkit-tap-highlight-color:transparent}button{cursor:pointer;border:0;border-radius:12px;padding:11px 17px;background:var(--soft);color:var(--ink);font-weight:600}button:hover{filter:brightness(.96)}button:disabled{cursor:default;opacity:.55}button:focus-visible,input:focus-visible,textarea:focus-visible,summary:focus-visible{outline:3px solid #a997f0;outline-offset:3px}.primary{background:var(--accent);color:#fff}.quiet{background:transparent;color:var(--accent)}.small{padding:8px 13px;font-size:13px}.danger{color:#a83248}p{line-height:1.6}h1,h2,h3,p{margin-top:0}.mark{display:block;flex:none}.muted,.footnote{color:var(--muted)}.footnote{font-size:13px}.loading{display:grid;place-items:center;min-height:100dvh;color:var(--muted)}.shell{height:100dvh;display:grid;grid-template-columns:340px 1fr;max-width:1600px;margin:auto;background:var(--surface)}.sidebar{min-width:0;border-right:1px solid var(--line);display:flex;flex-direction:column;background:var(--surface)}.sidebar-header{display:flex;align-items:center;justify-content:space-between;padding:28px 22px 18px;gap:12px}.brand{display:flex;gap:12px;align-items:center;font-size:20px;font-weight:700;letter-spacing:-.5px}.brand .mark{width:35px;height:35px}.search-wrap{padding:0 20px 20px}input[type=search]{width:100%;border:0;background:#f4f2f7;border-radius:12px;padding:11px 14px;font-size:14px;color:var(--ink)}.thread-list{flex:1;overflow:auto;padding:0 10px}.thread-row{display:flex;align-items:center;gap:12px;width:100%;text-align:left;background:transparent;padding:14px 12px;border-radius:13px;margin:3px 0;font-weight:400}.thread-row.active{background:#eee9fc}.avatar{width:44px;height:44px;border-radius:50%;display:inline-grid;place-items:center;flex:none;font-size:18px;font-weight:600;background:#e8e1fa;color:#6550a7}.thread-summary{min-width:0;flex:1}.thread-top{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:6px}.thread-top strong{font-size:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.thread-top time{font-size:11px;color:var(--muted);flex:none}.preview{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:13px;color:var(--muted)}.list-empty{padding:25px 10px;text-align:center;color:var(--muted);font-size:14px}.account-menu{padding:15px 22px;position:relative}.account-menu summary{display:flex;gap:12px;align-items:center;cursor:pointer;font-size:14px}.account-menu summary .muted{margin-left:auto}.account-menu .avatar{width:32px;height:32px;font-size:14px}.menu-items{position:absolute;bottom:75px;left:16px;right:16px;background:var(--surface);border:1px solid var(--line);border-radius:15px;padding:8px;box-shadow:0 12px 36px #231d4315;z-index:2}.menu-items button{display:block;text-align:left;width:100%;background:transparent;font-size:14px}.connection{font-size:12px;color:var(--muted);margin:0;padding:0 15px}.connection:not(:empty){padding:10px 15px}.empty-pane{display:flex;align-items:center;justify-content:center;flex-direction:column;background:radial-gradient(ellipse at center,#f2eefc 0%,#faf9fd 70%);padding:30px;text-align:center}.empty-pane .mark{width:88px;height:88px;margin-bottom:30px}.empty-pane h1{font-size:34px;letter-spacing:-1.1px;margin-bottom:12px}.empty-pane p{color:var(--muted);margin-bottom:27px}.encryption-note{font-size:12px;color:var(--muted);margin-top:25px}.conversation{display:flex;flex-direction:column;min-width:0;min-height:0;background:#fbfafd}.chat-header{height:90px;display:flex;align-items:center;gap:14px;padding:18px 30px;background:var(--surface);border-bottom:1px solid var(--line)}.chat-title{flex:1;min-width:0}.chat-title h1{font-size:17px;margin:0 0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.chat-title p{font-size:12px;color:var(--muted);margin:0}.back{display:none}.messages{flex:1;overflow:auto;padding:25px 40px;display:flex;flex-direction:column;gap:8px;overscroll-behavior:contain}.date-label{text-align:center;color:var(--muted);font-size:11px;margin:14px 0}.message{max-width:min(76%,560px);padding:11px 15px 8px;border-radius:19px;align-self:flex-start;background:var(--surface);box-shadow:0 1px 3px #21113b0b;min-width:74px;overflow-wrap:anywhere}.message.mine{align-self:flex-end;background:var(--accent);color:#fff;border-bottom-right-radius:5px}.message.theirs{border-bottom-left-radius:5px;border:1px solid #efebf4}.message p{margin:0;white-space:pre-wrap;font-size:15px;line-height:1.5}.message time{display:block;text-align:right;margin-top:4px;font-size:10px;opacity:.8}.sender-name{font-size:11px;display:block;margin-bottom:4px;color:var(--accent)}.composer{display:flex;align-items:flex-end;gap:12px;padding:10px 18px 14px;background:transparent}.composer textarea{resize:none;flex:1;min-width:0;border:0;background:transparent;border-radius:24px;padding:8px 16px;line-height:22px;min-height:38px;max-height:140px;color:var(--ink)}.send{border-radius:50%;height:46px;width:46px;padding:0;font-size:26px}.composer-note{background:transparent;padding:0 28px}.start-note{margin:auto;text-align:center;color:var(--muted);font-size:13px}.request{background:var(--soft);border-radius:14px;margin:8px 2px 14px;padding:15px}.request strong{font-size:14px}.request p{font-size:12px;margin:5px 0 10px}.actions{display:flex;gap:8px}.welcome-wrap{min-height:100dvh;display:grid;place-items:center;padding:32px 20px;background:radial-gradient(ellipse at 50% 25%,#eee8ff,#faf9fd 65%)}.welcome{width:min(100%,440px)}.welcome>.mark{width:72px;height:72px;margin-bottom:34px}.welcome h1{font-size:42px;line-height:1.12;letter-spacing:-1.7px;max-width:400px;margin-bottom:20px}.lead{font-size:20px;color:#58516f}.welcome form{margin:25px 0 16px}.welcome form>.primary{width:100%}.welcome>.quiet{padding-left:0}.field{display:flex;flex-direction:column;gap:9px;font-size:13px;font-weight:600;margin:18px 0}.field input{width:100%;padding:13px 14px;border:1px solid #d9d3e6;border-radius:11px;background:var(--surface);color:var(--ink);font-size:16px}.notice{font-size:13px;color:var(--muted);padding:9px 0;margin:0}.error{color:#a83248}.onboard-form .field{margin-top:0}dialog{width:min(92vw,480px);max-height:85dvh;overflow:auto;border:1px solid var(--line);border-radius:22px;padding:25px;color:var(--ink);background:var(--surface);box-shadow:0 25px 100px #20103d33}dialog::backdrop{background:#21182d55;backdrop-filter:blur(3px)}dialog header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:20px}dialog h2{font-size:20px;margin:0}dialog header button{font-size:12px;padding:7px}dialog p{font-size:14px;color:var(--muted)}dialog textarea{width:100%;padding:12px;border:1px solid var(--line);border-radius:10px;margin:10px 0;resize:vertical;font-size:12px;color:var(--ink);background:var(--soft)}dialog form>.primary{width:100%;margin-top:12px}.choices label{display:flex;align-items:center;gap:12px;padding:12px 0}.choices input{width:18px;height:18px;accent-color:var(--accent)}.member{display:flex;align-items:center;justify-content:space-between;padding:10px 0}.safety{font-size:15px;line-height:1.8;white-space:pre-wrap;text-align:center;background:var(--soft);padding:18px;border-radius:14px}
@media(max-width:700px){.shell{display:block}.sidebar,.conversation{height:100dvh}.sidebar-header{padding-top:max(22px,env(safe-area-inset-top))}.empty-pane{display:none}.conversation-open .sidebar{display:none}.shell:not(.conversation-open) .conversation{display:none}.chat-header{height:auto;min-height:76px;padding:14px 16px;padding-top:max(14px,env(safe-area-inset-top));gap:10px}.back{display:block;font-size:32px;padding:0 5px}.chat-header .avatar{width:37px;height:37px;font-size:16px}.chat-title p{font-size:10px}.chat-header>.quiet:last-child{font-size:12px;padding:8px}.messages{padding:10px 15px}.message{max-width:85%}.composer{padding:12px 13px max(14px,env(safe-area-inset-bottom));gap:8px}.welcome h1{font-size:37px}.account-menu{padding-bottom:max(16px,env(safe-area-inset-bottom))}}
@media(prefers-color-scheme:dark){:root{--accent:#7965de;--ink:#f0edf8;--muted:#aaa3bb;--line:#302b3c;--surface:#211d2a;--soft:#30283f;color:var(--ink);background:#19161f;color-scheme:dark}.empty-pane,.welcome-wrap{background:radial-gradient(ellipse at center,#302440,#19161f 75%)}.conversation{background:#19161f}input[type=search]{background:#2a2433}.thread-row.active{background:#372c4d}.avatar{background:#423258;color:#d4c2ef}.message.theirs{border-color:#383044}.lead{color:#c2b6d3}.field input{border-color:#493d59}.error,.danger{color:#f7a5b2}}
/* 🤖 Setup gets its own stage. The travelling light is indeterminate, not a fabricated percentage. */
@@ -16,9 +16,12 @@
.profile-avatar img,.avatar { object-fit:cover; border-radius:50%; }
/* 🤖 The feed stops above a borderless composer; only the input carries a rim. */
-.composer-input{display:flex;align-items:flex-end;gap:6px;width:100%;border:1px solid var(--line);border-radius:26px;padding:3px;background:transparent}
+.composer-input{display:flex;align-items:flex-end;gap:6px;width:100%;border:1px solid var(--line);border-radius:26px;padding:3px;background:#fff;color:#252332;color-scheme:light}
.composer-input:focus-within{border-color:#a997f0}
.composer textarea:focus-visible{outline:none}
-.composer .send{flex:none;width:38px;height:38px;margin:2px;font-size:25px}
+.composer .send{flex:none;width:38px;height:38px;margin:0;font-size:25px}
.send[hidden]{display:none}
.retry-send{display:inline;padding:0 0 0 7px;background:none;color:inherit;font:inherit;text-decoration:underline}
+
+.composer-input textarea{color:inherit}
+.composer-input textarea::placeholder{color:#706c7f}
diff --git a/test/infinity/composer-layout.mjs b/test/infinity/composer-layout.mjs
new file mode 100644
index 0000000..f12456f
--- /dev/null
+++ b/test/infinity/composer-layout.mjs
@@ -0,0 +1,39 @@
+// 🤖 Exercise real composer layout in both engines without creating or messaging accounts.
+import {createRequire} from 'node:module';
+import {readFile} from 'node:fs/promises';
+import assert from 'node:assert/strict';
+const require=createRequire(new URL('./package.json',import.meta.url));
+const {chromium,webkit}=require('playwright');
+const origin=process.env.OCHO_HOME_URL||'http://localhost:18799';
+for(const [name,engine] of Object.entries({chromium,webkit})) {
+ const browser=await engine.launch();
+ try {for(const colorScheme of ['light','dark']) {
+ const page=await browser.newPage({viewport:{width:1100,height:760},colorScheme});
+ if(!process.env.OCHO_HOME_URL)await page.route('**/*',async route=>{
+ const path=new URL(route.request().url()).pathname;
+ if(path==='/api/profile')return route.fulfill({json:{registration:true}});
+ const file=path==='/'?'index.html':path.replace('/static/','');
+ const contentType=file.endsWith('.js')?'text/javascript':file.endsWith('.css')?'text/css':file.endsWith('.svg')?'image/svg+xml':'text/html';
+ await route.fulfill({body:await readFile(new URL('../../internal/infinity/static/'+file,import.meta.url)),contentType});
+ });
+ await page.goto(origin);await page.getByRole('button',{name:'Sign up',exact:true}).waitFor();
+ await page.evaluate(async()=>{
+ const {Chat}=await import('/static/chat.js');
+ const client={state:{name:'Preview'},view:{requests:[],threads:[{id:'preview',name:'Preview',members:[],messages:[],pending:false,left:false}]}};
+ const chat=new Chat(document.querySelector('#app'),client);chat.selected='preview';chat.render();
+ });
+ const field=page.getByRole('textbox',{name:'Message',exact:true});await field.fill('Composer alignment');
+ const layout=await page.evaluate(()=>{
+ const field=document.querySelector('#composer'),box=field.parentElement,button=box.querySelector('.send');
+ const a=box.getBoundingClientRect(),b=button.getBoundingClientRect(),c=field.getBoundingClientRect(),style=getComputedStyle(field);
+ return{height:a.height,offset:Math.abs(a.y+a.height/2-b.y-b.height/2),textOffset:Math.abs(a.y+a.height/2-c.y-parseFloat(style.paddingTop)-parseFloat(style.lineHeight)/2),background:getComputedStyle(box).backgroundColor,color:style.color};
+ });
+ assert.equal(layout.background,'rgb(255, 255, 255)');assert.equal(layout.color,'rgb(37, 35, 50)');
+ assert.ok(layout.height<=48&&layout.offset<=1&&layout.textOffset<=1,JSON.stringify(layout));
+ if(colorScheme==='light')await page.locator('.composer').screenshot({path:'/tmp/ocho-composer-'+name+'.png'});
+ await field.fill('First line\nSecond line\nThird line');
+ assert.ok(await field.evaluate(el=>el.clientHeight>=66));
+ await field.fill('Short again');assert.ok(await field.evaluate(el=>el.clientHeight<=40));
+ console.log(name,colorScheme,layout);await page.close();
+ }}finally{await browser.close();}
+}
diff --git a/test/infinity/outgoing-browser.mjs b/test/infinity/outgoing-browser.mjs
index 1801107..32d4ce6 100644
--- a/test/infinity/outgoing-browser.mjs
+++ b/test/infinity/outgoing-browser.mjs
@@ -8,7 +8,10 @@ export async function outgoingChecks(page,text) {
let release,seen;const gate=new Promise(r=>release=r),arrived=new Promise(r=>seen=r),bodies=[];
const match=url=>url.pathname.endsWith('/send');
await page.route(match,async route=>{bodies.push(route.request().postDataJSON());if(bodies.length===1){seen();await gate;await route.abort();}else await route.continue();});
- await field.fill(text);assert.equal(await send.isVisible(),true);await send.click();
+ await field.fill(text);assert.equal(await send.isVisible(),true);
+ const geometry=await page.evaluate(()=>{const input=document.querySelector('.composer-input'),send=document.querySelector('.send'),field=document.querySelector('#composer');const a=input.getBoundingClientRect(),b=send.getBoundingClientRect();return {background:getComputedStyle(input).backgroundColor,height:a.height,offset:Math.abs(a.y+a.height/2-b.y-b.height/2),textHeight:field.getBoundingClientRect().height};});
+ assert.equal(geometry.background,'rgb(255, 255, 255)');assert.ok(geometry.height<=48,JSON.stringify(geometry));assert.ok(geometry.offset<=1,JSON.stringify(geometry));
+ await send.click();
await page.getByRole('log').getByText(text,{exact:true}).waitFor({timeout:1500});
assert.match(await page.getByRole('log').innerText(),/Sending…/);
await arrived;await field.fill('A newer draft');release();
diff --git a/tools/infinity/check b/tools/infinity/check
index 1d02b78..403c714 100755
--- a/tools/infinity/check
+++ b/tools/infinity/check
@@ -13,6 +13,7 @@ case "${1:-all}" in
INFINITY_BROWSER=webkit node test/infinity/browser.mjs
;;
home-browser)
+ node test/infinity/composer-layout.mjs
node --test test/infinity/outgoing.test.mjs
CGO_ENABLED=0 go test -c -o .build/native-check/home-test ./internal/infinityhome
node test/infinity/home-browser.mjs