azamat-ui-kit-cli 0.2.2 → 0.3.3

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 (103) hide show
  1. package/README.md +11 -0
  2. package/dist/index.cjs +452 -0
  3. package/package.json +2 -2
  4. package/vendor/src/components/actions/action-menu.tsx +21 -18
  5. package/vendor/src/components/calendar/calendar.tsx +153 -102
  6. package/vendor/src/components/calendar/date-picker.tsx +24 -14
  7. package/vendor/src/components/calendar/date-range-picker.tsx +137 -58
  8. package/vendor/src/components/charts/charts.tsx +32 -21
  9. package/vendor/src/components/command/command-palette.tsx +68 -57
  10. package/vendor/src/components/data-table/data-table-bulk-actions.tsx +23 -20
  11. package/vendor/src/components/data-table/data-table-column-visibility-menu.tsx +21 -10
  12. package/vendor/src/components/data-table/data-table-pagination.tsx +6 -6
  13. package/vendor/src/components/data-table/data-table-toolbar.tsx +72 -44
  14. package/vendor/src/components/data-table/data-table.tsx +15 -11
  15. package/vendor/src/components/data-table/table-export-menu.tsx +1 -1
  16. package/vendor/src/components/data-table/table-import-button.tsx +1 -1
  17. package/vendor/src/components/display/data-state.tsx +20 -8
  18. package/vendor/src/components/display/index.ts +19 -15
  19. package/vendor/src/components/display/metric-card.tsx +35 -0
  20. package/vendor/src/components/display/progress-circle.tsx +24 -0
  21. package/vendor/src/components/display/smart-card.tsx +49 -27
  22. package/vendor/src/components/display/status-dot.tsx +45 -0
  23. package/vendor/src/components/display/user-card.tsx +30 -0
  24. package/vendor/src/components/feedback/alert.tsx +21 -11
  25. package/vendor/src/components/feedback/empty-state.tsx +2 -2
  26. package/vendor/src/components/feedback/loading-state.tsx +2 -2
  27. package/vendor/src/components/feedback/page-state.tsx +19 -15
  28. package/vendor/src/components/feedback/status-badge.tsx +43 -43
  29. package/vendor/src/components/form/form-app-input.tsx +147 -0
  30. package/vendor/src/components/form/form-date-input.tsx +16 -19
  31. package/vendor/src/components/form/form-field-shell.tsx +11 -8
  32. package/vendor/src/components/form/form-field-utils.ts +76 -0
  33. package/vendor/src/components/form/form-input.tsx +423 -44
  34. package/vendor/src/components/form/form-number-input.tsx +16 -15
  35. package/vendor/src/components/form/form-phone-input.tsx +15 -9
  36. package/vendor/src/components/form/form-search-input.tsx +16 -19
  37. package/vendor/src/components/form/form-select.tsx +4 -3
  38. package/vendor/src/components/form/public.ts +16 -14
  39. package/vendor/src/components/form/smart-form-shell.tsx +13 -12
  40. package/vendor/src/components/inputs/app-input.tsx +27 -0
  41. package/vendor/src/components/inputs/async-select.tsx +113 -84
  42. package/vendor/src/components/inputs/clearable-input.tsx +81 -61
  43. package/vendor/src/components/inputs/date-input.tsx +21 -17
  44. package/vendor/src/components/inputs/date-range-input.tsx +10 -10
  45. package/vendor/src/components/inputs/index.ts +1 -0
  46. package/vendor/src/components/inputs/input-decorator.tsx +101 -57
  47. package/vendor/src/components/inputs/masked-input.tsx +20 -20
  48. package/vendor/src/components/inputs/money-input.tsx +2 -2
  49. package/vendor/src/components/inputs/number-input.tsx +29 -19
  50. package/vendor/src/components/inputs/password-input.tsx +82 -45
  51. package/vendor/src/components/inputs/phone-input.tsx +24 -2
  52. package/vendor/src/components/inputs/quantity-input.tsx +2 -2
  53. package/vendor/src/components/inputs/search-input.tsx +54 -3
  54. package/vendor/src/components/inputs/simple-select.tsx +110 -22
  55. package/vendor/src/components/layout/app-shell.tsx +2 -2
  56. package/vendor/src/components/layout/index.ts +5 -4
  57. package/vendor/src/components/layout/page-header.tsx +79 -35
  58. package/vendor/src/components/layout/public.ts +12 -10
  59. package/vendor/src/components/layout/section-header.tsx +56 -0
  60. package/vendor/src/components/layout/stack.tsx +106 -0
  61. package/vendor/src/components/layout/stat-card.tsx +66 -29
  62. package/vendor/src/components/navigation/index.ts +1 -0
  63. package/vendor/src/components/navigation/nav-tabs.tsx +60 -0
  64. package/vendor/src/components/navigation/page-tabs.tsx +41 -26
  65. package/vendor/src/components/navigation/pagination.tsx +14 -10
  66. package/vendor/src/components/overlay/alert-dialog.tsx +65 -0
  67. package/vendor/src/components/overlay/drawer.tsx +71 -0
  68. package/vendor/src/components/overlay/index.ts +4 -2
  69. package/vendor/src/components/patterns/data-view.tsx +13 -8
  70. package/vendor/src/components/ui/badge.tsx +96 -52
  71. package/vendor/src/components/ui/button.tsx +99 -61
  72. package/vendor/src/components/ui/card.tsx +84 -25
  73. package/vendor/src/components/ui/checkbox.tsx +68 -68
  74. package/vendor/src/components/ui/command.tsx +32 -32
  75. package/vendor/src/components/ui/dialog.tsx +135 -138
  76. package/vendor/src/components/ui/dropdown-menu.tsx +21 -21
  77. package/vendor/src/components/ui/hover-card.tsx +49 -0
  78. package/vendor/src/components/ui/input-primitive.tsx +24 -0
  79. package/vendor/src/components/ui/input.tsx +191 -20
  80. package/vendor/src/components/ui/kbd.tsx +33 -0
  81. package/vendor/src/components/ui/popover.tsx +11 -11
  82. package/vendor/src/components/ui/radio-group.tsx +102 -0
  83. package/vendor/src/components/ui/right-click-menu.tsx +60 -0
  84. package/vendor/src/components/ui/scroll-box.tsx +27 -0
  85. package/vendor/src/components/ui/segmented-control.tsx +21 -17
  86. package/vendor/src/components/ui/select.tsx +187 -189
  87. package/vendor/src/components/ui/skeleton.tsx +2 -2
  88. package/vendor/src/components/ui/switch.tsx +60 -60
  89. package/vendor/src/components/ui/table.tsx +114 -114
  90. package/vendor/src/components/ui/tabs.tsx +2 -2
  91. package/vendor/src/components/ui/textarea.tsx +1 -1
  92. package/vendor/src/components/upload/file-dropzone.tsx +38 -0
  93. package/vendor/src/components/upload/file-upload.tsx +4 -4
  94. package/vendor/src/components/upload/image-upload.tsx +22 -19
  95. package/vendor/src/components/upload/index.ts +2 -0
  96. package/vendor/src/families/catalog.ts +1 -0
  97. package/vendor/src/families/docs-groups.ts +10 -1
  98. package/vendor/src/families/member-metadata.ts +24 -0
  99. package/vendor/src/families/member-snippets.ts +41 -2
  100. package/vendor/src/families/migration-map.ts +3 -0
  101. package/vendor/src/index.ts +23 -18
  102. package/vendor/templates/styles/globals.css +253 -0
  103. package/dist/index.js +0 -432
