azamat-ui-kit-cli 0.3.13 → 0.3.14

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.
Files changed (30) hide show
  1. package/dist/index.cjs +807 -107
  2. package/package.json +1 -1
  3. package/vendor/src/components/data-table/data-table-pagination.tsx +1 -1
  4. package/vendor/src/components/data-table/data-table-toolbar.tsx +13 -12
  5. package/vendor/src/components/data-table/data-table.tsx +14 -14
  6. package/vendor/src/components/display/smart-card.tsx +17 -14
  7. package/vendor/src/components/form/form-input.tsx +3 -1
  8. package/vendor/src/components/form/form-textarea.tsx +15 -12
  9. package/vendor/src/components/inputs/async-select.tsx +106 -47
  10. package/vendor/src/components/inputs/clearable-input.tsx +1 -0
  11. package/vendor/src/components/inputs/input-chrome.tsx +1 -1
  12. package/vendor/src/components/inputs/input-decorator.tsx +16 -8
  13. package/vendor/src/components/inputs/simple-select.tsx +28 -28
  14. package/vendor/src/components/layout/app-sidebar.tsx +454 -154
  15. package/vendor/src/components/layout/breadcrumbs.tsx +67 -22
  16. package/vendor/src/components/layout/sidebar-nav.tsx +316 -128
  17. package/vendor/src/components/overlay/confirm-dialog.tsx +31 -20
  18. package/vendor/src/components/ui/badge.tsx +33 -32
  19. package/vendor/src/components/ui/button.tsx +15 -17
  20. package/vendor/src/components/ui/card.tsx +26 -25
  21. package/vendor/src/components/ui/dialog.tsx +6 -3
  22. package/vendor/src/components/ui/dropdown-menu.tsx +9 -9
  23. package/vendor/src/components/ui/input-primitive.tsx +1 -1
  24. package/vendor/src/components/ui/input.tsx +105 -2
  25. package/vendor/src/components/ui/popover.tsx +1 -1
  26. package/vendor/src/components/ui/select.tsx +3 -3
  27. package/vendor/src/components/ui/table.tsx +4 -4
  28. package/vendor/src/components/ui/tabs.tsx +2 -2
  29. package/vendor/src/families/member-metadata.ts +3 -3
  30. package/vendor/templates/styles/globals.css +706 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azamat-ui-kit-cli",
3
- "version": "0.3.13",
3
+ "version": "0.3.14",
4
4
  "description": "CLI for Azamat UI Kit source-copy, presets, and theme setup.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,7 +49,7 @@ function DataTablePagination({
49
49
  <div
50
50
  data-slot="data-table-pagination"
51
51
  className={cn(
52
- "flex flex-col gap-3 border-t border-border/70 bg-muted/[0.18] px-4 py-3 sm:flex-row sm:items-center sm:justify-between",
52
+ "flex flex-col gap-3 border-t px-4 py-3 sm:flex-row sm:items-center sm:justify-between",
53
53
  className
54
54
  )}
55
55
  {...props}
@@ -3,12 +3,12 @@ import { cva, type VariantProps } from "class-variance-authority"
3
3
 
4
4
  import { cn } from "@/lib/utils"
5
5
 
