From 99d239c8bfc3a54632ba66e835373f05fbdbbada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 4 Aug 2026 17:07:30 +0800 Subject: [PATCH] =?UTF-8?q?style:=20=20=E7=94=BB=E5=B8=83=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=E5=A4=9A=E8=A1=8C=E8=BE=93=E5=85=A5=E6=A1=86?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E8=B0=83=E6=95=B4=20(#130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复用了画布agent的输入框: * 高度自适应 * 滑动条样式变为浅色和位置不再出界 before ![shotmd-1785745933-compressed.webp](/attachments/95727f6d-63e1-48f7-b806-8203cd73d1eb) after ![shotmd-1785745957-compressed.webp](/attachments/b5af077d-d3b9-4572-9135-39e288291e24) Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/130 Co-authored-by: 王德宇 Co-committed-by: 王德宇 --- ...架构】图片画布编辑器MVP接入方案-2026-06-11.md | 2 +- .../【编辑器】画布Agent对话面板-2026-07-03.md | 2 +- .../common/AutoGrowTextArea.test.tsx | 288 ++++++++++++++++++ src/components/common/AutoGrowTextArea.tsx | 130 ++++++++ .../EditorAgentDraftTextarea.test.tsx | 106 ++----- .../EditorAgentDraftTextarea.tsx | 80 +---- ...CanvasBasicGenerationComposerView.test.tsx | 12 +- ...ImageCanvasBasicGenerationComposerView.tsx | 7 +- ...CanvasCharacterAnimationPanelView.test.tsx | 15 +- ...ImageCanvasCharacterAnimationPanelView.tsx | 9 +- ...asCharacterGenerationComposerView.test.tsx | 2 + ...eCanvasCharacterGenerationComposerView.tsx | 7 +- ...CanvasEditorGenerationIntegration.test.tsx | 3 +- ...ImageCanvasGenerationComposerView.test.tsx | 7 +- .../ImageCanvasGenerationComposerView.tsx | 12 +- ...CanvasIconSpritesheetComposerView.test.tsx | 6 +- ...ImageCanvasIconSpritesheetComposerView.tsx | 7 +- ...PublicationMaterialsDemoPanelView.test.tsx | 8 + ...anvasPublicationMaterialsDemoPanelView.tsx | 8 +- ...mageCanvasSpecGenerationPanelView.test.tsx | 7 + .../ImageCanvasSpecGenerationPanelView.tsx | 13 +- .../ImageCanvasTextAreaStyles.test.ts | 55 ++++ src/index.css | 12 +- vitest.config.ts | 1 + 24 files changed, 571 insertions(+), 228 deletions(-) create mode 100644 src/components/common/AutoGrowTextArea.test.tsx create mode 100644 src/components/common/AutoGrowTextArea.tsx create mode 100644 src/components/image-editor/ImageCanvasTextAreaStyles.test.ts diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index fa82c0075..f70dc236f 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -79,7 +79,7 @@ - 登录态上传和生成结果必须先落 OSS / asset object,再向 `editor_project_resource` / `editor_asset` 写入轻量 `imageSrc: "/"`、`objectKey` 和 `assetObjectId`;未登录演示态可以在内存里使用 Data URL 预览,但项目、素材库、项目资源和 `editor_canvas.layers_json` 不得写入 `data:image/*`、`data:video/*`、`data:audio/*` 或 `blob:`。旧数据读取时如果已有 `objectKey`,`imageSrc` 归一成 `/`;没有 `objectKey` 的旧 Data URL 需要走修复上传并回写轻量引用。裁扩在项目上下文中虽然由前端 canvas 本地渲染 PNG,也必须先上传 OSS / asset object 并创建 `editor_project_resource`,再把带正式 `resourceId/objectKey/assetObjectId` 的裁扩图层加入画布;不能先把 `local-resource-*` + Data URL 图层交给项目保存或后续去背景。上传到生成面板参考图槽位的图片必须先创建 `editor_project_resource` 行;没有当前工程 ID 时才创建账号级 `editor_asset` 行,随后把对应 `resourceId` 或 `assetId` 写入参考图临时状态;生成请求提交前必须把临时状态解析成 `objectKey`、项目资源 ID 或素材 ID,未登记的本地图片和普通图片路径先上传 OSS,不能直接提交 Data URL、Blob URL 或临时图片源。 - 资源表保存资源和素材级元数据;图层位置、层级、分组选中所需 ID 和 groupId 保存在 `editor_canvas` 的布局 JSON。布局 JSON 是混合数组:普通图层按 `layerId/resourceId` 保存,生成器占位和生成器对话框按 `itemType: "generation-dialog"` 保存,不新增单独表。普通图层的新保存不再把 `assetKind/generationInputs` 写入布局 JSON;刷新时优先从 `editor_project_resource` 恢复,旧布局中的同名字段只作为兼容兜底。生成器快照必须包含生成器 ID、模式、提示词、参数、参考图、状态、占位框位置和可选 `generatedLayerId`;角色、图标等纯色抠图生成器的前端用户路径不保存或恢复 `screenColor` / `segModel`,同源重绘也不再从 `generationInputs.fields` 恢复 `抠图背景色` 或 `抠图模型`;宣发素材生成器还必须保存并恢复 `publicationWorkflowId`、`publicationGameInfo` 和 `publicationReferences`,避免刷新后生成卡片字段或参考图丢失。生成器快照中的参考图同样只保存 `resourceId/sourceAssetId` 行引用和展示所需 label,不保存图片 Data URL、signed URL 或 `objectKey`;刷新时用 `editor_project_resource` / `editor_asset` 行恢复临时生成请求所需图片源。生成成功后仍保存该快照,只是渲染时由 `generatedLayerId` 锚定到成品图层而不重复显示灰色占位框。`generationInputs.references` 是用户可见输入快照中的行级索引,只允许保存 `{ title, label, refType, refId }`;生成接口只接收提交前临时状态解析出的 `objectKey` 或资源 ID;Data URL、Blob URL 和 signed URL 不进入请求体,不进入资源 / 素材元数据。图层展示尺寸不再作为独立 `Size` 真相保存,刷新与新建图层均按 `Resolution`(`originalWidth/originalHeight`)原分辨率显示。图层组第一版是画布内布局语义,不单独建表。 - 图片类、生成视频和音频结果除作为 `editor_project_resource` 和画布图层保存外,还要写入账号级 `editor_asset` 素材库;该写入由生成 BFF 在请求携带 `assetFolderId` 时完成。角色、图标图集、UI 提取和角色动作等多产物任务把实际产生的 provider 原始输出及后处理结果分别入库:所有条目沿用 `character`、`icon-spritesheet`、`character-animation` 等真实类型,provider 原始输出承载任务模型成本,后处理派生产物阶段成本为 0。后台素材查询以最终产物为父行、每个中间产物为可展开的独立子行,分页只计算父任务;手动重拆图集保留独立 `taskId` 用于存储隔离和日志排障,通过私有 provenance 从服务端生成账号素材的 source resource、asset object 或 Object Key 取得可信来源任务,并把它写入 `groupTaskId`,不信任客户端可提交的 resource `taskId/assetKind`;跨项目复用后仍可通过稳定媒体引用找回来源。没有可信来源的新拆分显式归到自身任务,不走历史资源链回溯。每个手动切片同时写入 `groupTaskExpectedAssetCount`,全部切片落库后写独立 cohort 完成事实;后台 read model 只让同一根任务的一个已完成拆分批次并入原图集父项,用户后来删除单片不会让批次脱组,部分失败批次和后续重复拆分批次按各自真实任务分页,避免残缺批次抢占根任务、单组无限增长或素材丢失。历史行在项目资源仍存在时兼容回溯,删除项目资源前只固化直接受影响行的真实来源字段,有界展示 ID 不反写数据库。`GENARRATIVE_EXTERNAL_GENERATION_MODE=queue` 下,画布图片、改图、图标素材、UI 素材提取、角色动作、视频、音效和背景音乐生成都先返回 `queueState`,前端轮询 `/api/runtime/external-generation/jobs/{jobId}` 到完成后重新读取项目快照;`inline` 或无项目上下文时才使用响应中的 resource / asset 快照做本地落画布兜底,不再把同一生成结果二次调用素材创建接口。生成请求失败、inline 完成或 queue 任务终态完成 / 失败后,右上角泥点 chip 必须通过 `/profile/dashboard` 回读余额,不做本地乐观扣减。生成视频会单独抽取首帧封面写入 `thumbnailSrc`,素材栏和拖回画布时沿用该封面作为 poster。 -- 生成面板不展示资源名称输入,默认使用原有自动编号;提示词输入保持统一可见边框。内部命名契约仍使用可选 `assetLabel`,最大 80 字符并在提交时 trim;历史状态或内部调用携带非空名称时,同一个名称必须贯穿 `assetLabel`、`canvasCompletion.title`、项目资源、账号素材和本地兜底图层,刷新后不得退回模板名。图标图集与角色动作请求同样兼容该字段,中间原图使用主名称加固定后缀;图标拆分素材按全连通域视觉顺序自动命名为 `素材 N`。 +- 生成面板不展示资源名称输入,默认使用原有自动编号;提示词输入保持统一可见边框。图片生成、快速编辑、重绘、修改图片、视频、音频、角色、图标、UI / 自定义规范、角色动画和宣发素材描述统一直接使用公共 `AutoGrowTextArea`,不经 `PlatformTextField` 的 textarea 分支;`AutoGrowTextArea` 负责尺寸、溢出及统一的 disabled 光标 / 透明度反馈,画布内生成面板统一由 `image-canvas-editor__generation-prompt` 承载边框、背景、padding、圆角和 focus chrome,独立修改图片弹窗继续使用 `image-canvas-editor__generate-prompt`,用途专属类只追加最小高度等差异,不得新建第三套 textarea chrome。Enter 继续输入换行而不提交表单。内部命名契约仍使用可选 `assetLabel`,最大 80 字符并在提交时 trim;历史状态或内部调用携带非空名称时,同一个名称必须贯穿 `assetLabel`、`canvasCompletion.title`、项目资源、账号素材和本地兜底图层,刷新后不得退回模板名。图标图集与角色动作请求同样兼容该字段,中间原图使用主名称加固定后缀;图标拆分素材按全连通域视觉顺序自动命名为 `素材 N`。 - 画布 Agent 会话按“SpacetimeDB 元数据 + OSS 消息正文”存储:`editor_agent_conversation` 只保存 `conversationId/projectId/ownerUserId/title/messagesObjectKey/deleted/createdAt/updatedAt` 等会话元数据;消息正文整体保存为私有 OSS JSON 文档 `editor-agent/{conversationId}.json`。消息文档单对象上限为 2 MiB,同一会话的消息追加和工具结果回填由 api-server 按 `conversationId` 串行化,避免“读 OSS → 改消息 → 写 OSS”并发覆盖。前端只通过 api-server BFF 读取和发送会话,不直接读写 SpacetimeDB,也不直接读写 OSS。 - Agent 消息附件只允许引用当前工程画布资源或账号素材库图片,来源类型为 `canvas_resource` / `library_asset`,最多 9 张。附件请求可携带展示用 `imageSrc/thumbnailSrc/objectKey/width/height/label`,但持久化真相仍以后端校验后的 resource / asset 行和 OSS 对象为准;不得把 Data URL、signed URL 或 blob URL 当作会话长期事实。 - 前端不直接订阅 SpacetimeDB,统一通过 api-server 的 `/api/editor/projects*` BFF 读写。 diff --git a/docs/【编辑器】画布Agent对话面板-2026-07-03.md b/docs/【编辑器】画布Agent对话面板-2026-07-03.md index 165f831e2..01aad43ae 100644 --- a/docs/【编辑器】画布Agent对话面板-2026-07-03.md +++ b/docs/【编辑器】画布Agent对话面板-2026-07-03.md @@ -61,7 +61,7 @@ - 对话框与左侧素材 / 图层侧栏**不互斥**,允许同时展开,便于在对话中选取和核对画布素材;左侧栏切换不改变 Agent 面板开关状态。 - 桌面端对话框固定宽约 360–400px;移动端抽屉式全宽覆盖;收起态为胶囊/圆形入口按钮。 - 底部消息输入框随输入内容从单行高度自动增长,最大高度为 - 128px;输入框及其 Enter 提交、原生自适应和兼容降级统一封装在独立 `EditorAgentDraftTextarea` 组件中。支持 `field-sizing: content` 的浏览器使用原生内容尺寸自适应,不支持该属性的旧 Safari / iOS WebView 使用前端测量降级,并在宽度变化时重新计算换行高度。内容超过最大高度后停止增长并启用内部纵向滚动,内容缩短或清空后同步收缩。内部滚动条使用浅灰窄滑块和透明轨道,上下留白不得溢出输入框圆角边界;输入框在窄屏下允许收缩且不产生横向滚动。 + 128px;原生自适应和兼容降级统一由公共 `AutoGrowTextArea` 组件承载,`EditorAgentDraftTextarea` 只保留 Agent 专属视觉、粘贴和 Enter 提交语义。支持 `field-sizing: content` 的浏览器使用原生内容尺寸自适应;不支持该属性的旧 Safari / iOS WebView 使用前端测量降级,受控 `value` 更新、非受控输入事件和宽度变化都必须重新计算换行高度,同时继续转发调用方的 `onInput`。降级测量必须按元素实际 `box-sizing` 区分 border-box 与 content-box,不能重复计入 padding 或 border。内容超过最大高度后停止增长并启用内部纵向滚动,内容缩短或清空后同步收缩。内部滚动条使用浅灰窄滑块和透明轨道,上下留白不得溢出输入框圆角边界;输入框在窄屏下允许收缩且不产生横向滚动。 - Enter 发送必须同时排除 `isComposing` 和旧 Safari / WebKit 候选词确认事件的 `keyCode === 229`,避免输入法选词时误发送。 - 用户消息必须包含去除首尾空白后的非空文本;附件只能随文本消息发送,前端发送门禁与后端 `module-editor-agent` 领域校验必须同时拒绝纯附件消息。 - 会话管理入口在对话框头部:当前会话标题 + 历史会话下拉(按更新时间倒序)+ 新建对话按钮,全部包在对话框内。 diff --git a/src/components/common/AutoGrowTextArea.test.tsx b/src/components/common/AutoGrowTextArea.test.tsx new file mode 100644 index 000000000..04caaa941 --- /dev/null +++ b/src/components/common/AutoGrowTextArea.test.tsx @@ -0,0 +1,288 @@ +/* @vitest-environment jsdom */ + +import { act, fireEvent, render, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { AutoGrowTextArea } from './AutoGrowTextArea'; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +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); + + render( + , + ); + + 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); + }); + + 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, + }; + }), + ); + + const { rerender, unmount } = render( + , + ); + 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( + , + ); + 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, + ); + }); + expect(input.style.height).toBe('128px'); + expect(input.style.overflowY).toBe('auto'); + + contentHeight = 30; + rerender( + , + ); + expect(input.style.height).toBe('80px'); + expect(input.style.overflowY).toBe('hidden'); + + unmount(); + expect(disconnect).toHaveBeenCalledTimes(1); + }); + + 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( + , + ); + 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( + , + ); + 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( + , + ); + expect(input.style.height).toBe('104px'); + expect(input.style.overflowY).toBe('hidden'); + + contentHeight = 160; + rerender( + , + ); + expect(input.style.height).toBe('128px'); + expect(input.style.overflowY).toBe('auto'); + }); +}); diff --git a/src/components/common/AutoGrowTextArea.tsx b/src/components/common/AutoGrowTextArea.tsx new file mode 100644 index 000000000..282c7c549 --- /dev/null +++ b/src/components/common/AutoGrowTextArea.tsx @@ -0,0 +1,130 @@ +import { + type InputEventHandler, + type TextareaHTMLAttributes, + useLayoutEffect, + useRef, +} from 'react'; + +const DEFAULT_MIN_HEIGHT_PX = 40; +const DEFAULT_MAX_HEIGHT_PX = 128; + +function supportsNativeFieldSizing() { + return ( + typeof CSS !== 'undefined' && + typeof CSS.supports === 'function' && + CSS.supports('field-sizing', 'content') + ); +} + +function parseComputedPixelValue(value: string, fallback: number) { + const parsedValue = Number.parseFloat(value); + return Number.isFinite(parsedValue) ? parsedValue : fallback; +} + +function resizeFallbackTextArea(textarea: HTMLTextAreaElement) { + textarea.style.height = 'auto'; + + const computedStyle = window.getComputedStyle(textarea); + const minHeight = parseComputedPixelValue( + computedStyle.minHeight, + DEFAULT_MIN_HEIGHT_PX, + ); + const maxHeight = Math.max( + minHeight, + parseComputedPixelValue(computedStyle.maxHeight, DEFAULT_MAX_HEIGHT_PX), + ); + const contentHeight = textarea.scrollHeight; + const borderHeight = Math.max( + 0, + textarea.offsetHeight - textarea.clientHeight, + ); + const isBorderBox = computedStyle.boxSizing === 'border-box'; + const paddingHeight = + parseComputedPixelValue(computedStyle.paddingTop, 0) + + parseComputedPixelValue(computedStyle.paddingBottom, 0); + const measuredHeight = isBorderBox + ? contentHeight + borderHeight + : Math.max(0, contentHeight - paddingHeight); + const nextHeight = Math.min( + Math.max(measuredHeight, minHeight), + maxHeight, + ); + + textarea.style.height = `${nextHeight}px`; + textarea.style.overflowY = measuredHeight > maxHeight ? 'auto' : 'hidden'; +} + +export function AutoGrowTextArea({ + className, + onInput, + rows = 1, + value, + ...textareaProps +}: TextareaHTMLAttributes) { + const textareaRef = useRef(null); + + useLayoutEffect(() => { + const textarea = textareaRef.current; + if (!textarea || supportsNativeFieldSizing()) { + return; + } + resizeFallbackTextArea(textarea); + }, [value]); + + useLayoutEffect(() => { + const textarea = textareaRef.current; + if (!textarea || supportsNativeFieldSizing()) { + return undefined; + } + + let previousWidth = textarea.getBoundingClientRect().width; + const resizeWhenWidthChanges = (nextWidth: number) => { + if (Math.abs(nextWidth - previousWidth) < 0.5) { + return; + } + previousWidth = nextWidth; + resizeFallbackTextArea(textarea); + }; + + if (typeof ResizeObserver !== 'undefined') { + const observer = new ResizeObserver((entries) => { + const entry = entries[0]; + if (entry) { + resizeWhenWidthChanges(entry.contentRect.width); + } + }); + observer.observe(textarea); + return () => observer.disconnect(); + } + + const handleWindowResize = () => { + resizeWhenWidthChanges(textarea.getBoundingClientRect().width); + }; + window.addEventListener('resize', handleWindowResize); + return () => window.removeEventListener('resize', handleWindowResize); + }, []); + + const resolvedClassName = [ + 'auto-grow-text-area max-h-32 min-h-10 resize-none overflow-x-hidden overflow-y-auto overscroll-contain [field-sizing:content] disabled:cursor-not-allowed disabled:opacity-60', + className, + ] + .filter(Boolean) + .join(' '); + const handleInput: InputEventHandler = (event) => { + if (!supportsNativeFieldSizing()) { + resizeFallbackTextArea(event.currentTarget); + } + onInput?.(event); + }; + + return ( +