@@ -1,19 +1,16 @@
1
- import {
2
- FormInput,
3
- type FormInputSearchVariantProps as BaseFormSearchInputProps,
4
- } from "@/components/form/form-input"
5
- import type { FieldPath, FieldValues } from "react-hook-form"
6
-
7
- export type FormSearchInputProps<
8
- TFieldValues extends FieldValues = FieldValues,
9
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
10
- > = Omit<BaseFormSearchInputProps<TFieldValues, TName>, "kind">
11
-
12
- function FormSearchInput<
13
- TFieldValues extends FieldValues = FieldValues,
14
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
15
- >(props: FormSearchInputProps<TFieldValues, TName>) {
16
- return <FormInput {...props} kind="search" />
17
- }
18
-
19
- export { FormSearchInput }
1
+ import { FormAppInput, type FormAppInputProps } from "@/components/form/form-app-input"
2
+ import type { FieldPath, FieldValues } from "react-hook-form"
3
+
4
+ export type FormSearchInputProps<
5
+ TFieldValues extends FieldValues = FieldValues,
6
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
7
+ > = Omit<FormAppInputProps<TFieldValues, TName>, "kind">
8
+
9
+ function FormSearchInput<
10
+ TFieldValues extends FieldValues = FieldValues,
11
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
12
+ >(props: FormSearchInputProps<TFieldValues, TName>) {
13
+ return <FormAppInput {...props} kind="search" />
14
+ }
15
+
16
+ export { FormSearchInput }
@@ -93,6 +93,7 @@ function FormSelect<
93
93
  >(props: FormSelectProps<TFieldValues, TName, TValue, TData, TOption>) {
94
94
  const shellProps = buildShellProps(props)
95
95
  const kind = props.kind ?? "simple"
96
+ const resolvedFieldClassName = props.fieldClassName ?? "w-full"
96
97
 
97
98
  return (
98
99
  <Controller
@@ -139,7 +140,7 @@ function FormSelect<
139
140
  field.onChange(nextValue ?? emptyValue)
140
141
  onValueChange?.(nextValue, option)
141
142
  }}
