Skip to content
Merged
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
2 changes: 2 additions & 0 deletions apps/plugin/plugin-src/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions packages/backend/src/tailwind/conversionTables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const nearestValue = (goal: number, array: Array<number>): number => {
export const nearestValueWithThreshold = (
goal: number,
array: Array<number>,
thresholdPercent: number = 15,
thresholdPercent: number = localTailwindSettings.thresholdPercent,
): number | null => {
const nearest = nearestValue(goal, array);
const diff = Math.abs(nearest - goal);
Expand Down Expand Up @@ -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];
Expand All @@ -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];
Expand All @@ -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);
};

Expand All @@ -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);
};

Expand Down
10 changes: 10 additions & 0 deletions packages/backend/src/tailwind/tailwindTextBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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): {
Expand Down Expand Up @@ -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";
}
Expand Down
215 changes: 42 additions & 173 deletions packages/plugin-ui/src/components/TailwindSettings.tsx
Original file line number Diff line number Diff line change
@@ -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<InputGroupProps> = ({ label, children }) => (
<div className="mb-2">
<div className="flex items-center gap-1.5 mb-1.5">
<label className="text-xs font-medium text-gray-700 dark:text-gray-300">
{label}
</label>

{/* This is where the success message will appear, rendered by the child component */}
</div>
{children}
</div>
);

// Enhanced InputWithText component
interface InputWithTextProps {
value: string | number;
onChange: (value: number) => void;
placeholder?: string;
suffix?: string;
min?: number;
max?: number;
}

const InputWithText: React.FC<InputWithTextProps> = ({
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<HTMLInputElement>(null);

// Update internal state when value changes (from parent)
useEffect(() => {
setInputValue(String(value));
setHasChanges(false);
}, [value]);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
applyChanges();
inputRef.current?.blur();
}
};

return (
<div className="flex flex-col w-full">
{showSuccess && (
<span className="text-xs text-green-500 flex items-center gap-1 animate-fade-in-out ml-auto mb-1">
<Check className="w-3 h-3" /> Applied
</span>
)}

<div className="flex 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 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
className="py-1.5 px-2.5 text-sm border border-l-0 border-gray-300 dark:border-gray-600
bg-gray-100 dark:bg-gray-700 rounded-r-md text-gray-700 dark:text-gray-300"
>
{suffix}
</span>
)}
</div>

{hasError && (
<p className="text-xs text-red-500 mt-1">{errorMessage}</p>
)}
</div>

{hasChanges && (
<button
onClick={applyChanges}
disabled={hasError}
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
hasError
? "bg-gray-200 text-gray-500 dark:bg-gray-800 dark:text-gray-600 cursor-not-allowed"
: "bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50"
}`}
>
Done
</button>
)}
</div>
</div>
);
};

interface TailwindSettingsProps {
settings: PluginSettings | null;
onPreferenceChanged: (
Expand All @@ -194,6 +21,12 @@ export const TailwindSettings: React.FC<TailwindSettingsProps> = ({
const handleBaseFontSizeChange = (value: number) => {
onPreferenceChanged("baseFontSize", value);
};
const handleThresholdPercentChange = (value: number) => {
onPreferenceChanged("thresholdPercent", value);
};
const handleBaseFontFamilyChange = (newValue: string) => {
onPreferenceChanged("baseFontFamily", newValue);
};

return (
<div className="mt-2">
Expand Down Expand Up @@ -239,6 +72,42 @@ export const TailwindSettings: React.FC<TailwindSettingsProps> = ({
Use this value to calculate rem values (default: 16px)
</p>
</div>

{/* Threshold percent setting */}
<div className="mb-3">
<FormField
label="Rounding Threshold"
initialValue={settings.thresholdPercent || 15}
onValueChange={(d) => {
handleThresholdPercentChange(d as any);
}}
placeholder="15"
suffix="%"
type="number"
min={0}
max={50}
/>
<p className="text-xs text-neutral-500 mt-1">
Maximum allowed difference when rounding values (default: 15%)
</p>
</div>

{/* Base font family setting */}
<div className="mb-3">
<FormField
label="Base Font Family"
initialValue={settings.baseFontFamily || ''}
onValueChange={(d) => {
handleBaseFontFamilyChange(String(d));
}}
placeholder="sans-serif"
helpText="Font family that won't be included in generated classes."
type="text"
/>
<p className="text-xs text-neutral-500 mt-1">
{`Elements with this font won't have "font-[<value>]" class added`}
</p>
</div>
</div>
</div>
);
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down