feat(workspace): composer like Claude Desktop, image thumbnails, tool icons in the picker
This commit is contained in:
@@ -63,7 +63,7 @@
|
||||
*
|
||||
* ----------------------------------------------------------------------------- */
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { ChevronDown, Check } from "lucide-react";
|
||||
|
||||
/** Single option in a {@link Select} list. */
|
||||
@@ -74,6 +74,8 @@ export interface SelectOption<T extends string> {
|
||||
label: string;
|
||||
/** Optional secondary line (smaller gray text). */
|
||||
hint?: string;
|
||||
/** Optional leading icon, shown in the trigger and the list. */
|
||||
icon?: ReactNode;
|
||||
}
|
||||
|
||||
/** Props for {@link Select}. */
|
||||
@@ -86,13 +88,25 @@ export interface SelectProps<T extends string> {
|
||||
options: SelectOption<T>[];
|
||||
/** Disables opening the list. */
|
||||
disabled?: boolean;
|
||||
/** `ghost`: a borderless, content-wide trigger for a composer's toolbar. */
|
||||
variant?: "field" | "ghost";
|
||||
/** Accessible name and tooltip of the trigger (a ghost one has no label). */
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessible custom select control.
|
||||
* @typeParam T - string union of allowed option values.
|
||||
*/
|
||||
export function Select<T extends string>({ value, onChange, options, disabled }: SelectProps<T>) {
|
||||
export function Select<T extends string>({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
disabled,
|
||||
variant = "field",
|
||||
label,
|
||||
}: SelectProps<T>) {
|
||||
const ghost = variant === "ghost";
|
||||
const [open, setOpen] = useState(false);
|
||||
const [active, setActive] = useState(() =>
|
||||
Math.max(
|
||||
@@ -166,21 +180,31 @@ export function Select<T extends string>({ value, onChange, options, disabled }:
|
||||
const current = options.find((o) => o.value === value) || options[0];
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative">
|
||||
<div ref={containerRef} className={ghost ? "relative inline-block max-w-[11rem]" : "relative"}>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
aria-label={label}
|
||||
aria-haspopup="listbox"
|
||||
title={label}
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
onKeyDown={onKey}
|
||||
className="w-full flex items-center justify-between gap-2 bg-surface-2 border border-border rounded-md px-3 py-1.5 text-[11px] text-fg-primary focus:outline-none focus:border-accent/50 hover:bg-surface-3 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
className={
|
||||
ghost
|
||||
? "flex max-w-full items-center gap-1.5 rounded-md px-2 py-1 text-xs text-fg-secondary hover:bg-surface-3 hover:text-fg-primary focus:outline-none focus-visible:ring-1 focus-visible:ring-accent/50 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
: "w-full flex items-center justify-between gap-2 bg-surface-2 border border-border rounded-md px-3 py-1.5 text-[11px] text-fg-primary focus:outline-none focus:border-accent/50 hover:bg-surface-3 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
}
|
||||
>
|
||||
<span className="truncate">{current?.label ?? "-"}</span>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
{current?.icon}
|
||||
<span className="truncate">{current?.label ?? "-"}</span>
|
||||
</span>
|
||||
<ChevronDown className="w-3.5 h-3.5 text-fg-muted flex-shrink-0" />
|
||||
</button>
|
||||
{open && (
|
||||
<div
|
||||
className={`absolute z-30 left-0 right-0 rounded-md border border-border bg-surface-1 shadow-lg shadow-black/40 max-h-72 overflow-auto py-1 ${
|
||||
className={`absolute z-30 left-0 ${ghost ? "w-max min-w-[12rem] max-w-[20rem]" : "right-0"} rounded-md border border-border bg-surface-1 shadow-lg shadow-black/40 max-h-72 overflow-auto py-1 ${
|
||||
openUp ? "bottom-full mb-1" : "top-full mt-1"
|
||||
}`}
|
||||
>
|
||||
@@ -199,6 +223,7 @@ export function Select<T extends string>({ value, onChange, options, disabled }:
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{opt.icon}
|
||||
<span
|
||||
className={`text-[11px] flex-1 truncate ${
|
||||
isSelected ? "text-accent font-medium" : "text-fg-secondary"
|
||||
|
||||
Reference in New Issue
Block a user