@vvedantb/ui 0.16.3 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -198,7 +198,7 @@ import { SessionsHoverCard } from "@vvedantb/ui";
198
198
  ## Panels, search and confirm
199
199
 
200
200
  ```tsx
201
- import { ConfirmDialog, SlideOverPanel, ToolbarSearchInput } from "@vvedantb/ui";
201
+ import { ConfirmDialog, SearchInput, SlideOverPanel } from "@vvedantb/ui";
202
202
 
203
203
  <div className="relative">
204
204
  <SlideOverPanel open={open} onClose={() => setOpen(false)} header={<h2>Details</h2>}>
@@ -206,7 +206,9 @@ import { ConfirmDialog, SlideOverPanel, ToolbarSearchInput } from "@vvedantb/ui"
206
206
  </SlideOverPanel>
207
207
  </div>
208
208
 
209
- <ToolbarSearchInput value={query} onChange={setQuery} />
209
+ <SearchInput value={query} onChange={setQuery} label="Search files" shortcut="/" />
210
+
211
+ <SearchInput variant="toolbar" value={query} onChange={setQuery} />
210
212
 
211
213
  <ConfirmDialog size="sm" open={open} onOpenChange={setOpen} title="Delete item?" variant="destructive" onConfirm={remove} />
212
214
 
@@ -214,7 +216,9 @@ import { ConfirmDialog, SlideOverPanel, ToolbarSearchInput } from "@vvedantb/ui"
214
216
  ```
215
217
 
216
218
  - `SlideOverPanel`: a right-side panel, absolutely positioned inside its nearest positioned parent. It is not a portal or dialog. It slides in with a spring and is hidden below `md`. Only the close button closes it (no Escape, no outside click). `width` is `"w-96"` (default) or `"w-80"`. `closeLabel` defaults to `"Close panel"`. `className` merges last.
217
- - `ToolbarSearchInput`: a compact `h-8` search field for toolbars, with a search icon and a clear button. The clear button stays mounted and is hidden while the value is blank. Clearing calls `onChange("")` and does not move focus. Defaults: `placeholder="Search..."`, `label="Search"` (the `aria-label`), `clearLabel="Clear search"`. The ref goes to the `<input>`.
219
+ - `SearchInput`: the search field. A `ClearInput` with a search icon on the left and the clear button on the right (shown while there is text; clearing calls `onChange("")`). `label` (default `"Search"`) is the `aria-label` and, without `placeholder`, the placeholder. `clearLabel` defaults to `"Clear search"`. `shortcut` (such as `"/"` or `"⌘K"`) shows a `Kbd` hint on the right while the value is empty. `size`: `"sm"` (`h-8`, default) or `"md"` (`h-10`). `variant`: `"default"` or `"toolbar"` (transparent until hover or focus, `rounded-field`, full width on mobile and `sm:w-44 md:w-52`). Other input props pass through; `enterKeyHint="search"`. `className` goes to the `<input>`, `wrapperClassName` to the wrapper, the ref to the `<input>`.
220
+ - `ToolbarSearchInput` (deprecated in 0.17.0): a wrapper over `<SearchInput variant="toolbar">` with `placeholder="Search..."`. `className` goes to the wrapper, `inputClassName` to the `<input>`.
221
+ - Field focus: every field (`Input`, `Textarea`, `Select` and `RadixSelect` triggers, `ClearInput`, `SearchInput`, `InputGroup`, date and time picker triggers) uses `fieldFocusClass`: the border turns `focus-border` and a 1px `ring-focus-ring` shows. Wrappers use `fieldFocusWithinClass` (`has-focus-visible:`). Both are exported for app fields.
218
222
  - `ConfirmDialog` `size`: `"default"` keeps the alert dialog. `"sm"` renders a compact, dismissible dialog (outside click, Escape and the close button close it) with small Cancel and confirm buttons. Cancel calls `onCancel` and then `onOpenChange(false)`. Confirm calls `onConfirm` only, so the caller closes it. `"md"` (0.5.2) is dismissible like `"sm"`, with the title in a header (`pb-4`), the description in its own section (`py-6`) and a footer (`pt-4`) with default-size buttons. It has the same Cancel and confirm behaviour as `"sm"`.