142
- triggerClassName={fieldClassName}
143
+ triggerClassName={fieldClassName ?? resolvedFieldClassName}
143
144
  />
144
145
  </FormFieldShell>
145
146
  )
@@ -180,9 +181,9 @@ function FormSelect<
180
181
  disabled={disabled || readOnly}
181
182
  onValueChange={(nextValue) => {
182
183
  field.onChange(nextValue || emptyValue)
183
- onValueChange?.(nextValue)
184
+ onValueChange?.(nextValue as string)
184
185
  }}
185
- triggerClassName={fieldClassName}
186
+ triggerClassName={fieldClassName ?? resolvedFieldClassName}
186
187
  />
187
188
  </FormFieldShell>
188
189
  )
@@ -1,14 +1,16 @@
1
- export * from "./form-field-shell"
2
- export * from "./form-input"
3
- export * from "./form-select"
4
- export * from "./form-async-select"
5
- export * from "./form-textarea"
6
- export * from "./form-switch"
7
- export * from "./form-search-input"
8
- export * from "./form-password-input"
9
- export * from "./form-number-input"
10
- export * from "./form-phone-input"
11
- export * from "./form-date-input"
12
- export * from "./form-date-range-input"
13
- export * from "./form-date-picker"
14
- export * from "./form-date-range-picker"
1
+ export * from "./form-field-shell"
2
+ export * from "./form-field-utils"
3
+ export * from "./form-app-input"
4
+ export * from "./form-input"
5
+ export * from "./form-select"
6
+ export * from "./form-async-select"
7
+ export * from "./form-textarea"
8
+ export * from "./form-switch"
9
+ export * from "./form-search-input"
10
+ export * from "./form-password-input"
11
+ export * from "./form-number-input"
12
+ export * from "./form-phone-input"
13
+ export * from "./form-date-input"
14
+ export * from "./form-date-range-input"
15
+ export * from "./form-date-picker"
16
+ export * from "./form-date-range-picker"
@@ -6,14 +6,15 @@ import { cn } from "@/lib/utils"
6
6
 
