feat(chat): share a chat by link — anonymous read-only /shared/<token> page, share/unshare
This commit is contained in:
@@ -338,6 +338,37 @@ html::after {
|
||||
the whole control below 640px (mirrored in the ≤640 block below). */
|
||||
.save-chat-btn svg { width: 16px; height: 16px; display: none; }
|
||||
|
||||
/* Phase 51 (owner-locked 2026-08-29, TODO.md L6): the "Share" pill —
|
||||
the EXACT visual family of .save-chat-btn (same declarations, so the
|
||||
chat-shell actions always read as a pair): solid brand pill, --bg
|
||||
text on --brand (5.2:1, WCAG AA), borderless, ≥44px target, hover
|
||||
lightens the brand fill, focus-visible via the global 3px rule.
|
||||
Shipped hidden in index.html — app.js reveals it for admin only
|
||||
(phase 16 absent-not-hidden); ≤640px overrides below mirror the
|
||||
Save ones (label stays visible in .chat-shell, icon hidden there). */
|
||||
.share-chat-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
min-height: 44px;
|
||||
padding: 0.5rem 0.9rem;
|
||||
border-radius: 999px;
|
||||
border: 0;
|
||||
background: var(--brand);
|
||||
color: var(--bg);
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
.share-chat-btn:hover { background: #f55a72; color: var(--bg); }
|
||||
/* The link mark is hidden on desktop (the label carries the pill); it
|
||||
is the whole control below 640px (mirrored in the ≤640 block
|
||||
below). */
|
||||
.share-chat-btn svg { width: 16px; height: 16px; display: none; }
|
||||
|
||||
/* Phase 16: header auth controls (Sign in link / Sign out button) — the
|
||||
same ghost pill as New chat, so the bar keeps one visual language.
|
||||
ink-soft on surface ≈6.9:1; hover pair brand-ink/brand-soft ≈6.9:1.
|
||||
@@ -1897,6 +1928,60 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
cursor: pointer;
|
||||
}
|
||||
.history-confirm-no:hover { background: var(--brand-soft); color: var(--brand-ink); }
|
||||
/* Share column (phase 51, owner-locked 2026-08-29): the Tune/Retry-
|
||||
family ghost buttons — Create link (unshared) and Copy (shared);
|
||||
Unshare is the .history-unshare ghost and its two-step confirm reuses
|
||||
the .history-confirm-* pair CSS above (the phase-50 pattern). The
|
||||
row-action language of .history-delete: --line border, transparent
|
||||
fill, ink-soft (>=5.1:1), ≥44px comfortable target, focus-visible
|
||||
via the global 3px rule; hover takes the brand pair (6.9:1). */
|
||||
.history-share { display: inline-flex; align-items: center; gap: 0.4rem; flex-wrap: nowrap; }
|
||||
.history-share-cell { white-space: nowrap; }
|
||||
.history-share-create,
|
||||
.history-share-copy,
|
||||
.history-unshare {
|
||||
min-height: 44px;
|
||||
padding: 0.35rem 0.7rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--ink-soft);
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
font-size: 0.82rem;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
.history-share-create:hover:not(:disabled),
|
||||
.history-share-copy:hover:not(:disabled) { background: var(--brand-soft); color: var(--brand-ink); }
|
||||
.history-unshare:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
|
||||
.history-share-create:disabled,
|
||||
.history-share-copy:disabled,
|
||||
.history-unshare:disabled { opacity: 0.5; cursor: wait; }
|
||||
/* The share link's inline fallback field (phase 51, owner-locked): a
|
||||
non-secure (http) origin rejects the clipboard, so the full URL is
|
||||
offered as an input-like <a> that selects itself on focus — click or
|
||||
Tab, then Ctrl/Cmd+C. Mono (the URL is data), surface fill, --line
|
||||
border; truncates with an ellipsis at narrow widths (the full URL is
|
||||
the title + the text selection). Ink on surface ≈13.8:1. */
|
||||
.share-link-fallback {
|
||||
display: inline-block;
|
||||
max-width: 14rem;
|
||||
padding: 0.35rem 0.55rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface);
|
||||
color: var(--ink);
|
||||
font-family: var(--mono);
|
||||
font-size: 0.78rem;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.share-link-fallback:hover { border-color: var(--brand); }
|
||||
.share-link-fallback:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
|
||||
/* Empty-state row: the muted centered message at full table width
|
||||
(the .git-sources-empty language, inline in the table). */
|
||||
.history-empty-row td {
|
||||
@@ -1906,6 +1991,61 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ---------- Shared page (phase 51, task 03) ----------
|
||||
/shared/<token>: the anonymous read-only conversation (owner-locked
|
||||
2026-08-29, TODO.md L6). The shell maps to the PLAN §7 centered
|
||||
46rem chat column — the conversation reads exactly like the chat
|
||||
page (the .msg/.bubble/.thinking/.tool-calls/.msg-meta rules apply
|
||||
unchanged) with NO composer, so the column contract holds for a
|
||||
guest. Zero interactive controls (owner-locked): the chips are
|
||||
plain text, so the pill families' pointer treatments are switched
|
||||
off IN THIS SCOPE ONLY — the chat page's interactive chips keep
|
||||
their styles untouched. Every pair reuses the Phase-08 AA palette
|
||||
(ink-soft ≥6.9:1 on surface/bg, brand-ink on brand-soft 6.9:1);
|
||||
:focus-visible via the global 3px outline rule. No CDN, system
|
||||
fonts. */
|
||||
.shared-shell {
|
||||
width: 100%;
|
||||
max-width: 46rem; /* the PLAN §7 centered chat column */
|
||||
margin-inline: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
/* Page title (JS-filled with the shared chat's title; the static
|
||||
fallback is "Shared conversation") — the page-head h1 size. */
|
||||
#shared-title { margin: 0; font-size: 1.7rem; }
|
||||
/* The muted meta line under the h1 ("Shared via … — read-only."):
|
||||
ink-soft on the page bg ≥8.6:1, the page-sub language. */
|
||||
.shared-note {
|
||||
margin: 0;
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
/* The invalid / revoked state: a centered muted card (the not-found
|
||||
language — surface fill, --line border, italic ink-soft). Revealed
|
||||
by shared.js for a malformed token (no fetch) or a failed read. */
|
||||
#shared-invalid {
|
||||
padding: 1.4rem 1rem;
|
||||
text-align: center;
|
||||
color: var(--ink-soft);
|
||||
font-style: italic;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
/* Static chips: a guest's "Maybe try" and source chips are TEXT — no
|
||||
pointer, no hover, no cursor (owner-locked zero controls). The pill
|
||||
look is kept; the interactivity is scoped off here and only here
|
||||
(pointer-events: none also makes the :hover rules unreachable). */
|
||||
.shared-shell .suggestion-chip,
|
||||
.shared-shell .source-chip {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* ---------- Document viewer (phase 10; two-row header since phase 34) ---------- */
|
||||
/* Phase 34 (owner confirmation 2026-08-26): the viewer header is TWO
|
||||
rows in one sticky <header> — row 1 reuses the standard .app-header /
|
||||
@@ -2445,12 +2585,19 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
.save-chat-btn { padding: 0.4rem 0.3rem; }
|
||||
.save-chat-label { display: none; }
|
||||
.save-chat-btn svg { display: block; }
|
||||
/* Phase 51: the Share pill squeezes with Save (same family, same
|
||||
rules — the chat-shell overrides below keep both labels visible). */
|
||||
.share-chat-btn { padding: 0.4rem 0.3rem; }
|
||||
.share-chat-label { display: none; }
|
||||
.share-chat-btn svg { display: block; }
|
||||
/* But on the chat page there is room — keep the label visible and
|
||||
hide the icon (the button lives inside .chat-shell, not the navbar). */
|
||||
.chat-shell .new-chat-label { display: inline; }
|
||||
.chat-shell .new-chat-btn svg { display: none; }
|
||||
.chat-shell .save-chat-label { display: inline; }
|
||||
.chat-shell .save-chat-btn svg { display: none; }
|
||||
.chat-shell .share-chat-label { display: inline; }
|
||||
.chat-shell .share-chat-btn svg { display: none; }
|
||||
/* Phase 16: the auth pill goes icon-only like New chat — brand text
|
||||
ellipsizes as the designated squeeze target, no bar overflow. */
|
||||
.auth-link { padding: 0.4rem 0.3rem; }
|
||||
@@ -2564,6 +2711,12 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
the two-step confirm pair fits the phone width. */
|
||||
.history-actions-cell { white-space: normal; }
|
||||
.history-actions { flex-wrap: wrap; }
|
||||
/* Phase 51: the shared page squeezes like the chat column — the
|
||||
title and the note step down (the empty-state-title family); the
|
||||
shell keeps its 46rem column (it is already the narrowest box on
|
||||
the page) and .msg-body's 92% override above applies. */
|
||||
#shared-title { font-size: 1.35rem; }
|
||||
.shared-note { font-size: 0.88rem; }
|
||||
.footer-inner { flex-direction: column; gap: 0.2rem; text-align: center; }
|
||||
main { padding-bottom: env(safe-area-inset-bottom, 0); }
|
||||
/* Sync button goes icon-only on mobile; the label hides, aria-label
|
||||
|
||||
Reference in New Issue
Block a user