fix(ui): mention color picker, send-animation conflict, DM virtualizer (N69,N10,N22)
- N69: @mention highlight color now uses HexColorPickerPopOut + react-colorful HexColorPicker behind a folds Button (color swatch); built-in onRemove replaces the separate Reset, dropping the OS-native <input type="color"> - N10: mentionPulseKeyframes animates only box-shadow (dropped the imperceptible scale(1.003)) so it no longer fights MsgAppearClass over `transform` on self-sent @mention messages - N22: Direct.tsx virtualizer estimateSize 38 -> 52 (two-line DM row height) to avoid the initial-render jump before measureElement corrects each row Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+3
-3
@@ -314,7 +314,7 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
|
||||
| N7 | Delivery Status | `Message.tsx` | 89–148 | `DeliveryStatus` renders Unicode glyphs (`⟳ ✓ ✕`) in a `<span>` with `fontSize: '10px'` instead of folds `<Icon>` components — **FIXED**: replaced with `Icons.Check/Cross/Send` via `<Icon size="100">` | `Icons.Check`, `Icons.Cross`, etc. are used for all other status glyphs; folds `Text` size tokens for all supplementary text |
|
||||
| N8 | GIF Picker | `GifPicker.tsx` | 83–124 | GIF picker container uses fully bespoke inline styles (`borderRadius: '12px'`, `boxShadow: '0 8px 32px rgba(0,0,0,0.4)'`, raw `rgba` border) — two separate style sets for TDS and non-TDS paths | `EmojiBoard` has no caller-applied container styling; folds components handle their own surface internally via design tokens |
|
||||
| N9 | GIF Button | `RoomInput.tsx` | 1076–1087 | GIF toolbar button renders `<Text size="T200">` with hand-rolled `fontWeight`/`fontSize`/`letterSpacing` instead of `<Icon>` — **WON'T FIX (deliberate)**: folds has no GIF icon, and "GIF" is a widely-recognized text affordance (Slack/Discord/Element all use a text label). Converting to an arbitrary icon would be less clear, not more. | All 8 other toolbar buttons (`Smile`, `Sticker`, `Location`, `Poll`, etc.) use `<Icon src={...} />` exclusively |
|
||||
| N10 | Send Animation | `Message.tsx` + `Animations.css.ts` | 979–998 / 60–71 | `MsgAppearClass` and `MentionHighlightPulse` both animate `transform: scale` on the same `MessageBase` DOM node — on self-sent mention messages both classes apply simultaneously and fight over the `transform` property | Pre-existing `highlightAnime` only animates `backgroundColor`; no prior `transform` animation on `MessageBase` |
|
||||
| N10 | Send Animation | `Message.tsx` + `Animations.css.ts` | 979–998 / 60–71 | `MsgAppearClass` and `MentionHighlightPulse` both animate `transform: scale` on the same `MessageBase` DOM node — on self-sent mention messages both classes apply simultaneously and fight over the `transform` property — **FIXED**: `mentionPulseKeyframes` now animates only `box-shadow` (dropped the imperceptible `scale(1.003)`), so the appear-scale and the mention glow no longer contend for `transform` | Pre-existing `highlightAnime` only animates `backgroundColor`; no prior `transform` animation on `MessageBase` |
|
||||
| N11 | AvatarDecoration | `AvatarDecoration.tsx` | 5 / 38–41 | Fixed 8px inset on all sides regardless of avatar size — at folds size `"200"` (~32px) the decoration bleeds 50% of the avatar diameter, clipping against `overflow: hidden` parent containers in member lists. **Inset issue still OPEN.** _Related regression fixed in `useAvatarDecoration.ts`_: the decoration fetch cached **all** failures (including transient 429/5xx) as "no decoration" permanently for the session, so a single rate-limited burst (member list / timeline mount many avatars at once) would make decorations vanish until a full reload. Now only a genuine 404 is cached; transient errors retry on the next mount. | Folds `Avatar` and `PresenceRingAvatar` do not emit overflow outside their bounding box |
|
||||
| N12 | MediaGallery Drawer | `MediaGallery.tsx` | 651–661 | Drawer uses `position: 'fixed'` with hardcoded `width: '320px'` as inline styles on a `<Box>` — **FIXED**: moved positioning/width into co-located `MediaGallery.css.ts` using `toRem(320)` + a `max-width: 750px` full-screen media query (mirrors `MembersDrawer`); border/header now use `config.borderWidth`/`config.space` tokens. Added Escape-to-close on the panel (previously only the lightbox handled Escape). **Full chrome redesign (round 2)** to match native conventions: panel + header switched from `Surface` to `Background` variant (matching `MembersDrawer`/Saved Messages); header now `Text size="H4"` + plain close `IconButton` (dropped the bespoke tooltip-wrapped button); tabs moved to a bordered toolbar strip with the `variant={active?'Primary':'Secondary'} fill={active?'Solid':'Soft'}` pattern from `PolicyListViewer` and now show per-tab counts; the centered "lines + label" month divider replaced with a left-aligned group label (Cinny group-label pattern); thumbnail tiles moved hover/focus styling to CSS `:hover`/`:focus-visible` (no JS hover state) and into `MediaGallery.css.ts`; file rows + grid tokenized. **Docking fix (round 3)** — the core of the finding: the gallery was a `position: fixed` overlay floating over the timeline, mounted from `RoomViewHeader`. It is now a **docked flex sibling** in the room layout row, exactly like `MembersDrawer`: open state lifted to a `mediaGalleryAtom` (mirrors `bookmarksPanelAtom`), rendered in `Room.tsx` with a vertical `Line` separator on desktop and `key={room.roomId}` to reset per room; the CSS is static-width on desktop and only `position: fixed; inset: 0` full-screen on mobile (identical strategy to `MembersDrawer.css`). It now shares the row with the timeline instead of overlapping it. | `MembersDrawer` uses a vanilla-extract class with `width: toRem(266)` and is placed by the layout system, not `position: fixed`. 54px width discrepancy also breaks visual rhythm if both panels could be open |
|
||||
| N13 | ScheduledMessagesTray | `ScheduledMessagesTray.tsx` | 108–126 | Collapsible tray header is `<Box as="button">` with `cursor: 'pointer'` inline style and no folds variant — no hover state, no focus ring — **FIXED**: replaced with folds `<Button variant="Secondary" fill="None" radii="0">` using `before`/`after` icon props (gains design-system hover/focus) | All clickable header/toggle elements in the room view use folds `<Button>` or `<IconButton>` with explicit variants for hover/focus; `<Box as="button">` with no variant is used nowhere else |
|
||||
@@ -326,7 +326,7 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
|
||||
| N19 | Presence Labels | `useUserPresence.ts` vs `SettingsTab.tsx` | 55–62 / 36–42 | `PresenceBadge` tooltip shows "Active / Busy / Away"; `PresencePicker` options read "Online / Idle / Do Not Disturb / Invisible" — a DND user shows tooltip "Busy", not "Do Not Disturb" — **FIXED**: aligned `usePresenceLabel` reader vocabulary to the setter (online→"Online", unavailable→"Idle", offline→"Offline") | Within the same Lotus feature set the user-facing vocabulary is inconsistent between the setter UI and the reader tooltip |
|
||||
| N20 | Notification Presets | `Notifications.tsx` | 57–107 | Gaming/Work/Sleep preset buttons are bare `<button>` elements with Lotus-specific CSS vars (`--border-interactive-normal`, `--bg-surface-low`) not defined in all themes — **FIXED**: converted to folds `<Button variant="Secondary" fill="Soft" radii="300">` (auto height) wrapping the emoji/label/description column; undefined vars removed | Grouped preset/action buttons elsewhere use folds `Chip variant="Primary/Secondary" outlined radii="Pill"` (e.g., Composer Toolbar toggles in `General.tsx:1100–1113`) |
|
||||
| N21 | Notification Sound Selects | `SystemNotification.tsx` | 111–305 | Message sound, invite sound, and quiet-hours time pickers are bare `<select>`/`<input type="time">` with `colorScheme: 'dark'` workaround | All other dropdowns in settings use the `Button`+`PopOut`+`Menu`+`MenuItem` folds pattern; the native select renders OS-styled on all platforms |
|
||||
| N22 | DM Preview Virtualizer | `RoomNavItem.tsx` / `Direct.tsx` | 608–627 / 232 | DM preview adds a second text row to each DM item, making it taller than 38px, but `useVirtualizer` in `Direct.tsx` still uses `estimateSize: () => 38` — causes layout jump/overlap on initial render | Non-DM rooms in Home.tsx also estimate 38px; DM items with a preview are now a different height, creating two visual densities in the same nav column |
|
||||
| N22 | DM Preview Virtualizer | `RoomNavItem.tsx` / `Direct.tsx` | 608–627 / 232 | DM preview adds a second text row to each DM item, making it taller than 38px, but `useVirtualizer` in `Direct.tsx` still uses `estimateSize: () => 38` — causes layout jump/overlap on initial render — **FIXED**: bumped `estimateSize` to 52 (the two-line DM-row height) so the initial estimate matches the common case; `measureElement` still corrects each row exactly | Non-DM rooms in Home.tsx also estimate 38px; DM items with a preview are now a different height, creating two visual densities in the same nav column |
|
||||
| N23 | RoomServerACL | `RoomServerACL.tsx` | 100–115 / 298–309 | Server-name text input is a raw `<input type="text">` with inline style object; "Allow IP literal addresses" is a raw `<input type="checkbox">` with `style={{ width: 16, height: 16 }}` — **FIXED**: text input → folds `<Input variant={error?'Critical':'Secondary'}>`; checkbox → folds `<Checkbox variant="Primary">` | All other text/boolean controls in room settings use folds `Input` and `Checkbox` components (`RoomAddress.tsx:163`, `RoomAddress.tsx:330`) |
|
||||
| N24 | PolicyListViewer | `PolicyListViewer.tsx` | 245–264 | Room-ID add input is a raw `<input type="text">` with manually replicated folds token values — **FIXED**: replaced with folds `<Input variant={error?'Critical':'Secondary'} size="400" radii="300">` | Native pattern: folds `<Input variant="Secondary" size="300" radii="300">` — no inline style needed |
|
||||
| N25 | ExportRoomHistory Inputs | `ExportRoomHistory.tsx` | 258–292 | Both date range pickers are raw `<input type="date">` with inline styles — **FIXED**: replaced with folds `<Input type="date" variant="Secondary" size="400" radii="300">` | Native pattern: folds `Input` component; `<input type="date">` renders OS-native date picker, unstyled relative to the rest of the settings panel |
|
||||
@@ -418,7 +418,7 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
|
||||
| N66 | DateRangeButton — Native `<input type="date">` | `SearchFilters.tsx` | 558–589 | "From" and "To" date fields are raw `<input type="date">` with inline style overrides including `fontSize: '0.82rem'` — **FIXED**: replaced both with folds `<Input type="date" variant="SurfaceVariant" size="300" radii="300">`; removed now-unused `color` import | `SelectRoomButton` (same file, line 224) and `SelectSenderButton` (line 424) both use folds `<Input size="300" radii="300">`; the date inputs are the only native browser inputs in the search filter row |
|
||||
| N67 | SeasonalEffect / NightLight Z-Index Order | `SeasonalEffect.tsx` / `App.tsx` | 759 / 62–77 | `SeasonalEffect` mounts at `zIndex: 9999`; `NightLightOverlay` at `zIndex: 9998`. Seasonal particles render **above** Night Light so they are never tinted. `SeasonalEffect` also shares `z-index: 9999` with the skip-to-content link in `ClientLayout.tsx` — **FIXED**: lowered `SeasonalEffect` overlay to `zIndex: 9997` (below Night Light at 9998 and modals at 9999), so Night Light now tints the particles and dialogs are never obscured | Expected UX: Night Light tints all visible content including effects; requires either a higher Night Light z-index or a lower SeasonalEffect z-index |
|
||||
| N68 | Syntax Highlighting — `--lt-accent-*` Vars in Non-TDS Themes | `syntaxHighlight.ts` | 313–323 | `tokenStyle()` returns `var(--lt-accent-cyan/green/orange/purple, hardcoded-fallback)` — `--lt-*` vars only exist in TDS mode; fallbacks are Monokai dark colors that have poor contrast on light themes and no relationship to the existing `--prism-*` variables in `ReactPrism.css` — **FIXED**: `tokenStyle()` now maps to the `--prism-*` family (keyword/selector/boolean/atrule/comment) which has proper light/dark/TDS palettes; comment uses `--prism-comment` instead of an opacity hack | `ReactPrism.css` uses `--prism-keyword`, `--prism-selector` etc. which switch correctly between light and dark palettes; syntax highlighting should use the same variable family |
|
||||
| N69 | Mention Highlight — `<input type="color">` Instead of `HexColorPickerPopOut` | `General.tsx` | 644–675 | Raw `<input type="color">` with hardcoded pixel dimensions; OS-native color picker chrome renders completely differently from the rest of settings UI | `PowersEditor.tsx:125–143` establishes `<HexColorPickerPopOut picker={<HexColorPicker ...>}>` as the codebase's color-picking pattern; Reset button should be `<Button size="300" variant="Secondary" radii="300">` |
|
||||
| N69 | Mention Highlight — `<input type="color">` Instead of `HexColorPickerPopOut` | `General.tsx` | 644–675 | Raw `<input type="color">` with hardcoded pixel dimensions; OS-native color picker chrome renders completely differently from the rest of settings UI — **FIXED**: replaced with `<HexColorPickerPopOut>` + `<HexColorPicker>` (react-colorful) behind a folds `<Button>` trigger showing a color swatch; the picker's built-in `onRemove` replaces the separate Reset button | `PowersEditor.tsx:125–143` establishes `<HexColorPickerPopOut picker={<HexColorPicker ...>}>` as the codebase's color-picking pattern; Reset button should be `<Button size="300" variant="Secondary" radii="300">` |
|
||||
| N70 | ChatBgGrid / SeasonalBgGrid — Raw `<button>` Elements | `General.tsx` | 1648–1689 / 1711–1742 | Both pickers use raw HTML `<button>` elements with hardcoded `width: toRem(76)`, `height: toRem(50/56)`, `borderRadius: toRem(8)`, `border: 2px solid rgba(...)` — no focus ring via folds, no `variant` prop, no hover state from the design system | Native Cinny theme pickers use folds `<MenuItem>` or `<Chip>` which respond to theme and provide focus/hover states automatically |
|
||||
|
||||
---
|
||||
|
||||
@@ -110,10 +110,15 @@ export type MessageBaseVariants = RecipeVariants<typeof MessageBase>;
|
||||
|
||||
// ── Mention pulse animation ───────────────────────────────────────────────────
|
||||
|
||||
// Animates only `box-shadow` — NOT `transform`. A self-sent @mention message
|
||||
// carries both this class and `MsgAppearClass` (which animates a scale), and two
|
||||
// animations on the same element cannot share the `transform` property: the
|
||||
// later one wins and the other is silently dropped. Pulsing the glow alone keeps
|
||||
// both effects working. (The previous scale(1.003) was imperceptible anyway.)
|
||||
const mentionPulseKeyframes = keyframes({
|
||||
'0%': { transform: 'scale(1)', boxShadow: 'none' },
|
||||
'30%': { transform: 'scale(1.003)', boxShadow: `0 0 8px ${color.Warning.Main}` },
|
||||
'100%': { transform: 'scale(1)', boxShadow: 'none' },
|
||||
'0%': { boxShadow: 'none' },
|
||||
'30%': { boxShadow: `0 0 8px ${color.Warning.Main}` },
|
||||
'100%': { boxShadow: 'none' },
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,7 +32,9 @@ import {
|
||||
toRem,
|
||||
} from 'folds';
|
||||
import { isKeyHotkey } from 'is-hotkey';
|
||||
import { HexColorPicker } from 'react-colorful';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { HexColorPickerPopOut } from '../../../components/HexColorPickerPopOut';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import {
|
||||
@@ -640,33 +642,42 @@ function Appearance() {
|
||||
title="@Mention Highlight Color"
|
||||
description="Color used to highlight messages that mention you. Leave empty to use the theme default."
|
||||
after={
|
||||
<Box alignItems="Center" gap="200">
|
||||
<input
|
||||
type="color"
|
||||
value={mentionHighlightColor || '#4caf50'}
|
||||
onChange={(e) => setMentionHighlightColor(e.target.value)}
|
||||
style={{
|
||||
width: '36px',
|
||||
height: '28px',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
padding: '2px',
|
||||
}}
|
||||
/>
|
||||
{mentionHighlightColor && (
|
||||
<HexColorPickerPopOut
|
||||
picker={
|
||||
<HexColorPicker
|
||||
color={mentionHighlightColor || '#4caf50'}
|
||||
onChange={setMentionHighlightColor}
|
||||
/>
|
||||
}
|
||||
onRemove={mentionHighlightColor ? () => setMentionHighlightColor('') : undefined}
|
||||
>
|
||||
{(openPicker, opened) => (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setMentionHighlightColor('')}
|
||||
aria-pressed={opened}
|
||||
onClick={openPicker}
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
before={
|
||||
<span
|
||||
style={{
|
||||
width: toRem(16),
|
||||
height: toRem(16),
|
||||
borderRadius: config.radii.R300,
|
||||
background: mentionHighlightColor || color.Primary.Main,
|
||||
border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
|
||||
display: 'inline-block',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Text size="B300">Reset</Text>
|
||||
<Text size="B300">{mentionHighlightColor ? 'Change' : 'Pick'}</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</HexColorPickerPopOut>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
|
||||
@@ -227,7 +227,11 @@ export function Direct() {
|
||||
const virtualizer = useVirtualizer({
|
||||
count: filteredDirects.length,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: () => 38,
|
||||
// DM rows render a two-line layout (name + message preview), so they are
|
||||
// taller than the 38px single-line rooms elsewhere. Estimating the common
|
||||
// two-line height avoids the initial-render jump/overlap before
|
||||
// `measureElement` corrects each row to its exact size.
|
||||
estimateSize: () => 52,
|
||||
overscan: 10,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user