Skip to content

Commit ce75389

Browse files
authored
Merge pull request #125 from ycode/develop
develop
2 parents c85bc56 + b208198 commit ce75389

File tree

5 files changed

+142
-41
lines changed

5 files changed

+142
-41
lines changed

app/(builder)/ycode/components/LayerStylesPanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ export default function LayerStylesPanel({
327327
}
328328

329329
return (
330-
<div className="flex flex-col gap-2 pb-2 pt-2">
330+
<div className="flex flex-col gap-1 pb-2 pt-3">
331331
{/* Style Selector or Rename Input */}
332332
{!isCreating && (
333333
<>

app/(builder)/ycode/components/RightSidebar.tsx

Lines changed: 109 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ import { useLayerLocks } from '@/hooks/use-layer-locks';
8585

8686
// 6. Utils, APIs, lib
8787
import { classesToDesign, mergeDesign, removeConflictsForClass, getRemovedPropertyClasses } from '@/lib/tailwind-class-mapper';
88+
import { resetLayerToStyle, hasStyleOverrides } from '@/lib/layer-style-utils';
89+
import { updateStyleAcrossStores } from '@/lib/layer-style-store-utils';
90+
import { useLiveLayerStyleUpdates } from '@/hooks/use-live-layer-style-updates';
8891
import { cn } from '@/lib/utils';
8992
import { sanitizeHtmlId } from '@/lib/html-utils';
9093
import { isFieldVariable, getCollectionVariable, findParentCollectionLayer, findAllParentCollectionLayers, isTextEditable, isTextContentLayer, isRichTextLayer, isHeadingLayer, findLayerWithParent, resetBindingsOnCollectionSourceChange, isInputInsideFilter, resolveFilterInputId, getLayerIndexes, indexedFindLayerById, indexedFindLayerWithParent, indexedFindParentCollectionLayer } from '@/lib/layer-utils';
@@ -612,6 +615,8 @@ const RightSidebar = React.memo(function RightSidebar({
612615

613616
// Get applied layer style and its classes
614617
const getStyleById = useLayerStylesStore((state) => state.getStyleById);
618+
const updateStyle = useLayerStylesStore((state) => state.updateStyle);
619+
const liveLayerStyleUpdates = useLiveLayerStyleUpdates();
615620
const appliedStyle = selectedLayer?.styleId ? getStyleById(selectedLayer.styleId) : undefined;
616621
const styleClassesArray = useMemo(() => {
617622
if (!appliedStyle || !appliedStyle.classes) return [];
@@ -632,14 +637,18 @@ const RightSidebar = React.memo(function RightSidebar({
632637
if (styleClassesArray.length === 0) return new Set<string>();
633638
const overridden = new Set<string>();
634639

635-
// 1. Check for classes overridden by layer's custom classes
640+
// 1. Check for style classes explicitly removed (not present in layer classes at all)
641+
for (const styleClass of styleClassesArray) {
642+
if (!classesArray.includes(styleClass)) {
643+
overridden.add(styleClass);
644+
}
645+
}
646+
647+
// 2. Check for classes overridden by layer's custom classes (conflict detection)
636648
if (layerOnlyClasses.length > 0) {
637649
for (const layerClass of layerOnlyClasses) {
638-
// Use the conflict detection utility
639-
// If adding this layer class would remove any style classes, those are overridden
640650
const classesWithoutConflicts = removeConflictsForClass(styleClassesArray, layerClass);
641651

642-
// Find which style classes were removed (those are the overridden ones)
643652
for (const styleClass of styleClassesArray) {
644653
if (!classesWithoutConflicts.includes(styleClass)) {
645654
overridden.add(styleClass);
@@ -648,7 +657,7 @@ const RightSidebar = React.memo(function RightSidebar({
648657
}
649658
}
650659

651-
// 2. Check for classes from properties explicitly removed on the layer
660+
// 3. Check for classes from properties explicitly removed on the layer
652661
if (appliedStyle?.design && selectedLayer) {
653662
const removedClasses = getRemovedPropertyClasses(
654663
selectedLayer.design,
@@ -659,7 +668,7 @@ const RightSidebar = React.memo(function RightSidebar({
659668
}
660669

661670
return overridden;
662-
}, [layerOnlyClasses, styleClassesArray, appliedStyle, selectedLayer]);
671+
}, [classesArray, layerOnlyClasses, styleClassesArray, appliedStyle, selectedLayer]);
663672

664673
// Update local state when selected layer changes (for settings fields)
665674
const [prevSelectedLayerId, setPrevSelectedLayerId] = useState<string | null>(null);
@@ -758,6 +767,63 @@ const RightSidebar = React.memo(function RightSidebar({
758767
}
759768
}, [classesArray, handleClassesChange, selectedLayer, showTextStyleControls, activeTextStyleKey, handleLayerUpdate]);
760769

770+
// Remove a class that belongs to the applied style — tracks as styleOverrides
771+
const removeStyleClass = useCallback((classToRemove: string) => {
772+
if (!selectedLayer) return;
773+
const newClasses = classesArray.filter(cls => cls !== classToRemove).join(' ');
774+
setClassesInput(newClasses);
775+
handleLayerUpdate(selectedLayer.id, {
776+
classes: newClasses,
777+
styleOverrides: {
778+
classes: newClasses,
779+
design: selectedLayer.styleOverrides?.design ?? selectedLayer.design,
780+
},
781+
});
782+
}, [classesArray, handleLayerUpdate, selectedLayer]);
783+
784+
// Whether the style has any overrides (classes or design)
785+
const styleHasOverrides = useMemo(() => {
786+
if (!appliedStyle || !selectedLayer) return false;
787+
return hasStyleOverrides(selectedLayer, appliedStyle);
788+
}, [appliedStyle, selectedLayer]);
789+
790+
// Update the style definition with current layer values
791+
const handleUpdateStyleFromClasses = useCallback(async () => {
792+
if (!selectedLayer || !appliedStyle) return;
793+
const currentClasses = classesInput;
794+
const currentDesign = selectedLayer.design;
795+
796+
await updateStyle(appliedStyle.id, {
797+
classes: currentClasses,
798+
design: currentDesign,
799+
});
800+
801+
updateStyleAcrossStores(appliedStyle.id, currentClasses, currentDesign);
802+
handleLayerUpdate(selectedLayer.id, { styleOverrides: undefined });
803+
804+
if (liveLayerStyleUpdates) {
805+
liveLayerStyleUpdates.broadcastStyleUpdate(appliedStyle.id, {
806+
classes: currentClasses,
807+
design: currentDesign,
808+
});
809+
}
810+
}, [selectedLayer, appliedStyle, classesInput, updateStyle, handleLayerUpdate, liveLayerStyleUpdates]);
811+
812+
// Reset overrides back to the style's original classes/design
813+
const handleResetStyleOverrides = useCallback(() => {
814+
if (!selectedLayer || !appliedStyle) return;
815+
const updatedLayer = resetLayerToStyle(selectedLayer, appliedStyle);
816+
const resetClasses = Array.isArray(updatedLayer.classes)
817+
? updatedLayer.classes.join(' ')
818+
: updatedLayer.classes || '';
819+
setClassesInput(resetClasses);
820+
handleLayerUpdate(selectedLayer.id, {
821+
classes: updatedLayer.classes,
822+
design: updatedLayer.design,
823+
styleOverrides: undefined,
824+
});
825+
}, [selectedLayer, appliedStyle, handleLayerUpdate]);
826+
761827
// Handle key press for adding classes
762828
const handleKeyPress = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
763829
if (e.key === 'Enter') {
@@ -1690,21 +1756,27 @@ const RightSidebar = React.memo(function RightSidebar({
16901756
<hr className="mt-4" />
16911757

16921758
{/* Design tab */}
1693-
<TabsContent value="design" className="flex-1 flex flex-col divide-y overflow-y-auto no-scrollbar data-[state=inactive]:hidden overflow-x-hidden mt-0">
1759+
<TabsContent value="design" className="flex-1 flex flex-col divide-y data-[state=inactive]:hidden mt-0 overflow-hidden">
16941760

1695-
{/* Layer Styles Panel - hide in text style mode except for richText sublayers */}
1696-
{(!showTextStyleControls || (selectedLayer && isRichTextLayer(selectedLayer))) && (
1697-
<LayerStylesPanel
1698-
layer={selectedLayer}
1699-
pageId={currentPageId}
1700-
onLayerUpdate={handleLayerUpdate}
1701-
activeTextStyleKey={selectedLayer && isRichTextLayer(selectedLayer) ? activeTextStyleKey : null}
1702-
/>
1703-
)}
1761+
<div className="flex flex-col divide-y">
17041762

1705-
{activeTab === 'design' && (
1706-
<UIStateSelector selectedLayer={selectedLayer} />
1707-
)}
1763+
{/* Layer Styles Panel - hide in text style mode except for richText sublayers */}
1764+
{(!showTextStyleControls || (selectedLayer && isRichTextLayer(selectedLayer))) && (
1765+
<LayerStylesPanel
1766+
layer={selectedLayer}
1767+
pageId={currentPageId}
1768+
onLayerUpdate={handleLayerUpdate}
1769+
activeTextStyleKey={selectedLayer && isRichTextLayer(selectedLayer) ? activeTextStyleKey : null}
1770+
/>
1771+
)}
1772+
1773+
{activeTab === 'design' && (
1774+
<UIStateSelector selectedLayer={selectedLayer} />
1775+
)}
1776+
1777+
</div>
1778+
1779+
<div className="overflow-y-auto no-scrollbar overflow-x-hidden divide-y ">
17081780

17091781
{shouldShowControl('layout', selectedLayer) && !showTextStyleControls && (
17101782
<LayoutControls layer={selectedLayer} onLayerUpdate={handleLayerUpdate} />
@@ -1789,9 +1861,10 @@ const RightSidebar = React.memo(function RightSidebar({
17891861
{layerOnlyClasses.map((cls, index) => (
17901862
<Badge
17911863
variant="secondary"
1864+
className="truncate max-w-50"
17921865
key={`layer-${index}`}
17931866
>
1794-
<span>{cls}</span>
1867+
<span className="truncate">{cls}</span>
17951868
<Button
17961869
onClick={() => removeClass(cls)}
17971870
className="size-4! p-0! -mr-1"
@@ -1805,7 +1878,7 @@ const RightSidebar = React.memo(function RightSidebar({
18051878
</div>
18061879
)}
18071880

1808-
{/* Layer style classes (strikethrough if overridden) */}
1881+
{/* Layer style classes (removable, strikethrough if overridden) */}
18091882
{styleClassesArray.length > 0 && (
18101883
<div className="flex flex-col gap-2.5">
18111884
<div className="py-1 w-full flex items-center gap-2">
@@ -1823,11 +1896,21 @@ const RightSidebar = React.memo(function RightSidebar({
18231896
<Badge
18241897
variant="secondary"
18251898
key={`style-${index}`}
1826-
className="opacity-60"
1899+
className="opacity-60 truncate max-w-50"
18271900
>
1828-
<span className={isOverridden ? 'line-through' : ''}>
1901+
<span className={isOverridden ? 'line-through truncate' : 'truncate'}>
18291902
{cls}
18301903
</span>
1904+
{!isOverridden && (
1905+
<Button
1906+
onClick={() => removeStyleClass(cls)}
1907+
className="size-4! p-0! -mr-1"
1908+
variant="outline"
1909+
disabled={isLockedByOther}
1910+
>
1911+
<Icon name="x" className="size-2" />
1912+
</Button>
1913+
)}
18311914
</Badge>
18321915
);
18331916
})}
@@ -1836,6 +1919,9 @@ const RightSidebar = React.memo(function RightSidebar({
18361919
)}
18371920
</div>
18381921
</SettingsPanel>
1922+
1923+
</div>
1924+
18391925
</TabsContent>
18401926

18411927
<TabsContent value="settings" className="flex-1 overflow-y-auto no-scrollbar mt-0 data-[state=inactive]:hidden">

app/(builder)/ycode/components/UIStateSelector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default function UIStateSelector({ selectedLayer }: UIStateSelectorProps)
2525
};
2626

2727
return (
28-
<div className="sticky -top-2 bg-background z-30 py-4 flex flex-row gap-2">
28+
<div className="bg-background z-30 py-3 flex flex-row gap-2">
2929
<Select value={activeUIState} onValueChange={(value) => setActiveUIState(value as UIState)}>
3030
<SelectTrigger className={`w-full ${activeUIState !== 'neutral' ? 'text-[#8dd92f]' : ''}`}>
3131
<SelectValue placeholder="Select..." />

components/LayerRenderer.tsx

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ interface LayerRendererProps {
124124
isSlideChild?: boolean;
125125
/** Server-side settings (for preview/published pages where Zustand store is not available) */
126126
serverSettings?: Record<string, unknown>;
127+
/** When true, the component root layer (layer.id === parentComponentLayerId) renders its own context menu */
128+
componentRootContextMenu?: boolean;
127129
}
128130

129131
const LayerRenderer: React.FC<LayerRendererProps> = ({
@@ -169,6 +171,7 @@ const LayerRenderer: React.FC<LayerRendererProps> = ({
169171
ancestorComponentIds,
170172
isSlideChild: isSlideChildProp,
171173
serverSettings,
174+
componentRootContextMenu,
172175
}) => {
173176
const [editingLayerId, setEditingLayerId] = useState<string | null>(null);
174177
const [editingContent, setEditingContent] = useState<string>('');
@@ -310,6 +313,7 @@ const LayerRenderer: React.FC<LayerRendererProps> = ({
310313
ancestorComponentIds={ancestorComponentIds}
311314
isSlideChild={isSlideChildProp}
312315
serverSettings={serverSettings}
316+
componentRootContextMenu={componentRootContextMenu}
313317
/>
314318
);
315319
};
@@ -371,6 +375,7 @@ const LayerItem: React.FC<{
371375
ancestorComponentIds?: Set<string>;
372376
isSlideChild?: boolean;
373377
serverSettings?: Record<string, unknown>;
378+
componentRootContextMenu?: boolean;
374379
}> = ({
375380
layer,
376381
isEditMode,
@@ -420,6 +425,7 @@ const LayerItem: React.FC<{
420425
ancestorComponentIds,
421426
isSlideChild,
422427
serverSettings,
428+
componentRootContextMenu,
423429
}) => {
424430
// Subscribe to selection state from the store for reactive updates without
425431
// forcing the entire LayerRenderer tree to re-render when selection changes
@@ -1570,21 +1576,22 @@ const LayerItem: React.FC<{
15701576
? resolveVariableLinks(layer.componentOverrides, parentComponentOverrides, parentComponentVariables)
15711577
: layer.componentOverrides;
15721578

1579+
const needsRootContextMenu = isEditMode && !!pageId && !isEditing && !parentComponentLayerId;
1580+
15731581
return (
1574-
<div className="contents">
1575-
<LayerRenderer
1576-
layers={layersWithInstanceId}
1577-
{...sharedRendererProps}
1578-
editorHiddenLayerIds={componentEditorHiddenLayerIds}
1579-
enableDragDrop={enableDragDrop}
1580-
activeLayerId={activeLayerId}
1581-
projected={projected}
1582-
parentComponentLayerId={layer.id}
1583-
parentComponentOverrides={effectiveOverrides}
1584-
parentComponentVariables={component?.variables}
1585-
ancestorComponentIds={effectiveAncestorIds}
1586-
/>
1587-
</div>
1582+
<LayerRenderer
1583+
layers={layersWithInstanceId}
1584+
{...sharedRendererProps}
1585+
editorHiddenLayerIds={componentEditorHiddenLayerIds}
1586+
enableDragDrop={enableDragDrop}
1587+
activeLayerId={activeLayerId}
1588+
projected={projected}
1589+
parentComponentLayerId={layer.id}
1590+
parentComponentOverrides={effectiveOverrides}
1591+
parentComponentVariables={component?.variables}
1592+
ancestorComponentIds={effectiveAncestorIds}
1593+
componentRootContextMenu={needsRootContextMenu || undefined}
1594+
/>
15881595
);
15891596
}
15901597

@@ -3011,6 +3018,13 @@ const LayerItem: React.FC<{
30113018
return renderContent();
30123019
}
30133020

3021+
// Component instances render without a wrapper element so they participate
3022+
// directly in the parent's layout (required for divide-* utilities).
3023+
// The component root handles its own context menu via componentRootContextMenu.
3024+
if (transformedComponentLayers) {
3025+
return renderContent();
3026+
}
3027+
30143028
// Wrap with context menu in edit mode
30153029
// Don't wrap layers inside component instances (they're not directly editable)
30163030
let content = renderContent();
@@ -3047,7 +3061,8 @@ const LayerItem: React.FC<{
30473061
}
30483062
}
30493063

3050-
if (isEditMode && pageId && !isEditing && !parentComponentLayerId) {
3064+
const isComponentRoot = componentRootContextMenu && parentComponentLayerId && layer.id === parentComponentLayerId;
3065+
if (isEditMode && pageId && !isEditing && (!parentComponentLayerId || isComponentRoot)) {
30513066
const isLocked = layer.id === 'body';
30523067

30533068
return (

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ycode",
3-
"version": "0.10.1",
3+
"version": "0.10.2",
44
"description": "Visual website builder you can self-host",
55
"license": "MIT",
66
"private": false,

0 commit comments

Comments
 (0)