web: native-wrapper signal + drop hand cursors in the Mac app (#53)
* web: drop hand cursors when wrapped by the native Mac app The Mac app advertises itself via a "lchatMac" user agent; index.html and join.html detect it and add html.native, and style.css then uses the normal arrow (not the web hand) cursor on clickable elements — links included, since they get pointer from the browser default. Text I-beam and zoom-in are untouched. No effect in browsers (the class is never added). Pairs with the applicationNameForUserAgent marker in the LChatMac app. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * build: exclude the native clients (/ios) from the server build context The Go server embeds only web/; /ios (and its large derived build output) never belongs in the Docker context. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * web: refine native cursor rules — chrome only, content keeps pointer Drop the blanket <a> override so message URLs and link-preview cards keep the pointer (they are real links); images keep zoom-in. `html.native button` still gives every nav row / header + composer control / reaction chip / menu item the arrow, plus the few non-button pointer bits (toggle-row, reply-count, mention-link, zoomable avatar). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * web: !important on native cursor rules (beat the inline group-subtitle cursor) The group "N members" subtitle (#thread-sub) sets cursor:pointer via an inline JS style, which a class rule cannot override — so the native chrome cursor rules now use !important. Also hardens against specificity edge cases. Content links + image zoom-in are still untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * web: gate button cursor via --ui-cursor variable (WKWebView flicker fix attempt) Buttons now read cursor: var(--ui-cursor, pointer); the native wrapper sets --ui-cursor:default. That gives each button a single resolved cursor value instead of a base pointer + an !important override, so WKWebView has nothing to flip-flop between. :disabled specificity is preserved (button rule keeps its 0,0,1). Non-button pointer bits still override directly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * web: header titles use the arrow, not the text I-beam, in the native wrapper The header (.side-head/.thread-head) is a window-drag region in the Mac app, so its title text is not selectable — set cursor:default + user-select:none under html.native so the "Chats" title and group name do not show the selectable I-beam. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> ---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 files changed,
+41
−1
.dockerignore+4 −0web/pages/index.html+6 −0web/pages/join.html+6 −0web/static/style.css+25 −1
diff --git a/.dockerignore b/.dockerignore| index 1a36c44..43135e3 100644 |
| --- a/.dockerignore |
| +++ b/.dockerignore |
| @@ -6,3 +6,7 @@ |
| *.db-shm |
| .DS_Store |
| README.md |
| +# The Go server embeds only web/ — the native clients (and their large derived |
| +# build output) are never part of the server build context. |
| +/ios |
| +.claude |
diff --git a/web/pages/index.html b/web/pages/index.html| index 6b7fe4f..d5f6b6a 100644 |
| --- a/web/pages/index.html |
| +++ b/web/pages/index.html |
| @@ -6,6 +6,12 @@ |
| UI only; the server enforces it regardless. "1" = on, "0" = off. --> |
| <meta name="user-invites" content="__USER_INVITES__" /> |
| <title>__GROUP_NAME__</title> |
| +<!-- Native wrapper detection: the Mac app's user agent carries "lchatMac". Tag the |
| + document so CSS can adopt native conventions (e.g. drop web hand cursors). --> |
| +<script> |
| + if (navigator.userAgent.includes("lchatMac")) |
| + document.documentElement.classList.add("native"); |
| +</script> |
| <link rel="stylesheet" href="/static/style.css" /> |
| <link rel="manifest" href="/manifest.webmanifest" /> |
| <link rel="icon" href="/static/icon-192.png" /> |
diff --git a/web/pages/join.html b/web/pages/join.html| index 45c1f4c..c07af80 100644 |
| --- a/web/pages/join.html |
| +++ b/web/pages/join.html |
| @@ -2,6 +2,12 @@ |
| <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /> |
| <meta name="theme-color" content="#5b6cff" /> |
| <title>__GROUP_NAME__</title> |
| +<!-- Native wrapper detection: the Mac app's user agent carries "lchatMac". Tag the |
| + document so CSS can adopt native conventions (e.g. drop web hand cursors). --> |
| +<script> |
| + if (navigator.userAgent.includes("lchatMac")) |
| + document.documentElement.classList.add("native"); |
| +</script> |
| <link rel="stylesheet" href="/static/style.css" /> |
| <!-- Same identity as the chat root, so "Add to Home Screen" from here installs as |
| __GROUP_NAME__ (not a generic "Join") and launches at the app's start_url ("/"). --> |
diff --git a/web/static/style.css b/web/static/style.css| index 5ae062f..0597d71 100644 |
| --- a/web/static/style.css |
| +++ b/web/static/style.css |
| @@ -165,7 +165,7 @@ html, body { |
| button { |
| font: inherit; |
| - cursor: pointer; |
| + cursor: var(--ui-cursor, pointer); |
| border: none; |
| border-radius: 999px; |
| padding: 10px 18px; |
| @@ -1317,6 +1317,30 @@ input:focus, textarea:focus { outline: 2px solid var(--accent); border-color: tr |
| .back-btn { display: inline-flex; } |
| } |
| +/* Native wrapper (html.native, set from the "lchatMac" user agent): app chrome uses |
| + the normal arrow, not the web "hand" cursor. Content is untouched — message URLs and |
| + link-preview cards keep the pointer, images keep zoom-in. |
| + Buttons switch through the --ui-cursor variable so each button has just ONE cursor |
| + declaration (resolving to default) rather than a base `pointer` + an override — a |
| + WKWebView flickers when it has two competing values to choose between. The remaining |
| + non-button pointer bits still override directly (!important because #thread-sub sets |
| + its cursor via an inline JS style). Mirror new chrome cursors here. */ |
| +html.native { --ui-cursor: default; } |
| +html.native #thread-sub, |
| +html.native .toggle-row, |
| +html.native .reply-count, |
| +html.native .mention-link, |
| +html.native .avatar.zoomable { cursor: default !important; } |
| + |
| +/* The header is a window-drag region in the native wrapper, so its title text isn't |
| + selectable — don't show the text I-beam or a selection affordance on it. */ |
| +html.native .side-head, |
| +html.native .thread-head { |
| + cursor: default; |
| + -webkit-user-select: none; |
| + user-select: none; |
| +} |
| + |
| /* Native shell (iOS/macOS) embeds ONE thread's feed — the app draws its own |
| sidebar, title bar, and composer. Hide the web sidebar / thread header / composer |
| and let the feed fill the view (overriding the mobile one-pane-at-a-time rules). |