Skip to content
Open
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
21 changes: 20 additions & 1 deletion builder/builder/doctype/builder_page/builder_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -1322,10 +1322,29 @@ def append_state_style(style_obj, style_tag, style_class, device="desktop"):
if ":" in key:
state, property = key.split(":", 1)
css_property = camel_case_to_kebab_case(property)
style_string = f".{style_class}:{state} {{ {css_property}: {value}; }}"
declaration = f"{css_property}: {value};"
if state == "dark":
style_string = dark_mode_style(style_class, declaration)
else:
style_string = f".{style_class}:{state} {{ {declaration} }}"
style_tag.append(wrap_with_media_query(style_string, device))


def dark_mode_style(style_class, declaration):
"""CSS for a dark-mode-only style (e.g. a dark variant of a background image).

Dark mode is active for the OS scheme unless the page is manually pinned to
light, and for the manual dark toggle — mirroring how Builder scopes dark mode
elsewhere via data-theme / data-prefers-color-scheme.
"""
not_light = ':root:not([data-theme="light"]):not([data-prefers-color-scheme="light"])'
forced = f':root[data-theme="dark"] .{style_class}, :root[data-prefers-color-scheme="dark"] .{style_class}'
return (
f"@media (prefers-color-scheme: dark) {{ {not_light} .{style_class} {{ {declaration} }} }} "
f"{forced} {{ {declaration} }}"
)


def get_font_family(font: str) -> str:
"""Return the first family from a CSS font stack (e.g. 'Inter, sans-serif' -> 'Inter')."""
return font.split(",")[0].strip().strip("'\"")
Expand Down
24 changes: 16 additions & 8 deletions frontend/src/components/BackgroundHandler.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
:component="BackgroundInput"
label="Background"
:enableStates="true"
:variants="bgVariants"
:allowDynamicValue="true"
placeholder="Set Background"
readonly
Expand Down Expand Up @@ -176,6 +177,10 @@ const BackgroundInput = defineComponent({
},
});

// "Dark Mode" sits alongside the hover/active/focus states in the "+" menu; its
// value is stored as `dark:background*` styles and rendered under prefers-color-scheme.
const bgVariants = [{ name: "dark", property: "dark:background", label: "Dark Mode" }];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Mixed backgrounds corrupt dark variants

When selected blocks have different base backgrounds, adding Dark Mode copies blockController.getStyle()'s mixed-value sentinel into every block, causing invalid or incorrect dark background CSS instead of preserving each block's own value.

Context Used: AGENTS.md (source)

Knowledge Base Used: Properties Panel: Editing Block Styles and Props


const activeState = ref<string | null>(null);

const updateActiveState = (e: FocusEvent) => {
Expand Down Expand Up @@ -386,14 +391,17 @@ const handleSetVariant = (variantName: string, value: string | number | boolean
return;
}

// Basic transition logic for states
blockController.getSelectedBlocks().forEach((block) => {
if (!block.getStyle("transitionDuration")) {
block.setStyle("transitionDuration", "300ms");
block.setStyle("transitionTimingFunction", "ease");
block.setStyle("transitionProperty", "all");
}
});
// Basic transition logic for interaction states; dark mode is a theme switch,
// not a hover-style state, so it shouldn't add a transition.
if (variantName !== "dark") {
blockController.getSelectedBlocks().forEach((block) => {
if (!block.getStyle("transitionDuration")) {
block.setStyle("transitionDuration", "300ms");
block.setStyle("transitionTimingFunction", "ease");
block.setStyle("transitionProperty", "all");
}
});
}

// the trigger input is read-only, so a non-null value here can only come from
// the "copy current value to state" dropdown — copy the actual base styles
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/components/BuilderBlock.vue
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,13 @@ const styles = computed(() => {
}

Object.keys(styleMap).forEach((key) => {
if (key.startsWith("hover:")) {
if (key.startsWith("dark:")) {
// preview the dark variant when the canvas is in dark mode
if (!props.preview && builderStore.canvasDarkMode) {
styleMap[key.slice(5)] = styleMap[key];
}
delete styleMap[key];
} else if (key.startsWith("hover:")) {
// state style preview on hover
// if (!isHovered.value) {
// delete styleMap[key];
Expand Down
12 changes: 7 additions & 5 deletions frontend/src/utils/builderBlockCopyPaste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,16 +389,18 @@ function updateURLsInBlock(block: Block, currentSiteURL: string): void {
}
}

// Update background image URLs
if (block) {
const bgSrc = block.getStyle("backgroundImage");
// Update background image URLs (including the dark-mode variant)
const absolutizeBackground = (styleKey: string) => {
const bgSrc = block.getStyle(styleKey);
if (bgSrc && typeof bgSrc === "string" && bgSrc.startsWith("url(")) {
const urlMatch = bgSrc.match(/url\(["']?([^"']+)["']?\)/);
if (urlMatch && urlMatch[1] && urlMatch[1].startsWith("/")) {
block.setStyle("backgroundImage", `url(${currentSiteURL}${urlMatch[1]})`);
block.setStyle(styleKey, `url(${currentSiteURL}${urlMatch[1]})`);
}
}
}
};
absolutizeBackground("backgroundImage");
absolutizeBackground("dark:backgroundImage");

// Update href attributes for links
// if (block.isLink()) {
Expand Down
Loading