syntaxmatrix 2.6.4.4__py3-none-any.whl → 3.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- syntaxmatrix/__init__.py +6 -4
- syntaxmatrix/agentic/agents.py +206 -26
- syntaxmatrix/agentic/agents_orchestrer.py +16 -10
- syntaxmatrix/client_docs.py +237 -0
- syntaxmatrix/commentary.py +96 -25
- syntaxmatrix/core.py +142 -56
- syntaxmatrix/dataset_preprocessing.py +2 -2
- syntaxmatrix/db.py +0 -17
- syntaxmatrix/kernel_manager.py +174 -150
- syntaxmatrix/page_builder_generation.py +656 -63
- syntaxmatrix/page_layout_contract.py +25 -3
- syntaxmatrix/page_patch_publish.py +368 -15
- syntaxmatrix/plugins/__init__.py +0 -0
- syntaxmatrix/premium/__init__.py +10 -2
- syntaxmatrix/premium/catalogue/__init__.py +121 -0
- syntaxmatrix/premium/gate.py +15 -3
- syntaxmatrix/premium/state.py +507 -0
- syntaxmatrix/premium/verify.py +222 -0
- syntaxmatrix/profiles.py +1 -1
- syntaxmatrix/routes.py +9847 -8004
- syntaxmatrix/settings/model_map.py +50 -65
- syntaxmatrix/settings/prompts.py +1186 -414
- syntaxmatrix/settings/string_navbar.py +4 -4
- syntaxmatrix/static/icons/bot_icon.png +0 -0
- syntaxmatrix/static/icons/bot_icon2.png +0 -0
- syntaxmatrix/templates/admin_billing.html +408 -0
- syntaxmatrix/templates/admin_branding.html +65 -2
- syntaxmatrix/templates/admin_features.html +54 -0
- syntaxmatrix/templates/dashboard.html +285 -8
- syntaxmatrix/templates/edit_page.html +199 -18
- syntaxmatrix/themes.py +17 -17
- syntaxmatrix/workspace_db.py +0 -23
- syntaxmatrix-3.0.1.dist-info/METADATA +219 -0
- {syntaxmatrix-2.6.4.4.dist-info → syntaxmatrix-3.0.1.dist-info}/RECORD +38 -33
- {syntaxmatrix-2.6.4.4.dist-info → syntaxmatrix-3.0.1.dist-info}/WHEEL +1 -1
- syntaxmatrix/settings/default.yaml +0 -13
- syntaxmatrix-2.6.4.4.dist-info/METADATA +0 -539
- syntaxmatrix-2.6.4.4.dist-info/licenses/LICENSE.txt +0 -21
- /syntaxmatrix/{plugin_manager.py → plugins/plugin_manager.py} +0 -0
- /syntaxmatrix/static/icons/{logo3.png → logo2.png} +0 -0
- {syntaxmatrix-2.6.4.4.dist-info → syntaxmatrix-3.0.1.dist-info}/top_level.txt +0 -0
|
@@ -5,13 +5,12 @@ import io
|
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
7
|
from typing import Any, Dict, List, Optional, Tuple
|
|
8
|
-
|
|
9
8
|
import requests
|
|
10
|
-
from PIL import Image
|
|
11
9
|
from bs4 import BeautifulSoup
|
|
10
|
+
from PIL import Image
|
|
12
11
|
|
|
13
|
-
PIXABAY_API_URL = "https://pixabay.com/api/"
|
|
14
12
|
|
|
13
|
+
PIXABAY_API_URL = "https://pixabay.com/api/"
|
|
15
14
|
|
|
16
15
|
# ─────────────────────────────────────────────────────────
|
|
17
16
|
# Icons (inline SVG)
|
|
@@ -373,9 +372,21 @@ def fill_layout_images_from_pixabay(
|
|
|
373
372
|
|
|
374
373
|
return layout
|
|
375
374
|
|
|
375
|
+
def _make_thumb(full_path: str, thumb_path: str, max_w: int = 640, max_h: int = 420) -> bool:
|
|
376
|
+
"""
|
|
377
|
+
Create a JPEG/PNG thumbnail that fits within max_w x max_h, preserving aspect ratio.
|
|
378
|
+
Returns True if created.
|
|
379
|
+
"""
|
|
380
|
+
try:
|
|
381
|
+
os.makedirs(os.path.dirname(thumb_path), exist_ok=True)
|
|
382
|
+
with Image.open(full_path) as im:
|
|
383
|
+
im = im.convert("RGB") if im.mode in ("P", "RGBA") else im
|
|
384
|
+
im.thumbnail((max_w, max_h))
|
|
385
|
+
im.save(thumb_path, quality=82, optimize=True)
|
|
386
|
+
return True
|
|
387
|
+
except Exception:
|
|
388
|
+
return False
|
|
376
389
|
|
|
377
|
-
import re
|
|
378
|
-
from typing import Dict, Any, Optional
|
|
379
390
|
|
|
380
391
|
def _extract_hero_image_url_from_layout(layout: Dict[str, Any]) -> str:
|
|
381
392
|
"""Find hero image URL from the saved layout JSON (builder)."""
|
|
@@ -710,34 +721,135 @@ def _theme_style_from_layout(layout: Dict[str, Any]) -> str:
|
|
|
710
721
|
# ─────────────────────────────────────────────────────────
|
|
711
722
|
# Compile layout JSON → modern HTML with animations
|
|
712
723
|
# ─────────────────────────────────────────────────────────
|
|
724
|
+
_ALLOWED_RICH_TAGS = {
|
|
725
|
+
"p", "br", "strong", "b", "em", "i", "u",
|
|
726
|
+
"ul", "ol", "li", "hr",
|
|
727
|
+
"span", "div",
|
|
728
|
+
"a",
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
_ALLOWED_ATTRS = {
|
|
732
|
+
"span": {"style"},
|
|
733
|
+
"div": {"style"},
|
|
734
|
+
"p": {"style"},
|
|
735
|
+
"a": {"href", "target", "rel"},
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
_ALLOWED_STYLE_PROPS = {
|
|
739
|
+
"color",
|
|
740
|
+
"background-color",
|
|
741
|
+
"font-size",
|
|
742
|
+
"font-weight",
|
|
743
|
+
"font-style",
|
|
744
|
+
"text-decoration",
|
|
745
|
+
"text-align",
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
_COLOR_RE = re.compile(r"^(#[0-9a-fA-F]{3,8}|rgb\([^\)]*\)|rgba\([^\)]*\)|hsl\([^\)]*\)|hsla\([^\)]*\))$")
|
|
749
|
+
_SIZE_RE = re.compile(r"^\d+(\.\d+)?(px|rem|em|%)$")
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _sanitize_style(style: str) -> str:
|
|
753
|
+
if not style:
|
|
754
|
+
return ""
|
|
755
|
+
out = []
|
|
756
|
+
# split "a:b; c:d"
|
|
757
|
+
for part in style.split(";"):
|
|
758
|
+
if ":" not in part:
|
|
759
|
+
continue
|
|
760
|
+
k, v = part.split(":", 1)
|
|
761
|
+
k = k.strip().lower()
|
|
762
|
+
v = v.strip()
|
|
763
|
+
if k not in _ALLOWED_STYLE_PROPS:
|
|
764
|
+
continue
|
|
765
|
+
|
|
766
|
+
if k in ("color", "background-color"):
|
|
767
|
+
if not _COLOR_RE.match(v):
|
|
768
|
+
continue
|
|
769
|
+
|
|
770
|
+
if k == "font-size":
|
|
771
|
+
if not _SIZE_RE.match(v):
|
|
772
|
+
continue
|
|
773
|
+
|
|
774
|
+
if k == "text-align":
|
|
775
|
+
vv = v.lower()
|
|
776
|
+
if vv not in ("left", "center", "right", "justify"):
|
|
777
|
+
continue
|
|
778
|
+
v = vv
|
|
779
|
+
|
|
780
|
+
out.append(f"{k}:{v}")
|
|
781
|
+
return ";".join(out)
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def _sanitize_rich_html(html: str) -> str:
|
|
785
|
+
if not html:
|
|
786
|
+
return ""
|
|
787
|
+
|
|
788
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
789
|
+
|
|
790
|
+
# hard remove scripts/styles
|
|
791
|
+
for bad in soup(["script", "style"]):
|
|
792
|
+
bad.decompose()
|
|
793
|
+
|
|
794
|
+
for tag in list(soup.find_all(True)):
|
|
795
|
+
name = (tag.name or "").lower()
|
|
796
|
+
|
|
797
|
+
if name not in _ALLOWED_RICH_TAGS:
|
|
798
|
+
tag.unwrap()
|
|
799
|
+
continue
|
|
800
|
+
|
|
801
|
+
# strip attrs
|
|
802
|
+
allowed = _ALLOWED_ATTRS.get(name, set())
|
|
803
|
+
attrs = dict(tag.attrs) if tag.attrs else {}
|
|
804
|
+
for a in list(attrs.keys()):
|
|
805
|
+
if a not in allowed:
|
|
806
|
+
tag.attrs.pop(a, None)
|
|
807
|
+
|
|
808
|
+
# style sanitise
|
|
809
|
+
if "style" in tag.attrs:
|
|
810
|
+
s = _sanitize_style(tag.get("style") or "")
|
|
811
|
+
if s:
|
|
812
|
+
tag["style"] = s
|
|
813
|
+
else:
|
|
814
|
+
tag.attrs.pop("style", None)
|
|
815
|
+
|
|
816
|
+
# link sanitise
|
|
817
|
+
if name == "a":
|
|
818
|
+
href = (tag.get("href") or "").strip()
|
|
819
|
+
low = href.lower()
|
|
820
|
+
if low.startswith("javascript:") or low.startswith("data:"):
|
|
821
|
+
tag.attrs.pop("href", None)
|
|
822
|
+
tag["rel"] = "noopener noreferrer"
|
|
823
|
+
|
|
824
|
+
# if target set, ensure it’s safe
|
|
825
|
+
if tag.get("target") and tag.get("target") != "_blank":
|
|
826
|
+
tag.attrs.pop("target", None)
|
|
827
|
+
|
|
828
|
+
return str(soup)
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def _safe_align(val: str) -> str:
|
|
832
|
+
v = (val or "").strip().lower()
|
|
833
|
+
return v if v in ("left", "center", "right", "justify") else ""
|
|
834
|
+
|
|
835
|
+
|
|
713
836
|
def compile_layout_to_html(layout: Dict[str, Any], *, page_slug: str) -> str:
|
|
714
837
|
page_id = re.sub(r"[^a-z0-9\-]+", "-", (page_slug or "page").lower()).strip("-") or "page"
|
|
715
838
|
|
|
716
839
|
css = """
|
|
717
840
|
<style>
|
|
718
|
-
.smxp{--r:18px;--bd:rgba(148,163,184,.28);--mut:#94a3b8;--fg:#0f172a;--card:rgba(255,255,255,.72);
|
|
719
841
|
.smxp{
|
|
720
842
|
--r:18px;
|
|
721
843
|
--bd: rgba(148,163,184,.25);
|
|
722
844
|
--fg: #0f172a;
|
|
723
|
-
--mut
|
|
845
|
+
--mut:#000000; /* <- darker, readable */
|
|
724
846
|
--card: rgba(255,255,255,.78);
|
|
725
847
|
--bg: #f8fafc;
|
|
726
848
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
|
727
849
|
background: var(--bg);
|
|
728
|
-
color: var(--fg);
|
|
729
850
|
overflow-x: clip;
|
|
730
851
|
}
|
|
731
|
-
|
|
732
|
-
.smxp{
|
|
733
|
-
--fg:#e2e8f0;
|
|
734
|
-
--mut:#a7b3c6;
|
|
735
|
-
--card:rgba(2,6,23,.45);
|
|
736
|
-
--bg: radial-gradient(circle at 20% 10%, rgba(30,64,175,.25), rgba(2,6,23,.95) 55%);
|
|
737
|
-
--bd: rgba(148,163,184,.18);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
852
|
+
|
|
741
853
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; color: var(--fg); }
|
|
742
854
|
@media (prefers-color-scheme: dark){
|
|
743
855
|
.smxp{--fg:#e2e8f0;--card:rgba(2,6,23,.45);--bd:rgba(148,163,184,.18);--mut:#a7b3c6;}
|
|
@@ -759,7 +871,31 @@ def compile_layout_to_html(layout: Dict[str, Any], *, page_slug: str) -> str:
|
|
|
759
871
|
.smxp .btn:hover{transform:translateY(-1px)}
|
|
760
872
|
.smxp .grid{display:grid;gap:12px}
|
|
761
873
|
.smxp .card{border:1px solid var(--bd);border-radius:var(--r);background:var(--card);padding:14px;min-width:0}
|
|
762
|
-
.smxp .card h3{
|
|
874
|
+
.smxp .card h3{
|
|
875
|
+
margin:10px 0 6px;
|
|
876
|
+
font-size:1.05rem;
|
|
877
|
+
line-height:1.25;
|
|
878
|
+
display:-webkit-box;
|
|
879
|
+
-webkit-line-clamp:2;
|
|
880
|
+
-webkit-box-orient:vertical;
|
|
881
|
+
overflow:hidden;
|
|
882
|
+
}
|
|
883
|
+
.smxp .smx-rich{
|
|
884
|
+
margin-top:8px;
|
|
885
|
+
color: var(--mut);
|
|
886
|
+
line-height:1.65;
|
|
887
|
+
}
|
|
888
|
+
.smxp .smx-rich p{margin:0 0 10px}
|
|
889
|
+
.smxp .smx-rich ul, .smxp .smx-rich ol{margin:0 0 10px 22px}
|
|
890
|
+
.smxp .smx-rich hr{
|
|
891
|
+
border:0;
|
|
892
|
+
border-top:1px solid var(--bd);
|
|
893
|
+
margin:14px 0;
|
|
894
|
+
}
|
|
895
|
+
.smxp .card p{
|
|
896
|
+
white-space: pre-wrap; /* ✅ preserve new lines */
|
|
897
|
+
overflow-wrap: anywhere;
|
|
898
|
+
}
|
|
763
899
|
.smxp .icon{width:20px;height:20px;opacity:.9}
|
|
764
900
|
.smxp img{width:100%;height:auto;border-radius:calc(var(--r) - 6px);display:block}
|
|
765
901
|
.smxp .reveal{opacity:0;transform:translateY(14px);transition:opacity .55s ease, transform .55s ease}
|
|
@@ -767,20 +903,20 @@ def compile_layout_to_html(layout: Dict[str, Any], *, page_slug: str) -> str:
|
|
|
767
903
|
|
|
768
904
|
.smxp .hero{ padding:0; }
|
|
769
905
|
.smxp .hero-banner{
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
906
|
+
position:relative;
|
|
907
|
+
width:100%;
|
|
908
|
+
min-height:clamp(380px, 60vh, 680px);
|
|
909
|
+
display:flex;
|
|
910
|
+
align-items:flex-end;
|
|
911
|
+
overflow:hidden;
|
|
776
912
|
}
|
|
777
913
|
.smxp .hero-bg{
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
914
|
+
position:absolute; inset:0;
|
|
915
|
+
background-position:center;
|
|
916
|
+
background-size:cover;
|
|
917
|
+
background-repeat:no-repeat;
|
|
918
|
+
transform:scale(1.02);
|
|
919
|
+
filter:saturate(1.02);
|
|
784
920
|
}
|
|
785
921
|
.smxp .hero-overlay{
|
|
786
922
|
position:absolute; inset:0;
|
|
@@ -802,18 +938,205 @@ def compile_layout_to_html(layout: Dict[str, Any], *, page_slug: str) -> str:
|
|
|
802
938
|
}
|
|
803
939
|
.smxp .hero-content{ position:relative; width:100%; padding:72px 18px 48px; }
|
|
804
940
|
.smxp .hero-panel{
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
941
|
+
max-width:760px;
|
|
942
|
+
border:1px solid rgba(148,163,184,.30);
|
|
943
|
+
background: rgba(2,6,23,.16); /* VERY transparent */
|
|
944
|
+
border-radius:var(--r);
|
|
945
|
+
padding:18px;
|
|
946
|
+
|
|
947
|
+
-webkit-backdrop-filter: blur(18px) saturate(155%);
|
|
948
|
+
backdrop-filter: blur(18px) saturate(155%);
|
|
949
|
+
|
|
950
|
+
box-shadow: 0 18px 45px rgba(2,6,23,.26);
|
|
951
|
+
color: rgba(248,250,252,.96);
|
|
811
952
|
}
|
|
812
|
-
|
|
813
|
-
.smxp .hero-panel{
|
|
953
|
+
|
|
954
|
+
.smxp .hero-panel p{ color: rgba(226,232,240,.84); }
|
|
955
|
+
.smxp .hero-panel h1{
|
|
956
|
+
color: rgba(248,250,252,.98);
|
|
957
|
+
text-shadow: 0 10px 30px rgba(2,6,23,.45);
|
|
958
|
+
}
|
|
959
|
+
.smxp .hero-panel .kicker{
|
|
960
|
+
color: rgba(165,180,252,.95);
|
|
961
|
+
text-transform: uppercase;
|
|
962
|
+
letter-spacing: .18em;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
.smxp .hero-panel .btn{
|
|
966
|
+
background: rgba(15,23,42,.42);
|
|
967
|
+
border-color: rgba(148,163,184,.45);
|
|
968
|
+
color: rgba(248,250,252,.96);
|
|
969
|
+
}
|
|
970
|
+
.smxp .hero-panel .btn-primary{
|
|
971
|
+
background: rgba(79,70,229,.92);
|
|
972
|
+
border-color: rgba(129,140,248,.70);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
</style>
|
|
976
|
+
""".strip()
|
|
977
|
+
|
|
978
|
+
gallery_css = """
|
|
979
|
+
<style>
|
|
980
|
+
/* Gallery: horizontal rail (ONLY) */
|
|
981
|
+
.smxp section[data-section-type="gallery"] .grid{
|
|
982
|
+
display:flex !important;
|
|
983
|
+
gap:14px;
|
|
984
|
+
overflow-x:auto;
|
|
985
|
+
padding: 6px 2px 12px;
|
|
986
|
+
scroll-snap-type:x mandatory;
|
|
987
|
+
-webkit-overflow-scrolling: touch;
|
|
988
|
+
scrollbar-gutter: stable;
|
|
989
|
+
}
|
|
990
|
+
/* IMAGE tiles (true gallery items) */
|
|
991
|
+
.smxp section[data-section-type="gallery"] .gimg{
|
|
992
|
+
flex: 0 0 auto;
|
|
993
|
+
width: clamp(220px, 30vw, 380px);
|
|
994
|
+
aspect-ratio: 16 / 10;
|
|
995
|
+
scroll-snap-align: start;
|
|
996
|
+
cursor: zoom-in;
|
|
997
|
+
border:1px solid var(--bd);
|
|
998
|
+
border-radius: var(--r);
|
|
999
|
+
background: var(--card);
|
|
1000
|
+
overflow:hidden;
|
|
1001
|
+
position:relative;
|
|
1002
|
+
}
|
|
1003
|
+
.smxp section[data-section-type="gallery"] .gimg img{
|
|
1004
|
+
width:100%;
|
|
1005
|
+
height:100%;
|
|
1006
|
+
object-fit: cover;
|
|
1007
|
+
border-radius: 0; /* tile already rounds */
|
|
1008
|
+
}
|
|
1009
|
+
.smxp section[data-section-type="gallery"] .gimg figcaption{
|
|
1010
|
+
position:absolute;
|
|
1011
|
+
left:0; right:0; bottom:0;
|
|
1012
|
+
padding:10px 12px;
|
|
1013
|
+
background: linear-gradient(180deg, rgba(2,6,23,0) 0%, rgba(2,6,23,.55) 55%, rgba(2,6,23,.78) 100%);
|
|
1014
|
+
color: rgba(255,255,255,.92);
|
|
1015
|
+
font-weight: 700;
|
|
1016
|
+
font-size: .92rem;
|
|
1017
|
+
line-height: 1.2;
|
|
1018
|
+
opacity: 0;
|
|
1019
|
+
transform: translateY(6px);
|
|
1020
|
+
transition: opacity .16s ease, transform .16s ease;
|
|
1021
|
+
pointer-events:none;
|
|
1022
|
+
}
|
|
1023
|
+
.smxp section[data-section-type="gallery"] .gimg:hover figcaption{
|
|
1024
|
+
opacity:1;
|
|
1025
|
+
transform:none;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/* CARD items inside a gallery rail stay as content cards (not lightbox) */
|
|
1029
|
+
.smxp section[data-section-type="gallery"] .card{
|
|
1030
|
+
flex: 0 0 auto;
|
|
1031
|
+
width: clamp(260px, 34vw, 420px);
|
|
1032
|
+
scroll-snap-align: start;
|
|
1033
|
+
cursor: default;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
@media (max-width: 860px){
|
|
1037
|
+
.smxp section[data-section-type="gallery"] .gimg{ width: 86vw; }
|
|
1038
|
+
.smxp section[data-section-type="gallery"] .card{ width: 86vw; }
|
|
814
1039
|
}
|
|
815
|
-
.smxp .lead{ margin-top:10px; font-size:1.05rem; line-height:1.65; }
|
|
816
1040
|
|
|
1041
|
+
/* Rail nav buttons (injected by JS) */
|
|
1042
|
+
.smxp .smx-gallery-wrap{ position:relative; }
|
|
1043
|
+
.smxp .smx-gallery-nav{
|
|
1044
|
+
position:absolute;
|
|
1045
|
+
top:50%;
|
|
1046
|
+
transform:translateY(-50%);
|
|
1047
|
+
width:38px;
|
|
1048
|
+
height:38px;
|
|
1049
|
+
border-radius:999px;
|
|
1050
|
+
border:1px solid var(--bd);
|
|
1051
|
+
background: rgba(255,255,255,.82);
|
|
1052
|
+
color: var(--fg);
|
|
1053
|
+
display:flex;
|
|
1054
|
+
align-items:center;
|
|
1055
|
+
justify-content:center;
|
|
1056
|
+
cursor:pointer;
|
|
1057
|
+
z-index: 5;
|
|
1058
|
+
box-shadow: 0 10px 24px rgba(15,23,42,.12);
|
|
1059
|
+
}
|
|
1060
|
+
.smxp .smx-gallery-nav:hover{ transform:translateY(-50%) scale(1.03); }
|
|
1061
|
+
.smxp .smx-gallery-nav:active{ transform:translateY(-50%) scale(.98); }
|
|
1062
|
+
.smxp .smx-gallery-nav.prev{ left: 10px; }
|
|
1063
|
+
.smxp .smx-gallery-nav.next{ right: 10px; }
|
|
1064
|
+
.smxp .smx-gallery-nav[disabled]{ opacity:.35; cursor:default; }
|
|
1065
|
+
|
|
1066
|
+
/* Lightbox (“lightbulb”) overlay */
|
|
1067
|
+
.smxp .smx-lightbox{
|
|
1068
|
+
position:fixed; inset:0;
|
|
1069
|
+
display:none;
|
|
1070
|
+
align-items:center;
|
|
1071
|
+
justify-content:center;
|
|
1072
|
+
padding: 20px;
|
|
1073
|
+
background: rgba(2,6,23,.72);
|
|
1074
|
+
z-index: 9999;
|
|
1075
|
+
}
|
|
1076
|
+
.smxp .smx-lightbox.open{ display:flex; }
|
|
1077
|
+
|
|
1078
|
+
.smxp .smx-lightbox-panel{
|
|
1079
|
+
width:min(1100px, 96vw);
|
|
1080
|
+
max-height: 92vh;
|
|
1081
|
+
border-radius: 16px;
|
|
1082
|
+
border: 1px solid rgba(148,163,184,.28);
|
|
1083
|
+
background: rgba(255,255,255,.95);
|
|
1084
|
+
overflow:hidden;
|
|
1085
|
+
display:flex;
|
|
1086
|
+
flex-direction:column;
|
|
1087
|
+
}
|
|
1088
|
+
.smxp .smx-lightbox-top{
|
|
1089
|
+
display:flex;
|
|
1090
|
+
align-items:center;
|
|
1091
|
+
justify-content:space-between;
|
|
1092
|
+
gap: 10px;
|
|
1093
|
+
padding: 10px 12px;
|
|
1094
|
+
border-bottom: 1px solid rgba(148,163,184,.22);
|
|
1095
|
+
}
|
|
1096
|
+
.smxp .smx-lightbox-title{ font-weight:700; color: var(--fg); }
|
|
1097
|
+
.smxp .smx-lightbox-close{
|
|
1098
|
+
border:1px solid rgba(148,163,184,.28);
|
|
1099
|
+
background: rgba(255,255,255,.75);
|
|
1100
|
+
color: var(--fg);
|
|
1101
|
+
border-radius: 12px;
|
|
1102
|
+
padding: 8px 10px;
|
|
1103
|
+
cursor:pointer;
|
|
1104
|
+
}
|
|
1105
|
+
.smxp .smx-lightbox-body{
|
|
1106
|
+
position:relative;
|
|
1107
|
+
padding: 12px;
|
|
1108
|
+
display:grid;
|
|
1109
|
+
gap: 10px;
|
|
1110
|
+
}
|
|
1111
|
+
.smxp .smx-lightbox-img{
|
|
1112
|
+
width:100%;
|
|
1113
|
+
max-height: 68vh;
|
|
1114
|
+
object-fit: contain;
|
|
1115
|
+
border-radius: 14px;
|
|
1116
|
+
background: rgba(0,0,0,.04);
|
|
1117
|
+
}
|
|
1118
|
+
.smxp .smx-lightbox-caption{
|
|
1119
|
+
color: var(--mut);
|
|
1120
|
+
line-height: 1.6;
|
|
1121
|
+
}
|
|
1122
|
+
.smxp .smx-lightbox-arrow{
|
|
1123
|
+
position:absolute;
|
|
1124
|
+
top:50%;
|
|
1125
|
+
transform:translateY(-50%);
|
|
1126
|
+
width:44px;
|
|
1127
|
+
height:44px;
|
|
1128
|
+
border-radius:999px;
|
|
1129
|
+
border:1px solid rgba(148,163,184,.28);
|
|
1130
|
+
background: rgba(255,255,255,.82);
|
|
1131
|
+
color: var(--fg);
|
|
1132
|
+
display:flex;
|
|
1133
|
+
align-items:center;
|
|
1134
|
+
justify-content:center;
|
|
1135
|
+
cursor:pointer;
|
|
1136
|
+
}
|
|
1137
|
+
.smxp .smx-lightbox-arrow.prev{ left: 10px; }
|
|
1138
|
+
.smxp .smx-lightbox-arrow.next{ right: 10px; }
|
|
1139
|
+
.smxp .smx-lightbox-arrow[disabled]{ opacity:.35; cursor:default; }
|
|
817
1140
|
</style>
|
|
818
1141
|
""".strip()
|
|
819
1142
|
|
|
@@ -831,11 +1154,226 @@ def compile_layout_to_html(layout: Dict[str, Any], *, page_slug: str) -> str:
|
|
|
831
1154
|
</script>
|
|
832
1155
|
""".strip()
|
|
833
1156
|
|
|
1157
|
+
gallery_js = f"""
|
|
1158
|
+
<script>
|
|
1159
|
+
(function(){{
|
|
1160
|
+
const root = document.getElementById("smxp-{page_id}") || document.querySelector(".smxp");
|
|
1161
|
+
if(!root) return;
|
|
1162
|
+
|
|
1163
|
+
const gallerySecs = root.querySelectorAll('section[data-section-type="gallery"]');
|
|
1164
|
+
if(!gallerySecs.length) return;
|
|
1165
|
+
|
|
1166
|
+
// Ensure single lightbox per page
|
|
1167
|
+
let lb = root.querySelector(".smx-lightbox");
|
|
1168
|
+
if(!lb){{
|
|
1169
|
+
lb = document.createElement("div");
|
|
1170
|
+
lb.className = "smx-lightbox";
|
|
1171
|
+
lb.innerHTML = `
|
|
1172
|
+
<div class="smx-lightbox-panel" role="dialog" aria-modal="true">
|
|
1173
|
+
<div class="smx-lightbox-top">
|
|
1174
|
+
<div class="smx-lightbox-title"></div>
|
|
1175
|
+
<button class="smx-lightbox-close" type="button">Close</button>
|
|
1176
|
+
</div>
|
|
1177
|
+
<div class="smx-lightbox-body">
|
|
1178
|
+
<button class="smx-lightbox-arrow prev" type="button" aria-label="Previous">‹</button>
|
|
1179
|
+
<img class="smx-lightbox-img" alt="" />
|
|
1180
|
+
<button class="smx-lightbox-arrow next" type="button" aria-label="Next">›</button>
|
|
1181
|
+
<div class="smx-lightbox-caption"></div>
|
|
1182
|
+
</div>
|
|
1183
|
+
</div>`;
|
|
1184
|
+
root.appendChild(lb);
|
|
1185
|
+
}}
|
|
1186
|
+
|
|
1187
|
+
const lbTitle = lb.querySelector(".smx-lightbox-title");
|
|
1188
|
+
const lbImg = lb.querySelector(".smx-lightbox-img");
|
|
1189
|
+
const lbCap = lb.querySelector(".smx-lightbox-caption");
|
|
1190
|
+
const btnClose= lb.querySelector(".smx-lightbox-close");
|
|
1191
|
+
const btnPrev = lb.querySelector(".smx-lightbox-arrow.prev");
|
|
1192
|
+
const btnNext = lb.querySelector(".smx-lightbox-arrow.next");
|
|
1193
|
+
|
|
1194
|
+
let slides = [];
|
|
1195
|
+
let idx = 0;
|
|
1196
|
+
let lastFocus = null;
|
|
1197
|
+
let prevOverflow = "";
|
|
1198
|
+
|
|
1199
|
+
function render(){{
|
|
1200
|
+
const s = slides[idx];
|
|
1201
|
+
if(!s) return;
|
|
1202
|
+
lbImg.src = s.src;
|
|
1203
|
+
lbImg.alt = s.title || "image";
|
|
1204
|
+
lbTitle.textContent = s.title || "";
|
|
1205
|
+
lbCap.textContent = s.caption || "";
|
|
1206
|
+
btnPrev.disabled = (idx <= 0);
|
|
1207
|
+
btnNext.disabled = (idx >= slides.length - 1);
|
|
1208
|
+
}}
|
|
1209
|
+
|
|
1210
|
+
function openAt(i){{
|
|
1211
|
+
idx = Math.max(0, Math.min(i, slides.length - 1));
|
|
1212
|
+
lastFocus = document.activeElement;
|
|
1213
|
+
prevOverflow = document.body.style.overflow;
|
|
1214
|
+
document.body.style.overflow = "hidden";
|
|
1215
|
+
lb.classList.add("open");
|
|
1216
|
+
render();
|
|
1217
|
+
btnClose.focus();
|
|
1218
|
+
}}
|
|
1219
|
+
|
|
1220
|
+
function close(){{
|
|
1221
|
+
lb.classList.remove("open");
|
|
1222
|
+
document.body.style.overflow = prevOverflow || "";
|
|
1223
|
+
if(lastFocus && lastFocus.focus) lastFocus.focus();
|
|
1224
|
+
}}
|
|
1225
|
+
|
|
1226
|
+
function move(d){{
|
|
1227
|
+
const ni = idx + d;
|
|
1228
|
+
if(ni < 0 || ni >= slides.length) return;
|
|
1229
|
+
idx = ni;
|
|
1230
|
+
render();
|
|
1231
|
+
}}
|
|
1232
|
+
|
|
1233
|
+
btnClose.addEventListener("click", close);
|
|
1234
|
+
lb.addEventListener("click", (e)=> {{ if(e.target === lb) close(); }});
|
|
1235
|
+
btnPrev.addEventListener("click", ()=> move(-1));
|
|
1236
|
+
btnNext.addEventListener("click", ()=> move(+1));
|
|
1237
|
+
|
|
1238
|
+
document.addEventListener("keydown", (e)=> {{
|
|
1239
|
+
if(!lb.classList.contains("open")) return;
|
|
1240
|
+
if(e.key === "Escape") return close();
|
|
1241
|
+
if(e.key === "ArrowLeft") return move(-1);
|
|
1242
|
+
if(e.key === "ArrowRight") return move(+1);
|
|
1243
|
+
}});
|
|
1244
|
+
|
|
1245
|
+
function ensureNav(sec, rail){{
|
|
1246
|
+
let wrap = sec.querySelector(".smx-gallery-wrap");
|
|
1247
|
+
if(!wrap){{
|
|
1248
|
+
wrap = document.createElement("div");
|
|
1249
|
+
wrap.className = "smx-gallery-wrap";
|
|
1250
|
+
rail.parentNode.insertBefore(wrap, rail);
|
|
1251
|
+
wrap.appendChild(rail);
|
|
1252
|
+
}}
|
|
1253
|
+
|
|
1254
|
+
let prev = wrap.querySelector("button.smx-gallery-nav.prev");
|
|
1255
|
+
let next = wrap.querySelector("button.smx-gallery-nav.next");
|
|
1256
|
+
|
|
1257
|
+
if(!prev){{
|
|
1258
|
+
prev = document.createElement("button");
|
|
1259
|
+
prev.className = "smx-gallery-nav prev";
|
|
1260
|
+
prev.type = "button";
|
|
1261
|
+
prev.setAttribute("aria-label","Scroll left");
|
|
1262
|
+
prev.innerHTML = "‹";
|
|
1263
|
+
wrap.appendChild(prev);
|
|
1264
|
+
}}
|
|
1265
|
+
if(!next){{
|
|
1266
|
+
next = document.createElement("button");
|
|
1267
|
+
next.className = "smx-gallery-nav next";
|
|
1268
|
+
next.type = "button";
|
|
1269
|
+
next.setAttribute("aria-label","Scroll right");
|
|
1270
|
+
next.innerHTML = "›";
|
|
1271
|
+
wrap.appendChild(next);
|
|
1272
|
+
}}
|
|
1273
|
+
|
|
1274
|
+
function update(){{
|
|
1275
|
+
const max = rail.scrollWidth - rail.clientWidth;
|
|
1276
|
+
const canScroll = max > 4;
|
|
1277
|
+
prev.style.display = canScroll ? "" : "none";
|
|
1278
|
+
next.style.display = canScroll ? "" : "none";
|
|
1279
|
+
prev.disabled = rail.scrollLeft <= 2;
|
|
1280
|
+
next.disabled = rail.scrollLeft >= (max - 2);
|
|
1281
|
+
}}
|
|
1282
|
+
|
|
1283
|
+
prev.addEventListener("click", ()=> {{
|
|
1284
|
+
if(prev.disabled) return;
|
|
1285
|
+
rail.scrollBy({{ left: -Math.max(260, rail.clientWidth * 0.85), behavior:"smooth" }});
|
|
1286
|
+
}});
|
|
1287
|
+
next.addEventListener("click", ()=> {{
|
|
1288
|
+
if(next.disabled) return;
|
|
1289
|
+
rail.scrollBy({{ left: Math.max(260, rail.clientWidth * 0.85), behavior:"smooth" }});
|
|
1290
|
+
}});
|
|
1291
|
+
|
|
1292
|
+
rail.addEventListener("scroll", update, {{ passive:true }});
|
|
1293
|
+
window.addEventListener("resize", update);
|
|
1294
|
+
update();
|
|
1295
|
+
}}
|
|
1296
|
+
|
|
1297
|
+
function initGallerySection(sec){{
|
|
1298
|
+
const rail = sec.querySelector(".grid");
|
|
1299
|
+
if(!rail) return;
|
|
1300
|
+
|
|
1301
|
+
ensureNav(sec, rail);
|
|
1302
|
+
|
|
1303
|
+
// Build slides from IMAGE tiles only (cards remain content cards)
|
|
1304
|
+
const tiles = Array.from(rail.querySelectorAll('[data-item-type="image"]'));
|
|
1305
|
+
if(!tiles.length) return;
|
|
1306
|
+
|
|
1307
|
+
// Build a local slide list for THIS rail
|
|
1308
|
+
const localSlides = [];
|
|
1309
|
+
tiles.forEach(tile => {{
|
|
1310
|
+
const img = tile.querySelector("img");
|
|
1311
|
+
if(!img) return;
|
|
1312
|
+
|
|
1313
|
+
const full = (tile.getAttribute("data-full") || img.getAttribute("data-full") || img.getAttribute("src") || "").trim();
|
|
1314
|
+
if(!full) return;
|
|
1315
|
+
|
|
1316
|
+
const title = (tile.getAttribute("data-title") || "").trim();
|
|
1317
|
+
const caption = (tile.getAttribute("data-caption") || "").trim();
|
|
1318
|
+
|
|
1319
|
+
const slideIndex = localSlides.length;
|
|
1320
|
+
localSlides.push({{ src: full, title, caption }});
|
|
1321
|
+
|
|
1322
|
+
tile.dataset.smxSlide = String(slideIndex);
|
|
1323
|
+
|
|
1324
|
+
// IMPORTANT: don’t double-bind (dynamic inserts / re-init safe)
|
|
1325
|
+
if(tile.dataset.smxBound === "1") return;
|
|
1326
|
+
tile.dataset.smxBound = "1";
|
|
1327
|
+
|
|
1328
|
+
tile.addEventListener("click", (e)=> {{
|
|
1329
|
+
if(e.target && e.target.closest && e.target.closest("a,button")) return;
|
|
1330
|
+
slides = localSlides; // activate the correct slide set for this rail
|
|
1331
|
+
const i = parseInt(tile.dataset.smxSlide || "0", 10);
|
|
1332
|
+
openAt(i);
|
|
1333
|
+
}});
|
|
1334
|
+
}});
|
|
1335
|
+
}}
|
|
1336
|
+
|
|
1337
|
+
gallerySecs.forEach(sec => initGallerySection(sec));
|
|
1338
|
+
|
|
1339
|
+
// Re-init when new gallery sections or new tiles are inserted dynamically (Admin editor)
|
|
1340
|
+
const obs = new MutationObserver((mutations) => {{
|
|
1341
|
+
for(const m of mutations){{
|
|
1342
|
+
if(m.type !== "childList") continue;
|
|
1343
|
+
|
|
1344
|
+
// If a gallery section is added, init it
|
|
1345
|
+
m.addedNodes && m.addedNodes.forEach(node => {{
|
|
1346
|
+
if(!(node instanceof Element)) return;
|
|
1347
|
+
|
|
1348
|
+
if(node.matches && node.matches('section[data-section-type="gallery"]')){{
|
|
1349
|
+
initGallerySection(node);
|
|
1350
|
+
return;
|
|
1351
|
+
}}
|
|
1352
|
+
|
|
1353
|
+
// If anything added inside/near a gallery section, init the nearest section
|
|
1354
|
+
const sec = node.closest ? node.closest('section[data-section-type="gallery"]') : null;
|
|
1355
|
+
if(sec) initGallerySection(sec);
|
|
1356
|
+
}});
|
|
1357
|
+
}}
|
|
1358
|
+
}});
|
|
1359
|
+
|
|
1360
|
+
obs.observe(root, {{ childList: true, subtree: true }});
|
|
1361
|
+
|
|
1362
|
+
}})();
|
|
1363
|
+
</script>
|
|
1364
|
+
""".strip()
|
|
1365
|
+
|
|
1366
|
+
|
|
834
1367
|
def esc(s: str) -> str:
|
|
835
1368
|
s = s or ""
|
|
836
1369
|
s = s.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
837
1370
|
s = s.replace('"', """).replace("'", "'")
|
|
838
1371
|
return s
|
|
1372
|
+
|
|
1373
|
+
def esc_nl(s: str) -> str:
|
|
1374
|
+
# preserve paragraphs in plain text
|
|
1375
|
+
return esc(s).replace("\n", "<br>")
|
|
1376
|
+
|
|
839
1377
|
|
|
840
1378
|
def icon_svg(name: str) -> str:
|
|
841
1379
|
svg = _ICON_SVGS.get((name or "").strip().lower())
|
|
@@ -843,7 +1381,7 @@ def compile_layout_to_html(layout: Dict[str, Any], *, page_slug: str) -> str:
|
|
|
843
1381
|
return ""
|
|
844
1382
|
return f'<span class="icon">{svg}</span>'
|
|
845
1383
|
|
|
846
|
-
parts: List[str] = [f'<div class="smxp" id="smxp-{page_id}">', css]
|
|
1384
|
+
parts: List[str] = [f'<div class="smxp" id="smxp-{page_id}">', css, gallery_css]
|
|
847
1385
|
sections = layout.get("sections") if isinstance(layout.get("sections"), list) else []
|
|
848
1386
|
|
|
849
1387
|
# Map first section id by type (used for default Hero CTA anchors)
|
|
@@ -869,7 +1407,7 @@ def compile_layout_to_html(layout: Dict[str, Any], *, page_slug: str) -> str:
|
|
|
869
1407
|
for s in sections:
|
|
870
1408
|
stype = (s.get("type") or "section").lower()
|
|
871
1409
|
title = esc(s.get("title") or "")
|
|
872
|
-
text =
|
|
1410
|
+
text = esc_nl(s.get("text") or "")
|
|
873
1411
|
items = s.get("items") if isinstance(s.get("items"), list) else []
|
|
874
1412
|
sec_dom_id = (s.get("id") or "").strip()
|
|
875
1413
|
if not sec_dom_id:
|
|
@@ -913,10 +1451,10 @@ def compile_layout_to_html(layout: Dict[str, Any], *, page_slug: str) -> str:
|
|
|
913
1451
|
)
|
|
914
1452
|
|
|
915
1453
|
btn_row_html = f'<div class="btnRow">{"".join(btns)}</div>' if btns else ""
|
|
916
|
-
|
|
1454
|
+
|
|
917
1455
|
parts.append(
|
|
918
1456
|
f'''
|
|
919
|
-
<section id="{esc(sec_dom_id)}" class="hero hero-banner">
|
|
1457
|
+
<section id="{esc(sec_dom_id)}" class="hero hero-banner" data-section-type="hero">
|
|
920
1458
|
<div class="hero-bg"{bg_style}></div>
|
|
921
1459
|
<div class="hero-overlay"></div>
|
|
922
1460
|
<div class="wrap hero-content">
|
|
@@ -939,38 +1477,94 @@ def compile_layout_to_html(layout: Dict[str, Any], *, page_slug: str) -> str:
|
|
|
939
1477
|
cols = 3
|
|
940
1478
|
cols = max(1, min(5, cols))
|
|
941
1479
|
|
|
942
|
-
|
|
1480
|
+
tiles: List[str] = []
|
|
943
1481
|
for it in items:
|
|
944
1482
|
if not isinstance(it, dict):
|
|
945
1483
|
continue
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1484
|
+
|
|
1485
|
+
it_type = str(it.get("type") or "card").lower().strip()
|
|
1486
|
+
|
|
1487
|
+
title_plain = (it.get("title") or "").strip()
|
|
1488
|
+
cap_plain = (it.get("text") or "").strip()
|
|
1489
|
+
|
|
1490
|
+
it_title = esc(title_plain)
|
|
1491
|
+
it_text = esc_nl(cap_plain)
|
|
1492
|
+
|
|
1493
|
+
full = (it.get("imageUrl") or "").strip()
|
|
1494
|
+
thumb = (it.get("thumbUrl") or it.get("thumb_url") or "").strip()
|
|
1495
|
+
src = thumb or full # rail prefers thumb if available
|
|
1496
|
+
|
|
1497
|
+
# IMAGE tiles: true gallery behaviour (thumb in rail, full in lightbox)
|
|
1498
|
+
if stype == "gallery" and it_type == "image":
|
|
1499
|
+
if not src:
|
|
1500
|
+
continue
|
|
1501
|
+
full_for_lb = full or src
|
|
1502
|
+
tiles.append(
|
|
1503
|
+
f'''
|
|
1504
|
+
<figure class="gimg reveal"
|
|
1505
|
+
data-item-type="image"
|
|
1506
|
+
data-full="{esc(full_for_lb)}"
|
|
1507
|
+
data-title="{esc(title_plain)}"
|
|
1508
|
+
data-caption="{esc(cap_plain)}">
|
|
1509
|
+
<img loading="lazy" decoding="async"
|
|
1510
|
+
src="{esc(src)}"
|
|
1511
|
+
data-full="{esc(full_for_lb)}"
|
|
1512
|
+
alt="{esc(title_plain or 'image')}">
|
|
1513
|
+
{f'<figcaption>{it_title}</figcaption>' if title_plain else ''}
|
|
1514
|
+
</figure>
|
|
1515
|
+
'''.strip()
|
|
1516
|
+
)
|
|
1517
|
+
continue
|
|
1518
|
+
|
|
1519
|
+
# Default: render as a content card (Card, FAQ, Quote, etc.)
|
|
949
1520
|
ic = icon_svg(it.get("icon") or "")
|
|
1521
|
+
img_html = f'<img loading="lazy" decoding="async" src="{esc(full)}" alt="{it_title}">' if full else ""
|
|
1522
|
+
|
|
1523
|
+
title_align = _safe_align(str(it.get("titleAlign") or ""))
|
|
1524
|
+
text_align = _safe_align(str(it.get("textAlign") or ""))
|
|
1525
|
+
|
|
1526
|
+
title_align_css = f"text-align:{title_align};" if title_align else ""
|
|
1527
|
+
text_align_css = f"text-align:{text_align};" if text_align else ""
|
|
950
1528
|
|
|
951
|
-
|
|
952
|
-
|
|
1529
|
+
raw_html = str(it.get("textHtml") or "").strip()
|
|
1530
|
+
if raw_html:
|
|
1531
|
+
safe_html = _sanitize_rich_html(raw_html)
|
|
1532
|
+
body_html = f'<div class="smx-rich" style="{text_align_css}">{safe_html}</div>'
|
|
1533
|
+
else:
|
|
1534
|
+
body_html = f'<p style="margin-top:8px;{text_align_css}">{it_text}</p>'
|
|
1535
|
+
|
|
1536
|
+
tiles.append(
|
|
953
1537
|
f'''
|
|
954
|
-
<div class="card reveal">
|
|
1538
|
+
<div class="card reveal" data-item-type="{esc(it_type)}">
|
|
955
1539
|
{img_html}
|
|
956
|
-
<div style="display:flex;gap:10px;align-items:
|
|
1540
|
+
<div style="display:flex;gap:10px;align-items:flex-start;margin-top:{'10px' if img_html else '0'};">
|
|
957
1541
|
{ic}
|
|
958
|
-
<h3 style="margin:0">{it_title}</h3>
|
|
1542
|
+
<h3 style="margin:0;{title_align_css}">{it_title}</h3>
|
|
959
1543
|
</div>
|
|
960
|
-
|
|
1544
|
+
{body_html}
|
|
961
1545
|
</div>
|
|
962
1546
|
'''.strip()
|
|
963
1547
|
)
|
|
964
1548
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1549
|
+
if tiles:
|
|
1550
|
+
if stype == "gallery":
|
|
1551
|
+
grid_html = '<div class="grid">' + "\n".join(tiles) + "</div>"
|
|
1552
|
+
else:
|
|
1553
|
+
grid_html = (
|
|
1554
|
+
f'<div class="grid" style="grid-template-columns:repeat({cols}, minmax(0, 1fr));">'
|
|
1555
|
+
+ "\n".join(tiles) +
|
|
1556
|
+
"</div>"
|
|
1557
|
+
)
|
|
1558
|
+
else:
|
|
1559
|
+
grid_html = ""
|
|
1560
|
+
|
|
1561
|
+
|
|
1562
|
+
sec_class = "sec sec-gallery" if stype == "gallery" else "sec"
|
|
1563
|
+
sec_extra_attr = ' data-section-type="gallery"' if stype == "gallery" else ""
|
|
970
1564
|
|
|
971
1565
|
parts.append(
|
|
972
1566
|
f'''
|
|
973
|
-
|
|
1567
|
+
<section id="{esc(sec_dom_id)}" class="sec" data-section-type="{esc(stype or 'section')}">
|
|
974
1568
|
<div class="wrap">
|
|
975
1569
|
<h2 class="reveal">{title}</h2>
|
|
976
1570
|
{'<p class="reveal" style="margin-bottom:14px;">'+text+'</p>' if text else ''}
|
|
@@ -981,12 +1575,11 @@ def compile_layout_to_html(layout: Dict[str, Any], *, page_slug: str) -> str:
|
|
|
981
1575
|
)
|
|
982
1576
|
|
|
983
1577
|
parts.append(js)
|
|
1578
|
+
parts.append(gallery_js)
|
|
984
1579
|
parts.append("</div>")
|
|
985
|
-
return "\n\n".join(parts)
|
|
986
1580
|
|
|
1581
|
+
return "\n\n".join(parts)
|
|
987
1582
|
|
|
988
|
-
from bs4 import BeautifulSoup
|
|
989
|
-
from typing import Dict, Any, List, Tuple
|
|
990
1583
|
|
|
991
1584
|
def _layout_non_hero_sections(layout: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
992
1585
|
sections = layout.get("sections") if isinstance(layout.get("sections"), list) else []
|