7
7
  /** @deprecated Prefer `FormFamily.Section`/`FormFamily.Actions` composition or `FormFamily.Builder` for new public usage. */
8
8
  export type SmartFormSection = {
9
- key: string
10
- title?: React.ReactNode
11
- description?: React.ReactNode
12
- fields?: React.ReactNode
13
- content?: React.ReactNode
14
- columns?: 1 | 2 | 3
15
- hidden?: boolean
16
- }
9
+ key: string
10
+ title?: React.ReactNode
11
+ description?: React.ReactNode
12
+ content?: React.ReactNode
13
+ fields?: React.ReactNode
14
+ children?: React.ReactNode
15
+ columns?: 1 | 2 | 3
16
+ hidden?: boolean
17
+ }
17
18
 
18
19
  /** @deprecated Prefer `FormFamily` entry members for new public usage. */
19
20
  export type SmartFormShellProps = React.ComponentProps<"form"> & {
@@ -45,10 +46,10 @@ function SmartFormShell({ title, description, sections, actions, loading = false
45
46
  ))}
46
47
  <div className={cn("grid gap-4", contentClassName)}>
47
48
  {visibleSections?.map((section) => renderSection?.(section) ?? (
48
- <FormSection key={section.key} title={section.title} description={section.description} columns={section.columns}>
49
- {section.content ?? section.fields}
50
- </FormSection>
51
- ))}
49
+ <FormSection key={section.key} title={section.title} description={section.description} columns={section.columns}>
50
+ {section.children ?? section.content ?? section.fields}
51
+ </FormSection>
52
+ ))}
52
53
  {children}
53
54
  </div>
54
55
  {actions ?? <FormActions loading={loading} disabled={disabled} submitLabel={submitLabel} cancelLabel={cancelLabel} onCancel={onCancel} />}
@@ -0,0 +1,27 @@
1
+ import * as React from "react"
2
+
3
+ import {
4
+ Input,
5
+ type InputKind,
6
+ type InputProps,
7
+ } from "@/components/ui/input"
8
+
9
+ export type AppInputKind = InputKind
10
+ export type AppInputProps = InputProps
11
+ export type UniversalInputProps = InputProps
12
+
13
+ /**
14
+ * @deprecated Use {@link Input} with `kind` instead.
15
+ */
16
+ const AppInput = React.forwardRef<HTMLInputElement | HTMLDivElement, AppInputProps>((props, ref) => {
17
+ return <Input ref={ref as React.ForwardedRef<HTMLInputElement | HTMLDivElement>} {...props} />
18
+ })
19
+ AppInput.displayName = "AppInput"
20
+
21
+ /**
22
+ * @deprecated Use {@link Input} with `kind` instead.
23
+ */
24
+ const UniversalInput = AppInput
25
+
26
+ export { AppInput, UniversalInput }
27
+
@@ -304,7 +304,16 @@ function AsyncStateMessage({
304
304
  className?: string
305
305
  children: React.ReactNode
306
306
  }) {
307
- return <div className={cn("px-2 py-3 text-sm text-muted-foreground", className)}>{children}</div>
307
+ return (
308
+ <div
309
+ 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)]",
311
+ className
312
+ )}
313
+ >
314
+ {children}
315
+ </div>
316
+ )
308
317
  }
309
318
 
310
319
  function getOptionLabelText<TValue extends string, TData>(option: AsyncSelectOption<TValue, TData>) {
@@ -334,14 +343,14 @@ function AsyncOptionButton<
334
343
  }) {
335
344
  return (
336
345
  <button
337
- type="button"
338
- disabled={option.disabled}
339
- className={cn(
340
- "flex w-full items-center gap-2 rounded-md px-2 py-2 text-left text-sm outline-none hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50",
341
- selected && "bg-accent text-accent-foreground",
342
- optionClassName
343
- )}
344
- onClick={() => onSelect(option)}
346
+ type="button"
347
+ disabled={option.disabled}
348
+ 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)]",
351
+ optionClassName
352
+ )}
353
+ onClick={() => onSelect(option)}
345
354
  >
