@brainfish-ai/components 0.19.0 → 0.19.1
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/esm/chunks/font-picker.DX1mER3H.js +181 -0
- package/dist/esm/chunks/font-picker.DX1mER3H.js.map +1 -0
- package/dist/esm/components/font-picker.js +2 -0
- package/dist/esm/components/font-picker.js.map +1 -0
- package/dist/esm/index.js +5 -178
- package/dist/esm/index.js.map +1 -1
- package/dist/font-picker.d.ts +106 -0
- package/dist/stats.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Check, SpinnerGap } from '@phosphor-icons/react';
|
|
3
|
+
import { c as cn } from './utils.Cwtlq8dh.js';
|
|
4
|
+
import { CommandItem } from '../components/ui/command.js';
|
|
5
|
+
import { C as Combobox } from './combobox.DNYCWyub.js';
|
|
6
|
+
|
|
7
|
+
const FONT_CACHE_KEY = "brainfish-google-fonts-cache";
|
|
8
|
+
const FONT_CACHE_TIMESTAMP_KEY = "brainfish-google-fonts-cache-timestamp";
|
|
9
|
+
const FONT_CACHE_DURATION_MS = 24 * 60 * 60 * 1e3;
|
|
10
|
+
const loadedFonts = /* @__PURE__ */ new Set();
|
|
11
|
+
const getCachedFonts = () => {
|
|
12
|
+
try {
|
|
13
|
+
const timestamp = localStorage.getItem(FONT_CACHE_TIMESTAMP_KEY);
|
|
14
|
+
if (!timestamp) return null;
|
|
15
|
+
const cacheAge = Date.now() - parseInt(timestamp, 10);
|
|
16
|
+
if (cacheAge > FONT_CACHE_DURATION_MS) {
|
|
17
|
+
localStorage.removeItem(FONT_CACHE_KEY);
|
|
18
|
+
localStorage.removeItem(FONT_CACHE_TIMESTAMP_KEY);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const cached = localStorage.getItem(FONT_CACHE_KEY);
|
|
22
|
+
if (!cached) return null;
|
|
23
|
+
return JSON.parse(cached);
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const setCachedFonts = (fonts) => {
|
|
29
|
+
try {
|
|
30
|
+
localStorage.setItem(FONT_CACHE_KEY, JSON.stringify(fonts));
|
|
31
|
+
localStorage.setItem(FONT_CACHE_TIMESTAMP_KEY, Date.now().toString());
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const loadFont = (fontFamily) => {
|
|
36
|
+
if (loadedFonts.has(fontFamily)) return;
|
|
37
|
+
const link = document.createElement("link");
|
|
38
|
+
link.href = `https://fonts.googleapis.com/css2?family=${fontFamily.replace(/ /g, "+")}&display=swap`;
|
|
39
|
+
link.rel = "stylesheet";
|
|
40
|
+
document.head.appendChild(link);
|
|
41
|
+
loadedFonts.add(fontFamily);
|
|
42
|
+
};
|
|
43
|
+
const clearFontCache = () => {
|
|
44
|
+
try {
|
|
45
|
+
localStorage.removeItem(FONT_CACHE_KEY);
|
|
46
|
+
localStorage.removeItem(FONT_CACHE_TIMESTAMP_KEY);
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function useFontOptions({ apiKey, maxFonts = 500, onError }) {
|
|
52
|
+
const [fonts, setFonts] = React.useState([]);
|
|
53
|
+
const [isLoading, setIsLoading] = React.useState(true);
|
|
54
|
+
const onErrorRef = React.useRef(onError);
|
|
55
|
+
React.useEffect(() => {
|
|
56
|
+
onErrorRef.current = onError;
|
|
57
|
+
}, [onError]);
|
|
58
|
+
React.useEffect(() => {
|
|
59
|
+
const fetchFonts = async () => {
|
|
60
|
+
const cachedFonts = getCachedFonts();
|
|
61
|
+
if (cachedFonts && cachedFonts.length > 0) {
|
|
62
|
+
setFonts(cachedFonts.slice(0, maxFonts));
|
|
63
|
+
setIsLoading(false);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(`https://www.googleapis.com/webfonts/v1/webfonts?key=${apiKey}&sort=popularity`);
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
throw new Error(`Failed to fetch fonts: ${response.statusText}`);
|
|
70
|
+
}
|
|
71
|
+
const data = await response.json();
|
|
72
|
+
const fontOptions = data.items.map((fontItem) => ({
|
|
73
|
+
value: fontItem.family,
|
|
74
|
+
label: fontItem.family,
|
|
75
|
+
category: fontItem.category,
|
|
76
|
+
variants: fontItem.variants
|
|
77
|
+
}));
|
|
78
|
+
setCachedFonts(fontOptions);
|
|
79
|
+
setFonts(fontOptions.slice(0, maxFonts));
|
|
80
|
+
} catch (err) {
|
|
81
|
+
const error = err instanceof Error ? err : new Error("Failed to fetch fonts");
|
|
82
|
+
console.error("FontPicker: Failed to fetch Google Fonts", error);
|
|
83
|
+
onErrorRef.current?.(error);
|
|
84
|
+
} finally {
|
|
85
|
+
setIsLoading(false);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
void fetchFonts();
|
|
89
|
+
}, [apiKey, maxFonts]);
|
|
90
|
+
return { fonts, isLoading, loadFont };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const FontPickerItem = React.memo(function FontPickerItem2({ font, isSelected, onSelect }) {
|
|
94
|
+
const itemRef = React.useRef(null);
|
|
95
|
+
const [isVisible, setIsVisible] = React.useState(false);
|
|
96
|
+
React.useEffect(() => {
|
|
97
|
+
const observer = new IntersectionObserver(
|
|
98
|
+
([entry]) => {
|
|
99
|
+
if (entry.isIntersecting) {
|
|
100
|
+
setIsVisible(true);
|
|
101
|
+
observer.disconnect();
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{ threshold: 0.1 }
|
|
105
|
+
);
|
|
106
|
+
if (itemRef.current) {
|
|
107
|
+
observer.observe(itemRef.current);
|
|
108
|
+
}
|
|
109
|
+
return () => observer.disconnect();
|
|
110
|
+
}, []);
|
|
111
|
+
React.useEffect(() => {
|
|
112
|
+
if (isVisible) {
|
|
113
|
+
loadFont(font.value);
|
|
114
|
+
}
|
|
115
|
+
}, [isVisible, font.value]);
|
|
116
|
+
const ariaLabel = `Select ${font.label} font${font.category ? `, ${font.category} category` : ""}`;
|
|
117
|
+
return /* @__PURE__ */ React.createElement(
|
|
118
|
+
CommandItem,
|
|
119
|
+
{
|
|
120
|
+
ref: itemRef,
|
|
121
|
+
value: font.label,
|
|
122
|
+
onSelect,
|
|
123
|
+
className: "cursor-pointer",
|
|
124
|
+
"aria-label": ariaLabel,
|
|
125
|
+
"aria-selected": isSelected
|
|
126
|
+
},
|
|
127
|
+
/* @__PURE__ */ React.createElement("span", { style: { fontFamily: isVisible ? font.value : void 0 }, className: "flex-1 truncate" }, font.label),
|
|
128
|
+
font.category && /* @__PURE__ */ React.createElement("span", { className: "text-xs text-muted-foreground capitalize" }, font.category),
|
|
129
|
+
/* @__PURE__ */ React.createElement(Check, { className: cn("ml-2 h-4 w-4", isSelected ? "opacity-100" : "opacity-0") })
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
function FontPicker({
|
|
134
|
+
value,
|
|
135
|
+
onChange,
|
|
136
|
+
apiKey,
|
|
137
|
+
placeholder = "Select a font...",
|
|
138
|
+
searchPlaceholder = "Search fonts...",
|
|
139
|
+
noResultsText = "No fonts found.",
|
|
140
|
+
loadingText = "Loading fonts...",
|
|
141
|
+
disabled = false,
|
|
142
|
+
className,
|
|
143
|
+
dropdownWidth,
|
|
144
|
+
maxFonts = 500,
|
|
145
|
+
onError
|
|
146
|
+
}) {
|
|
147
|
+
const { fonts, isLoading, loadFont } = useFontOptions({ apiKey, maxFonts, onError });
|
|
148
|
+
React.useEffect(() => {
|
|
149
|
+
if (value) {
|
|
150
|
+
loadFont(value);
|
|
151
|
+
}
|
|
152
|
+
}, [value, loadFont]);
|
|
153
|
+
const options = fonts;
|
|
154
|
+
return /* @__PURE__ */ React.createElement(
|
|
155
|
+
Combobox,
|
|
156
|
+
{
|
|
157
|
+
options,
|
|
158
|
+
value,
|
|
159
|
+
onChange,
|
|
160
|
+
defaultValueLabel: placeholder,
|
|
161
|
+
placeholder: searchPlaceholder,
|
|
162
|
+
noResultsLabel: noResultsText,
|
|
163
|
+
disabled: disabled || isLoading,
|
|
164
|
+
className,
|
|
165
|
+
dropdownWidth,
|
|
166
|
+
renderTrigger: ({ selectedItem }) => {
|
|
167
|
+
if (isLoading) {
|
|
168
|
+
return /* @__PURE__ */ React.createElement("span", { className: "flex items-center gap-2 text-muted-foreground" }, /* @__PURE__ */ React.createElement(SpinnerGap, { className: "h-4 w-4 animate-spin" }), loadingText);
|
|
169
|
+
}
|
|
170
|
+
if (selectedItem) {
|
|
171
|
+
return /* @__PURE__ */ React.createElement("span", { style: { fontFamily: selectedItem.value } }, selectedItem.label);
|
|
172
|
+
}
|
|
173
|
+
return /* @__PURE__ */ React.createElement("span", { className: "text-muted-foreground" }, placeholder);
|
|
174
|
+
},
|
|
175
|
+
renderItem: ({ item, isSelected, onSelect }) => /* @__PURE__ */ React.createElement(FontPickerItem, { font: item, isSelected, onSelect })
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export { FontPicker as F, FontPickerItem as a, clearFontCache as c, getCachedFonts as g, loadFont as l, setCachedFonts as s, useFontOptions as u };
|
|
181
|
+
//# sourceMappingURL=font-picker.DX1mER3H.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"font-picker.DX1mER3H.js","sources":["../../../src/components/font-picker/font-utils.ts","../../../src/components/font-picker/use-font-options.ts","../../../src/components/font-picker/font-picker-item.tsx","../../../src/components/font-picker/font-picker.tsx"],"sourcesContent":["import { FontOption } from './types';\n\n// Cache keys for localStorage\nconst FONT_CACHE_KEY = 'brainfish-google-fonts-cache';\nconst FONT_CACHE_TIMESTAMP_KEY = 'brainfish-google-fonts-cache-timestamp';\nconst FONT_CACHE_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours\n\n// Cache for loaded font stylesheets to prevent duplicate loads\nconst loadedFonts = new Set<string>();\n\n/**\n * Get cached fonts from localStorage\n */\nexport const getCachedFonts = (): FontOption[] | null => {\n try {\n const timestamp = localStorage.getItem(FONT_CACHE_TIMESTAMP_KEY);\n if (!timestamp) return null;\n\n const cacheAge = Date.now() - parseInt(timestamp, 10);\n if (cacheAge > FONT_CACHE_DURATION_MS) {\n localStorage.removeItem(FONT_CACHE_KEY);\n localStorage.removeItem(FONT_CACHE_TIMESTAMP_KEY);\n\n return null;\n }\n\n const cached = localStorage.getItem(FONT_CACHE_KEY);\n if (!cached) return null;\n\n return JSON.parse(cached) as FontOption[];\n } catch {\n return null;\n }\n};\n\n/**\n * Save fonts to localStorage cache\n */\nexport const setCachedFonts = (fonts: FontOption[]): void => {\n try {\n localStorage.setItem(FONT_CACHE_KEY, JSON.stringify(fonts));\n localStorage.setItem(FONT_CACHE_TIMESTAMP_KEY, Date.now().toString());\n } catch {\n // localStorage might be full or unavailable, ignore\n }\n};\n\n/**\n * Load a Google Font stylesheet dynamically\n */\nexport const loadFont = (fontFamily: string): void => {\n if (loadedFonts.has(fontFamily)) return;\n\n const link = document.createElement('link');\n link.href = `https://fonts.googleapis.com/css2?family=${fontFamily.replace(/ /g, '+')}&display=swap`;\n link.rel = 'stylesheet';\n document.head.appendChild(link);\n loadedFonts.add(fontFamily);\n};\n\n/**\n * Clear the font cache from localStorage\n * Useful for forcing a fresh fetch of fonts\n */\nexport const clearFontCache = (): void => {\n try {\n localStorage.removeItem(FONT_CACHE_KEY);\n localStorage.removeItem(FONT_CACHE_TIMESTAMP_KEY);\n } catch {\n // localStorage might be unavailable, ignore\n }\n};\n","import * as React from 'react';\n\nimport { FontOption } from './types';\nimport { getCachedFonts, setCachedFonts, loadFont } from './font-utils';\n\nexport interface UseFontOptionsProps {\n apiKey: string;\n maxFonts?: number;\n onError?: (error: Error) => void;\n}\n\nexport interface UseFontOptionsReturn {\n fonts: FontOption[];\n isLoading: boolean;\n loadFont: typeof loadFont;\n}\n\n/**\n * Hook to fetch and manage Google Fonts options\n * Handles caching, loading state, and font stylesheet loading\n */\nexport function useFontOptions({ apiKey, maxFonts = 500, onError }: UseFontOptionsProps): UseFontOptionsReturn {\n const [fonts, setFonts] = React.useState<FontOption[]>([]);\n const [isLoading, setIsLoading] = React.useState(true);\n\n // Stabilize onError callback reference to prevent effect re-runs when\n // consumers pass inline functions (which create new references on every render)\n const onErrorRef = React.useRef(onError);\n React.useEffect(() => {\n onErrorRef.current = onError;\n }, [onError]);\n\n React.useEffect(() => {\n const fetchFonts = async () => {\n // Check cache first\n const cachedFonts = getCachedFonts();\n if (cachedFonts && cachedFonts.length > 0) {\n setFonts(cachedFonts.slice(0, maxFonts));\n setIsLoading(false);\n\n return;\n }\n\n try {\n const response = await fetch(`https://www.googleapis.com/webfonts/v1/webfonts?key=${apiKey}&sort=popularity`);\n\n if (!response.ok) {\n throw new Error(`Failed to fetch fonts: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n const fontOptions: FontOption[] = data.items.map((fontItem: { family: string; category: string; variants: string[] }) => ({\n value: fontItem.family,\n label: fontItem.family,\n category: fontItem.category,\n variants: fontItem.variants,\n }));\n\n // Cache all fonts, but only display up to maxFonts\n setCachedFonts(fontOptions);\n setFonts(fontOptions.slice(0, maxFonts));\n } catch (err) {\n const error = err instanceof Error ? err : new Error('Failed to fetch fonts');\n // eslint-disable-next-line no-console\n console.error('FontPicker: Failed to fetch Google Fonts', error);\n onErrorRef.current?.(error);\n } finally {\n setIsLoading(false);\n }\n };\n\n void fetchFonts();\n }, [apiKey, maxFonts]);\n\n return { fonts, isLoading, loadFont };\n}\n","import * as React from 'react';\nimport { Check } from '@phosphor-icons/react';\n\nimport { FontOption } from './types';\nimport { loadFont } from './font-utils';\n\nimport { cn } from '@/lib/utils';\nimport { CommandItem } from '@/components/ui/command';\n\nexport interface FontPickerItemProps {\n font: FontOption;\n isSelected: boolean;\n onSelect: () => void;\n}\n\n/**\n * Individual font item in the dropdown list\n * Loads font on mount for preview using Intersection Observer\n * Memoized to prevent unnecessary re-renders\n */\nexport const FontPickerItem = React.memo(function FontPickerItem({ font, isSelected, onSelect }: FontPickerItemProps) {\n const itemRef = React.useRef<HTMLDivElement>(null);\n const [isVisible, setIsVisible] = React.useState(false);\n\n // Use Intersection Observer to lazy-load fonts\n React.useEffect(() => {\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) {\n setIsVisible(true);\n observer.disconnect();\n }\n },\n { threshold: 0.1 },\n );\n\n if (itemRef.current) {\n observer.observe(itemRef.current);\n }\n\n return () => observer.disconnect();\n }, []);\n\n // Load font when visible\n React.useEffect(() => {\n if (isVisible) {\n loadFont(font.value);\n }\n }, [isVisible, font.value]);\n\n // Build accessible label\n const ariaLabel = `Select ${font.label} font${font.category ? `, ${font.category} category` : ''}`;\n\n return (\n <CommandItem\n ref={itemRef}\n value={font.label}\n onSelect={onSelect}\n className=\"cursor-pointer\"\n aria-label={ariaLabel}\n aria-selected={isSelected}\n >\n <span style={{ fontFamily: isVisible ? font.value : undefined }} className=\"flex-1 truncate\">\n {font.label}\n </span>\n {font.category && <span className=\"text-xs text-muted-foreground capitalize\">{font.category}</span>}\n <Check className={cn('ml-2 h-4 w-4', isSelected ? 'opacity-100' : 'opacity-0')} />\n </CommandItem>\n );\n});\n","import * as React from 'react';\nimport { SpinnerGap } from '@phosphor-icons/react';\n\nimport { useFontOptions } from './use-font-options';\nimport { FontPickerItem } from './font-picker-item';\n\nimport { Combobox } from '@/components/combobox/combobox';\n\nexport interface FontPickerProps {\n /** Currently selected font family */\n value?: string;\n /** Callback when font selection changes */\n onChange?: (fontFamily: string) => void;\n /** Google Fonts API key (required) */\n apiKey: string;\n /** Placeholder text when no font is selected */\n placeholder?: string;\n /** Text shown in search input */\n searchPlaceholder?: string;\n /** Text shown when no fonts match the search */\n noResultsText?: string;\n /** Text shown while fonts are loading */\n loadingText?: string;\n /** Whether the picker is disabled */\n disabled?: boolean;\n /** Additional CSS classes for the trigger button */\n className?: string;\n /** Width of the dropdown (defaults to trigger width) */\n dropdownWidth?: number;\n /** Maximum number of fonts to display (for performance) */\n maxFonts?: number;\n /** Callback when an error occurs fetching fonts */\n onError?: (error: Error) => void;\n}\n\n/**\n * FontPicker - A searchable dropdown for selecting Google Fonts\n *\n * @example\n * ```tsx\n * const [font, setFont] = useState('Inter');\n *\n * <FontPicker\n * apiKey=\"YOUR_API_KEY\"\n * value={font}\n * onChange={setFont}\n * placeholder=\"Select a font\"\n * />\n * ```\n */\nexport function FontPicker({\n value,\n onChange,\n apiKey,\n placeholder = 'Select a font...',\n searchPlaceholder = 'Search fonts...',\n noResultsText = 'No fonts found.',\n loadingText = 'Loading fonts...',\n disabled = false,\n className,\n dropdownWidth,\n maxFonts = 500,\n onError,\n}: FontPickerProps) {\n const { fonts, isLoading, loadFont } = useFontOptions({ apiKey, maxFonts, onError });\n\n // Load font stylesheet when selected\n React.useEffect(() => {\n if (value) {\n loadFont(value);\n }\n }, [value, loadFont]);\n\n // fonts already have value/label which Combobox requires\n const options = fonts;\n\n return (\n <Combobox\n options={options}\n value={value}\n onChange={onChange}\n defaultValueLabel={placeholder}\n placeholder={searchPlaceholder}\n noResultsLabel={noResultsText}\n disabled={disabled || isLoading}\n className={className}\n dropdownWidth={dropdownWidth}\n renderTrigger={({ selectedItem }) => {\n if (isLoading) {\n return (\n <span className=\"flex items-center gap-2 text-muted-foreground\">\n <SpinnerGap className=\"h-4 w-4 animate-spin\" />\n {loadingText}\n </span>\n );\n }\n\n if (selectedItem) {\n return <span style={{ fontFamily: selectedItem.value }}>{selectedItem.label}</span>;\n }\n\n return <span className=\"text-muted-foreground\">{placeholder}</span>;\n }}\n renderItem={({ item, isSelected, onSelect }) => (\n <FontPickerItem font={item} isSelected={isSelected} onSelect={onSelect} />\n )}\n />\n );\n}\n\nexport default FontPicker;\n"],"names":["FontPickerItem"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"font-picker.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
package/dist/esm/index.js
CHANGED
|
@@ -13,8 +13,7 @@ export { ScrollArea, ScrollBar } from './components/ui/scroll-area.js';
|
|
|
13
13
|
export { Label } from './components/ui/label.js';
|
|
14
14
|
export { Input } from './components/ui/input.js';
|
|
15
15
|
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from './components/ui/popover.js';
|
|
16
|
-
|
|
17
|
-
export { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandList, CommandSeparator, CommandShortcut } from './components/ui/command.js';
|
|
16
|
+
export { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut } from './components/ui/command.js';
|
|
18
17
|
export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from './components/ui/dialog.js';
|
|
19
18
|
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './components/ui/card.js';
|
|
20
19
|
export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow } from './components/ui/table.js';
|
|
@@ -34,15 +33,16 @@ export { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHe
|
|
|
34
33
|
export { Spinner } from './components/ui/spinner.js';
|
|
35
34
|
export { S as SimpleSelect } from './chunks/simpleSelect.B1rktKkt.js';
|
|
36
35
|
export { D as DatePicker } from './chunks/date-picker.sQXSlqNZ.js';
|
|
37
|
-
|
|
36
|
+
export { C as Combobox } from './chunks/combobox.DNYCWyub.js';
|
|
38
37
|
export { T as TwoLevelCombobox } from './chunks/two-level-combobox.DsWPDcI6.js';
|
|
39
38
|
export { F as Filter, a as Filters, O as Operator } from './chunks/filters.Be4KPvMA.js';
|
|
40
39
|
export { F as FileUpload, a as FileUploadStatus, f as formatFileSize } from './chunks/file-upload-status.Vw0Zxmir.js';
|
|
41
40
|
export { D as DataTable } from './chunks/data-table.DbcAYxMY.js';
|
|
42
41
|
export { L as Logo } from './chunks/logo.BXk28Fqo.js';
|
|
43
42
|
export { T as Trend, a as TrendValue } from './chunks/trend-value.COSukPwk.js';
|
|
43
|
+
export { F as FontPicker, a as FontPickerItem, c as clearFontCache, g as getCachedFonts, l as loadFont, s as setCachedFonts, u as useFontOptions } from './chunks/font-picker.DX1mER3H.js';
|
|
44
44
|
import * as React from 'react';
|
|
45
|
-
import { Plus
|
|
45
|
+
import { Plus } from '@phosphor-icons/react';
|
|
46
46
|
import { c as cn } from './chunks/utils.Cwtlq8dh.js';
|
|
47
47
|
|
|
48
48
|
function InputWithTags({
|
|
@@ -147,178 +147,5 @@ function InputWithTags({
|
|
|
147
147
|
))));
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
const FONT_CACHE_TIMESTAMP_KEY = "brainfish-google-fonts-cache-timestamp";
|
|
152
|
-
const FONT_CACHE_DURATION_MS = 24 * 60 * 60 * 1e3;
|
|
153
|
-
const loadedFonts = /* @__PURE__ */ new Set();
|
|
154
|
-
const getCachedFonts = () => {
|
|
155
|
-
try {
|
|
156
|
-
const timestamp = localStorage.getItem(FONT_CACHE_TIMESTAMP_KEY);
|
|
157
|
-
if (!timestamp) return null;
|
|
158
|
-
const cacheAge = Date.now() - parseInt(timestamp, 10);
|
|
159
|
-
if (cacheAge > FONT_CACHE_DURATION_MS) {
|
|
160
|
-
localStorage.removeItem(FONT_CACHE_KEY);
|
|
161
|
-
localStorage.removeItem(FONT_CACHE_TIMESTAMP_KEY);
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
const cached = localStorage.getItem(FONT_CACHE_KEY);
|
|
165
|
-
if (!cached) return null;
|
|
166
|
-
return JSON.parse(cached);
|
|
167
|
-
} catch {
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
const setCachedFonts = (fonts) => {
|
|
172
|
-
try {
|
|
173
|
-
localStorage.setItem(FONT_CACHE_KEY, JSON.stringify(fonts));
|
|
174
|
-
localStorage.setItem(FONT_CACHE_TIMESTAMP_KEY, Date.now().toString());
|
|
175
|
-
} catch {
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
const loadFont = (fontFamily) => {
|
|
179
|
-
if (loadedFonts.has(fontFamily)) return;
|
|
180
|
-
const link = document.createElement("link");
|
|
181
|
-
link.href = `https://fonts.googleapis.com/css2?family=${fontFamily.replace(/ /g, "+")}&display=swap`;
|
|
182
|
-
link.rel = "stylesheet";
|
|
183
|
-
document.head.appendChild(link);
|
|
184
|
-
loadedFonts.add(fontFamily);
|
|
185
|
-
};
|
|
186
|
-
const clearFontCache = () => {
|
|
187
|
-
try {
|
|
188
|
-
localStorage.removeItem(FONT_CACHE_KEY);
|
|
189
|
-
localStorage.removeItem(FONT_CACHE_TIMESTAMP_KEY);
|
|
190
|
-
} catch {
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
function useFontOptions({ apiKey, maxFonts = 500, onError }) {
|
|
195
|
-
const [fonts, setFonts] = React.useState([]);
|
|
196
|
-
const [isLoading, setIsLoading] = React.useState(true);
|
|
197
|
-
const onErrorRef = React.useRef(onError);
|
|
198
|
-
React.useEffect(() => {
|
|
199
|
-
onErrorRef.current = onError;
|
|
200
|
-
}, [onError]);
|
|
201
|
-
React.useEffect(() => {
|
|
202
|
-
const fetchFonts = async () => {
|
|
203
|
-
const cachedFonts = getCachedFonts();
|
|
204
|
-
if (cachedFonts && cachedFonts.length > 0) {
|
|
205
|
-
setFonts(cachedFonts.slice(0, maxFonts));
|
|
206
|
-
setIsLoading(false);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
try {
|
|
210
|
-
const response = await fetch(`https://www.googleapis.com/webfonts/v1/webfonts?key=${apiKey}&sort=popularity`);
|
|
211
|
-
if (!response.ok) {
|
|
212
|
-
throw new Error(`Failed to fetch fonts: ${response.statusText}`);
|
|
213
|
-
}
|
|
214
|
-
const data = await response.json();
|
|
215
|
-
const fontOptions = data.items.map((fontItem) => ({
|
|
216
|
-
value: fontItem.family,
|
|
217
|
-
label: fontItem.family,
|
|
218
|
-
category: fontItem.category,
|
|
219
|
-
variants: fontItem.variants
|
|
220
|
-
}));
|
|
221
|
-
setCachedFonts(fontOptions);
|
|
222
|
-
setFonts(fontOptions.slice(0, maxFonts));
|
|
223
|
-
} catch (err) {
|
|
224
|
-
const error = err instanceof Error ? err : new Error("Failed to fetch fonts");
|
|
225
|
-
console.error("FontPicker: Failed to fetch Google Fonts", error);
|
|
226
|
-
onErrorRef.current?.(error);
|
|
227
|
-
} finally {
|
|
228
|
-
setIsLoading(false);
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
void fetchFonts();
|
|
232
|
-
}, [apiKey, maxFonts]);
|
|
233
|
-
return { fonts, isLoading, loadFont };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const FontPickerItem = React.memo(function FontPickerItem2({ font, isSelected, onSelect }) {
|
|
237
|
-
const itemRef = React.useRef(null);
|
|
238
|
-
const [isVisible, setIsVisible] = React.useState(false);
|
|
239
|
-
React.useEffect(() => {
|
|
240
|
-
const observer = new IntersectionObserver(
|
|
241
|
-
([entry]) => {
|
|
242
|
-
if (entry.isIntersecting) {
|
|
243
|
-
setIsVisible(true);
|
|
244
|
-
observer.disconnect();
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
{ threshold: 0.1 }
|
|
248
|
-
);
|
|
249
|
-
if (itemRef.current) {
|
|
250
|
-
observer.observe(itemRef.current);
|
|
251
|
-
}
|
|
252
|
-
return () => observer.disconnect();
|
|
253
|
-
}, []);
|
|
254
|
-
React.useEffect(() => {
|
|
255
|
-
if (isVisible) {
|
|
256
|
-
loadFont(font.value);
|
|
257
|
-
}
|
|
258
|
-
}, [isVisible, font.value]);
|
|
259
|
-
const ariaLabel = `Select ${font.label} font${font.category ? `, ${font.category} category` : ""}`;
|
|
260
|
-
return /* @__PURE__ */ React.createElement(
|
|
261
|
-
CommandItem,
|
|
262
|
-
{
|
|
263
|
-
ref: itemRef,
|
|
264
|
-
value: font.label,
|
|
265
|
-
onSelect,
|
|
266
|
-
className: "cursor-pointer",
|
|
267
|
-
"aria-label": ariaLabel,
|
|
268
|
-
"aria-selected": isSelected
|
|
269
|
-
},
|
|
270
|
-
/* @__PURE__ */ React.createElement("span", { style: { fontFamily: isVisible ? font.value : void 0 }, className: "flex-1 truncate" }, font.label),
|
|
271
|
-
font.category && /* @__PURE__ */ React.createElement("span", { className: "text-xs text-muted-foreground capitalize" }, font.category),
|
|
272
|
-
/* @__PURE__ */ React.createElement(Check, { className: cn("ml-2 h-4 w-4", isSelected ? "opacity-100" : "opacity-0") })
|
|
273
|
-
);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
function FontPicker({
|
|
277
|
-
value,
|
|
278
|
-
onChange,
|
|
279
|
-
apiKey,
|
|
280
|
-
placeholder = "Select a font...",
|
|
281
|
-
searchPlaceholder = "Search fonts...",
|
|
282
|
-
noResultsText = "No fonts found.",
|
|
283
|
-
loadingText = "Loading fonts...",
|
|
284
|
-
disabled = false,
|
|
285
|
-
className,
|
|
286
|
-
dropdownWidth,
|
|
287
|
-
maxFonts = 500,
|
|
288
|
-
onError
|
|
289
|
-
}) {
|
|
290
|
-
const { fonts, isLoading, loadFont } = useFontOptions({ apiKey, maxFonts, onError });
|
|
291
|
-
React.useEffect(() => {
|
|
292
|
-
if (value) {
|
|
293
|
-
loadFont(value);
|
|
294
|
-
}
|
|
295
|
-
}, [value, loadFont]);
|
|
296
|
-
const options = fonts;
|
|
297
|
-
return /* @__PURE__ */ React.createElement(
|
|
298
|
-
Combobox,
|
|
299
|
-
{
|
|
300
|
-
options,
|
|
301
|
-
value,
|
|
302
|
-
onChange,
|
|
303
|
-
defaultValueLabel: placeholder,
|
|
304
|
-
placeholder: searchPlaceholder,
|
|
305
|
-
noResultsLabel: noResultsText,
|
|
306
|
-
disabled: disabled || isLoading,
|
|
307
|
-
className,
|
|
308
|
-
dropdownWidth,
|
|
309
|
-
renderTrigger: ({ selectedItem }) => {
|
|
310
|
-
if (isLoading) {
|
|
311
|
-
return /* @__PURE__ */ React.createElement("span", { className: "flex items-center gap-2 text-muted-foreground" }, /* @__PURE__ */ React.createElement(SpinnerGap, { className: "h-4 w-4 animate-spin" }), loadingText);
|
|
312
|
-
}
|
|
313
|
-
if (selectedItem) {
|
|
314
|
-
return /* @__PURE__ */ React.createElement("span", { style: { fontFamily: selectedItem.value } }, selectedItem.label);
|
|
315
|
-
}
|
|
316
|
-
return /* @__PURE__ */ React.createElement("span", { className: "text-muted-foreground" }, placeholder);
|
|
317
|
-
},
|
|
318
|
-
renderItem: ({ item, isSelected, onSelect }) => /* @__PURE__ */ React.createElement(FontPickerItem, { font: item, isSelected, onSelect })
|
|
319
|
-
}
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
export { Button, Combobox, CommandItem, FontPicker, FontPickerItem, InputWithTags, Textarea, clearFontCache, getCachedFonts, loadFont, setCachedFonts, useFontOptions };
|
|
150
|
+
export { Button, InputWithTags, Textarea };
|
|
324
151
|
//# sourceMappingURL=index.js.map
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../src/components/input-with-tags/input-with-tags.tsx","../../src/components/font-picker/font-utils.ts","../../src/components/font-picker/use-font-options.ts","../../src/components/font-picker/font-picker-item.tsx","../../src/components/font-picker/font-picker.tsx"],"sourcesContent":["import * as React from 'react';\nimport { Plus } from '@phosphor-icons/react';\n\nimport { cn } from '@/lib/utils';\nimport { Textarea } from '@/components/ui/textarea';\nimport { Button } from '@/components/ui/button';\n\nexport interface Tag {\n label: string;\n value: string;\n}\n\nexport interface InputWithTagsProps {\n title: string;\n placeholder?: string;\n tags: Tag[];\n value?: string;\n defaultValue?: string;\n onChange?: (value: string) => void;\n onTagClick?: (tag: Tag) => void;\n singleValue?: boolean; // If true, clicking a tag replaces the value instead of appending\n className?: string;\n inputClassName?: string;\n tagsClassName?: string;\n disabled?: boolean;\n // Standard textarea props\n name?: string;\n id?: string;\n required?: boolean;\n maxLength?: number;\n minLength?: number;\n rows?: number;\n}\n\nexport function InputWithTags({\n title,\n placeholder,\n tags,\n value: controlledValue,\n defaultValue,\n onChange,\n onTagClick,\n singleValue = false,\n className,\n inputClassName,\n tagsClassName,\n disabled,\n name,\n id,\n required,\n maxLength,\n minLength,\n rows,\n}: InputWithTagsProps) {\n // Helper to find tags present in value (tags are appended as: `${value} ${tag.value}`)\n const findTagsInValue = React.useCallback(\n (val: string): Set<string> => {\n if (!val) return new Set();\n const trimmedVal = val.trim();\n const foundTags = new Set<string>();\n\n for (const tag of tags) {\n const tagValue = tag.value.trim();\n if (singleValue) {\n if (trimmedVal === tagValue) {\n foundTags.add(tag.value);\n }\n } else {\n // Check if tag appears as complete segment (tags are appended as: `${value} ${tag.value}`)\n // Need to check: exact match, at start, in middle, or at end\n if (\n trimmedVal === tagValue ||\n trimmedVal.startsWith(tagValue + ' ') ||\n trimmedVal.includes(' ' + tagValue + ' ') ||\n trimmedVal.endsWith(' ' + tagValue)\n ) {\n foundTags.add(tag.value);\n }\n }\n }\n\n return foundTags;\n },\n [tags, singleValue],\n );\n\n // Initialize clickedTags based on initial value\n const initialValue = controlledValue !== undefined ? controlledValue : defaultValue || '';\n const [internalValue, setInternalValue] = React.useState(defaultValue || '');\n const [clickedTags, setClickedTags] = React.useState<Set<string>>(() => findTagsInValue(initialValue));\n\n // Use controlled value if provided, otherwise use internal state\n const value = controlledValue !== undefined ? controlledValue : internalValue;\n\n // Update clickedTags when value or tags change\n React.useEffect(() => {\n const currentValue = controlledValue !== undefined ? controlledValue : internalValue;\n setClickedTags(findTagsInValue(currentValue));\n }, [controlledValue, internalValue, tags, singleValue, findTagsInValue]);\n\n // Filter out clicked tags\n const availableTags = tags.filter((tag) => !clickedTags.has(tag.value));\n\n const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n const newValue = e.target.value;\n if (controlledValue === undefined) {\n setInternalValue(newValue);\n // Sync clickedTags with the new value in uncontrolled mode\n setClickedTags(findTagsInValue(newValue));\n }\n onChange?.(newValue);\n };\n\n const handleTagClick = (tag: Tag) => {\n const newValue = singleValue ? tag.value : value ? `${value} ${tag.value}` : tag.value;\n if (controlledValue === undefined) {\n // In uncontrolled mode, update state and sync clickedTags immediately\n setInternalValue(newValue);\n // Sync clickedTags with the new value\n if (singleValue) {\n setClickedTags(new Set([tag.value]));\n } else {\n setClickedTags(findTagsInValue(newValue));\n }\n }\n // In controlled mode, only call onChange - let useEffect sync clickedTags when controlledValue updates\n onChange?.(newValue);\n onTagClick?.(tag);\n };\n\n return (\n <div className={cn('w-full flex flex-col gap-3', className)}>\n {title && (\n <label htmlFor={id} className=\"text-sm font-semibold\">\n {title}\n </label>\n )}\n <div className=\"relative w-full\">\n <Textarea\n id={id}\n name={name}\n placeholder={placeholder}\n value={value}\n onChange={handleInputChange}\n disabled={disabled}\n required={required}\n maxLength={maxLength}\n minLength={minLength}\n rows={rows}\n className={inputClassName}\n />\n </div>\n {availableTags.length > 0 && (\n <div className={cn('flex flex-wrap gap-2', tagsClassName)}>\n {availableTags.map((tag, index) => (\n <Button\n key={`${tag.value}-${index}`}\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => handleTagClick(tag)}\n disabled={disabled}\n className=\"rounded-full\"\n >\n <Plus size={16} className=\"text-muted-foreground\" />\n <span>{tag.label}</span>\n </Button>\n ))}\n </div>\n )}\n </div>\n );\n}\n","import { FontOption } from './types';\n\n// Cache keys for localStorage\nconst FONT_CACHE_KEY = 'brainfish-google-fonts-cache';\nconst FONT_CACHE_TIMESTAMP_KEY = 'brainfish-google-fonts-cache-timestamp';\nconst FONT_CACHE_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours\n\n// Cache for loaded font stylesheets to prevent duplicate loads\nconst loadedFonts = new Set<string>();\n\n/**\n * Get cached fonts from localStorage\n */\nexport const getCachedFonts = (): FontOption[] | null => {\n try {\n const timestamp = localStorage.getItem(FONT_CACHE_TIMESTAMP_KEY);\n if (!timestamp) return null;\n\n const cacheAge = Date.now() - parseInt(timestamp, 10);\n if (cacheAge > FONT_CACHE_DURATION_MS) {\n localStorage.removeItem(FONT_CACHE_KEY);\n localStorage.removeItem(FONT_CACHE_TIMESTAMP_KEY);\n\n return null;\n }\n\n const cached = localStorage.getItem(FONT_CACHE_KEY);\n if (!cached) return null;\n\n return JSON.parse(cached) as FontOption[];\n } catch {\n return null;\n }\n};\n\n/**\n * Save fonts to localStorage cache\n */\nexport const setCachedFonts = (fonts: FontOption[]): void => {\n try {\n localStorage.setItem(FONT_CACHE_KEY, JSON.stringify(fonts));\n localStorage.setItem(FONT_CACHE_TIMESTAMP_KEY, Date.now().toString());\n } catch {\n // localStorage might be full or unavailable, ignore\n }\n};\n\n/**\n * Load a Google Font stylesheet dynamically\n */\nexport const loadFont = (fontFamily: string): void => {\n if (loadedFonts.has(fontFamily)) return;\n\n const link = document.createElement('link');\n link.href = `https://fonts.googleapis.com/css2?family=${fontFamily.replace(/ /g, '+')}&display=swap`;\n link.rel = 'stylesheet';\n document.head.appendChild(link);\n loadedFonts.add(fontFamily);\n};\n\n/**\n * Clear the font cache from localStorage\n * Useful for forcing a fresh fetch of fonts\n */\nexport const clearFontCache = (): void => {\n try {\n localStorage.removeItem(FONT_CACHE_KEY);\n localStorage.removeItem(FONT_CACHE_TIMESTAMP_KEY);\n } catch {\n // localStorage might be unavailable, ignore\n }\n};\n","import * as React from 'react';\n\nimport { FontOption } from './types';\nimport { getCachedFonts, setCachedFonts, loadFont } from './font-utils';\n\nexport interface UseFontOptionsProps {\n apiKey: string;\n maxFonts?: number;\n onError?: (error: Error) => void;\n}\n\nexport interface UseFontOptionsReturn {\n fonts: FontOption[];\n isLoading: boolean;\n loadFont: typeof loadFont;\n}\n\n/**\n * Hook to fetch and manage Google Fonts options\n * Handles caching, loading state, and font stylesheet loading\n */\nexport function useFontOptions({ apiKey, maxFonts = 500, onError }: UseFontOptionsProps): UseFontOptionsReturn {\n const [fonts, setFonts] = React.useState<FontOption[]>([]);\n const [isLoading, setIsLoading] = React.useState(true);\n\n // Stabilize onError callback reference to prevent effect re-runs when\n // consumers pass inline functions (which create new references on every render)\n const onErrorRef = React.useRef(onError);\n React.useEffect(() => {\n onErrorRef.current = onError;\n }, [onError]);\n\n React.useEffect(() => {\n const fetchFonts = async () => {\n // Check cache first\n const cachedFonts = getCachedFonts();\n if (cachedFonts && cachedFonts.length > 0) {\n setFonts(cachedFonts.slice(0, maxFonts));\n setIsLoading(false);\n\n return;\n }\n\n try {\n const response = await fetch(`https://www.googleapis.com/webfonts/v1/webfonts?key=${apiKey}&sort=popularity`);\n\n if (!response.ok) {\n throw new Error(`Failed to fetch fonts: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n const fontOptions: FontOption[] = data.items.map((fontItem: { family: string; category: string; variants: string[] }) => ({\n value: fontItem.family,\n label: fontItem.family,\n category: fontItem.category,\n variants: fontItem.variants,\n }));\n\n // Cache all fonts, but only display up to maxFonts\n setCachedFonts(fontOptions);\n setFonts(fontOptions.slice(0, maxFonts));\n } catch (err) {\n const error = err instanceof Error ? err : new Error('Failed to fetch fonts');\n // eslint-disable-next-line no-console\n console.error('FontPicker: Failed to fetch Google Fonts', error);\n onErrorRef.current?.(error);\n } finally {\n setIsLoading(false);\n }\n };\n\n void fetchFonts();\n }, [apiKey, maxFonts]);\n\n return { fonts, isLoading, loadFont };\n}\n","import * as React from 'react';\nimport { Check } from '@phosphor-icons/react';\n\nimport { FontOption } from './types';\nimport { loadFont } from './font-utils';\n\nimport { cn } from '@/lib/utils';\nimport { CommandItem } from '@/components/ui/command';\n\nexport interface FontPickerItemProps {\n font: FontOption;\n isSelected: boolean;\n onSelect: () => void;\n}\n\n/**\n * Individual font item in the dropdown list\n * Loads font on mount for preview using Intersection Observer\n * Memoized to prevent unnecessary re-renders\n */\nexport const FontPickerItem = React.memo(function FontPickerItem({ font, isSelected, onSelect }: FontPickerItemProps) {\n const itemRef = React.useRef<HTMLDivElement>(null);\n const [isVisible, setIsVisible] = React.useState(false);\n\n // Use Intersection Observer to lazy-load fonts\n React.useEffect(() => {\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) {\n setIsVisible(true);\n observer.disconnect();\n }\n },\n { threshold: 0.1 },\n );\n\n if (itemRef.current) {\n observer.observe(itemRef.current);\n }\n\n return () => observer.disconnect();\n }, []);\n\n // Load font when visible\n React.useEffect(() => {\n if (isVisible) {\n loadFont(font.value);\n }\n }, [isVisible, font.value]);\n\n // Build accessible label\n const ariaLabel = `Select ${font.label} font${font.category ? `, ${font.category} category` : ''}`;\n\n return (\n <CommandItem\n ref={itemRef}\n value={font.label}\n onSelect={onSelect}\n className=\"cursor-pointer\"\n aria-label={ariaLabel}\n aria-selected={isSelected}\n >\n <span style={{ fontFamily: isVisible ? font.value : undefined }} className=\"flex-1 truncate\">\n {font.label}\n </span>\n {font.category && <span className=\"text-xs text-muted-foreground capitalize\">{font.category}</span>}\n <Check className={cn('ml-2 h-4 w-4', isSelected ? 'opacity-100' : 'opacity-0')} />\n </CommandItem>\n );\n});\n","import * as React from 'react';\nimport { SpinnerGap } from '@phosphor-icons/react';\n\nimport { useFontOptions } from './use-font-options';\nimport { FontPickerItem } from './font-picker-item';\n\nimport { Combobox } from '@/components/combobox/combobox';\n\nexport interface FontPickerProps {\n /** Currently selected font family */\n value?: string;\n /** Callback when font selection changes */\n onChange?: (fontFamily: string) => void;\n /** Google Fonts API key (required) */\n apiKey: string;\n /** Placeholder text when no font is selected */\n placeholder?: string;\n /** Text shown in search input */\n searchPlaceholder?: string;\n /** Text shown when no fonts match the search */\n noResultsText?: string;\n /** Text shown while fonts are loading */\n loadingText?: string;\n /** Whether the picker is disabled */\n disabled?: boolean;\n /** Additional CSS classes for the trigger button */\n className?: string;\n /** Width of the dropdown (defaults to trigger width) */\n dropdownWidth?: number;\n /** Maximum number of fonts to display (for performance) */\n maxFonts?: number;\n /** Callback when an error occurs fetching fonts */\n onError?: (error: Error) => void;\n}\n\n/**\n * FontPicker - A searchable dropdown for selecting Google Fonts\n *\n * @example\n * ```tsx\n * const [font, setFont] = useState('Inter');\n *\n * <FontPicker\n * apiKey=\"YOUR_API_KEY\"\n * value={font}\n * onChange={setFont}\n * placeholder=\"Select a font\"\n * />\n * ```\n */\nexport function FontPicker({\n value,\n onChange,\n apiKey,\n placeholder = 'Select a font...',\n searchPlaceholder = 'Search fonts...',\n noResultsText = 'No fonts found.',\n loadingText = 'Loading fonts...',\n disabled = false,\n className,\n dropdownWidth,\n maxFonts = 500,\n onError,\n}: FontPickerProps) {\n const { fonts, isLoading, loadFont } = useFontOptions({ apiKey, maxFonts, onError });\n\n // Load font stylesheet when selected\n React.useEffect(() => {\n if (value) {\n loadFont(value);\n }\n }, [value, loadFont]);\n\n // fonts already have value/label which Combobox requires\n const options = fonts;\n\n return (\n <Combobox\n options={options}\n value={value}\n onChange={onChange}\n defaultValueLabel={placeholder}\n placeholder={searchPlaceholder}\n noResultsLabel={noResultsText}\n disabled={disabled || isLoading}\n className={className}\n dropdownWidth={dropdownWidth}\n renderTrigger={({ selectedItem }) => {\n if (isLoading) {\n return (\n <span className=\"flex items-center gap-2 text-muted-foreground\">\n <SpinnerGap className=\"h-4 w-4 animate-spin\" />\n {loadingText}\n </span>\n );\n }\n\n if (selectedItem) {\n return <span style={{ fontFamily: selectedItem.value }}>{selectedItem.label}</span>;\n }\n\n return <span className=\"text-muted-foreground\">{placeholder}</span>;\n }}\n renderItem={({ item, isSelected, onSelect }) => (\n <FontPickerItem font={item} isSelected={isSelected} onSelect={onSelect} />\n )}\n />\n );\n}\n\nexport default FontPicker;\n"],"names":["FontPickerItem"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../src/components/input-with-tags/input-with-tags.tsx"],"sourcesContent":["import * as React from 'react';\nimport { Plus } from '@phosphor-icons/react';\n\nimport { cn } from '@/lib/utils';\nimport { Textarea } from '@/components/ui/textarea';\nimport { Button } from '@/components/ui/button';\n\nexport interface Tag {\n label: string;\n value: string;\n}\n\nexport interface InputWithTagsProps {\n title: string;\n placeholder?: string;\n tags: Tag[];\n value?: string;\n defaultValue?: string;\n onChange?: (value: string) => void;\n onTagClick?: (tag: Tag) => void;\n singleValue?: boolean; // If true, clicking a tag replaces the value instead of appending\n className?: string;\n inputClassName?: string;\n tagsClassName?: string;\n disabled?: boolean;\n // Standard textarea props\n name?: string;\n id?: string;\n required?: boolean;\n maxLength?: number;\n minLength?: number;\n rows?: number;\n}\n\nexport function InputWithTags({\n title,\n placeholder,\n tags,\n value: controlledValue,\n defaultValue,\n onChange,\n onTagClick,\n singleValue = false,\n className,\n inputClassName,\n tagsClassName,\n disabled,\n name,\n id,\n required,\n maxLength,\n minLength,\n rows,\n}: InputWithTagsProps) {\n // Helper to find tags present in value (tags are appended as: `${value} ${tag.value}`)\n const findTagsInValue = React.useCallback(\n (val: string): Set<string> => {\n if (!val) return new Set();\n const trimmedVal = val.trim();\n const foundTags = new Set<string>();\n\n for (const tag of tags) {\n const tagValue = tag.value.trim();\n if (singleValue) {\n if (trimmedVal === tagValue) {\n foundTags.add(tag.value);\n }\n } else {\n // Check if tag appears as complete segment (tags are appended as: `${value} ${tag.value}`)\n // Need to check: exact match, at start, in middle, or at end\n if (\n trimmedVal === tagValue ||\n trimmedVal.startsWith(tagValue + ' ') ||\n trimmedVal.includes(' ' + tagValue + ' ') ||\n trimmedVal.endsWith(' ' + tagValue)\n ) {\n foundTags.add(tag.value);\n }\n }\n }\n\n return foundTags;\n },\n [tags, singleValue],\n );\n\n // Initialize clickedTags based on initial value\n const initialValue = controlledValue !== undefined ? controlledValue : defaultValue || '';\n const [internalValue, setInternalValue] = React.useState(defaultValue || '');\n const [clickedTags, setClickedTags] = React.useState<Set<string>>(() => findTagsInValue(initialValue));\n\n // Use controlled value if provided, otherwise use internal state\n const value = controlledValue !== undefined ? controlledValue : internalValue;\n\n // Update clickedTags when value or tags change\n React.useEffect(() => {\n const currentValue = controlledValue !== undefined ? controlledValue : internalValue;\n setClickedTags(findTagsInValue(currentValue));\n }, [controlledValue, internalValue, tags, singleValue, findTagsInValue]);\n\n // Filter out clicked tags\n const availableTags = tags.filter((tag) => !clickedTags.has(tag.value));\n\n const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n const newValue = e.target.value;\n if (controlledValue === undefined) {\n setInternalValue(newValue);\n // Sync clickedTags with the new value in uncontrolled mode\n setClickedTags(findTagsInValue(newValue));\n }\n onChange?.(newValue);\n };\n\n const handleTagClick = (tag: Tag) => {\n const newValue = singleValue ? tag.value : value ? `${value} ${tag.value}` : tag.value;\n if (controlledValue === undefined) {\n // In uncontrolled mode, update state and sync clickedTags immediately\n setInternalValue(newValue);\n // Sync clickedTags with the new value\n if (singleValue) {\n setClickedTags(new Set([tag.value]));\n } else {\n setClickedTags(findTagsInValue(newValue));\n }\n }\n // In controlled mode, only call onChange - let useEffect sync clickedTags when controlledValue updates\n onChange?.(newValue);\n onTagClick?.(tag);\n };\n\n return (\n <div className={cn('w-full flex flex-col gap-3', className)}>\n {title && (\n <label htmlFor={id} className=\"text-sm font-semibold\">\n {title}\n </label>\n )}\n <div className=\"relative w-full\">\n <Textarea\n id={id}\n name={name}\n placeholder={placeholder}\n value={value}\n onChange={handleInputChange}\n disabled={disabled}\n required={required}\n maxLength={maxLength}\n minLength={minLength}\n rows={rows}\n className={inputClassName}\n />\n </div>\n {availableTags.length > 0 && (\n <div className={cn('flex flex-wrap gap-2', tagsClassName)}>\n {availableTags.map((tag, index) => (\n <Button\n key={`${tag.value}-${index}`}\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => handleTagClick(tag)}\n disabled={disabled}\n className=\"rounded-full\"\n >\n <Plus size={16} className=\"text-muted-foreground\" />\n <span>{tag.label}</span>\n </Button>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as React_2 from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Clear the font cache from localStorage
|
|
5
|
+
* Useful for forcing a fresh fetch of fonts
|
|
6
|
+
*/
|
|
7
|
+
export declare const clearFontCache: () => void;
|
|
8
|
+
|
|
9
|
+
export declare interface FontOption {
|
|
10
|
+
value: string;
|
|
11
|
+
label: string;
|
|
12
|
+
category?: string;
|
|
13
|
+
variants?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* FontPicker - A searchable dropdown for selecting Google Fonts
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* const [font, setFont] = useState('Inter');
|
|
22
|
+
*
|
|
23
|
+
* <FontPicker
|
|
24
|
+
* apiKey="YOUR_API_KEY"
|
|
25
|
+
* value={font}
|
|
26
|
+
* onChange={setFont}
|
|
27
|
+
* placeholder="Select a font"
|
|
28
|
+
* />
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function FontPicker({ value, onChange, apiKey, placeholder, searchPlaceholder, noResultsText, loadingText, disabled, className, dropdownWidth, maxFonts, onError, }: FontPickerProps): React_2.JSX.Element;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Individual font item in the dropdown list
|
|
35
|
+
* Loads font on mount for preview using Intersection Observer
|
|
36
|
+
* Memoized to prevent unnecessary re-renders
|
|
37
|
+
*/
|
|
38
|
+
export declare const FontPickerItem: React_2.NamedExoticComponent<FontPickerItemProps>;
|
|
39
|
+
|
|
40
|
+
export declare interface FontPickerItemProps {
|
|
41
|
+
font: FontOption;
|
|
42
|
+
isSelected: boolean;
|
|
43
|
+
onSelect: () => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export declare interface FontPickerProps {
|
|
47
|
+
/** Currently selected font family */
|
|
48
|
+
value?: string;
|
|
49
|
+
/** Callback when font selection changes */
|
|
50
|
+
onChange?: (fontFamily: string) => void;
|
|
51
|
+
/** Google Fonts API key (required) */
|
|
52
|
+
apiKey: string;
|
|
53
|
+
/** Placeholder text when no font is selected */
|
|
54
|
+
placeholder?: string;
|
|
55
|
+
/** Text shown in search input */
|
|
56
|
+
searchPlaceholder?: string;
|
|
57
|
+
/** Text shown when no fonts match the search */
|
|
58
|
+
noResultsText?: string;
|
|
59
|
+
/** Text shown while fonts are loading */
|
|
60
|
+
loadingText?: string;
|
|
61
|
+
/** Whether the picker is disabled */
|
|
62
|
+
disabled?: boolean;
|
|
63
|
+
/** Additional CSS classes for the trigger button */
|
|
64
|
+
className?: string;
|
|
65
|
+
/** Width of the dropdown (defaults to trigger width) */
|
|
66
|
+
dropdownWidth?: number;
|
|
67
|
+
/** Maximum number of fonts to display (for performance) */
|
|
68
|
+
maxFonts?: number;
|
|
69
|
+
/** Callback when an error occurs fetching fonts */
|
|
70
|
+
onError?: (error: Error) => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get cached fonts from localStorage
|
|
75
|
+
*/
|
|
76
|
+
export declare const getCachedFonts: () => FontOption[] | null;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Load a Google Font stylesheet dynamically
|
|
80
|
+
*/
|
|
81
|
+
export declare const loadFont: (fontFamily: string) => void;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Save fonts to localStorage cache
|
|
85
|
+
*/
|
|
86
|
+
export declare const setCachedFonts: (fonts: FontOption[]) => void;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Hook to fetch and manage Google Fonts options
|
|
90
|
+
* Handles caching, loading state, and font stylesheet loading
|
|
91
|
+
*/
|
|
92
|
+
export declare function useFontOptions({ apiKey, maxFonts, onError }: UseFontOptionsProps): UseFontOptionsReturn;
|
|
93
|
+
|
|
94
|
+
export declare interface UseFontOptionsProps {
|
|
95
|
+
apiKey: string;
|
|
96
|
+
maxFonts?: number;
|
|
97
|
+
onError?: (error: Error) => void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export declare interface UseFontOptionsReturn {
|
|
101
|
+
fonts: FontOption[];
|
|
102
|
+
isLoading: boolean;
|
|
103
|
+
loadFont: typeof loadFont;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export { }
|