@shipfox/react-ui 0.13.0 → 0.15.0

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 (268) hide show
  1. package/.storybook/preview.tsx +7 -0
  2. package/.turbo/turbo-build.log +7 -7
  3. package/.turbo/turbo-check.log +2 -2
  4. package/.turbo/turbo-type.log +1 -1
  5. package/CHANGELOG.md +16 -0
  6. package/dist/components/avatar/avatar.js +1 -1
  7. package/dist/components/avatar/avatar.js.map +1 -1
  8. package/dist/components/button-group/button-group.d.ts +17 -0
  9. package/dist/components/button-group/button-group.d.ts.map +1 -0
  10. package/dist/components/button-group/button-group.js +74 -0
  11. package/dist/components/button-group/button-group.js.map +1 -0
  12. package/dist/components/button-group/button-group.stories.js +644 -0
  13. package/dist/components/button-group/button-group.stories.js.map +1 -0
  14. package/dist/components/button-group/index.d.ts +2 -0
  15. package/dist/components/button-group/index.d.ts.map +1 -0
  16. package/dist/components/button-group/index.js +3 -0
  17. package/dist/components/button-group/index.js.map +1 -0
  18. package/dist/components/code-block/code-block-footer.d.ts.map +1 -1
  19. package/dist/components/code-block/code-block-footer.js +13 -5
  20. package/dist/components/code-block/code-block-footer.js.map +1 -1
  21. package/dist/components/command/command.d.ts +28 -0
  22. package/dist/components/command/command.d.ts.map +1 -0
  23. package/dist/components/command/command.js +190 -0
  24. package/dist/components/command/command.js.map +1 -0
  25. package/dist/components/command/command.stories.js +228 -0
  26. package/dist/components/command/command.stories.js.map +1 -0
  27. package/dist/components/command/index.d.ts +2 -0
  28. package/dist/components/command/index.d.ts.map +1 -0
  29. package/dist/components/command/index.js +3 -0
  30. package/dist/components/command/index.js.map +1 -0
  31. package/dist/components/confetti/confetti.d.ts +21 -0
  32. package/dist/components/confetti/confetti.d.ts.map +1 -0
  33. package/dist/components/confetti/confetti.js +101 -0
  34. package/dist/components/confetti/confetti.js.map +1 -0
  35. package/dist/components/confetti/confetti.stories.js +41 -0
  36. package/dist/components/confetti/confetti.stories.js.map +1 -0
  37. package/dist/components/confetti/index.d.ts +2 -0
  38. package/dist/components/confetti/index.d.ts.map +1 -0
  39. package/dist/components/confetti/index.js +3 -0
  40. package/dist/components/confetti/index.js.map +1 -0
  41. package/dist/components/dashboard/components/analytics-content.d.ts +2 -0
  42. package/dist/components/dashboard/components/analytics-content.d.ts.map +1 -0
  43. package/dist/components/dashboard/components/analytics-content.js +180 -0
  44. package/dist/components/dashboard/components/analytics-content.js.map +1 -0
  45. package/dist/components/dashboard/components/animated-logo.d.ts +4 -0
  46. package/dist/components/dashboard/components/animated-logo.d.ts.map +1 -0
  47. package/dist/components/dashboard/components/animated-logo.js +23 -0
  48. package/dist/components/dashboard/components/animated-logo.js.map +1 -0
  49. package/dist/components/dashboard/components/complete-setup-button.d.ts +4 -0
  50. package/dist/components/dashboard/components/complete-setup-button.d.ts.map +1 -0
  51. package/dist/components/dashboard/components/complete-setup-button.js +28 -0
  52. package/dist/components/dashboard/components/complete-setup-button.js.map +1 -0
  53. package/dist/components/dashboard/components/jobs-content.d.ts +2 -0
  54. package/dist/components/dashboard/components/jobs-content.d.ts.map +1 -0
  55. package/dist/components/dashboard/components/jobs-content.js +69 -0
  56. package/dist/components/dashboard/components/jobs-content.js.map +1 -0
  57. package/dist/components/dashboard/components/mobile-menu.d.ts +2 -0
  58. package/dist/components/dashboard/components/mobile-menu.d.ts.map +1 -0
  59. package/dist/components/dashboard/components/mobile-menu.js +65 -0
  60. package/dist/components/dashboard/components/mobile-menu.js.map +1 -0
  61. package/dist/components/dashboard/components/organization-selector.d.ts +2 -0
  62. package/dist/components/dashboard/components/organization-selector.d.ts.map +1 -0
  63. package/dist/components/dashboard/components/organization-selector.js +92 -0
  64. package/dist/components/dashboard/components/organization-selector.js.map +1 -0
  65. package/dist/components/dashboard/components/top-menu.d.ts +5 -0
  66. package/dist/components/dashboard/components/top-menu.d.ts.map +1 -0
  67. package/dist/components/dashboard/components/top-menu.js +31 -0
  68. package/dist/components/dashboard/components/top-menu.js.map +1 -0
  69. package/dist/components/dashboard/components/topbar-button.d.ts +7 -0
  70. package/dist/components/dashboard/components/topbar-button.d.ts.map +1 -0
  71. package/dist/components/dashboard/components/topbar-button.js +18 -0
  72. package/dist/components/dashboard/components/topbar-button.js.map +1 -0
  73. package/dist/components/dashboard/components/topbar.d.ts +4 -0
  74. package/dist/components/dashboard/components/topbar.d.ts.map +1 -0
  75. package/dist/components/dashboard/components/topbar.js +62 -0
  76. package/dist/components/dashboard/components/topbar.js.map +1 -0
  77. package/dist/components/dashboard/components/user-profile.d.ts +2 -0
  78. package/dist/components/dashboard/components/user-profile.d.ts.map +1 -0
  79. package/dist/components/dashboard/components/user-profile.js +146 -0
  80. package/dist/components/dashboard/components/user-profile.js.map +1 -0
  81. package/dist/components/dashboard/dashboard.d.ts +2 -0
  82. package/dist/components/dashboard/dashboard.d.ts.map +1 -0
  83. package/dist/components/dashboard/dashboard.js +70 -0
  84. package/dist/components/dashboard/dashboard.js.map +1 -0
  85. package/dist/components/dashboard/dashboard.stories.js +23 -0
  86. package/dist/components/dashboard/dashboard.stories.js.map +1 -0
  87. package/dist/components/dashboard/index.d.ts +2 -0
  88. package/dist/components/dashboard/index.d.ts.map +1 -0
  89. package/dist/components/dashboard/index.js +3 -0
  90. package/dist/components/dashboard/index.js.map +1 -0
  91. package/dist/components/form/form.stories.js +6 -1
  92. package/dist/components/form/form.stories.js.map +1 -1
  93. package/dist/components/icon/icon.d.ts +3 -2
  94. package/dist/components/icon/icon.d.ts.map +1 -1
  95. package/dist/components/icon/icon.js +7 -2
  96. package/dist/components/icon/icon.js.map +1 -1
  97. package/dist/components/index.d.ts +9 -0
  98. package/dist/components/index.d.ts.map +1 -1
  99. package/dist/components/index.js +9 -0
  100. package/dist/components/index.js.map +1 -1
  101. package/dist/components/kbd/index.d.ts +2 -0
  102. package/dist/components/kbd/index.d.ts.map +1 -0
  103. package/dist/components/kbd/index.js +3 -0
  104. package/dist/components/kbd/index.js.map +1 -0
  105. package/dist/components/kbd/kbd.d.ts +7 -0
  106. package/dist/components/kbd/kbd.d.ts.map +1 -0
  107. package/dist/components/kbd/kbd.js +18 -0
  108. package/dist/components/kbd/kbd.js.map +1 -0
  109. package/dist/components/kbd/kbd.stories.js +119 -0
  110. package/dist/components/kbd/kbd.stories.js.map +1 -0
  111. package/dist/components/modal/modal.stories.js +227 -168
  112. package/dist/components/modal/modal.stories.js.map +1 -1
  113. package/dist/components/search/index.d.ts +7 -0
  114. package/dist/components/search/index.d.ts.map +1 -0
  115. package/dist/components/search/index.js +8 -0
  116. package/dist/components/search/index.js.map +1 -0
  117. package/dist/components/search/search-context.d.ts +11 -0
  118. package/dist/components/search/search-context.d.ts.map +1 -0
  119. package/dist/components/search/search-context.js +56 -0
  120. package/dist/components/search/search-context.js.map +1 -0
  121. package/dist/components/search/search-inline.d.ts +9 -0
  122. package/dist/components/search/search-inline.d.ts.map +1 -0
  123. package/dist/components/search/search-inline.js +85 -0
  124. package/dist/components/search/search-inline.js.map +1 -0
  125. package/dist/components/search/search-modal.d.ts +25 -0
  126. package/dist/components/search/search-modal.d.ts.map +1 -0
  127. package/dist/components/search/search-modal.js +162 -0
  128. package/dist/components/search/search-modal.js.map +1 -0
  129. package/dist/components/search/search-trigger.d.ts +9 -0
  130. package/dist/components/search/search-trigger.d.ts.map +1 -0
  131. package/dist/components/search/search-trigger.js +37 -0
  132. package/dist/components/search/search-trigger.js.map +1 -0
  133. package/dist/components/search/search-variants.d.ts +14 -0
  134. package/dist/components/search/search-variants.d.ts.map +1 -0
  135. package/dist/components/search/search-variants.js +90 -0
  136. package/dist/components/search/search-variants.js.map +1 -0
  137. package/dist/components/search/search.d.ts +11 -0
  138. package/dist/components/search/search.d.ts.map +1 -0
  139. package/dist/components/search/search.js +35 -0
  140. package/dist/components/search/search.js.map +1 -0
  141. package/dist/components/search/search.stories.js +630 -0
  142. package/dist/components/search/search.stories.js.map +1 -0
  143. package/dist/components/select/index.d.ts +2 -0
  144. package/dist/components/select/index.d.ts.map +1 -0
  145. package/dist/components/select/index.js +3 -0
  146. package/dist/components/select/index.js.map +1 -0
  147. package/dist/components/select/select.d.ts +25 -0
  148. package/dist/components/select/select.d.ts.map +1 -0
  149. package/dist/components/select/select.js +153 -0
  150. package/dist/components/select/select.js.map +1 -0
  151. package/dist/components/select/select.stories.js +393 -0
  152. package/dist/components/select/select.stories.js.map +1 -0
  153. package/dist/components/shiny-text/index.d.ts +2 -0
  154. package/dist/components/shiny-text/index.d.ts.map +1 -0
  155. package/dist/components/shiny-text/index.js +3 -0
  156. package/dist/components/shiny-text/index.js.map +1 -0
  157. package/dist/components/shiny-text/shiny-text.d.ts +10 -0
  158. package/dist/components/shiny-text/shiny-text.d.ts.map +1 -0
  159. package/dist/components/shiny-text/shiny-text.js +17 -0
  160. package/dist/components/shiny-text/shiny-text.js.map +1 -0
  161. package/dist/components/skeleton/index.d.ts +2 -0
  162. package/dist/components/skeleton/index.d.ts.map +1 -0
  163. package/dist/components/skeleton/index.js +3 -0
  164. package/dist/components/skeleton/index.js.map +1 -0
  165. package/dist/components/skeleton/skeleton.d.ts +5 -0
  166. package/dist/components/skeleton/skeleton.d.ts.map +1 -0
  167. package/dist/components/skeleton/skeleton.js +11 -0
  168. package/dist/components/skeleton/skeleton.js.map +1 -0
  169. package/dist/components/skeleton/skeleton.stories.js +345 -0
  170. package/dist/components/skeleton/skeleton.stories.js.map +1 -0
  171. package/dist/components/table/data-table.d.ts +70 -0
  172. package/dist/components/table/data-table.d.ts.map +1 -0
  173. package/dist/components/table/data-table.js +159 -0
  174. package/dist/components/table/data-table.js.map +1 -0
  175. package/dist/components/table/index.d.ts +6 -0
  176. package/dist/components/table/index.d.ts.map +1 -0
  177. package/dist/components/table/index.js +6 -0
  178. package/dist/components/table/index.js.map +1 -0
  179. package/dist/components/table/table-column-header.d.ts +79 -0
  180. package/dist/components/table/table-column-header.d.ts.map +1 -0
  181. package/dist/components/table/table-column-header.js +99 -0
  182. package/dist/components/table/table-column-header.js.map +1 -0
  183. package/dist/components/table/table-pagination.d.ts +53 -0
  184. package/dist/components/table/table-pagination.d.ts.map +1 -0
  185. package/dist/components/table/table-pagination.js +139 -0
  186. package/dist/components/table/table-pagination.js.map +1 -0
  187. package/dist/components/table/table.d.ts +11 -0
  188. package/dist/components/table/table.d.ts.map +1 -0
  189. package/dist/components/table/table.js +64 -0
  190. package/dist/components/table/table.js.map +1 -0
  191. package/dist/components/table/table.stories.columns.d.ts +24 -0
  192. package/dist/components/table/table.stories.columns.d.ts.map +1 -0
  193. package/dist/components/table/table.stories.columns.js +310 -0
  194. package/dist/components/table/table.stories.columns.js.map +1 -0
  195. package/dist/components/table/table.stories.components.d.ts +14 -0
  196. package/dist/components/table/table.stories.components.d.ts.map +1 -0
  197. package/dist/components/table/table.stories.components.js +107 -0
  198. package/dist/components/table/table.stories.components.js.map +1 -0
  199. package/dist/components/table/table.stories.data.d.ts +54 -0
  200. package/dist/components/table/table.stories.data.d.ts.map +1 -0
  201. package/dist/components/table/table.stories.data.js +122 -0
  202. package/dist/components/table/table.stories.data.js.map +1 -0
  203. package/dist/components/table/table.stories.js +302 -0
  204. package/dist/components/table/table.stories.js.map +1 -0
  205. package/dist/index.d.ts +1 -0
  206. package/dist/index.d.ts.map +1 -1
  207. package/dist/index.js +1 -0
  208. package/dist/index.js.map +1 -1
  209. package/dist/styles.css +1 -1
  210. package/index.css +79 -0
  211. package/package.json +6 -2
  212. package/src/components/avatar/avatar.tsx +1 -1
  213. package/src/components/button-group/button-group.stories.tsx +361 -0
  214. package/src/components/button-group/button-group.tsx +111 -0
  215. package/src/components/button-group/index.ts +1 -0
  216. package/src/components/code-block/code-block-footer.tsx +19 -2
  217. package/src/components/command/command.stories.tsx +133 -0
  218. package/src/components/command/command.tsx +265 -0
  219. package/src/components/command/index.ts +1 -0
  220. package/src/components/confetti/confetti.stories.tsx +38 -0
  221. package/src/components/confetti/confetti.tsx +140 -0
  222. package/src/components/confetti/index.ts +1 -0
  223. package/src/components/dashboard/components/analytics-content.tsx +102 -0
  224. package/src/components/dashboard/components/animated-logo.tsx +25 -0
  225. package/src/components/dashboard/components/complete-setup-button.tsx +30 -0
  226. package/src/components/dashboard/components/jobs-content.tsx +51 -0
  227. package/src/components/dashboard/components/mobile-menu.tsx +50 -0
  228. package/src/components/dashboard/components/organization-selector.tsx +51 -0
  229. package/src/components/dashboard/components/top-menu.tsx +26 -0
  230. package/src/components/dashboard/components/topbar-button.tsx +27 -0
  231. package/src/components/dashboard/components/topbar.tsx +40 -0
  232. package/src/components/dashboard/components/user-profile.tsx +90 -0
  233. package/src/components/dashboard/dashboard.stories.tsx +25 -0
  234. package/src/components/dashboard/dashboard.tsx +61 -0
  235. package/src/components/dashboard/index.ts +1 -0
  236. package/src/components/form/form.stories.tsx +5 -0
  237. package/src/components/icon/icon.tsx +7 -3
  238. package/src/components/index.ts +9 -0
  239. package/src/components/kbd/index.ts +1 -0
  240. package/src/components/kbd/kbd.stories.tsx +64 -0
  241. package/src/components/kbd/kbd.tsx +32 -0
  242. package/src/components/modal/modal.stories.tsx +58 -4
  243. package/src/components/search/index.ts +28 -0
  244. package/src/components/search/search-context.tsx +78 -0
  245. package/src/components/search/search-inline.tsx +107 -0
  246. package/src/components/search/search-modal.tsx +198 -0
  247. package/src/components/search/search-trigger.tsx +47 -0
  248. package/src/components/search/search-variants.ts +88 -0
  249. package/src/components/search/search.stories.tsx +392 -0
  250. package/src/components/search/search.tsx +47 -0
  251. package/src/components/select/index.ts +1 -0
  252. package/src/components/select/select.stories.tsx +207 -0
  253. package/src/components/select/select.tsx +220 -0
  254. package/src/components/shiny-text/index.ts +1 -0
  255. package/src/components/shiny-text/shiny-text.tsx +21 -0
  256. package/src/components/skeleton/index.ts +1 -0
  257. package/src/components/skeleton/skeleton.stories.tsx +178 -0
  258. package/src/components/skeleton/skeleton.tsx +14 -0
  259. package/src/components/table/data-table.tsx +254 -0
  260. package/src/components/table/index.ts +5 -0
  261. package/src/components/table/table-column-header.tsx +141 -0
  262. package/src/components/table/table-pagination.tsx +161 -0
  263. package/src/components/table/table.stories.columns.tsx +198 -0
  264. package/src/components/table/table.stories.components.tsx +104 -0
  265. package/src/components/table/table.stories.data.ts +117 -0
  266. package/src/components/table/table.stories.tsx +256 -0
  267. package/src/components/table/table.tsx +95 -0
  268. package/src/index.ts +1 -0