346
355
  <span className="flex size-4 shrink-0 items-center justify-center">
347
356
  {selected && <CheckIcon className="size-4" />}
@@ -375,12 +384,12 @@ function AsyncCreateButton({
375
384
  onCreate: () => void
376
385
  }) {
377
386
  return (
378
- <button
379
- type="button"
380
- disabled={isCreating}
381
- className="flex w-full items-center gap-2 rounded-md px-2 py-2 text-left text-sm outline-none hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
382
- onClick={onCreate}
383
- >
387
+ <button
388
+ type="button"
389
+ 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"
391
+ onClick={onCreate}
392
+ >
384
393
  <span className="flex size-4 shrink-0 items-center justify-center">
385
394
  {isCreating ? <Loader2Icon className="size-4 animate-spin" /> : <PlusIcon className="size-4" />}
386
395
  </span>
@@ -575,14 +584,17 @@ function AsyncSelect<
575
584
  <PopoverTrigger
576
585
  render={
577
586
  <Button
578
- type="button"
579
- variant="outline"
580
- disabled={disabled}
581
- aria-expanded={open}
582
- className={cn("w-full justify-between", triggerClassName)}
583
- />
584
- }
585
- >
587
+ type="button"
588
+ variant="outline"
589
+ disabled={disabled}
590
+ aria-expanded={open}
591
+ 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)]",
593
+ triggerClassName
594
+ )}
595
+ />
596
+ }
597
+ >
586
598
  <span className="min-w-0 flex-1 text-left">
