feat: placeholder input mode switch

This commit is contained in:
FoxxMD 2026-10-12 18:53:59 +00:00
parent 1308719e18
commit c06b13a8b9
5 changed files with 42 additions and 12 deletions

View file

@ -35,7 +35,10 @@ import {
LuBraces,
LuText,
LuCode,
LuPlus
LuPlus,
LuSearch,
LuTextCursor,
LuCaseUpper
} from "react-icons/lu"
import { VscDebugRestart } from 'react-icons/vsc';
import { HiMiniStop } from "react-icons/hi2";
@ -332,4 +335,16 @@ export const CodeButton = makeIconButton(LuCode);
export const PlusIconRaw = LuPlus;
export const PlusIcon = makeChakraIcon(LuPlus);
export const PlusButton = makeIconButton(LuPlus);
export const PlusButton = makeIconButton(LuPlus);
export const SearchIconRaw = LuSearch;
export const SearchIcon = makeChakraIcon(LuSearch);
export const SearchButton = makeIconButton(LuSearch);
export const CursorIconRaw = LuTextCursor;
export const CursorIcon = makeChakraIcon(LuTextCursor);
export const CursorButton = makeIconButton(LuTextCursor);
export const LettersIconRaw = LuCaseUpper;
export const LettersIcon = makeChakraIcon(LuCaseUpper);
export const LettersButton = makeIconButton(LuCaseUpper);

View file

