feat: profile bio

This commit is contained in:
gosangam
2026-08-07 23:47:22 +05:30
parent 274c735b4d
commit 6ff29db68a
9 changed files with 168 additions and 28 deletions
+29 -7
View File
@@ -33,22 +33,30 @@ const SUGGEST_LIMIT = 8;
const MENTION_RE = /(^|\s)@([A-Za-z0-9_]{0,30})$/;
type Mention = { start: number; query: string };
type PanelPos = { left: number; bottom: number; width: number };
/** Exactly one of `top`/`bottom` is set — see the flip logic in detect(). */
type PanelPos = { left: number; width: number; top?: number; bottom?: number };
/** Keep in sync with the panel's `max-h-56` (14rem). Used to decide whether
* the suggestion list fits above the field or has to flip below it. */
const PANEL_MAX_H = 224;
export const MentionTextarea = forwardRef<
HTMLTextAreaElement,
{
value: string;
onChange: (v: string) => void;
/** Called on Enter (no Shift) when the suggestion list is closed. */
/** Called on Enter (no Shift) when the suggestion list is closed. Omit for
* multi-line fields — without it, Enter inserts a newline as usual. */
onSubmit?: () => void;
placeholder?: string;
className?: string;
maxLength?: number;
rows?: number;
/** Forwarded to the <textarea> so a <Label htmlFor> can address it. */
id?: string;
}
>(function MentionTextarea(
{ value, onChange, onSubmit, placeholder, className, maxLength, rows = 1 },
{ value, onChange, onSubmit, placeholder, className, maxLength, rows = 1, id },
ref,
) {
const innerRef = useRef<HTMLTextAreaElement>(null);
@@ -73,10 +81,18 @@ export const MentionTextarea = forwardRef<
setMention({ start: caret - m[2].length - 1, query: m[2] });
setActive(0);
const r = el.getBoundingClientRect();
// Prefer opening ABOVE the field: that's what the comment composer needs
// (it sits at the bottom of a drawer, often above the on-screen keyboard).
// But flip BELOW when the field is too near the top of the viewport for the
// panel to fit — otherwise the list renders off-screen and the feature
// looks broken (e.g. the bio field high up on the profile editor).
const fitsAbove = r.top >= PANEL_MAX_H + 8;
setPos({
left: r.left,
bottom: window.innerHeight - r.top + 6,
width: r.width,
...(fitsAbove
? { bottom: window.innerHeight - r.top + 6 }
: { top: r.bottom + 6 }),
});
}
@@ -143,9 +159,12 @@ export const MentionTextarea = forwardRef<
return;
}
}
if (e.key === "Enter" && !e.shiftKey) {
// Only hijack Enter when the caller actually wants submit-on-Enter.
// Without the `onSubmit` guard we'd swallow the keystroke for multi-line
// callers (e.g. the profile bio), leaving them unable to type a newline.
if (e.key === "Enter" && !e.shiftKey && onSubmit) {
e.preventDefault();
onSubmit?.();
onSubmit();
}
}
@@ -153,6 +172,7 @@ export const MentionTextarea = forwardRef<
<div className="relative flex-1">
<textarea
ref={innerRef}
id={id}
value={value}
rows={rows}
maxLength={maxLength}
@@ -180,8 +200,10 @@ export const MentionTextarea = forwardRef<
style={{
position: "fixed",
left: pos.left,
bottom: pos.bottom,
width: pos.width,
...(pos.top !== undefined
? { top: pos.top }
: { bottom: pos.bottom }),
}}
// pointer-events-auto: the comments drawer is a modal vaul sheet
// (body gets pointer-events:none); without this the list would be