eleven / ocho Public

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>
Paul Campbell b8b1c723799c0eae5a93467fb677af7d7464b446 parent 6029b28
4 files changed, +41 −1
  • .dockerignore +4 −0
  • web/pages/index.html +6 −0
  • web/pages/join.html +6 −0
  • web/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).