mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
feat(studio): add flat trigger variants for ColorField, FontFamilyField, TextAreaField
This commit is contained in:
@@ -331,6 +331,12 @@
|
|||||||
"file": "packages/studio/src/components/editor/manualEditingAvailability.ts",
|
"file": "packages/studio/src/components/editor/manualEditingAvailability.ts",
|
||||||
"exports": ["STUDIO_FLAT_INSPECTOR_ENABLED"],
|
"exports": ["STUDIO_FLAT_INSPECTOR_ENABLED"],
|
||||||
},
|
},
|
||||||
|
// TextAreaField: newly exported for FlatTextSection (flat inspector
|
||||||
|
// redesign, Task 8), which lands in a later commit on this branch.
|
||||||
|
{
|
||||||
|
"file": "packages/studio/src/components/editor/propertyPanelSections.tsx",
|
||||||
|
"exports": ["TextAreaField"],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
"ignoreDependencies": [
|
"ignoreDependencies": [
|
||||||
// Runtime/dynamic deps not visible to static analysis: tsup `external`,
|
// Runtime/dynamic deps not visible to static analysis: tsup `external`,
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// @vitest-environment happy-dom
|
||||||
|
|
||||||
|
import React, { act } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { ColorField } from "./propertyPanelColor";
|
||||||
|
|
||||||
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ColorField flat trigger", () => {
|
||||||
|
it("renders label and value inline with a small swatch, no boxed border", () => {
|
||||||
|
const host = document.createElement("div");
|
||||||
|
document.body.append(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
act(() => {
|
||||||
|
root.render(<ColorField flat label="Color" value="rgb(255, 176, 32)" onCommit={vi.fn()} />);
|
||||||
|
});
|
||||||
|
const trigger = host.querySelector<HTMLButtonElement>('[data-flat-color-trigger="true"]');
|
||||||
|
expect(trigger).not.toBeNull();
|
||||||
|
expect(trigger?.className).not.toContain("border-neutral-800");
|
||||||
|
expect(host.textContent).toContain("Color");
|
||||||
|
act(() => root.unmount());
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -121,11 +121,13 @@ export function ColorField({
|
|||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
disabled,
|
disabled,
|
||||||
|
flat,
|
||||||
onCommit,
|
onCommit,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
flat?: boolean;
|
||||||
onCommit: (nextValue: string) => void;
|
onCommit: (nextValue: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||||
@@ -349,6 +351,30 @@ export function ColorField({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (flat) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[30px] items-center justify-between">
|
||||||
|
<span className="text-[11px] text-panel-text-2">{label}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-flat-color-trigger="true"
|
||||||
|
disabled={disabled}
|
||||||
|
aria-label={`Pick ${label.toLowerCase()} color`}
|
||||||
|
ref={buttonRef}
|
||||||
|
onClick={openPicker}
|
||||||
|
className="flex items-center gap-2 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="h-4 w-4 flex-shrink-0 rounded-[4px]"
|
||||||
|
style={{ backgroundColor: value || "transparent" }}
|
||||||
|
/>
|
||||||
|
<span className="font-mono text-[11px] text-panel-text-0">{value}</span>
|
||||||
|
</button>
|
||||||
|
{picker}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid min-w-0 gap-1.5">
|
<div className="grid min-w-0 gap-1.5">
|
||||||
<span className={LABEL}>{label}</span>
|
<span className={LABEL}>{label}</span>
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// @vitest-environment happy-dom
|
||||||
|
|
||||||
|
import React, { act } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { FontFamilyField } from "./propertyPanelFont";
|
||||||
|
|
||||||
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
document.body.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("FontFamilyField flat trigger", () => {
|
||||||
|
it("renders as a label/value row with a trailing dropdown caret, no boxed border", () => {
|
||||||
|
const host = document.createElement("div");
|
||||||
|
document.body.append(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
act(() => {
|
||||||
|
root.render(
|
||||||
|
<FontFamilyField flat value="JetBrains Mono" importedFonts={[]} onCommit={vi.fn()} />,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const trigger = host.querySelector<HTMLButtonElement>('[data-flat-font-trigger="true"]');
|
||||||
|
expect(trigger).not.toBeNull();
|
||||||
|
expect(trigger?.className).not.toContain("border-neutral-800");
|
||||||
|
expect(host.textContent).toContain("JetBrains Mono");
|
||||||
|
act(() => root.unmount());
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -123,12 +123,14 @@ function loadImportedFontStylesheet(asset: ImportedFontAsset): void {
|
|||||||
export function FontFamilyField({
|
export function FontFamilyField({
|
||||||
value,
|
value,
|
||||||
disabled,
|
disabled,
|
||||||
|
flat,
|
||||||
importedFonts,
|
importedFonts,
|
||||||
onImportFonts,
|
onImportFonts,
|
||||||
onCommit,
|
onCommit,
|
||||||
}: {
|
}: {
|
||||||
value: string;
|
value: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
flat?: boolean;
|
||||||
importedFonts: ImportedFontAsset[];
|
importedFonts: ImportedFontAsset[];
|
||||||
onImportFonts?: (files: FileList | File[]) => Promise<ImportedFontAsset[]>;
|
onImportFonts?: (files: FileList | File[]) => Promise<ImportedFontAsset[]>;
|
||||||
onCommit: (nextValue: string) => void;
|
onCommit: (nextValue: string) => void;
|
||||||
@@ -366,6 +368,130 @@ export function FontFamilyField({
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dropdown = open && (
|
||||||
|
<div className="absolute left-0 right-0 top-[calc(100%+6px)] z-50 overflow-hidden rounded-xl border border-neutral-700 bg-neutral-950 shadow-2xl">
|
||||||
|
<div className="grid grid-cols-[minmax(0,1fr)_auto_auto] gap-2 border-b border-neutral-800 p-2">
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
value={query}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder={loadingGoogleFonts ? "Loading Google Fonts..." : "Search fonts"}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
if (e.key === "Enter" && filteredOptions[0]) {
|
||||||
|
e.preventDefault();
|
||||||
|
commitFamily(filteredOptions[0]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="min-w-0 rounded-lg border border-neutral-800 bg-neutral-900 px-2.5 py-2 text-[11px] font-medium text-neutral-100 outline-none placeholder:text-neutral-600 focus:border-neutral-600"
|
||||||
|
/>
|
||||||
|
{canQueryLocalFonts && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={disabled || loadingLocalFonts}
|
||||||
|
onClick={loadBrowserLocalFonts}
|
||||||
|
className="rounded-lg border border-neutral-700 bg-neutral-900 px-2.5 text-[10px] font-medium text-neutral-400 transition-colors hover:border-neutral-600 hover:text-neutral-100 disabled:cursor-not-allowed disabled:text-neutral-700"
|
||||||
|
>
|
||||||
|
{loadingLocalFonts ? "..." : "Local"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={disabled || importingFonts || !onImportFonts}
|
||||||
|
onClick={() => fontInputRef.current?.click()}
|
||||||
|
className="rounded-lg border border-neutral-700 bg-neutral-900 px-2.5 text-[10px] font-medium text-neutral-400 transition-colors hover:border-neutral-600 hover:text-neutral-100 disabled:cursor-not-allowed disabled:text-neutral-700"
|
||||||
|
>
|
||||||
|
{importingFonts ? "..." : "Import"}
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref={fontInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".ttf,.otf,.ttc,.woff,.woff2,.eot,font/*"
|
||||||
|
multiple
|
||||||
|
aria-label="Import local font files"
|
||||||
|
disabled={disabled || importingFonts || !onImportFonts}
|
||||||
|
className="hidden"
|
||||||
|
onChange={async (event) => {
|
||||||
|
await handleImportFonts(event.target.files);
|
||||||
|
event.target.value = "";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{fontNotice && (
|
||||||
|
<div className="border-b border-neutral-800 px-3 py-2 text-[10px] leading-4 text-neutral-500">
|
||||||
|
{fontNotice}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="max-h-64 overflow-y-auto p-1">
|
||||||
|
{filteredOptions.length === 0 ? (
|
||||||
|
<div className="px-2 py-3 text-[11px] text-neutral-500">No fonts found.</div>
|
||||||
|
) : (
|
||||||
|
filteredOptions.map((option) => (
|
||||||
|
<button
|
||||||
|
key={`${option.source}-${option.family}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => commitFamily(option)}
|
||||||
|
className={`flex w-full min-w-0 items-center justify-between gap-3 rounded-lg px-2 py-2 text-left text-[11px] transition-colors ${
|
||||||
|
option.family === currentFamily
|
||||||
|
? "bg-studio-accent/15 text-neutral-50"
|
||||||
|
: "text-neutral-300 hover:bg-neutral-900 hover:text-neutral-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="flex min-w-0 items-center gap-1.5">
|
||||||
|
<span className="truncate font-medium">{option.family}</span>
|
||||||
|
{renderAliasFor(option.family) && (
|
||||||
|
<span className="flex-shrink-0 text-[9px] text-neutral-500">
|
||||||
|
→ {renderAliasFor(option.family)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="flex-shrink-0 text-[9px] uppercase tracking-[0.14em] text-neutral-600">
|
||||||
|
{option.source}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (flat) {
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} className="relative flex min-h-[30px] items-center justify-between">
|
||||||
|
<span className="text-[11px] text-panel-text-2">Font</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-flat-font-trigger="true"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => setOpen((next) => !next)}
|
||||||
|
className="flex items-center gap-1.5 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="max-w-[200px] truncate font-mono text-[11px] text-panel-text-0"
|
||||||
|
style={{ fontFamily: value }}
|
||||||
|
>
|
||||||
|
{currentFamily}
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
width="10"
|
||||||
|
height="10"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
fill="currentColor"
|
||||||
|
className="flex-shrink-0 text-panel-text-5"
|
||||||
|
>
|
||||||
|
<path d="M2 3l3 4 3-4z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{dropdown}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="relative grid min-w-0 gap-1.5">
|
<div ref={containerRef} className="relative grid min-w-0 gap-1.5">
|
||||||
<span className={LABEL}>Font family</span>
|
<span className={LABEL}>Font family</span>
|
||||||
@@ -385,98 +511,7 @@ export function FontFamilyField({
|
|||||||
Font
|
Font
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
{dropdown}
|
||||||
{open && (
|
|
||||||
<div className="absolute left-0 right-0 top-[calc(100%+6px)] z-50 overflow-hidden rounded-xl border border-neutral-700 bg-neutral-950 shadow-2xl">
|
|
||||||
<div className="grid grid-cols-[minmax(0,1fr)_auto_auto] gap-2 border-b border-neutral-800 p-2">
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
type="text"
|
|
||||||
value={query}
|
|
||||||
disabled={disabled}
|
|
||||||
placeholder={loadingGoogleFonts ? "Loading Google Fonts..." : "Search fonts"}
|
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
e.preventDefault();
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
if (e.key === "Enter" && filteredOptions[0]) {
|
|
||||||
e.preventDefault();
|
|
||||||
commitFamily(filteredOptions[0]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="min-w-0 rounded-lg border border-neutral-800 bg-neutral-900 px-2.5 py-2 text-[11px] font-medium text-neutral-100 outline-none placeholder:text-neutral-600 focus:border-neutral-600"
|
|
||||||
/>
|
|
||||||
{canQueryLocalFonts && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={disabled || loadingLocalFonts}
|
|
||||||
onClick={loadBrowserLocalFonts}
|
|
||||||
className="rounded-lg border border-neutral-700 bg-neutral-900 px-2.5 text-[10px] font-medium text-neutral-400 transition-colors hover:border-neutral-600 hover:text-neutral-100 disabled:cursor-not-allowed disabled:text-neutral-700"
|
|
||||||
>
|
|
||||||
{loadingLocalFonts ? "..." : "Local"}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={disabled || importingFonts || !onImportFonts}
|
|
||||||
onClick={() => fontInputRef.current?.click()}
|
|
||||||
className="rounded-lg border border-neutral-700 bg-neutral-900 px-2.5 text-[10px] font-medium text-neutral-400 transition-colors hover:border-neutral-600 hover:text-neutral-100 disabled:cursor-not-allowed disabled:text-neutral-700"
|
|
||||||
>
|
|
||||||
{importingFonts ? "..." : "Import"}
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
ref={fontInputRef}
|
|
||||||
type="file"
|
|
||||||
accept=".ttf,.otf,.ttc,.woff,.woff2,.eot,font/*"
|
|
||||||
multiple
|
|
||||||
aria-label="Import local font files"
|
|
||||||
disabled={disabled || importingFonts || !onImportFonts}
|
|
||||||
className="hidden"
|
|
||||||
onChange={async (event) => {
|
|
||||||
await handleImportFonts(event.target.files);
|
|
||||||
event.target.value = "";
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{fontNotice && (
|
|
||||||
<div className="border-b border-neutral-800 px-3 py-2 text-[10px] leading-4 text-neutral-500">
|
|
||||||
{fontNotice}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="max-h-64 overflow-y-auto p-1">
|
|
||||||
{filteredOptions.length === 0 ? (
|
|
||||||
<div className="px-2 py-3 text-[11px] text-neutral-500">No fonts found.</div>
|
|
||||||
) : (
|
|
||||||
filteredOptions.map((option) => (
|
|
||||||
<button
|
|
||||||
key={`${option.source}-${option.family}`}
|
|
||||||
type="button"
|
|
||||||
onClick={() => commitFamily(option)}
|
|
||||||
className={`flex w-full min-w-0 items-center justify-between gap-3 rounded-lg px-2 py-2 text-left text-[11px] transition-colors ${
|
|
||||||
option.family === currentFamily
|
|
||||||
? "bg-studio-accent/15 text-neutral-50"
|
|
||||||
: "text-neutral-300 hover:bg-neutral-900 hover:text-neutral-100"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="flex min-w-0 items-center gap-1.5">
|
|
||||||
<span className="truncate font-medium">{option.family}</span>
|
|
||||||
{renderAliasFor(option.family) && (
|
|
||||||
<span className="flex-shrink-0 text-[9px] text-neutral-500">
|
|
||||||
→ {renderAliasFor(option.family)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span className="flex-shrink-0 text-[9px] uppercase tracking-[0.14em] text-neutral-600">
|
|
||||||
{option.source}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState, type ChangeEvent } from "react";
|
||||||
import { Plus, Type } from "../../icons/SystemIcons";
|
import { Plus, Type } from "../../icons/SystemIcons";
|
||||||
import { isTextEditableSelection, type DomEditSelection } from "./domEditing";
|
import { isTextEditableSelection, type DomEditSelection } from "./domEditing";
|
||||||
import type { ImportedFontAsset } from "./fontAssets";
|
import type { ImportedFontAsset } from "./fontAssets";
|
||||||
@@ -59,17 +59,19 @@ function detectAvailableWeights(fontFamily: string): string[] {
|
|||||||
return available.length > 0 ? available : ALL_WEIGHTS;
|
return available.length > 0 ? available : ALL_WEIGHTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TextAreaField({
|
export function TextAreaField({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
disabled,
|
disabled,
|
||||||
autoFocus,
|
autoFocus,
|
||||||
|
flat,
|
||||||
onCommit,
|
onCommit,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
|
flat?: boolean;
|
||||||
onCommit: (nextValue: string) => void;
|
onCommit: (nextValue: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [draft, setDraft] = useState(value);
|
const [draft, setDraft] = useState(value);
|
||||||
@@ -105,6 +107,38 @@ function TextAreaField({
|
|||||||
}, 120);
|
}, 120);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
focusedRef.current = true;
|
||||||
|
};
|
||||||
|
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setDraft(e.target.value);
|
||||||
|
scheduleCommit(e.target.value);
|
||||||
|
};
|
||||||
|
const handleBlur = () => {
|
||||||
|
focusedRef.current = false;
|
||||||
|
commitDraft(draft);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (flat) {
|
||||||
|
return (
|
||||||
|
<div className="border-l-2 border-panel-border-input py-0.5 pl-[10px]">
|
||||||
|
<div className="mb-[3px] text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
value={draft}
|
||||||
|
disabled={disabled}
|
||||||
|
rows={2}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
className="w-full resize-none bg-transparent font-mono text-[11px] leading-normal text-panel-text-0 outline-none disabled:cursor-not-allowed disabled:text-panel-text-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className="grid min-w-0 gap-1.5">
|
<label className="grid min-w-0 gap-1.5">
|
||||||
<span className={LABEL}>{label}</span>
|
<span className={LABEL}>{label}</span>
|
||||||
@@ -114,17 +148,9 @@ function TextAreaField({
|
|||||||
value={draft}
|
value={draft}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
rows={4}
|
rows={4}
|
||||||
onFocus={() => {
|
onFocus={handleFocus}
|
||||||
focusedRef.current = true;
|
onChange={handleChange}
|
||||||
}}
|
onBlur={handleBlur}
|
||||||
onChange={(e) => {
|
|
||||||
setDraft(e.target.value);
|
|
||||||
scheduleCommit(e.target.value);
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
focusedRef.current = false;
|
|
||||||
commitDraft(draft);
|
|
||||||
}}
|
|
||||||
className="w-full resize-none bg-transparent text-[11px] font-medium text-neutral-100 outline-none disabled:cursor-not-allowed disabled:text-neutral-600"
|
className="w-full resize-none bg-transparent text-[11px] font-medium text-neutral-100 outline-none disabled:cursor-not-allowed disabled:text-neutral-600"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user