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>
8 files changed,
+61
−11
apple/Infinity/MacMessageField.swift+3 −2apple/Infinity/NativeConversationView.swift+4 −4apple/InfinityMacUITests/NativeChatTests.swift+3 −0apple/project.yml+1 −1internal/infinity/static/style.css+6 −3test/infinity/composer-layout.mjs+39 −0test/infinity/outgoing-browser.mjs+4 −1tools/infinity/check+1 −0
Intent
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 |