@vvedantb/ui 0.16.4 → 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 +8 -4
- package/package.json +2 -2
- package/src/artifacts/artifact-list-toolbar.tsx +3 -2
- package/src/composer/Composer.tsx +1 -1
- package/src/index.ts +1 -0
- package/src/ui/_field-classes.ts +7 -0
- package/src/ui/_picker-classes.ts +3 -1
- package/src/ui/calendar.tsx +1 -1
- package/src/ui/clear-input.alt.tsx +3 -1
- package/src/ui/clear-input.tsx +3 -1
- package/src/ui/input-group.alt.tsx +2 -1
- package/src/ui/input-group.tsx +1 -1
- package/src/ui/input.tsx +3 -1
- package/src/ui/radix-select.tsx +3 -1
- package/src/ui/search-input.tsx +62 -7
- package/src/ui/select.tsx +3 -1
- package/src/ui/textarea.tsx +3 -1
- package/src/ui/toolbar-search-input.tsx +11 -57
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,
|
|
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
|
-
<
|
|
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
|
-
- `
|
|
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 `
|
|
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.
|
|
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.
|
|
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 {
|
|
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
|
-
<
|
|
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-
|
|
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
|
-
|
|
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";
|
package/src/ui/calendar.tsx
CHANGED
|
@@ -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-
|
|
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
|
|
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
|
)}
|
package/src/ui/clear-input.tsx
CHANGED
|
@@ -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
|
|
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
|
-
|
|
23
|
+
fieldFocusWithinClass,
|
|
23
24
|
error &&
|
|
24
25
|
"border-danger-border has-focus-visible:border-danger has-focus-visible:ring-danger-ring",
|
|
25
26
|
className,
|
package/src/ui/input-group.tsx
CHANGED
|
@@ -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-
|
|
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
|
|
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}
|
package/src/ui/radix-select.tsx
CHANGED
|
@@ -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
|
|
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}
|
package/src/ui/search-input.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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/textarea.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 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
|
|
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 {
|
|
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
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
);
|