diff --git a/app/ycode/components/BorderControls.tsx b/app/ycode/components/BorderControls.tsx index e956190d..af6f4994 100644 --- a/app/ycode/components/BorderControls.tsx +++ b/app/ycode/components/BorderControls.tsx @@ -105,6 +105,11 @@ const BorderControls = memo(function BorderControls({ layer, onLayerUpdate, acti const divideColor = getDesignProperty('borders', 'divideColor') || ''; const hasDivider = !!(divideX || divideY || divideColor || designBorders?.divideX || designBorders?.divideY || designBorders?.divideColor); + const outlineWidth = getDesignProperty('borders', 'outlineWidth') || ''; + const outlineColor = getDesignProperty('borders', 'outlineColor') || ''; + const outlineOffset = getDesignProperty('borders', 'outlineOffset') || ''; + const hasOutline = !!(outlineWidth || outlineColor || designBorders?.outlineWidth || designBorders?.outlineColor); + // Local controlled inputs (prevents repopulation bug) const inputs = useControlledInputs({ borderRadius, @@ -119,6 +124,8 @@ const BorderControls = memo(function BorderControls({ layer, onLayerUpdate, acti borderLeftWidth, divideX, divideY, + outlineWidth, + outlineOffset, }, extractMeasurementValue); const [borderRadiusInput, setBorderRadiusInput] = inputs.borderRadius; @@ -133,6 +140,8 @@ const BorderControls = memo(function BorderControls({ layer, onLayerUpdate, acti const [borderLeftWidthInput, setBorderLeftWidthInput] = inputs.borderLeftWidth; const [divideXInput, setDivideXInput] = inputs.divideX; const [divideYInput, setDivideYInput] = inputs.divideY; + const [outlineWidthInput, setOutlineWidthInput] = inputs.outlineWidth; + const [outlineOffsetInput, setOutlineOffsetInput] = inputs.outlineOffset; // Use mode toggle hooks for radius and width const radiusModeToggle = useModeToggle({ @@ -297,6 +306,43 @@ const BorderControls = memo(function BorderControls({ layer, onLayerUpdate, acti ]); }; + const handleOutlineWidthChange = (value: string) => { + setOutlineWidthInput(value); + const sanitized = removeSpaces(value); + debouncedUpdateDesignProperty('borders', 'outlineWidth', sanitized || null); + }; + + const handleOutlineColorChange = (value: string) => { + const sanitized = removeSpaces(value); + debouncedUpdateDesignProperty('borders', 'outlineColor', sanitized || null); + }; + + const handleOutlineColorImmediate = (value: string) => { + const sanitized = removeSpaces(value); + updateDesignProperty('borders', 'outlineColor', sanitized || null); + }; + + const handleOutlineOffsetChange = (value: string) => { + setOutlineOffsetInput(value); + const sanitized = removeSpaces(value); + debouncedUpdateDesignProperty('borders', 'outlineOffset', sanitized || null); + }; + + const handleAddOutline = () => { + updateDesignProperties([ + { category: 'borders', property: 'outlineWidth', value: '1px' }, + { category: 'borders', property: 'outlineColor', value: '#000000' }, + ]); + }; + + const handleRemoveOutline = () => { + updateDesignProperties([ + { category: 'borders', property: 'outlineWidth', value: null }, + { category: 'borders', property: 'outlineColor', value: null }, + { category: 'borders', property: 'outlineOffset', value: null }, + ]); + }; + return (
@@ -314,6 +360,12 @@ const BorderControls = memo(function BorderControls({ layer, onLayerUpdate, acti > Dividers + + Outline +
@@ -680,6 +732,85 @@ const BorderControls = memo(function BorderControls({ layer, onLayerUpdate, acti
)} + {hasOutline && ( +
+ +
+ + + + + +
+
+ +
+ handleOutlineWidthChange(e.target.value)} + placeholder="1" + /> +
+
+
+ +
+ +
+
+
+ +
+ handleOutlineOffsetChange(e.target.value)} + placeholder="0" + /> +
+
+
+
+
+ + + +
+
+ )} + diff --git a/app/ycode/components/ColorPropertyField.tsx b/app/ycode/components/ColorPropertyField.tsx index 5be86976..e51f7a0a 100644 --- a/app/ycode/components/ColorPropertyField.tsx +++ b/app/ycode/components/ColorPropertyField.tsx @@ -14,7 +14,7 @@ import type { Collection, CollectionField, CollectionFieldType, FieldVariable, D import type { FieldGroup, FieldSourceType } from '@/lib/collection-field-utils'; /** Design property names that can be bound to color fields */ -export type ColorDesignProperty = 'backgroundColor' | 'color' | 'borderColor' | 'divideColor' | 'textDecorationColor' | 'placeholderColor'; +export type ColorDesignProperty = 'backgroundColor' | 'color' | 'borderColor' | 'divideColor' | 'outlineColor' | 'textDecorationColor' | 'placeholderColor'; interface ColorPropertyFieldProps { value: string; diff --git a/lib/collection-usage-utils.ts b/lib/collection-usage-utils.ts index 40f0e471..8bf313f8 100644 --- a/lib/collection-usage-utils.ts +++ b/lib/collection-usage-utils.ts @@ -143,6 +143,7 @@ function layerFieldVarsContainId(layer: Layer, fieldId: string): boolean { if (designColorUsesField(vars.design.color, fieldId)) return true; if (designColorUsesField(vars.design.borderColor, fieldId)) return true; if (designColorUsesField(vars.design.divideColor, fieldId)) return true; + if (designColorUsesField(vars.design.outlineColor, fieldId)) return true; if (designColorUsesField(vars.design.textDecorationColor, fieldId)) return true; } diff --git a/lib/layer-utils.ts b/lib/layer-utils.ts index 2bb63945..033d9ff2 100644 --- a/lib/layer-utils.ts +++ b/lib/layer-utils.ts @@ -2714,7 +2714,7 @@ function resetLayerVariableBindings(variables: LayerVariables | undefined, ctx: // --- Design color bindings --- if (updated.design) { - const designKeys = ['backgroundColor', 'color', 'borderColor', 'divideColor', 'textDecorationColor'] as const; + const designKeys = ['backgroundColor', 'color', 'borderColor', 'divideColor', 'outlineColor', 'textDecorationColor'] as const; let designChanged = false; const newDesign = { ...updated.design }; @@ -3144,7 +3144,7 @@ function stripPageSourceFromVariables(variables: LayerVariables): LayerVariables // Design color bindings if (updated.design) { - const designKeys = ['backgroundColor', 'color', 'borderColor', 'divideColor', 'textDecorationColor'] as const; + const designKeys = ['backgroundColor', 'color', 'borderColor', 'divideColor', 'outlineColor', 'textDecorationColor'] as const; let designChanged = false; const newDesign = { ...updated.design }; for (const key of designKeys) { @@ -3446,7 +3446,7 @@ function resetVariablesForCollectionLayer(variables: LayerVariables, collectionL // --- Design color bindings --- if (updated.design) { - const designKeys = ['backgroundColor', 'color', 'borderColor', 'divideColor', 'textDecorationColor'] as const; + const designKeys = ['backgroundColor', 'color', 'borderColor', 'divideColor', 'outlineColor', 'textDecorationColor'] as const; let designChanged = false; const newDesign = { ...updated.design }; @@ -3752,7 +3752,7 @@ function resetVariablesForDeletedField(variables: LayerVariables, deletedFieldId // --- Design color bindings --- if (updated.design) { - const designKeys = ['backgroundColor', 'color', 'borderColor', 'divideColor', 'textDecorationColor'] as const; + const designKeys = ['backgroundColor', 'color', 'borderColor', 'divideColor', 'outlineColor', 'textDecorationColor'] as const; let designChanged = false; const newDesign = { ...updated.design }; diff --git a/lib/resolve-components.ts b/lib/resolve-components.ts index 07fc7d76..33f899fa 100644 --- a/lib/resolve-components.ts +++ b/lib/resolve-components.ts @@ -172,7 +172,7 @@ function remapVariableCollectionLayerIds(vars: LayerVariables, idMap: Map = { divideStyle: /^divide-(solid|dashed|dotted|double|none)$/, divideColor: /^divide-((\w+)(-\d+)?|\[(?:#|rgb|color:var).+\])(\/\d+)?$/, + // Outline + outlineWidth: /^outline(-\d+|-\[(?!#|rgb|color:var).+\])?$/, + outlineColor: /^outline-((\w+)(-\d+)?|\[(?:#|rgb|color:var).+\])(\/\d+)?$/, + outlineOffset: /^outline-offset-(\d+|-?\[.+\])$/, + // Effects opacity: /^opacity-(\d+|\[.+\])$/, boxShadow: /^shadow(-none|-sm|-md|-lg|-xl|-2xl|-inner|-\[.+\])?$/, @@ -781,6 +786,25 @@ export function propertyToClass( return `divide-[${value}]`; } return `divide-${value}`; + case 'outlineWidth': + return formatMeasurementClass(value, 'outline'); + case 'outlineColor': + if (value.startsWith('color:var(')) { + return `outline-[${value}]`; + } + if (value.startsWith('var(')) { + return `outline-[color:${value}]`; + } + if (value.match(/^#|^rgb/)) { + const parts = value.split('/'); + if (parts.length === 2) { + return `outline-[${parts[0]}]/${parts[1]}`; + } + return `outline-[${value}]`; + } + return `outline-${value}`; + case 'outlineOffset': + return formatMeasurementClass(value, 'outline-offset'); } } @@ -1490,6 +1514,26 @@ export function classesToDesign(classes: string | string[]): Layer['design'] { if (value) design.borders!.divideColor = value; } + // Outline Width + if (cls.startsWith('outline-[') && !cls.includes('#') && !cls.includes('rgb') && !cls.includes('color:var')) { + const value = extractArbitraryValue(cls); + if (value) design.borders!.outlineWidth = value; + } else if (cls.match(/^outline-\d+$/)) { + design.borders!.outlineWidth = cls.replace('outline-', '') + 'px'; + } + + // Outline Color + if (cls.startsWith('outline-[#') || cls.startsWith('outline-[rgb') || cls.startsWith('outline-[color:var(')) { + const value = extractArbitraryValueWithOpacity(cls); + if (value) design.borders!.outlineColor = value; + } + + // Outline Offset + if (cls.startsWith('outline-offset-')) { + const value = extractArbitraryValue(cls) || cls.replace('outline-offset-', '') + 'px'; + if (value) design.borders!.outlineOffset = value; + } + // ===== BACKGROUNDS ===== // Background Color if (cls.startsWith('bg-[#') || cls.startsWith('bg-[rgb') || cls.startsWith('bg-[color:var(')) { diff --git a/lib/variable-utils.ts b/lib/variable-utils.ts index 9df6d7d1..1a477099 100644 --- a/lib/variable-utils.ts +++ b/lib/variable-utils.ts @@ -457,6 +457,7 @@ export const DESIGN_COLOR_CSS_MAP: Record = { color: 'color', borderColor: 'borderColor', divideColor: '--tw-divide-color', + outlineColor: 'outlineColor', textDecorationColor: 'textDecorationColor', }; diff --git a/types/index.ts b/types/index.ts index 97b5469d..4012fb7f 100644 --- a/types/index.ts +++ b/types/index.ts @@ -94,6 +94,9 @@ export interface BordersDesign { divideY?: string; divideStyle?: string; divideColor?: string; + outlineWidth?: string; + outlineColor?: string; + outlineOffset?: string; } export interface BackgroundsDesign { @@ -433,6 +436,7 @@ export interface LayerVariables { color?: DesignColorVariable; // text color borderColor?: DesignColorVariable; divideColor?: DesignColorVariable; + outlineColor?: DesignColorVariable; textDecorationColor?: DesignColorVariable; placeholderColor?: DesignColorVariable; };