6
- const dataTableToolbarVariants = cva("flex flex-col", {
6
+ const dataTableToolbarVariants = cva("flex flex-col", {
7
7
  variants: {
8
8
  variant: {
9
- default: "rounded-[var(--radius-2xl)] border border-border/70 bg-card/72 shadow-sm ring-1 ring-foreground/5",
9
+ default: "",
10
10
  plain: "border-transparent bg-transparent shadow-none",
11
- soft: "rounded-[var(--radius-2xl)] border border-transparent bg-muted/32 shadow-none",
11
+ soft: "",
12
12
  },
13
13
  density: {
14
14
  compact: "gap-3 p-3",
@@ -62,10 +62,11 @@ function DataTableToolbar({
62
62
  const hasSelection = selectedCount > 0 && Boolean(selectionActions)
63
63
 
64
64
  return (
65
- <div
66
- data-slot="data-table-toolbar"
67
- data-density={density ?? "default"}
68
- className={cn(dataTableToolbarVariants({ variant, density }), className)}
65
+ <div
66
+ data-slot="data-table-toolbar"
67
+ data-variant={variant ?? "plain"}
68
+ data-density={density ?? "default"}
69
+ className={cn(dataTableToolbarVariants({ variant, density }), className)}
69
70
  {...props}
70
71
  >
71
72
  {(hasHeading || actions) && (
@@ -89,11 +90,11 @@ function DataTableToolbar({
89
90
  {children}
90
91
  </div>
91
92
 
92
- {hasSelection && (
93
- <div className="flex shrink-0 items-center gap-2 rounded-full border border-border/75 bg-background/92 px-2.5 py-1.5 text-sm shadow-[0_1px_0_rgba(255,255,255,0.08)] backdrop-blur">
94
- <span className="text-muted-foreground">{selectedLabel(selectedCount, totalCount)}</span>
95
- {selectionActions}
96
- </div>
93
+ {hasSelection && (
94
+ <div data-slot="data-table-selection-bar" className="flex shrink-0 items-center gap-2 rounded-full border px-2.5 py-1.5 text-sm backdrop-blur">
95
+ <span className="text-muted-foreground">{selectedLabel(selectedCount, totalCount)}</span>
96
+ {selectionActions}
97
+ </div>
97
98
  )}
98
99
  </div>
99
100
  )}
@@ -225,16 +225,17 @@ function DataTable<TData, TValue = unknown>({
225
225
  rowClassName,
226
226
  ...props
227
227
  }: DataTableProps<TData, TValue>) {
228
- const resolvedColumns = React.useMemo<ColumnDef<TData, TValue | unknown>[]>(() => {
229
- if (!rowActions || features?.rowActions === false) return columns
230
-
231
- return [
232
- ...columns,
233
- createDataTableActionsColumn<TData>({
234
- getActions: rowActions,
235
- }) as ColumnDef<TData, TValue | unknown>,
236
- ]
237
- }, [columns, features?.rowActions, rowActions])
228
+ const resolvedColumns = React.useMemo<ColumnDef<TData, unknown>[]>(() => {
229
+ const baseColumns = columns as ColumnDef<TData, unknown>[]
230
+ if (!rowActions || features?.rowActions === false) return baseColumns
231
+
232
+ return [
233
+ ...baseColumns,
234
+ createDataTableActionsColumn<TData>({
235
+ getActions: rowActions,
236
+ }) as ColumnDef<TData, unknown>,
237
+ ]
238
+ }, [columns, features?.rowActions, rowActions])
238
239
 
239
240
  const paginationConfig = pagination === false ? undefined : pagination
240
241
  const controlledPagination = paginationConfig
@@ -408,7 +409,7 @@ function DataTable<TData, TValue = unknown>({
408
409
  data-striped={striped || undefined}
409
410
  data-bordered={bordered || undefined}
410
411
  className={cn(
411
- "overflow-hidden rounded-[var(--radius-2xl)] border border-border/70 bg-[linear-gradient(180deg,color-mix(in_oklch,var(--card),white_8%),var(--card))] shadow-sm ring-1 ring-foreground/5 backdrop-blur",
412
+ "overflow-hidden rounded-[var(--radius-2xl)] border backdrop-blur",
412
413
  !bordered && "border-border",
413
414
  renderMobileCard && "hidden md:block",
414
415
  tableWrapperClassName
@@ -419,7 +420,7 @@ function DataTable<TData, TValue = unknown>({
419
420
  className={cn("text-[0.95rem]", tableClassName)}
420
421
  >
421
422
  <TableHeader
422
- className={cn(stickyHeader && "sticky top-0 z-10 bg-[linear-gradient(180deg,color-mix(in_oklch,var(--background),white_5%),var(--background))] shadow-sm backdrop-blur")}
423
+ className={cn(stickyHeader && "sticky top-0 z-10 shadow-sm backdrop-blur")}
423
424
  >
424
425
  {table.getHeaderGroups().map((headerGroup) => (
425
426
  <TableRow key={headerGroup.id}>
@@ -430,7 +431,7 @@ function DataTable<TData, TValue = unknown>({
430
431
  className={cn(
431
432
  densityHeadClassName[density],
432
433
  "text-muted-foreground",
433
- stickyHeader && "bg-[linear-gradient(180deg,color-mix(in_oklch,var(--background),white_5%),var(--background))] backdrop-blur",
434
+ stickyHeader && "backdrop-blur",
434
435
  bordered && "border-r last:border-r-0",
435
436
  getHeaderCellClassName(header, headerCellClassName)
436
437
  )}
@@ -460,7 +461,6 @@ function DataTable<TData, TValue = unknown>({
460
461
  className={cn(
461
462
  onRowClick && !rowDisabled && "cursor-pointer",
462
463
  !rowDisabled && "transition-colors",
463
- striped && rowIndex % 2 === 1 && "bg-muted/20",
464
464
  rowDisabled && "pointer-events-none opacity-55",
465
465
  getRowClassName(row, rowClassName)
466
466
  )}
@@ -47,11 +47,12 @@ export type SmartCardRenderContext = {
47
47
  /** @deprecated Prefer `InfoCardProps` via `InfoCard` or `CardFamily.Info` for new public usage. */
48
48
  export type SmartCardProps = Omit<React.ComponentProps<typeof Card>, "title" | "content" | "size"> & SmartCardRenderContext & {
49
49
  orientation?: SmartCardOrientation
50
- variant?: SmartCardVariant
51
- size?: SmartCardSize
52
- density?: SmartCardDensity
53
- loading?: boolean
54
- disabled?: boolean
50
+ variant?: SmartCardVariant
51
+ size?: SmartCardSize
52
+ density?: SmartCardDensity
53
+ compact?: boolean
54
+ loading?: boolean
55
+ disabled?: boolean
55
56
  selected?: boolean
56
57
  interactive?: boolean
57
58
  classNames?: SmartCardClassNames
@@ -93,10 +94,11 @@ function SmartCard({
93
94
  content,
94
95
  footer,
95
96
  orientation = "vertical",
96
- variant = "outline",
97
- size = "md",
98
- density = "default",
99
- loading = false,
97
+ variant = "outline",
98
+ size = "md",
99
+ density,
100
+ compact,
101
+ loading = false,
100
102
  disabled = false,
101
103
  selected = false,
102
104
  interactive,
@@ -109,9 +111,10 @@ function SmartCard({
109
111
  children,
110
112
  onClick,
111
113
  ...props
112
- }: SmartCardProps) {
113
- const ctx: SmartCardRenderContext = { eyebrow, title, description, media, icon, status, actions, meta, content, footer }
114
- const clickable = Boolean(onClick || interactive)
114
+ }: SmartCardProps) {
115
+ const ctx: SmartCardRenderContext = { eyebrow, title, description, media, icon, status, actions, meta, content, footer }
116
+ const clickable = Boolean(onClick || interactive)
117
+ const resolvedDensity = compact ? "compact" : density ?? "default"
115
118
 
116
119
  return (
117
120
  <Card
@@ -131,7 +134,7 @@ function SmartCard({
131
134
  {...props}
132
135
  >
133
136
  {loading ? (
134
- <div className={cn("grid gap-3", densityClassName[density])}>
137
+ <div className={cn("grid gap-3", densityClassName[resolvedDensity])}>
135
138
  <Skeleton className="h-5 w-1/2" />
136
139
  <SkeletonText rows={3} />
137
140
  </div>
@@ -150,7 +153,7 @@ function SmartCard({
150
153
  {media}
151
154
  </div>
152
155
  ))}
153
- <div data-slot="smart-card-body" className={cn("grid min-w-0 flex-1 gap-3", densityClassName[density], classNames?.body)}>
156
+ <div data-slot="smart-card-body" className={cn("grid min-w-0 flex-1 gap-3", densityClassName[resolvedDensity], classNames?.body)}>
154
157
  {renderHeader?.(ctx) ?? (
155
158
  <div data-slot="smart-card-header" className={cn("flex items-start justify-between gap-3", classNames?.header)}>
156
159
  <div className="flex min-w-0 items-start gap-3">
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-unused-vars */
2
2
  import * as React from "react"
3
- import { Controller, type Control, type FieldPath, type FieldValues } from "react-hook-form"
3
+ import { Controller, type Control, type FieldPath, type FieldValues, type RegisterOptions } from "react-hook-form"
4
4
 
5
5
  import {
6
6
  DateRangeInput,
@@ -57,6 +57,7 @@ type FormControlledFieldProps<
57
57
  > = FormFieldShellControlProps & {
58
58
  control: Control<TFieldValues>
59
59
  name: TName
60
+ rules?: RegisterOptions<TFieldValues, TName>
60
61
  fieldClassName?: string
61
62
  id?: string
62
63
  }
@@ -226,6 +227,7 @@ function FormInput<
226
227
  <Controller
227
228
  control={props.control}
228
229
  name={props.name}
230
+ rules={props.rules}
229
231
  render={({ field, fieldState }) => {
230
232
  const error = fieldState.error?.message
231
233
 
@@ -1,5 +1,5 @@
1
1
  import * as React from "react"
2
- import { Controller, type Control, type FieldPath, type FieldValues } from "react-hook-form"
2
+ import { Controller, type Control, type FieldPath, type FieldValues, type RegisterOptions } from "react-hook-form"
3
3
 
4
4
  import { FormFieldShell, type FormFieldShellControlProps } from "@/components/form/form-field-shell"
5
5
  import { Textarea } from "@/components/ui/textarea"
@@ -9,10 +9,11 @@ export type FormTextareaProps<
9
9
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
10
10
  > = Omit<React.ComponentProps<typeof Textarea>, "name" | "value" | "defaultValue"> &
11
11
  FormFieldShellControlProps & {
12
- control: Control<TFieldValues>
13
- name: TName
14
- fieldClassName?: string
15
- transformIn?: (value: unknown) => string | number | readonly string[] | undefined
12
+ control: Control<TFieldValues>
13
+ name: TName
14
+ rules?: RegisterOptions<TFieldValues, TName>
15
+ fieldClassName?: string
16
+ transformIn?: (value: unknown) => string | number | readonly string[] | undefined
16
17
  transformOut?: (event: React.ChangeEvent<HTMLTextAreaElement>) => unknown
17
18
  }
18
19
 
@@ -20,9 +21,10 @@ function FormTextarea<
20
21
  TFieldValues extends FieldValues = FieldValues,
21
22
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
22
23
  >({
23
- control,
24
- name,
25
- label,
24
+ control,
25
+ name,
26
+ rules,
27
+ label,
26
28
  description,
27
29
  required,
28
30
  className,
@@ -50,10 +52,11 @@ function FormTextarea<
50
52
  const textareaId = id ?? name
51
53
 
52
54
  return (
53
- <Controller
54
- control={control}
55
- name={name}
56
- render={({ field, fieldState }) => (
55
+ <Controller
56
+ control={control}
57
+ name={name}
58
+ rules={rules}
59
+ render={({ field, fieldState }) => (
57
60
  <FormFieldShell
58
61
  label={label}
59
62
  description={description}
@@ -282,7 +282,7 @@ function getCachedOptionGroups<
282
282
  return entry.groups
283
283
  }
284
284
 
285
- function setCachedOptionGroups<
285
+ function setCachedOptionGroups<
286
286
  TValue extends string,
287
287
  TData,
288
288
  TOption extends AsyncSelectOption<TValue, TData>,
@@ -291,11 +291,15 @@ function setCachedOptionGroups<
291
291
  key: string,
292
292
  groups: AsyncSelectOptionGroup<TValue, TData, TOption>[]
293
293
  ) {
294
- cache.set(key, {
295
- groups,
296
- createdAt: Date.now(),
297
- })
298
- }
294
+ cache.set(key, {
295
+ groups,
296
+ createdAt: Date.now(),
297
+ })
298
+ }
299
+
300
+ function normalizeAsyncSearch(search: string) {
301
+ return search.trim().toLowerCase()
302
+ }
299
303
 
300
304
  function AsyncStateMessage({
301
305
  className,
@@ -306,8 +310,9 @@ function AsyncStateMessage({
306
310
  }) {
307
311
  return (
308
312
  <div
313
+ data-slot="async-select-state"
309
314
  className={cn(
310
- "rounded-[min(var(--radius-xl),16px)] border border-border/70 bg-[linear-gradient(180deg,color-mix(in_oklch,var(--muted),white_10%),color-mix(in_oklch,var(--muted),transparent_8%))] px-3 py-3 text-sm text-muted-foreground shadow-[inset_0_1px_0_rgba(255,255,255,0.08)]",
315
+ "rounded-[min(var(--radius-xl),16px)] border px-3 py-3 text-sm text-muted-foreground",
311
316
  className
312
317
  )}
313
318
  >
@@ -342,12 +347,14 @@ function AsyncOptionButton<
342
347
  onSelect: (option: TOption) => void
343
348
  }) {
344
349
  return (
345
- <button
350
+ <button
346
351
  type="button"
347
352
  disabled={option.disabled}
353
+ data-slot="async-select-option"
354
+ data-selected={selected || undefined}
355
+ data-disabled={option.disabled || undefined}
348
356
  className={cn(
349
- "flex w-full items-start gap-2 rounded-[min(var(--radius-xl),16px)] border border-transparent px-3 py-2.5 text-left text-sm outline-none transition-[background-color,border-color,color,box-shadow] hover:border-border/70 hover:bg-accent/60 hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50",
350
- selected && "border-primary/18 bg-primary/8 text-foreground shadow-[inset_0_1px_0_rgba(255,255,255,0.12)]",
357
+ "flex w-full items-start gap-2 rounded-[min(var(--radius-xl),16px)] border border-transparent px-3 py-2.5 text-left text-sm outline-none transition-[background-color,border-color,color,box-shadow] disabled:pointer-events-none disabled:opacity-50",
351
358
  optionClassName
352
359
  )}
353
360
  onClick={() => onSelect(option)}
@@ -387,7 +394,8 @@ function AsyncCreateButton({
387
394
  <button
388
395
  type="button"
389
396
  disabled={isCreating}
390
- className="flex w-full items-center gap-2 rounded-[min(var(--radius-xl),16px)] border border-dashed border-border/80 px-3 py-2.5 text-left text-sm outline-none transition-[background-color,border-color,color,box-shadow] hover:border-primary/25 hover:bg-primary/6 hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
397
+ data-slot="async-select-create"
398
+ className="flex w-full items-center gap-2 rounded-[min(var(--radius-xl),16px)] border border-dashed px-3 py-2.5 text-left text-sm outline-none transition-[background-color,border-color,color,box-shadow] disabled:pointer-events-none disabled:opacity-50"
391
399
  onClick={onCreate}
392
400
  >
393
401
  <span className="flex size-4 shrink-0 items-center justify-center">
@@ -462,10 +470,14 @@ function AsyncSelect<
462
470
  )
463
471
  const canClear = clearable && Boolean(value) && !disabled
464
472
  const searchTooShort = searchKey.length < minSearchLength
465
- const canCreate =
466
- !searchTooShort &&
467
- Boolean(onCreateOption) &&
468
- (showCreateOption ?? defaultShowCreateOption)(search, flatOptions)
473
+ const canCreate =
474
+ !searchTooShort &&
475
+ Boolean(onCreateOption) &&
476
+ (showCreateOption ?? defaultShowCreateOption)(search, flatOptions)
477
+
478
+ React.useEffect(() => {
479
+ cacheRef.current.clear()
480
+ }, [loadOptions])
469
481
 
470
482
  React.useEffect(() => {
471
483
  setOptionGroups(resolvedDefaultGroups)
@@ -561,17 +573,36 @@ function AsyncSelect<
561
573
  event.stopPropagation()
562
574
  clearSelection()
563
575
  }
576
+
577
+ const handleTriggerKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
578
+ if (disabled) return
579
+
580
+ if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
581
+ event.preventDefault()
582
+ setOpen(true)
583
+ return
584
+ }
585
+
586
+ if ((event.key === "Backspace" || event.key === "Delete") && value) {
587
+ event.preventDefault()
588
+ clearSelection()
589
+ }
590
+ }
564
591
 
565
592
  const handleCreate = async () => {
566
593
  if (!onCreateOption || !search.trim() || searchTooShort) return
567
594
 
568
595
  setIsCreating(true)
569
596
 
570
- try {
571
- const createdOption = await onCreateOption(search.trim())
572
- setOptionGroups((previousGroups) => [{ options: [createdOption] }, ...previousGroups])
573
- onValueChange?.(createdOption.value, createdOption)
574
- setSearch("")
597
+ try {
598
+ const createdOption = await onCreateOption(search.trim())
599
+ const normalizedSearch = normalizeAsyncSearch(search)
600
+ setOptionGroups((previousGroups) => [{ options: [createdOption] }, ...previousGroups])
601
+ if (cacheOptions && normalizedSearch) {
602
+ setCachedOptionGroups(cacheRef.current, normalizedSearch, [{ options: [createdOption] }])
603
+ }
604
+ onValueChange?.(createdOption.value, createdOption)
605
+ setSearch("")
575
606
  setOpen(false)
576
607
  } finally {
577
608
  setIsCreating(false)
@@ -583,15 +614,17 @@ function AsyncSelect<
583
614
  <Popover open={open} onOpenChange={setOpen}>
584
615
  <PopoverTrigger
585
616
  render={
586
- <Button
617
+ <Button
587
618
  type="button"
588
619
  variant="outline"
589
620
  disabled={disabled}
590
621
  aria-expanded={open}
622
+ data-slot="async-select-trigger"
591
623
  className={cn(
592
- "min-h-11 w-full justify-between border-border/80 bg-[linear-gradient(180deg,color-mix(in_oklch,var(--background),white_12%),var(--background))] shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_1px_0_rgba(255,255,255,0.06)]",
624
+ "min-h-11 w-full justify-between",
593
625
  triggerClassName
594
626
  )}
627
+ onKeyDown={handleTriggerKeyDown}
595
628
  />
596
629
  }
597
630
  >
@@ -632,18 +665,20 @@ function AsyncSelect<
632
665
  </PopoverTrigger>
633
666
  <PopoverContent
634
667
  align="start"
668
+ data-slot="async-select-content"
635
669
  className={cn(
636
- "w-(--anchor-width) gap-3 rounded-[var(--radius-2xl)] border-border/80 bg-[linear-gradient(180deg,color-mix(in_oklch,var(--popover),white_10%),var(--popover))] p-3.5 shadow-[0_24px_80px_rgba(15,23,42,0.18)] backdrop-blur",
670
+ "w-(--anchor-width) gap-3 rounded-[var(--radius-2xl)] p-3.5",
637
671
  contentClassName
638
672
  )}
639
673
  >
640
- <div className="relative">
674
+ <div data-slot="async-select-search-wrap" className="relative">
641
675
  <SearchIcon className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
642
676
  <Input
677
+ data-slot="async-select-search"
643
678
  value={search}
644
679
  onChange={(event) => setSearch(event.target.value)}
645
680
  placeholder={labels?.searchPlaceholder ?? "Search..."}
646
- className={cn("border-border/75 bg-[linear-gradient(180deg,color-mix(in_oklch,var(--background),white_12%),var(--background))] pl-8 shadow-[inset_0_1px_0_rgba(255,255,255,0.12),0_1px_0_rgba(255,255,255,0.05)]", searchClassName)}
681
+ className={cn("pl-8", searchClassName)}
647
682
  />
648
683
  </div>
649
684
 
@@ -795,7 +830,11 @@ function AsyncMultiSelect<
795
830
  (showCreateOption ?? defaultShowCreateOption)(search, flatOptions)
796
831
  const visibleSelectableOptions = flatOptions.filter((option) => !option.disabled)
797
832
  const unselectedVisibleOptions = visibleSelectableOptions.filter((option) => !selectedSet.has(option.value))
798
- const canSelectAll = showSelectAll && unselectedVisibleOptions.length > 0 && !isMaxReached
833
+ const canSelectAll = showSelectAll && unselectedVisibleOptions.length > 0 && !isMaxReached
834
+
835
+ React.useEffect(() => {
836
+ cacheRef.current.clear()
837
+ }, [loadOptions])
799
838
 
800
839
  React.useEffect(() => {
801
840
  setOptionGroups(resolvedDefaultGroups)
@@ -907,7 +946,7 @@ function AsyncMultiSelect<
907
946
  }
908
947
 
909
948
  const handleTagRemoveKeyDown = (event: React.KeyboardEvent<HTMLElement>, option: TOption) => {
910
- if (event.key !== "Enter" && event.key !== " ") return
949
+ if (event.key !== "Enter" && event.key !== " " && event.key !== "Backspace" && event.key !== "Delete") return
911
950
 
912
951
  event.preventDefault()
913
952
  event.stopPropagation()
@@ -915,7 +954,15 @@ function AsyncMultiSelect<
915
954
  }
916
955
 
917
956
  const handleTriggerKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
918
- if (disabled || search.length > 0 || values.length === 0) return
957
+ if (disabled) return
958
+
959
+ if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
960
+ event.preventDefault()
961
+ setOpen(true)
962
+ return
963
+ }
964
+
965
+ if (search.length > 0 || values.length === 0) return
919
966
 
920
967
  if (event.key === "Backspace" || event.key === "Delete") {
921
968
  event.preventDefault()
@@ -953,17 +1000,21 @@ function AsyncMultiSelect<
953
1000
 
954
1001
  setIsCreating(true)
955
1002
 
956
- try {
957
- const createdOption = await onCreateOption(search.trim())
958
- const nextKnownOptions = mergeUniqueOptions(allKnownOptions, [createdOption])
959
- const nextValues = selectedSet.has(createdOption.value)
960
- ? values
1003
+ try {
1004
+ const createdOption = await onCreateOption(search.trim())
1005
+ const normalizedSearch = normalizeAsyncSearch(search)
1006
+ const nextKnownOptions = mergeUniqueOptions(allKnownOptions, [createdOption])
1007
+ const nextValues = selectedSet.has(createdOption.value)
1008
+ ? values
961
1009
  : isMaxReached
962
1010
  ? values
963
1011
  : [...values, createdOption.value]
964
-
965
- setOptionGroups((previousGroups) => [{ options: [createdOption] }, ...previousGroups])
966
- emitChange(nextValues, nextKnownOptions)
1012
+
1013
+ setOptionGroups((previousGroups) => [{ options: [createdOption] }, ...previousGroups])
1014
+ if (cacheOptions && normalizedSearch) {
1015
+ setCachedOptionGroups(cacheRef.current, normalizedSearch, [{ options: [createdOption] }])
1016
+ }
1017
+ emitChange(nextValues, nextKnownOptions)
967
1018
  setSearch("")
968
1019
 
969
1020
  if (closeOnSelect) {
@@ -984,8 +1035,9 @@ function AsyncMultiSelect<
984
1035
  variant="outline"
985
1036
  disabled={disabled}
986
1037
  aria-expanded={open}
1038
+ data-slot="async-select-trigger"
987
1039
  className={cn(
988
- "min-h-11 w-full justify-between border-border/80 bg-[linear-gradient(180deg,color-mix(in_oklch,var(--background),white_12%),var(--background))] shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_1px_0_rgba(255,255,255,0.06)]",
1040
+ "min-h-11 w-full justify-between",
989
1041
  triggerClassName
990
1042
  )}
991
1043
  onKeyDown={handleTriggerKeyDown}
@@ -995,10 +1047,11 @@ function AsyncMultiSelect<
995
1047
  <span className="flex min-w-0 flex-1 flex-wrap gap-1 text-left">
996
1048
  {currentOptions.length > 0 ? (
997
1049
  currentOptions.map((option) => (
998
- <span
1050
+ <span
999
1051
  key={option.value}
1052
+ data-slot="async-select-tag"
1000
1053
  className={cn(
1001
- "inline-flex max-w-full items-center gap-1 rounded-full border border-border/70 bg-[linear-gradient(180deg,color-mix(in_oklch,var(--muted),white_10%),color-mix(in_oklch,var(--muted),transparent_8%))] px-2 py-1 text-xs text-foreground shadow-[0_1px_0_rgba(255,255,255,0.04)]",
1054
+ "inline-flex max-w-full items-center gap-1 rounded-full border px-2 py-1 text-xs text-foreground",
1002
1055
  tagClassName
1003
1056
  )}
1004
1057
  >
@@ -1016,6 +1069,7 @@ function AsyncMultiSelect<
1016
1069
  <span
1017
1070
  role="button"
1018
1071
  tabIndex={0}
1072
+ data-slot="async-select-tag-remove"
1019
1073
  className="rounded-full text-muted-foreground transition-colors hover:text-foreground"
1020
1074
  aria-label={`Remove ${getOptionLabelText(option)}`}
1021
1075
  onClick={(event) => {
@@ -1040,7 +1094,8 @@ function AsyncMultiSelect<
1040
1094
  <span
1041
1095
  role="button"
1042
1096
  tabIndex={0}
1043
- className="rounded-full border border-border/65 p-1 text-muted-foreground transition-colors hover:border-border hover:bg-muted/55 hover:text-foreground"
1097
+ data-slot="async-select-clear"
1098
+ className="rounded-full border p-1 text-muted-foreground transition-colors hover:text-foreground"
1044
1099
  aria-label={labels?.clearAll ?? labels?.clear ?? "Clear all"}
1045
1100
  onClick={handleClear}
1046
1101
  onKeyDown={(event) => {
@@ -1058,22 +1113,24 @@ function AsyncMultiSelect<
1058
1113
  </PopoverTrigger>
1059
1114
  <PopoverContent
1060
1115
  align="start"
1116
+ data-slot="async-select-content"
1061
1117
  className={cn(
1062
- "w-(--anchor-width) gap-3 rounded-[var(--radius-2xl)] border-border/80 bg-[linear-gradient(180deg,color-mix(in_oklch,var(--popover),white_10%),var(--popover))] p-3.5 shadow-[0_24px_80px_rgba(15,23,42,0.18)] backdrop-blur",
1118
+ "w-(--anchor-width) gap-3 rounded-[var(--radius-2xl)] p-3.5",
1063
1119
  contentClassName
1064
1120
  )}
1065
1121
  >
1066
- <div className="relative">
1122
+ <div data-slot="async-select-search-wrap" className="relative">
1067
1123
  <SearchIcon className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
1068
1124
  <Input
1125
+ data-slot="async-select-search"
1069
1126
  value={search}
1070
1127
  onChange={(event) => setSearch(event.target.value)}
1071
1128
  placeholder={labels?.searchPlaceholder ?? "Search..."}
1072
- className={cn("border-border/75 bg-[linear-gradient(180deg,color-mix(in_oklch,var(--background),white_12%),var(--background))] pl-8 shadow-[inset_0_1px_0_rgba(255,255,255,0.12),0_1px_0_rgba(255,255,255,0.05)]", searchClassName)}
1129
+ className={cn("pl-8", searchClassName)}
1073
1130
  />
1074
1131
  </div>
1075
1132
 
1076
- <div className="flex flex-wrap items-center justify-between gap-2 rounded-[min(var(--radius-xl),16px)] border border-border/70 bg-[linear-gradient(180deg,color-mix(in_oklch,var(--muted),white_10%),color-mix(in_oklch,var(--muted),transparent_8%))] px-3 py-2 text-xs text-muted-foreground">
1133
+ <div data-slot="async-select-meta" className="flex flex-wrap items-center justify-between gap-2 rounded-[min(var(--radius-xl),16px)] border px-3 py-2 text-xs text-muted-foreground">
1077
1134
  {hasValue && labels?.selectedCount && <span>{labels.selectedCount(values.length)}</span>}
1078
1135
  {isMaxReached &&
1079
1136
  (renderMaxSelected?.(state) ?? (
@@ -1083,6 +1140,7 @@ function AsyncMultiSelect<
1083
1140
  {canSelectAll && (
1084
1141
  <button
1085
1142
  type="button"
1143
+ data-slot="async-select-meta-action"
1086
1144
  className="rounded-full border border-border/75 px-2.5 py-1 font-medium text-foreground transition-colors hover:bg-background"
1087
1145
  onClick={handleSelectAllVisible}
1088
1146
  >
@@ -1092,6 +1150,7 @@ function AsyncMultiSelect<
1092
1150
  {canClear && (
1093
1151
  <button
1094
1152
  type="button"
1153
+ data-slot="async-select-meta-action"
1095
1154
  className="rounded-full border border-border/75 px-2.5 py-1 font-medium text-foreground transition-colors hover:bg-background"
1096
1155
  onClick={() => onValueChange?.([], [])}
1097
1156
  >
@@ -1146,9 +1205,9 @@ function AsyncMultiSelect<
1146
1205
  optionGroups.map((group, groupIndex) => (
1147
1206
  <div key={groupIndex}>
1148
1207
  {group.label && (
1149
- <div className="sticky top-0 z-10 bg-popover px-2 py-1 text-xs font-medium text-muted-foreground">
1150
- {group.label}
1151
- </div>
1208
+ <div className="sticky top-0 z-10 bg-popover px-2 py-1 text-xs font-medium text-muted-foreground">
1209
+ <span data-slot="async-select-group-label">{group.label}</span>
1210
+ </div>
1152
1211
  )}
1153
1212
  {group.options.map((option) => (
1154
1213
  <AsyncOptionButton
@@ -79,6 +79,7 @@ const ClearableInput = React.forwardRef<HTMLInputElement, ClearableInputProps>(
79
79
  {canClear && (
80
80
  <button
81
81
  type="button"
82
+ data-slot="clearable-input-clear"
82
83
  aria-label={clearLabel}
83
84
  className="inline-flex size-7 items-center justify-center rounded-full border border-transparent bg-transparent text-muted-foreground/74 transition hover:border-border/60 hover:bg-muted/58 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
84
85
  onClick={clearValue}
@@ -22,7 +22,7 @@ function InputChrome({
22
22
  <div
23
23
  data-slot="input-chrome"
24
24
  className={cn(
25
- "flex h-8 w-full min-w-0 items-center rounded-lg border border-input bg-transparent transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50",
25
+ "flex h-10 w-full min-w-0 items-center rounded-[var(--aui-control-radius,var(--radius-lg))] border border-[color:var(--aui-control-border-strong,var(--input))] bg-[color:var(--aui-control-surface,var(--background))] shadow-[var(--aui-control-shadow,none)] transition-[background-color,border-color,box-shadow] hover:border-[color:var(--aui-control-hover-border,var(--ring))] hover:bg-[color:var(--aui-control-surface-hover,var(--background))] focus-within:border-[color:var(--ring)] focus-within:shadow-[var(--aui-control-shadow,none),0_0_0_1px_var(--aui-focus-ring,var(--ring)),0_0_0_5px_var(--aui-focus-ring-soft,transparent)]",
26
26
  className
27
27
  )}
28
28
  {...props}