azamat-ui-kit-cli 0.2.2

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 (213) hide show
  1. package/README.md +8 -0
  2. package/dist/index.js +432 -0
  3. package/package.json +34 -0
  4. package/vendor/package.json +4 -0
  5. package/vendor/src/components/actions/action-bar.tsx +35 -0
  6. package/vendor/src/components/actions/action-menu.tsx +120 -0
  7. package/vendor/src/components/actions/button-group.tsx +47 -0
  8. package/vendor/src/components/actions/copy-button.tsx +91 -0
  9. package/vendor/src/components/actions/copy-field.tsx +31 -0
  10. package/vendor/src/components/actions/floating-action-button.tsx +33 -0
  11. package/vendor/src/components/actions/index.ts +7 -0
  12. package/vendor/src/components/actions/public.ts +5 -0
  13. package/vendor/src/components/actions/quick-action-grid.tsx +162 -0
  14. package/vendor/src/components/calendar/calendar.tsx +328 -0
  15. package/vendor/src/components/calendar/date-picker.tsx +78 -0
  16. package/vendor/src/components/calendar/date-range-picker.tsx +96 -0
  17. package/vendor/src/components/calendar/date-utils.ts +89 -0
  18. package/vendor/src/components/calendar/index.ts +4 -0
  19. package/vendor/src/components/charts/charts.tsx +275 -0
  20. package/vendor/src/components/charts/horizontal-bar-chart.tsx +46 -0
  21. package/vendor/src/components/charts/index.ts +4 -0
  22. package/vendor/src/components/charts/kpi.tsx +68 -0
  23. package/vendor/src/components/charts/progress-ring.tsx +45 -0
  24. package/vendor/src/components/charts/public.ts +1 -0
  25. package/vendor/src/components/command/command-palette.tsx +375 -0
  26. package/vendor/src/components/command/index.ts +1 -0
  27. package/vendor/src/components/data-table/data-table-actions-column.tsx +58 -0
  28. package/vendor/src/components/data-table/data-table-bulk-actions.tsx +84 -0
  29. package/vendor/src/components/data-table/data-table-column-visibility-menu.tsx +79 -0
  30. package/vendor/src/components/data-table/data-table-pagination.tsx +91 -0
  31. package/vendor/src/components/data-table/data-table-row-actions.tsx +48 -0
  32. package/vendor/src/components/data-table/data-table-select-column.tsx +59 -0
  33. package/vendor/src/components/data-table/data-table-sortable-header.tsx +45 -0
  34. package/vendor/src/components/data-table/data-table-toolbar.tsx +76 -0
  35. package/vendor/src/components/data-table/data-table-view-presets.tsx +128 -0
  36. package/vendor/src/components/data-table/data-table.tsx +507 -0
  37. package/vendor/src/components/data-table/index.ts +12 -0
  38. package/vendor/src/components/data-table/public.ts +10 -0
  39. package/vendor/src/components/data-table/table-export-menu.tsx +56 -0
  40. package/vendor/src/components/data-table/table-import-button.tsx +43 -0
  41. package/vendor/src/components/display/activity-feed.tsx +97 -0
  42. package/vendor/src/components/display/avatar.tsx +131 -0
  43. package/vendor/src/components/display/code-block.tsx +33 -0
  44. package/vendor/src/components/display/data-state.tsx +63 -0
  45. package/vendor/src/components/display/description-list.tsx +119 -0
  46. package/vendor/src/components/display/descriptions.tsx +83 -0
  47. package/vendor/src/components/display/entity-card.tsx +53 -0
  48. package/vendor/src/components/display/file-card.tsx +54 -0
  49. package/vendor/src/components/display/index.ts +30 -0
  50. package/vendor/src/components/display/kanban.tsx +104 -0
  51. package/vendor/src/components/display/keyboard-shortcut.tsx +31 -0
  52. package/vendor/src/components/display/list.tsx +100 -0
  53. package/vendor/src/components/display/metric-grid.tsx +86 -0
  54. package/vendor/src/components/display/progress.tsx +162 -0
  55. package/vendor/src/components/display/property-grid.tsx +54 -0
  56. package/vendor/src/components/display/result.tsx +90 -0
  57. package/vendor/src/components/display/smart-card.tsx +168 -0
  58. package/vendor/src/components/display/statistic.tsx +107 -0
  59. package/vendor/src/components/display/status-legend.tsx +108 -0
  60. package/vendor/src/components/display/tag-list.tsx +52 -0
  61. package/vendor/src/components/display/timeline.tsx +132 -0
  62. package/vendor/src/components/display/tree-view.tsx +116 -0
  63. package/vendor/src/components/feedback/alert.tsx +69 -0
  64. package/vendor/src/components/feedback/empty-state.tsx +56 -0
  65. package/vendor/src/components/feedback/index.ts +5 -0
  66. package/vendor/src/components/feedback/loading-state.tsx +39 -0
  67. package/vendor/src/components/feedback/page-state.tsx +69 -0
  68. package/vendor/src/components/feedback/status-badge.tsx +62 -0
  69. package/vendor/src/components/filters/filter-bar.tsx +89 -0
  70. package/vendor/src/components/filters/filter-chips.tsx +69 -0
  71. package/vendor/src/components/filters/index.ts +2 -0
  72. package/vendor/src/components/form/form-actions.tsx +53 -0
  73. package/vendor/src/components/form/form-async-select.tsx +26 -0
  74. package/vendor/src/components/form/form-date-input.tsx +19 -0
  75. package/vendor/src/components/form/form-date-picker.tsx +54 -0
  76. package/vendor/src/components/form/form-date-range-input.tsx +79 -0
  77. package/vendor/src/components/form/form-date-range-picker.tsx +57 -0
  78. package/vendor/src/components/form/form-field-shell.tsx +191 -0
  79. package/vendor/src/components/form/form-input.tsx +480 -0
  80. package/vendor/src/components/form/form-number-input.tsx +19 -0
  81. package/vendor/src/components/form/form-password-input.tsx +19 -0
  82. package/vendor/src/components/form/form-phone-input.tsx +22 -0
  83. package/vendor/src/components/form/form-search-input.tsx +19 -0
  84. package/vendor/src/components/form/form-section.tsx +29 -0
  85. package/vendor/src/components/form/form-select.tsx +194 -0
  86. package/vendor/src/components/form/form-switch.tsx +145 -0
  87. package/vendor/src/components/form/form-textarea.tsx +103 -0
  88. package/vendor/src/components/form/index.ts +17 -0
  89. package/vendor/src/components/form/public.ts +14 -0
  90. package/vendor/src/components/form/smart-form-shell.tsx +59 -0
  91. package/vendor/src/components/inputs/async-select.tsx +1143 -0
  92. package/vendor/src/components/inputs/clearable-input.tsx +78 -0
  93. package/vendor/src/components/inputs/color-input.tsx +47 -0
  94. package/vendor/src/components/inputs/combobox.tsx +89 -0
  95. package/vendor/src/components/inputs/date-input.tsx +32 -0
  96. package/vendor/src/components/inputs/date-range-input.tsx +67 -0
  97. package/vendor/src/components/inputs/index.ts +19 -0
  98. package/vendor/src/components/inputs/input-chrome.tsx +37 -0
  99. package/vendor/src/components/inputs/input-decorator.tsx +64 -0
  100. package/vendor/src/components/inputs/input-value.ts +42 -0
  101. package/vendor/src/components/inputs/masked-input.tsx +51 -0
  102. package/vendor/src/components/inputs/money-input.tsx +73 -0
  103. package/vendor/src/components/inputs/number-input.tsx +87 -0
  104. package/vendor/src/components/inputs/numeric-value.ts +39 -0
  105. package/vendor/src/components/inputs/otp-input.tsx +102 -0
  106. package/vendor/src/components/inputs/password-input.tsx +85 -0
  107. package/vendor/src/components/inputs/phone-input.tsx +46 -0
  108. package/vendor/src/components/inputs/quantity-input.tsx +116 -0
  109. package/vendor/src/components/inputs/quantity-stepper.tsx +49 -0
  110. package/vendor/src/components/inputs/rating.tsx +98 -0
  111. package/vendor/src/components/inputs/search-input.tsx +26 -0
  112. package/vendor/src/components/inputs/simple-select.tsx +72 -0
  113. package/vendor/src/components/inputs/slider.tsx +149 -0
  114. package/vendor/src/components/inputs/tag-input.tsx +104 -0
  115. package/vendor/src/components/layout/app-header.tsx +46 -0
  116. package/vendor/src/components/layout/app-shell.tsx +243 -0
  117. package/vendor/src/components/layout/app-sidebar.tsx +179 -0
  118. package/vendor/src/components/layout/breadcrumbs.tsx +72 -0
  119. package/vendor/src/components/layout/index.ts +11 -0
  120. package/vendor/src/components/layout/page-container.tsx +30 -0
  121. package/vendor/src/components/layout/page-header.tsx +60 -0
  122. package/vendor/src/components/layout/public.ts +10 -0
  123. package/vendor/src/components/layout/section.tsx +76 -0
  124. package/vendor/src/components/layout/sidebar-nav.tsx +147 -0
  125. package/vendor/src/components/layout/stat-card.tsx +88 -0
  126. package/vendor/src/components/layout/sticky-footer-bar.tsx +23 -0
  127. package/vendor/src/components/layout/workspace-shell.tsx +50 -0
  128. package/vendor/src/components/navigation/anchor-nav.tsx +44 -0
  129. package/vendor/src/components/navigation/index.ts +4 -0
  130. package/vendor/src/components/navigation/page-tabs.tsx +67 -0
  131. package/vendor/src/components/navigation/pagination.tsx +179 -0
  132. package/vendor/src/components/navigation/stepper-tabs.tsx +67 -0
  133. package/vendor/src/components/notifications/index.ts +1 -0
  134. package/vendor/src/components/notifications/toast.tsx +259 -0
  135. package/vendor/src/components/overlay/confirm-dialog.tsx +66 -0
  136. package/vendor/src/components/overlay/dialog-actions.tsx +68 -0
  137. package/vendor/src/components/overlay/index.ts +4 -0
  138. package/vendor/src/components/overlay/modal-shell.tsx +93 -0
  139. package/vendor/src/components/overlay/sheet-shell.tsx +212 -0
  140. package/vendor/src/components/patterns/action-system.tsx +116 -0
  141. package/vendor/src/components/patterns/crud-system.tsx +53 -0
  142. package/vendor/src/components/patterns/data-view.tsx +84 -0
  143. package/vendor/src/components/patterns/entity-details.tsx +66 -0
  144. package/vendor/src/components/patterns/filter-builder.tsx +113 -0
  145. package/vendor/src/components/patterns/form-builder-presets.ts +131 -0
  146. package/vendor/src/components/patterns/form-builder.tsx +334 -0
  147. package/vendor/src/components/patterns/index.ts +12 -0
  148. package/vendor/src/components/patterns/public.ts +4 -0
  149. package/vendor/src/components/patterns/resource-detail-page.tsx +160 -0
  150. package/vendor/src/components/patterns/resource-page.tsx +159 -0
  151. package/vendor/src/components/patterns/resource-system.tsx +61 -0
  152. package/vendor/src/components/patterns/settings-section.tsx +46 -0
  153. package/vendor/src/components/patterns/status-system.tsx +89 -0
  154. package/vendor/src/components/theme-provider.tsx +51 -0
  155. package/vendor/src/components/ui/badge.tsx +52 -0
  156. package/vendor/src/components/ui/button.tsx +61 -0
  157. package/vendor/src/components/ui/card.tsx +103 -0
  158. package/vendor/src/components/ui/checkbox.tsx +82 -0
  159. package/vendor/src/components/ui/collapse.tsx +126 -0
  160. package/vendor/src/components/ui/command.tsx +194 -0
  161. package/vendor/src/components/ui/dialog.tsx +160 -0
  162. package/vendor/src/components/ui/divider.tsx +46 -0
  163. package/vendor/src/components/ui/dropdown-menu.tsx +266 -0
  164. package/vendor/src/components/ui/input-group.tsx +158 -0
  165. package/vendor/src/components/ui/input.tsx +20 -0
  166. package/vendor/src/components/ui/popover.tsx +90 -0
  167. package/vendor/src/components/ui/segmented-control.tsx +78 -0
  168. package/vendor/src/components/ui/select.tsx +201 -0
  169. package/vendor/src/components/ui/skeleton.tsx +75 -0
  170. package/vendor/src/components/ui/spinner.tsx +50 -0
  171. package/vendor/src/components/ui/switch.tsx +71 -0
  172. package/vendor/src/components/ui/table.tsx +114 -0
  173. package/vendor/src/components/ui/tabs.tsx +55 -0
  174. package/vendor/src/components/ui/textarea.tsx +18 -0
  175. package/vendor/src/components/ui/tooltip.tsx +38 -0
  176. package/vendor/src/components/upload/file-upload.tsx +483 -0
  177. package/vendor/src/components/upload/image-upload.tsx +118 -0
  178. package/vendor/src/components/upload/index.ts +2 -0
  179. package/vendor/src/components/wizard/index.ts +2 -0
  180. package/vendor/src/components/wizard/stepper.tsx +53 -0
  181. package/vendor/src/components/wizard/wizard.tsx +60 -0
  182. package/vendor/src/families/card-family.ts +28 -0
  183. package/vendor/src/families/catalog.ts +96 -0
  184. package/vendor/src/families/data-table-family.ts +31 -0
  185. package/vendor/src/families/docs-adoption.ts +103 -0
  186. package/vendor/src/families/docs-groups.ts +209 -0
  187. package/vendor/src/families/docs-queries.ts +84 -0
  188. package/vendor/src/families/docs-routing.ts +89 -0
  189. package/vendor/src/families/form-family.ts +45 -0
  190. package/vendor/src/families/index.ts +17 -0
  191. package/vendor/src/families/input-family.ts +61 -0
  192. package/vendor/src/families/member-metadata.ts +466 -0
  193. package/vendor/src/families/member-queries.ts +28 -0
  194. package/vendor/src/families/member-snippet-queries.ts +54 -0
  195. package/vendor/src/families/member-snippets.ts +673 -0
  196. package/vendor/src/families/migration-map.ts +79 -0
  197. package/vendor/src/families/queries.ts +63 -0
  198. package/vendor/src/families/select-family.ts +33 -0
  199. package/vendor/src/families/views.ts +81 -0
  200. package/vendor/src/hooks/index.ts +6 -0
  201. package/vendor/src/hooks/use-before-unload-when-dirty.ts +21 -0
  202. package/vendor/src/hooks/use-data-table-view-state.ts +122 -0
  203. package/vendor/src/hooks/use-debounce.ts +52 -0
  204. package/vendor/src/hooks/use-disclosure.ts +38 -0
  205. package/vendor/src/hooks/use-is-mobile.ts +28 -0
  206. package/vendor/src/hooks/use-session-storage-state.ts +85 -0
  207. package/vendor/src/index.ts +38 -0
  208. package/vendor/src/lib/utils.ts +6 -0
  209. package/vendor/templates/components/button.tsx +0 -0
  210. package/vendor/templates/components/data-table.tsx +0 -0
  211. package/vendor/templates/components/input.tsx +0 -0
  212. package/vendor/templates/lib/utils.ts +0 -0
  213. package/vendor/templates/styles/globals.css +0 -0
