@carefully-built/cli 0.1.1 → 0.1.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 +101 -80
  2. package/dist/index.mjs +8 -5
  3. package/dist/index.mjs.map +1 -1
  4. package/package.json +3 -3
  5. package/registry/ui/avatar/manifest.json +33 -0
  6. package/registry/ui/avatar/primitives/avatar.tsx +64 -0
  7. package/registry/ui/avatar/utils/cn.ts +6 -0
  8. package/registry/ui/button/manifest.json +24 -5
  9. package/registry/ui/button/utils/cn.ts +6 -0
  10. package/registry/ui/calendar/manifest.json +35 -0
  11. package/registry/ui/calendar/primitives/button.tsx +89 -0
  12. package/registry/ui/calendar/primitives/calendar.tsx +68 -0
  13. package/registry/ui/calendar/utils/cn.ts +6 -0
  14. package/registry/ui/card/manifest.json +36 -0
  15. package/registry/ui/card/primitives/card.tsx +80 -0
  16. package/registry/ui/card/utils/cn.ts +6 -0
  17. package/registry/ui/chip/manifest.json +36 -0
  18. package/registry/ui/chip/primitives/chip-utils.ts +10 -0
  19. package/registry/ui/chip/primitives/chip.tsx +74 -0
  20. package/registry/ui/chip/utils/cn.ts +6 -0
  21. package/registry/ui/chip-utils/manifest.json +33 -0
  22. package/registry/ui/chip-utils/primitives/chip-utils.ts +10 -0
  23. package/registry/ui/chip-utils/utils/cn.ts +6 -0
  24. package/registry/ui/date-display/manifest.json +33 -0
  25. package/registry/ui/date-display/utils/cn.ts +6 -0
  26. package/registry/ui/date-display/utils/date-display.ts +61 -0
  27. package/registry/ui/dialog/manifest.json +43 -0
  28. package/registry/ui/dialog/primitives/button.tsx +89 -0
  29. package/registry/ui/dialog/primitives/dialog.tsx +147 -0
  30. package/registry/ui/dialog/utils/cn.ts +6 -0
  31. package/registry/ui/display-date/manifest.json +36 -0
  32. package/registry/ui/display-date/primitives/display-date.tsx +20 -0
  33. package/registry/ui/display-date/utils/cn.ts +6 -0
  34. package/registry/ui/display-date/utils/date-display.ts +61 -0
  35. package/registry/ui/drawer/manifest.json +37 -0
  36. package/registry/ui/drawer/primitives/drawer.tsx +99 -0
  37. package/registry/ui/drawer/utils/cn.ts +6 -0
  38. package/registry/ui/dropdown-menu/manifest.json +37 -0
  39. package/registry/ui/dropdown-menu/primitives/dropdown-menu.tsx +140 -0
  40. package/registry/ui/dropdown-menu/utils/cn.ts +6 -0
  41. package/registry/ui/empty-state/empty-state/collection-empty-state.ts +29 -0
  42. package/registry/ui/empty-state/empty-state/empty-state-card.tsx +72 -0
  43. package/registry/ui/empty-state/empty-state/index.ts +8 -0
  44. package/registry/ui/empty-state/empty-state/initial-empty-state.tsx +36 -0
  45. package/registry/ui/empty-state/empty-state/no-results-state.tsx +20 -0
  46. package/registry/ui/empty-state/manifest.json +63 -0
  47. package/registry/ui/empty-state/primitives/button.tsx +89 -0
  48. package/registry/ui/empty-state/primitives/card.tsx +80 -0
  49. package/registry/ui/empty-state/utils/cn.ts +6 -0
  50. package/registry/ui/error-page/error-page/error-code.tsx +16 -0
  51. package/registry/ui/error-page/error-page/error-page-content.ts +75 -0
  52. package/registry/ui/error-page/error-page/index.ts +19 -0
  53. package/registry/ui/error-page/error-page/posthog-error-capture.ts +83 -0
  54. package/registry/ui/error-page/error-page/saas-error-page.tsx +146 -0
  55. package/registry/ui/error-page/manifest.json +64 -0
  56. package/registry/ui/error-page/primitives/button.tsx +89 -0
  57. package/registry/ui/error-page/utils/cn.ts +6 -0
  58. package/registry/ui/field-detail-row/manifest.json +32 -0
  59. package/registry/ui/field-detail-row/primitives/field-detail-row.tsx +28 -0
  60. package/registry/ui/field-detail-row/utils/cn.ts +6 -0
  61. package/registry/ui/file-dropzone/manifest.json +35 -0
  62. package/registry/ui/file-dropzone/primitives/button.tsx +89 -0
  63. package/registry/ui/file-dropzone/primitives/file-dropzone.tsx +236 -0
  64. package/registry/ui/file-dropzone/utils/cn.ts +6 -0
  65. package/registry/ui/help-info-button/manifest.json +72 -0
  66. package/registry/ui/help-info-button/overlays/responsive-sheet.footer.tsx +88 -0
  67. package/registry/ui/help-info-button/overlays/responsive-sheet.layouts.tsx +207 -0
  68. package/registry/ui/help-info-button/overlays/responsive-sheet.shortcuts.ts +103 -0
  69. package/registry/ui/help-info-button/overlays/responsive-sheet.tsx +132 -0
  70. package/registry/ui/help-info-button/primitives/button.tsx +89 -0
  71. package/registry/ui/help-info-button/primitives/drawer.tsx +99 -0
  72. package/registry/ui/help-info-button/primitives/help-info-button.tsx +63 -0
  73. package/registry/ui/help-info-button/primitives/keyboard-shortcut-hint.tsx +40 -0
  74. package/registry/ui/help-info-button/primitives/sheet.tsx +103 -0
  75. package/registry/ui/help-info-button/primitives/tooltip.tsx +57 -0
  76. package/registry/ui/help-info-button/utils/cn.ts +6 -0
  77. package/registry/ui/help-info-button/utils/use-media-query.ts +28 -0
  78. package/registry/ui/input/manifest.json +31 -0
  79. package/registry/ui/input/primitives/input.tsx +19 -0
  80. package/registry/ui/input/utils/cn.ts +6 -0
  81. package/registry/ui/keyboard-shortcut-hint/manifest.json +32 -0
  82. package/registry/ui/keyboard-shortcut-hint/primitives/keyboard-shortcut-hint.tsx +40 -0
  83. package/registry/ui/keyboard-shortcut-hint/utils/cn.ts +6 -0
  84. package/registry/ui/label/manifest.json +31 -0
  85. package/registry/ui/label/primitives/label.tsx +21 -0
  86. package/registry/ui/label/utils/cn.ts +6 -0
  87. package/registry/ui/pagination/manifest.json +36 -0
  88. package/registry/ui/pagination/primitives/button.tsx +89 -0
  89. package/registry/ui/pagination/primitives/pagination.tsx +143 -0
  90. package/registry/ui/pagination/utils/cn.ts +6 -0
  91. package/registry/ui/popover/manifest.json +33 -0
  92. package/registry/ui/popover/primitives/popover.tsx +46 -0
  93. package/registry/ui/popover/utils/cn.ts +6 -0
  94. package/registry/ui/responsive-sheet/manifest.json +66 -0
  95. package/registry/ui/responsive-sheet/overlays/responsive-sheet.footer.tsx +88 -0
  96. package/registry/ui/responsive-sheet/overlays/responsive-sheet.layouts.tsx +207 -0
  97. package/registry/ui/responsive-sheet/overlays/responsive-sheet.shortcuts.ts +103 -0
  98. package/registry/ui/responsive-sheet/overlays/responsive-sheet.tsx +132 -0
  99. package/registry/ui/responsive-sheet/primitives/button.tsx +89 -0
  100. package/registry/ui/responsive-sheet/primitives/drawer.tsx +99 -0
  101. package/registry/ui/responsive-sheet/primitives/keyboard-shortcut-hint.tsx +40 -0
  102. package/registry/ui/responsive-sheet/primitives/sheet.tsx +103 -0
  103. package/registry/ui/responsive-sheet/utils/cn.ts +6 -0
  104. package/registry/ui/responsive-sheet/utils/use-media-query.ts +28 -0
  105. package/registry/ui/responsive-sheet.footer/manifest.json +40 -0
  106. package/registry/ui/responsive-sheet.footer/overlays/responsive-sheet.footer.tsx +88 -0
  107. package/registry/ui/responsive-sheet.footer/primitives/button.tsx +89 -0
  108. package/registry/ui/responsive-sheet.footer/primitives/keyboard-shortcut-hint.tsx +40 -0
  109. package/registry/ui/responsive-sheet.footer/utils/cn.ts +6 -0
  110. package/registry/ui/responsive-sheet.shortcuts/manifest.json +34 -0
  111. package/registry/ui/responsive-sheet.shortcuts/overlays/responsive-sheet.shortcuts.ts +103 -0
  112. package/registry/ui/responsive-sheet.shortcuts/utils/cn.ts +6 -0
  113. package/registry/ui/scroll-fade-area/manifest.json +31 -0
  114. package/registry/ui/scroll-fade-area/primitives/scroll-fade-area.tsx +295 -0
  115. package/registry/ui/scroll-fade-area/utils/cn.ts +6 -0
  116. package/registry/ui/search/manifest.json +35 -0
  117. package/registry/ui/search/utils/cn.ts +6 -0
  118. package/registry/ui/search/utils/search.ts +227 -0
  119. package/registry/ui/searchable-select/manifest.json +48 -0
  120. package/registry/ui/searchable-select/primitives/input.tsx +19 -0
  121. package/registry/ui/searchable-select/search/searchable-select-position.ts +95 -0
  122. package/registry/ui/searchable-select/search/searchable-select.tsx +431 -0
  123. package/registry/ui/searchable-select/utils/cn.ts +6 -0
  124. package/registry/ui/searchable-select/utils/search.ts +227 -0
  125. package/registry/ui/searchable-select-position/manifest.json +32 -0
  126. package/registry/ui/searchable-select-position/search/searchable-select-position.ts +95 -0
  127. package/registry/ui/searchable-select-position/utils/cn.ts +6 -0
  128. package/registry/ui/segmented-toggle/manifest.json +41 -0
  129. package/registry/ui/segmented-toggle/primitives/scroll-fade-area.tsx +295 -0
  130. package/registry/ui/segmented-toggle/primitives/segmented-toggle.tsx +106 -0
  131. package/registry/ui/segmented-toggle/primitives/tabs.tsx +97 -0
  132. package/registry/ui/segmented-toggle/utils/cn.ts +6 -0
  133. package/registry/ui/select/manifest.json +37 -0
  134. package/registry/ui/select/primitives/select.tsx +142 -0
  135. package/registry/ui/select/utils/cn.ts +6 -0
  136. package/registry/ui/sheet/manifest.json +39 -0
  137. package/registry/ui/sheet/primitives/button.tsx +89 -0
  138. package/registry/ui/sheet/primitives/sheet.tsx +103 -0
  139. package/registry/ui/sheet/utils/cn.ts +6 -0
  140. package/registry/ui/skeleton/manifest.json +31 -0
  141. package/registry/ui/skeleton/primitives/skeleton.tsx +13 -0
  142. package/registry/ui/skeleton/utils/cn.ts +6 -0
  143. package/registry/ui/smart-table/manifest.json +115 -0
  144. package/registry/ui/smart-table/primitives/button.tsx +89 -0
  145. package/registry/ui/smart-table/primitives/card.tsx +80 -0
  146. package/registry/ui/smart-table/primitives/display-date.tsx +20 -0
  147. package/registry/ui/smart-table/primitives/pagination.tsx +143 -0
  148. package/registry/ui/smart-table/primitives/skeleton.tsx +13 -0
  149. package/registry/ui/smart-table/primitives/table.tsx +92 -0
  150. package/registry/ui/smart-table/primitives/tooltip.tsx +57 -0
  151. package/registry/ui/smart-table/smart-table/DesktopView.tsx +343 -0
  152. package/registry/ui/smart-table/smart-table/MobileView.tsx +170 -0
  153. package/registry/ui/smart-table/smart-table/SmartTable.tsx +85 -0
  154. package/registry/ui/smart-table/smart-table/SmartTableActions.tsx +71 -0
  155. package/registry/ui/smart-table/smart-table/TruncatedContent.tsx +147 -0
  156. package/registry/ui/smart-table/smart-table/index.ts +15 -0
  157. package/registry/ui/smart-table/smart-table/sorting.ts +148 -0
  158. package/registry/ui/smart-table/smart-table/truncated-content.utils.ts +22 -0
  159. package/registry/ui/smart-table/smart-table/types.ts +95 -0
  160. package/registry/ui/smart-table/smart-table/utils.ts +150 -0
  161. package/registry/ui/smart-table/utils/cn.ts +6 -0
  162. package/registry/ui/smart-table/utils/date-display.ts +61 -0
  163. package/registry/ui/smart-table/utils/use-media-query.ts +28 -0
  164. package/registry/ui/switch/manifest.json +31 -0
  165. package/registry/ui/switch/primitives/switch.tsx +31 -0
  166. package/registry/ui/switch/utils/cn.ts +6 -0
  167. package/registry/ui/table/manifest.json +38 -0
  168. package/registry/ui/table/primitives/table.tsx +92 -0
  169. package/registry/ui/table/utils/cn.ts +6 -0
  170. package/registry/ui/table-toolbar/manifest.json +93 -0
  171. package/registry/ui/table-toolbar/overlays/responsive-sheet.footer.tsx +88 -0
  172. package/registry/ui/table-toolbar/overlays/responsive-sheet.layouts.tsx +207 -0
  173. package/registry/ui/table-toolbar/overlays/responsive-sheet.shortcuts.ts +103 -0
  174. package/registry/ui/table-toolbar/overlays/responsive-sheet.tsx +132 -0
  175. package/registry/ui/table-toolbar/primitives/button.tsx +89 -0
  176. package/registry/ui/table-toolbar/primitives/drawer.tsx +99 -0
  177. package/registry/ui/table-toolbar/primitives/input.tsx +19 -0
  178. package/registry/ui/table-toolbar/primitives/keyboard-shortcut-hint.tsx +40 -0
  179. package/registry/ui/table-toolbar/primitives/sheet.tsx +103 -0
  180. package/registry/ui/table-toolbar/search/searchable-select-position.ts +95 -0
  181. package/registry/ui/table-toolbar/search/searchable-select.tsx +431 -0
  182. package/registry/ui/table-toolbar/table-toolbar/index.ts +9 -0
  183. package/registry/ui/table-toolbar/table-toolbar/table-toolbar.tsx +552 -0
  184. package/registry/ui/table-toolbar/utils/cn.ts +6 -0
  185. package/registry/ui/table-toolbar/utils/search.ts +227 -0
  186. package/registry/ui/table-toolbar/utils/use-media-query.ts +28 -0
  187. package/registry/ui/tabs/manifest.json +40 -0
  188. package/registry/ui/tabs/primitives/scroll-fade-area.tsx +295 -0
  189. package/registry/ui/tabs/primitives/tabs.tsx +97 -0
  190. package/registry/ui/tabs/utils/cn.ts +6 -0
  191. package/registry/ui/textarea/manifest.json +31 -0
  192. package/registry/ui/textarea/primitives/textarea.tsx +18 -0
  193. package/registry/ui/textarea/utils/cn.ts +6 -0
  194. package/registry/ui/tooltip/manifest.json +34 -0
  195. package/registry/ui/tooltip/primitives/tooltip.tsx +57 -0
  196. package/registry/ui/tooltip/utils/cn.ts +6 -0
  197. package/registry/ui/use-media-query/manifest.json +32 -0
  198. package/registry/ui/use-media-query/utils/cn.ts +6 -0
  199. package/registry/ui/use-media-query/utils/use-media-query.ts +28 -0
  200. package/registry/ui/user-picker/manifest.json +52 -0
  201. package/registry/ui/user-picker/primitives/avatar.tsx +64 -0
  202. package/registry/ui/user-picker/primitives/button.tsx +89 -0
  203. package/registry/ui/user-picker/primitives/input.tsx +19 -0
  204. package/registry/ui/user-picker/primitives/popover.tsx +46 -0
  205. package/registry/ui/user-picker/primitives/user-picker-utils.ts +113 -0
  206. package/registry/ui/user-picker/primitives/user-picker.tsx +226 -0
  207. package/registry/ui/user-picker/utils/cn.ts +6 -0
  208. package/registry/ui/user-picker-utils/manifest.json +38 -0
  209. package/registry/ui/user-picker-utils/primitives/user-picker-utils.ts +113 -0
  210. package/registry/ui/user-picker-utils/utils/cn.ts +6 -0
  211. package/assets/hero.png +0 -0
  212. package/registry/ui/button/cn.ts +0 -6
  213. /package/registry/ui/button/{button.tsx → primitives/button.tsx} +0 -0
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]): string {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "scroll-fade-area",
3
+ "description": "Editable source registry entry for scroll-fade-area.",
4
+ "importPath": "@carefully-built/ui",
5
+ "exports": [
6
+ "ScrollFadeArea"
7
+ ],
8
+ "dependencies": [
9
+ "class-variance-authority",
10
+ "clsx",
11
+ "tailwind-merge"
12
+ ],
13
+ "peerDependencies": [
14
+ "react",
15
+ "react-dom",
16
+ "radix-ui",
17
+ "lucide-react",
18
+ "react-day-picker",
19
+ "vaul"
20
+ ],
21
+ "files": [
22
+ {
23
+ "source": "primitives/scroll-fade-area.tsx",
24
+ "target": "components/ui/scroll-fade-area.tsx"
25
+ },
26
+ {
27
+ "source": "utils/cn.ts",
28
+ "target": "lib/utils.ts"
29
+ }
30
+ ]
31
+ }
@@ -0,0 +1,295 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+
5
+ import type { PointerEvent } from 'react';
6
+
7
+ import { cn } from '@/lib/utils';
8
+
9
+ type ScrollFadeOrientation = 'horizontal' | 'vertical';
10
+
11
+ interface ScrollFadeAreaProps extends React.ComponentProps<'div'> {
12
+ readonly fadeSize?: number;
13
+ readonly orientation?: ScrollFadeOrientation;
14
+ readonly scrollbarVisibility?: 'hidden' | 'section-hover';
15
+ readonly viewportClassName?: string;
16
+ }
17
+
18
+ interface VerticalScrollbarState {
19
+ readonly isScrollable: boolean;
20
+ readonly thumbHeight: number;
21
+ readonly thumbTop: number;
22
+ }
23
+
24
+ const MIN_THUMB_SIZE = 40;
25
+ const SCROLLBAR_INSET = 6;
26
+
27
+ function getScrollFadeMask({
28
+ canScrollEnd,
29
+ canScrollStart,
30
+ orientation,
31
+ }: {
32
+ readonly canScrollEnd: boolean;
33
+ readonly canScrollStart: boolean;
34
+ readonly orientation: ScrollFadeOrientation;
35
+ }): string {
36
+ const direction = orientation === 'horizontal' ? 'right' : 'bottom';
37
+
38
+ if (canScrollStart && canScrollEnd) {
39
+ return `linear-gradient(to ${direction}, transparent, black var(--scroll-fade-size), black calc(100% - var(--scroll-fade-size)), transparent)`;
40
+ }
41
+
42
+ if (canScrollStart) {
43
+ return `linear-gradient(to ${direction}, transparent, black var(--scroll-fade-size))`;
44
+ }
45
+
46
+ if (canScrollEnd) {
47
+ return `linear-gradient(to ${direction}, black calc(100% - var(--scroll-fade-size)), transparent)`;
48
+ }
49
+
50
+ return 'none';
51
+ }
52
+
53
+ function getScrollState(
54
+ scrollArea: HTMLDivElement,
55
+ orientation: ScrollFadeOrientation,
56
+ ): {
57
+ canScrollEnd: boolean;
58
+ canScrollStart: boolean;
59
+ } {
60
+ if (orientation === 'horizontal') {
61
+ return {
62
+ canScrollStart: scrollArea.scrollLeft > 1,
63
+ canScrollEnd: scrollArea.scrollLeft + scrollArea.clientWidth < scrollArea.scrollWidth - 1,
64
+ };
65
+ }
66
+
67
+ return {
68
+ canScrollStart: scrollArea.scrollTop > 1,
69
+ canScrollEnd: scrollArea.scrollTop + scrollArea.clientHeight < scrollArea.scrollHeight - 1,
70
+ };
71
+ }
72
+
73
+ function getVerticalScrollbarState(scrollArea: HTMLDivElement): VerticalScrollbarState {
74
+ const trackHeight = scrollArea.clientHeight - SCROLLBAR_INSET * 2;
75
+ const maxScrollTop = scrollArea.scrollHeight - scrollArea.clientHeight;
76
+
77
+ if (maxScrollTop <= 0 || trackHeight <= 0) {
78
+ return {
79
+ isScrollable: false,
80
+ thumbHeight: 0,
81
+ thumbTop: 0,
82
+ };
83
+ }
84
+
85
+ const thumbHeight = Math.max(
86
+ (scrollArea.clientHeight / scrollArea.scrollHeight) * trackHeight,
87
+ MIN_THUMB_SIZE,
88
+ );
89
+ const maxThumbTop = trackHeight - thumbHeight;
90
+ const scrollProgress = scrollArea.scrollTop / maxScrollTop;
91
+
92
+ return {
93
+ isScrollable: true,
94
+ thumbHeight,
95
+ thumbTop: maxThumbTop * scrollProgress,
96
+ };
97
+ }
98
+
99
+ export function ScrollFadeArea({
100
+ children,
101
+ className,
102
+ fadeSize = 24,
103
+ onPointerEnter,
104
+ onPointerLeave,
105
+ onScroll,
106
+ orientation = 'vertical',
107
+ scrollbarVisibility = 'hidden',
108
+ style,
109
+ viewportClassName,
110
+ ...props
111
+ }: ScrollFadeAreaProps): React.ReactElement {
112
+ const scrollAreaRef = React.useRef<HTMLDivElement>(null);
113
+ const dragOffsetRef = React.useRef(0);
114
+ const [scrollState, setScrollState] = React.useState({
115
+ canScrollStart: false,
116
+ canScrollEnd: false,
117
+ });
118
+ const [verticalScrollbarState, setVerticalScrollbarState] =
119
+ React.useState<VerticalScrollbarState>({
120
+ isScrollable: false,
121
+ thumbHeight: 0,
122
+ thumbTop: 0,
123
+ });
124
+ const [isSectionActive, setIsSectionActive] = React.useState(false);
125
+ const [isDraggingScrollbar, setIsDraggingScrollbar] = React.useState(false);
126
+ const shouldRenderSectionScrollbar =
127
+ orientation === 'vertical' && scrollbarVisibility === 'section-hover';
128
+ const isSectionScrollbarVisible = isSectionActive || isDraggingScrollbar;
129
+
130
+ const updateScrollState = React.useCallback(() => {
131
+ const scrollArea = scrollAreaRef.current;
132
+
133
+ if (!scrollArea) {
134
+ return;
135
+ }
136
+
137
+ const nextScrollState = getScrollState(scrollArea, orientation);
138
+
139
+ setScrollState((currentScrollState) => {
140
+ if (
141
+ currentScrollState.canScrollStart === nextScrollState.canScrollStart &&
142
+ currentScrollState.canScrollEnd === nextScrollState.canScrollEnd
143
+ ) {
144
+ return currentScrollState;
145
+ }
146
+
147
+ return nextScrollState;
148
+ });
149
+
150
+ if (shouldRenderSectionScrollbar) {
151
+ setVerticalScrollbarState(getVerticalScrollbarState(scrollArea));
152
+ }
153
+ }, [orientation, shouldRenderSectionScrollbar]);
154
+
155
+ const scrollToThumbPosition = React.useCallback(
156
+ (thumbTop: number): void => {
157
+ const scrollArea = scrollAreaRef.current;
158
+
159
+ if (!scrollArea) {
160
+ return;
161
+ }
162
+
163
+ const trackHeight = scrollArea.clientHeight - SCROLLBAR_INSET * 2;
164
+ const maxThumbTop = trackHeight - verticalScrollbarState.thumbHeight;
165
+ const maxScrollTop = scrollArea.scrollHeight - scrollArea.clientHeight;
166
+ const clampedThumbTop = Math.min(Math.max(thumbTop, 0), maxThumbTop);
167
+ const scrollProgress = maxThumbTop > 0 ? clampedThumbTop / maxThumbTop : 0;
168
+
169
+ scrollArea.scrollTop = maxScrollTop * scrollProgress;
170
+ updateScrollState();
171
+ },
172
+ [updateScrollState, verticalScrollbarState.thumbHeight],
173
+ );
174
+
175
+ function handleTrackPointerDown(event: PointerEvent<HTMLDivElement>): void {
176
+ if (event.target !== event.currentTarget) {
177
+ return;
178
+ }
179
+
180
+ scrollToThumbPosition(event.nativeEvent.offsetY - verticalScrollbarState.thumbHeight / 2);
181
+ }
182
+
183
+ function handleThumbPointerDown(event: PointerEvent<HTMLDivElement>): void {
184
+ event.preventDefault();
185
+ event.currentTarget.setPointerCapture(event.pointerId);
186
+ setIsDraggingScrollbar(true);
187
+ dragOffsetRef.current = event.clientY - SCROLLBAR_INSET - verticalScrollbarState.thumbTop;
188
+ }
189
+
190
+ function handleThumbPointerMove(event: PointerEvent<HTMLDivElement>): void {
191
+ if (!event.currentTarget.hasPointerCapture(event.pointerId)) {
192
+ return;
193
+ }
194
+
195
+ scrollToThumbPosition(event.clientY - SCROLLBAR_INSET - dragOffsetRef.current);
196
+ }
197
+
198
+ function handleThumbPointerUp(event: PointerEvent<HTMLDivElement>): void {
199
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
200
+ event.currentTarget.releasePointerCapture(event.pointerId);
201
+ }
202
+
203
+ setIsDraggingScrollbar(false);
204
+ }
205
+
206
+ React.useEffect(() => {
207
+ const scrollArea = scrollAreaRef.current;
208
+
209
+ if (!scrollArea) {
210
+ return;
211
+ }
212
+
213
+ updateScrollState();
214
+
215
+ const resizeObserver = new ResizeObserver(updateScrollState);
216
+ resizeObserver.observe(scrollArea);
217
+
218
+ if (scrollArea.firstElementChild) {
219
+ resizeObserver.observe(scrollArea.firstElementChild);
220
+ }
221
+
222
+ return () => {
223
+ resizeObserver.disconnect();
224
+ };
225
+ }, [children, updateScrollState]);
226
+
227
+ const maskImage = getScrollFadeMask({
228
+ canScrollEnd: scrollState.canScrollEnd,
229
+ canScrollStart: scrollState.canScrollStart,
230
+ orientation,
231
+ });
232
+ const maskStyle = {
233
+ '--scroll-fade-size': `${String(fadeSize)}px`,
234
+ WebkitMaskImage: maskImage,
235
+ maskImage,
236
+ } satisfies React.CSSProperties & Record<'--scroll-fade-size', string>;
237
+
238
+ return (
239
+ <div
240
+ className={cn('group/scroll-fade relative min-h-0 min-w-0', className)}
241
+ style={style}
242
+ onPointerEnter={(event) => {
243
+ setIsSectionActive(true);
244
+ onPointerEnter?.(event);
245
+ }}
246
+ onPointerLeave={(event) => {
247
+ setIsSectionActive(false);
248
+ onPointerLeave?.(event);
249
+ }}
250
+ {...props}
251
+ >
252
+ <div
253
+ ref={scrollAreaRef}
254
+ className={cn(
255
+ 'min-h-0 min-w-0',
256
+ orientation === 'horizontal' ? 'overflow-x-auto' : 'h-full overflow-y-auto',
257
+ '[-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
258
+ viewportClassName,
259
+ )}
260
+ style={maskStyle}
261
+ onScroll={(event) => {
262
+ updateScrollState();
263
+ onScroll?.(event);
264
+ }}
265
+ >
266
+ {children}
267
+ </div>
268
+ {shouldRenderSectionScrollbar && verticalScrollbarState.isScrollable ? (
269
+ <div
270
+ aria-hidden="true"
271
+ className={cn(
272
+ 'absolute top-1.5 right-0 bottom-1.5 z-10 w-3 transition-opacity duration-150',
273
+ isSectionScrollbarVisible
274
+ ? 'pointer-events-auto opacity-100'
275
+ : 'pointer-events-none opacity-0',
276
+ )}
277
+ data-section-scrollbar="true"
278
+ onPointerDown={handleTrackPointerDown}
279
+ >
280
+ <div
281
+ className="absolute right-1 w-1.5 rounded-full bg-black/15 transition-colors hover:bg-black/25"
282
+ onPointerDown={handleThumbPointerDown}
283
+ onPointerMove={handleThumbPointerMove}
284
+ onPointerUp={handleThumbPointerUp}
285
+ onPointerCancel={handleThumbPointerUp}
286
+ style={{
287
+ height: `${String(verticalScrollbarState.thumbHeight)}px`,
288
+ transform: `translateY(${String(verticalScrollbarState.thumbTop)}px)`,
289
+ }}
290
+ />
291
+ </div>
292
+ ) : null}
293
+ </div>
294
+ );
295
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]): string {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "search",
3
+ "description": "Editable source registry entry for search.",
4
+ "importPath": "@carefully-built/ui",
5
+ "exports": [
6
+ "SearchTextPart",
7
+ "buildSearchText",
8
+ "filterAndRankBySearch",
9
+ "rankBySearch",
10
+ "scoreFuzzyMatch"
11
+ ],
12
+ "dependencies": [
13
+ "class-variance-authority",
14
+ "clsx",
15
+ "tailwind-merge"
16
+ ],
17
+ "peerDependencies": [
18
+ "react",
19
+ "react-dom",
20
+ "radix-ui",
21
+ "lucide-react",
22
+ "react-day-picker",
23
+ "vaul"
24
+ ],
25
+ "files": [
26
+ {
27
+ "source": "utils/cn.ts",
28
+ "target": "lib/utils.ts"
29
+ },
30
+ {
31
+ "source": "utils/search.ts",
32
+ "target": "components/ui/search.ts"
33
+ }
34
+ ]
35
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]): string {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,227 @@
1
+ function normalizeSearchValue(value: string): string {
2
+ return value
3
+ .normalize('NFD')
4
+ .replace(/\p{Diacritic}/gu, '')
5
+ .toLocaleLowerCase()
6
+ .replace(/[^\p{L}\p{N}\s]/gu, ' ')
7
+ .replace(/\s+/g, ' ')
8
+ .trim();
9
+ }
10
+
11
+ function tokenize(value: string): string[] {
12
+ return normalizeSearchValue(value)
13
+ .split(' ')
14
+ .filter((token) => token.length > 0);
15
+ }
16
+
17
+ function flattenSearchPart(
18
+ part: SearchTextPart,
19
+ fragments: string[],
20
+ ): void {
21
+ if (typeof part === 'string') {
22
+ const normalized = part.trim();
23
+ if (normalized.length > 0) {
24
+ fragments.push(normalized);
25
+ }
26
+ return;
27
+ }
28
+
29
+ if (!part) {
30
+ return;
31
+ }
32
+
33
+ for (const nestedPart of part) {
34
+ flattenSearchPart(nestedPart, fragments);
35
+ }
36
+ }
37
+
38
+ function getMaxDistance(term: string): number {
39
+ if (term.length <= 4) {
40
+ return 1;
41
+ }
42
+
43
+ if (term.length <= 8) {
44
+ return 2;
45
+ }
46
+
47
+ return 3;
48
+ }
49
+
50
+ function damerauLevenshtein(left: string, right: string): number {
51
+ const rows = left.length + 1;
52
+ const columns = right.length + 1;
53
+ const matrix = Array.from({ length: rows }, () => Array<number>(columns).fill(0));
54
+
55
+ for (let row = 0; row < rows; row += 1) {
56
+ const matrixRow = matrix[row];
57
+ if (!matrixRow) {
58
+ continue;
59
+ }
60
+ matrixRow[0] = row;
61
+ }
62
+
63
+ for (let column = 0; column < columns; column += 1) {
64
+ const firstRow = matrix[0];
65
+ if (!firstRow) {
66
+ return 0;
67
+ }
68
+ firstRow[column] = column;
69
+ }
70
+
71
+ for (let row = 1; row < rows; row += 1) {
72
+ const currentRow = matrix[row];
73
+ const previousRow = matrix[row - 1];
74
+ if (!currentRow || !previousRow) {
75
+ continue;
76
+ }
77
+
78
+ for (let column = 1; column < columns; column += 1) {
79
+ const substitutionCost = left[row - 1] === right[column - 1] ? 0 : 1;
80
+ const leftCost = currentRow[column - 1];
81
+ const topCost = previousRow[column];
82
+ const diagonalCost = previousRow[column - 1];
83
+
84
+ if (
85
+ leftCost === undefined ||
86
+ topCost === undefined ||
87
+ diagonalCost === undefined
88
+ ) {
89
+ continue;
90
+ }
91
+
92
+ currentRow[column] = Math.min(
93
+ topCost + 1,
94
+ leftCost + 1,
95
+ diagonalCost + substitutionCost,
96
+ );
97
+
98
+ if (
99
+ row > 1 &&
100
+ column > 1 &&
101
+ left[row - 1] === right[column - 2] &&
102
+ left[row - 2] === right[column - 1]
103
+ ) {
104
+ const transpositionRow = matrix[row - 2];
105
+ const transpositionCost = transpositionRow?.[column - 2];
106
+
107
+ if (transpositionCost !== undefined) {
108
+ currentRow[column] = Math.min(currentRow[column] ?? Number.POSITIVE_INFINITY, transpositionCost + 1);
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ return matrix[left.length]?.[right.length] ?? 0;
115
+ }
116
+
117
+ function scoreTokenMatch(
118
+ searchTerm: string,
119
+ candidateToken: string,
120
+ isLastTerm: boolean,
121
+ ): number | null {
122
+ if (candidateToken === searchTerm) {
123
+ return 120;
124
+ }
125
+
126
+ if (isLastTerm && candidateToken.startsWith(searchTerm)) {
127
+ return 90 - Math.max(candidateToken.length - searchTerm.length, 0);
128
+ }
129
+
130
+ const distance = damerauLevenshtein(searchTerm, candidateToken);
131
+ if (distance > getMaxDistance(searchTerm)) {
132
+ return null;
133
+ }
134
+
135
+ return 70 - distance * 10 - Math.abs(candidateToken.length - searchTerm.length);
136
+ }
137
+
138
+ export type SearchTextPart =
139
+ | string
140
+ | null
141
+ | undefined
142
+ | readonly SearchTextPart[];
143
+
144
+ export function buildSearchText(...parts: SearchTextPart[]): string {
145
+ const fragments: string[] = [];
146
+
147
+ for (const part of parts) {
148
+ flattenSearchPart(part, fragments);
149
+ }
150
+
151
+ return fragments.join(' ').replace(/\s+/g, ' ').trim();
152
+ }
153
+
154
+ export function scoreFuzzyMatch(query: string, candidate: string): number | null {
155
+ const normalizedQuery = normalizeSearchValue(query);
156
+ if (normalizedQuery.length === 0) {
157
+ return 0;
158
+ }
159
+
160
+ const queryTerms = tokenize(normalizedQuery);
161
+ const candidateTerms = tokenize(candidate);
162
+ const normalizedCandidate = normalizeSearchValue(candidate);
163
+
164
+ if (queryTerms.length === 0 || candidateTerms.length === 0) {
165
+ return null;
166
+ }
167
+
168
+ let score = normalizedCandidate.includes(normalizedQuery) ? 40 : 0;
169
+
170
+ for (const [index, queryTerm] of queryTerms.entries()) {
171
+ let bestScore: number | null = null;
172
+
173
+ for (const candidateTerm of candidateTerms) {
174
+ const tokenScore = scoreTokenMatch(
175
+ queryTerm,
176
+ candidateTerm,
177
+ index === queryTerms.length - 1,
178
+ );
179
+
180
+ if (tokenScore !== null && (bestScore === null || tokenScore > bestScore)) {
181
+ bestScore = tokenScore;
182
+ }
183
+ }
184
+
185
+ if (bestScore === null) {
186
+ return null;
187
+ }
188
+
189
+ score += bestScore;
190
+ }
191
+
192
+ return score;
193
+ }
194
+
195
+ export function filterAndRankBySearch<T extends { searchText: string }>(
196
+ items: readonly T[],
197
+ query: string,
198
+ ): T[] {
199
+ const normalizedQuery = normalizeSearchValue(query);
200
+
201
+ if (normalizedQuery.length === 0) {
202
+ return [...items];
203
+ }
204
+
205
+ return items
206
+ .map((item) => ({
207
+ item,
208
+ score: scoreFuzzyMatch(normalizedQuery, item.searchText),
209
+ }))
210
+ .filter((result): result is { item: T; score: number } => result.score !== null)
211
+ .sort((left, right) => right.score - left.score)
212
+ .map((result) => result.item);
213
+ }
214
+
215
+ export function rankBySearch<T>(
216
+ items: readonly T[],
217
+ query: string,
218
+ getSearchText: (item: T) => string,
219
+ ): T[] {
220
+ return filterAndRankBySearch(
221
+ items.map((item) => ({
222
+ item,
223
+ searchText: getSearchText(item),
224
+ })),
225
+ query,
226
+ ).map((result) => result.item);
227
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "searchable-select",
3
+ "description": "Editable source registry entry for searchable-select.",
4
+ "importPath": "@carefully-built/ui",
5
+ "exports": [
6
+ "AUTO_SEARCHABLE_SELECT_THRESHOLD",
7
+ "SearchableSelect",
8
+ "SearchableSelectOption",
9
+ "SearchableSelectProps",
10
+ "getSearchableSelectPortalContainer",
11
+ "isSearchableSelectPointerInside"
12
+ ],
13
+ "dependencies": [
14
+ "class-variance-authority",
15
+ "clsx",
16
+ "tailwind-merge"
17
+ ],
18
+ "peerDependencies": [
19
+ "react",
20
+ "react-dom",
21
+ "radix-ui",
22
+ "lucide-react",
23
+ "react-day-picker",
24
+ "vaul"
25
+ ],
26
+ "files": [
27
+ {
28
+ "source": "primitives/input.tsx",
29
+ "target": "components/ui/input.tsx"
30
+ },
31
+ {
32
+ "source": "search/searchable-select-position.ts",
33
+ "target": "components/ui/search/searchable-select-position.ts"
34
+ },
35
+ {
36
+ "source": "search/searchable-select.tsx",
37
+ "target": "components/ui/search/searchable-select.tsx"
38
+ },
39
+ {
40
+ "source": "utils/cn.ts",
41
+ "target": "lib/utils.ts"
42
+ },
43
+ {
44
+ "source": "utils/search.ts",
45
+ "target": "components/ui/search.ts"
46
+ }
47
+ ]
48
+ }
@@ -0,0 +1,19 @@
1
+ import * as React from 'react';
2
+
3
+ import { cn } from '@/lib/utils';
4
+
5
+ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
6
+ return (
7
+ <input
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ 'dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
12
+ className,
13
+ )}
14
+ {...props}
15
+ />
16
+ );
17
+ }
18
+
19
+ export { Input };