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;
};