diff --git a/builder/api.py b/builder/api.py index 1629aecd9..fec7b87c9 100644 --- a/builder/api.py +++ b/builder/api.py @@ -20,7 +20,7 @@ from builder import builder_analytics from builder.builder.doctype.builder_page.builder_page import BuilderPageRenderer from builder.builder.doctype.builder_snapshot import builder_snapshot -from builder.utils import compact_json, has_page_read, has_page_write +from builder.utils import compact_json, has_page_read, has_page_write, normalize_renamed_doc @frappe.whitelist() @@ -340,7 +340,8 @@ def create_page_from_bundle(bundle: dict, project_folder: str | None = None) -> for font in bundle.get("fonts") or []: import_doc(docdict=font) for var in bundle.get("variables") or []: - import_doc(docdict=var) + # a hub still on the pre-rename schema sends Builder Variable docs + import_doc(docdict=normalize_renamed_doc(var)) for comp in bundle.get("components") or []: import_doc(docdict=comp) diff --git a/builder/builder/doctype/builder_page/builder_page.py b/builder/builder/doctype/builder_page/builder_page.py index d83a15b85..d314db011 100644 --- a/builder/builder/doctype/builder_page/builder_page.py +++ b/builder/builder/doctype/builder_page/builder_page.py @@ -357,11 +357,11 @@ def get_context(self, context): if context.preview: context.disable_auto_dark_mode = 0 - # /builder_assets/variables.css is a rendered route, not a real file, so + # /builder_assets/tokens.css is a rendered route, not a real file, so # the preview/PDF generator can't fetch it. Inline the variables instead. - from builder.builder.doctype.builder_variable.builder_variable import get_variables_css + from builder.builder.doctype.builder_token.builder_token import get_variables_css - context.inline_variables_css = get_variables_css() + context.inline_tokens_css = get_variables_css() # Honour the dark/light mode the editor previews in (canvasDarkMode), so the # initial server render matches it instead of falling back to the OS scheme. scheme = frappe.form_dict.get("prefers_color_scheme") @@ -1375,10 +1375,24 @@ def append_state_style(style_obj, style_tag, style_class, device="desktop"): def get_font_family(font: str) -> str: - """Return the first family from a CSS font stack (e.g. 'Inter, sans-serif' -> 'Inter').""" + """Return the first family from a CSS font stack (e.g. 'Inter, sans-serif' -> 'Inter'). + A Font design token (var(--id)) resolves to its family so the Google Fonts + links include tokenized families.""" + font = resolve_font_token(font) return font.split(",")[0].strip().strip("'\"") +def resolve_font_token(font: str) -> str: + match = re.match(r"\s*var\(\s*(--[^),\s]+)", font or "") + if not match: + return font + from builder.builder.doctype.builder_token.builder_token import get_css_variables + + # the cached token map, so a page full of tokenized fonts is not a query each + css_variables, _ = get_css_variables() + return css_variables.get(match.group(1), "") + + def set_fonts(styles, font_map, inherited_font=None): weight_map = { "thin": "100", @@ -1425,8 +1439,8 @@ def set_fonts(styles, font_map, inherited_font=None): # Use the first family from a fallback list, e.g. "Inter, sans-serif" -> "Inter" font = get_font_family(font) - # Skip if it is a system font - if font.lower() in system_fonts: + # Skip system fonts and unresolvable font tokens + if not font or font.lower() in system_fonts: continue weight = str(style.get("fontWeight") or "400").lower() diff --git a/builder/builder/doctype/builder_variable/__init__.py b/builder/builder/doctype/builder_token/__init__.py similarity index 100% rename from builder/builder/doctype/builder_variable/__init__.py rename to builder/builder/doctype/builder_token/__init__.py diff --git a/builder/builder/doctype/builder_variable/builder_variable.js b/builder/builder/doctype/builder_token/builder_token.js similarity index 87% rename from builder/builder/doctype/builder_variable/builder_variable.js rename to builder/builder/doctype/builder_token/builder_token.js index d38eda6ff..92cf72f6e 100644 --- a/builder/builder/doctype/builder_variable/builder_variable.js +++ b/builder/builder/doctype/builder_token/builder_token.js @@ -1,7 +1,7 @@ // Copyright (c) 2025, Frappe Technologies Pvt Ltd and contributors // For license information, please see license.txt -frappe.ui.form.on("Builder Variable", { +frappe.ui.form.on("Builder Token", { refresh: function (frm) { // Only show is_standard field in developer mode frm.get_field("is_standard").toggle(frappe.boot.developer_mode); diff --git a/builder/builder/doctype/builder_variable/builder_variable.json b/builder/builder/doctype/builder_token/builder_token.json similarity index 89% rename from builder/builder/doctype/builder_variable/builder_variable.json rename to builder/builder/doctype/builder_token/builder_token.json index da5305134..f12244c7e 100644 --- a/builder/builder/doctype/builder_variable/builder_variable.json +++ b/builder/builder/doctype/builder_token/builder_token.json @@ -5,7 +5,7 @@ "engine": "InnoDB", "field_order": [ "is_standard", - "variable_name", + "token_name", "group", "type", "value", @@ -17,7 +17,7 @@ "fieldname": "type", "fieldtype": "Select", "label": "Type", - "options": "Color\nDimension" + "options": "Color\nDimension\nFont" }, { "fieldname": "value", @@ -28,10 +28,10 @@ "reqd": 1 }, { - "fieldname": "variable_name", + "fieldname": "token_name", "fieldtype": "Data", "in_list_view": 1, - "label": "Variable Name", + "label": "Token Name", "reqd": 1 }, { @@ -61,7 +61,7 @@ "modified": "2026-05-24 12:00:00.000000", "modified_by": "Administrator", "module": "Builder", - "name": "Builder Variable", + "name": "Builder Token", "naming_rule": "By script", "owner": "Administrator", "permissions": [ @@ -89,5 +89,5 @@ "sort_field": "creation", "sort_order": "DESC", "states": [], - "title_field": "variable_name" + "title_field": "token_name" } diff --git a/builder/builder/doctype/builder_variable/builder_variable.py b/builder/builder/doctype/builder_token/builder_token.py similarity index 64% rename from builder/builder/doctype/builder_variable/builder_variable.py rename to builder/builder/doctype/builder_token/builder_token.py index cac5c9d4a..e7177063f 100644 --- a/builder/builder/doctype/builder_variable/builder_variable.py +++ b/builder/builder/doctype/builder_token/builder_token.py @@ -10,7 +10,7 @@ from frappe.website.utils import delete_page_cache -class BuilderVariable(Document): +class BuilderToken(Document): # begin: auto-generated types # This code is auto-generated. Do not modify anything in this block. @@ -22,9 +22,9 @@ class BuilderVariable(Document): dark_value: DF.Data | None group: DF.Data | None is_standard: DF.Check - type: DF.Literal["Color", "Dimension"] + type: DF.Literal["Color", "Dimension", "Font"] value: DF.Data - variable_name: DF.Data + token_name: DF.Data # end: auto-generated types def autoname(self): @@ -32,37 +32,37 @@ def autoname(self): self.name = str(uuid.uuid4()) def after_insert(self): - clear_builder_variable_cache() + clear_builder_token_cache() def on_update(self): - clear_builder_variable_cache() + clear_builder_token_cache() if self.is_standard: export_to_files( - record_list=[["Builder Variable", self.name, "builder_variable"]], record_module="builder" + record_list=[["Builder Token", self.name, "builder_token"]], record_module="builder" ) if self.has_value_changed("is_standard") and not self.is_standard: - delete_folder("builder", "builder_variable", self.name) + delete_folder("builder", "builder_token", self.name) def on_trash(self): - clear_builder_variable_cache() + clear_builder_token_cache() if self.is_standard: - delete_folder("builder", "builder_variable", self.name) + delete_folder("builder", "builder_token", self.name) @redis_cache(ttl=10 * 24 * 3600) def get_css_variables(): - builder_variables = frappe.get_all("Builder Variable", fields=["name", "value", "dark_value"]) + builder_tokens = frappe.get_all("Builder Token", fields=["name", "value", "dark_value"]) css_variables = {} dark_mode_css_variables = {} - for builder_variable in builder_variables: - if not builder_variable.value: + for builder_token in builder_tokens: + if not builder_token.value: continue - key = f"--{builder_variable.name}" - css_variables[key] = builder_variable.value - if builder_variable.dark_value: - dark_mode_css_variables[key] = builder_variable.dark_value + key = f"--{builder_token.name}" + css_variables[key] = builder_token.value + if builder_token.dark_value: + dark_mode_css_variables[key] = builder_token.dark_value return css_variables, dark_mode_css_variables @@ -70,10 +70,10 @@ def get_css_variables(): def get_variables_css() -> str: """Render the CSS variables as an inline `:root {...}` rule. - The /builder_assets/variables.css route is a dynamically rendered page, not a + The /builder_assets/tokens.css route is a dynamically rendered page, not a real file, so the preview/PDF generator can't fetch it (it blocks access to non-existent local paths). Preview rendering inlines this string instead of - linking the route. Mirrors www/builder_assets/variables.css.""" + linking the route. Mirrors www/builder_assets/tokens.css.""" css_variables, dark_mode_css_variables = get_css_variables() if not css_variables: return "" @@ -89,7 +89,8 @@ def get_variables_css() -> str: return ":root {\n" + "\n".join(declarations) + "\n}" -def clear_builder_variable_cache(doc=None, method=None): +def clear_builder_token_cache(doc=None, method=None): get_css_variables.clear_cache() - # bust the rendered page cache for /builder_assets/variables.css + # bust the rendered page cache for tokens.css and its compat alias variables.css + delete_page_cache("builder_assets/tokens.css") delete_page_cache("builder_assets/variables.css") diff --git a/builder/builder/doctype/builder_variable/test_builder_variable.py b/builder/builder/doctype/builder_token/test_builder_token.py similarity index 77% rename from builder/builder/doctype/builder_variable/test_builder_variable.py rename to builder/builder/doctype/builder_token/test_builder_token.py index d20d8d6c8..46491f5aa 100644 --- a/builder/builder/doctype/builder_variable/test_builder_variable.py +++ b/builder/builder/doctype/builder_token/test_builder_token.py @@ -11,18 +11,18 @@ IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] -class UnitTestbuilderVariable(UnitTestCase): +class UnitTestbuilderToken(UnitTestCase): """ - Unit tests for builderVariable. + Unit tests for builderToken. Use this class for testing individual functions and methods. """ pass -class IntegrationTestbuilderVariable(IntegrationTestCase): +class IntegrationTestbuilderToken(IntegrationTestCase): """ - Integration tests for builderVariable. + Integration tests for builderToken. Use this class for testing interactions between multiple components. """ diff --git a/builder/builder/patches/refactor_builder_variables.py b/builder/builder/patches/refactor_builder_variables.py index f2b13dc7f..d109c7793 100644 --- a/builder/builder/patches/refactor_builder_variables.py +++ b/builder/builder/patches/refactor_builder_variables.py @@ -3,30 +3,33 @@ import frappe -from builder.builder.doctype.builder_variable.builder_variable import get_css_variables +from builder.builder.doctype.builder_token.builder_token import get_css_variables from builder.utils import camel_case_to_kebab_case UUID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$") def execute(): - """Migrate Builder Variables to UUID names. + """Migrate Builder Tokens to UUID names. - 1. Assign a UUID to every variable that doesn't already have one. + 1. Assign a UUID to every token that doesn't already have one. 2. Rewrite `var(--old-name)` → `var(--)` in page/component blocks. 3. Normalise legacy type "Spacing" → "Dimension". + + Runs after rename_builder_variable_to_builder_token (pre_model_sync), so the + doctype is always Builder Token by the time this executes. """ - if not frappe.db.has_column("Builder Variable", "variable_name"): + if not frappe.db.table_exists("Builder Token"): return - variables = frappe.get_all("Builder Variable", fields=["name", "variable_name"]) - non_uuid = [v for v in variables if not UUID_RE.match(v.name)] + tokens = frappe.get_all("Builder Token", fields=["name", "token_name"]) + non_uuid = [t for t in tokens if not UUID_RE.match(t.name)] if non_uuid: rename_map, css_rewrite_map = build_maps(non_uuid) pages_updated = rewrite_doctype_blocks("Builder Page", ["blocks", "draft_blocks"], css_rewrite_map) components_updated = rewrite_doctype_blocks("Builder Component", ["block"], css_rewrite_map) - renamed = rename_variables(rename_map) + renamed = rename_tokens(rename_map) print( f"refactor_builder_variables: renamed={renamed} " f"pages_updated={pages_updated} components_updated={components_updated}" @@ -36,35 +39,35 @@ def execute(): get_css_variables.clear_cache() -def build_maps(variables): - """Return (rename_map, css_rewrite_map) for the given non-UUID variables.""" +def build_maps(tokens): + """Return (rename_map, css_rewrite_map) for the given non-UUID tokens.""" rename_map = {} css_rewrite_map = {} - for var in variables: + for token in tokens: new_id = str(uuid.uuid4()) - rename_map[var.name] = new_id + rename_map[token.name] = new_id # kebab-case of the display label - if var.variable_name: - css_rewrite_map[camel_case_to_kebab_case(var.variable_name, True)] = new_id + if token.token_name: + css_rewrite_map[camel_case_to_kebab_case(token.token_name, True)] = new_id # snake_case doc names used before this refactor - if "_" in var.name: - css_rewrite_map[var.name.replace("_", "-")] = new_id + if "_" in token.name: + css_rewrite_map[token.name.replace("_", "-")] = new_id # 10-char hex hashes from an earlier refactor - if re.match(r"^[a-f0-9]{10}$", var.name): - css_rewrite_map[var.name] = new_id + if re.match(r"^[a-f0-9]{10}$", token.name): + css_rewrite_map[token.name] = new_id return rename_map, css_rewrite_map -def rename_variables(rename_map): +def rename_tokens(rename_map): renamed = 0 for old_name, new_id in rename_map.items(): try: - frappe.rename_doc("Builder Variable", old_name, new_id, force=True, merge=False) + frappe.rename_doc("Builder Token", old_name, new_id, force=True, merge=False) renamed += 1 except Exception as e: frappe.log_error( @@ -75,7 +78,7 @@ def rename_variables(rename_map): def normalise_type_spacing(): - frappe.db.sql("UPDATE `tabBuilder Variable` SET type='Dimension' WHERE type='Spacing'") + frappe.db.sql("UPDATE `tabBuilder Token` SET type='Dimension' WHERE type='Spacing'") def rewrite_doctype_blocks(doctype, fields, css_rewrite_map): diff --git a/builder/builder/patches/rename_builder_variable_to_builder_token.py b/builder/builder/patches/rename_builder_variable_to_builder_token.py new file mode 100644 index 000000000..5bbe92b02 --- /dev/null +++ b/builder/builder/patches/rename_builder_variable_to_builder_token.py @@ -0,0 +1,36 @@ +import frappe +from frappe.model.utils.rename_field import rename_field + + +def execute(): + """Builder Variable → Builder Token. Only the DocType and the label field are + renamed — token doc names (the CSS `--` handles) are untouched, so every + existing page's var(--id) references keep resolving.""" + # guard on the table, not the DocType row: syncing the old model back (a + # downgrade, or migrating on develop) drops the Builder Token DocType but + # leaves tabBuilder Token behind, and rename_doc can't rename onto it + if frappe.db.table_exists("Builder Token"): + merge_stale_builder_variables() + return + if not frappe.db.exists("DocType", "Builder Variable"): + return + frappe.rename_doc("DocType", "Builder Variable", "Builder Token", force=True) + frappe.reload_doc("builder", "doctype", "builder_token") + rename_field("Builder Token", "variable_name", "token_name") + + +def merge_stale_builder_variables(): + """Both doctypes exist when a site ran the rename and later re-synced the old + model. Keep Builder Token, salvage rows only the old table has, drop the rest.""" + if frappe.db.table_exists("Builder Variable"): + frappe.db.sql( + """INSERT INTO `tabBuilder Token` + (name, creation, modified, modified_by, owner, docstatus, + token_name, type, value, dark_value, is_standard, `group`) + SELECT name, creation, modified, modified_by, owner, docstatus, + variable_name, type, value, dark_value, is_standard, `group` + FROM `tabBuilder Variable` bv + WHERE NOT EXISTS (SELECT 1 FROM `tabBuilder Token` bt WHERE bt.name = bv.name)""" + ) + frappe.delete_doc("DocType", "Builder Variable", ignore_missing=True, force=True) + frappe.db.sql_ddl("DROP TABLE IF EXISTS `tabBuilder Variable`") diff --git a/builder/export_import_standard_page.py b/builder/export_import_standard_page.py index 1671d97c4..805884651 100644 --- a/builder/export_import_standard_page.py +++ b/builder/export_import_standard_page.py @@ -263,7 +263,7 @@ def copy_font_file(file_url, assets_path, target_app="builder"): def export_variables(variables, builder_files_path): - """Export Builder Variable records""" + """Export Builder Token records""" if not variables: return @@ -277,19 +277,19 @@ def export_variables(variables, builder_files_path): # Try to find the variable by name var_docs = frappe.get_all( - "Builder Variable", + "Builder Token", filters=[ - ["variable_name", "in", [var_name, db_name, var_name.replace("-", " ").title()]], + ["token_name", "in", [var_name, db_name, var_name.replace("-", " ").title()]], ], - fields=["name", "variable_name", "type", "value", "dark_value"], + fields=["name", "token_name", "type", "value", "dark_value"], ) if not var_docs: # Also try searching by the scrubbed name var_docs = frappe.get_all( - "Builder Variable", + "Builder Token", filters={"name": db_name}, - fields=["name", "variable_name", "type", "value", "dark_value"], + fields=["name", "token_name", "type", "value", "dark_value"], ) if not var_docs: @@ -298,15 +298,15 @@ def export_variables(variables, builder_files_path): var_doc = var_docs[0] var_config = { - "doctype": "Builder Variable", + "doctype": "Builder Token", "name": var_doc.name, - "variable_name": var_doc.variable_name, + "token_name": var_doc.token_name, "type": var_doc.type, "value": var_doc.value, "dark_value": var_doc.dark_value, } - safe_var_name = frappe.scrub(var_doc.variable_name) + safe_var_name = frappe.scrub(var_doc.token_name) var_dir = os.path.join(variables_path, safe_var_name) os.makedirs(var_dir, exist_ok=True) var_file_path = os.path.join(var_dir, f"{safe_var_name}.json") diff --git a/builder/install.py b/builder/install.py index 19e58352e..2479384d8 100644 --- a/builder/install.py +++ b/builder/install.py @@ -4,7 +4,7 @@ from builder.utils import ( add_composite_index_to_web_page_view, sync_block_templates, - sync_builder_variables, + sync_builder_tokens, sync_page_templates, ) @@ -14,7 +14,7 @@ def after_install(): create_new_folder("Fonts", "Home/Builder Uploads") sync_page_templates() sync_block_templates() - sync_builder_variables() + sync_builder_tokens() add_composite_index_to_web_page_view() sync_standard_builder_pages() @@ -22,7 +22,7 @@ def after_install(): def after_migrate(): sync_page_templates() sync_block_templates() - sync_builder_variables() + sync_builder_tokens() sync_standard_builder_pages() diff --git a/builder/patches.txt b/builder/patches.txt index f1a67c5f9..d3682d700 100644 --- a/builder/patches.txt +++ b/builder/patches.txt @@ -3,6 +3,7 @@ builder.builder.doctype.builder_page.patches.create_upload_folder_for_builder # builder.builder.patches.rename_web_page_beta_to_builder_page builder.builder.patches.rename_web_page_component_to_builder_component execute:frappe.delete_doc("DocType", "Builder Page Library", ignore_missing=True, force=True) +builder.builder.patches.rename_builder_variable_to_builder_token [post_model_sync] builder.builder.doctype.builder_component.patches.set_component_id diff --git a/builder/template_sync.py b/builder/template_sync.py index 3e5095bc5..0a3c8bdde 100644 --- a/builder/template_sync.py +++ b/builder/template_sync.py @@ -1,7 +1,7 @@ """Import/export of builder template groups. A template group is a set of highly-functional pages (landing, contact, ...) -that share one set of Builder Components and Builder Variables (the variable +that share one set of Builder Components and Builder Tokens (the variable `group` matches the template group). Fixtures live on disk at builder/builder/builder_templates//: @@ -263,14 +263,14 @@ def export_template_variables(group, variables_path): """Write fixtures for all variables of the group, pinning their uuid names so var(--) references in blocks survive the round-trip.""" for var in frappe.get_all( - "Builder Variable", + "Builder Token", filters={"group": group}, - fields=["name", "variable_name", "type", "value", "dark_value", "group"], + fields=["name", "token_name", "type", "value", "dark_value", "group"], ): var_config = { - "doctype": "Builder Variable", + "doctype": "Builder Token", "name": var.name, - "variable_name": var.variable_name, + "token_name": var.token_name, "type": var.type, "value": var.value, "dark_value": var.dark_value, diff --git a/builder/templates/generators/webpage.html b/builder/templates/generators/webpage.html index 9c7165688..482c81d7a 100644 --- a/builder/templates/generators/webpage.html +++ b/builder/templates/generators/webpage.html @@ -41,9 +41,9 @@ {% endfor %} {{ style }} {%- if preview -%} - + {%- else -%} - + {%- endif -%} {%- if custom_fonts -%} diff --git a/builder/utils.py b/builder/utils.py index 10fad056d..ee58f263c 100644 --- a/builder/utils.py +++ b/builder/utils.py @@ -11,8 +11,8 @@ import frappe import yaml from frappe.model.document import Document -from frappe.modules.import_file import import_file_by_path -from frappe.utils import get_url +from frappe.modules.import_file import import_doc, import_file_by_path, update_modified +from frappe.utils import get_datetime, get_url from frappe.utils.safe_exec import ( SERVER_SCRIPT_FILE_PREFIX, FrappeTransformer, @@ -318,10 +318,34 @@ def sync_block_templates(): make_records(builder_block_template_path) -def sync_builder_variables(): - print("Syncing Builder Builder Variables") - builder_variable_path = frappe.get_module_path("builder", "builder_variable") - make_records(builder_variable_path) +def sync_builder_tokens(): + print("Syncing Builder Tokens") + builder_token_path = frappe.get_module_path("builder", "builder_token") + make_records(builder_token_path) + + +# Compat alias, external scripts may still call the old name +sync_builder_variables = sync_builder_tokens + + +# Fixture exports and template bundles made before the Builder Token rename still +# say Builder Variable, and carry the pre-rename fieldname +RENAMED_FIXTURE_DOCTYPES = {"Builder Variable": "Builder Token"} +RENAMED_FIXTURE_FIELDS = {"Builder Token": {"variable_name": "token_name"}} + + +def normalize_renamed_doc(docdict): + """Rewrite a doc exported under a doctype's old name so it can be imported. + + A no-op while the old doctype is still around, i.e. before the rename patch runs.""" + new_doctype = RENAMED_FIXTURE_DOCTYPES.get(docdict.get("doctype")) + if not new_doctype or frappe.db.exists("DocType", docdict["doctype"]): + return docdict + docdict["doctype"] = new_doctype + for old_field, new_field in RENAMED_FIXTURE_FIELDS[new_doctype].items(): + if old_field in docdict: + docdict.setdefault(new_field, docdict.pop(old_field)) + return docdict def make_records(path): @@ -329,7 +353,24 @@ def make_records(path): return for fname in os.listdir(path): if os.path.isdir(join(path, fname)) and fname != "__pycache__": - import_file_by_path(f"{path}/{fname}/{fname}.json") + import_fixture_record(f"{path}/{fname}/{fname}.json") + + +def import_fixture_record(fpath): + """import_file_by_path, but tolerant of fixtures exported under a doctype's old name.""" + with open(fpath, encoding="utf-8") as f: + docdict = frappe.parse_json(f.read()) + old_doctype = docdict.get("doctype") + normalize_renamed_doc(docdict) + if docdict.get("doctype") == old_doctype: + import_file_by_path(fpath) + return + db_modified = frappe.db.get_value(docdict["doctype"], docdict.get("name"), "modified") + if db_modified and get_datetime(docdict.get("modified")) <= get_datetime(db_modified): + return + import_doc(docdict) + if docdict.get("modified"): + update_modified(docdict["modified"], docdict) def copy_img_to_asset_folder(block, page_doc, app=None): diff --git a/builder/www/builder_assets/tokens.css b/builder/www/builder_assets/tokens.css new file mode 100644 index 000000000..792b25cb6 --- /dev/null +++ b/builder/www/builder_assets/tokens.css @@ -0,0 +1,3 @@ +{%- if css_variables -%}:root { +{%- for key, value in css_variables.items() %}{%- set dark_value = (dark_mode_css_variables or {}).get(key) %}{{ key }}: {% if dark_value is not none and dark_value != value %}light-dark({{ value }}, {{ dark_value }}){% else %}{{ value }}{% endif %}; +{%- endfor %}}{%- endif -%} \ No newline at end of file diff --git a/builder/www/builder_assets/tokens.py b/builder/www/builder_assets/tokens.py new file mode 100644 index 000000000..eff7554e2 --- /dev/null +++ b/builder/www/builder_assets/tokens.py @@ -0,0 +1,7 @@ +from builder.builder.doctype.builder_token.builder_token import get_css_variables + + +def get_context(context): + css_variables, dark_mode_css_variables = get_css_variables() + context.css_variables = css_variables + context.dark_mode_css_variables = dark_mode_css_variables diff --git a/builder/www/builder_assets/variables.py b/builder/www/builder_assets/variables.py index 87bfe9796..33137ce77 100644 --- a/builder/www/builder_assets/variables.py +++ b/builder/www/builder_assets/variables.py @@ -1,7 +1,5 @@ -from builder.builder.doctype.builder_variable.builder_variable import get_css_variables +# Compat route: pages published before the Builder Token rename link +# /builder_assets/variables.css. Serves the same CSS as tokens.css. +from builder.www.builder_assets.tokens import get_context - -def get_context(context): - css_variables, dark_mode_css_variables = get_css_variables() - context.css_variables = css_variables - context.dark_mode_css_variables = dark_mode_css_variables +__all__ = ["get_context"] diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 363ab1fea..173afbc10 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -76,6 +76,8 @@ declare module 'vue' { EyeDropper: typeof import('./src/components/Icons/EyeDropper.vue')['default'] Files: typeof import('./src/components/Icons/Files.vue')['default'] FitScreen: typeof import('./src/components/Icons/FitScreen.vue')['default'] + FontInput: typeof import('./src/components/Controls/FontInput.vue')['default'] + FontInputActions: typeof import('./src/components/Controls/FontInputActions.vue')['default'] FontUploader: typeof import('./src/components/Controls/FontUploader.vue')['default'] GlobalAI: typeof import('./src/components/Settings/GlobalAI.vue')['default'] GlobalAnalytics: typeof import('./src/components/Settings/GlobalAnalytics.vue')['default'] @@ -100,7 +102,7 @@ declare module 'vue' { MiddleTruncate: typeof import('./src/components/MiddleTruncate.vue')['default'] MoreStylesPanel: typeof import('./src/components/MoreStylesPanel.vue')['default'] NewBlockTemplate: typeof import('./src/components/Modals/NewBlockTemplate.vue')['default'] - NewBuilderVariable: typeof import('./src/components/Modals/NewBuilderVariable.vue')['default'] + NewBuilderToken: typeof import('./src/components/Modals/NewBuilderToken.vue')['default'] NumberArrows: typeof import('./src/components/Controls/NumberArrows.vue')['default'] NumberOptions: typeof import('./src/components/PropsOptions/NumberOptions.vue')['default'] ObjectEditor: typeof import('./src/components/ObjectEditor.vue')['default'] @@ -152,10 +154,10 @@ declare module 'vue' { TemplatesDialog: typeof import('./src/components/Templates/TemplatesDialog.vue')['default'] TextBlock: typeof import('./src/components/TextBlock.vue')['default'] TextBlockBubbleMenu: typeof import('./src/components/TextBlockBubbleMenu.vue')['default'] + TokenManager: typeof import('./src/components/Modals/TokenManager.vue')['default'] TopClicksList: typeof import('./src/components/Settings/TopClicksList.vue')['default'] TopReferrersList: typeof import('./src/components/Settings/TopReferrersList.vue')['default'] TrackingDisabledNotice: typeof import('./src/components/Settings/TrackingDisabledNotice.vue')['default'] - VariableManager: typeof import('./src/components/Modals/VariableManager.vue')['default'] VariantControl: typeof import('./src/components/Controls/VariantControl.vue')['default'] VersionHistory: typeof import('./src/components/VersionHistory.vue')['default'] VisibilityInput: typeof import('./src/components/VisibilityInput.vue')['default'] diff --git a/frontend/src/components/BackgroundHandler.vue b/frontend/src/components/BackgroundHandler.vue index 504c31755..23609ac12 100644 --- a/frontend/src/components/BackgroundHandler.vue +++ b/frontend/src/components/BackgroundHandler.vue @@ -139,12 +139,12 @@ import useBuilderStore from "@/stores/builderStore"; import blockController from "@/utils/blockController"; import { cssUrl } from "@/utils/helpers"; import { getOptimizeButtonText, optimizeImage, shouldShowOptimizeButton } from "@/utils/imageUtils"; -import { useBuilderVariable } from "@/utils/useBuilderVariable"; +import { useBuilderToken } from "@/utils/useBuilderToken"; import { FileUploader, Popover, Switch } from "frappe-ui"; import { computed, defineComponent, h, ref, watch } from "vue"; const builderStore = useBuilderStore(); -const { getVariableName, resolveVariableValue, variables } = useBuilderVariable(); +const { getVariableName, resolveVariableValue, variables } = useBuilderToken(); // wraps Input to style the value like ColorInput does when it displays a variable name const BackgroundInput = defineComponent({ @@ -155,7 +155,7 @@ const BackgroundInput = defineComponent({ const showsVariableName = computed(() => { return ( !!props.modelValue && - variables.value.some((builderVariable) => builderVariable.variable_name === props.modelValue) + variables.value.some((builderToken) => builderToken.token_name === props.modelValue) ); }); return () => diff --git a/frontend/src/components/BlockPropertySections/TypographySection.ts b/frontend/src/components/BlockPropertySections/TypographySection.ts index 74c2d9642..1d53fbc31 100644 --- a/frontend/src/components/BlockPropertySections/TypographySection.ts +++ b/frontend/src/components/BlockPropertySections/TypographySection.ts @@ -1,12 +1,10 @@ import Autocomplete from "@/components/Controls/Autocomplete.vue"; import BasePropertyControl from "@/components/Controls/BasePropertyControl.vue"; -import FontUploader from "@/components/Controls/FontUploader.vue"; +import FontInput from "@/components/Controls/FontInput.vue"; import OptionToggle from "@/components/Controls/OptionToggle.vue"; import StylePropertyControl from "@/components/Controls/StylePropertyControl.vue"; -import userFonts from "@/data/userFonts"; -import { UserFont } from "@/types/doctypes"; import blockController from "@/utils/blockController"; -import { setFont as _setFont, fontListItems, getFontWeightOptions, loadFontList } from "@/utils/fontManager"; +import { setFont as _setFont, getFontWeightOptions, loadFontList } from "@/utils/fontManager"; import { BOX_UNIT_OPTIONS } from "@/utils/unitOptions"; const setFont = (font: string) => { @@ -40,49 +38,8 @@ const typographySectionProperties = [ getProps: () => { return { label: "Family", - component: Autocomplete, + component: FontInput, propertyKey: "fontFamily", - getOptions: async (filterString: string) => { - await loadFontList(); - const fontOptions = [] as { label: string; value: string }[]; - userFonts.data?.forEach((font: UserFont) => { - if (fontOptions.length >= 20) { - return; - } - const fontName = font.font_name as string; - if (fontName.toLowerCase().includes(filterString.toLowerCase()) || !filterString) { - fontOptions.push({ - label: fontName, - value: fontName, - }); - } - }); - if (fontOptions.length) { - fontOptions.unshift({ - label: "Custom", - value: "_separator_1", - }); - fontOptions.push({ - label: "Default", - value: "_separator_2", - }); - } - fontListItems.value.forEach((font) => { - if (fontOptions.length >= 20) { - return; - } - if (font.family.toLowerCase().includes(filterString.toLowerCase()) || !filterString) { - fontOptions.push({ - label: font.family, - value: font.family, - }); - } - }); - return fontOptions; - }, - actionButton: { - component: FontUploader, - }, getModelValue: () => blockController.getFontFamily(), setModelValue: (val: string) => setFont(val), }; diff --git a/frontend/src/components/BuilderCanvas.vue b/frontend/src/components/BuilderCanvas.vue index bef3b334e..1f8c08a46 100644 --- a/frontend/src/components/BuilderCanvas.vue +++ b/frontend/src/components/BuilderCanvas.vue @@ -135,13 +135,25 @@ import { } from "@/utils/scriptSandbox"; import { useBlockEventHandlers } from "@/utils/useBlockEventHandlers"; import { useBlockSelection } from "@/utils/useBlockSelection"; -import { useBuilderVariable } from "@/utils/useBuilderVariable"; +import { setFont } from "@/utils/fontManager"; +import { useBuilderToken } from "@/utils/useBuilderToken"; import { useCanvasDropZone } from "@/utils/useCanvasDropZone"; import { useCanvasEvents } from "@/utils/useCanvasEvents"; import { useCanvasMarqueeSelection } from "@/utils/useCanvasMarqueeSelection"; import { useCanvasUtils } from "@/utils/useCanvasUtils"; import { Tooltip } from "frappe-ui"; -import { Ref, computed, onMounted, onUnmounted, provide, reactive, ref, useId, watch } from "vue"; +import { + Ref, + computed, + onMounted, + onUnmounted, + provide, + reactive, + ref, + useId, + watch, + watchEffect, +} from "vue"; import setPanAndZoom from "../utils/panAndZoom"; import BlockSnapGuides from "./BlockSnapGuides.vue"; import BuilderBlock from "./BuilderBlock.vue"; @@ -153,7 +165,13 @@ const canvasStore = useCanvasStore(); const pageStore = usePageStore(); const canvasId = `builder-canvas-${useId()}`; -const { cssVariables, darkCssVariables } = useBuilderVariable(); +const { cssVariables, darkCssVariables, fontTokens } = useBuilderToken(); + +// Font tokens' families must be loaded for the canvas to render them — blocks +// reference var(--id), which resolves via CSS but never hits a font loader. +watchEffect(() => { + fontTokens.value.forEach((t: any) => setFont(t.value)); +}); const variables = computed(() => { return { diff --git a/frontend/src/components/BuilderLeftPanel.vue b/frontend/src/components/BuilderLeftPanel.vue index 2f41ebfc9..90509571e 100644 --- a/frontend/src/components/BuilderLeftPanel.vue +++ b/frontend/src/components/BuilderLeftPanel.vue @@ -18,7 +18,7 @@ size="md" :variant=" builderStore.leftPanelActiveTab === option.value || - (showVariableManager && option.value === 'variables') + (showTokenManager && option.value === 'variables') ? 'subtle' : 'ghost' " @@ -77,13 +77,13 @@ - + diff --git a/frontend/src/components/Controls/FontInputActions.vue b/frontend/src/components/Controls/FontInputActions.vue new file mode 100644 index 000000000..6636e1096 --- /dev/null +++ b/frontend/src/components/Controls/FontInputActions.vue @@ -0,0 +1,44 @@ + + + diff --git a/frontend/src/components/Modals/NewBuilderVariable.vue b/frontend/src/components/Modals/NewBuilderToken.vue similarity index 60% rename from frontend/src/components/Modals/NewBuilderVariable.vue rename to frontend/src/components/Modals/NewBuilderToken.vue index d359c1935..38531408e 100644 --- a/frontend/src/components/Modals/NewBuilderVariable.vue +++ b/frontend/src/components/Modals/NewBuilderToken.vue @@ -2,7 +2,7 @@ -
+
Light Mode Color
Dark Mode Color
@@ -47,32 +47,32 @@ diff --git a/frontend/src/components/Modals/VariableManager.vue b/frontend/src/components/Modals/TokenManager.vue similarity index 72% rename from frontend/src/components/Modals/VariableManager.vue rename to frontend/src/components/Modals/TokenManager.vue index f2719cc34..c03be82c4 100644 --- a/frontend/src/components/Modals/VariableManager.vue +++ b/frontend/src/components/Modals/TokenManager.vue @@ -12,19 +12,25 @@ v-if="modelValue" :placement-offset-top="8" :placement-offset-left="65" - action-label="Add Variable" + action-label="Add Token" :action-handler="addNewVariable" placement="top-left"> - +