替换公共自适应输入框实现
使用 Lexical 管理受控纯文本输入 接入 OverlayScrollbars 统一内部滚动条 补充编辑器状态与滚动结构行为测试
This commit is contained in:
Generated
+29
@@ -30,6 +30,8 @@
|
||||
"lexical": "^0.47.0",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"overlayscrollbars": "2.16.0",
|
||||
"overlayscrollbars-react": "0.5.6",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@@ -15895,6 +15897,22 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/overlayscrollbars": {
|
||||
"version": "2.16.0",
|
||||
"resolved": "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-2.16.0.tgz",
|
||||
"integrity": "sha512-N03oje/q7j93D0aLZtoCdsDSYLmhheSsv8H7oSLE7HhdV9P/bmCURtLV/KbPye7P/bpfyt/obSfDpGUYoJ0OWg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/overlayscrollbars-react": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/overlayscrollbars-react/-/overlayscrollbars-react-0.5.6.tgz",
|
||||
"integrity": "sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"overlayscrollbars": "^2.0.0",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
@@ -31502,6 +31520,17 @@
|
||||
"wcwidth": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"overlayscrollbars": {
|
||||
"version": "2.16.0",
|
||||
"resolved": "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-2.16.0.tgz",
|
||||
"integrity": "sha512-N03oje/q7j93D0aLZtoCdsDSYLmhheSsv8H7oSLE7HhdV9P/bmCURtLV/KbPye7P/bpfyt/obSfDpGUYoJ0OWg=="
|
||||
},
|
||||
"overlayscrollbars-react": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/overlayscrollbars-react/-/overlayscrollbars-react-0.5.6.tgz",
|
||||
"integrity": "sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw==",
|
||||
"requires": {}
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
|
||||
@@ -191,6 +191,8 @@
|
||||
"lexical": "^0.47.0",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"overlayscrollbars": "2.16.0",
|
||||
"overlayscrollbars-react": "0.5.6",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { act } from '@testing-library/react';
|
||||
import {
|
||||
$createLineBreakNode,
|
||||
$createParagraphNode,
|
||||
$createTextNode,
|
||||
$getRoot,
|
||||
getNearestEditorFromDOMNode,
|
||||
} from 'lexical';
|
||||
|
||||
export function setPlainTextEditorValue(
|
||||
contentEditable: HTMLElement,
|
||||
value: string,
|
||||
) {
|
||||
const editor = getNearestEditorFromDOMNode(contentEditable);
|
||||
if (!editor) {
|
||||
throw new Error('Expected a Lexical editor root');
|
||||
}
|
||||
|
||||
act(() => {
|
||||
editor.update(
|
||||
() => {
|
||||
const paragraph = $createParagraphNode();
|
||||
value.split('\n').forEach((line, index, lines) => {
|
||||
if (line) {
|
||||
paragraph.append($createTextNode(line));
|
||||
}
|
||||
if (index < lines.length - 1) {
|
||||
paragraph.append($createLineBreakNode());
|
||||
}
|
||||
});
|
||||
$getRoot().clear().append(paragraph);
|
||||
paragraph.selectEnd();
|
||||
},
|
||||
{ discrete: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function getPlainTextEditorHost(contentEditable: HTMLElement) {
|
||||
const host = contentEditable.closest<HTMLElement>(
|
||||
'.auto-grow-text-area',
|
||||
);
|
||||
if (!host) {
|
||||
throw new Error('Expected an auto-grow plain-text editor host');
|
||||
}
|
||||
return host;
|
||||
}
|
||||
@@ -1,288 +1,175 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useState } from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AutoGrowTextArea } from './AutoGrowTextArea';
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
beforeEach(() => {
|
||||
Range.prototype.getBoundingClientRect = vi.fn(() => new DOMRect());
|
||||
Range.prototype.getClientRects = vi.fn(() => [] as unknown as DOMRectList);
|
||||
vi.stubGlobal(
|
||||
'ResizeObserver',
|
||||
class ResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
},
|
||||
);
|
||||
vi.stubGlobal(
|
||||
'ClipboardEvent',
|
||||
class ClipboardEvent extends Event {
|
||||
clipboardData: DataTransfer;
|
||||
|
||||
constructor(
|
||||
type: string,
|
||||
init: EventInit & { clipboardData: DataTransfer },
|
||||
) {
|
||||
super(type, init);
|
||||
this.clipboardData = init.clipboardData;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
function pastePlainText(editor: HTMLElement, value: string) {
|
||||
const clipboardData = {
|
||||
files: [],
|
||||
getData: (type: string) => (type === 'text/plain' ? value : ''),
|
||||
items: [],
|
||||
types: ['text/plain'],
|
||||
} as unknown as DataTransfer;
|
||||
fireEvent(
|
||||
editor,
|
||||
new ClipboardEvent('paste', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clipboardData,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function ControlledEditor({
|
||||
initialValue = '',
|
||||
maxLength,
|
||||
}: {
|
||||
initialValue?: string;
|
||||
maxLength?: number;
|
||||
}) {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
return (
|
||||
<>
|
||||
<AutoGrowTextArea
|
||||
aria-label="受控编辑器"
|
||||
value={value}
|
||||
maxLength={maxLength}
|
||||
onValueChange={setValue}
|
||||
/>
|
||||
<output aria-label="React 文本状态">{value}</output>
|
||||
<button type="button" onClick={() => setValue('外部\n更新')}>
|
||||
外部更新
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
describe('AutoGrowTextArea', () => {
|
||||
it('uses native field sizing when the browser supports it', () => {
|
||||
const supports = vi.fn().mockReturnValue(true);
|
||||
const resizeObserver = vi.fn();
|
||||
const onChange = vi.fn();
|
||||
vi.stubGlobal('CSS', { supports });
|
||||
vi.stubGlobal('ResizeObserver', resizeObserver);
|
||||
it('keeps React state authoritative and preserves exact line breaks', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ControlledEditor initialValue={'第一行\n第二行'} />);
|
||||
|
||||
render(
|
||||
<AutoGrowTextArea
|
||||
aria-label="自动增长输入框"
|
||||
value=""
|
||||
onChange={onChange}
|
||||
/>,
|
||||
const editor = screen.getByRole('textbox', { name: '受控编辑器' });
|
||||
expect(screen.getByLabelText('React 文本状态').textContent).toBe(
|
||||
'第一行\n第二行',
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText(
|
||||
'自动增长输入框',
|
||||
) as HTMLTextAreaElement;
|
||||
expect(supports).toHaveBeenCalledWith('field-sizing', 'content');
|
||||
expect(resizeObserver).not.toHaveBeenCalled();
|
||||
expect(input.getAttribute('rows')).toBe('1');
|
||||
expect(input.className).toContain('auto-grow-text-area');
|
||||
expect(input.className).toContain('[field-sizing:content]');
|
||||
expect(input.className).toContain('min-h-10');
|
||||
expect(input.className).toContain('max-h-32');
|
||||
expect(input.className).toContain('overflow-y-auto');
|
||||
expect(input.className).toContain('disabled:cursor-not-allowed');
|
||||
expect(input.className).toContain('disabled:opacity-60');
|
||||
expect(input.style.height).toBe('');
|
||||
|
||||
fireEvent.change(input, { target: { value: '新内容' } });
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
await user.click(screen.getByRole('button', { name: '外部更新' }));
|
||||
expect(screen.getByLabelText('React 文本状态').textContent).toBe(
|
||||
'外部\n更新',
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to measured sizing, preserves css limits and remeasures width changes', () => {
|
||||
let resizeObserverCallback!: ResizeObserverCallback;
|
||||
const observe = vi.fn();
|
||||
const disconnect = vi.fn();
|
||||
vi.stubGlobal('CSS', { supports: vi.fn().mockReturnValue(false) });
|
||||
vi.stubGlobal(
|
||||
'ResizeObserver',
|
||||
vi.fn().mockImplementation((callback: ResizeObserverCallback) => {
|
||||
resizeObserverCallback = callback;
|
||||
return {
|
||||
observe,
|
||||
unobserve: vi.fn(),
|
||||
disconnect,
|
||||
};
|
||||
}),
|
||||
it('publishes edits through onValueChange and enforces maxLength', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ControlledEditor maxLength={4} />);
|
||||
|
||||
const editor = screen.getByRole('textbox', { name: '受控编辑器' });
|
||||
await user.click(editor);
|
||||
pastePlainText(editor, '12345');
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByLabelText('React 文本状态').textContent).toBe('1234'),
|
||||
);
|
||||
expect(editor.textContent).toBe('1234');
|
||||
});
|
||||
|
||||
it('supports undo and redo while keeping React state synchronized', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ControlledEditor />);
|
||||
|
||||
const editor = screen.getByRole('textbox', { name: '受控编辑器' });
|
||||
await user.click(editor);
|
||||
pastePlainText(editor, '草稿');
|
||||
await waitFor(() =>
|
||||
expect(screen.getByLabelText('React 文本状态').textContent).toBe('草稿'),
|
||||
);
|
||||
|
||||
const { rerender, unmount } = render(
|
||||
await user.keyboard('{Control>}z{/Control}');
|
||||
expect(screen.getByLabelText('React 文本状态').textContent).toBe('');
|
||||
|
||||
await user.keyboard('{Control>}{Shift>}z{/Shift}{/Control}');
|
||||
expect(screen.getByLabelText('React 文本状态').textContent).toBe('草稿');
|
||||
});
|
||||
|
||||
it('exposes read-only and disabled accessibility without form controls', () => {
|
||||
const { container } = render(
|
||||
<AutoGrowTextArea
|
||||
aria-label="自动增长输入框"
|
||||
value=""
|
||||
style={{
|
||||
boxSizing: 'border-box',
|
||||
minHeight: '80px',
|
||||
maxHeight: '128px',
|
||||
}}
|
||||
onChange={vi.fn()}
|
||||
aria-label="只读编辑器"
|
||||
value="只读内容"
|
||||
disabled
|
||||
readOnly
|
||||
onValueChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
const input = screen.getByLabelText(
|
||||
'自动增长输入框',
|
||||
) as HTMLTextAreaElement;
|
||||
expect(observe).toHaveBeenCalledWith(input);
|
||||
let contentHeight = 92;
|
||||
Object.defineProperty(input, 'offsetHeight', {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
const styleHeight = Number.parseFloat(input.style.height);
|
||||
return Number.isNaN(styleHeight) ? 82 : styleHeight;
|
||||
},
|
||||
});
|
||||
Object.defineProperty(input, 'clientHeight', {
|
||||
configurable: true,
|
||||
get: () => input.offsetHeight - 2,
|
||||
});
|
||||
Object.defineProperty(input, 'scrollHeight', {
|
||||
configurable: true,
|
||||
get: () => contentHeight,
|
||||
});
|
||||
|
||||
rerender(
|
||||
const editor = screen.getByRole('textbox', { name: '只读编辑器' });
|
||||
expect(editor.getAttribute('aria-disabled')).toBe('true');
|
||||
expect(editor.getAttribute('aria-readonly')).toBe('true');
|
||||
expect(editor.getAttribute('tabindex')).toBe('-1');
|
||||
expect(container.querySelector('textarea')).toBeNull();
|
||||
expect(container.querySelector('form')).toBeNull();
|
||||
});
|
||||
|
||||
it('uses a distinct internal viewport for overflow content', async () => {
|
||||
const { container } = render(
|
||||
<AutoGrowTextArea
|
||||
aria-label="自动增长输入框"
|
||||
value="第一行\n第二行\n第三行\n第四行\n第五行"
|
||||
style={{
|
||||
boxSizing: 'border-box',
|
||||
minHeight: '80px',
|
||||
maxHeight: '128px',
|
||||
}}
|
||||
onChange={vi.fn()}
|
||||
aria-label="滚动编辑器"
|
||||
value={Array.from({ length: 20 }, (_, index) => `第 ${index + 1} 行`).join(
|
||||
'\n',
|
||||
)}
|
||||
onValueChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(input.style.height).toBe('94px');
|
||||
expect(input.style.overflowY).toBe('hidden');
|
||||
|
||||
contentHeight = 180;
|
||||
act(() => {
|
||||
resizeObserverCallback(
|
||||
[
|
||||
{
|
||||
target: input,
|
||||
contentRect: { width: 180 },
|
||||
} as unknown as ResizeObserverEntry,
|
||||
],
|
||||
{} as ResizeObserver,
|
||||
const viewport = await waitFor(() => {
|
||||
const element = container.querySelector<HTMLElement>(
|
||||
'[data-overlayscrollbars-viewport]',
|
||||
);
|
||||
expect(element).not.toBeNull();
|
||||
return element!;
|
||||
});
|
||||
expect(input.style.height).toBe('128px');
|
||||
expect(input.style.overflowY).toBe('auto');
|
||||
|
||||
contentHeight = 30;
|
||||
rerender(
|
||||
<AutoGrowTextArea
|
||||
aria-label="自动增长输入框"
|
||||
value="短内容"
|
||||
style={{
|
||||
boxSizing: 'border-box',
|
||||
minHeight: '80px',
|
||||
maxHeight: '128px',
|
||||
}}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
const content = container.querySelector<HTMLElement>(
|
||||
'[data-overlayscrollbars-content]',
|
||||
);
|
||||
expect(input.style.height).toBe('80px');
|
||||
expect(input.style.overflowY).toBe('hidden');
|
||||
|
||||
unmount();
|
||||
expect(disconnect).toHaveBeenCalledTimes(1);
|
||||
expect(viewport).not.toBe(content);
|
||||
expect(
|
||||
viewport.contains(
|
||||
screen.getByRole('textbox', { name: '滚动编辑器' }),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('remeasures uncontrolled input changes and forwards the consumer input handler', () => {
|
||||
const onInput = vi.fn();
|
||||
vi.stubGlobal('CSS', { supports: vi.fn().mockReturnValue(false) });
|
||||
vi.stubGlobal(
|
||||
'ResizeObserver',
|
||||
vi.fn().mockImplementation(() => ({
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
})),
|
||||
);
|
||||
|
||||
render(
|
||||
<AutoGrowTextArea
|
||||
aria-label="非受控自动增长输入框"
|
||||
defaultValue="初始内容"
|
||||
style={{
|
||||
boxSizing: 'border-box',
|
||||
minHeight: '40px',
|
||||
maxHeight: '128px',
|
||||
}}
|
||||
onInput={onInput}
|
||||
/>,
|
||||
);
|
||||
const input = screen.getByLabelText(
|
||||
'非受控自动增长输入框',
|
||||
) as HTMLTextAreaElement;
|
||||
let contentHeight = 90;
|
||||
Object.defineProperty(input, 'offsetHeight', {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
const styleHeight = Number.parseFloat(input.style.height);
|
||||
return Number.isNaN(styleHeight) ? 42 : styleHeight;
|
||||
},
|
||||
});
|
||||
Object.defineProperty(input, 'clientHeight', {
|
||||
configurable: true,
|
||||
get: () => input.offsetHeight - 2,
|
||||
});
|
||||
Object.defineProperty(input, 'scrollHeight', {
|
||||
configurable: true,
|
||||
get: () => contentHeight,
|
||||
});
|
||||
|
||||
fireEvent.input(input, {
|
||||
target: { value: '第一行\n第二行\n第三行\n第四行' },
|
||||
});
|
||||
expect(input.style.height).toBe('92px');
|
||||
expect(input.style.overflowY).toBe('hidden');
|
||||
|
||||
contentHeight = 180;
|
||||
fireEvent.input(input, {
|
||||
target: { value: '很多行内容'.repeat(30) },
|
||||
});
|
||||
expect(input.style.height).toBe('128px');
|
||||
expect(input.style.overflowY).toBe('auto');
|
||||
|
||||
contentHeight = 20;
|
||||
fireEvent.input(input, { target: { value: '短内容' } });
|
||||
expect(input.style.height).toBe('40px');
|
||||
expect(input.style.overflowY).toBe('hidden');
|
||||
expect(onInput).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('measures content-box height without double-counting padding or borders', () => {
|
||||
vi.stubGlobal('CSS', { supports: vi.fn().mockReturnValue(false) });
|
||||
vi.stubGlobal(
|
||||
'ResizeObserver',
|
||||
vi.fn().mockImplementation(() => ({
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
})),
|
||||
);
|
||||
|
||||
const { rerender } = render(
|
||||
<AutoGrowTextArea
|
||||
aria-label="内容盒输入框"
|
||||
value=""
|
||||
style={{
|
||||
boxSizing: 'content-box',
|
||||
minHeight: '40px',
|
||||
maxHeight: '128px',
|
||||
paddingTop: '10px',
|
||||
paddingBottom: '6px',
|
||||
}}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
const input = screen.getByLabelText(
|
||||
'内容盒输入框',
|
||||
) as HTMLTextAreaElement;
|
||||
let contentHeight = 120;
|
||||
Object.defineProperty(input, 'offsetHeight', {
|
||||
configurable: true,
|
||||
get: () => 122,
|
||||
});
|
||||
Object.defineProperty(input, 'clientHeight', {
|
||||
configurable: true,
|
||||
get: () => 120,
|
||||
});
|
||||
Object.defineProperty(input, 'scrollHeight', {
|
||||
configurable: true,
|
||||
get: () => contentHeight,
|
||||
});
|
||||
|
||||
rerender(
|
||||
<AutoGrowTextArea
|
||||
aria-label="内容盒输入框"
|
||||
value="多行内容"
|
||||
style={{
|
||||
boxSizing: 'content-box',
|
||||
minHeight: '40px',
|
||||
maxHeight: '128px',
|
||||
paddingTop: '10px',
|
||||
paddingBottom: '6px',
|
||||
}}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(input.style.height).toBe('104px');
|
||||
expect(input.style.overflowY).toBe('hidden');
|
||||
|
||||
contentHeight = 160;
|
||||
rerender(
|
||||
<AutoGrowTextArea
|
||||
aria-label="内容盒输入框"
|
||||
value="更多多行内容"
|
||||
style={{
|
||||
boxSizing: 'content-box',
|
||||
minHeight: '40px',
|
||||
maxHeight: '128px',
|
||||
paddingTop: '10px',
|
||||
paddingBottom: '6px',
|
||||
}}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(input.style.height).toBe('128px');
|
||||
expect(input.style.overflowY).toBe('auto');
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+132
-39
@@ -6783,13 +6783,13 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
}
|
||||
|
||||
.image-canvas-editor__generation-prompt {
|
||||
--auto-grow-editor-radius: 1.05rem;
|
||||
--auto-grow-editor-min-height: 3.35rem;
|
||||
--auto-grow-editor-padding: 0.52rem 0.82rem;
|
||||
grid-column: 1 / -1;
|
||||
min-height: 3.35rem;
|
||||
resize: none;
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
border-radius: 1.05rem;
|
||||
border-radius: var(--auto-grow-editor-radius);
|
||||
background: #f8fafc;
|
||||
padding: 0.52rem 0.82rem;
|
||||
color: #0f172a;
|
||||
font: inherit;
|
||||
font-size: 0.82rem;
|
||||
@@ -6798,17 +6798,13 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.image-canvas-editor__generation-prompt:focus,
|
||||
.image-canvas-editor__generation-prompt:focus-within,
|
||||
.image-canvas-editor__video-prompt:focus {
|
||||
border-color: var(--image-canvas-brand-border-strong);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 3px var(--image-canvas-brand-focus-ring);
|
||||
}
|
||||
|
||||
.image-canvas-editor__generation-prompt::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.image-canvas-editor__video-prompt {
|
||||
grid-column: 1 / -1;
|
||||
min-height: 4.1rem;
|
||||
@@ -7604,8 +7600,8 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
|
||||
.image-canvas-editor__character-composer
|
||||
.image-canvas-editor__generation-prompt {
|
||||
min-height: 5.1rem;
|
||||
padding-top: 0.42rem;
|
||||
--auto-grow-editor-min-height: 5.1rem;
|
||||
--auto-grow-editor-padding: 0.42rem 0.82rem 0.52rem;
|
||||
}
|
||||
|
||||
.image-canvas-editor__character-composer
|
||||
@@ -7653,7 +7649,7 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
}
|
||||
|
||||
.image-canvas-editor__publication-description {
|
||||
min-height: 4rem;
|
||||
--auto-grow-editor-min-height: 4rem;
|
||||
}
|
||||
|
||||
.image-canvas-editor__publication-composer
|
||||
@@ -7690,8 +7686,8 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
}
|
||||
|
||||
.image-canvas-editor__icon-composer .image-canvas-editor__generation-prompt {
|
||||
min-height: 5.1rem;
|
||||
padding-top: 0.42rem;
|
||||
--auto-grow-editor-min-height: 5.1rem;
|
||||
--auto-grow-editor-padding: 0.42rem 0.82rem 0.52rem;
|
||||
}
|
||||
|
||||
.image-canvas-editor__character-composer
|
||||
@@ -7964,11 +7960,7 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
}
|
||||
|
||||
.image-canvas-editor__spec-textarea {
|
||||
min-height: 3.8rem;
|
||||
}
|
||||
|
||||
.image-canvas-editor__spec-textarea::placeholder {
|
||||
color: #94a3b8;
|
||||
--auto-grow-editor-min-height: 3.8rem;
|
||||
}
|
||||
|
||||
.image-canvas-editor__spec-textarea--view {
|
||||
@@ -8549,7 +8541,7 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
}
|
||||
|
||||
.image-canvas-editor__quick-edit-prompt {
|
||||
min-height: 5.6rem;
|
||||
--auto-grow-editor-min-height: 5.6rem;
|
||||
}
|
||||
|
||||
.image-canvas-editor__quick-edit-controls {
|
||||
@@ -8699,7 +8691,8 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
|
||||
.image-canvas-editor__character-animation-textarea {
|
||||
grid-column: 1 / -1;
|
||||
min-height: 8rem;
|
||||
--auto-grow-editor-min-height: 8rem;
|
||||
--auto-grow-editor-max-height: 16rem;
|
||||
}
|
||||
|
||||
.image-canvas-editor__character-animation-presets {
|
||||
@@ -8791,12 +8784,12 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
}
|
||||
|
||||
.image-canvas-editor__generate-prompt {
|
||||
min-height: 7rem;
|
||||
resize: none;
|
||||
--auto-grow-editor-radius: 0.45rem;
|
||||
--auto-grow-editor-min-height: 7rem;
|
||||
--auto-grow-editor-padding: 0.7rem;
|
||||
border: 1px solid #d9dee8;
|
||||
border-radius: 0.45rem;
|
||||
border-radius: var(--auto-grow-editor-radius);
|
||||
background: #f8fafc;
|
||||
padding: 0.7rem;
|
||||
color: #1f2937;
|
||||
font: inherit;
|
||||
font-size: 0.82rem;
|
||||
@@ -8804,7 +8797,7 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.image-canvas-editor__generate-prompt:focus {
|
||||
.image-canvas-editor__generate-prompt:focus-within {
|
||||
border-color: var(--image-canvas-brand-border-strong);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 3px var(--image-canvas-brand-focus-ring);
|
||||
@@ -16796,28 +16789,128 @@ button {
|
||||
}
|
||||
|
||||
.auto-grow-text-area {
|
||||
scrollbar-color: #cbd5e1 transparent;
|
||||
scrollbar-width: thin;
|
||||
position: relative;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: var(--auto-grow-editor-min-height, 2.5rem);
|
||||
max-height: var(--auto-grow-editor-max-height, 8rem);
|
||||
border-radius: var(--auto-grow-editor-radius, 1rem);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auto-grow-text-area::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
.auto-grow-text-area__content {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: calc(var(--auto-grow-editor-min-height, 2.5rem) - 2px);
|
||||
padding: var(--auto-grow-editor-padding, 0.5rem 0.75rem);
|
||||
padding-right: 2.25rem;
|
||||
outline: none;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.auto-grow-text-area::-webkit-scrollbar-track {
|
||||
margin-block: 10px;
|
||||
background-color: transparent;
|
||||
.auto-grow-text-area__placeholder {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
box-sizing: border-box;
|
||||
padding: var(--auto-grow-editor-padding, 0.5rem 0.75rem);
|
||||
padding-right: 2.25rem;
|
||||
overflow: hidden;
|
||||
color: #94a3b8;
|
||||
pointer-events: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.auto-grow-text-area::-webkit-scrollbar-thumb {
|
||||
border: 1px solid transparent;
|
||||
.auto-grow-text-area__viewport,
|
||||
.auto-grow-text-area [data-overlayscrollbars-viewport] {
|
||||
/* 独立 viewport 不随内容增高,达到上限后只由它承接滚动。 */
|
||||
display: block;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.auto-grow-text-area__content-root {
|
||||
box-sizing: border-box;
|
||||
min-height: calc(var(--auto-grow-editor-min-height, 2.5rem) - 2px);
|
||||
}
|
||||
|
||||
.auto-grow-text-area[data-disabled='true'] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.auto-grow-text-area[data-readonly='true'] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.editor-agent-conversation__draft-input {
|
||||
--auto-grow-editor-radius: 1.5rem;
|
||||
--auto-grow-editor-padding: 0.5rem 0.75rem;
|
||||
border-radius: var(--auto-grow-editor-radius);
|
||||
}
|
||||
|
||||
/* 微信式编辑器滚动条:轨道透明,8px 胶囊滑块内缩于输入框圆角之内。 */
|
||||
.auto-grow-text-area
|
||||
> .os-theme-auto-grow-text-area.os-scrollbar-vertical.os-scrollbar-cornerless {
|
||||
--os-size: 8px;
|
||||
--os-padding-axis: 0;
|
||||
--os-padding-perpendicular: 0;
|
||||
--os-track-border-radius: 999px;
|
||||
--os-track-bg: transparent;
|
||||
--os-track-bg-hover: transparent;
|
||||
--os-track-bg-active: transparent;
|
||||
--os-handle-border-radius: 999px;
|
||||
--os-handle-bg: rgba(98, 98, 98, 0.56);
|
||||
--os-handle-bg-hover: rgba(98, 98, 98, 0.72);
|
||||
--os-handle-bg-active: rgba(98, 98, 98, 0.86);
|
||||
--os-handle-min-size: 24px;
|
||||
--os-handle-perpendicular-size: 100%;
|
||||
--os-handle-perpendicular-size-hover: 100%;
|
||||
--os-handle-perpendicular-size-active: 100%;
|
||||
box-sizing: border-box;
|
||||
top: var(--auto-grow-editor-radius, 1rem) !important;
|
||||
right: 5px;
|
||||
bottom: var(--auto-grow-editor-radius, 1rem) !important;
|
||||
width: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.auto-grow-text-area
|
||||
> .os-theme-auto-grow-text-area.os-scrollbar-vertical
|
||||
.os-scrollbar-track {
|
||||
width: 8px;
|
||||
border-radius: 999px;
|
||||
background-color: #cbd5e1;
|
||||
background-clip: padding-box;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.auto-grow-text-area::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #94a3b8;
|
||||
.auto-grow-text-area
|
||||
> .os-theme-auto-grow-text-area.os-scrollbar-vertical
|
||||
.os-scrollbar-handle {
|
||||
width: 8px;
|
||||
min-height: 24px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: rgba(98, 98, 98, 0.56);
|
||||
}
|
||||
|
||||
.auto-grow-text-area
|
||||
> .os-theme-auto-grow-text-area.os-scrollbar-vertical:hover
|
||||
.os-scrollbar-handle {
|
||||
background: rgba(98, 98, 98, 0.72);
|
||||
}
|
||||
|
||||
.auto-grow-text-area
|
||||
> .os-theme-auto-grow-text-area.os-scrollbar-vertical:active
|
||||
.os-scrollbar-handle {
|
||||
background: rgba(98, 98, 98, 0.86);
|
||||
}
|
||||
|
||||
.image-canvas-editor__metadata-dialog.platform-modal-shell {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
|
||||
import 'overlayscrollbars/styles/overlayscrollbars.css';
|
||||
import './index.css';
|
||||
|
||||
import {StrictMode, Suspense} from 'react';
|
||||
|
||||
Reference in New Issue
Block a user