Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5606891
feat: add Builder Style Book doctype
Vibhuti410 Mar 2, 2026
eae84a5
feat: add get_style_book and update_style_book API endpoints
Vibhuti410 Mar 2, 2026
96869a5
feat-add style book data resource
Vibhuti410 Mar 3, 2026
090a79a
feat-add styleBook data resource
Vibhuti410 Mar 3, 2026
13022f0
feat: add Style dropdown to Typography section
Vibhuti410 Mar 3, 2026
e5098f5
feat: update to use Builder Style Preset doctype
Vibhuti410 Mar 4, 2026
563b946
feat: updated style preset
Vibhuti410 Mar 7, 2026
661e7a4
feat: handling null selection
Vibhuti410 Mar 9, 2026
1c43160
feat: syle preset inheritance with placeholder
Vibhuti410 Mar 11, 2026
101c324
refactor: used preStyle and stykeKeyMap for handling the preset
Vibhuti410 Mar 11, 2026
c6e31f9
refactor: used preStyle and stykeKeyMap for handling the preset
Vibhuti410 Mar 11, 2026
b293bfc
fix: map numeric value of fontWeight to label in preset placeholder
Vibhuti410 Mar 12, 2026
b3ea745
fix: resolved merge conflict
Vibhuti410 Mar 13, 2026
16af940
fix: removed unused block of code
Vibhuti410 Mar 13, 2026
245390d
fix: made style_map field as required
Vibhuti410 Mar 13, 2026
584fcfe
refactor: simplified style preset block in typography.ts
Vibhuti410 Mar 15, 2026
a32d516
feat: applied preset styles on page publish
Vibhuti410 Mar 16, 2026
8e2d61d
refactor: update preset style structure
Vibhuti410 Mar 17, 2026
f1efc6b
Merge branch 'develop' into feature-style-book
surajshetty3416 Mar 19, 2026
0e040a7
Merge branch 'develop' into feature-style-book
surajshetty3416 Mar 20, 2026
4be1f50
Merge branch 'develop' into feature-style-book
surajshetty3416 Mar 20, 2026
9612d72
feat: ship standard style preset json files
Vibhuti410 Mar 22, 2026
9f2fa47
Merge branch 'develop' into feature-style-book
mergify[bot] Mar 25, 2026
76827e7
fix: handle preset again after manual change
Vibhuti410 Mar 25, 2026
04d15db
Merge branch 'feature-style-book' of https://github.com/Vibhuti410/bu…
Vibhuti410 Mar 25, 2026
ad07217
fix: style presets stay on refresh
Vibhuti410 Mar 25, 2026
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
14 changes: 13 additions & 1 deletion builder/builder/doctype/builder_page/builder_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -756,8 +756,20 @@ def generate_and_apply_styles(block: dict, state: dict) -> str:
style_tag = state["style_tag"]
font_map = state["font_map"]

preset_styles = {}
if block.get("stylePreset"):
preset = frappe.db.get_value(
Comment thread
Vibhuti410 marked this conversation as resolved.
Outdated
"Builder Style Preset", # DocType name
{"style_name": block.get("stylePreset")}, # filter by style_name
"style_map", # field to fetch
Comment thread
Vibhuti410 marked this conversation as resolved.
Outdated
)
if preset: # parse to dic from JSON
preset_styles = frappe.parse_json(preset)
Comment thread
Vibhuti410 marked this conversation as resolved.
Outdated

styles = {
"base": split_styles(block.get("baseStyles", {})),
"base": split_styles(
{**preset_styles, **block.get("baseStyles", {})}
), # if user manually set fontWeight, it overrides preset's fontWeight
"mobile": split_styles(block.get("mobileStyles", {})),
"tablet": split_styles(block.get("tabletStyles", {})),
"raw": split_styles(block.get("rawStyles", {})),
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) 2026, Frappe Technologies Pvt Ltd and contributors
// For license information, please see license.txt

// frappe.ui.form.on("Builder Style Preset", {
// refresh(frm) {

