From 4f2e70076cbae217c95198876690b9e80f7501a6 Mon Sep 17 00:00:00 2001 From: Bruno Fantauzzi Date: Tue, 2 Sep 2025 01:12:39 -0300 Subject: [PATCH 01/21] fix wrong lineHeight convertion (#229) --- .../backend/src/tailwind/tailwindConfig.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/tailwind/tailwindConfig.ts b/packages/backend/src/tailwind/tailwindConfig.ts index 081412a3..cb08676d 100644 --- a/packages/backend/src/tailwind/tailwindConfig.ts +++ b/packages/backend/src/tailwind/tailwindConfig.ts @@ -78,16 +78,14 @@ const fontSize = { }; const lineHeight = { - 0.75: "3", - 1: "none", - 1.25: "tight", - 1.375: "snug", - 1.5: "normal", - 1.625: "relaxed", - 2: "loose", - 1.75: "7", - 2.25: "9", - 2.5: "10", + 0.75: "3", // 0.75rem + 1: "4", // 1rem + 1.25: "5", // 1.25rem + 1.5: "6", // 1.5rem + 1.75: "7", // 1.75rem + 2: "8", // 2rem + 2.25: "9", // 2.25rem + 2.5: "10", // 2.5rem }; const letterSpacing = { From f98c64402e381d8fb436ae2998f0291274b07020 Mon Sep 17 00:00:00 2001 From: Bruno Fantauzzi Date: Thu, 11 Sep 2025 13:39:30 -0300 Subject: [PATCH 02/21] Add thresholdPercent and baseFontFamily to PluginSettings; update related components for new settings (#230) --- apps/plugin/plugin-src/code.ts | 2 + .../backend/src/tailwind/conversionTables.ts | 14 +- .../src/tailwind/tailwindTextBuilder.ts | 10 + .../src/components/TailwindSettings.tsx | 215 ++++-------------- packages/types/src/types.ts | 2 + 5 files changed, 63 insertions(+), 180 deletions(-) diff --git a/apps/plugin/plugin-src/code.ts b/apps/plugin/plugin-src/code.ts index 9d4bb5b5..b9aa12e7 100644 --- a/apps/plugin/plugin-src/code.ts +++ b/apps/plugin/plugin-src/code.ts @@ -36,6 +36,8 @@ export const defaultPluginSettings: PluginSettings = { tailwindGenerationMode: "jsx", baseFontSize: 16, useTailwind4: false, + thresholdPercent: 15, + baseFontFamily: "", }; // A helper type guard to ensure the key belongs to the PluginSettings type diff --git a/packages/backend/src/tailwind/conversionTables.ts b/packages/backend/src/tailwind/conversionTables.ts index 81a428c5..4532d608 100644 --- a/packages/backend/src/tailwind/conversionTables.ts +++ b/packages/backend/src/tailwind/conversionTables.ts @@ -14,7 +14,7 @@ export const nearestValue = (goal: number, array: Array): number => { export const nearestValueWithThreshold = ( goal: number, array: Array, - thresholdPercent: number = 15, + thresholdPercent: number = localTailwindSettings.thresholdPercent, ): number | null => { const nearest = nearestValue(goal, array); const diff = Math.abs(nearest - goal); @@ -61,7 +61,7 @@ const pxToRemToTailwind = ( return conversionMap[convertedValue]; } else if (localTailwindSettings.roundTailwindValues) { // Only round if the nearest value is within acceptable threshold - const thresholdValue = nearestValueWithThreshold(remValue, keys, 15); + const thresholdValue = nearestValueWithThreshold(remValue, keys); if (thresholdValue !== null) { return conversionMap[thresholdValue]; @@ -82,7 +82,7 @@ const pxToTailwind = ( return conversionMap[convertedValue]; } else if (localTailwindSettings.roundTailwindValues) { // Only round if the nearest value is within acceptable threshold - const thresholdValue = nearestValueWithThreshold(value, keys, 15); + const thresholdValue = nearestValueWithThreshold(value, keys); if (thresholdValue !== null) { return conversionMap[thresholdValue]; @@ -106,8 +106,8 @@ export const pxToFontSize = (value: number): string => { export const pxToBorderRadius = (value: number): string => { const conversionMap = localTailwindSettings.useTailwind4 - ? config.borderRadiusV4 - : config.borderRadius; + ? config.borderRadiusV4 + : config.borderRadius; return pxToRemToTailwind(value, conversionMap); }; @@ -121,8 +121,8 @@ export const pxToOutline = (value: number): string | null => { export const pxToBlur = (value: number): string | null => { const conversionMap = localTailwindSettings.useTailwind4 - ? config.blurV4 - : config.blur; + ? config.blurV4 + : config.blur; return pxToTailwind(value, conversionMap); }; diff --git a/packages/backend/src/tailwind/tailwindTextBuilder.ts b/packages/backend/src/tailwind/tailwindTextBuilder.ts index 66ee2882..9b1873ab 100644 --- a/packages/backend/src/tailwind/tailwindTextBuilder.ts +++ b/packages/backend/src/tailwind/tailwindTextBuilder.ts @@ -12,6 +12,7 @@ import { import { TailwindDefaultBuilder } from "./tailwindDefaultBuilder"; import { config } from "./tailwindConfig"; import { StyledTextSegmentSubset } from "types"; +import { localTailwindSettings } from "./tailwindMain"; export class TailwindTextBuilder extends TailwindDefaultBuilder { getTextSegments(node: TextNode): { @@ -93,6 +94,15 @@ export class TailwindTextBuilder extends TailwindDefaultBuilder { }; fontFamily = (fontName: FontName): string => { + // Check if the font matches the base font family setting + const baseFontFamily = localTailwindSettings.baseFontFamily; + + // If the font matches exactly the base font, don't add a class + 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"; } diff --git a/packages/plugin-ui/src/components/TailwindSettings.tsx b/packages/plugin-ui/src/components/TailwindSettings.tsx index 3959cc7f..64d9f5cb 100644 --- a/packages/plugin-ui/src/components/TailwindSettings.tsx +++ b/packages/plugin-ui/src/components/TailwindSettings.tsx @@ -1,179 +1,6 @@ -import { ChevronDown, ChevronRight, HelpCircle, Check } from "lucide-react"; -import { useState, useRef, useEffect } from "react"; import { PluginSettings } from "types"; import FormField from "./CustomPrefixInput"; // Still importing from the same file -// Added InputGroup component -interface InputGroupProps { - label: string; - children: React.ReactNode; -} - -const InputGroup: React.FC = ({ label, children }) => ( -
-
- - - {/* This is where the success message will appear, rendered by the child component */} -
- {children} -
-); - -// Enhanced InputWithText component -interface InputWithTextProps { - value: string | number; - onChange: (value: number) => void; - placeholder?: string; - suffix?: string; - min?: number; - max?: number; -} - -const InputWithText: React.FC = ({ - value, - onChange, - placeholder, - suffix, - min = 1, - max = 100, -}) => { - const [inputValue, setInputValue] = useState(String(value)); - const [isFocused, setIsFocused] = useState(false); - const [hasChanges, setHasChanges] = useState(false); - const [showSuccess, setShowSuccess] = useState(false); - const [hasError, setHasError] = useState(false); - const [errorMessage, setErrorMessage] = useState(""); - const inputRef = useRef(null); - - // Update internal state when value changes (from parent) - useEffect(() => { - setInputValue(String(value)); - setHasChanges(false); - }, [value]); - - const handleChange = (e: React.ChangeEvent) => { - const newValue = e.target.value; - setInputValue(newValue); - - // Check for non-numeric characters - if (/[^0-9]/.test(newValue)) { - setHasError(true); - setErrorMessage("Only numbers are allowed"); - setHasChanges(newValue !== String(value)); - return; - } - - const numValue = parseInt(newValue, 10); - - if (isNaN(numValue)) { - setHasError(true); - setErrorMessage("Please enter a valid number"); - } else if (numValue < min) { - setHasError(true); - setErrorMessage(`Minimum value is ${min}`); - } else if (numValue > max) { - setHasError(true); - setErrorMessage(`Maximum value is ${max}`); - } else { - setHasError(false); - setErrorMessage(""); - } - - setHasChanges(newValue !== String(value)); - }; - - const applyChanges = () => { - if (hasError) return; - - const numValue = parseInt(inputValue, 10); - if (!isNaN(numValue) && numValue >= min && numValue <= max) { - onChange(numValue); - - // Show success indicator briefly - setShowSuccess(true); - setTimeout(() => setShowSuccess(false), 1500); - setHasChanges(false); - } - }; - - const handleBlur = () => { - setIsFocused(false); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - e.preventDefault(); - applyChanges(); - inputRef.current?.blur(); - } - }; - - return ( -
- {showSuccess && ( - - Applied - - )} - -
-
-
- setIsFocused(true)} - onBlur={handleBlur} - onKeyDown={handleKeyDown} - placeholder={placeholder} - className={`p-1.5 px-2.5 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 && ( - - {suffix} - - )} -
- - {hasError && ( -

{errorMessage}

- )} -
- - {hasChanges && ( - - )} -
-
- ); -}; - interface TailwindSettingsProps { settings: PluginSettings | null; onPreferenceChanged: ( @@ -194,6 +21,12 @@ export const TailwindSettings: React.FC = ({ const handleBaseFontSizeChange = (value: number) => { onPreferenceChanged("baseFontSize", value); }; + const handleThresholdPercentChange = (value: number) => { + onPreferenceChanged("thresholdPercent", value); + }; + const handleBaseFontFamilyChange = (newValue: string) => { + onPreferenceChanged("baseFontFamily", newValue); + }; return (
@@ -239,6 +72,42 @@ export const TailwindSettings: React.FC = ({ Use this value to calculate rem values (default: 16px)

+ + {/* Threshold percent setting */} +
+ { + handleThresholdPercentChange(d as any); + }} + placeholder="15" + suffix="%" + type="number" + min={0} + max={50} + /> +

+ Maximum allowed difference when rounding values (default: 15%) +

+
+ + {/* Base font family setting */} +
+ { + handleBaseFontFamilyChange(String(d)); + }} + placeholder="sans-serif" + helpText="Font family that won't be included in generated classes." + type="text" + /> +

+ {`Elements with this font won't have "font-[]" class added`} +

+
); diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index af158dde..eb76dbd7 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -17,6 +17,8 @@ export interface TailwindSettings extends HTMLSettings { embedVectors: boolean; baseFontSize: number; useTailwind4: boolean; + thresholdPercent: number; + baseFontFamily: string; } export interface FlutterSettings { flutterGenerationMode: "fullApp" | "stateless" | "snippet"; From 452478c9bad2821997d5f3b11495f285778fc165 Mon Sep 17 00:00:00 2001 From: paulehermancab Date: Mon, 20 Oct 2025 19:25:30 +0200 Subject: [PATCH 03/21] Add(textBuilder): support truncate (#235) --- .../backend/src/tailwind/tailwindTextBuilder.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/backend/src/tailwind/tailwindTextBuilder.ts b/packages/backend/src/tailwind/tailwindTextBuilder.ts index 9b1873ab..94c70380 100644 --- a/packages/backend/src/tailwind/tailwindTextBuilder.ts +++ b/packages/backend/src/tailwind/tailwindTextBuilder.ts @@ -55,6 +55,7 @@ export class TailwindTextBuilder extends TailwindDefaultBuilder { // textIndentStyle, blurStyle, shadowStyle, + this.truncateText(node), ] .filter(Boolean) .join(" "); @@ -68,6 +69,19 @@ export class TailwindTextBuilder extends TailwindDefaultBuilder { }); } + truncateText = ( + node: TextNode, + ) => { + if (node.textTruncation !== "DISABLED" && node.maxLines) { + if (node.maxLines > 0 && node.maxLines < 7) { + return `line-clamp-${node.maxLines}` + } else { + return `line-clamp-[${node.maxLines}]` + } + } + return ""; + }; + getTailwindColorFromFills = ( fills: ReadonlyArray | PluginAPI["mixed"], ) => { @@ -112,6 +126,9 @@ export class TailwindTextBuilder extends TailwindDefaultBuilder { if (config.fontFamily.mono.includes(fontName.family)) { return "font-mono"; } + if (config.fontFamily.display.includes(fontName.family)) { + return "font-display"; + } const underscoreFontName = fontName.family.replace(/\s/g, "_"); return "font-['" + underscoreFontName + "']"; From 427381b113c9a7ee4296415e27ab9903264f01b5 Mon Sep 17 00:00:00 2001 From: paulehermancab Date: Tue, 21 Oct 2025 15:28:22 +0200 Subject: [PATCH 04/21] Hotfix(textBuilder): remove custom font (#237) --- packages/backend/src/tailwind/tailwindTextBuilder.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/backend/src/tailwind/tailwindTextBuilder.ts b/packages/backend/src/tailwind/tailwindTextBuilder.ts index 94c70380..6c106d07 100644 --- a/packages/backend/src/tailwind/tailwindTextBuilder.ts +++ b/packages/backend/src/tailwind/tailwindTextBuilder.ts @@ -126,9 +126,6 @@ export class TailwindTextBuilder extends TailwindDefaultBuilder { if (config.fontFamily.mono.includes(fontName.family)) { return "font-mono"; } - if (config.fontFamily.display.includes(fontName.family)) { - return "font-display"; - } const underscoreFontName = fontName.family.replace(/\s/g, "_"); return "font-['" + underscoreFontName + "']"; From 6450a8cf5dd4248bf73ec3e2c1cd5121f834b2bb Mon Sep 17 00:00:00 2001 From: paulehermancab Date: Fri, 28 Nov 2025 23:17:55 +0100 Subject: [PATCH 05/21] [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 --- apps/plugin/plugin-src/code.ts | 1 + .../src/tailwind/tailwindTextBuilder.ts | 32 +++-- .../src/components/CustomPrefixInput.tsx | 124 +++++++++++++++--- .../src/components/TailwindSettings.tsx | 41 +++++- packages/types/src/types.ts | 1 + 5 files changed, 168 insertions(+), 31 deletions(-) diff --git a/apps/plugin/plugin-src/code.ts b/apps/plugin/plugin-src/code.ts index b9aa12e7..fafd2569 100644 --- a/apps/plugin/plugin-src/code.ts +++ b/apps/plugin/plugin-src/code.ts @@ -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 diff --git a/packages/backend/src/tailwind/tailwindTextBuilder.ts b/packages/backend/src/tailwind/tailwindTextBuilder.ts index 6c106d07..9b631d5c 100644 --- a/packages/backend/src/tailwind/tailwindTextBuilder.ts +++ b/packages/backend/src/tailwind/tailwindTextBuilder.ts @@ -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 + "']"; diff --git a/packages/plugin-ui/src/components/CustomPrefixInput.tsx b/packages/plugin-ui/src/components/CustomPrefixInput.tsx index d397a4dd..4aa1efd2 100644 --- a/packages/plugin-ui/src/components/CustomPrefixInput.tsx +++ b/packages/plugin-ui/src/components/CustomPrefixInput.tsx @@ -10,7 +10,7 @@ interface FormFieldProps { helpText?: string; // Validation props - type?: "text" | "number"; + type?: "text" | "number"| "json"; min?: number; max?: number; suffix?: string; @@ -50,6 +50,7 @@ const FormField = React.memo( const [hasError, setHasError] = useState(false); const [errorMessage, setErrorMessage] = useState(""); const inputRef = useRef(null); + const textareaRef = useRef(null); // Update internal state when initialValue changes (from parent) useEffect(() => { @@ -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; }; @@ -116,6 +163,13 @@ const FormField = React.memo( setHasChanges(newValue !== String(initialValue)); }; + const handleTextareaChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + setInputValue(newValue); + validateInput(newValue); + setHasChanges(newValue !== String(initialValue)); + }; + const applyChanges = () => { if (hasError) return; @@ -147,6 +201,15 @@ const FormField = React.memo( } }; + const handleTextareaKeyDown = (e: React.KeyboardEvent) => { + // 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) => (
@@ -190,25 +253,46 @@ const FormField = React.memo(
- 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" ? ( +