Skip to content

Commit 6450a8c

Browse files
[Feature] - Add support for a tailwind custom font family config (#238)
* Add(tailwind): support font family custom config * Improvement(tailwind): add textarea to handle custom font tailwind config
1 parent 427381b commit 6450a8c

File tree

5 files changed

+168
-31
lines changed

5 files changed

+168
-31
lines changed

apps/plugin/plugin-src/code.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const defaultPluginSettings: PluginSettings = {
3838
useTailwind4: false,
3939
thresholdPercent: 15,
4040
baseFontFamily: "",
41+
fontFamilyCustomConfig: {},
4142
};
4243

4344
// A helper type guard to ensure the key belongs to the PluginSettings type

packages/backend/src/tailwind/tailwindTextBuilder.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -115,17 +115,29 @@ export class TailwindTextBuilder extends TailwindDefaultBuilder {
115115
if (baseFontFamily && fontName.family.toLowerCase() === baseFontFamily.toLowerCase()) {
116116
return "";
117117
}
118-
119-
// Check if the font is in one of the Tailwind default font stacks
120-
if (config.fontFamily.sans.includes(fontName.family)) {
121-
return "font-sans";
122-
}
123-
if (config.fontFamily.serif.includes(fontName.family)) {
124-
return "font-serif";
125-
}
126-
if (config.fontFamily.mono.includes(fontName.family)) {
127-
return "font-mono";
118+
119+
const fontFamilyCustomConfig = localTailwindSettings.fontFamilyCustomConfig;
120+
121+
if (fontFamilyCustomConfig) {
122+
// Check if current font is part of custom tailwind config
123+
for (const family in fontFamilyCustomConfig) {
124+
if (fontFamilyCustomConfig[family].includes(fontName.family)) {
125+
return `font-${family}`
126+
}
127+
}
128+
} else {
129+
// Check if the font is in one of the Tailwind default font stacks
130+
if (config.fontFamily.sans.includes(fontName.family)) {
131+
return "font-sans";
132+
}
133+
if (config.fontFamily.serif.includes(fontName.family)) {
134+
return "font-serif";
135+
}
136+
if (config.fontFamily.mono.includes(fontName.family)) {
137+
return "font-mono";
138+
}
128139
}
140+
129141
const underscoreFontName = fontName.family.replace(/\s/g, "_");
130142

131143
return "font-['" + underscoreFontName + "']";

packages/plugin-ui/src/components/CustomPrefixInput.tsx

Lines changed: 104 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ interface FormFieldProps {
1010
helpText?: string;
1111

1212
// Validation props
13-
type?: "text" | "number";
13+
type?: "text" | "number"| "json";
1414
min?: number;
1515
max?: number;
1616
suffix?: string;
@@ -50,6 +50,7 @@ const FormField = React.memo(
5050
const [hasError, setHasError] = useState(false);
5151
const [errorMessage, setErrorMessage] = useState("");
5252
const inputRef = useRef<HTMLInputElement>(null);
53+
const textareaRef = useRef<HTMLTextAreaElement>(null);
5354

5455
// Update internal state when initialValue changes (from parent)
5556
useEffect(() => {
@@ -106,6 +107,52 @@ const FormField = React.memo(
106107
return true;
107108
}
108109

110+
if (type === "json") {
111+
// Check if the string is empty skip validation
112+
if (!value.trim()) {
113+
setHasError(false);
114+
setErrorMessage("");
115+
return true;
116+
}
117+
118+
try {
119+
// Try to parse the JSON
120+
const config = JSON.parse(value);
121+
122+
// Validate that the config is an object
123+
if (typeof config !== 'object' || Array.isArray(config) || config === null) {
124+
throw new Error("Configuration must be a valid JSON object");
125+
}
126+
127+
for (const item in config) {
128+
if (!Array.isArray(config[item])) {
129+
throw new Error(`Key ${item} is not valid and should be an array`);
130+
}
131+
config[item].forEach((val) => {
132+
if (typeof val !== 'string') {
133+
throw new Error(`Values from Key ${item} should be string`);
134+
}
135+
});
136+
}
137+
138+
// Additional validation could be added here based on expected structure
139+
// For example, checking specific properties or types
140+
141+
// If valid, update the preference
142+
setHasError(false);
143+
setErrorMessage("");
144+
return true
145+
} catch (error) {
146+
// Handle parsing errors
147+
console.error("Invalid JSON configuration:", error);
148+
setHasError(true);
149+
setErrorMessage(`Invalid JSON configuration: ${error}`)
150+
// You could show an error message to the user here
151+
// Or reset to default/previous value
152+
return false
153+
}
154+
}
155+
109156
return true;
110157
};
111158

@@ -116,6 +163,13 @@ const FormField = React.memo(
116163
setHasChanges(newValue !== String(initialValue));
117164
};
118165

166+
const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
167+
const newValue = e.target.value;
168+
setInputValue(newValue);
169+
validateInput(newValue);
170+
setHasChanges(newValue !== String(initialValue));
171+
};
172+
119173
const applyChanges = () => {
120174
if (hasError) return;
121175

@@ -147,6 +201,15 @@ const FormField = React.memo(
147201
}
148202
};
149203

204+
const handleTextareaKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
205+
// Only apply changes on Ctrl+Enter or Command+Enter for textarea
206+
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
207+
e.preventDefault();
208+
applyChanges();
209+
textareaRef.current?.blur();
210+
}
211+
};
212+
150213
// Default preview transform for text prefixes
151214
const defaultPreviewTransform = (value: string, example: string) => (
152215
<div className="flex items-center gap-1.5">
@@ -190,25 +253,46 @@ const FormField = React.memo(
190253
<div className="flex w-full items-start gap-2">
191254
<div className="flex-1 flex flex-col">
192255
<div className="flex items-center">
193-
<input
194-
ref={inputRef}
195-
type="text"
196-
value={inputValue}
197-
onChange={handleChange}
198-
onFocus={() => setIsFocused(true)}
199-
onBlur={handleBlur}
200-
onKeyDown={handleKeyDown}
201-
placeholder={placeholder}
202-
className={`p-1.5 px-2.5 text-sm w-full transition-all focus:outline-hidden ${
203-
suffix ? "rounded-l-md" : "rounded-md"
204-
} ${
205-
hasError
206-
? "border border-red-300 dark:border-red-700 bg-red-50 dark:bg-red-900/20"
207-
: isFocused
208-
? "border border-green-400 dark:border-green-600 ring-1 ring-green-300 dark:ring-green-800 bg-white dark:bg-neutral-800"
209-
: "border border-gray-300 dark:border-gray-600 bg-white dark:bg-neutral-800 hover:border-gray-400 dark:hover:border-gray-500"
210-
}`}
211-
/>
256+
{type === "json" ? (
257+
<textarea
258+
ref={textareaRef}
259+
value={inputValue}
260+
onChange={handleTextareaChange}
261+
onFocus={() => setIsFocused(true)}
262+
onBlur={handleBlur}
263+
onKeyDown={handleTextareaKeyDown}
264+
placeholder={placeholder}
265+
rows={5}
266+
className={`p-1.5 px-2.5 text-sm w-full transition-all focus:outline-hidden rounded-md font-mono resize-y
267+
${
268+
hasError
269+
? "border border-red-300 dark:border-red-700 bg-red-50 dark:bg-red-900/20"
270+
: isFocused
271+
? "border border-green-400 dark:border-green-600 ring-1 ring-green-300 dark:ring-green-800 bg-white dark:bg-neutral-800"
272+
: "border border-gray-300 dark:border-gray-600 bg-white dark:bg-neutral-800 hover:border-gray-400 dark:hover:border-gray-500"
273+
}`}
274+
/>
275+
) : (
276+
<input
277+
ref={inputRef}
278+
type="text"
279+
value={inputValue}
280+
onChange={handleChange}
281+
onFocus={() => setIsFocused(true)}
282+
onBlur={handleBlur}
283+
onKeyDown={handleKeyDown}
284+
placeholder={placeholder}
285+
className={`p-1.5 px-2.5 text-sm w-full transition-all focus:outline-hidden ${
286+
suffix ? "rounded-l-md" : "rounded-md"
287+
} ${
288+
hasError
289+
? "border border-red-300 dark:border-red-700 bg-red-50 dark:bg-red-900/20"
290+
: isFocused
291+
? "border border-green-400 dark:border-green-600 ring-1 ring-green-300 dark:ring-green-800 bg-white dark:bg-neutral-800"
292+
: "border border-gray-300 dark:border-gray-600 bg-white dark:bg-neutral-800 hover:border-gray-400 dark:hover:border-gray-500"
293+
}`}
294+
/>
295+
)}
212296

213297
{suffix && (
214298
<span

packages/plugin-ui/src/components/TailwindSettings.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ interface TailwindSettingsProps {
55
settings: PluginSettings | null;
66
onPreferenceChanged: (
77
key: keyof PluginSettings,
8-
value: boolean | string | number,
8+
value: boolean | string | number | Record<string, string[]>,
99
) => void;
1010
}
1111

@@ -27,6 +27,23 @@ export const TailwindSettings: React.FC<TailwindSettingsProps> = ({
2727
const handleBaseFontFamilyChange = (newValue: string) => {
2828
onPreferenceChanged("baseFontFamily", newValue);
2929
};
30+
const handleFontFamilyCustomConfigChange = (newValue: string) => {
31+
try {
32+
// Check if the string is empty, use default empty object
33+
if (!newValue.trim()) {
34+
onPreferenceChanged("fontFamilyCustomConfig", {});
35+
return;
36+
}
37+
38+
// parse the JSON
39+
const config = JSON.parse(newValue);
40+
41+
onPreferenceChanged("fontFamilyCustomConfig", config);
42+
} catch (error) {
43+
// Handle parsing errors
44+
console.error("Invalid JSON configuration:", error);
45+
}
46+
};
3047

3148
return (
3249
<div className="mt-2">
@@ -108,6 +125,28 @@ export const TailwindSettings: React.FC<TailwindSettingsProps> = ({
108125
{`Elements with this font won't have "font-[<value>]" class added`}
109126
</p>
110127
</div>
128+
<div className="mb-3">
129+
<FormField
130+
type="json"
131+
label="Font Family Custom Config"
132+
initialValue={settings.fontFamilyCustomConfig ? JSON.stringify(settings.fontFamilyCustomConfig) : ''}
133+
onValueChange={(d) => {
134+
handleFontFamilyCustomConfigChange(String(d));
135+
}}
136+
placeholder="Your custom config"
137+
helpText="Paste your tailwind custom font family config"
138+
/>
139+
<p className="text-xs text-neutral-500 mt-1">
140+
{`This allow to override the custom font handling e.g. "font-comic"`}
141+
<pre>
142+
{`{
143+
"sans":["Arial","verdana"],
144+
"display":["Times","Roboto"],
145+
"comic":["Comic Sans MS"]
146+
}`}
147+
</pre>
148+
</p>
149+
</div>
111150
</div>
112151
</div>
113152
);

packages/types/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface TailwindSettings extends HTMLSettings {
1919
useTailwind4: boolean;
2020
thresholdPercent: number;
2121
baseFontFamily: string;
22+
fontFamilyCustomConfig: Record<string, string[]>
2223
}
2324
export interface FlutterSettings {
2425
flutterGenerationMode: "fullApp" | "stateless" | "snippet";

0 commit comments

Comments
 (0)