Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/plugin/plugin-src/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const defaultPluginSettings: PluginSettings = {
useTailwind4: false,
thresholdPercent: 15,
baseFontFamily: "",
fontFamilyCustomConfig: {},
};

// A helper type guard to ensure the key belongs to the PluginSettings type
Expand Down
32 changes: 22 additions & 10 deletions packages/backend/src/tailwind/tailwindTextBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,17 +115,29 @@ export class TailwindTextBuilder extends TailwindDefaultBuilder {
if (baseFontFamily && fontName.family.toLowerCase() === baseFontFamily.toLowerCase()) {
return "";
}

// Check if the font is in one of the Tailwind default font stacks
if (config.fontFamily.sans.includes(fontName.family)) {
return "font-sans";
}
if (config.fontFamily.serif.includes(fontName.family)) {
return "font-serif";
}
if (config.fontFamily.mono.includes(fontName.family)) {
return "font-mono";

const fontFamilyCustomConfig = localTailwindSettings.fontFamilyCustomConfig;

if (fontFamilyCustomConfig) {
// Check if current font is part of custom tailwind config
for (const family in fontFamilyCustomConfig) {
if (fontFamilyCustomConfig[family].includes(fontName.family)) {
return `font-${family}`
}
}
} else {
// Check if the font is in one of the Tailwind default font stacks
if (config.fontFamily.sans.includes(fontName.family)) {
return "font-sans";
}
if (config.fontFamily.serif.includes(fontName.family)) {
return "font-serif";
}
if (config.fontFamily.mono.includes(fontName.family)) {
return "font-mono";
}
}

const underscoreFontName = fontName.family.replace(/\s/g, "_");

return "font-['" + underscoreFontName + "']";
Expand Down
124 changes: 104 additions & 20 deletions packages/plugin-ui/src/components/CustomPrefixInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface FormFieldProps {
helpText?: string;

// Validation props
type?: "text" | "number";
type?: "text" | "number"| "json";
min?: number;
max?: number;
suffix?: string;
Expand Down Expand Up @@ -50,6 +50,7 @@ const FormField = React.memo(
const [hasError, setHasError] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);

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

if (type === "json") {
// Check if the string is empty skip validation
if (!value.trim()) {
setHasError(false);
setErrorMessage("");
return true;
}

try {
// Try to parse the JSON
const config = JSON.parse(value);

// Validate that the config is an object
if (typeof config !== 'object' || Array.isArray(config) || config === null) {
throw new Error("Configuration must be a valid JSON object");
}

for (const item in config) {
if (!Array.isArray(config[item])) {
throw new Error(`Key ${item} is not valid and should be an array`);
}
config[item].forEach((val) => {
if (typeof val !== 'string') {
throw new Error(`Values from Key ${item} should be string`);
}
});
}

// Additional validation could be added here based on expected structure
// For example, checking specific properties or types

// If valid, update the preference
setHasError(false);
setErrorMessage("");
return true
} catch (error) {
// Handle parsing errors
console.error("Invalid JSON configuration:", error);
setHasError(true);
setErrorMessage(`Invalid JSON configuration: ${error}`)
// You could show an error message to the user here
// Or reset to default/previous value
return false
}
}

return true;
};

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

const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value;
setInputValue(newValue);
validateInput(newValue);
setHasChanges(newValue !== String(initialValue));
};

const applyChanges = () => {
if (hasError) return;

Expand Down Expand Up @@ -147,6 +201,15 @@ const FormField = React.memo(
}
};

const handleTextareaKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// Only apply changes on Ctrl+Enter or Command+Enter for textarea
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
e.preventDefault();
applyChanges();
textareaRef.current?.blur();
}
};

