@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,28 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ export function useMediaQuery(query: string, defaultValue = false): boolean {
6
+ const [matches, setMatches] = useState(defaultValue);
7
+
8
+ useEffect(() => {
9
+ const mediaQuery = window.matchMedia(query);
10
+
11
+ const handleChange = (): void => {
12
+ setMatches(mediaQuery.matches);
13
+ };
14
+
15
+ handleChange();
16
+ mediaQuery.addEventListener('change', handleChange);
17
+
18
+ return () => {
19
+ mediaQuery.removeEventListener('change', handleChange);
20
+ };
21
+ }, [query]);
22
+
23
+ return matches;
24
+ }
25
+
26
+ export function useIsMobile(maxWidth = 767): boolean {
27
+ return useMediaQuery(`(max-width: ${String(maxWidth)}px)`);
28
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "tabs",
3
+ "description": "Editable source registry entry for tabs.",
4
+ "importPath": "@carefully-built/ui",
5
+ "exports": [
6
+ "Tabs",
7
+ "TabsContent",
8
+ "TabsList",
9
+ "TabsScrollArea",
10
+ "TabsTrigger",
11
+ "tabsListVariants"
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/scroll-fade-area.tsx",
29
+ "target": "components/ui/scroll-fade-area.tsx"
30
+ },
31
+ {
32
+ "source": "primitives/tabs.tsx",
33
+ "target": "components/ui/tabs.tsx"
34
+ },
35
+ {
36
+ "source": "utils/cn.ts",
37
+ "target": "lib/utils.ts"
38
+ }
39
+ ]
40
+ }
@@ -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,97 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { cva, type VariantProps } from 'class-variance-authority';
5
+ import { Tabs as TabsPrimitive } from 'radix-ui';
6
+
7
+ import { ScrollFadeArea } from '@/components/ui/scroll-fade-area';
8
+ import { cn } from '@/lib/utils';
9
+
10
+ function Tabs({
11
+ className,
12
+ orientation = 'horizontal',
13
+ ...props
14
+ }: React.ComponentProps<typeof TabsPrimitive.Root>) {
15
+ return (
16
+ <TabsPrimitive.Root
17
+ data-slot="tabs"
18
+ data-orientation={orientation}
19
+ className={cn('group/tabs flex gap-2 data-horizontal:flex-col', className)}
20
+ {...props}
21
+ />
22
+ );
23
+ }
24
+
25
+ const tabsListVariants = cva(
26
+ 'gap-2 rounded-lg p-[3px] group-data-horizontal/tabs:h-8 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col',
27
+ {
28
+ variants: {
29
+ variant: {
30
+ default: 'bg-muted',
31
+ line: 'gap-2 bg-transparent',
32
+ },
33
+ },
34
+ defaultVariants: {
35
+ variant: 'default',
36
+ },
37
+ },
38
+ );
39
+
40
+ function TabsList({
41
+ className,
42
+ variant = 'default',
43
+ ...props
44
+ }: React.ComponentProps<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>) {
45
+ return (
46
+ <TabsPrimitive.List
47
+ data-slot="tabs-list"
48
+ data-variant={variant}
49
+ className={cn(tabsListVariants({ variant }), className)}
50
+ {...props}
51
+ />
52
+ );
53
+ }
54
+
55
+ function TabsScrollArea({
56
+ className,
57
+ viewportClassName,
58
+ ...props
59
+ }: React.ComponentProps<typeof ScrollFadeArea>) {
60
+ return (
61
+ <ScrollFadeArea
62
+ {...props}
63
+ data-slot="tabs-scroll-area"
64
+ orientation="horizontal"
65
+ className={cn('w-full min-w-0', className)}
66
+ viewportClassName={cn('min-w-0', viewportClassName)}
67
+ />
68
+ );
69
+ }
70
+
71
+ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
72
+ return (
73
+ <TabsPrimitive.Trigger
74
+ data-slot="tabs-trigger"
75
+ className={cn(
76
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
77
+ 'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent',
78
+ 'data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground',
79
+ 'after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100',
80
+ className,
81
+ )}
82
+ {...props}
83
+ />
84
+ );
85
+ }
86
+
87
+ function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
88
+ return (
89
+ <TabsPrimitive.Content
90
+ data-slot="tabs-content"
91
+ className={cn('flex-1 text-sm outline-none', className)}
92
+ {...props}
93
+ />
94
+ );
95
+ }
96
+
97
+ export { Tabs, TabsList, TabsScrollArea, TabsTrigger, TabsContent, tabsListVariants };
@@ -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
+ }