@@ -0,0 +1,39 @@
1
+ function clampNumericValue(value: number, min?: number, max?: number) {
2
+ let nextValue = value
3
+
4
+ if (typeof min === "number") nextValue = Math.max(nextValue, min)
5
+ if (typeof max === "number") nextValue = Math.min(nextValue, max)
6
+
7
+ return nextValue
8
+ }
9
+
10
+ function normalizeDecimalInput(value: string) {
11
+ return value.trim().replace(/\s/g, "").replace(/,/g, ".")
12
+ }
13
+
14
+ function parseNormalizedNumber(value: string) {
15
+ if (!value || value === "-" || value === "." || value === "-.") {
16
+ return null
17
+ }
18
+
19
+ const parsed = Number(value)
20
+ return Number.isFinite(parsed) ? parsed : null
21
+ }
22
+
23
+ function parseDecimalInput(value: string) {
24
+ return parseNormalizedNumber(normalizeDecimalInput(value))
25
+ }
26
+
27
+ function parseMoneyLikeInput(value: string) {
28
+ return parseNormalizedNumber(
29
+ normalizeDecimalInput(value).replace(/[^0-9.-]/g, "")
30
+ )
31
+ }
32
+
33
+ export {
34
+ clampNumericValue,
35
+ normalizeDecimalInput,
36
+ parseDecimalInput,
37
+ parseMoneyLikeInput,
38
+ parseNormalizedNumber,
39
+ }
@@ -0,0 +1,102 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ export type OtpInputProps = Omit<React.ComponentProps<"div">, "onChange"> & {
6
+ value?: string
7
+ defaultValue?: string
8
+ onValueChange?: (value: string) => void
9
+ length?: number
10
+ disabled?: boolean
11
+ inputMode?: React.HTMLAttributes<HTMLInputElement>["inputMode"]
12
+ pattern?: string
13
+ mask?: boolean
14
+ labels?: {
15
+ group?: string
16
+ cell?: (index: number) => string
17
+ }
18
+ }
19
+
20
+ function OtpInput({
21
+ value,
22
+ defaultValue = "",
23
+ onValueChange,
24
+ length = 6,
25
+ disabled = false,
26
+ inputMode = "numeric",
27
+ pattern = "[0-9]*",
28
+ mask = false,
29
+ labels,
30
+ className,
31
+ ...props
32
+ }: OtpInputProps) {
33
+ const [internalValue, setInternalValue] = React.useState(defaultValue.slice(0, length))
34
+ const currentValue = (value ?? internalValue).slice(0, length)
35
+ const inputRefs = React.useRef<Array<HTMLInputElement | null>>([])
36
+
37
+ const updateValue = (nextValue: string) => {
38
+ const trimmedValue = nextValue.slice(0, length)
39
+ if (value === undefined) setInternalValue(trimmedValue)
40
+ onValueChange?.(trimmedValue)
41
+ }
42
+
43
+ const setCharAt = (index: number, nextChar: string) => {
44
+ const chars = String(Array.isArray(currentValue) ? currentValue.join("") : currentValue).padEnd(length, " ").split("")
45
+ chars[index] = nextChar.slice(-1)
46
+ updateValue(chars.join("").replace(/\s+$/g, ""))
47
+ }
48
+
49
+ const focusIndex = (index: number) => {
50
+ inputRefs.current[Math.max(0, Math.min(index, length - 1))]?.focus()
51
+ }
52
+
53
+ return (
54
+ <div
55
+ data-slot="otp-input"
56
+ role="group"
57
+ aria-label={labels?.group ?? "One-time password"}
58
+ className={cn("flex items-center gap-2", className)}
59
+ {...props}
60
+ >
61
+ {Array.from({ length }, (_, index) => (
62
+ <input
63
+ key={index}
64
+ ref={(node) => {
65
+ inputRefs.current[index] = node
66
+ }}
67
+ aria-label={labels?.cell?.(index) ?? `Digit ${index + 1}`}
68
+ inputMode={inputMode}
69
+ pattern={pattern}
70
+ type={mask ? "password" : "text"}
71
+ autoComplete="one-time-code"
72
+ maxLength={1}
73
+ disabled={disabled}
74
+ value={currentValue[index] ?? ""}
75
+ className="flex size-10 rounded-md border bg-background text-center text-sm font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
76
+ onChange={(event) => {
77
+ const nextChar = event.currentTarget.value
78
+ setCharAt(index, nextChar)
79
+ if (nextChar) focusIndex(index + 1)
80
+ }}
81
+ onKeyDown={(event) => {
82
+ if (event.key === "Backspace" && !currentValue[index]) {
83
+ event.preventDefault()
84
+ focusIndex(index - 1)
85
+ setCharAt(index - 1, "")
86
+ }
87
+ if (event.key === "ArrowLeft") focusIndex(index - 1)
88
+ if (event.key === "ArrowRight") focusIndex(index + 1)
89
+ }}
90
+ onPaste={(event) => {
91
+ event.preventDefault()
92
+ const pasted = event.clipboardData.getData("text").trim()
93
+ updateValue(pasted)
94
+ focusIndex(Math.min(pasted.length, length - 1))
95
+ }}
96
+ />
97
+ ))}
98
+ </div>
99
+ )
100
+ }
101
+
102
+ export { OtpInput }
@@ -0,0 +1,85 @@
1
+ import * as React from "react"
2
+ import { EyeIcon, EyeOffIcon } from "lucide-react"
3
+
4
+ import { InputDecorator } from "@/components/inputs/input-decorator"
5
+
6
+ export type PasswordInputProps = Omit<
7
+ React.ComponentProps<typeof InputDecorator>,
8
+ "type" | "value" | "onChange"
9
+ > & {
10
+ value?: string | null
11
+ onChange?: React.ChangeEventHandler<HTMLInputElement>
12
+ onValueChange?: (value: string) => void
13
+ visible?: boolean
14
+ defaultVisible?: boolean
15
+ onVisibleChange?: (visible: boolean) => void
16
+ showToggle?: boolean
17
+ wrapperClassName?: string
18
+ inputClassName?: string
19
+ showLabel?: string
20
+ hideLabel?: string
21
+ }
22
+
23
+ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
24
+ (
25
+ {
26
+ value,
27
+ onChange,
28
+ onValueChange,
29
+ visible,
30
+ defaultVisible = false,
31
+ onVisibleChange,
32
+ showToggle = true,
33
+ showLabel = "Show password",
34
+ hideLabel = "Hide password",
35
+ disabled,
36
+ ...props
37
+ },
38
+ ref
39
+ ) => {
40
+ const isControlled = visible !== undefined
41
+ const [internalVisible, setInternalVisible] = React.useState(defaultVisible)
42
+ const currentVisible = isControlled ? visible : internalVisible
43
+
44
+ const setVisibleState = (nextVisible: boolean) => {
45
+ if (!isControlled) {
46
+ setInternalVisible(nextVisible)
47
+ }
48
+
49
+ onVisibleChange?.(nextVisible)
50
+ }
51
+
52
+ const handleChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
53
+ onChange?.(event)
54
+ onValueChange?.(event.target.value)
55
+ }
56
+
57
+ return (
58
+ <InputDecorator
59
+ data-slot="password-input"
60
+ ref={ref}
61
+ type={currentVisible ? "text" : "password"}
62
+ value={value ?? ""}
63
+ disabled={disabled}
64
+ onChange={handleChange}
65
+ trailing={
66
+ showToggle ? (
67
+ <button
68
+ type="button"
69
+ disabled={disabled}
70
+ aria-label={currentVisible ? hideLabel : showLabel}
71
+ className="rounded-sm p-0.5 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
72
+ onClick={() => setVisibleState(!currentVisible)}
73
+ >
74
+ {currentVisible ? <EyeOffIcon className="size-4" /> : <EyeIcon className="size-4" />}
75
+ </button>
76
+ ) : null
77
+ }
78
+ {...props}
79
+ />
80
+ )
81
+ }
82
+ )
83
+ PasswordInput.displayName = "PasswordInput"
84
+
85
+ export { PasswordInput }
@@ -0,0 +1,46 @@
1
+ import * as React from "react"
2
+
3
+ import { MaskedInput, type MaskedInputProps } from "@/components/inputs/masked-input"
4
+
5
+ export type PhoneInputProps = Omit<MaskedInputProps, "mask" | "unmask" | "inputMode"> & {
6
+ countryCode?: string
7
+ maxDigits?: number
8
+ }
9
+
10
+ function onlyDigits(value: string) {
11
+ return value.replace(/\D/g, "")
12
+ }
13
+
14
+ function formatPhoneDigits(digits: string, countryCode = "+998", maxDigits = 12) {
15
+ const normalized = onlyDigits(digits).slice(0, maxDigits)
16
+ const countryDigits = countryCode.replace(/\D/g, "")
17
+ const withoutCountry = normalized.startsWith(countryDigits)
18
+ ? normalized.slice(countryDigits.length)
19
+ : normalized
20
+
21
+ const part1 = withoutCountry.slice(0, 2)
22
+ const part2 = withoutCountry.slice(2, 5)
23
+ const part3 = withoutCountry.slice(5, 7)
24
+ const part4 = withoutCountry.slice(7, 9)
25
+ const parts = [part1, part2, part3, part4].filter(Boolean)
26
+
27
+ return [countryCode, ...parts].join(" ").trim()
28
+ }
29
+
30
+ const PhoneInput = React.forwardRef<HTMLInputElement, PhoneInputProps>(
31
+ ({ countryCode = "+998", maxDigits = 12, onValueChange, ...props }, ref) => {
32
+ return (
33
+ <MaskedInput
34
+ ref={ref}
35
+ inputMode="tel"
36
+ mask={(rawValue) => formatPhoneDigits(rawValue, countryCode, maxDigits)}
37
+ unmask={onlyDigits}
38
+ onValueChange={(maskedValue, rawValue) => onValueChange?.(maskedValue, rawValue)}
39
+ {...props}
40
+ />
41
+ )
42
+ }
43
+ )
44
+ PhoneInput.displayName = "PhoneInput"
45
+
46
+ export { PhoneInput, formatPhoneDigits, onlyDigits }
@@ -0,0 +1,116 @@
1
+ import * as React from "react"
2
+ import { MinusIcon, PlusIcon } from "lucide-react"
3
+
4
+ import { InputChrome } from "@/components/inputs/input-chrome"
5
+ import { getInputValue } from "@/components/inputs/input-value"
6
+ import { clampNumericValue, parseDecimalInput } from "@/components/inputs/numeric-value"
7
+ import { Button } from "@/components/ui/button"
8
+ import { Input } from "@/components/ui/input"
9
+ import { cn } from "@/lib/utils"
10
+
11
+ export type QuantityInputProps = Omit<
12
+ React.ComponentProps<"input">,
13
+ "value" | "onChange" | "type"
14
+ > & {
15
+ value?: number | null
16
+ onChange?: React.ChangeEventHandler<HTMLInputElement>
17
+ onValueChange?: (value: number | null) => void
18
+ min?: number
19
+ max?: number
20
+ step?: number
21
+ showControls?: boolean
22
+ wrapperClassName?: string
23
+ inputClassName?: string
24
+ }
25
+
26
+ function clampQuantity(value: number, min?: number, max?: number) {
27
+ return clampNumericValue(value, min, max)
28
+ }
29
+
30
+ function QuantityInput({
31
+ value,
32
+ onChange,
33
+ onValueChange,
34
+ min,
35
+ max,
36
+ step = 1,
37
+ showControls = true,
38
+ wrapperClassName,
39
+ inputClassName,
40
+ className,
41
+ inputMode = "decimal",
42
+ ...props
43
+ }: QuantityInputProps) {
44
+ const canDecrease = typeof value !== "number" || typeof min !== "number" || value > min
45
+ const canIncrease = typeof value !== "number" || typeof max !== "number" || value < max
46
+
47
+ const setNextValue = (nextValue: number | null) => {
48
+ onValueChange?.(nextValue)
49
+ }
50
+
51
+ const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
52
+ onChange?.(event)
53
+
54
+ const parsed = parseDecimalInput(event.target.value)
55
+ if (parsed == null) {
56
+ setNextValue(null)
57
+ return
58
+ }
59
+
60
+ setNextValue(clampQuantity(parsed, min, max))
61
+ }
62
+
63
+ const handleStep = (direction: 1 | -1) => {
64
+ const baseValue = typeof value === "number" ? value : min ?? 0
65
+ setNextValue(clampQuantity(baseValue + step * direction, min, max))
66
+ }
67
+
68
+ return (
69
+ <InputChrome
70
+ data-slot="quantity-input"
71
+ className={cn("overflow-hidden", wrapperClassName)}
72
+ start={
73
+ showControls ? (
74
+ <Button
75
+ type="button"
76
+ variant="ghost"
77
+ size="icon-sm"
78
+ className="h-full rounded-none"
79
+ disabled={!canDecrease || props.disabled}
80
+ onClick={() => handleStep(-1)}
81
+ >
82
+ <MinusIcon />
83
+ </Button>
84
+ ) : null
85
+ }
86
+ end={
87
+ showControls ? (
88
+ <Button
89
+ type="button"
90
+ variant="ghost"
91
+ size="icon-sm"
92
+ className="h-full rounded-none"
93
+ disabled={!canIncrease || props.disabled}
94
+ onClick={() => handleStep(1)}
95
+ >
96
+ <PlusIcon />
97
+ </Button>
98
+ ) : null
99
+ }
100
+ >
101
+ <Input
102
+ value={getInputValue(value)}
103
+ onChange={handleInputChange}
104
+ inputMode={inputMode}
105
+ className={cn(
106
+ "h-full border-0 bg-transparent text-center shadow-none focus-visible:border-0 focus-visible:ring-0",
107
+ inputClassName,
108
+ className
109
+ )}
110
+ {...props}
111
+ />
112
+ </InputChrome>
113
+ )
114
+ }
115
+
116
+ export { QuantityInput, clampQuantity }
@@ -0,0 +1,49 @@
1
+ import * as React from "react"
2
+ import { MinusIcon, PlusIcon } from "lucide-react"
3
+
4
+ import { Button } from "@/components/ui/button"
5
+ import { Input } from "@/components/ui/input"
6
+ import { cn } from "@/lib/utils"
7
+
8
+ export type QuantityStepperProps = Omit<React.ComponentProps<"div">, "onChange"> & {
9
+ value?: number
10
+ defaultValue?: number
11
+ onValueChange?: (value: number) => void
12
+ min?: number
13
+ max?: number
14
+ step?: number
15
+ disabled?: boolean
16
+ }
17
+
18
+ function QuantityStepper({ value, defaultValue = 0, onValueChange, min = 0, max, step = 1, disabled = false, className, ...props }: QuantityStepperProps) {
19
+ const [internalValue, setInternalValue] = React.useState(defaultValue)
20
+ const currentValue = value ?? internalValue
21
+
22
+ const updateValue = (nextValue: number) => {
23
+ const normalized = Math.max(min, Math.min(max ?? nextValue, Number.isFinite(nextValue) ? nextValue : min))
24
+ if (value === undefined) setInternalValue(normalized)
25
+ onValueChange?.(normalized)
26
+ }
27
+
28
+ return (
29
+ <div data-slot="quantity-stepper" className={cn("inline-flex items-center rounded-lg border bg-background", className)} {...props}>
30
+ <Button type="button" variant="ghost" size="icon-sm" disabled={disabled || currentValue <= min} onClick={() => updateValue(currentValue - step)}>
31
+ <MinusIcon />
32
+ <span className="sr-only">Decrease</span>
33
+ </Button>
34
+ <Input
35
+ value={currentValue}
36
+ disabled={disabled}
37
+ inputMode="numeric"
38
+ className="h-8 w-14 border-0 text-center shadow-none focus-visible:ring-0"
39
+ onChange={(event) => updateValue(Number(event.currentTarget.value))}
40
+ />
41
+ <Button type="button" variant="ghost" size="icon-sm" disabled={disabled || (max !== undefined && currentValue >= max)} onClick={() => updateValue(currentValue + step)}>
42
+ <PlusIcon />
43
+ <span className="sr-only">Increase</span>
44
+ </Button>
45
+ </div>
46
+ )
47
+ }
48
+
49
+ export { QuantityStepper }
@@ -0,0 +1,98 @@
1
+ import * as React from "react"
2
+ import { StarIcon } from "lucide-react"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ export type RatingProps = Omit<React.ComponentProps<"div">, "onChange"> & {
7
+ value?: number
8
+ defaultValue?: number
9
+ onValueChange?: (value: number) => void
10
+ count?: number
11
+ allowClear?: boolean
12
+ disabled?: boolean
13
+ readOnly?: boolean
14
+ labels?: {
15
+ rate?: (value: number) => string
16
+ clear?: string
17
+ }
18
+ icon?: React.ReactNode
19
+ }
20
+
21
+ function Rating({
22
+ value,
23
+ defaultValue = 0,
24
+ onValueChange,
25
+ count = 5,
26
+ allowClear = true,
27
+ disabled = false,
28
+ readOnly = false,
29
+ labels,
30
+ icon,
31
+ className,
32
+ ...props
33
+ }: RatingProps) {
34
+ const [internalValue, setInternalValue] = React.useState(defaultValue)
35
+ const [hoverValue, setHoverValue] = React.useState<number | null>(null)
36
+ const currentValue = value ?? internalValue
37
+ const displayValue = hoverValue ?? currentValue
38
+ const interactive = !disabled && !readOnly
39
+
40
+ const setRating = (nextValue: number) => {
41
+ if (!interactive) return
42
+ const resolvedValue = allowClear && nextValue === currentValue ? 0 : nextValue
43
+ if (value === undefined) setInternalValue(resolvedValue)
44
+ onValueChange?.(resolvedValue)
45
+ }
46
+
47
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>, nextValue: number) => {
48
+ if (!interactive) return
49
+ if (event.key === "Enter" || event.key === " ") {
50
+ event.preventDefault()
51
+ setRating(nextValue)
52
+ }
53
+ }
54
+
55
+ return (
56
+ <div
57
+ data-slot="rating"
58
+ role="radiogroup"
59
+ aria-disabled={disabled || undefined}
60
+ className={cn("inline-flex items-center gap-1", className)}
61
+ onMouseLeave={() => setHoverValue(null)}
62
+ {...props}
63
+ >
64
+ {Array.from({ length: count }, (_, index) => {
65
+ const nextValue = index + 1
66
+ const selected = nextValue <= displayValue
67
+ return (
68
+ <button
69
+ key={nextValue}
70
+ type="button"
71
+ role="radio"
72
+ aria-checked={nextValue === currentValue}
73
+ aria-label={labels?.rate?.(nextValue) ?? `Rate ${nextValue}`}
74
+ disabled={disabled}
75
+ tabIndex={readOnly ? -1 : 0}
76
+ className={cn(
77
+ "rounded-sm text-muted-foreground outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
78
+ interactive && "hover:text-primary",
79
+ selected && "text-primary"
80
+ )}
81
+ onMouseEnter={() => interactive && setHoverValue(nextValue)}
82
+ onClick={() => setRating(nextValue)}
83
+ onKeyDown={(event) => handleKeyDown(event, nextValue)}
84
+ >
85
+ {icon ?? <StarIcon className={cn("size-5", selected && "fill-current")} />}
86
+ </button>
87
+ )
88
+ })}
89
+ {allowClear && currentValue > 0 && interactive && (
90
+ <button type="button" className="ml-1 text-xs text-muted-foreground hover:text-foreground" onClick={() => setRating(0)}>
91
+ {labels?.clear ?? "Clear"}
92
+ </button>
93
+ )}
94
+ </div>
95
+ )
96
+ }
97
+
98
+ export { Rating }
@@ -0,0 +1,26 @@
1
+ import * as React from "react"
2
+ import { SearchIcon } from "lucide-react"
3
+
4
+ import { ClearableInput, type ClearableInputProps } from "@/components/inputs/clearable-input"
5
+
6
+ export type SearchInputProps = Omit<ClearableInputProps, "leadingIcon" | "type"> & {
7
+ searchIcon?: React.ReactNode
8
+ }
9
+
10
+ const SearchInput = React.forwardRef<HTMLInputElement, SearchInputProps>(
11
+ ({ searchIcon, placeholder = "Search...", inputMode = "search", ...props }, ref) => {
12
+ return (
13
+ <ClearableInput
14
+ ref={ref}
15
+ type="search"
16
+ inputMode={inputMode}
17
+ placeholder={placeholder}
18
+ leadingIcon={searchIcon ?? <SearchIcon />}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+ )
24
+ SearchInput.displayName = "SearchInput"
25
+
26
+ export { SearchInput }
@@ -0,0 +1,72 @@
1
+ import * as React from "react"
2
+
3
+ import {
4
+ Select,
5
+ SelectContent,
6
+ SelectItem,
7
+ SelectTrigger,
8
+ SelectValue,
9
+ } from "@/components/ui/select"
10
+ import { cn } from "@/lib/utils"
11
+
12
+ export type SimpleSelectOption = {
13
+ label: React.ReactNode
14
+ value: string
15
+ disabled?: boolean
16
+ description?: React.ReactNode
17
+ }
18
+
19
+ export type SimpleSelectProps = Omit<
20
+ React.ComponentProps<typeof Select>,
21
+ "value" | "onValueChange"
22
+ > & {
23
+ value?: string
24
+ onValueChange?: (value: string) => void
25
+ options: SimpleSelectOption[]
26
+ placeholder?: string
27
+ size?: "sm" | "default"
28
+ triggerClassName?: string
29
+ contentClassName?: string
30
+ itemClassName?: string
31
+ }
32
+
33
+ function SimpleSelect({
34
+ value,
35
+ onValueChange,
36
+ options,
37
+ placeholder = "Select",
38
+ size = "default",
39
+ triggerClassName,
40
+ contentClassName,
41
+ itemClassName,
42
+ ...props
43
+ }: SimpleSelectProps) {
44
+ return (
45
+ <Select value={value} onValueChange={(val) => onValueChange?.(val as string)} {...props}>
46
+ <SelectTrigger size={size} className={cn("w-full", triggerClassName)}>
47
+ <SelectValue placeholder={placeholder} />
48
+ </SelectTrigger>
49
+ <SelectContent className={contentClassName}>
50
+ {options.map((option) => (
51
+ <SelectItem
52
+ key={option.value}
53
+ value={option.value}
54
+ disabled={option.disabled}
55
+ className={itemClassName}
56
+ >
57
+ <span className="flex min-w-0 flex-col">
58
+ <span className="truncate">{option.label}</span>
59
+ {option.description && (
60
+ <span className="truncate text-xs text-muted-foreground">
61
+ {option.description}
62
+ </span>
63
+ )}
64
+ </span>
65
+ </SelectItem>
66
+ ))}
67
+ </SelectContent>
68
+ </Select>
69
+ )
70
+ }
71
+
72
+ export { SimpleSelect }