Fix theme input scrollbar line, jumbo emoji, zalgo clip, and Windows autocorrect dupe.

Make twilight/mocha scrollbar tracks transparent so the editor no longer shows a vertical strip, size standalone emoji to 100px, loosen zalgo clipping with padded overflow, and handle insertReplacementText via onDOMBeforeInput so Slate does not insert the correction twice.
This commit is contained in:
2026-07-21 15:33:20 +10:00
parent 154f4dfdb0
commit acd53966b2
5 changed files with 60 additions and 51 deletions

View File

@@ -6,12 +6,11 @@ import React, {
forwardRef,
useCallback,
useState,
useEffect,
useRef,
} from 'react';
import { Box, Scroll, Text } from 'folds';
import { Descendant, Editor, createEditor, Transforms, Range, Element as SlateElement, Text as SlateText, Point } from 'slate';
import {
ReactEditor,
Slate,
Editable,
withReact,
@@ -172,48 +171,41 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
[editor, onKeyDown]
);
const handleBeforeInput = useCallback(
(event: Event) => {
const inputEvent = event as InputEvent;
/**
* Spellcheck / right-click autocorrect (insertReplacementText).
* Must run as Editable's onDOMBeforeInput and return true so Slate skips its
* own insert — slate-react ignores event.defaultPrevented when this prop is unset,
* which previously duplicated the corrected word (especially on Windows Electron).
*/
const handleDOMBeforeInput = useCallback(
(event: InputEvent) => {
if (event.inputType !== 'insertReplacementText') return false;
// Handle autocorrect replacement that causes text duplication
if (inputEvent.inputType === 'insertReplacementText' ||
inputEvent.inputType === 'insertFromComposition') {
const { selection } = editor;
if (!selection) return;
const data =
event.dataTransfer?.getData('text/plain') || event.data || '';
if (!data) return false;
// Get the data being inserted
const data = inputEvent.data || inputEvent.dataTransfer?.getData('text/plain');
if (data) {
event.preventDefault();
// If there's selected text, delete it first
if (selection && !Range.isCollapsed(selection)) {
Transforms.delete(editor, { at: selection });
const [targetRange] = event.getTargetRanges();
if (targetRange) {
const slateRange = ReactEditor.toSlateRange(editor, targetRange, {
exactMatch: false,
suppressThrow: true,
});
if (slateRange) {
Transforms.select(editor, slateRange);
}
} else if (editor.selection && !Range.isCollapsed(editor.selection)) {
Transforms.delete(editor, { at: editor.selection });
}
// Insert the replacement text
editor.insertText(data);
}
}
Transforms.insertText(editor, data);
return true;
},
[editor]
);
const editableRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const editableElement = editableRef.current?.querySelector('[data-slate-editor="true"]');
if (!editableElement) return;
editableElement.addEventListener('beforeinput', handleBeforeInput, { capture: true });
return () => {
editableElement.removeEventListener('beforeinput', handleBeforeInput, { capture: true });
};
}, [handleBeforeInput]);
const renderPlaceholder = useCallback(
({ attributes, children }: RenderPlaceholderProps) => (
<span {...attributes} className={css.EditorPlaceholderContainer}>
@@ -259,7 +251,6 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
size="300"
visibility="Hover"
hideTrack
ref={editableRef}
>
<Editable
data-editable-name={editableName}
@@ -269,6 +260,7 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
renderElement={renderElement}
renderLeaf={renderLeaf}
decorate={decorate}
onDOMBeforeInput={handleDOMBeforeInput}
onKeyDown={handleKeydown}
onKeyUp={onKeyUp}
onPaste={onPaste}

View File

@@ -35,6 +35,8 @@ export const ReplyContent = style({
overflow: 'clip',
overflowY: 'clip',
lineHeight: '1.5',
paddingBlock: toRem(4),
marginBlock: toRem(-4),
selectors: {
[`${Reply}:hover &`]: {
opacity: config.opacity.P500,

View File

@@ -1,6 +1,7 @@
import { createVar, keyframes, style, styleVariants } from '@vanilla-extract/css';
import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
import { DefaultReset, color, config, toRem } from 'folds';
import * as htmlCss from '../../../styles/CustomHtml.css';
export const StickySection = style({
position: 'sticky',
@@ -212,9 +213,12 @@ export const UsernameBold = style({
export const MessageTextBody = recipe({
base: {
wordBreak: 'break-word',
// Clip zalgo/combining marks with room so normal text + emoji aren't shaved
overflow: 'clip',
overflowY: 'clip',
lineHeight: '1.5',
padding: toRem(20),
margin: toRem(-20),
userSelect: 'text',
WebkitUserSelect: 'text',
},
@@ -226,8 +230,27 @@ export const MessageTextBody = recipe({
},
jumboEmoji: {
true: {
fontSize: '1.504em',
lineHeight: '1.4962em',
fontSize: toRem(100),
lineHeight: toRem(100),
selectors: {
[`& .${htmlCss.EmoticonBase}`]: {
height: toRem(100),
padding: 0,
verticalAlign: 'bottom',
},
[`& .${htmlCss.Emoticon.classNames.base}`]: {
fontSize: toRem(100),
height: toRem(100),
minWidth: toRem(100),
lineHeight: toRem(100),
top: 0,
},
[`& .${htmlCss.EmoticonImg}`]: {
height: toRem(100),
width: toRem(100),
objectFit: 'contain',
},
},
},
},
emote: {

View File

@@ -19,7 +19,7 @@ export const MarginSpaced = style({
export const Paragraph = style([
DefaultReset,
{
overflow: 'hidden',
// Zalgo clipping is handled by MessageTextBody padding + overflow
lineHeight: '1.5',
},
]);
@@ -29,7 +29,6 @@ export const Heading = style([
MarginSpaced,
{
marginTop: config.space.S400,
overflow: 'hidden',
lineHeight: '1.5',
selectors: {
'&:first-child': {
@@ -46,7 +45,6 @@ export const BlockQuote = style([
paddingLeft: config.space.S200,
borderLeft: `${config.borderWidth.B700} solid ${color.SurfaceVariant.ContainerLine}`,
fontStyle: 'italic',
overflow: 'hidden',
lineHeight: '1.5',
},
]);
@@ -73,7 +71,6 @@ export const Code = style([
{
padding: `0 ${config.space.S100}`,
display: 'inline',
overflow: 'hidden',
lineHeight: '1.5',
},
]);
@@ -120,7 +117,6 @@ export const Spoiler = recipe({
padding: `0 ${config.space.S100}`,
backgroundColor: color.SurfaceVariant.ContainerActive,
borderRadius: config.radii.R300,
overflow: 'hidden',
lineHeight: '1.5',
selectors: {
'&[aria-pressed=true]': {
@@ -182,7 +178,6 @@ export const List = style([
{
padding: `0 ${config.space.S100}`,
paddingLeft: config.space.S600,
overflow: 'hidden',
lineHeight: '1.5',
},
]);
@@ -211,7 +206,6 @@ export const Mention = recipe({
padding: `0 ${toRem(2)}`,
borderRadius: config.radii.R300,
fontWeight: config.fontWeight.W500,
overflow: 'hidden',
lineHeight: '1.5',
},
],
@@ -238,7 +232,6 @@ export const Command = recipe({
padding: `0 ${toRem(2)}`,
borderRadius: config.radii.R300,
fontWeight: config.fontWeight.W500,
overflow: 'hidden',
lineHeight: '1.5',
},
],
@@ -308,7 +301,6 @@ export const highlightText = style([
{
backgroundColor: 'yellow',
color: 'black',
overflow: 'hidden',
lineHeight: '1.5',
},
]);

View File

@@ -257,7 +257,7 @@ body.mocha-theme {
}
.twilight-theme ::-webkit-scrollbar-track {
background: rgba(20, 18, 31, 0.5);
background: transparent;
}
.twilight-theme ::-webkit-scrollbar-thumb {
@@ -355,7 +355,7 @@ body.mocha-theme {
}
.mocha-theme ::-webkit-scrollbar-track {
background: rgba(26, 22, 20, 0.5);
background: transparent;
}
.mocha-theme ::-webkit-scrollbar-thumb {