Align dictation controls with settings dropdowns and show setup guidance
This commit is contained in:
@@ -1,12 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { DICTATION_LANGUAGES, type DictationLanguage } from "@/src/core";
|
||||
import { MenuShell } from "@/components/menu-shell";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Hint } from "@/components/ui/hint";
|
||||
import { SettingsRow } from "@/components/settings/controls";
|
||||
import { SettingsMenuTrigger } from "@/components/settings/controls";
|
||||
import { SearchMenuList } from "@/components/settings/search-menu-list";
|
||||
import { useDictationActive } from "@/components/dictation-state";
|
||||
import { useDictationLanguage } from "@/components/dictation-preference";
|
||||
@@ -16,9 +14,13 @@ type LanguagePreference = ReturnType<typeof useDictationLanguage>;
|
||||
export function DictationLanguageSetting() {
|
||||
const preference = useDictationLanguage();
|
||||
const active = useDictationActive();
|
||||
return <SettingsRow label="Language" testid="row-dictation-language" dimmed={active}>
|
||||
return <div
|
||||
data-testid="row-dictation-language"
|
||||
className={`flex items-center justify-between gap-3 rounded-sm px-1 py-1.5${active ? " opacity-40" : ""}`}
|
||||
>
|
||||
<span className="font-ui text-xs text-foreground">Language</span>
|
||||
<DictationLanguagePicker preference={preference} disabled={active} />
|
||||
</SettingsRow>;
|
||||
</div>;
|
||||
}
|
||||
|
||||
function DictationLanguagePicker({ preference, disabled }: {
|
||||
@@ -58,11 +60,9 @@ function DictationLanguagePicker({ preference, disabled }: {
|
||||
|
||||
return <>
|
||||
<Hint label={error ?? `Dictation language: ${label}`} side="top">
|
||||
<Button
|
||||
<SettingsMenuTrigger
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
value={label}
|
||||
data-testid="dictation-language"
|
||||
aria-label={`Dictation language: ${label}`}
|
||||
aria-expanded={open && !disabled}
|
||||
@@ -72,11 +72,8 @@ function DictationLanguagePicker({ preference, disabled }: {
|
||||
setOpen((value) => !value);
|
||||
if (language === undefined) void load().catch(() => {});
|
||||
}}
|
||||
className="h-8 min-w-0 max-w-[65%] px-2.5 font-normal"
|
||||
>
|
||||
<span className="truncate">{label}</span>
|
||||
<ChevronDownIcon aria-hidden className="size-3" />
|
||||
</Button>
|
||||
className="min-w-20 max-w-[65%]"
|
||||
/>
|
||||
</Hint>
|
||||
{open && !disabled && <MenuShell
|
||||
anchorRef={triggerRef}
|
||||
|
||||
@@ -71,7 +71,7 @@ import {
|
||||
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
import { prettyModel } from "@/components/usage-format";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SettingsGroup, SettingsRow, Toggle, CommitInput } from "./controls.tsx";
|
||||
import { SettingsGroup, SettingsRow, SettingsMenuTrigger, Toggle, CommitInput } from "./controls.tsx";
|
||||
import { ModelMenuList, ThinkingMenuList } from "./model-menu";
|
||||
import { ChatGPTBackgroundAccess } from "./chatgpt-connection";
|
||||
import { NativeSubscriptionBackgroundAccess } from "./subscription-connections";
|
||||
@@ -245,23 +245,17 @@ function MenuTrigger({
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const button = (
|
||||
<button
|
||||
type="button"
|
||||
<SettingsMenuTrigger
|
||||
value={value || emptyLabel}
|
||||
aria-label={ariaLabel}
|
||||
aria-expanded={open}
|
||||
data-testid={testid}
|
||||
onClick={() => {
|
||||
if (!disabled) onClick();
|
||||
}}
|
||||
className={`flex h-7 items-center gap-1 rounded-sm border border-border bg-background px-1.5 font-ui text-xs text-foreground transition-colors ${
|
||||
disabled ? "cursor-not-allowed" : "hover:border-muted-foreground/50"
|
||||
} ${widthClass}`}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate">{value || emptyLabel}</span>
|
||||
<span aria-hidden className="text-muted-foreground">
|
||||
▾
|
||||
</span>
|
||||
</button>
|
||||
unavailable={disabled}
|
||||
className={widthClass}
|
||||
/>
|
||||
);
|
||||
return hint ? <Hint label={hint}>{button}</Hint> : button;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ import { MenuShell } from "@/components/menu-shell";
|
||||
import { Hint } from "@/components/ui/hint";
|
||||
import { useAppearance } from "./appearance.tsx";
|
||||
import { FontMenuList } from "./model-menu";
|
||||
import { CommitInput, SettingsRow, ThinSelect, Toggle } from "./controls.tsx";
|
||||
import { CommitInput, SettingsMenuTrigger, SettingsRow, ThinSelect, Toggle } from "./controls.tsx";
|
||||
|
||||
/** The factory reading family — the DEFAULT row's identity (unset =
|
||||
* this). Exported for tests. */
|
||||
@@ -210,29 +210,22 @@ export function AppearanceSection({
|
||||
testid="row-font"
|
||||
>
|
||||
<span ref={fontMenuOpen ? menuAnchorRef : undefined} className="relative flex items-center gap-2">
|
||||
{/* The MenuTrigger look (agent-section's collapsed
|
||||
select — it lives unexported there, so the
|
||||
classes ride inline; the searchable machine is
|
||||
the shared part). The tooltip rides the CLOSED
|
||||
button ONLY (owner ruling 2026-09-26: never while
|
||||
{/* The shared Agent/Appearance collapsed-menu trigger.
|
||||
The tooltip rides the CLOSED button ONLY
|
||||
(owner ruling 2026-09-26: never while
|
||||
the dropdown is out — it popped over the open
|
||||
menu when the wrap covered the whole anchor
|
||||
span). */}
|
||||
{(() => {
|
||||
const triggerButton = (
|
||||
<button
|
||||
type="button"
|
||||
<SettingsMenuTrigger
|
||||
value={appearance?.fontFamily || DEFAULT_READING_FONT}
|
||||
aria-label="Reading font"
|
||||
aria-expanded={fontMenuOpen}
|
||||
data-testid="font-trigger"
|
||||
onClick={openFontMenu}
|
||||
className="flex h-7 min-w-20 items-center gap-1 rounded-sm border border-border bg-background px-1.5 font-ui text-xs text-foreground transition-colors hover:border-muted-foreground/50"
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate">{appearance?.fontFamily || DEFAULT_READING_FONT}</span>
|
||||
<span aria-hidden className="text-muted-foreground">
|
||||
▾
|
||||
</span>
|
||||
</button>
|
||||
className="min-w-20"
|
||||
/>
|
||||
);
|
||||
return fontMenuOpen ? (
|
||||
triggerButton
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* control together) and the control itself locks — the row owns the dim.
|
||||
*/
|
||||
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { useState, type ComponentPropsWithRef, type ReactNode } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/** A section block: pronounced mono heading + content. */
|
||||
@@ -81,6 +81,30 @@ export function SettingsRow({
|
||||
);
|
||||
}
|
||||
|
||||
/** The existing Agent/Appearance collapsed-menu look, shared with dictation.
|
||||
* `unavailable` preserves callers that gate clicks without native disabling;
|
||||
* `disabled` retains native button semantics where the caller requires them. */
|
||||
export function SettingsMenuTrigger({
|
||||
value, unavailable, disabled, className, ...props
|
||||
}: Omit<ComponentPropsWithRef<"button">, "value" | "children"> & {
|
||||
value: string;
|
||||
unavailable?: boolean;
|
||||
}) {
|
||||
return <button
|
||||
type="button"
|
||||
{...props}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"flex h-7 items-center gap-1 rounded-sm border border-border bg-background px-1.5 font-ui text-xs text-foreground transition-colors",
|
||||
disabled || unavailable ? "cursor-not-allowed" : "hover:border-muted-foreground/50",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate">{value}</span>
|
||||
<span aria-hidden className="text-muted-foreground">▾</span>
|
||||
</button>;
|
||||
}
|
||||
|
||||
/** The on/off switch — a plain button in the switch role. */
|
||||
export function Toggle({
|
||||
checked,
|
||||
|
||||
@@ -490,7 +490,17 @@ export function KeysSection({
|
||||
testid={`key-${status.id.toLowerCase()}`}
|
||||
/>
|
||||
))}
|
||||
{label === "Dictation" && <DictationLanguageSetting />}
|
||||
{label === "Dictation" && <>
|
||||
<DictationLanguageSetting />
|
||||
<p data-testid="dictation-setup-hint" className="px-1 text-[11px] leading-relaxed text-muted-foreground/70">
|
||||
Create a Deepgram key with the <a
|
||||
href="https://developers.deepgram.com/reference/auth/tokens/grant"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline underline-offset-2 hover:text-foreground"
|
||||
>Member role or higher</a>, then add it above.
|
||||
</p>
|
||||
</>}
|
||||
</SettingsGroup>)}
|
||||
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user