// Default preview transform for text prefixes
const defaultPreviewTransform = (value: string, example: string) => (
<div className="flex items-center gap-1.5">
Expand Down Expand Up @@ -190,25 +253,46 @@ const FormField = React.memo(
<div className="flex w-full items-start gap-2">
<div className="flex-1 flex flex-col">
<div className="flex items-center">
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={handleChange}
onFocus={() => setIsFocused(true)}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
placeholder={placeholder}
className={`p-1.5 px-2.5 text-sm w-full transition-all focus:outline-hidden ${
suffix ? "rounded-l-md" : "rounded-md"
} ${
hasError
? "border border-red-300 dark:border-red-700 bg-red-50 dark:bg-red-900/20"
: isFocused
? "border border-green-400 dark:border-green-600 ring-1 ring-green-300 dark:ring-green-800 bg-white dark:bg-neutral-800"
: "border border-gray-300 dark:border-gray-600 bg-white dark:bg-neutral-800 hover:border-gray-400 dark:hover:border-gray-500"
}`}
/>
{type === "json" ? (
<textarea
ref={textareaRef}
value={inputValue}
onChange={handleTextareaChange}
onFocus={() => setIsFocused(true)}
onBlur={handleBlur}
onKeyDown={handleTextareaKeyDown}
placeholder={placeholder}
rows={5}
className={`p-1.5 px-2.5 text-sm w-full transition-all focus:outline-hidden rounded-md font-mono resize-y
${
hasError
? "border border-red-300 dark:border-red-700 bg-red-50 dark:bg-red-900/20"
: isFocused
? "border border-green-400 dark:border-green-600 ring-1 ring-green-300 dark:ring-green-800 bg-white dark:bg-neutral-800"
: "border border-gray-300 dark:border-gray-600 bg-white dark:bg-neutral-800 hover:border-gray-400 dark:hover:border-gray-500"
}`}
/>
) : (
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={handleChange}
onFocus={() => setIsFocused(true)}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
placeholder={placeholder}
className={`p-1.5 px-2.5 text-sm w-full transition-all focus:outline-hidden ${
suffix ? "rounded-l-md" : "rounded-md"
} ${
hasError
? "border border-red-300 dark:border-red-700 bg-red-50 dark:bg-red-900/20"
: isFocused
? "border border-green-400 dark:border-green-600 ring-1 ring-green-300 dark:ring-green-800 bg-white dark:bg-neutral-800"
: "border border-gray-300 dark:border-gray-600 bg-white dark:bg-neutral-800 hover:border-gray-400 dark:hover:border-gray-500"
}`}
/>
)}

{suffix && (
<span
Expand Down
41 changes: 40 additions & 1 deletion packages/plugin-ui/src/components/TailwindSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ interface TailwindSettingsProps {
settings: PluginSettings | null;
onPreferenceChanged: (
key: keyof PluginSettings,
value: boolean | string | number,
value: boolean | string | number | Record<string, string[]>,
) => void;
}

Expand All @@ -27,6 +27,23 @@ export const TailwindSettings: React.FC<TailwindSettingsProps> = ({
const handleBaseFontFamilyChange = (newValue: string) => {
onPreferenceChanged("baseFontFamily", newValue);
};
const handleFontFamilyCustomConfigChange = (newValue: string) => {
try {
// Check if the string is empty, use default empty object
if (!newValue.trim()) {
onPreferenceChanged("fontFamilyCustomConfig", {});
return;
}

// parse the JSON
const config = JSON.parse(newValue);

onPreferenceChanged("fontFamilyCustomConfig", config);
} catch (error) {
// Handle parsing errors
console.error("Invalid JSON configuration:", error);
}
};

return (
<div className="mt-2">
Expand Down Expand Up @@ -108,6 +125,28 @@ export const TailwindSettings: React.FC<TailwindSettingsProps> = ({
{`Elements with this font won't have "font-[<value>]" class added`}
</p>
</div>
<div className="mb-3">
<FormField
type="json"
label="Font Family Custom Config"
initialValue={settings.fontFamilyCustomConfig ? JSON.stringify(settings.fontFamilyCustomConfig) : ''}
onValueChange={(d) => {
handleFontFamilyCustomConfigChange(String(d));
}}
placeholder="Your custom config"
helpText="Paste your tailwind custom font family config"
/>
<p className="text-xs text-neutral-500 mt-1">
{`This allow to override the custom font handling e.g. "font-comic"`}
<pre>
{`{
"sans":["Arial","verdana"],
"display":["Times","Roboto"],
"comic":["Comic Sans MS"]
}`}
</pre>
</p>
</div>
</div>
</div>
);
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface TailwindSettings extends HTMLSettings {
useTailwind4: boolean;
thresholdPercent: number;
baseFontFamily: string;
fontFamilyCustomConfig: Record<string, string[]>
}
export interface FlutterSettings {
flutterGenerationMode: "fullApp" | "stateless" | "snippet";
Expand Down