@ -1,8 +1,8 @@
import { Field, HStack, Input, Stack } from "@chakra-ui/react";
import { Field, HStack, Input, InputGroup, Stack } from "@chakra-ui/react";
import type { FieldWithValue } from "@tanstack/react-form";
import { useState, type ReactNode } from "react";
import type { Credit, MusicServices } from "../../../core/Atomic"
import { PlusButton } from "../icons/ChakraIcons";
import { PlusButton, SearchButton, SearchIcon } from "../icons/ChakraIcons";
import { MusicServiceInput } from "../musicServices/MusicServiceEditable";
import { MusicServiceIndicator } from "../musicServices/MusicServiceIndicators";
@ -11,6 +11,7 @@ export interface CreditInputProps {
value?: Credit
onChange: (val: Credit) => void
onBlur?: () => void
onInputSwitch?: () => void
placeholder?: string
/** Limits the music service types that can be chosen EX ['track'] */
useWith?: string[]
@ -21,6 +22,7 @@ export const CreditInput = (props: CreditInputProps) => {
value = { name: '' },
onChange,
onBlur,
onInputSwitch,
placeholder,
useWith
} = props;
@ -38,11 +40,13 @@ export const CreditInput = (props: CreditInputProps) => {
return (
<Stack width="100%" gap="2">
<Input
value={value.name}
placeholder={placeholder}
onBlur={onBlur}
onChange={(e) => onChange({ ...value, name: e.currentTarget.value })} />
<InputGroup endElement={onInputSwitch !== undefined ? <SearchButton variant="ghost" size="xs" onClick={() => onInputSwitch()}/> : undefined}>
<Input
value={value.name}
placeholder={placeholder}
onBlur={onBlur}
onChange={(e) => onChange({ ...value, name: e.currentTarget.value })} />
</InputGroup>
<HStack wrap="wrap" alignItems="normal">
{metadata.map((x, i) => (
<MusicServiceIndicator

View file

@ -1,12 +1,13 @@
import { AbsoluteCenter, Avatar, Box, Combobox, HStack, Icon, Menu, Portal, Span, Spinner, Switch, StackSeparator, Text, InputGroup, type ListCollection, Flex } from "@chakra-ui/react"
import type { MetadataResultBase, MetadataResultImage, MetadataResultServiceScore } from "../../../core/Api"
import { BracesIcon, getMusicServiceIconElement, TextIcon } from "../icons/ChakraIcons"
import { BracesIcon, getMusicServiceIconElement, LettersButton, TextIcon } from "../icons/ChakraIcons"
import React, { useRef, useState } from "react"
import { useDebouncer } from "@tanstack/react-pacer"
import { MSErrorBoundary } from "../ErrorBoundary"
import { EllipsisButtonMenu } from "../buttonMenus/ButtonMenu"
import { formatNumber } from "../../../core/DataUtils"
import { TextMuted } from "../TextMuted"
import { Tooltip } from "../ToggleTip"
export const LeftSideMetadataResultContent = (props: MetadataResultServiceScore & MetadataResultImage) => {
if (props.image === undefined) {
@ -83,11 +84,12 @@ export interface MetadataSearchComboboxProps<T extends MetadataSearchResult> {
onFreetext: (name: string) => void
/** Called when a search should run. Typing is debounced here, a context mode switch is not */
onQueryChange: (query: string, contextMode: boolean) => void
onInputSwitch?: () => void,
renderItem: (item: T, onPick: (val: T) => void) => React.JSX.Element
}
export const MetadataSearchCombobox = <T extends MetadataSearchResult>(props: MetadataSearchComboboxProps<T>) => {
const { placeholder, collection, isLoading, isError, onChange, onFreetext, onQueryChange, renderItem, initialInput = '' } = props;
const { placeholder, collection, isLoading, isError, onChange, onFreetext, onQueryChange, renderItem, initialInput = '', onInputSwitch } = props;
// a ref, not state: combobox captures onInteractOutside when the popup opens so state read there is stale
const rawInput = useRef<string | undefined>(initialInput === '' ? undefined : initialInput);
@ -144,6 +146,11 @@ export const MetadataSearchCombobox = <T extends MetadataSearchResult>(props: Me
<HStack>Switch to {contextMode ? 'text-only' : 'context'} {modeIconElm} search mode {isLoading ? <Spinner size="xs" borderWidth="1px" /> : undefined}</HStack>
</Combobox.Item>);
let plainInputSwitch: React.JSX.Element | undefined = undefined;
if(onInputSwitch !== undefined) {
plainInputSwitch = <Tooltip content="Switch to plain text input mode."><LettersButton variant="ghost" onClick={() => onInputSwitch()}/></Tooltip>
}
return (
<Flex flexGrow="1">
<MSErrorBoundary>
@ -190,6 +197,7 @@ export const MetadataSearchCombobox = <T extends MetadataSearchResult>(props: Me
<Combobox.IndicatorGroup>
{hasSearched && !isLoading ? <TextMuted textStyle="xs" hideBelow="sm">{collection.size} Matches</TextMuted> : undefined}
{!open && isLoading ? <Spinner size="xs" borderWidth="1px" /> : undefined}
{plainInputSwitch}
<ModeIconHint/>
<Combobox.ClearTrigger />
<Combobox.Trigger />

View file

@ -101,6 +101,7 @@ export type TrackOnChange = Pick<TrackData, 'track' | 'artists' | 'album' | 'alb
export interface TrackSearchProps {
initial?: Credit
onChange: (val: TrackOnChange) => void
onInputSwitch?: () => void
contextData?: TrackDataCreditBase
}
@ -169,6 +170,7 @@ export const TrackSearch = (props: TrackSearchProps) => {
isError={query.isError}
initialInput={selectedItem?.name}
onChange={doChange}
onInputSwitch={props.onInputSwitch}
onFreetext={(name) => doChange({ track: { name } })}
onQueryChange={search}
renderItem={(item, onPick) => <TrackSearchResultItem data={item} onPick={onPick} />}

View file

@ -31,7 +31,8 @@ const meta = preview.type<{ args: PropsAndCustomArgs }>().meta({
],
args: {
initial: undefined,
onChange: (val) => console.log(val)
onChange: (val) => console.log(val),
onInputSwitch: () => console.log('changing input')
},
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#story-args
});