diff --git a/builder/builder/doctype/builder_page/builder_page.json b/builder/builder/doctype/builder_page/builder_page.json
index e1cf3b7a0..5bc114806 100644
--- a/builder/builder/doctype/builder_page/builder_page.json
+++ b/builder/builder/doctype/builder_page/builder_page.json
@@ -21,11 +21,14 @@
"section_break_ujsp",
"blocks",
"draft_blocks",
+ "tiles",
+ "draft_tiles",
"scripting_tab",
"page_data_script",
"head_html",
"body_html",
"client_scripts",
+ "include_alpinejs",
"settings_tab",
"section_break_shab",
"preview",
@@ -242,11 +245,32 @@
"label": "App",
"mandatory_depends_on": "is_standard",
"no_copy": 1
+ },
+ {
+ "fieldname": "tiles",
+ "fieldtype": "Table",
+ "label": "Tiles",
+ "options": "Builder Tile",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "description": "AlpineJS is included by default in pages with Tiles.",
+ "fieldname": "include_alpinejs",
+ "fieldtype": "Check",
+ "label": "Include AlpineJS"
+ },
+ {
+ "fieldname": "draft_tiles",
+ "fieldtype": "Table",
+ "label": "Draft Tiles",
+ "options": "Builder Tile",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2026-01-05 16:23:52.605250",
+ "modified": "2026-04-15 21:28:34.013846",
"modified_by": "Administrator",
"module": "Builder",
"name": "Builder Page",
diff --git a/builder/builder/doctype/builder_page/builder_page.py b/builder/builder/doctype/builder_page/builder_page.py
index dc440073d..73fe52dfc 100644
--- a/builder/builder/doctype/builder_page/builder_page.py
+++ b/builder/builder/doctype/builder_page/builder_page.py
@@ -100,6 +100,7 @@ class BuilderPage(WebsiteGenerator):
from builder.builder.doctype.builder_page_client_script.builder_page_client_script import (
BuilderPageClientScript,
)
+ from builder.builder.doctype.builder_tile.builder_tile import BuilderTile
app: DF.Literal[None]
authenticated_access: DF.Check
@@ -109,9 +110,11 @@ class BuilderPage(WebsiteGenerator):
client_scripts: DF.TableMultiSelect[BuilderPageClientScript]
disable_indexing: DF.Check
draft_blocks: DF.LongText | None
+ draft_tiles: DF.Table[BuilderTile]
dynamic_route: DF.Check
favicon: DF.AttachImage | None
head_html: DF.Code | None
+ include_alpinejs: DF.Check
is_standard: DF.Check
is_template: DF.Check
language: DF.Data | None
@@ -124,6 +127,7 @@ class BuilderPage(WebsiteGenerator):
project_folder: DF.Link | None
published: DF.Check
route: DF.Data | None
+ tiles: DF.Table[BuilderTile]
# end: auto-generated types
def onload(self):
@@ -225,6 +229,7 @@ def publish(self):
if self.draft_blocks:
self.blocks = self.draft_blocks
self.draft_blocks = None
+ self.draft_tiles = []
self.save()
frappe.enqueue_doc(
self.doctype,
@@ -241,9 +246,49 @@ def unpublish(self):
self.published = 0
self.save()
+ @frappe.whitelist()
+ def save_tiles(self, for_draft: bool = False):
+ _, _, _, _, tiles = get_block_html((self.draft_blocks if for_draft else self.blocks) or "[]")
+ if not for_draft:
+ self.tiles = []
+ for tile_id, tile_block in tiles.items():
+ tile_doc = self.append("tiles", {})
+ tile_doc.block_id = tile_id
+ tile_doc.blocks = frappe.as_json(tile_block)
+ else:
+ self.draft_tiles = []
+ for tile_id, tile_block in tiles.items():
+ tile_doc = self.append("draft_tiles", {})
+ tile_doc.block_id = tile_id
+ tile_doc.blocks = frappe.as_json(tile_block)
+ self.save()
+
def get_context(self, context):
- # delete default favicon
- del context.favicon
+ # Handle tile partial-rendering requests
+
+ if frappe.form_dict and frappe.form_dict.get("render_tile"):
+ context["template"] = "templates/generators/tile.html"
+
+ tiles = self.draft_tiles if getattr(frappe.local.request, "for_preview", None) else self.tiles
+
+ block_id = frappe.form_dict.get("block_id")
+ block_data = frappe.parse_json(frappe.form_dict.get("block_data")) or {}
+ props = frappe.form_dict.get("props") or {}
+
+ for tile in tiles:
+ if tile.block_id == block_id:
+ content, _, _, _, _ = get_block_html(tile.blocks, for_tile=True)
+ context.__content = content
+ context.update({"block_data": block_data, "props": props, "has_tile": True})
+ context["__content"] = render_template(context.__content, context)
+ break
+ else:
+ frappe.throw(f"Tile with block_id {block_id} not found", frappe.DoesNotExistError)
+ return
+
+ # Handle normal page rendering
+
+ del context.favicon # delete default favicon
context.disable_indexing = self.disable_indexing
page_data = self.get_page_data()
if page_data.get("title"):
@@ -255,7 +300,7 @@ def get_context(self, context):
if context.preview and self.draft_blocks:
blocks = self.draft_blocks
- content, style, fonts, has_block_script = get_block_html(blocks)
+ content, style, fonts, has_block_script, tiles = get_block_html(blocks)
if self.dynamic_route or page_data or has_block_script:
context.no_cache = 1
@@ -264,6 +309,7 @@ def get_context(self, context):
context.fonts = fonts
context.__content = content
context.style = render_template(style, page_data)
+ context.has_tile = len(tiles) > 0
context.editor_link = f"/{builder_path}/page/{self.name}"
if frappe.form_dict and self.dynamic_route:
query_string = "&".join(
@@ -281,6 +327,8 @@ def get_context(self, context):
else:
context.base_url = frappe.utils.get_url(self.route)
+ context.include_alpinejs = self.include_alpinejs
+
context.update(page_data)
self.set_style_and_script(context)
@@ -512,12 +560,13 @@ def get_block_data(
return block_data
-def get_block_html(blocks: str | list) -> tuple[str, str, dict, bool]:
+def get_block_html(blocks: str | list, for_tile: bool = False) -> tuple[str, str, dict, bool]:
"""
Main entry point for converting blocks to HTML.
#### Args:
blocks: JSON string or list of block dictionaries
+ for_tile: Whether to render blocks for a tile
#### Returns:
Tuple of (`html_content`, `css_styles`, `font_map`, `has_block_script`)
@@ -539,6 +588,7 @@ def get_block_html(blocks: str | list) -> tuple[str, str, dict, bool]:
"standard_props_stack": {}, # prop_name -> list of prop_info
"global_script_tag": soup.new_tag("script"),
"used_block_scripts": set(),
+ "tile_blocks": {}, # block_id -> block json
}
html_parts = []
@@ -554,22 +604,36 @@ def get_block_html(blocks: str | list) -> tuple[str, str, dict, bool]:
# Add global script to the top
tag.insert(0, shared_state["global_script_tag"])
- html = wrap_html_with_context(str(tag), block_context)
+ html = wrap_html_with_context(str(tag), block_context, for_tile, block.get("blockId", ""))
# Write html to a file for debugging
with open("output.html", "w") as f:
f.write(html)
html_parts.append(html)
- return "".join(html_parts), str(style_tag), font_map, shared_state["has_block_script"]
+ return (
+ "".join(html_parts),
+ str(style_tag),
+ font_map,
+ shared_state["has_block_script"],
+ shared_state["tile_blocks"],
+ )
-def build_tag(block: dict, state: dict, data_key: dict | None = None) -> bs.Tag:
+def build_tag(
+ block: dict,
+ state: dict,
+ data_key: dict | None = None,
+) -> bs.Tag:
"""
Transforms a single block to an HTML tag.
#### Returns:
BeautifulSoup tag element
"""
+ is_tile = block.get("isTile", False)
+
+ if is_tile:
+ state["tile_blocks"][block.get("blockId")] = copy.deepcopy(block)
props = process_block_props(block, data_key, state["standard_props_stack"])
@@ -577,12 +641,17 @@ def build_tag(block: dict, state: dict, data_key: dict | None = None) -> bs.Tag:
tag = create_html_tag(block, state)
+ if is_tile:
+ tag["data-tile-id"] = block.get("blockId")
+
if is_repeater_block(block):
render_repeater_children(tag, block, data_key, state)
else:
render_children(tag, block, data_key, state)
attach_client_script(tag, block, state)
+ if is_tile:
+ attach_client_data(tag, block, state)
# Add body scripts for body element
effective_element = block.get("originalElement") or block.get("element")
@@ -605,6 +674,7 @@ def get_block_context(block: dict, props: dict) -> dict:
passed_down_props = {name: info["value"] for name, info in props.items() if info["is_passed_down"]}
return {
+ "block_id": block.get("blockId"),
"all_props": all_props,
"passed_down_props": passed_down_props,
"block_data_script": block.get("blockDataScript"),
@@ -675,7 +745,7 @@ def get_dynamic_props_template(
) -> str:
"""Get a Jinja template reference for dynamic properties."""
if comes_from == "blockDataScript":
- key = jinja_safe_key(f"block.{prop_value}")
+ key = jinja_safe_key(f"block_data.{prop_value}")
elif comes_from == "props":
key = jinja_safe_key(f"props.{prop_value}")
else: # dataScript
@@ -764,7 +834,7 @@ def build_tag_classes(block: dict, state: dict) -> list[str]:
def generate_and_apply_styles(block: dict, state: dict) -> str:
"""Generate a unique style class and append all styles to the style tag."""
- style_class = f"fb-{frappe.generate_hash(length=8)}"
+ style_class = f"fb-{block.get('blockId')}"
style_tag = state["style_tag"]
font_map = state["font_map"]
@@ -883,8 +953,8 @@ def get_loop_info(block: dict, data_key: dict | None, props_stack: dict) -> dict
elif comes_from == "blockDataScript":
return {
- "loop_var": "block",
- "iterator_key": jinja_safe_key(f"block.{iterator_key}"),
+ "loop_var": "block_data",
+ "iterator_key": jinja_safe_key(f"block_data.{iterator_key}"),
"data_key": data_key,
}
@@ -941,13 +1011,23 @@ def get_visibility_condition_key(block: dict, data_key: dict | None) -> str | No
if comes_from == "props":
return jinja_safe_key(f"props.{key}")
elif comes_from == "blockDataScript":
- return jinja_safe_key(f"block.{key}")
+ return jinja_safe_key(f"block_data.{key}")
else: # dataScript
if data_key:
return f"{extract_data_key(data_key)}.{key}"
return key
+def attach_client_data(tag: bs.Tag, block: dict, state: dict):
+ tag.attrs["x-data"] = f"block('{block.get('blockId')}')"
+ script_tag = state["soup"].new_tag("script", type="application/json")
+ script_tag.string = (
+ '{ "block_data": {{ block_data | to_safe_json }}, "props": {{ props | to_safe_json }} }'
+ )
+ script_tag.attrs["data-block-data"] = block.get("blockId")
+ tag.append(script_tag)
+
+
def attach_client_script(tag: bs.Tag, block: dict, state: dict):
"""Attach client-side JavaScript to the block."""
script = block.get("blockClientScript")
@@ -971,21 +1051,33 @@ def attach_client_script(tag: bs.Tag, block: dict, state: dict):
# Add local script to call the function
local_script = state["soup"].new_tag("script")
- local_script.string = (
+ local_script['defer'] = True
+ script_content = (
+ f"const element = document.querySelector('[data-block-uid=\"{{{{ unique_hash }}}}\"]');"
f"(client_script_{script_unique_id}).call("
- f"document.querySelector('[data-block-uid=\"{{{{ unique_hash }}}}\"]'), "
- f"{{{{ props | to_safe_json }}}}, "
- f"{{{{ block.block_data | to_safe_json }}}}"
+ f" element, "
+ f" {{{{ props | to_safe_json }}}}, "
+ f" {{{{ {jinja_safe_key('block_data.block_data')} | to_safe_json }}}}"
f");"
)
- tag.append(local_script)
+ local_script.string = (
+ f"{{% if has_tile or include_alpinejs %}}"
+ f"function exec() {{ {script_content} }}"
+ f"if (window.Alpine) {{ exec() }} else {{ document.addEventListener('alpine:initialized', exec) }}"
+ f"{{% else %}}"
+ f"{{ {script_content} }}"
+ f"{{% endif %}}"
+ )
+ tag.insert(0, local_script)
def append_child_with_context(parent: bs.Tag, child: bs.Tag, context: dict):
"""Append child tag with proper Jinja context wrapping."""
# Generate unique hash for this block instance
# This is unique for each block irrespective of loops or components
- parent.append("{% with unique_hash = (loop.index if loop is defined else 0) | hash %}")
+ parent.append(
+ f"{{% with unique_hash = '{context.get('block_id', '')}' ~ '_' ~ (loop.index if loop is defined else 0) %}}"
+ )
if context.get("default_props"):
props_str = ", ".join([f"'{var}': {var}" for var in context["default_props"]])
@@ -1000,7 +1092,9 @@ def append_child_with_context(parent: bs.Tag, child: bs.Tag, context: dict):
if context.get("block_data_script"):
escaped_script = escape_single_quotes(context["block_data_script"])
- parent.append(f"{{% with block = block | execute_script_and_combine('{escaped_script}', props) %}}")
+ parent.append(
+ f"{{% with block_data = block_data | execute_block_data_script('{escaped_script}', props) %}}"
+ )
if context.get("visibility_key"):
parent.append(f"{{% if {context['visibility_key']} %}}")
@@ -1070,7 +1164,7 @@ def get_dynamic_value_key(dynamic_value_doc: dict, original_key: str, data_key:
if comes_from == "props":
return jinja_safe_key(f"props.{original_key}")
elif comes_from == "blockDataScript":
- return jinja_safe_key(f"block.{original_key}")
+ return jinja_safe_key(f"block_data.{original_key}")
else: # dataScript
key = dynamic_value_doc.get("key")
if data_key:
@@ -1079,7 +1173,7 @@ def get_dynamic_value_key(dynamic_value_doc: dict, original_key: str, data_key:
return key
-def wrap_html_with_context(html: str, context: dict) -> str:
+def wrap_html_with_context(html: str, context: dict, for_tile: bool = False, tile_block_id: str = "") -> str:
"""
Wrap HTML with Jinja context variables.
@@ -1091,14 +1185,17 @@ def wrap_html_with_context(html: str, context: dict) -> str:
script_escaped = escape_single_quotes(context.get("block_data_script") or "")
html = (
- f"{{% with block = {{}} | execute_script_and_combine('{script_escaped}', {all_props_literal}) %}}"
+ f"{{% with block_data = {'block_data' if for_tile else '{}'} %}}"
+ f"{{% with block_data = block_data | execute_block_data_script('{script_escaped}', {all_props_literal}) %}}"
f"{html}"
f"{{% endwith %}}"
+ f"{{% endwith %}}"
)
# Set props contexts
html = f"{{% with props = {all_props_literal} %}}{html}{{% endwith %}}"
html = f"{{% with passed_down_props = {passed_down_literal} %}}{html}{{% endwith %}}"
+ html = f"{{% with unique_hash = '{tile_block_id if for_tile else 'root'}' %}}{html}{{% endwith %}}"
return html
diff --git a/builder/builder/doctype/builder_tile/__init__.py b/builder/builder/doctype/builder_tile/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/builder/builder/doctype/builder_tile/builder_tile.json b/builder/builder/doctype/builder_tile/builder_tile.json
new file mode 100644
index 000000000..9ed1705da
--- /dev/null
+++ b/builder/builder/doctype/builder_tile/builder_tile.json
@@ -0,0 +1,45 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2026-04-15 09:48:15.518085",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "block_id",
+ "blocks"
+ ],
+ "fields": [
+ {
+ "fieldname": "block_id",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Block Id",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "blocks",
+ "fieldtype": "Long Text",
+ "in_list_view": 1,
+ "label": "Blocks",
+ "read_only": 1,
+ "reqd": 1
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2026-04-15 09:48:15.518085",
+ "modified_by": "Administrator",
+ "module": "Builder",
+ "name": "Builder Tile",
+ "owner": "Administrator",
+ "permissions": [],
+ "row_format": "Dynamic",
+ "rows_threshold_for_grid_search": 20,
+ "sort_field": "creation",
+ "sort_order": "DESC",
+ "states": []
+}
diff --git a/builder/builder/doctype/builder_tile/builder_tile.py b/builder/builder/doctype/builder_tile/builder_tile.py
new file mode 100644
index 000000000..585c0719e
--- /dev/null
+++ b/builder/builder/doctype/builder_tile/builder_tile.py
@@ -0,0 +1,24 @@
+# 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 BuilderTile(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
+
+ block_id: DF.Data
+ blocks: DF.LongText
+ parent: DF.Data
+ parentfield: DF.Data
+ parenttype: DF.Data
+ # end: auto-generated types
+
+ pass
diff --git a/builder/hooks.py b/builder/hooks.py
index 49c80bdce..f2149e34a 100644
--- a/builder/hooks.py
+++ b/builder/hooks.py
@@ -57,7 +57,7 @@
jinja = {
"filters": [
"builder.utils.combine",
- "builder.utils.execute_script_and_combine",
+ "builder.utils.execute_block_data_script",
"builder.utils.hash",
"builder.utils.to_safe_json",
],
diff --git a/builder/templates/generators/tile.html b/builder/templates/generators/tile.html
new file mode 100644
index 000000000..08c9799ad
--- /dev/null
+++ b/builder/templates/generators/tile.html
@@ -0,0 +1,3 @@
+{% block content %}
+ {{ __content }}
+{% endblock %}
\ No newline at end of file
diff --git a/builder/templates/generators/webpage_scripts.html b/builder/templates/generators/webpage_scripts.html
index 886439125..daf3b55ab 100644
--- a/builder/templates/generators/webpage_scripts.html
+++ b/builder/templates/generators/webpage_scripts.html
@@ -1,4 +1,14 @@
{% block script %}
+
+{% if has_tile or include_alpinejs %}
+
+
+
+{% endif %}
{%- if scripts -%}
{% for script_path in scripts %}
@@ -10,7 +20,7 @@
{% if enable_view_tracking and not preview %}
{% endif %}
{% if not preview %}
diff --git a/builder/utils.py b/builder/utils.py
index 8d3ed8efd..eafafa561 100644
--- a/builder/utils.py
+++ b/builder/utils.py
@@ -657,7 +657,7 @@ def get_export_paths(app_path, export_name):
}
-def to_dict_with_fallback(obj):
+def safe_dict_conversion(obj):
try:
return frappe._dict(obj)
except TypeError:
@@ -672,23 +672,23 @@ def combine(a, b):
return b
if b is None:
return a
- res = to_dict_with_fallback(a)
- res.update(to_dict_with_fallback(b))
+ res = safe_dict_conversion(a)
+ res.update(safe_dict_conversion(b))
return res
def hash(s):
- return f"{frappe.generate_hash(length=6)}-{s}"
+ return f"{frappe.generate_hash(length=6)}_{s}"
def to_safe_json(data):
return frappe.as_json(data or {})
-def execute_script_and_combine(prev_block_data, block_data_script, props):
+def execute_block_data_script(prev_block_data, block_data_script, props):
props = frappe._dict(frappe.parse_json(props or "{}"))
block_data = frappe._dict()
- _locals = dict(block=to_dict_with_fallback(prev_block_data or {}), props=props)
+ _locals = dict(block=safe_dict_conversion(prev_block_data or {}), props=props)
execute_script(unescape_html(block_data_script), _locals, "sample")
block_data.update(_locals["block"])
return block_data
diff --git a/frontend/src/block.ts b/frontend/src/block.ts
index 2914a0bf8..0c45f869e 100644
--- a/frontend/src/block.ts
+++ b/frontend/src/block.ts
@@ -73,6 +73,7 @@ class Block implements BlockOptions {
// @ts-expect-error
referenceComponent: Block | null;
customAttributes: BlockAttributeMap;
+ isTile: boolean;
constructor(options: BlockOptions) {
const componentStore = useComponentStore();
this.element = options.element;
@@ -82,6 +83,7 @@ class Block implements BlockOptions {
this.isChildOfComponent = options.isChildOfComponent;
this.referenceBlockId = options.referenceBlockId;
this.parentBlock = options.parentBlock || null;
+ this.isTile = Boolean(options.isTile);
if (this.extendedFromComponent) {
componentStore.loadComponent(this.extendedFromComponent);
}
@@ -1026,6 +1028,15 @@ class Block implements BlockOptions {
setBlockProps(props: BlockProps) {
this.props = props;
}
+ getIsTile(): boolean {
+ if (this.isExtendedFromComponent()) {
+ return !!this.referenceComponent?.isTile;
+ }
+ return this.isTile;
+ }
+ setIsTile(val: boolean){
+ this.isTile = val;
+ }
}
function extendWithComponent(
diff --git a/frontend/src/builder.d.ts b/frontend/src/builder.d.ts
index 9f9da6007..f30f2ab22 100644
--- a/frontend/src/builder.d.ts
+++ b/frontend/src/builder.d.ts
@@ -48,6 +48,7 @@ declare interface BlockOptions {
children?: Array;
dynamicValues?: Array;
draggable?: boolean;
+ isTile?: boolean;
[key: string]: any;
}
diff --git a/frontend/src/components/BlockContextMenu.vue b/frontend/src/components/BlockContextMenu.vue
index 69a1f7a9d..6f9fa3406 100644
--- a/frontend/src/components/BlockContextMenu.vue
+++ b/frontend/src/components/BlockContextMenu.vue
@@ -13,6 +13,7 @@ import NewComponent from "@/components/Modals/NewComponent.vue";
import useBuilderStore from "@/stores/builderStore";
import useCanvasStore from "@/stores/canvasStore";
import useComponentStore from "@/stores/componentStore";
+import blockController from "@/utils/blockController";
import getBlockTemplate from "@/utils/blockTemplate";
import { confirm, detachBlockFromComponent, getBlockCopy, triggerCopyEvent } from "@/utils/helpers";
import { useStorage } from "@vueuse/core";
@@ -226,6 +227,26 @@ const contextMenuOptions: ContextMenuOption[] = [
condition: () => !block.value.isExtendedFromComponent() && Boolean(window.is_developer_mode),
disabled: () => builderStore.readOnlyMode,
},
+ {
+ label: "Convert to Tile",
+ condition: () =>
+ !blockController.multipleBlocksSelected() &&
+ !blockController.isRoot() &&
+ !blockController.getSelectedBlocks()[0].getIsTile(),
+ action: () => {
+ blockController.getSelectedBlocks()[0].setIsTile(true);
+ },
+ },
+ {
+ label: "Revert to Block",
+ condition: () =>
+ !blockController.multipleBlocksSelected() &&
+ !blockController.isRoot() &&
+ blockController.getSelectedBlocks()[0].getIsTile(),
+ action: () => {
+ blockController.getSelectedBlocks()[0].setIsTile(false);
+ },
+ },
{
label: "Save As Component",
action: () => (showNewComponentDialog.value = true),
diff --git a/frontend/src/components/BlockLayers.vue b/frontend/src/components/BlockLayers.vue
index 3d981ce0c..a3b100c91 100644
--- a/frontend/src/components/BlockLayers.vue
+++ b/frontend/src/components/BlockLayers.vue
@@ -55,7 +55,7 @@
'text-purple-500 opacity-80 dark:opacity-100 dark:brightness-125 dark:saturate-[0.3]':
element.isExtendedFromComponent(),
}"
- v-if="!Boolean(element.extendedFromComponent)" />
+ v-if="!Boolean(element.extendedFromComponent) && !element.getIsTile()" />
+
{
@@ -125,6 +134,8 @@ import { ref, watch } from "vue";
import draggable from "vuedraggable";
import BlockLayers from "./BlockLayers.vue";
import BlocksIcon from "./Icons/Blocks.vue";
+// @ts-ignore
+import TilesIcon from "~icons/lucide/blocks";
type LayerInstance = InstanceType;
diff --git a/frontend/src/components/Settings/PageCode.vue b/frontend/src/components/Settings/PageCode.vue
index 71ce45e83..29be5f094 100644
--- a/frontend/src/components/Settings/PageCode.vue
+++ b/frontend/src/components/Settings/PageCode.vue
@@ -20,6 +20,16 @@
class="shrink-0"
@update:modelValue="pageStore.updateActivePage('body_html', $event)"
:showLineNumbers="true">
+ {
+ pageStore.updateActivePage('include_alpinejs', val);
+ }
+ " />