587
599
  {currentOption ? (
588
600
  <span className="flex min-w-0 flex-col">
@@ -602,7 +614,7 @@ function AsyncSelect<
602
614
  <span
603
615
  role="button"
604
616
  tabIndex={0}
605
- className="rounded-sm p-0.5 text-muted-foreground hover:text-foreground"
617
+ className="rounded-full border border-border/65 p-1 text-muted-foreground transition-colors hover:border-border hover:bg-muted/55 hover:text-foreground"
606
618
  aria-label={labels?.clear ?? "Clear"}
607
619
  onClick={handleClear}
608
620
  onKeyDown={(event) => {
@@ -618,21 +630,24 @@ function AsyncSelect<
618
630
  <ChevronsUpDownIcon className="size-4 opacity-60" />
619
631
  </span>
620
632
  </PopoverTrigger>
621
- <PopoverContent
622
- align="start"
623
- className={cn("w-(--anchor-width) gap-2 p-2", contentClassName)}
624
- >
625
- <div className="relative">
626
- <SearchIcon className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
627
- <Input
628
- value={search}
629
- onChange={(event) => setSearch(event.target.value)}
630
- placeholder={labels?.searchPlaceholder ?? "Search..."}
631
- className={cn("pl-8", searchClassName)}
632
- />
633
- </div>
634
-
635
- <div className="max-h-64 overflow-y-auto">
633
+ <PopoverContent
634
+ align="start"
635
+ 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",
637
+ contentClassName
638
+ )}
639
+ >
640
+ <div className="relative">
641
+ <SearchIcon className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
642
+ <Input
643
+ value={search}
644
+ onChange={(event) => setSearch(event.target.value)}
645
+ 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)}
647
+ />
648
+ </div>
649
+
650
+ <div className="max-h-64 space-y-1 overflow-y-auto pr-1">
636
651
  {searchTooShort && flatOptions.length === 0 && (
637
652
  renderMinSearch?.(state) ?? (
638
653
  <AsyncStateMessage>
@@ -677,9 +692,9 @@ function AsyncSelect<
677
692
  optionGroups.map((group, groupIndex) => (
678
693
  <div key={groupIndex}>
679
694
  {group.label && (
680
- <div className="sticky top-0 z-10 bg-popover px-2 py-1 text-xs font-medium text-muted-foreground">
681
- {group.label}
682
- </div>
695
+ <div className="sticky top-0 z-10 bg-popover px-2 py-1 text-xs font-medium text-muted-foreground">
696
+ {group.label}
697
+ </div>
683
698
  )}
684
699
  {group.options.map((option) => (
685
700
  <AsyncOptionButton
@@ -969,7 +984,10 @@ function AsyncMultiSelect<
969
984
  variant="outline"
970
985
  disabled={disabled}
971
986
  aria-expanded={open}
972
- className={cn("min-h-8 w-full justify-between", triggerClassName)}
987
+ 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)]",
989
+ triggerClassName
990
+ )}
973
991
  onKeyDown={handleTriggerKeyDown}
974
992
  />
975
993
  }
@@ -978,12 +996,12 @@ function AsyncMultiSelect<
978
996
  {currentOptions.length > 0 ? (
979
997
  currentOptions.map((option) => (
980
998
  <span
981
- key={option.value}
982
- className={cn(
983
- "inline-flex max-w-full items-center gap-1 rounded-md bg-muted px-1.5 py-0.5 text-xs text-foreground",
984
- tagClassName
985
- )}
986
- >
999
+ key={option.value}
1000
+ 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)]",
1002
+ tagClassName
1003
+ )}
1004
+ >
987
1005
  <span className="flex min-w-0 flex-col">
988
1006
  <span className="truncate">
989
1007
  {renderTag?.(option, { remove: () => removeOption(option) }) ??
@@ -998,7 +1016,7 @@ function AsyncMultiSelect<
998
1016
  <span
999
1017
  role="button"
1000
1018
  tabIndex={0}
1001
- className="rounded-sm text-muted-foreground hover:text-foreground"
1019
+ className="rounded-full text-muted-foreground transition-colors hover:text-foreground"
1002
1020
  aria-label={`Remove ${getOptionLabelText(option)}`}
1003
1021
  onClick={(event) => {
1004
1022
  event.stopPropagation()
@@ -1022,7 +1040,7 @@ function AsyncMultiSelect<
1022
1040
  <span
1023
1041
  role="button"
1024
1042
  tabIndex={0}
1025
- className="rounded-sm p-0.5 text-muted-foreground hover:text-foreground"
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"
1026
1044
  aria-label={labels?.clearAll ?? labels?.clear ?? "Clear all"}
1027
1045
  onClick={handleClear}
1028
1046
  onKeyDown={(event) => {
@@ -1038,41 +1056,52 @@ function AsyncMultiSelect<
1038
1056
  <ChevronsUpDownIcon className="size-4 opacity-60" />
1039
1057
  </span>
1040
1058
  </PopoverTrigger>
1041
- <PopoverContent
1042
- align="start"
1043
- className={cn("w-(--anchor-width) gap-2 p-2", contentClassName)}
1044
- >
1045
- <div className="relative">
1046
- <SearchIcon className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
1047
- <Input
1048
- value={search}
1049
- onChange={(event) => setSearch(event.target.value)}
1050
- placeholder={labels?.searchPlaceholder ?? "Search..."}
1051
- className={cn("pl-8", searchClassName)}
1052
- />
1053
- </div>
1054
-
1055
- <div className="flex flex-wrap items-center justify-between gap-2 px-1 text-xs text-muted-foreground">
1056
- {hasValue && labels?.selectedCount && <span>{labels.selectedCount(values.length)}</span>}
1057
- {isMaxReached &&
1058
- (renderMaxSelected?.(state) ?? (
1059
+ <PopoverContent
1060
+ align="start"
1061
+ 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",
1063
+ contentClassName
1064
+ )}
1065
+ >
1066
+ <div className="relative">
1067
+ <SearchIcon className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
1068
+ <Input
1069
+ value={search}
1070
+ onChange={(event) => setSearch(event.target.value)}
1071
+ 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)}
1073
+ />
1074
+ </div>
1075
+
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">
1077
+ {hasValue && labels?.selectedCount && <span>{labels.selectedCount(values.length)}</span>}
1078
+ {isMaxReached &&
1079
+ (renderMaxSelected?.(state) ?? (
1059
1080
  <span>{labels?.maxSelected?.(maxSelected ?? values.length) ?? `Maximum ${maxSelected} selected`}</span>
1060
1081
  ))}
1061
- <div className="ml-auto flex items-center gap-2">
1062
- {canSelectAll && (
1063
- <button type="button" className="font-medium text-foreground hover:underline" onClick={handleSelectAllVisible}>
1064
- {labels?.selectAll ?? "Select all"}
1065
- </button>
1066
- )}
1067
- {canClear && (
1068
- <button type="button" className="font-medium text-foreground hover:underline" onClick={() => onValueChange?.([], [])}>
1069
- {labels?.clearAll ?? labels?.clear ?? "Clear all"}
1070
- </button>
1071
- )}
1072
- </div>
1073
- </div>
1074
-
1075
- <div className="max-h-64 overflow-y-auto">
1082
+ <div className="ml-auto flex items-center gap-2">
1083
+ {canSelectAll && (
1084
+ <button
1085
+ type="button"
1086
+ className="rounded-full border border-border/75 px-2.5 py-1 font-medium text-foreground transition-colors hover:bg-background"
1087
+ onClick={handleSelectAllVisible}
1088
+ >
1089
+ {labels?.selectAll ?? "Select all"}
1090
+ </button>
1091
+ )}
1092
+ {canClear && (
1093
+ <button
1094
+ type="button"
1095
+ className="rounded-full border border-border/75 px-2.5 py-1 font-medium text-foreground transition-colors hover:bg-background"
1096
+ onClick={() => onValueChange?.([], [])}
1097
+ >
1098
+ {labels?.clearAll ?? labels?.clear ?? "Clear all"}
1099
+ </button>
1100
+ )}
1101
+ </div>
1102
+ </div>
1103
+
1104
+ <div className="max-h-64 space-y-1 overflow-y-auto pr-1">
1076
1105
  {searchTooShort && flatOptions.length === 0 && (
1077
1106
  renderMinSearch?.(state) ?? (
1078
1107
  <AsyncStateMessage>
@@ -1,19 +1,21 @@
1
- import * as React from "react"
2
- import { XIcon } from "lucide-react"
3
-
4
- import { InputDecorator } from "@/components/inputs/input-decorator"
5
- import { createInputChangeHandler, getInputValue } from "@/components/inputs/input-value"
1
+ import * as React from "react"
2
+ import { XIcon } from "lucide-react"
6
3
 
7
- export type ClearableInputProps = Omit<
8
- React.ComponentProps<typeof InputDecorator>,
9
- "value" | "onChange"
10
- > & {
11
- value?: string | number | null
4
+ import { InputDecorator } from "@/components/inputs/input-decorator"
5
+ import { createInputChangeHandler, getInputValue } from "@/components/inputs/input-value"
6
+
7
+ export type ClearableInputProps = Omit<
8
+ React.ComponentProps<typeof InputDecorator>,
9
+ "value" | "onChange"
10
+ > & {
11
+ value?: string | number | null
12
12
  onChange?: React.ChangeEventHandler<HTMLInputElement>
13
13
  onValueChange?: (value: string) => void
14
14
  onClear?: () => void
15
15
  clearable?: boolean
16
16
  clearLabel?: string
17
+ clearOnEscape?: boolean
18
+ focusAfterClear?: boolean
17
19
  leadingIcon?: React.ReactNode
18
20
  trailing?: React.ReactNode
19
21
  wrapperClassName?: string
@@ -22,57 +24,75 @@ export type ClearableInputProps = Omit<
22
24
 
23
25
  const ClearableInput = React.forwardRef<HTMLInputElement, ClearableInputProps>(
24
26
  (
25
- {
26
- value,
27
- onChange,
28
- onValueChange,
29
- onClear,
30
- clearable = true,
31
- clearLabel = "Clear",
32
- leadingIcon,
33
- trailing,
34
- disabled,
35
- ...props
36
- },
37
- ref
38
- ) => {
39
- const stringValue = getInputValue(value)
40
- const canClear = clearable && stringValue.length > 0 && !disabled
41
- const handleChange = createInputChangeHandler({ onChange, onValueChange })
42
-
43
- const handleClear = () => {
44
- onValueChange?.("")
45
- onClear?.()
46
- }
47
-
48
- return (
49
- <InputDecorator
50
- data-slot="clearable-input"
51
- ref={ref}
52
- value={stringValue}
53
- disabled={disabled}
54
- onChange={handleChange}
55
- leading={leadingIcon}
56
- trailing={
57
- <>
58
- {trailing}
59
- {canClear && (
60
- <button
61
- type="button"
62
- aria-label={clearLabel}
63
- className="rounded-sm p-0.5 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
64
- onClick={handleClear}
65
- >
66
- <XIcon className="size-4" />
67
- </button>
68
- )}
69
- </>
70
- }
71
- {...props}
72
- />
73
- )
74
- }
75
- )
27
+ {
28
+ value,
29
+ onChange,
30
+ onValueChange,
31
+ onClear,
32
+ clearable = true,
33
+ clearLabel = "Clear",
34
+ clearOnEscape = true,
35
+ focusAfterClear = true,
36
+ leadingIcon,
37
+ trailing,
38
+ disabled,
39
+ onKeyDown,
40
+ ...props
41
+ },
42
+ ref
43
+ ) => {
44
+ const inputRef = React.useRef<HTMLInputElement | null>(null)
45
+ const stringValue = getInputValue(value)
46
+ const canClear = clearable && stringValue.length > 0 && !disabled
47
+ const handleChange = createInputChangeHandler({ onChange, onValueChange })
48
+
49
+ React.useImperativeHandle(ref, () => inputRef.current as HTMLInputElement)
50
+
51
+ const clearValue = () => {
52
+ if (!canClear) return
53
+ onValueChange?.("")
54
+ onClear?.()
55
+ if (focusAfterClear) inputRef.current?.focus()
56
+ }
57
+
58
+ const handleKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
59
+ onKeyDown?.(event)
60
+ if (event.defaultPrevented) return
61
+ if (clearOnEscape && event.key === "Escape" && canClear) {
62
+ event.preventDefault()
63
+ clearValue()
64
+ }
65
+ }
66
+
67
+ return (
68
+ <InputDecorator
69
+ data-slot="clearable-input"
70
+ ref={inputRef}
71
+ value={stringValue}
72
+ disabled={disabled}
73
+ onChange={handleChange}
74
+ onKeyDown={handleKeyDown}
75
+ leading={leadingIcon}
76
+ trailing={
77
+ <>
78
+ {trailing}
79
+ {canClear && (
80
+ <button
81
+ type="button"
82
+ aria-label={clearLabel}
83
+ className="rounded-sm p-0.5 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
84
+ onClick={clearValue}
85
+ >
86
+ <XIcon className="size-4" />
87
+ </button>
88
+ )}
89
+ </>
90
+ }
91
+ {...props}
92
+ />
93
+ )
94
+ }
95
+ )
76
96
  ClearableInput.displayName = "ClearableInput"
77
97
 
78
98
  export { ClearableInput }