@crfrsr/ui 1.0.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.
- package/dist/ThemeProvider.d.ts +22 -0
- package/dist/ThemeProvider.js +53 -0
- package/dist/components/Button.d.ts +14 -0
- package/dist/components/Button.js +12 -0
- package/dist/components/Combobox.d.ts +33 -0
- package/dist/components/Combobox.js +255 -0
- package/dist/components/Pill.d.ts +20 -0
- package/dist/components/Pill.js +19 -0
- package/dist/components/command.d.ts +74 -0
- package/dist/components/command.js +26 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.js +6 -0
- package/dist/components/popover.d.ts +7 -0
- package/dist/components/popover.js +10 -0
- package/dist/hooks/useIsMobile.d.ts +8 -0
- package/dist/hooks/useIsMobile.js +18 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/lib/cn.d.ts +7 -0
- package/dist/lib/cn.js +9 -0
- package/dist/styles/button.css +89 -0
- package/dist/styles/combobox.css +200 -0
- package/dist/styles/pill.css +44 -0
- package/dist/styles/reset.css +273 -0
- package/dist/styles/styles.css +14 -0
- package/dist/styles/tokens.css +100 -0
- package/dist/typography/Heading.d.ts +12 -0
- package/dist/typography/Heading.js +31 -0
- package/dist/typography/Text.d.ts +13 -0
- package/dist/typography/Text.js +21 -0
- package/dist/typography/index.d.ts +2 -0
- package/dist/typography/index.js +2 -0
- package/package.json +72 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { Theme, ThemeOverrides, ColorMode } from '@crfrsr/core';
|
|
3
|
+
interface ThemeContextValue {
|
|
4
|
+
theme: Theme;
|
|
5
|
+
setMode: React.Dispatch<React.SetStateAction<ColorMode>>;
|
|
6
|
+
toggleMode: () => void;
|
|
7
|
+
isLight: boolean;
|
|
8
|
+
isDark: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ThemeProviderProps {
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
initialMode?: ColorMode;
|
|
13
|
+
/**
|
|
14
|
+
* Per-app theme customization, merged over the library defaults. Define it at
|
|
15
|
+
* module level (or memoize it) so its identity is stable across renders.
|
|
16
|
+
*/
|
|
17
|
+
theme?: ThemeOverrides;
|
|
18
|
+
skipBodyFontFamily?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function ThemeProvider({ children, initialMode, theme: themeOverrides, skipBodyFontFamily, }: ThemeProviderProps): React.JSX.Element;
|
|
21
|
+
export declare function useTheme(): ThemeContextValue;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react';
|
|
2
|
+
import { createTheme, themeToCssVariables, } from '@crfrsr/core';
|
|
3
|
+
const ThemeContext = createContext(undefined);
|
|
4
|
+
export function ThemeProvider({ children, initialMode = 'light', theme: themeOverrides, skipBodyFontFamily = false, }) {
|
|
5
|
+
const [mode, setMode] = React.useState(initialMode);
|
|
6
|
+
const theme = React.useMemo(() => {
|
|
7
|
+
return createTheme(mode, themeOverrides);
|
|
8
|
+
}, [mode, themeOverrides]);
|
|
9
|
+
// Set global CSS variables from theme. useLayoutEffect so the app's palette
|
|
10
|
+
// lands before first paint — with useEffect the initial frame would flash the
|
|
11
|
+
// static tokens.css defaults.
|
|
12
|
+
React.useLayoutEffect(() => {
|
|
13
|
+
const root = document.documentElement;
|
|
14
|
+
// Full --crfrsr-* contract consumed by the shipped component CSS
|
|
15
|
+
const vars = themeToCssVariables(theme);
|
|
16
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
17
|
+
root.style.setProperty(name, value);
|
|
18
|
+
}
|
|
19
|
+
// Marker class so the static tokens.css `.crfrsr-dark` block can also apply
|
|
20
|
+
root.classList.toggle('crfrsr-dark', mode === 'dark');
|
|
21
|
+
// Legacy --theme-* variables kept for backward compatibility (examples/web)
|
|
22
|
+
root.style.setProperty('--theme-background', theme.colors.background);
|
|
23
|
+
root.style.setProperty('--theme-surface', theme.colors.surface);
|
|
24
|
+
root.style.setProperty('--theme-border', theme.colors.border);
|
|
25
|
+
root.style.setProperty('--theme-font-family', theme.typography.fontFamily.base);
|
|
26
|
+
root.style.setProperty('--theme-text-color', theme.colors.text);
|
|
27
|
+
if (!skipBodyFontFamily) {
|
|
28
|
+
document.body.style.fontFamily = theme.typography.fontFamily.base;
|
|
29
|
+
}
|
|
30
|
+
document.body.style.color = theme.colors.text;
|
|
31
|
+
document.body.style.backgroundColor = theme.colors.background;
|
|
32
|
+
}, [mode, theme, skipBodyFontFamily]);
|
|
33
|
+
const toggleMode = React.useCallback(() => {
|
|
34
|
+
setMode(prevMode => prevMode === 'dark' ? 'light' : 'dark');
|
|
35
|
+
}, []);
|
|
36
|
+
const isLight = mode === 'light';
|
|
37
|
+
const isDark = mode === 'dark';
|
|
38
|
+
const value = React.useMemo(() => ({
|
|
39
|
+
theme,
|
|
40
|
+
setMode,
|
|
41
|
+
toggleMode,
|
|
42
|
+
isLight,
|
|
43
|
+
isDark,
|
|
44
|
+
}), [theme, setMode, toggleMode, isLight, isDark]);
|
|
45
|
+
return (React.createElement(ThemeContext.Provider, { value: value }, children));
|
|
46
|
+
}
|
|
47
|
+
export function useTheme() {
|
|
48
|
+
const context = useContext(ThemeContext);
|
|
49
|
+
if (context === undefined) {
|
|
50
|
+
throw new Error('useTheme must be used within a ThemeProvider');
|
|
51
|
+
}
|
|
52
|
+
return context;
|
|
53
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
|
|
3
|
+
export type ButtonSize = 'sm' | 'md' | 'lg' | 'icon';
|
|
4
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
5
|
+
variant?: ButtonVariant;
|
|
6
|
+
size?: ButtonSize;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Single Button for the design system. Consolidates the two buttons that used to
|
|
10
|
+
* live in consumer apps (a custom primary/secondary button and a shadcn button).
|
|
11
|
+
* Styled entirely through the --crfrsr-* CSS variables; pass `className` to layer
|
|
12
|
+
* app-specific styles (e.g. Tailwind utilities) on top.
|
|
13
|
+
*/
|
|
14
|
+
export declare const Button: React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '../lib/cn';
|
|
3
|
+
/**
|
|
4
|
+
* Single Button for the design system. Consolidates the two buttons that used to
|
|
5
|
+
* live in consumer apps (a custom primary/secondary button and a shadcn button).
|
|
6
|
+
* Styled entirely through the --crfrsr-* CSS variables; pass `className` to layer
|
|
7
|
+
* app-specific styles (e.g. Tailwind utilities) on top.
|
|
8
|
+
*/
|
|
9
|
+
export const Button = React.forwardRef(({ variant = 'primary', size = 'md', type = 'button', className, ...props }, ref) => {
|
|
10
|
+
return (React.createElement("button", { ref: ref, type: type, className: cn('crfrsr-btn', `crfrsr-btn--${variant}`, `crfrsr-btn--${size}`, className), ...props }));
|
|
11
|
+
});
|
|
12
|
+
Button.displayName = 'Button';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface ComboboxProps<T> {
|
|
3
|
+
options: T[];
|
|
4
|
+
filteredOptions: T[];
|
|
5
|
+
isLoading?: boolean;
|
|
6
|
+
searchValue: string;
|
|
7
|
+
onSearchChange: (value: string) => void;
|
|
8
|
+
onSelect: (option: T) => void;
|
|
9
|
+
trigger: React.ReactNode;
|
|
10
|
+
renderOption: (option: T, index: number, onSelect: () => void) => React.ReactNode;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
emptyMessage?: string;
|
|
13
|
+
loadingMessage?: string;
|
|
14
|
+
/** CSS width value for the popover, e.g. "200px" or "12rem". Default "137.5px". */
|
|
15
|
+
popoverWidth?: string;
|
|
16
|
+
/** Extra class names for the popover content. */
|
|
17
|
+
popoverClassName?: string;
|
|
18
|
+
estimateItemSize?: number;
|
|
19
|
+
inputClassName?: string;
|
|
20
|
+
hasNextPage?: boolean;
|
|
21
|
+
isFetchingNextPage?: boolean;
|
|
22
|
+
onLoadMore?: () => void;
|
|
23
|
+
showClearButton?: boolean;
|
|
24
|
+
onClear?: () => void;
|
|
25
|
+
value?: unknown;
|
|
26
|
+
onOpenChange?: (open: boolean) => void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Generic, virtualized combobox with optional infinite scroll. Styling-agnostic:
|
|
30
|
+
* the trigger and each option are supplied by the consumer via render props, and
|
|
31
|
+
* the popover width is a plain CSS value.
|
|
32
|
+
*/
|
|
33
|
+
export declare function Combobox<T>({ filteredOptions, isLoading, searchValue, onSearchChange, onSelect, trigger, renderOption, placeholder, emptyMessage, loadingMessage, popoverWidth, popoverClassName, estimateItemSize, hasNextPage, isFetchingNextPage, onLoadMore, showClearButton, onClear, value, onOpenChange: externalOnOpenChange, inputClassName, }: ComboboxProps<T>): React.JSX.Element;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
3
|
+
import { cn } from '../lib/cn';
|
|
4
|
+
import { useIsMobile } from '../hooks/useIsMobile';
|
|
5
|
+
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandList, } from './command';
|
|
6
|
+
import { Popover, PopoverContent, PopoverTrigger } from './popover';
|
|
7
|
+
/**
|
|
8
|
+
* Generic, virtualized combobox with optional infinite scroll. Styling-agnostic:
|
|
9
|
+
* the trigger and each option are supplied by the consumer via render props, and
|
|
10
|
+
* the popover width is a plain CSS value.
|
|
11
|
+
*/
|
|
12
|
+
export function Combobox({ filteredOptions, isLoading = false, searchValue, onSearchChange, onSelect, trigger, renderOption, placeholder = 'Search...', emptyMessage = 'No items found.', loadingMessage = 'Loading...', popoverWidth = '137.5px', popoverClassName, estimateItemSize = 28, hasNextPage = false, isFetchingNextPage = false, onLoadMore, showClearButton = false, onClear, value, onOpenChange: externalOnOpenChange, inputClassName, }) {
|
|
13
|
+
const [open, setOpen] = React.useState(false);
|
|
14
|
+
const isMobile = useIsMobile();
|
|
15
|
+
// Notify parent of open state changes
|
|
16
|
+
React.useEffect(() => {
|
|
17
|
+
externalOnOpenChange?.(open);
|
|
18
|
+
}, [open, externalOnOpenChange]);
|
|
19
|
+
const commandListRef = React.useRef(null);
|
|
20
|
+
const parentRef = React.useRef(null);
|
|
21
|
+
const containerRef = React.useRef(null);
|
|
22
|
+
const isScrollingRef = React.useRef(false);
|
|
23
|
+
// Reset search when popover closes, after the close animation finishes.
|
|
24
|
+
// NOTE: 200ms must match the popover close-animation duration in combobox.css.
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
if (!open) {
|
|
27
|
+
const timeoutId = setTimeout(() => {
|
|
28
|
+
onSearchChange('');
|
|
29
|
+
}, 200);
|
|
30
|
+
return () => clearTimeout(timeoutId);
|
|
31
|
+
}
|
|
32
|
+
}, [open, onSearchChange]);
|
|
33
|
+
// Detect scrolling on mobile to prevent accidental combobox opens
|
|
34
|
+
React.useEffect(() => {
|
|
35
|
+
let scrollTimeout;
|
|
36
|
+
const handleScroll = () => {
|
|
37
|
+
isScrollingRef.current = true;
|
|
38
|
+
clearTimeout(scrollTimeout);
|
|
39
|
+
scrollTimeout = setTimeout(() => {
|
|
40
|
+
isScrollingRef.current = false;
|
|
41
|
+
}, 150); // Reset scrolling flag 150ms after scroll ends
|
|
42
|
+
};
|
|
43
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
44
|
+
window.addEventListener('touchmove', handleScroll, { passive: true });
|
|
45
|
+
return () => {
|
|
46
|
+
window.removeEventListener('scroll', handleScroll);
|
|
47
|
+
window.removeEventListener('touchmove', handleScroll);
|
|
48
|
+
clearTimeout(scrollTimeout);
|
|
49
|
+
};
|
|
50
|
+
}, []);
|
|
51
|
+
// Create full-page overlay when combobox opens
|
|
52
|
+
React.useEffect(() => {
|
|
53
|
+
if (!open)
|
|
54
|
+
return;
|
|
55
|
+
const overlay = document.createElement('div');
|
|
56
|
+
overlay.style.position = 'fixed';
|
|
57
|
+
overlay.style.top = '0';
|
|
58
|
+
overlay.style.left = '0';
|
|
59
|
+
overlay.style.right = '0';
|
|
60
|
+
overlay.style.bottom = '0';
|
|
61
|
+
overlay.style.width = '100%';
|
|
62
|
+
overlay.style.height = '100%';
|
|
63
|
+
overlay.style.userSelect = 'none';
|
|
64
|
+
overlay.style.zIndex = '40'; // Below popover (z-50) but above most content
|
|
65
|
+
document.body.appendChild(overlay);
|
|
66
|
+
return () => {
|
|
67
|
+
if (document.body.contains(overlay)) {
|
|
68
|
+
document.body.removeChild(overlay);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}, [open]);
|
|
72
|
+
// Virtualizer for the list
|
|
73
|
+
const rowVirtualizer = useVirtualizer({
|
|
74
|
+
count: filteredOptions.length,
|
|
75
|
+
getScrollElement: () => commandListRef.current,
|
|
76
|
+
estimateSize: () => estimateItemSize,
|
|
77
|
+
overscan: 10,
|
|
78
|
+
});
|
|
79
|
+
// Reset virtualizer when opening
|
|
80
|
+
React.useEffect(() => {
|
|
81
|
+
if (open) {
|
|
82
|
+
rowVirtualizer.measure();
|
|
83
|
+
}
|
|
84
|
+
}, [open, rowVirtualizer]);
|
|
85
|
+
// Update virtualizer when filtered options change
|
|
86
|
+
React.useEffect(() => {
|
|
87
|
+
if (open && filteredOptions.length > 0) {
|
|
88
|
+
rowVirtualizer.measure();
|
|
89
|
+
}
|
|
90
|
+
}, [filteredOptions.length, open, rowVirtualizer]);
|
|
91
|
+
// Check if we need to load more pages (when data changes or scrolling)
|
|
92
|
+
const virtualItems = rowVirtualizer.getVirtualItems();
|
|
93
|
+
const checkLoadMore = React.useCallback(() => {
|
|
94
|
+
// Skip if already fetching or no more pages
|
|
95
|
+
if (isFetchingNextPage || isLoading || !hasNextPage || !onLoadMore)
|
|
96
|
+
return;
|
|
97
|
+
const scrollElement = commandListRef.current;
|
|
98
|
+
if (!scrollElement)
|
|
99
|
+
return;
|
|
100
|
+
// Check scroll position - load when within 80% of scroll height
|
|
101
|
+
const scrollTop = scrollElement.scrollTop;
|
|
102
|
+
const scrollHeight = scrollElement.scrollHeight;
|
|
103
|
+
const clientHeight = scrollElement.clientHeight;
|
|
104
|
+
const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
|
|
105
|
+
// Also check virtual items as backup
|
|
106
|
+
const lastItem = virtualItems.length > 0 ? virtualItems[virtualItems.length - 1] : null;
|
|
107
|
+
// Load next page if:
|
|
108
|
+
// 1. Scrolled to 80% of content, OR
|
|
109
|
+
// 2. Last visible item is within 20 items of the end
|
|
110
|
+
if (scrollPercentage >= 0.8 ||
|
|
111
|
+
(lastItem && lastItem.index >= filteredOptions.length - 20)) {
|
|
112
|
+
onLoadMore();
|
|
113
|
+
}
|
|
114
|
+
}, [
|
|
115
|
+
virtualItems,
|
|
116
|
+
isFetchingNextPage,
|
|
117
|
+
isLoading,
|
|
118
|
+
hasNextPage,
|
|
119
|
+
onLoadMore,
|
|
120
|
+
filteredOptions.length,
|
|
121
|
+
]);
|
|
122
|
+
// Check for more pages when filtered options change (new data loaded)
|
|
123
|
+
React.useEffect(() => {
|
|
124
|
+
if (open && filteredOptions.length > 0 && onLoadMore) {
|
|
125
|
+
requestAnimationFrame(() => {
|
|
126
|
+
checkLoadMore();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}, [open, filteredOptions.length, checkLoadMore, onLoadMore]);
|
|
130
|
+
// Infinite scroll: load next page when scrolling near the bottom
|
|
131
|
+
React.useEffect(() => {
|
|
132
|
+
if (!open || !onLoadMore)
|
|
133
|
+
return;
|
|
134
|
+
const scrollElement = commandListRef.current;
|
|
135
|
+
if (!scrollElement) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const rafHandleScroll = () => {
|
|
139
|
+
requestAnimationFrame(() => {
|
|
140
|
+
checkLoadMore();
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
scrollElement.addEventListener('scroll', rafHandleScroll, { passive: true });
|
|
144
|
+
return () => {
|
|
145
|
+
scrollElement.removeEventListener('scroll', rafHandleScroll);
|
|
146
|
+
};
|
|
147
|
+
}, [open, checkLoadMore, onLoadMore]);
|
|
148
|
+
// Handle popover open change - prevent opening if scrolling
|
|
149
|
+
const handleOpenChange = React.useCallback((newOpen) => {
|
|
150
|
+
if (newOpen && isScrollingRef.current) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
setOpen(newOpen);
|
|
154
|
+
}, []);
|
|
155
|
+
// Touch handlers for mobile to prevent double-tap issue
|
|
156
|
+
const handleTouchStart = React.useCallback((e) => {
|
|
157
|
+
if (isMobile) {
|
|
158
|
+
const touch = e.touches[0];
|
|
159
|
+
if (touch) {
|
|
160
|
+
e.currentTarget.__touchStart = {
|
|
161
|
+
x: touch.clientX,
|
|
162
|
+
y: touch.clientY,
|
|
163
|
+
time: Date.now(),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}, [isMobile]);
|
|
168
|
+
const handleTouchMove = React.useCallback((e) => {
|
|
169
|
+
if (isMobile) {
|
|
170
|
+
const touchStart = e.currentTarget.__touchStart;
|
|
171
|
+
if (touchStart) {
|
|
172
|
+
const touch = e.touches[0];
|
|
173
|
+
if (touch) {
|
|
174
|
+
const deltaX = Math.abs(touch.clientX - touchStart.x);
|
|
175
|
+
const deltaY = Math.abs(touch.clientY - touchStart.y);
|
|
176
|
+
// If moved more than 10px, consider it a scroll
|
|
177
|
+
if (deltaX > 10 || deltaY > 10) {
|
|
178
|
+
e.currentTarget.__isScrollGesture = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}, [isMobile]);
|
|
184
|
+
const handleTouchEnd = React.useCallback((e) => {
|
|
185
|
+
// On mobile, trigger click immediately to open combobox.
|
|
186
|
+
// This prevents the double-tap issue.
|
|
187
|
+
if (isMobile) {
|
|
188
|
+
const target = e.currentTarget;
|
|
189
|
+
const isScrollGesture = target.__isScrollGesture;
|
|
190
|
+
if (isScrollGesture) {
|
|
191
|
+
// Don't open if it was a scroll gesture
|
|
192
|
+
e.preventDefault();
|
|
193
|
+
e.stopPropagation();
|
|
194
|
+
delete target.__touchStart;
|
|
195
|
+
delete target.__isScrollGesture;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
delete target.__touchStart;
|
|
199
|
+
delete target.__isScrollGesture;
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
e.stopPropagation();
|
|
202
|
+
// Programmatically trigger click to open the popover
|
|
203
|
+
const clickEvent = new MouseEvent('click', {
|
|
204
|
+
bubbles: true,
|
|
205
|
+
cancelable: true,
|
|
206
|
+
view: window,
|
|
207
|
+
});
|
|
208
|
+
e.currentTarget.dispatchEvent(clickEvent);
|
|
209
|
+
}
|
|
210
|
+
}, [isMobile]);
|
|
211
|
+
const handleTriggerClick = React.useCallback((e) => {
|
|
212
|
+
// Stop propagation to prevent container selection
|
|
213
|
+
e.stopPropagation();
|
|
214
|
+
}, []);
|
|
215
|
+
return (React.createElement("div", { ref: containerRef, className: "crfrsr-combobox" },
|
|
216
|
+
React.createElement(Popover, { open: open, onOpenChange: handleOpenChange },
|
|
217
|
+
React.createElement(PopoverTrigger, { asChild: true },
|
|
218
|
+
React.createElement("div", { onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onClick: handleTriggerClick }, trigger)),
|
|
219
|
+
React.createElement(PopoverContent, { className: cn('crfrsr-combobox__popover', popoverClassName), style: { width: popoverWidth }, align: "start", onClick: (e) => e.stopPropagation() },
|
|
220
|
+
React.createElement(Command, { shouldFilter: false },
|
|
221
|
+
React.createElement(CommandInput, { placeholder: placeholder, className: inputClassName, value: searchValue, onValueChange: onSearchChange }),
|
|
222
|
+
React.createElement(CommandList, { className: "crfrsr-combobox__list", ref: commandListRef },
|
|
223
|
+
isLoading ? (React.createElement(CommandEmpty, { className: "crfrsr-combobox__message" }, loadingMessage)) : filteredOptions.length === 0 ? (React.createElement(CommandEmpty, { className: "crfrsr-combobox__message" }, emptyMessage)) : null,
|
|
224
|
+
React.createElement(CommandGroup, { ref: parentRef, style: {
|
|
225
|
+
height: `${rowVirtualizer.getTotalSize()}px`,
|
|
226
|
+
width: '100%',
|
|
227
|
+
position: 'relative',
|
|
228
|
+
} }, filteredOptions.length === 0
|
|
229
|
+
? null
|
|
230
|
+
: rowVirtualizer.getVirtualItems().map((virtualItem) => {
|
|
231
|
+
const option = filteredOptions[virtualItem.index];
|
|
232
|
+
if (!option)
|
|
233
|
+
return null;
|
|
234
|
+
return (React.createElement("div", { key: virtualItem.key, style: {
|
|
235
|
+
position: 'absolute',
|
|
236
|
+
top: 0,
|
|
237
|
+
left: 0,
|
|
238
|
+
width: '100%',
|
|
239
|
+
height: `${virtualItem.size}px`,
|
|
240
|
+
transform: `translateY(${virtualItem.start}px)`,
|
|
241
|
+
} }, renderOption(option, virtualItem.index, () => {
|
|
242
|
+
if (!open) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
onSelect(option);
|
|
246
|
+
setOpen(false);
|
|
247
|
+
})));
|
|
248
|
+
})))))),
|
|
249
|
+
showClearButton && value != null && onClear && (React.createElement("button", { type: "button", onClick: (e) => {
|
|
250
|
+
e.stopPropagation();
|
|
251
|
+
onClear();
|
|
252
|
+
}, className: "crfrsr-combobox__clear", "aria-label": "Clear selection" },
|
|
253
|
+
React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "aria-hidden": "true" },
|
|
254
|
+
React.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }))))));
|
|
255
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export type PillSize = 'default' | 'small' | 'icon';
|
|
3
|
+
export interface PillProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
4
|
+
size?: PillSize;
|
|
5
|
+
/** Background color. Consumers pass a category color; CSS handles the rest. */
|
|
6
|
+
color?: string;
|
|
7
|
+
/** Background color on hover. When set, the pill becomes hoverable (CSS-driven,
|
|
8
|
+
* no JS color swap needed). */
|
|
9
|
+
hoverColor?: string;
|
|
10
|
+
/** Optional leading glyph slot (rendered before the label). */
|
|
11
|
+
glyph?: React.ReactNode;
|
|
12
|
+
/** Uppercase the label (default true). Glyph is never transformed. */
|
|
13
|
+
uppercase?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Generic pill / tag / badge. App-specific data (category colors, glyphs, exact
|
|
17
|
+
* font sizes) is supplied by the consumer via props and `className`; the library
|
|
18
|
+
* only owns the shape, sizing scale, and hover behavior.
|
|
19
|
+
*/
|
|
20
|
+
export declare const Pill: React.ForwardRefExoticComponent<PillProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '../lib/cn';
|
|
3
|
+
/**
|
|
4
|
+
* Generic pill / tag / badge. App-specific data (category colors, glyphs, exact
|
|
5
|
+
* font sizes) is supplied by the consumer via props and `className`; the library
|
|
6
|
+
* only owns the shape, sizing scale, and hover behavior.
|
|
7
|
+
*/
|
|
8
|
+
export const Pill = React.forwardRef(({ size = 'default', color, hoverColor, glyph, uppercase = true, className, style, children, ...props }, ref) => {
|
|
9
|
+
const pillVars = {
|
|
10
|
+
...(color != null ? { '--crfrsr-pill-bg': color } : {}),
|
|
11
|
+
...(hoverColor != null
|
|
12
|
+
? { '--crfrsr-pill-bg-hover': hoverColor }
|
|
13
|
+
: {}),
|
|
14
|
+
};
|
|
15
|
+
return (React.createElement("span", { ref: ref, className: cn('crfrsr-pill', `crfrsr-pill--${size}`, uppercase && 'crfrsr-pill--uppercase', hoverColor != null && 'crfrsr-pill--hoverable', className), style: { ...pillVars, ...style }, ...props },
|
|
16
|
+
glyph != null && React.createElement("span", { className: "crfrsr-pill__glyph" }, glyph),
|
|
17
|
+
children != null && React.createElement("span", { className: "crfrsr-pill__label" }, children)));
|
|
18
|
+
});
|
|
19
|
+
Pill.displayName = 'Pill';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
declare const Command: React.ForwardRefExoticComponent<Omit<{
|
|
3
|
+
children?: React.ReactNode;
|
|
4
|
+
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
5
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
6
|
+
} & {
|
|
7
|
+
asChild?: boolean;
|
|
8
|
+
}, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
|
|
9
|
+
label?: string;
|
|
10
|
+
shouldFilter?: boolean;
|
|
11
|
+
filter?: (value: string, search: string, keywords?: string[]) => number;
|
|
12
|
+
defaultValue?: string;
|
|
13
|
+
value?: string;
|
|
14
|
+
onValueChange?: (value: string) => void;
|
|
15
|
+
loop?: boolean;
|
|
16
|
+
disablePointerSelection?: boolean;
|
|
17
|
+
vimBindings?: boolean;
|
|
18
|
+
} & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
19
|
+
declare const CommandInput: React.ForwardRefExoticComponent<Omit<Omit<Pick<Pick<React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "key" | keyof React.InputHTMLAttributes<HTMLInputElement>> & {
|
|
20
|
+
ref?: React.Ref<HTMLInputElement>;
|
|
21
|
+
} & {
|
|
22
|
+
asChild?: boolean;
|
|
23
|
+
}, "key" | "asChild" | keyof React.InputHTMLAttributes<HTMLInputElement>>, "value" | "onChange" | "type"> & {
|
|
24
|
+
value?: string;
|
|
25
|
+
onValueChange?: (search: string) => void;
|
|
26
|
+
} & React.RefAttributes<HTMLInputElement>, "ref"> & React.RefAttributes<HTMLInputElement>>;
|
|
27
|
+
declare const CommandList: React.ForwardRefExoticComponent<Omit<{
|
|
28
|
+
children?: React.ReactNode;
|
|
29
|
+
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
30
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
31
|
+
} & {
|
|
32
|
+
asChild?: boolean;
|
|
33
|
+
}, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
|
|
34
|
+
label?: string;
|
|
35
|
+
} & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
36
|
+
declare const CommandEmpty: React.ForwardRefExoticComponent<Omit<{
|
|
37
|
+
children?: React.ReactNode;
|
|
38
|
+
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
39
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
40
|
+
} & {
|
|
41
|
+
asChild?: boolean;
|
|
42
|
+
}, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
43
|
+
declare const CommandGroup: React.ForwardRefExoticComponent<Omit<{
|
|
44
|
+
children?: React.ReactNode;
|
|
45
|
+
} & Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
46
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
47
|
+
} & {
|
|
48
|
+
asChild?: boolean;
|
|
49
|
+
}, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild">, "value" | "heading"> & {
|
|
50
|
+
heading?: React.ReactNode;
|
|
51
|
+
value?: string;
|
|
52
|
+
forceMount?: boolean;
|
|
53
|
+
} & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
54
|
+
declare const CommandSeparator: React.ForwardRefExoticComponent<Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
55
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
56
|
+
} & {
|
|
57
|
+
asChild?: boolean;
|
|
58
|
+
}, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
|
|
59
|
+
alwaysRender?: boolean;
|
|
60
|
+
} & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
61
|
+
declare const CommandItem: React.ForwardRefExoticComponent<Omit<{
|
|
62
|
+
children?: React.ReactNode;
|
|
63
|
+
} & Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
64
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
65
|
+
} & {
|
|
66
|
+
asChild?: boolean;
|
|
67
|
+
}, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild">, "value" | "disabled" | "onSelect"> & {
|
|
68
|
+
disabled?: boolean;
|
|
69
|
+
onSelect?: (value: string) => void;
|
|
70
|
+
value?: string;
|
|
71
|
+
keywords?: string[];
|
|
72
|
+
forceMount?: boolean;
|
|
73
|
+
} & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
74
|
+
export { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator, };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Command as CommandPrimitive } from 'cmdk';
|
|
3
|
+
import { cn } from '../lib/cn';
|
|
4
|
+
/** Inline magnifier icon (avoids pulling in an icon dependency). */
|
|
5
|
+
function SearchIcon({ className }) {
|
|
6
|
+
return (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: className, "aria-hidden": "true" },
|
|
7
|
+
React.createElement("circle", { cx: "11", cy: "11", r: "8" }),
|
|
8
|
+
React.createElement("path", { d: "m21 21-4.3-4.3" })));
|
|
9
|
+
}
|
|
10
|
+
const Command = React.forwardRef(({ className, ...props }, ref) => (React.createElement(CommandPrimitive, { ref: ref, className: cn('crfrsr-command', className), ...props })));
|
|
11
|
+
Command.displayName = CommandPrimitive.displayName;
|
|
12
|
+
const CommandInput = React.forwardRef(({ className, ...props }, ref) => (React.createElement("div", { className: "crfrsr-command__input-wrapper", "cmdk-input-wrapper": "" },
|
|
13
|
+
React.createElement(SearchIcon, { className: "crfrsr-command__input-icon" }),
|
|
14
|
+
React.createElement(CommandPrimitive.Input, { ref: ref, className: cn('crfrsr-command__input', className), ...props }))));
|
|
15
|
+
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
|
16
|
+
const CommandList = React.forwardRef(({ className, ...props }, ref) => (React.createElement(CommandPrimitive.List, { ref: ref, className: cn('crfrsr-command__list', className), ...props })));
|
|
17
|
+
CommandList.displayName = CommandPrimitive.List.displayName;
|
|
18
|
+
const CommandEmpty = React.forwardRef(({ className, ...props }, ref) => (React.createElement(CommandPrimitive.Empty, { ref: ref, className: cn('crfrsr-command__empty', className), ...props })));
|
|
19
|
+
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
|
20
|
+
const CommandGroup = React.forwardRef(({ className, ...props }, ref) => (React.createElement(CommandPrimitive.Group, { ref: ref, className: cn('crfrsr-command__group', className), ...props })));
|
|
21
|
+
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
|
22
|
+
const CommandSeparator = React.forwardRef(({ className, ...props }, ref) => (React.createElement(CommandPrimitive.Separator, { ref: ref, className: cn('crfrsr-command__separator', className), ...props })));
|
|
23
|
+
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
|
24
|
+
const CommandItem = React.forwardRef(({ className, ...props }, ref) => (React.createElement(CommandPrimitive.Item, { ref: ref, className: cn('crfrsr-command__item', className), ...props })));
|
|
25
|
+
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
|
26
|
+
export { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator, };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
|
3
|
+
declare const Popover: React.FC<PopoverPrimitive.PopoverProps>;
|
|
4
|
+
declare const PopoverTrigger: React.ForwardRefExoticComponent<PopoverPrimitive.PopoverTriggerProps & React.RefAttributes<HTMLButtonElement>>;
|
|
5
|
+
declare const PopoverAnchor: React.ForwardRefExoticComponent<PopoverPrimitive.PopoverAnchorProps & React.RefAttributes<HTMLDivElement>>;
|
|
6
|
+
declare const PopoverContent: React.ForwardRefExoticComponent<Omit<PopoverPrimitive.PopoverContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
7
|
+
export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
|
3
|
+
import { cn } from '../lib/cn';
|
|
4
|
+
const Popover = PopoverPrimitive.Root;
|
|
5
|
+
const PopoverTrigger = PopoverPrimitive.Trigger;
|
|
6
|
+
const PopoverAnchor = PopoverPrimitive.Anchor;
|
|
7
|
+
const PopoverContent = React.forwardRef(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (React.createElement(PopoverPrimitive.Portal, null,
|
|
8
|
+
React.createElement(PopoverPrimitive.Content, { ref: ref, align: align, sideOffset: sideOffset, className: cn('crfrsr-popover__content', className), ...props }))));
|
|
9
|
+
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
|
10
|
+
export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true when the viewport width is below `breakpoint` (default 768px),
|
|
3
|
+
* updating on resize. SSR-safe (returns false when `window` is unavailable).
|
|
4
|
+
*
|
|
5
|
+
* Replaces the app-level StyleContext dependency the Combobox previously relied
|
|
6
|
+
* on, so the component is self-contained.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useIsMobile(breakpoint?: number): boolean;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Returns true when the viewport width is below `breakpoint` (default 768px),
|
|
4
|
+
* updating on resize. SSR-safe (returns false when `window` is unavailable).
|
|
5
|
+
*
|
|
6
|
+
* Replaces the app-level StyleContext dependency the Combobox previously relied
|
|
7
|
+
* on, so the component is self-contained.
|
|
8
|
+
*/
|
|
9
|
+
export function useIsMobile(breakpoint = 768) {
|
|
10
|
+
const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' ? window.innerWidth < breakpoint : false);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const check = () => setIsMobile(window.innerWidth < breakpoint);
|
|
13
|
+
check();
|
|
14
|
+
window.addEventListener('resize', check);
|
|
15
|
+
return () => window.removeEventListener('resize', check);
|
|
16
|
+
}, [breakpoint]);
|
|
17
|
+
return isMobile;
|
|
18
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/lib/cn.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ClassValue } from 'clsx';
|
|
2
|
+
/**
|
|
3
|
+
* Class-name combiner for the design system. Uses clsx only — the library ships
|
|
4
|
+
* plain CSS class names (no Tailwind), so there is nothing for tailwind-merge to
|
|
5
|
+
* resolve. Consumer-provided classes are appended last and win by source order.
|
|
6
|
+
*/
|
|
7
|
+
export declare function cn(...inputs: ClassValue[]): string;
|
package/dist/lib/cn.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { clsx } from 'clsx';
|
|
2
|
+
/**
|
|
3
|
+
* Class-name combiner for the design system. Uses clsx only — the library ships
|
|
4
|
+
* plain CSS class names (no Tailwind), so there is nothing for tailwind-merge to
|
|
5
|
+
* resolve. Consumer-provided classes are appended last and win by source order.
|
|
6
|
+
*/
|
|
7
|
+
export function cn(...inputs) {
|
|
8
|
+
return clsx(inputs);
|
|
9
|
+
}
|