@@ -0,0 +1,78 @@
1
+ import {createContext, useCallback, useContext, useEffect, useState} from 'react';
2
+ import {SHORTCUT_KEY_REGEX} from './search-variants';
3
+
4
+ export type SearchContextValue = {
5
+ open: boolean;
6
+ setOpen: (open: boolean) => void;
7
+ searchValue: string;
8
+ setSearchValue: (value: string) => void;
9
+ };
10
+
11
+ export const SearchContext = createContext<SearchContextValue | null>(null);
12
+
13
+ export function useSearchContext() {
14
+ const context = useContext(SearchContext);
15
+ if (!context) {
16
+ throw new Error('Search components must be used within a Search component');
17
+ }
18
+ return context;
19
+ }
20
+
21
+ export function useControllableState<T>(
22
+ controlledValue: T | undefined,
23
+ defaultValue: T,
24
+ onChange?: (value: T) => void,
25
+ ): [T, (value: T) => void] {
26
+ const [internalValue, setInternalValue] = useState(defaultValue);
27
+ const isControlled = controlledValue !== undefined;
28
+ const value = isControlled ? controlledValue : internalValue;
29
+
30
+ const setValue = useCallback(
31
+ (newValue: T) => {
32
+ if (!isControlled) {
33
+ setInternalValue(newValue);
34
+ }
35
+ onChange?.(newValue);
36
+ },
37
+ [isControlled, onChange],
38
+ );
39
+
40
+ return [value, setValue];
41
+ }
42
+
43
+ export function useKeyboardShortcut(shortcutKey: string | undefined, onTrigger: () => void) {
44
+ useEffect(() => {
45
+ if (!shortcutKey) return;
46
+
47
+ const handleKeyDown = (e: KeyboardEvent) => {
48
+ const key = shortcutKey.toLowerCase();
49
+ const isMetaKey = key.startsWith('meta+') || key.startsWith('cmd+') || key.startsWith('⌘');
50
+ const isCtrlKey = key.startsWith('ctrl+');
51
+ const targetKey = key.replace(SHORTCUT_KEY_REGEX, '');
52
+
53
+ const shouldTrigger =
54
+ (isMetaKey && e.metaKey && e.key.toLowerCase() === targetKey) ||
55
+ (isCtrlKey && e.ctrlKey && e.key.toLowerCase() === targetKey) ||
56
+ (!isMetaKey && !isCtrlKey && e.key.toLowerCase() === targetKey && !e.metaKey && !e.ctrlKey);
57
+
58
+ if (!shouldTrigger) return;
59
+
60
+ if (!isMetaKey && !isCtrlKey) {
61
+ const target = e.target as HTMLElement;
62
+ if (
63
+ target.tagName === 'INPUT' ||
64
+ target.tagName === 'TEXTAREA' ||
65
+ target.isContentEditable
66
+ ) {
67
+ return;
68
+ }
69
+ }
70
+
71
+ e.preventDefault();
72
+ onTrigger();
73
+ };
74
+
75
+ document.addEventListener('keydown', handleKeyDown);
76
+ return () => document.removeEventListener('keydown', handleKeyDown);
77
+ }, [shortcutKey, onTrigger]);
78
+ }
@@ -0,0 +1,107 @@
1
+ import type {VariantProps} from 'class-variance-authority';
2
+ import type {ComponentProps} from 'react';
3
+ import {useCallback, useRef, useState} from 'react';
4
+ import {cn} from 'utils/cn';
5
+ import {Icon} from '../icon';
6
+ import {searchInputVariants} from './search-variants';
7
+
8
+ export type SearchInlineProps = Omit<ComponentProps<'input'>, 'size'> &
9
+ VariantProps<typeof searchInputVariants> & {
10
+ showClearButton?: boolean;
11
+ onClear?: () => void;
12
+ };
13
+
14
+ export function SearchInline({
15
+ className,
16
+ variant,
17
+ size,
18
+ radius,
19
+ value,
20
+ onChange,
21
+ onClear,
22
+ showClearButton = true,
23
+ ...props
24
+ }: SearchInlineProps) {
25
+ const inputRef = useRef<HTMLInputElement>(null);
26
+ const [internalValue, setInternalValue] = useState('');
27
+ const isControlled = value !== undefined;
28
+ const inputValue = isControlled ? value : internalValue;
29
+ const hasValue = Boolean(inputValue);
30
+ const isSmall = size === 'small';
31
+
32
+ const handleChange = useCallback(
33
+ (e: React.ChangeEvent<HTMLInputElement>) => {
34
+ if (!isControlled) {
35
+ setInternalValue(e.target.value);
36
+ }
37
+ onChange?.(e);
38
+ },
39
+ [isControlled, onChange],
40
+ );
41
+
42
+ const handleClear = useCallback(() => {
43
+ if (!isControlled) {
44
+ setInternalValue('');
45
+ }
46
+
47
+ if (onChange && inputRef.current) {
48
+ inputRef.current.value = '';
49
+ const syntheticEvent = {
50
+ target: inputRef.current,
51
+ currentTarget: inputRef.current,
52
+ } as React.ChangeEvent<HTMLInputElement>;
53
+ onChange(syntheticEvent);
54
+ }
55
+ onClear?.();
56
+ inputRef.current?.focus();
57
+ }, [isControlled, onChange, onClear]);
58
+
59
+ const handleKeyDown = useCallback(
60
+ (e: React.KeyboardEvent<HTMLInputElement>) => {
61
+ if (e.key === 'Escape' && hasValue) {
62
+ e.preventDefault();
63
+ handleClear();
64
+ }
65
+ },
66
+ [hasValue, handleClear],
67
+ );
68
+
69
+ return (
70
+ <div
71
+ data-slot="search-inline"
72
+ className={cn(searchInputVariants({variant, size, radius}), className)}
73
+ >
74
+ <Icon
75
+ name="searchLine"
76
+ className={cn('shrink-0 text-foreground-neutral-muted', isSmall ? 'size-14' : 'size-16')}
77
+ />
78
+ <input
79
+ ref={inputRef}
80
+ type="text"
81
+ value={inputValue}
82
+ onChange={handleChange}
83
+ onKeyDown={handleKeyDown}
84
+ className={cn(
85
+ 'flex-1 bg-transparent outline-none min-w-0',
86
+ 'text-foreground-neutral-base',
87
+ 'placeholder:text-foreground-neutral-muted',
88
+ 'disabled:cursor-not-allowed disabled:text-foreground-neutral-disabled',
89
+ )}
90
+ {...props}
91
+ />
92
+ {showClearButton && hasValue && (
93
+ <button
94
+ type="button"
95
+ onClick={handleClear}
96
+ className={cn(
97
+ 'shrink-0 cursor-pointer rounded-4 p-2 -mx-2',
98
+ 'text-foreground-neutral-muted hover:text-foreground-neutral-subtle transition-colors',
99
+ )}
100
+ aria-label="Clear search"
101
+ >
102
+ <Icon name="closeLine" className="size-16" />
103
+ </button>
104
+ )}
105
+ </div>
106
+ );
107
+ }
@@ -0,0 +1,198 @@
1
+ import {Command as CommandPrimitive} from 'cmdk';
2
+ import type {ComponentProps, ReactNode} from 'react';
3
+ import {useCallback} from 'react';
4
+ import {cn} from 'utils/cn';
5
+ import {Icon} from '../icon';
6
+ import {Kbd} from '../kbd';
7
+ import {Modal, ModalBody, ModalContent, type ModalContentProps} from '../modal/modal';
8
+ import {useSearchContext} from './search-context';
9
+
10
+ export type SearchContentProps = {
11
+ breakpoint?: string;
12
+ } & Omit<ModalContentProps, 'open' | 'onOpenChange'>;
13
+
14
+ export function SearchContent({
15
+ breakpoint = '(min-width: 768px)',
16
+ className,
17
+ children,
18
+ overlayClassName,
19
+ onEscapeKeyDown,
20
+ ...props
21
+ }: SearchContentProps) {
22
+ const {open, setOpen, searchValue, setSearchValue} = useSearchContext();
23
+
24
+ const handleEscapeKeyDown = useCallback(
25
+ (event: KeyboardEvent) => {
26
+ if (searchValue) {
27
+ event.preventDefault();
28
+ setSearchValue('');
29
+ } else {
30
+ onEscapeKeyDown?.(event);
31
+ }
32
+ },
33
+ [searchValue, setSearchValue, onEscapeKeyDown],
34
+ );
35
+
36
+ return (
37
+ <Modal open={open} onOpenChange={setOpen} breakpoint={breakpoint}>
38
+ <ModalContent
39
+ data-slot="search-content"
40
+ className={cn('top-[15%]! translate-y-0!', className)}
41
+ overlayClassName={cn('backdrop-blur-sm', overlayClassName)}
42
+ onEscapeKeyDown={handleEscapeKeyDown}
43
+ {...props}
44
+ >
45
+ <ModalBody className="flex flex-col p-0 min-h-0 overflow-hidden md:overflow-clip">
46
+ {children}
47
+ </ModalBody>
48
+ </ModalContent>
49
+ </Modal>
50
+ );
51
+ }
52
+
53
+ export type SearchInputProps = Omit<
54
+ ComponentProps<typeof CommandPrimitive.Input>,
55
+ 'value' | 'onValueChange'
56
+ >;
57
+
58
+ export function SearchInput({className, ...props}: SearchInputProps) {
59
+ const {open, searchValue, setSearchValue} = useSearchContext();
60
+
61
+ return (
62
+ <div className="w-full shrink-0 flex items-center gap-8 border-b border-border-neutral-strong px-16 py-12">
63
+ <Icon name="searchLine" className="size-16 shrink-0 text-foreground-neutral-muted" />
64
+ <CommandPrimitive.Input
65
+ data-slot="search-input"
66
+ autoFocus={open}
67
+ value={searchValue}
68
+ onValueChange={setSearchValue}
69
+ className={cn(
70
+ 'flex-1 bg-transparent text-sm leading-20 outline-none',
71
+ 'placeholder:text-foreground-neutral-muted',
72
+ 'disabled:cursor-not-allowed disabled:text-foreground-neutral-disabled',
73
+ className,
74
+ )}
75
+ {...props}
76
+ />
77
+ <Kbd>Esc</Kbd>
78
+ </div>
79
+ );
80
+ }
81
+
82
+ export type SearchListProps = ComponentProps<typeof CommandPrimitive.List>;
83
+
84
+ export function SearchList({className, ...props}: SearchListProps) {
85
+ return (
86
+ <CommandPrimitive.List
87
+ data-slot="search-list"
88
+ className={cn(
89
+ 'flex-1 min-h-0 w-full overflow-y-auto overflow-x-hidden px-8 py-4 scrollbar',
90
+ 'md:max-h-400',
91
+ className,
92
+ )}
93
+ {...props}
94
+ />
95
+ );
96
+ }
97
+
98
+ export type SearchEmptyProps = ComponentProps<typeof CommandPrimitive.Empty>;
99
+
100
+ export function SearchEmpty({className, ...props}: SearchEmptyProps) {
101
+ return (
102
+ <CommandPrimitive.Empty
103
+ data-slot="search-empty"
104
+ className={cn('py-32 text-center text-sm text-foreground-neutral-muted', className)}
105
+ {...props}
106
+ />
107
+ );
108
+ }
109
+
110
+ export type SearchGroupProps = ComponentProps<typeof CommandPrimitive.Group>;
111
+
112
+ export function SearchGroup({className, ...props}: SearchGroupProps) {
113
+ return (
114
+ <CommandPrimitive.Group
115
+ data-slot="search-group"
116
+ className={cn(
117
+ 'overflow-hidden',
118
+ '[&_[cmdk-group-heading]]:px-8 [&_[cmdk-group-heading]]:py-4',
119
+ '[&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:leading-20',
120
+ '[&_[cmdk-group-heading]]:text-foreground-neutral-subtle',
121
+ '[&_[cmdk-group-heading]]:select-none',
122
+ className,
123
+ )}
124
+ {...props}
125
+ />
126
+ );
127
+ }
128
+
129
+ export type SearchItemProps = ComponentProps<typeof CommandPrimitive.Item> & {
130
+ icon?: ReactNode;
131
+ description?: string;
132
+ };
133
+
134
+ export function SearchItem({className, children, icon, description, ...props}: SearchItemProps) {
135
+ return (
136
+ <CommandPrimitive.Item
137
+ data-slot="search-item"
138
+ className={cn(
139
+ 'relative flex cursor-pointer select-none items-center gap-12 rounded-8 p-8',
140
+ 'text-sm leading-20 text-foreground-neutral-subtle outline-none transition-colors',
141
+ 'aria-selected:bg-background-components-hover aria-selected:text-foreground-neutral-base',
142
+ 'data-[disabled=true]:pointer-events-none data-[disabled=true]:text-foreground-neutral-disabled',
143
+ className,
144
+ )}
145
+ {...props}
146
+ >
147
+ {icon && <span className="shrink-0 text-foreground-neutral-muted">{icon}</span>}
148
+ <div className="flex-1 min-w-0">
149
+ <div className="truncate">{children}</div>
150
+ {description && (
151
+ <div className="truncate text-xs text-foreground-neutral-muted">{description}</div>
152
+ )}
153
+ </div>
154
+ </CommandPrimitive.Item>
155
+ );
156
+ }
157
+
158
+ export type SearchSeparatorProps = ComponentProps<typeof CommandPrimitive.Separator>;
159
+
160
+ export function SearchSeparator({className, ...props}: SearchSeparatorProps) {
161
+ return (
162
+ <CommandPrimitive.Separator
163
+ data-slot="search-separator"
164
+ className={cn('my-8 h-px bg-border-neutral-base', className)}
165
+ {...props}
166
+ />
167
+ );
168
+ }
169
+
170
+ export type SearchFooterProps = ComponentProps<'div'>;
171
+
172
+ export function SearchFooter({className, ...props}: SearchFooterProps) {
173
+ return (
174
+ <div
175
+ data-slot="search-footer"
176
+ className={cn(
177
+ 'w-full shrink-0 flex items-center justify-end gap-12 px-16 py-12',
178
+ 'border-t border-border-neutral-strong',
179
+ 'bg-background-components-base',
180
+ className,
181
+ )}
182
+ {...props}
183
+ >
184
+ <div className="flex items-center gap-8">
185
+ <span className="text-xs font-medium text-foreground-neutral-subtle">Navigation</span>
186
+ <div className="flex items-center gap-4">
187
+ <Kbd>↓</Kbd>
188
+ <Kbd>↑</Kbd>
189
+ </div>
190
+ </div>
191
+ <div className="h-12 w-px bg-border-neutral-strong" />
192
+ <div className="flex items-center gap-8">
193
+ <span className="text-xs font-medium text-foreground-neutral-subtle">Open result</span>
194
+ <Kbd>↵</Kbd>
195
+ </div>
196
+ </div>
197
+ );
198
+ }
@@ -0,0 +1,47 @@
1
+ import type {VariantProps} from 'class-variance-authority';
2
+ import type {ComponentProps} from 'react';
3
+ import {cn} from 'utils/cn';
4
+ import {Icon} from '../icon';
5
+ import {Kbd} from '../kbd';
6
+ import {useSearchContext} from './search-context';
7
+ import {searchTriggerVariants} from './search-variants';
8
+
9
+ export type SearchTriggerProps = ComponentProps<'button'> &
10
+ VariantProps<typeof searchTriggerVariants> & {
11
+ placeholder?: string;
12
+ shortcut?: string;
13
+ };
14
+
15
+ export function SearchTrigger({
16
+ className,
17
+ variant,
18
+ size,
19
+ radius,
20
+ placeholder = 'Search',
21
+ shortcut = '⌘K',
22
+ ...props
23
+ }: SearchTriggerProps) {
24
+ const {setOpen} = useSearchContext();
25
+ const isSmall = size === 'small';
26
+
27
+ return (
28
+ <button
29
+ type="button"
30
+ data-slot="search-trigger"
31
+ onClick={() => setOpen(true)}
32
+ className={cn(searchTriggerVariants({variant, size, radius}), className)}
33
+ {...props}
34
+ >
35
+ <Icon name="searchLine" className={cn('shrink-0', isSmall ? 'size-14' : 'size-16')} />
36
+ <span className="flex-1 text-left truncate">{placeholder}</span>
37
+ <Kbd
38
+ className={cn(
39
+ isSmall && 'h-16 min-w-16 px-4 text-[10px]',
40
+ radius === 'rounded' && 'rounded-full',
41
+ )}
42
+ >
43
+ {shortcut}
44
+ </Kbd>
45
+ </button>
46
+ );
47
+ }
@@ -0,0 +1,88 @@
1
+ import {cva} from 'class-variance-authority';
2
+ import type {Transition} from 'framer-motion';
3
+
4
+ const sharedInputStyles = [
5
+ 'inline-flex items-center gap-8',
6
+ 'text-sm leading-20',
7
+ 'transition-[color,box-shadow,background-color] outline-none',
8
+ ];
9
+
10
+ const variantStyles = {
11
+ primary: {
12
+ base: ['bg-background-field-base', 'shadow-button-neutral'],
13
+ focus: 'focus-within:shadow-border-interactive-with-active',
14
+ hover: 'hover:bg-background-field-hover',
15
+ focusVisible: 'focus-visible:shadow-border-interactive-with-active',
16
+ },
17
+ secondary: {
18
+ base: ['bg-background-field-component', 'border border-border-neutral-strong'],
19
+ focus: 'focus-within:shadow-border-interactive-with-active',
20
+ hover: 'hover:bg-background-field-component-hover',
21
+ focusVisible: 'focus-visible:shadow-border-interactive-with-active',
22
+ },
23
+ };
24
+
25
+ const sizeStyles = {
26
+ base: 'h-32 px-8 py-6',
27
+ small: 'h-28 px-8 py-4',
28
+ };
29
+
30
+ const radiusStyles = {
31
+ rounded: 'rounded-full',
32
+ squared: 'rounded-6',
33
+ };
34
+
35
+ export const searchInputVariants = cva(sharedInputStyles, {
36
+ variants: {
37
+ variant: {
38
+ primary: [...variantStyles.primary.base, variantStyles.primary.focus],
39
+ secondary: [...variantStyles.secondary.base, variantStyles.secondary.focus],
40
+ },
41
+ size: sizeStyles,
42
+ radius: radiusStyles,
43
+ },
44
+ defaultVariants: {
45
+ variant: 'primary',
46
+ size: 'base',
47
+ radius: 'squared',
48
+ },
49
+ });
50
+
51
+ export const searchTriggerVariants = cva(
52
+ [
53
+ ...sharedInputStyles,
54
+ 'cursor-pointer text-foreground-neutral-muted',
55
+ 'disabled:pointer-events-none disabled:cursor-not-allowed disabled:text-foreground-neutral-disabled',
56
+ ],
57
+ {
58
+ variants: {
59
+ variant: {
60
+ primary: [
61
+ ...variantStyles.primary.base,
62
+ variantStyles.primary.hover,
63
+ variantStyles.primary.focusVisible,
64
+ ],
65
+ secondary: [
66
+ ...variantStyles.secondary.base,
67
+ variantStyles.secondary.hover,
68
+ variantStyles.secondary.focusVisible,
69
+ ],
70
+ },
71
+ size: sizeStyles,
72
+ radius: radiusStyles,
73
+ },
74
+ defaultVariants: {
75
+ variant: 'primary',
76
+ size: 'base',
77
+ radius: 'squared',
78
+ },
79
+ },
80
+ );
81
+
82
+ export const searchDefaultTransition: Transition = {
83
+ type: 'spring',
84
+ stiffness: 400,
85
+ damping: 30,
86
+ };
87
+
88
+ export const SHORTCUT_KEY_REGEX = /^(meta\+|cmd\+|ctrl\+|⌘\+?)/i;