// },
// });
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:style_name",
"creation": "2026-03-04 11:58:54.158151",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"style_name_section",
"style_name",
"style_map",
"sort_order"
],
"fields": [
{
"fieldname": "style_name_section",
"fieldtype": "Section Break",
"label": "Style Name"
},
{
"fieldname": "style_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Style Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "style_map",
"fieldtype": "JSON",
"label": "Style Map",
"reqd": 1
},
{
"fieldname": "sort_order",
"fieldtype": "Int",
"label": "Sort Order"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-03-13 12:47:52.513378",
"modified_by": "Administrator",
"module": "Builder",
"name": "Builder Style Preset",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright (c) 2026, Frappe Technologies Pvt Ltd and contributors
# For license information, please see license.txt

# import frappe
from frappe.model.document import Document


class BuilderStylePreset(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from frappe.types import DF

sort_order: DF.Int
style_map: DF.JSON
style_name: DF.Data
# end: auto-generated types

pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright (c) 2026, Frappe Technologies Pvt Ltd and Contributors
# See license.txt

# import frappe
from frappe.tests import IntegrationTestCase

# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]


class IntegrationTestBuilderStylePreset(IntegrationTestCase):
"""
Integration tests for BuilderStylePreset.
Use this class for testing interactions between multiple components.
"""

pass
1 change: 1 addition & 0 deletions frontend/src/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class Block implements BlockOptions {
blockClientScript?: string;
blockDataScript?: string;
props?: BlockProps;
stylePreset?: string;
// @ts-expect-error
referenceComponent: Block | null;
customAttributes: BlockAttributeMap;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import userFonts from "@/data/userFonts";
import { UserFont } from "@/types/Builder/UserFont";
import blockController from "@/utils/blockController";
import { setFont as _setFont, fontList, getFontWeightOptions } from "@/utils/fontManager";
import stylePreset from "@/data/stylePreset";

const setFont = (font: string) => {
_setFont(font, null).then(() => {
Expand Down Expand Up @@ -34,6 +35,33 @@ const typographySectionProperties = [
condition: () =>
(blockController.isText() || blockController.isButton()) && !blockController.multipleBlocksSelected(),
},
{
component: BasePropertyControl,
getProps: () => {
return {
label: "Style",
propertyKey: "textStylePreset",
controlType: "key",
allowDynamicValue: true,
type: "select",
options: stylePreset.data //list of items in dropdown
? [
{ label: "None", value: null },
...stylePreset.data.map((s: any) => ({ //s -> object and kept any not defined type of it
label: s.style_name, //if stylebook.data exists and loaded, do the map or else return empty array
value: s.style_name,
}))
Comment thread
Vibhuti410 marked this conversation as resolved.
Outdated
]
: [{ label: "None", value: " " }],
getModelValue: () => String(blockController.getKeyValue("stylePreset") ?? ""),
setModelValue: (val: string) => {
blockController.setKeyValue("stylePreset", val);
},
};
},
searchKeyWords: "Style, Preset, Typography",
condition: () => blockController.isText(),
},
{
component: StylePropertyControl,
getProps: () => {
Expand Down Expand Up @@ -81,7 +109,6 @@ const typographySectionProperties = [
actionButton: {
component: FontUploader,
},
getModelValue: () => blockController.getFontFamily(),
Comment thread
Vibhuti410 marked this conversation as resolved.
setModelValue: (val: string) => setFont(val),
};
},
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/components/BuilderBlock.vue
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import fetchBlockData from "@/data/blockData";
import usePageStore from "@/stores/pageStore";
import { toast } from "vue-sonner";
import useBlockDataStore from "@/stores/blockDataStore";
import stylePreset from "@/data/stylePreset";

const builderStore = useBuilderStore();
const canvasStore = useCanvasStore();
Expand Down Expand Up @@ -266,6 +267,14 @@ const target = computed(() => {
}
});

const presetStyles = computed(() => {
const presetName = props.block.stylePreset;
if (!presetName) return {};
const preset = stylePreset.data?.find((s: any) => s.style_name === presetName);
if (!preset) return {};
return typeof preset.style_map === "string" ? JSON.parse(preset.style_map) : preset.style_map;
});

const styles = computed(() => {
let dynamicStyles = {} as { [key: string]: string };
if (props.data || hasBlockProps.value) {
Expand Down Expand Up @@ -314,6 +323,7 @@ const styles = computed(() => {
}

const styleMap = {
...presetStyles.value,
...props.block.getStyles(props.breakpoint),
...props.block.getEditorStyles(),
...dynamicStyles,
Expand Down Expand Up @@ -478,6 +488,19 @@ watch(
{ deep: true, immediate: true },
);

// when preset changes, clear any manually set styles
// so the new preset values show as grey placeholders instead of black
watch(
() => props.block.stylePreset,
(val) => {
const styleKeyMap = ["fontFamily", "fontWeight", "fontSize", "lineHeight", "textTransform"];
styleKeyMap.forEach((cssProperty) => {
// setting to null deletes the property from baseStyles
props.block.setStyle(cssProperty as styleProperty, null);
});
},
);

Comment thread
Vibhuti410 marked this conversation as resolved.
Outdated
const isEditable = computed(() => {
// to ensure it is right block and not on different breakpoint
return (
Expand Down
32 changes: 29 additions & 3 deletions frontend/src/components/Controls/StylePropertyControl.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
</template>

<script lang="ts" setup>
import { getPresetMap } from "@/utils/presetUtils";
import BasePropertyControl from "@/components/Controls/BasePropertyControl.vue";
import blockController from "@/utils/blockController";
import type { Component } from "vue";
import { computed } from "vue";
import { getFontWeightOptions } from "@/utils/fontManager";

const props = withDefaults(
defineProps<{
Expand Down Expand Up @@ -92,15 +94,39 @@ const setVariantValue = (variantName: string, value: string | number | boolean |

const baseProps = computed(() => {
const { enableStates, enabledStates, variants, ...rest } = props;
const presetMap = getPresetMap();
const presetValue = presetMap?.[props.propertyKey];
const isInherited =
presetValue && blockController.getNativeStyle(props.propertyKey as styleProperty) === presetValue;
return {
...rest,
controlType: "style" as const,
getModelValue: props.getModelValue || (() => blockController.getNativeStyle(props.propertyKey) ?? ""),
getModelValue:
props.getModelValue ||
(() => (isInherited ? "" : String(blockController.getNativeStyle(props.propertyKey) ?? ""))),
setModelValue:
props.setModelValue ||
((value: string | number | boolean) => blockController.setStyle(props.propertyKey, value)),
((value: string | number | boolean) => {
if (!value && presetValue) {
blockController.setStyle(props.propertyKey, presetValue);
} else {
blockController.setStyle(props.propertyKey, value);
}
}),
getPlaceholder:
props.getPlaceholder || (() => blockController.getCascadingStyle(props.propertyKey) ?? "unset"),
props.getPlaceholder ||
(() => {
if (presetValue) {
if (props.propertyKey === "fontWeight") {
const options = getFontWeightOptions(
(blockController.getStyle("fontFamily") as string) || "Inter",
);
return options.find((o) => o.value === String(presetValue))?.label || String(presetValue);
}
return String(presetValue);
}
return String(blockController.getCascadingStyle(props.propertyKey) ?? "unset");
}),
};
});
</script>
14 changes: 14 additions & 0 deletions frontend/src/data/stylePreset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createListResource } from "frappe-ui";

const stylePreset = createListResource({
method: "GET",
doctype: "Builder Style Preset",
fields: ["style_name", "style_map", "sort_order"],
cache: "stylePresets",
start: 0,
pageLength: 50,
auto: true,
orderBy: "sort_order asc",
});

export default stylePreset;
7 changes: 4 additions & 3 deletions frontend/src/utils/blockController.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { default as Block, default as BlockDataKey } from "@/block";
import useCanvasStore from "@/stores/canvasStore";
import getBlockTemplate from "./blockTemplate";
import { set } from "@vueuse/core";

const canvasStore = useCanvasStore();

Expand Down Expand Up @@ -114,7 +115,7 @@ const blockController = {
block.removeAttribute(attribute);
});
},
getKeyValue: (key: "element" | "innerHTML" | "visibilityCondition") => {
getKeyValue: (key: "element" | "innerHTML" | "visibilityCondition" | "stylePreset") => {
if (key !== "visibilityCondition") {
let keyValue = "__initial__" as StyleValue | undefined;
canvasStore.activeCanvas?.selectedBlocks.forEach((block) => {
Expand Down Expand Up @@ -147,7 +148,7 @@ const blockController = {
return { key, comesFrom };
}
},
setKeyValue: (key: "element" | "innerHTML" | "visibilityCondition", value: any) => {
setKeyValue: (key: "element" | "innerHTML" | "visibilityCondition" | "stylePreset", value: any) => {
canvasStore.activeCanvas?.selectedBlocks.forEach((block) => {
if (key === "element" && block.blockName === "container") {
// reset blockName since it will not be a container anymore
Expand Down Expand Up @@ -379,7 +380,7 @@ const blockController = {
}
Object.keys(block.props).forEach((key) => {
if (!props[key]) {
delete block.props?.[key];
delete block.props?.[key];
Comment thread
Vibhuti410 marked this conversation as resolved.
Outdated
}
});
Object.assign(block.props, props);
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/utils/blockTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,10 @@ function getBlockTemplate(
name: "Text",
element: "p",
innerHTML: "Text",
stylePreset: "paragraph",
baseStyles: {
fontSize: "16px",
width: "fit-content",
height: "fit-content",
lineHeight: "1.4",
minWidth: "10px",
} as BlockStyleMap,
};
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/utils/presetUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import blockController from "@/utils/blockController";
import stylePreset from "@/data/stylePreset";

export const getPresetMap = () => {
const presetName = blockController.getKeyValue("stylePreset");
const preset = stylePreset.data?.find((s: any) => s.style_name === presetName);
return preset
? (typeof preset.style_map === "string" ? JSON.parse(preset.style_map) : preset.style_map)
: null;
};
Comment thread
Vibhuti410 marked this conversation as resolved.
Loading