219
223
  - Legacy (still supported): the `ConfirmDialog` `size`, `variant`, `cancelLabel` and `confirmLabel` props. For anything they do not cover, compose the parts (see [Composable parts](#composable-parts-060-options-061)).
220
224
 
@@ -981,7 +985,7 @@ const store: ArtifactStateStore = {
981
985
  - `ArtifactRender`: the body by format. Markdown renders with Streamdown, code with `CodeBlock` (`language`), and HTML, SVG and JSON in an `ArtifactFrame`.
982
986
  - `ArtifactCard`: a grid tile (type icon, title, description, compact updated time) or a `layout="list"` row. With `getHref` it is an `<a>`; a plain left click then calls `onOpen` instead of following the link, and modified clicks stay native. The actions menu (dots button and right-click) lists Open (`onOpen`), Open in new tab (`getHref`), Rename (`onRename`), Copy and Download (`getBody`, since list rows carry no body) and Delete (`onDelete`). Each shows only when its callback is set; with none, there is no menu.
983
987
  - `ArtifactList`: `ArtifactCard`s in a grid (one to three columns) or a list, with the card callbacks passed through. With no artifacts it shows an `EmptyState` (`emptyTitle`, `emptyDescription`, `emptyActions`).
984
- - `ArtifactListToolbar`: controlled `ToolbarSearchInput` (`query`, `onQueryChange`), a type filter menu (`type`, `onTypeChange`, `types`; `undefined` is "All types") and a grid or list toggle (`layout`, `onLayoutChange`). The filter and the toggle show only with their callbacks. `children` go at the end.
988
+ - `ArtifactListToolbar`: controlled `SearchInput variant="toolbar"` (`query`, `onQueryChange`), a type filter menu (`type`, `onTypeChange`, `types`; `undefined` is "All types") and a grid or list toggle (`layout`, `onLayoutChange`). The filter and the toggle show only with their callbacks. `children` go at the end.
985
989
  - `ArtifactViewer`: the ai-elements `Artifact` container with a header (`headerStart`, type icon, title) and actions: versions menu (`versions`), Rename, Copy, Download (the format's extension and MIME type from `ARTIFACT_FORMAT_FILES`), Open in new tab (`href`), your `actions`, and Delete. Below it, `ArtifactRender` of `artifact.body`. Pass the body of the version on screen.
986
990
  - `ArtifactVersionsMenu`: a "vN" button listing `versions` newest first, with the current one marked. With `onViewVersion`, picking a version shows it, and "Restore vN" (`onRestoreVersion`) appears while an older version is on screen. Without it, picking an older version restores it.
987
991
  - Rename and delete use `ConfirmDialog` (`ArtifactRenameDialog`, `ArtifactDeleteDialog`, and `useArtifactEditDialogs` for custom surfaces). Rename sends the trimmed title, and does nothing when it is unchanged. Alt-click on Delete skips the dialog, as with `useRequestConfirm`. The delete dialog says "This cannot be undone."; pass `deleteDescription` to `ArtifactViewer`, `ArtifactCard` or `ArtifactList` to change it (0.13.1), for example when your delete can be undone. A callback that rejects keeps its dialog open and shows an error toast; copy and load failures toast too, so mount `SonnerToaster`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vvedantb/ui",
3
- "version": "0.16.3",
3
+ "version": "0.17.0",
4
4
  "license": "MIT",
5
5
  "description": "Shared React 19 UI for web apps: one JavaScript entry (primitives, shell pieces, app helpers including the Convex and Clerk provider, VITE env parsing and ensure-user, artifact viewers over the @vvedantb/server artifacts core, native tokens), design-token CSS, shared Tailwind v4 CSS and a Tailwind preset.",
6
6
  "type": "module",
@@ -164,7 +164,7 @@
164
164
  "vitest": "^3.2.0",
165
165
  "zod": "^4.0.0",
166
166
  "zod3": "npm:zod@^3.25.76",
167
- "@vvedantb/server": "0.6.1"
167
+ "@vvedantb/server": "0.7.1"
168
168
  },
169
169
  "peerDependenciesMeta": {
170
170
  "@clerk/clerk-react": {
@@ -10,7 +10,7 @@ import {
10
10
  DropdownMenuSeparator,
11
11
  DropdownMenuTrigger,
12
12
  } from "../ui/dropdown-menu";
13
- import { ToolbarSearchInput } from "../ui/toolbar-search-input";
13
+ import { SearchInput } from "../ui/search-input";
14
14
  import { cn } from "../utils/cn";
15
15
  import type { ArtifactLayout } from "./artifact-card";
16
16
  import { ARTIFACT_TYPE_LABELS, ArtifactTypeIcon } from "./artifact-type-icon";
@@ -52,7 +52,8 @@ export function ArtifactListToolbar({
52
52
  }: ArtifactListToolbarProps) {
53
53
  return (
54
54
  <div className={cn("flex flex-wrap items-center gap-2", className)}>
55
- <ToolbarSearchInput
55
+ <SearchInput
56
+ variant="toolbar"
56
57
  value={query}
57
58
  onChange={onQueryChange}
58
59
  placeholder={searchPlaceholder}
@@ -229,7 +229,7 @@ function ComposerShell({
229
229
  data-stop-mode={stopMode}
230
230
  className={cn(
231
231
  "group/composer relative flex w-full min-w-0 flex-col rounded-[1.25rem] border border-border bg-field-background text-sm transition-[border-color,box-shadow] duration-200 ease-smooth",
232
- "has-[[data-composer-input]:focus-visible]:border-focus-border has-[[data-composer-input]:focus-visible]:ring-2 has-[[data-composer-input]:focus-visible]:ring-focus-ring",
232
+ "has-[[data-composer-input]:focus-visible]:border-focus-border has-[[data-composer-input]:focus-visible]:ring-1 has-[[data-composer-input]:focus-visible]:ring-focus-ring",
233
233
  disabled && "opacity-60",
234
234
  className,
235
235
  )}
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  // stylesheet in the app. This package does not ship a second token set.
3
3
  export { cn } from "./utils/cn";
4
4
  export { floatingSurfaceClass } from "./ui/_menu-classes";
5
+ export { fieldFocusClass, fieldFocusWithinClass } from "./ui/_field-classes";
5
6
  export * from "./ui/surface-classes";
6
7
  export * from "./motion/presets";
7
8
  export * from "./motion/panel-transition";
@@ -0,0 +1,7 @@
1
+ // light field focus: border colour plus a 1px ring
2
+ export const fieldFocusClass =
3
+ "focus-visible:outline-hidden focus-visible:border-focus-border focus-visible:ring-1 focus-visible:ring-focus-ring";
4
+
5
+ // the same on a wrapper whose control has focus
6
+ export const fieldFocusWithinClass =
7
+ "has-focus-visible:outline-hidden has-focus-visible:border-focus-border has-focus-visible:ring-1 has-focus-visible:ring-focus-ring";
@@ -1,6 +1,8 @@
1
+ import { fieldFocusClass } from "./_field-classes";
2
+
1
3
  // input-styled trigger shared by TimePicker and the date pickers
2
4
  export const pickerTriggerClass =
3
- "inline-flex h-8 items-center justify-between gap-2 rounded-field border border-border bg-field-background px-2.5 text-xs font-medium tabular-nums transition-[border-color,box-shadow,background-color] duration-200 ease-smooth hover:bg-field-background/90 focus-visible:border-focus-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50";
5
+ `inline-flex h-8 items-center justify-between gap-2 rounded-field border border-border bg-field-background px-2.5 text-xs font-medium tabular-nums transition-[border-color,box-shadow,background-color] duration-200 ease-smooth hover:bg-field-background/90 disabled:cursor-not-allowed disabled:opacity-50 ${fieldFocusClass}`;
4
6
 
5
7
  // selected item in a picker popover (TimePicker columns, calendar days, months)
6
8
  export const pickerSelectedClass = "bg-primary text-primary-foreground";
@@ -95,7 +95,7 @@ function Calendar({
95
95
  defaultClassNames.dropdowns,
96
96
  ),
97
97
  dropdown_root: cn(
98
- "relative rounded-field border border-border has-focus:border-focus-border has-focus:ring-2 has-focus:ring-focus-ring",
98
+ "relative rounded-field border border-border has-focus:border-focus-border has-focus:ring-1 has-focus:ring-focus-ring",
99
99
  defaultClassNames.dropdown_root,
100
100
  ),
101
101
  dropdown: cn(
@@ -2,6 +2,7 @@
2
2
  import * as React from "react";
3
3
  import { IconX } from "@tabler/icons-react";
4
4
  import { cn } from "../utils/cn";
5
+ import { fieldFocusClass } from "./_field-classes";
5
6
  export interface ClearInputProps
6
7
  extends Omit<React.ComponentProps<"input">, "value" | "onChange"> {
7
8
  value: string;
@@ -47,7 +48,8 @@ const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
47
48
  }}
48
49
  placeholder={placeholder}
49
50
  className={cn(
50
- "flex h-10 w-full rounded-field border border-border bg-field-background px-3.5 py-2 text-sm transition-[border-color,background-color] duration-200 ease-smooth placeholder:text-field-placeholder focus-visible:border-focus-border focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50",
51
+ "flex h-10 w-full rounded-field border border-border bg-field-background px-3.5 py-2 text-sm transition-[border-color,background-color] duration-200 ease-smooth placeholder:text-field-placeholder disabled:cursor-not-allowed disabled:opacity-50",
52
+ fieldFocusClass,
51
53
  "pr-9",
52
54
  className,
53
55
  )}
@@ -3,6 +3,7 @@
3
3
  import * as React from "react";
4
4
  import { IconX } from "@tabler/icons-react";
5
5
  import { cn } from "../utils/cn";
6
+ import { fieldFocusClass } from "./_field-classes";
6
7
  import { runClearInputDissolve } from "./clearInputDissolve";
7
8
 
8
9
  export interface ClearInputProps extends Omit<
@@ -125,7 +126,8 @@ const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
125
126
  }}
126
127
  placeholder={placeholder}
127
128
  className={cn(
128
- "flex h-10 w-full rounded-field border border-border bg-field-background px-3.5 py-2 text-base sm:text-sm transition-[border-color,box-shadow,background-color] duration-200 ease-smooth placeholder:text-field-placeholder focus-visible:border-focus-border focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50",
129
+ "flex h-10 w-full rounded-field border border-border bg-field-background px-3.5 py-2 text-base sm:text-sm transition-[border-color,box-shadow,background-color] duration-200 ease-smooth placeholder:text-field-placeholder disabled:cursor-not-allowed disabled:opacity-50",
130
+ fieldFocusClass,
129
131
  fieldClassName,
130
132
  )}
131
133
  {...props}
@@ -3,6 +3,7 @@
3
3
  import * as React from "react";
4
4
  import { cva, type VariantProps } from "class-variance-authority";
5
5
  import { cn } from "../utils/cn";
6
+ import { fieldFocusWithinClass } from "./_field-classes";
6
7
  import { Button, type ButtonProps } from "./button";
7
8
  import { Input } from "./input";
8
9
  import { Textarea } from "./textarea";
@@ -19,7 +20,7 @@ const InputGroup = React.forwardRef<
19
20
  "has-[textarea]:h-auto",
20
21
  "has-data-[slot=input-group-control]:h-auto",
21
22
  "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col",
22
- "has-focus-visible:border-focus-border has-focus-visible:outline-hidden has-focus-visible:ring-2 has-focus-visible:ring-focus-ring",
23
+ fieldFocusWithinClass,
23
24
  error &&
24
25
  "border-danger-border has-focus-visible:border-danger has-focus-visible:ring-danger-ring",
25
26
  className,
@@ -20,7 +20,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
20
20
  "[&>input]:has-[>[data-align=inline-end]]:pr-2",
21
21
  "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col [&>input]:has-[>[data-align=block-start]]:pb-3",
22
22
  "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col [&>input]:has-[>[data-align=block-end]]:pt-3",
23
- "has-[[data-slot=input-group-control]:focus-visible]:border-focus-border has-[[data-slot=input-group-control]:focus-visible]:ring-2 has-[[data-slot=input-group-control]:focus-visible]:ring-focus-ring",
23
+ "has-[[data-slot=input-group-control]:focus-visible]:border-focus-border has-[[data-slot=input-group-control]:focus-visible]:ring-1 has-[[data-slot=input-group-control]:focus-visible]:ring-focus-ring",
24
24
  "has-[[data-slot][aria-invalid=true]]:border-danger-border has-[[data-slot][aria-invalid=true]]:ring-danger-ring",
25
25
  className,
26
26
  )}
package/src/ui/input.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { cn } from "../utils/cn";
3
+ import { fieldFocusClass } from "./_field-classes";
3
4
 
4
5
  const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
5
6
  ({ className, type, ...props }, ref) => {
@@ -7,7 +8,8 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
7
8
  <input
8
9
  type={type}
9
10
  className={cn(
10
- "flex h-10 w-full max-sm:min-w-0 rounded-field border border-border bg-field-background px-3.5 py-2 text-base sm:text-sm transition-[border-color,box-shadow,background-color] duration-200 ease-smooth file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-field-placeholder focus-visible:border-focus-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50",
11
+ "flex h-10 w-full max-sm:min-w-0 rounded-field border border-border bg-field-background px-3.5 py-2 text-base sm:text-sm transition-[border-color,box-shadow,background-color] duration-200 ease-smooth file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-field-placeholder disabled:cursor-not-allowed disabled:opacity-50",
12
+ fieldFocusClass,
11
13
  className,
12
14
  )}
13
15
  ref={ref}
@@ -3,6 +3,7 @@ import { IconCheck, IconChevronDown, IconChevronUp } from "@tabler/icons-react";
3
3
  import { Select as SelectPrimitive } from "radix-ui";
4
4
 
5
5
  import { cn } from "../utils/cn";
6
+ import { fieldFocusClass } from "./_field-classes";
6
7
  import {
7
8
  floatingSurfaceAnimateClass,
8
9
  floatingSurfaceClass,
@@ -40,7 +41,8 @@ function SelectTrigger({
40
41
  data-slot="select-trigger"
41
42
  data-size={size}
42
43
  className={cn(
43
- "flex w-fit items-center justify-between gap-2 rounded-field border border-border bg-field-background px-3.5 py-2 text-sm whitespace-nowrap ring-offset-background transition-[border-color,box-shadow,background-color,transform] duration-200 ease-smooth active:duration-100 active:ease-out active:scale-[0.98] placeholder:text-field-placeholder focus-visible:border-focus-border focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-danger data-placeholder:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground data-[size=default]:h-10 data-[size=sm]:h-8 data-[slot=select-value]:*:line-clamp-1 data-[slot=select-value]:*:flex data-[slot=select-value]:*:items-center data-[slot=select-value]:*:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
44
+ "flex w-fit items-center justify-between gap-2 rounded-field border border-border bg-field-background px-3.5 py-2 text-sm whitespace-nowrap ring-offset-background transition-[border-color,box-shadow,background-color,transform] duration-200 ease-smooth active:duration-100 active:ease-out active:scale-[0.98] placeholder:text-field-placeholder disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-danger data-placeholder:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground data-[size=default]:h-10 data-[size=sm]:h-8 data-[slot=select-value]:*:line-clamp-1 data-[slot=select-value]:*:flex data-[slot=select-value]:*:items-center data-[slot=select-value]:*:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
45
+ fieldFocusClass,
44
46
  className,
45
47
  )}
46
48
  {...props}
@@ -4,20 +4,75 @@ import * as React from "react";
4
4
  import { IconSearch } from "@tabler/icons-react";
5
5
  import { cn } from "../utils/cn";
6
6
  import { ClearInput, type ClearInputProps } from "./clear-input";
7
+ import { Kbd } from "./kbd";
7
8
 
8
- export type SearchInputProps = Omit<ClearInputProps, "leading">;
9
+ const sizeClasses = {
10
+ sm: { input: "h-8 pl-8 text-sm", icon: "left-2" },
11
+ md: { input: "h-10 pl-9", icon: "left-3" },
12
+ };
13
+
14
+ const variantClasses = {
15
+ default: { wrapper: "", input: "" },
16
+ toolbar: {
17
+ wrapper: "min-w-0 flex-1 sm:flex-none",
18
+ input: "min-w-0 bg-transparent shadow-none hover:bg-default focus-visible:bg-default sm:w-44 md:w-52",
19
+ },
20
+ };
21
+
22
+ export interface SearchInputProps extends Omit<ClearInputProps, "leading" | "size"> {
23
+ /** `aria-label`, and the placeholder when `placeholder` is not set. Default `"Search"`. */
24
+ label?: string;
25
+ /** Key hint shown on the right while the value is empty, such as `"/"` or `"⌘K"`. */
26
+ shortcut?: string;
27
+ /** `sm` is `h-8`, `md` is `h-10`. Default `"sm"`. */
28
+ size?: keyof typeof sizeClasses;
29
+ /** `toolbar`: transparent until hover or focus, fixed width from `sm`. Default `"default"`. */
30
+ variant?: keyof typeof variantClasses;
31
+ }
9
32
 
10
33
  const SearchInput = React.forwardRef<HTMLInputElement, SearchInputProps>(
11
- ({ className, ...props }, ref) => {
34
+ (
35
+ {
36
+ label = "Search",
37
+ placeholder,
38
+ clearLabel = "Clear search",
39
+ shortcut,
40
+ size = "sm",
41
+ variant = "default",
42
+ className,
43
+ wrapperClassName,
44
+ ...props
45
+ },
46
+ ref,
47
+ ) => {
48
+ const showShortcut = Boolean(shortcut) && props.value.length === 0;
12
49
  return (
13
50
  <ClearInput
14
51
  ref={ref}
15
- className={cn("h-8 pl-8 text-sm", className)}
52
+ aria-label={label}
53
+ placeholder={placeholder ?? label}
54
+ clearLabel={clearLabel}
55
+ enterKeyHint="search"
56
+ wrapperClassName={cn(variantClasses[variant].wrapper, wrapperClassName)}
57
+ className={cn(
58
+ sizeClasses[size].input,
59
+ variantClasses[variant].input,
60
+ showShortcut && "pr-12",
61
+ className,
62
+ )}
16
63
  leading={
17
- <IconSearch
18
- size={16}
19
- className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 shrink-0 text-muted-foreground"
20
- />
64
+ <>
65
+ <IconSearch
66
+ size={16}
67
+ className={cn(
68
+ "pointer-events-none absolute top-1/2 -translate-y-1/2 shrink-0 text-muted-foreground",
69
+ sizeClasses[size].icon,
70
+ )}
71
+ />
72
+ {showShortcut ? (
73
+ <Kbd className="absolute right-2 top-1/2 -translate-y-1/2">{shortcut}</Kbd>
74
+ ) : null}
75
+ </>
21
76
  }
22
77
  {...props}
23
78
  />
package/src/ui/select.tsx CHANGED
@@ -2,6 +2,7 @@ import * as React from "react";
2
2
  import * as SelectPrimitive from "@radix-ui/react-select";
3
3
  import { IconCheck, IconChevronDown, IconChevronUp } from "@tabler/icons-react";
4
4
  import { cn } from "../utils/cn";
5
+ import { fieldFocusClass } from "./_field-classes";
5
6
  import {
6
7
  floatingSurfaceAnimateClass,
7
8
  floatingSurfaceClass,
@@ -18,7 +19,8 @@ const SelectTrigger = React.forwardRef<
18
19
  <SelectPrimitive.Trigger
19
20
  ref={ref}
20
21
  className={cn(
21
- "flex h-10 w-full max-sm:min-w-0 items-center justify-between whitespace-nowrap rounded-field border border-border bg-field-background px-3.5 py-2 text-base sm:text-sm ring-offset-background placeholder:text-field-placeholder transition-[border-color,box-shadow,background-color] duration-200 ease-smooth focus-visible:border-focus-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
22
+ "flex h-10 w-full max-sm:min-w-0 items-center justify-between whitespace-nowrap rounded-field border border-border bg-field-background px-3.5 py-2 text-base sm:text-sm ring-offset-background placeholder:text-field-placeholder transition-[border-color,box-shadow,background-color] duration-200 ease-smooth disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
23
+ fieldFocusClass,
22
24
  className,
23
25
  )}
24
26
  {...props}
package/src/ui/switch.tsx CHANGED
@@ -8,7 +8,7 @@ const Switch = React.forwardRef<
8
8
  >(({ className, ...props }, ref) => (
9
9
  <SwitchPrimitive.Root
10
10
  className={cn(
11
- "peer relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-border bg-default transition-[background-color,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=unchecked]:bg-default before:absolute before:inset-[-8px] before:content-['']",
11
+ "peer relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-border bg-default transition-[background-color,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=unchecked]:bg-default dark:data-[state=unchecked]:border-muted/60 dark:data-[state=unchecked]:bg-muted/40 before:absolute before:inset-[-8px] before:content-['']",
12
12
  className,
13
13
  )}
14
14
  {...props}
@@ -16,7 +16,7 @@ const Switch = React.forwardRef<
16
16
  >
17
17
  <SwitchPrimitive.Thumb
18
18
  className={cn(
19
- "pointer-events-none block size-4 rounded-full bg-background ring-0 transition-[transform,background-color] duration-200 ease-smooth data-[state=checked]:translate-x-4 data-[state=checked]:bg-primary-foreground data-[state=unchecked]:translate-x-0",
19
+ "pointer-events-none block size-4 rounded-full bg-background ring-0 transition-[transform,background-color] duration-200 ease-smooth data-[state=checked]:translate-x-4 data-[state=checked]:bg-primary-foreground data-[state=unchecked]:translate-x-0 dark:data-[state=unchecked]:bg-foreground",
20
20
  )}
21
21
  />
22
22
  </SwitchPrimitive.Root>
@@ -1,5 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { cn } from "../utils/cn";
3
+ import { fieldFocusClass } from "./_field-classes";
3
4
 
4
5
  const Textarea = React.forwardRef<
5
6
  HTMLTextAreaElement,
@@ -8,7 +9,8 @@ const Textarea = React.forwardRef<
8
9
  return (
9
10
  <textarea
10
11
  className={cn(
11
- "flex min-h-20 w-full max-sm:min-w-0 rounded-field border border-border bg-field-background px-3.5 py-2.5 text-base sm:text-sm placeholder:text-field-placeholder focus-visible:border-focus-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50",
12
+ "flex min-h-20 w-full max-sm:min-w-0 rounded-field border border-border bg-field-background px-3.5 py-2.5 text-base sm:text-sm placeholder:text-field-placeholder disabled:cursor-not-allowed disabled:opacity-50",
13
+ fieldFocusClass,
12
14
  className,
13
15
  )}
14
16
  ref={ref}
@@ -1,8 +1,5 @@
1
1
  import * as React from "react";
2
- import { IconSearch, IconX } from "@tabler/icons-react";
3
- import { cn } from "../utils/cn";
4
- import { Button } from "./button";
5
- import { Input } from "./input";
2
+ import { SearchInput } from "./search-input";
6
3
 
7
4
  export interface ToolbarSearchInputProps {
8
5
  value: string;
@@ -17,64 +14,21 @@ export interface ToolbarSearchInputProps {
17
14
  inputClassName?: string;
18
15
  }
19
16
 
20
- /**
21
- * Stable rounded search field for toolbars: no enter or clear animations, no
22
- * layout jump. The clear button stays mounted and is hidden while the value is
23
- * blank. Clearing does not move focus. The ref points at the `<input>`.
24
- */
17
+ /** @deprecated Use `<SearchInput variant="toolbar">`. */
25
18
  export const ToolbarSearchInput = React.forwardRef<HTMLInputElement, ToolbarSearchInputProps>(
26
19
  function ToolbarSearchInput(
27
- {
28
- value,
29
- onChange,
30
- placeholder = "Search...",
31
- label = "Search",
32
- clearLabel = "Clear search",
33
- className,
34
- inputClassName,
35
- },
20
+ { placeholder = "Search...", className, inputClassName, ...props },
36
21
  ref,
37
22
  ) {
38
- const active = value.trim().length > 0;
39
-
40
23
  return (
41
- <div className={cn("relative min-w-0 flex-1 sm:flex-none", className)}>
42
- <IconSearch
43
- size={14}
44
- className="pointer-events-none absolute left-2.5 top-1/2 z-[1] -translate-y-1/2 text-muted-foreground"
45
- />
46
- <Input
47
- ref={ref}
48
- type="text"
49
- value={value}
50
- onChange={(event) => onChange(event.target.value)}
51
- placeholder={placeholder}
52
- aria-label={label}
53
- enterKeyHint="search"
54
- className={cn(
55
- // match outline toolbar buttons beside it
56
- "h-8 w-full min-w-0 rounded-lg border border-border bg-transparent pl-8 pr-8 text-sm shadow-none sm:w-44 md:w-52",
57
- "placeholder:text-muted-foreground/70 hover:bg-default",
58
- "focus-visible:border-border focus-visible:bg-default focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
59
- inputClassName,
60
- )}
61
- />
62
- <Button
63
- type="button"
64
- variant="ghost"
65
- size="icon-sm"
66
- aria-label={clearLabel}
67
- tabIndex={active ? 0 : -1}
68
- aria-hidden={!active}
69
- className={cn(
70
- "absolute right-0.5 top-1/2 z-[1] h-7 w-7 -translate-y-1/2 text-muted-foreground hover:text-foreground",
71
- !active && "pointer-events-none opacity-0",
72
- )}
73
- onClick={() => onChange("")}
74
- >
75
- <IconX size={14} stroke={1.75} />
76
- </Button>
77
- </div>
24
+ <SearchInput
25
+ ref={ref}
26
+ variant="toolbar"
27
+ placeholder={placeholder}
28
+ wrapperClassName={className}
29
+ className={inputClassName}
30
+ {...props}
31
+ />
78
32
  );
79
33
  },
80
34
  );