@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.
@@ -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,4 @@
1
+ export * from './Button';
2
+ export * from './Pill';
3
+ export * from './Combobox';
4
+ export { CommandItem } from './command';
@@ -0,0 +1,6 @@
1
+ export * from './Button';
2
+ export * from './Pill';
3
+ export * from './Combobox';
4
+ // The generic Combobox renders each option via a render prop; consumers that
5
+ // build option rows with cmdk's item need this primitive.
6
+ export { CommandItem } from './command';
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @crfrsr/ui
3
+ * React components for crfrsr Design System
4
+ */
5
+ export * from './ThemeProvider';
6
+ export * from './typography';
7
+ export * from './components';
8
+ export * from './hooks/useIsMobile';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @crfrsr/ui
3
+ * React components for crfrsr Design System
4
+ */
5
+ export * from './ThemeProvider';
6
+ export * from './typography';
7
+ export * from './components';
8
+ export * from './hooks/useIsMobile';
@@ -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
+ }