@carefully-built/cli 0.1.0 → 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 (212) hide show
  1. package/README.md +148 -7
  2. package/dist/index.mjs +71 -11
  3. package/dist/index.mjs.map +1 -1
  4. package/package.json +4 -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/registry/ui/button/cn.ts +0 -6
  212. /package/registry/ui/button/{button.tsx → primitives/button.tsx} +0 -0
@@ -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,32 @@
1
+ {
2
+ "name": "searchable-select-position",
3
+ "description": "Editable source registry entry for searchable-select-position.",
4
+ "importPath": "@carefully-built/ui",
5
+ "exports": [
6
+ "SearchableSelectRect",
7
+ "resolveSearchableSelectDropdownPosition"
8
+ ],
9
+ "dependencies": [
10
+ "class-variance-authority",
11
+ "clsx",
12
+ "tailwind-merge"
13
+ ],
14
+ "peerDependencies": [
15
+ "react",
16
+ "react-dom",
17
+ "radix-ui",
18
+ "lucide-react",
19
+ "react-day-picker",
20
+ "vaul"
21
+ ],
22
+ "files": [
23
+ {
24
+ "source": "search/searchable-select-position.ts",
25
+ "target": "components/ui/search/searchable-select-position.ts"
26
+ },
27
+ {
28
+ "source": "utils/cn.ts",
29
+ "target": "lib/utils.ts"
30
+ }
31
+ ]
32
+ }
@@ -0,0 +1,95 @@
1
+ export interface SearchableSelectRect {
2
+ readonly top: number;
3
+ readonly left: number;
4
+ readonly right: number;
5
+ readonly bottom: number;
6
+ readonly width: number;
7
+ readonly height: number;
8
+ }
9
+
10
+ interface ResolveSearchableSelectDropdownPositionArgs {
11
+ readonly triggerRect: SearchableSelectRect;
12
+ readonly boundaryRect?: SearchableSelectRect;
13
+ readonly portalRect?: SearchableSelectRect;
14
+ readonly contentWidth: number;
15
+ readonly contentHeight: number;
16
+ readonly viewportWidth: number;
17
+ readonly viewportHeight: number;
18
+ readonly offset?: number;
19
+ readonly padding?: number;
20
+ }
21
+
22
+ interface SearchableSelectDropdownPosition {
23
+ readonly top: number;
24
+ readonly left: number;
25
+ readonly width: number;
26
+ readonly maxHeight: number;
27
+ readonly direction: 'up' | 'down';
28
+ }
29
+
30
+ function clamp(value: number, min: number, max: number): number {
31
+ if (max < min) {
32
+ return min;
33
+ }
34
+
35
+ return Math.min(Math.max(value, min), max);
36
+ }
37
+
38
+ export function resolveSearchableSelectDropdownPosition({
39
+ triggerRect,
40
+ boundaryRect,
41
+ portalRect,
42
+ contentWidth,
43
+ contentHeight,
44
+ viewportWidth,
45
+ viewportHeight,
46
+ offset = 8,
47
+ padding = 8,
48
+ }: ResolveSearchableSelectDropdownPositionArgs): SearchableSelectDropdownPosition {
49
+ const boundaryLeftEdge = boundaryRect?.left ?? 0;
50
+ const boundaryRightEdge = boundaryRect?.right ?? viewportWidth;
51
+ const boundaryTopEdge = boundaryRect?.top ?? 0;
52
+ const boundaryBottomEdge = boundaryRect?.bottom ?? viewportHeight;
53
+ const boundaryLeft = Math.max(padding, boundaryLeftEdge + padding);
54
+ const boundaryRight = Math.min(
55
+ viewportWidth - padding,
56
+ boundaryRightEdge - padding,
57
+ );
58
+ const boundaryTop = Math.max(padding, boundaryTopEdge + padding);
59
+ const boundaryBottom = Math.min(
60
+ viewportHeight - padding,
61
+ boundaryBottomEdge - padding,
62
+ );
63
+ const availableWidth = Math.max(0, boundaryRight - boundaryLeft);
64
+ const width = Math.min(contentWidth, availableWidth);
65
+ const alignedRightLeft = triggerRect.right - width;
66
+ const defaultLeft = triggerRect.left;
67
+ const shouldAlignRight =
68
+ defaultLeft + width > boundaryRight && alignedRightLeft >= boundaryLeft;
69
+ const left = clamp(
70
+ shouldAlignRight ? alignedRightLeft : defaultLeft,
71
+ boundaryLeft,
72
+ boundaryRight - width,
73
+ );
74
+
75
+ const spaceAbove = triggerRect.top - boundaryTop;
76
+ const spaceBelow = boundaryBottom - triggerRect.bottom;
77
+ const shouldOpenUp = spaceBelow < contentHeight + offset && spaceAbove > spaceBelow;
78
+ const direction = shouldOpenUp ? 'up' : 'down';
79
+ const maxHeight = Math.max(
80
+ 120,
81
+ Math.floor((shouldOpenUp ? spaceAbove : spaceBelow) - offset),
82
+ );
83
+ const renderedHeight = Math.min(contentHeight, maxHeight);
84
+ const viewportTop = shouldOpenUp
85
+ ? Math.max(boundaryTop, triggerRect.top - offset - renderedHeight)
86
+ : Math.min(boundaryBottom - renderedHeight, triggerRect.bottom + offset);
87
+
88
+ return {
89
+ top: viewportTop - (portalRect?.top ?? 0),
90
+ left: left - (portalRect?.left ?? 0),
91
+ width,
92
+ maxHeight,
93
+ direction,
94
+ };
95
+ }
@@ -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,41 @@
1
+ {
2
+ "name": "segmented-toggle",
3
+ "description": "Editable source registry entry for segmented-toggle.",
4
+ "importPath": "@carefully-built/ui",
5
+ "exports": [
6
+ "SegmentedToggle",
7
+ "SegmentedToggleOption",
8
+ "SegmentedToggleProps"
9
+ ],
10
+ "dependencies": [
11
+ "class-variance-authority",
12
+ "clsx",
13
+ "tailwind-merge"
14
+ ],
15
+ "peerDependencies": [
16
+ "react",
17
+ "react-dom",
18
+ "radix-ui",
19
+ "lucide-react",
20
+ "react-day-picker",
21
+ "vaul"
22
+ ],
23
+ "files": [
24
+ {
25
+ "source": "primitives/scroll-fade-area.tsx",
26
+ "target": "components/ui/scroll-fade-area.tsx"
27
+ },
28
+ {
29
+ "source": "primitives/segmented-toggle.tsx",
30
+ "target": "components/ui/segmented-toggle.tsx"
31
+ },
32
+ {
33
+ "source": "primitives/tabs.tsx",
34
+ "target": "components/ui/tabs.tsx"
35
+ },
36
+ {
37
+ "source": "utils/cn.ts",
38
+ "target": "lib/utils.ts"
39
+ }
40
+ ]
41
+ }
@@ -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
+ }