- Remove font-style:italic from .ht-italic (different glyph widths cause desync) - Remove font-weight:600 from .ht-heading (bolder glyphs are wider) - Remove background/border-radius from code token spans - Remove text-decoration from .ht-link - Token spans now ONLY use color — zero text geometry changes - Use inherit for font-size/line-height in shared .ht-backdrop/.ht-textarea - Add update check hook: polls GitHub releases, shows indicator in sidebar
This commit is contained in:
12
FEEDBACK.md
12
FEEDBACK.md
@@ -699,3 +699,15 @@
|
||||
- **Status:** done
|
||||
- **Completed:** 2026-02-13 — commit `84512b1`
|
||||
- **Description:** Avatar image shows as broken for some deployments. Bardak's instance (deployed by Pelouse) shows a broken image. Works on Nicolas's instance. Likely the avatar URL configured by Pelouse is invalid or blocked. PinchChat should handle broken avatar images gracefully (fallback to initials or default icon).
|
||||
|
||||
## Item #67
|
||||
- **Date:** 2026-02-13
|
||||
- **Priority:** medium
|
||||
- **Status:** pending
|
||||
- **Description:** Add an update indicator next to the version number in the UI. When a newer Docker image/release is available on GitHub (compare current version vs latest GitHub release tag), show a visual indicator (badge, dot, or link) so users know they can update.
|
||||
|
||||
## Item #68
|
||||
- **Date:** 2026-02-13
|
||||
- **Priority:** high
|
||||
- **Status:** pending
|
||||
- **Description:** Cursor desync in textarea STILL present after v1.39.2 fix. The cursor position still gets ahead of where characters actually appear. The previous fix (removing padding/bold from backdrop tokens) was insufficient. Need deeper investigation — likely the HighlightedTextarea backdrop and textarea have mismatched rendering (font metrics, line-height, word-wrap differences, or span wrapping in the backdrop creating different text flow). Consider disabling highlight entirely as test, or ensuring backdrop uses identical character-level rendering with zero extra styling that could affect text width/flow.
|
||||
|
||||
@@ -1,10 +1,33 @@
|
||||
import { useState, useMemo, useRef, useEffect, useCallback } from 'react';
|
||||
import { X, Sparkles, Search, Pin, Trash2, Columns2, Clock, Bot, MessageSquare, Globe, Zap } from 'lucide-react';
|
||||
import { X, Sparkles, Search, Pin, Trash2, Columns2, Clock, Bot, MessageSquare, Globe, Zap, ArrowUpCircle } from 'lucide-react';
|
||||
import type { Session } from '../types';
|
||||
import { useT } from '../hooks/useLocale';
|
||||
import { SessionIcon } from './SessionIcon';
|
||||
import { sessionDisplayName } from '../lib/sessionName';
|
||||
import { relativeTime } from '../lib/relativeTime';
|
||||
import { useUpdateCheck } from '../hooks/useUpdateCheck';
|
||||
|
||||
function VersionBadge() {
|
||||
const update = useUpdateCheck(__APP_VERSION__);
|
||||
if (update.available) {
|
||||
return (
|
||||
<a
|
||||
href={update.releaseUrl || '#'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="ml-1 inline-flex items-center gap-1 text-[9px] text-emerald-400 hover:text-emerald-300 transition-colors"
|
||||
title={`Update available: v${update.latestVersion}`}
|
||||
>
|
||||
<span className="text-pc-text-faint line-through select-all">v{__APP_VERSION__}</span>
|
||||
<ArrowUpCircle size={10} />
|
||||
<span>v{update.latestVersion}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="ml-1 text-[9px] text-pc-text-faint select-all" title={`PinchChat v${__APP_VERSION__}`}>v{__APP_VERSION__}</span>
|
||||
);
|
||||
}
|
||||
|
||||
const PINNED_KEY = 'pinchchat-pinned-sessions';
|
||||
const WIDTH_KEY = 'pinchchat-sidebar-width';
|
||||
@@ -490,7 +513,7 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, onSplit,
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-violet-300/60 shadow-[0_0_10px_rgba(168,85,247,0.5)]" />
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[var(--pc-accent-dim)] shadow-[0_0_10px_rgba(34,211,238,0.5)]" />
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-indigo-300/50 shadow-[0_0_10px_rgba(99,102,241,0.4)]" />
|
||||
<span className="ml-1 text-[9px] text-pc-text-faint select-all" title={`PinchChat v${__APP_VERSION__}`}>v{__APP_VERSION__}</span>
|
||||
<VersionBadge />
|
||||
</div>
|
||||
{/* Resize drag handle */}
|
||||
<div
|
||||
|
||||
65
src/hooks/useUpdateCheck.ts
Normal file
65
src/hooks/useUpdateCheck.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
const GITHUB_API = 'https://api.github.com/repos/MarlBurroW/pinchchat/releases/latest';
|
||||
const CHECK_INTERVAL = 60 * 60 * 1000; // 1 hour
|
||||
const CACHE_KEY = 'pinchchat-latest-version';
|
||||
|
||||
interface UpdateInfo {
|
||||
available: boolean;
|
||||
latestVersion: string | null;
|
||||
releaseUrl: string | null;
|
||||
}
|
||||
|
||||
export function useUpdateCheck(currentVersion: string): UpdateInfo {
|
||||
const [info, setInfo] = useState<UpdateInfo>({ available: false, latestVersion: null, releaseUrl: null });
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
async function check() {
|
||||
try {
|
||||
// Check cache first
|
||||
const cached = localStorage.getItem(CACHE_KEY);
|
||||
if (cached) {
|
||||
const { version, url, ts } = JSON.parse(cached);
|
||||
if (Date.now() - ts < CHECK_INTERVAL) {
|
||||
if (version && isNewer(version, currentVersion)) {
|
||||
setInfo({ available: true, latestVersion: version, releaseUrl: url });
|
||||
}
|
||||
timeout = setTimeout(check, CHECK_INTERVAL - (Date.now() - ts));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(GITHUB_API, { headers: { Accept: 'application/vnd.github.v3+json' } });
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const tag: string = data.tag_name?.replace(/^v/, '') || '';
|
||||
const url: string = data.html_url || '';
|
||||
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify({ version: tag, url, ts: Date.now() }));
|
||||
|
||||
if (tag && isNewer(tag, currentVersion)) {
|
||||
setInfo({ available: true, latestVersion: tag, releaseUrl: url });
|
||||
}
|
||||
} catch { /* silent */ }
|
||||
timeout = setTimeout(check, CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
check();
|
||||
return () => clearTimeout(timeout);
|
||||
}, [currentVersion]);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/** True if remote is newer than local (semver compare) */
|
||||
function isNewer(remote: string, local: string): boolean {
|
||||
const r = remote.split('.').map(Number);
|
||||
const l = local.split('.').map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if ((r[i] || 0) > (l[i] || 0)) return true;
|
||||
if ((r[i] || 0) < (l[i] || 0)) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -212,11 +212,12 @@ html, body {
|
||||
|
||||
.ht-backdrop,
|
||||
.ht-textarea {
|
||||
/* Must share identical text layout */
|
||||
/* Must share EXACTLY identical text layout — any difference causes cursor desync */
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
line-height: 1.25rem;
|
||||
padding: 0.75rem 1rem; /* py-3 px-4 */
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
letter-spacing: inherit;
|
||||
word-spacing: inherit;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
@@ -224,6 +225,7 @@ html, body {
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
/* padding, border, border-radius all come from the shared className */
|
||||
}
|
||||
|
||||
.ht-backdrop {
|
||||
@@ -231,9 +233,7 @@ html, body {
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
color: var(--pc-text-primary);
|
||||
border: 1px solid transparent; /* match textarea border width to keep text aligned */
|
||||
border-radius: 1rem; /* rounded-2xl */
|
||||
max-height: 200px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ht-textarea {
|
||||
@@ -246,38 +246,31 @@ html, body {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Token colors */
|
||||
/* Token colors — ONLY color allowed, nothing that changes text geometry */
|
||||
.ht-code-block {
|
||||
color: #67e8f9; /* cyan-300 */
|
||||
background: var(--pc-accent-glow);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ht-inline-code {
|
||||
color: #67e8f9;
|
||||
background: var(--pc-accent-glow);
|
||||
border-radius: 3px;
|
||||
/* No padding — must match textarea text layout exactly to avoid cursor desync */
|
||||
}
|
||||
|
||||
.ht-bold {
|
||||
color: var(--pc-text-primary);
|
||||
/* No font-weight change — bold text is wider and causes cursor desync */
|
||||
}
|
||||
|
||||
.ht-italic {
|
||||
color: var(--pc-text-secondary);
|
||||
font-style: italic;
|
||||
/* No font-style: italic — italic glyphs have different widths, causes cursor desync */
|
||||
}
|
||||
|
||||
.ht-heading {
|
||||
color: #a78bfa; /* violet-400 */
|
||||
font-weight: 600;
|
||||
/* No font-weight — bolder glyphs are wider, causes cursor desync */
|
||||
}
|
||||
|
||||
.ht-link {
|
||||
color: #67e8f9;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Accessibility: respect reduced-motion preferences */
|
||||
|
||||
Reference in New Issue
Block a user