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
131 changes: 131 additions & 0 deletions app/ycode/components/BorderControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -119,6 +124,8 @@ const BorderControls = memo(function BorderControls({ layer, onLayerUpdate, acti
borderLeftWidth,
divideX,
divideY,
outlineWidth,
outlineOffset,
}, extractMeasurementValue);

const [borderRadiusInput, setBorderRadiusInput] = inputs.borderRadius;
Expand All @@ -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({
Expand Down Expand Up @@ -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 (
<div className="py-5">
<header className="py-4 -mt-4 flex items-center justify-between">
Expand All @@ -314,6 +360,12 @@ const BorderControls = memo(function BorderControls({ layer, onLayerUpdate, acti
>
Dividers
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleAddOutline}
disabled={hasOutline}
>
Outline
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</header>
Expand Down Expand Up @@ -680,6 +732,85 @@ const BorderControls = memo(function BorderControls({ layer, onLayerUpdate, acti
</div>
)}

{hasOutline && (
<div className="grid grid-cols-3 items-start">
<Label variant="muted" className="h-8">Outline</Label>
<div className="col-span-2 flex items-center gap-2">
<Popover>
<PopoverTrigger asChild>
<Button
variant="input"
size="sm"
className="justify-start flex-1"
>
<div className="flex items-center gap-2">
<div className="size-5 rounded-[6px] shrink-0 -ml-1 relative overflow-hidden outline dark:outline-white/10 outline-offset-[-1px]">
<div className="absolute inset-0 z-20" style={{ background: parseBorderColorToCss(outlineColor, colorVariables) }} />
<div className="absolute inset-0 opacity-15 bg-checkerboard bg-background z-10" />
</div>
<Label variant="muted" className="cursor-pointer">{outlineWidth || '1px'}</Label>
</div>
</Button>
</PopoverTrigger>
<PopoverContent className="w-64 mr-4">
<div className="flex flex-col gap-2">
<div className="grid grid-cols-3">
<Label variant="muted">Width</Label>
<div className="col-span-2">
<Input
stepper
min="0"
step="1"
value={outlineWidthInput}
onChange={(e) => handleOutlineWidthChange(e.target.value)}
placeholder="1"
/>
</div>
</div>
<div className="grid grid-cols-3">
<Label variant="muted">Color</Label>
<div className="col-span-2 *:w-full">
<ColorPropertyField
solidOnly
value={outlineColor || '#000000'}
onChange={handleOutlineColorChange}
onImmediateChange={handleOutlineColorImmediate}
layer={layer}
onLayerUpdate={onLayerUpdate}
designProperty="outlineColor"
fieldGroups={fieldGroups}
allFields={allFields}
collections={collections}
/>
</div>
</div>
<div className="grid grid-cols-3">
<Label variant="muted">Offset</Label>
<div className="col-span-2">
<Input
stepper
step="1"
value={outlineOffsetInput}
onChange={(e) => handleOutlineOffsetChange(e.target.value)}
placeholder="0"
/>
</div>
</div>
</div>
</PopoverContent>
</Popover>
<span
role="button"
tabIndex={0}
className="p-0.5 rounded-sm opacity-70 hover:opacity-100 transition-opacity cursor-pointer"
onClick={handleRemoveOutline}
>
<Icon name="x" className="size-2.5" />
</span>
</div>
</div>
)}

</div>

</div>
Expand Down
2 changes: 1 addition & 1 deletion app/ycode/components/ColorPropertyField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions lib/collection-usage-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
8 changes: 4 additions & 4 deletions lib/layer-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 };

Expand Down Expand Up @@ -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 };

Expand Down
2 changes: 1 addition & 1 deletion lib/resolve-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ function remapVariableCollectionLayerIds(vars: LayerVariables, idMap: Map<string
if (vars.design) {
let designChanged = false;
const newDesign = { ...vars.design };
for (const key of ['backgroundColor', 'color', 'borderColor', 'divideColor', 'textDecorationColor'] as const) {
for (const key of ['backgroundColor', 'color', 'borderColor', 'divideColor', 'outlineColor', 'textDecorationColor'] as const) {
if (vars.design[key]) {
const r = remapDesignColor(vars.design[key], idMap);
if (r !== vars.design[key]) { (newDesign as any)[key] = r; designChanged = true; }
Expand Down
44 changes: 44 additions & 0 deletions lib/tailwind-class-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,11 @@ const CLASS_PROPERTY_MAP: Record<string, RegExp> = {
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|-\[.+\])?$/,
Expand Down Expand Up @@ -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');
}
}

Expand Down Expand Up @@ -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(')) {
Expand Down
1 change: 1 addition & 0 deletions lib/variable-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ export const DESIGN_COLOR_CSS_MAP: Record<string, string> = {
color: 'color',
borderColor: 'borderColor',
divideColor: '--tw-divide-color',
outlineColor: 'outlineColor',
textDecorationColor: 'textDecorationColor',
};

Expand Down
4 changes: 4 additions & 0 deletions types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ export interface BordersDesign {
divideY?: string;
divideStyle?: string;
divideColor?: string;
outlineWidth?: string;
outlineColor?: string;
outlineOffset?: string;
}

export interface BackgroundsDesign {
Expand Down Expand Up @@ -433,6 +436,7 @@ export interface LayerVariables {
color?: DesignColorVariable; // text color
borderColor?: DesignColorVariable;
divideColor?: DesignColorVariable;
outlineColor?: DesignColorVariable;
textDecorationColor?: DesignColorVariable;
placeholderColor?: DesignColorVariable;
};
Expand Down