ui changes

This commit is contained in:
2026-08-10 19:36:45 +05:30
parent 409dffe938
commit 5cd49a1655
49 changed files with 7988 additions and 470 deletions

View File

@@ -10,8 +10,12 @@ import { cn } from '@/lib/utils';
* deliberate newline. That combination is what makes typing a real question feel
* natural instead of cramped.
*
* `placeholders` rotate while the field is empty and unfocused, which teaches
* the range of what can be asked without spending vertical space on a legend.
* `placeholders` is a list, but only the first is shown. It used to rotate every
* few seconds; that was defensible when the panel opened on demand and wrong once
* Owliver became permanent furniture, because a permanently visible field that
* rewrites itself forever is movement in the corner of the eye with nothing to
* gain — the suggestion chips already teach the range of what can be asked, and
* they do it without a timer or a rerender every four seconds.
*/
export function PromptInput({
value,
@@ -26,7 +30,6 @@ export function PromptInput({
}) {
const textareaRef = React.useRef(null);
const [focused, setFocused] = React.useState(false);
const [placeholderIndex, setPlaceholderIndex] = React.useState(0);
/* Grow to fit the content, up to a cap, then scroll internally. */
React.useEffect(() => {
@@ -36,30 +39,6 @@ export function PromptInput({
el.style.height = `${Math.min(el.scrollHeight, 148)}px`;
}, [value]);
/* Rotate the examples only while the field is idle — changing the placeholder
under someone who is mid-thought is distracting. The crossfade is done with
a real element rather than the native placeholder, which cannot animate. */
const [fading, setFading] = React.useState(false);
React.useEffect(() => {
if (value || focused || placeholders.length < 2) return undefined;
const timer = setInterval(() => {
setFading(true);
// Swap the text at the midpoint of the fade so neither string is visible
// while it changes.
setTimeout(() => {
setPlaceholderIndex((i) => (i + 1) % placeholders.length);
setFading(false);
}, 220);
}, 4000);
return () => clearInterval(timer);
}, [value, focused, placeholders.length]);
/* A shorter list on a context change must not leave the index out of range. */
React.useEffect(() => {
setPlaceholderIndex(0);
}, [placeholders]);
React.useEffect(() => {
if (autoFocus) textareaRef.current?.focus();
}, [autoFocus]);
@@ -100,7 +79,7 @@ export function PromptInput({
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
disabled={busy}
aria-label="Ask Krow AI"
aria-label="Ask Owliver"
aria-busy={busy || undefined}
className={cn(
'block w-full resize-none bg-transparent leading-relaxed text-ink-1',
@@ -110,19 +89,19 @@ export function PromptInput({
)}
/>
{/* Animated placeholder. Hidden from assistive tech — the textarea's
aria-label is the accessible name. */}
{/* Rendered as an element rather than the native `placeholder` so it can
be truncated and swapped for the busy state. Hidden from assistive
tech — the textarea's aria-label is the accessible name. */}
{!value && (
<span
aria-hidden="true"
className={cn(
'pointer-events-none absolute inset-0 truncate text-ink-4 transition-opacity duration-base',
'pointer-events-none absolute inset-0 truncate text-ink-4',
'py-0.5 text-body-sm',
isCenter ? 'text-center' : 'text-left',
fading ? 'opacity-0' : 'opacity-100'
isCenter ? 'text-center' : 'text-left'
)}
>
{busy ? 'Thinking…' : placeholders[placeholderIndex]}
{busy ? 'Thinking…' : placeholders[0]}
</span>
)}
</div>