@officexapp/catalogs-cli 0.2.2 → 0.2.3
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.
- package/dist/index.js +769 -162
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -732,38 +732,158 @@ function getMime(filepath) {
|
|
|
732
732
|
}
|
|
733
733
|
function buildPreviewHtml(schema, port) {
|
|
734
734
|
const schemaJson = JSON.stringify(schema);
|
|
735
|
+
const themeColor = schema.settings?.theme?.primary_color || "#6366f1";
|
|
735
736
|
return `<!DOCTYPE html>
|
|
736
737
|
<html lang="en">
|
|
737
738
|
<head>
|
|
738
739
|
<meta charset="UTF-8" />
|
|
739
740
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
740
741
|
<title>${schema.slug || "Catalog"} \u2014 Local Preview</title>
|
|
742
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
743
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
744
|
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800;900&family=DM+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&display=swap" rel="stylesheet" />
|
|
745
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
741
746
|
<style>
|
|
747
|
+
/* \u2500\u2500 Production CSS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
748
|
+
:root {
|
|
749
|
+
--font-display: 'Outfit', system-ui, sans-serif;
|
|
750
|
+
--font-body: 'DM Sans', system-ui, sans-serif;
|
|
751
|
+
--font-size-body: 1.125rem;
|
|
752
|
+
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
|
753
|
+
--ease-out-back: cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
754
|
+
--theme-color: ${themeColor};
|
|
755
|
+
--theme-color-ring: ${themeColor}26;
|
|
756
|
+
}
|
|
742
757
|
*, *::before, *::after { box-sizing: border-box; }
|
|
743
|
-
body {
|
|
758
|
+
body {
|
|
759
|
+
margin: 0;
|
|
760
|
+
font-family: var(--font-body);
|
|
761
|
+
-webkit-font-smoothing: antialiased;
|
|
762
|
+
-moz-osx-font-smoothing: grayscale;
|
|
763
|
+
color: #1a1a2e;
|
|
764
|
+
}
|
|
765
|
+
h1, h2, h3, h4, h5, h6 { font-family: var(--font-display); }
|
|
766
|
+
|
|
767
|
+
/* Page transitions */
|
|
768
|
+
.page-enter-active { animation: pageReveal 0.35s var(--ease-out-expo) both; }
|
|
769
|
+
@keyframes pageReveal { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
|
770
|
+
.page-enter-active > * { animation: staggerIn 0.3s var(--ease-out-expo) both; }
|
|
771
|
+
.page-enter-active > *:nth-child(1) { animation-delay: 0.02s; }
|
|
772
|
+
.page-enter-active > *:nth-child(2) { animation-delay: 0.05s; }
|
|
773
|
+
.page-enter-active > *:nth-child(3) { animation-delay: 0.08s; }
|
|
774
|
+
.page-enter-active > *:nth-child(4) { animation-delay: 0.11s; }
|
|
775
|
+
.page-enter-active > *:nth-child(5) { animation-delay: 0.14s; }
|
|
776
|
+
.page-enter-active > *:nth-child(6) { animation-delay: 0.17s; }
|
|
777
|
+
.page-enter-active > *:nth-child(7) { animation-delay: 0.2s; }
|
|
778
|
+
.page-enter-active > *:nth-child(8) { animation-delay: 0.23s; }
|
|
779
|
+
@keyframes staggerIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
|
780
|
+
|
|
781
|
+
/* Cover page */
|
|
782
|
+
.cf-cover-overlay { background: linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.4) 50%, rgba(0,0,0,0.6) 100%); }
|
|
783
|
+
.cf-cover-content { animation: coverFloat 0.8s var(--ease-out-expo) both; }
|
|
784
|
+
@keyframes coverFloat { from { opacity: 0; transform: translateY(30px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } }
|
|
785
|
+
|
|
786
|
+
/* Top bar */
|
|
787
|
+
.cf-topbar { background: rgba(255,255,255,0.92); backdrop-filter: blur(16px) saturate(180%); -webkit-backdrop-filter: blur(16px) saturate(180%); }
|
|
788
|
+
|
|
789
|
+
/* Card */
|
|
790
|
+
.cf-card { background: white; border-radius: 20px; box-shadow: 0 0 0 1px rgba(0,0,0,0.03), 0 2px 4px rgba(0,0,0,0.02), 0 12px 40px rgba(0,0,0,0.06); }
|
|
791
|
+
|
|
792
|
+
/* Inputs */
|
|
793
|
+
.cf-input {
|
|
794
|
+
font-family: var(--font-body); border-radius: 12px; border: 1.5px solid #e2e4e9;
|
|
795
|
+
background: #fafbfc; padding: 12px 16px; font-size: var(--font-size-body);
|
|
796
|
+
color: #1a1a2e; outline: none; transition: all 0.2s var(--ease-out-expo); width: 100%;
|
|
797
|
+
}
|
|
798
|
+
.cf-input::placeholder { color: #a0a3b1; }
|
|
799
|
+
.cf-input:hover { border-color: #c8cbd4; background: #fff; }
|
|
800
|
+
.cf-input:focus { background: #fff; border-color: var(--theme-color); box-shadow: 0 0 0 3px var(--theme-color-ring); }
|
|
801
|
+
|
|
802
|
+
/* Buttons */
|
|
803
|
+
.cf-btn-primary {
|
|
804
|
+
font-family: var(--font-display); font-weight: 600; letter-spacing: -0.01em;
|
|
805
|
+
border-radius: 14px; padding: 14px 28px; font-size: 16px; color: white; border: none;
|
|
806
|
+
cursor: pointer; transition: all 0.25s var(--ease-out-expo);
|
|
807
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.08);
|
|
808
|
+
position: relative; overflow: hidden;
|
|
809
|
+
}
|
|
810
|
+
.cf-btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 16px rgba(0,0,0,0.16), 0 2px 4px rgba(0,0,0,0.1); }
|
|
811
|
+
.cf-btn-primary:active { transform: translateY(0) scale(0.98); }
|
|
812
|
+
|
|
813
|
+
/* Choice buttons */
|
|
814
|
+
.cf-choice {
|
|
815
|
+
border-radius: 14px; border: 1.5px solid #e2e4e9; background: #fafbfc;
|
|
816
|
+
transition: all 0.2s var(--ease-out-expo); cursor: pointer; width: 100%;
|
|
817
|
+
text-align: left; padding: 14px 18px; font-size: 1rem; color: #1a1a2e;
|
|
818
|
+
font-family: var(--font-body);
|
|
819
|
+
}
|
|
820
|
+
.cf-choice:hover { border-color: #c8cbd4; background: #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.04); }
|
|
821
|
+
.cf-choice[data-selected="true"] { border-color: var(--theme-color); background: #fff; box-shadow: 0 0 0 3px var(--theme-color-ring), 0 2px 12px rgba(0,0,0,0.06); }
|
|
822
|
+
|
|
823
|
+
/* Banner glass */
|
|
824
|
+
.cf-banner-glass { background: rgba(255,255,255,0.12); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.18); }
|
|
825
|
+
|
|
826
|
+
/* Images */
|
|
827
|
+
.ck-img { width: 100%; height: auto; object-fit: cover; display: block; }
|
|
828
|
+
|
|
829
|
+
/* Text balance */
|
|
830
|
+
.cf-text-balance { text-wrap: balance; }
|
|
831
|
+
|
|
832
|
+
/* Progress bar */
|
|
833
|
+
.progress-bar-fill { transition: width 0.6s var(--ease-out-expo); }
|
|
834
|
+
|
|
835
|
+
/* Noise texture */
|
|
836
|
+
.cf-noise::before {
|
|
837
|
+
content: ''; position: absolute; inset: 0; opacity: 0.03; pointer-events: none;
|
|
838
|
+
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
|
839
|
+
background-size: 256px 256px;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/* Dev banner */
|
|
744
843
|
.dev-banner {
|
|
745
844
|
position: fixed; top: 0; left: 0; right: 0; z-index: 99999;
|
|
746
845
|
background: #1a1a2e; color: #e0e0ff; font-size: 12px;
|
|
747
846
|
padding: 4px 12px; display: flex; align-items: center; gap: 8px;
|
|
748
847
|
font-family: monospace; border-bottom: 2px solid #6c63ff;
|
|
749
848
|
}
|
|
750
|
-
.dev-banner .dot { width: 8px; height: 8px; border-radius: 50%; background: #4ade80; }
|
|
849
|
+
.dev-banner .dot { width: 8px; height: 8px; border-radius: 50%; background: #4ade80; animation: pulse 2s ease-in-out infinite; }
|
|
850
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
751
851
|
.dev-banner .label { opacity: 0.7; }
|
|
752
852
|
.dev-banner .slug { font-weight: bold; color: #a5b4fc; }
|
|
753
853
|
.dev-banner .stub-tags { margin-left: auto; display: flex; gap: 6px; }
|
|
754
|
-
.dev-banner .stub-tag {
|
|
755
|
-
|
|
756
|
-
|
|
854
|
+
.dev-banner .stub-tag { background: rgba(255,255,255,0.1); border-radius: 3px; padding: 1px 6px; font-size: 11px; color: #fbbf24; }
|
|
855
|
+
|
|
856
|
+
/* Dev page nav */
|
|
857
|
+
.dev-page-nav {
|
|
858
|
+
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%); z-index: 99998;
|
|
859
|
+
background: rgba(26,26,46,0.95); backdrop-filter: blur(12px); border-radius: 16px;
|
|
860
|
+
padding: 6px; display: flex; gap: 4px; box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
861
|
+
max-width: calc(100vw - 32px); overflow-x: auto;
|
|
862
|
+
}
|
|
863
|
+
.dev-page-nav button {
|
|
864
|
+
padding: 6px 14px; border-radius: 12px; font-size: 12px; font-family: var(--font-display);
|
|
865
|
+
font-weight: 500; border: none; cursor: pointer; white-space: nowrap;
|
|
866
|
+
transition: all 0.2s ease; color: rgba(255,255,255,0.6); background: transparent;
|
|
757
867
|
}
|
|
758
|
-
|
|
868
|
+
.dev-page-nav button:hover { color: white; background: rgba(255,255,255,0.1); }
|
|
869
|
+
.dev-page-nav button.active { color: white; background: var(--theme-color); }
|
|
870
|
+
|
|
871
|
+
/* Checkout stub */
|
|
759
872
|
.checkout-stub {
|
|
760
|
-
background: #fef3c7
|
|
761
|
-
|
|
873
|
+
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
|
874
|
+
border: 2px dashed #f59e0b; border-radius: 16px;
|
|
875
|
+
padding: 24px; text-align: center;
|
|
762
876
|
}
|
|
763
|
-
.checkout-stub h3 { margin: 0 0 8px; color: #92400e; }
|
|
877
|
+
.checkout-stub h3 { margin: 0 0 8px; color: #92400e; font-family: var(--font-display); font-weight: 700; }
|
|
764
878
|
.checkout-stub p { margin: 0; color: #a16207; font-size: 14px; }
|
|
879
|
+
|
|
880
|
+
/* Pricing card */
|
|
881
|
+
.pricing-card {
|
|
882
|
+
border-radius: 20px; border: 1.5px solid #e2e4e9; background: white;
|
|
883
|
+
padding: 32px; text-align: center;
|
|
884
|
+
box-shadow: 0 2px 12px rgba(0,0,0,0.04);
|
|
885
|
+
}
|
|
765
886
|
</style>
|
|
766
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
767
887
|
</head>
|
|
768
888
|
<body>
|
|
769
889
|
<div class="dev-banner">
|
|
@@ -781,189 +901,653 @@ function buildPreviewHtml(schema, port) {
|
|
|
781
901
|
import React from 'https://esm.sh/react@18?bundle';
|
|
782
902
|
import ReactDOM from 'https://esm.sh/react-dom@18/client?bundle';
|
|
783
903
|
|
|
904
|
+
const h = React.createElement;
|
|
784
905
|
const schema = ${schemaJson};
|
|
906
|
+
const themeColor = '${themeColor}';
|
|
785
907
|
|
|
786
|
-
//
|
|
787
|
-
function
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
function renderComponent(comp, i) {
|
|
795
|
-
const props = comp.props || {};
|
|
796
|
-
const type = comp.type;
|
|
797
|
-
|
|
798
|
-
switch (type) {
|
|
799
|
-
case 'heading':
|
|
800
|
-
const Tag = props.level === 2 ? 'h2' : props.level === 3 ? 'h3' : 'h1';
|
|
801
|
-
return React.createElement(Tag, { key: i, className: 'text-2xl font-bold mb-4 px-4', dangerouslySetInnerHTML: { __html: props.text || '' } });
|
|
802
|
-
|
|
803
|
-
case 'paragraph':
|
|
804
|
-
return React.createElement('p', { key: i, className: 'text-gray-700 mb-4 px-4 leading-relaxed', dangerouslySetInnerHTML: { __html: props.text || '' } });
|
|
805
|
-
|
|
806
|
-
case 'image':
|
|
807
|
-
return React.createElement('div', { key: i, className: 'px-4 mb-4' },
|
|
808
|
-
React.createElement('img', {
|
|
809
|
-
src: props.src || '',
|
|
810
|
-
alt: props.alt || '',
|
|
811
|
-
className: 'max-w-full rounded-lg',
|
|
812
|
-
style: { maxHeight: '400px', objectFit: 'contain' }
|
|
813
|
-
})
|
|
814
|
-
);
|
|
908
|
+
// --- Markdown-ish text rendering ---
|
|
909
|
+
function inlineMarkdown(text) {
|
|
910
|
+
return text
|
|
911
|
+
.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>')
|
|
912
|
+
.replace(/\\*(.+?)\\*/g, '<em>$1</em>')
|
|
913
|
+
.replace(/~~(.+?)~~/g, '<del class="opacity-60">$1</del>')
|
|
914
|
+
.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer" class="underline decoration-1 underline-offset-2 hover:opacity-80 transition-opacity">$1</a>');
|
|
915
|
+
}
|
|
815
916
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
917
|
+
function renderMarkdownish(text) {
|
|
918
|
+
const lines = text.split(/\\n/);
|
|
919
|
+
let html = '', inUl = false, inOl = false;
|
|
920
|
+
for (const line of lines) {
|
|
921
|
+
const trimmed = line.trim();
|
|
922
|
+
const bullet = trimmed.match(/^[-\u2022]\\s+(.*)/);
|
|
923
|
+
const ordered = trimmed.match(/^\\d+\\.\\s+(.*)/);
|
|
924
|
+
if (bullet) {
|
|
925
|
+
if (inOl) { html += '</ol>'; inOl = false; }
|
|
926
|
+
if (!inUl) { html += '<ul class="list-disc pl-5 my-3 space-y-1.5">'; inUl = true; }
|
|
927
|
+
html += '<li>' + inlineMarkdown(bullet[1]) + '</li>';
|
|
928
|
+
} else if (ordered) {
|
|
929
|
+
if (inUl) { html += '</ul>'; inUl = false; }
|
|
930
|
+
if (!inOl) { html += '<ol class="list-decimal pl-5 my-3 space-y-1.5">'; inOl = true; }
|
|
931
|
+
html += '<li>' + inlineMarkdown(ordered[1]) + '</li>';
|
|
932
|
+
} else {
|
|
933
|
+
if (inUl) { html += '</ul>'; inUl = false; }
|
|
934
|
+
if (inOl) { html += '</ol>'; inOl = false; }
|
|
935
|
+
if (trimmed === '') html += '<br/>';
|
|
936
|
+
else { if (html.length > 0 && !html.endsWith('>')) html += '<br/>'; html += inlineMarkdown(trimmed); }
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
if (inUl) html += '</ul>';
|
|
940
|
+
if (inOl) html += '</ol>';
|
|
941
|
+
return html;
|
|
942
|
+
}
|
|
826
943
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
944
|
+
function alignClass(align) {
|
|
945
|
+
if (align === 'center') return 'text-center';
|
|
946
|
+
if (align === 'right') return 'text-right';
|
|
947
|
+
return 'text-left';
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// --- Routing helpers ---
|
|
951
|
+
function getNextPageId(currentId, routing, formState) {
|
|
952
|
+
if (!routing || !routing.edges) return null;
|
|
953
|
+
const edges = routing.edges.filter(e => e.from === currentId);
|
|
954
|
+
// Sort by priority (lower = higher priority)
|
|
955
|
+
edges.sort((a, b) => (a.priority ?? 999) - (b.priority ?? 999));
|
|
956
|
+
for (const edge of edges) {
|
|
957
|
+
if (!edge.conditions || edge.conditions.length === 0) return edge.to;
|
|
958
|
+
const match = edge.conditions.every(cond => {
|
|
959
|
+
const val = formState[cond.field];
|
|
960
|
+
if (cond.operator === 'equals') return val === cond.value;
|
|
961
|
+
if (cond.operator === 'not_equals') return val !== cond.value;
|
|
962
|
+
if (cond.operator === 'contains') return typeof val === 'string' && val.includes(cond.value);
|
|
963
|
+
return true;
|
|
964
|
+
});
|
|
965
|
+
if (match) return edge.to;
|
|
966
|
+
}
|
|
967
|
+
// Fallback: default edge (no conditions)
|
|
968
|
+
const defaultEdge = edges.find(e => !e.conditions || e.conditions.length === 0);
|
|
969
|
+
return defaultEdge ? defaultEdge.to : null;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// --- Component Renderers ---
|
|
973
|
+
function RenderComponent({ comp, isCover, formState, onFieldChange }) {
|
|
974
|
+
const props = comp.props || {};
|
|
975
|
+
const type = comp.type;
|
|
976
|
+
const compClass = comp.className || '';
|
|
977
|
+
const compStyle = comp.style || {};
|
|
978
|
+
|
|
979
|
+
switch (type) {
|
|
980
|
+
case 'heading': {
|
|
981
|
+
const level = props.level ?? 1;
|
|
982
|
+
const tag = 'h' + Math.min(Math.max(level, 1), 6);
|
|
983
|
+
const sizes = {
|
|
984
|
+
1: 'text-4xl sm:text-5xl font-extrabold tracking-tight leading-[1.1]',
|
|
985
|
+
2: 'text-3xl sm:text-4xl font-bold tracking-tight leading-[1.15]',
|
|
986
|
+
3: 'text-2xl sm:text-3xl font-bold leading-tight',
|
|
987
|
+
4: 'text-xl sm:text-2xl font-semibold leading-snug',
|
|
988
|
+
5: 'text-lg sm:text-xl font-semibold',
|
|
989
|
+
6: 'text-base sm:text-lg font-medium',
|
|
990
|
+
};
|
|
991
|
+
return h('div', { className: alignClass(props.align) + ' space-y-3 ' + compClass, style: compStyle },
|
|
992
|
+
props.micro_heading ? h('p', { className: 'text-xs sm:text-sm font-medium uppercase tracking-widest ' + (isCover ? 'text-white/60' : 'text-gray-400') }, props.micro_heading) : null,
|
|
993
|
+
h(tag, {
|
|
994
|
+
className: (sizes[level] || sizes[1]) + ' ' + (isCover ? 'text-white drop-shadow-lg' : 'text-gray-900') + ' cf-text-balance',
|
|
995
|
+
style: level <= 2 ? { letterSpacing: '-0.025em' } : undefined,
|
|
996
|
+
}, props.text ?? props.content),
|
|
997
|
+
props.subtitle ? h('p', { className: 'text-base sm:text-lg font-normal leading-relaxed ' + (isCover ? 'text-white/75' : 'text-gray-500') }, props.subtitle) : null
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
case 'paragraph':
|
|
1002
|
+
return h('div', {
|
|
1003
|
+
className: alignClass(props.align) + ' ' + (isCover ? 'text-white/85' : 'text-gray-600') + ' text-lg leading-[1.7] [&_ul]:text-left [&_ol]:text-left ' + compClass,
|
|
1004
|
+
style: compStyle,
|
|
1005
|
+
dangerouslySetInnerHTML: { __html: renderMarkdownish(props.text ?? props.content ?? '') }
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
case 'image': {
|
|
1009
|
+
const borderRadius = props.border_radius ?? 16;
|
|
1010
|
+
const img = h('img', { src: props.src, alt: props.alt || '', className: 'ck-img', loading: 'lazy' });
|
|
1011
|
+
return h('div', { className: 'w-full overflow-hidden ' + compClass, style: { borderRadius, ...compStyle } },
|
|
1012
|
+
props.link ? h('a', { href: props.link, target: '_blank', rel: 'noopener noreferrer' }, img) : img
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
case 'video': {
|
|
1017
|
+
const src = props.src || '';
|
|
1018
|
+
const isYT = /(?:youtube\\.com|youtu\\.be)/.test(src);
|
|
1019
|
+
const isVimeo = /vimeo\\.com/.test(src);
|
|
1020
|
+
if (isYT) {
|
|
1021
|
+
const match = src.match(/(?:youtube\\.com\\/(?:watch\\?v=|embed\\/)|youtu\\.be\\/)([a-zA-Z0-9_-]{11})/);
|
|
1022
|
+
const embedUrl = match ? 'https://www.youtube.com/embed/' + match[1] : src;
|
|
1023
|
+
return h('div', { className: 'relative w-full overflow-hidden rounded-2xl shadow-lg ' + compClass, style: { paddingTop: '56.25%', ...compStyle } },
|
|
1024
|
+
h('iframe', { src: embedUrl, className: 'absolute inset-0 w-full h-full', allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture', allowFullScreen: true })
|
|
845
1025
|
);
|
|
1026
|
+
}
|
|
1027
|
+
if (isVimeo) {
|
|
1028
|
+
const match = src.match(/vimeo\\.com\\/(\\d+)/);
|
|
1029
|
+
const embedUrl = match ? 'https://player.vimeo.com/video/' + match[1] : src;
|
|
1030
|
+
return h('div', { className: 'relative w-full overflow-hidden rounded-2xl shadow-lg ' + compClass, style: { paddingTop: '56.25%', ...compStyle } },
|
|
1031
|
+
h('iframe', { src: embedUrl, className: 'absolute inset-0 w-full h-full', allow: 'autoplay; fullscreen; picture-in-picture', allowFullScreen: true })
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
return h('div', { className: 'w-full ' + compClass, style: compStyle },
|
|
1035
|
+
h('div', { className: 'relative w-full overflow-hidden rounded-2xl shadow-lg' },
|
|
1036
|
+
h('video', { src: props.hls_url || src, controls: true, playsInline: true, poster: props.poster, className: 'w-full' })
|
|
1037
|
+
)
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
case 'html':
|
|
1042
|
+
return h('div', { className: compClass, style: compStyle, dangerouslySetInnerHTML: { __html: props.content || '' } });
|
|
846
1043
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1044
|
+
case 'banner': {
|
|
1045
|
+
const variants = {
|
|
1046
|
+
info: { bg: 'bg-blue-50', border: 'border-blue-100', text: 'text-blue-700', icon: '\\u2139\\uFE0F' },
|
|
1047
|
+
warning: { bg: 'bg-amber-50', border: 'border-amber-100', text: 'text-amber-700', icon: '\\u26A0\\uFE0F' },
|
|
1048
|
+
success: { bg: 'bg-emerald-50', border: 'border-emerald-100', text: 'text-emerald-700', icon: '\\u2705' },
|
|
1049
|
+
error: { bg: 'bg-red-50', border: 'border-red-100', text: 'text-red-700', icon: '\\u274C' },
|
|
1050
|
+
};
|
|
1051
|
+
const v = variants[props.variant ?? props.style] || variants.info;
|
|
1052
|
+
const bannerText = props.text ?? props.content ?? '';
|
|
1053
|
+
if (isCover) {
|
|
1054
|
+
return h('div', { className: 'cf-banner-glass rounded-2xl px-5 py-3.5 flex items-center justify-center gap-3 ' + compClass, style: compStyle },
|
|
1055
|
+
h('span', { className: 'text-base flex-shrink-0' }, v.icon),
|
|
1056
|
+
h('div', { className: 'text-sm leading-relaxed text-white/90 font-medium', dangerouslySetInnerHTML: { __html: renderMarkdownish(bannerText) } })
|
|
855
1057
|
);
|
|
1058
|
+
}
|
|
1059
|
+
return h('div', { className: v.bg + ' ' + v.border + ' ' + v.text + ' border rounded-2xl px-5 py-4 flex items-start gap-3 ' + compClass, style: compStyle },
|
|
1060
|
+
h('span', { className: 'text-lg flex-shrink-0 mt-0.5' }, v.icon),
|
|
1061
|
+
h('div', { className: 'text-sm leading-relaxed', dangerouslySetInnerHTML: { __html: renderMarkdownish(bannerText) } })
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
856
1064
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1065
|
+
case 'callout': {
|
|
1066
|
+
const title = props.title || '';
|
|
1067
|
+
const text = props.text || props.content || '';
|
|
1068
|
+
return h('div', { className: 'rounded-2xl px-5 py-4 ' + compClass, style: { backgroundColor: (props.color || themeColor) + '0a', border: '1.5px solid ' + (props.color || themeColor) + '20', ...compStyle } },
|
|
1069
|
+
title ? h('p', { className: 'font-semibold text-sm mb-1', style: { color: props.color || themeColor } }, title) : null,
|
|
1070
|
+
h('div', { className: 'text-sm leading-relaxed text-gray-600', dangerouslySetInnerHTML: { __html: renderMarkdownish(text) } })
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
case 'divider':
|
|
1075
|
+
return h('hr', { className: 'border-t border-gray-100 my-4 ' + compClass, style: compStyle });
|
|
1076
|
+
|
|
1077
|
+
case 'pricing_card': {
|
|
1078
|
+
const features = props.features || [];
|
|
1079
|
+
return h('div', { className: 'pricing-card ' + compClass, style: compStyle },
|
|
1080
|
+
props.badge ? h('span', { className: 'inline-block text-xs font-bold uppercase tracking-wider px-3 py-1 rounded-full text-white mb-4', style: { backgroundColor: themeColor } }, props.badge) : null,
|
|
1081
|
+
props.title ? h('h3', { className: 'text-2xl font-bold text-gray-900 mb-2', style: { fontFamily: 'var(--font-display)' } }, props.title) : null,
|
|
1082
|
+
props.subtitle ? h('p', { className: 'text-gray-500 text-sm mb-6' }, props.subtitle) : null,
|
|
1083
|
+
props.price != null ? h('div', { className: 'mb-6' },
|
|
1084
|
+
props.original_price ? h('span', { className: 'text-lg text-gray-400 line-through mr-2' }, props.original_price) : null,
|
|
1085
|
+
h('span', { className: 'text-4xl font-extrabold', style: { fontFamily: 'var(--font-display)', color: themeColor } }, typeof props.price === 'number' ? '$' + props.price : props.price),
|
|
1086
|
+
props.period ? h('span', { className: 'text-gray-400 text-sm ml-1' }, '/' + props.period) : null
|
|
1087
|
+
) : null,
|
|
1088
|
+
features.length > 0 ? h('ul', { className: 'text-left space-y-3 mb-6' },
|
|
1089
|
+
...features.map((f, i) => h('li', { key: i, className: 'flex items-start gap-2.5 text-sm text-gray-600' },
|
|
1090
|
+
h('span', { className: 'flex-shrink-0 mt-0.5', style: { color: themeColor } }, '\\u2713'),
|
|
1091
|
+
h('span', null, typeof f === 'string' ? f : f.text || f.label || '')
|
|
1092
|
+
))
|
|
1093
|
+
) : null,
|
|
1094
|
+
props.cta_text ? h('button', { className: 'cf-btn-primary w-full text-white', style: { backgroundColor: themeColor } }, props.cta_text) : null,
|
|
1095
|
+
props.reassurance ? h('p', { className: 'text-xs text-gray-400 mt-2' }, props.reassurance) : null
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
case 'testimonial':
|
|
1100
|
+
return h('div', { className: 'cf-card p-6 ' + compClass, style: compStyle },
|
|
1101
|
+
props.quote ? h('p', { className: 'text-gray-700 text-base leading-relaxed italic mb-4' }, '"' + props.quote + '"') : null,
|
|
1102
|
+
h('div', { className: 'flex items-center gap-3' },
|
|
1103
|
+
props.avatar ? h('img', { src: props.avatar, className: 'w-10 h-10 rounded-full object-cover' }) : null,
|
|
1104
|
+
h('div', null,
|
|
1105
|
+
props.name ? h('p', { className: 'font-semibold text-sm text-gray-900' }, props.name) : null,
|
|
1106
|
+
props.title_text || props.role ? h('p', { className: 'text-xs text-gray-400' }, props.title_text || props.role) : null
|
|
867
1107
|
)
|
|
868
|
-
)
|
|
1108
|
+
)
|
|
1109
|
+
);
|
|
1110
|
+
|
|
1111
|
+
case 'faq': {
|
|
1112
|
+
const items = props.items || [];
|
|
1113
|
+
return h('div', { className: 'space-y-3 ' + compClass, style: compStyle },
|
|
1114
|
+
...items.map((item, i) => h(FaqItem, { key: i, question: item.question || item.q, answer: item.answer || item.a, isCover }))
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
case 'timeline': {
|
|
1119
|
+
const items = props.items || [];
|
|
1120
|
+
return h('div', { className: 'relative pl-8 space-y-8 ' + compClass, style: compStyle },
|
|
1121
|
+
h('div', { className: 'absolute left-3 top-2 bottom-2 w-0.5 bg-gray-200' }),
|
|
1122
|
+
...items.map((item, i) => h('div', { key: i, className: 'relative' },
|
|
1123
|
+
h('div', { className: 'absolute -left-5 w-3 h-3 rounded-full border-2 border-white', style: { backgroundColor: themeColor, boxShadow: '0 0 0 3px ' + themeColor + '20' } }),
|
|
1124
|
+
h('div', null,
|
|
1125
|
+
item.title ? h('p', { className: 'font-semibold text-sm text-gray-900' }, item.title) : null,
|
|
1126
|
+
item.description ? h('p', { className: 'text-sm text-gray-500 mt-1' }, item.description) : null
|
|
1127
|
+
)
|
|
1128
|
+
))
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
case 'file_download':
|
|
1133
|
+
return h('div', { className: compClass, style: compStyle },
|
|
1134
|
+
h('a', { href: props.src || props.href || '#', download: props.filename || 'file', className: 'inline-flex items-center gap-3 border-1.5 border-gray-200 rounded-xl px-5 py-3.5 text-sm font-medium text-gray-700 hover:bg-gray-50 hover:border-gray-300 transition-all' },
|
|
1135
|
+
h('svg', { className: 'w-5 h-5 text-gray-400', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', strokeWidth: 2 },
|
|
1136
|
+
h('path', { strokeLinecap: 'round', strokeLinejoin: 'round', d: 'M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z' })
|
|
1137
|
+
),
|
|
1138
|
+
props.filename || props.label || 'Download File'
|
|
1139
|
+
)
|
|
1140
|
+
);
|
|
1141
|
+
|
|
1142
|
+
case 'iframe':
|
|
1143
|
+
return h('div', { className: 'w-full overflow-hidden rounded-2xl ' + compClass, style: { ...compStyle } },
|
|
1144
|
+
h('iframe', { src: props.src || props.url, className: 'w-full border-0', style: { height: props.height || '400px' }, allow: props.allow || 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope', allowFullScreen: true })
|
|
1145
|
+
);
|
|
1146
|
+
|
|
1147
|
+
// --- Input components ---
|
|
1148
|
+
case 'short_text':
|
|
1149
|
+
case 'email':
|
|
1150
|
+
case 'phone':
|
|
1151
|
+
case 'url':
|
|
1152
|
+
case 'number':
|
|
1153
|
+
case 'password':
|
|
1154
|
+
return h(TextInput, { comp, type, formState, onFieldChange, isCover, compClass, compStyle });
|
|
1155
|
+
|
|
1156
|
+
case 'long_text':
|
|
1157
|
+
return h('div', { className: 'space-y-1.5 ' + compClass, style: compStyle },
|
|
1158
|
+
props.label ? h('label', { className: 'block text-base font-medium ' + (isCover ? 'text-white' : 'text-gray-700') },
|
|
1159
|
+
props.label,
|
|
1160
|
+
(props.required) ? h('span', { className: 'text-red-500 ml-1' }, '*') : null
|
|
1161
|
+
) : null,
|
|
1162
|
+
props.description ? h('p', { className: 'text-xs ' + (isCover ? 'text-white/70' : 'text-gray-500') }, props.description) : null,
|
|
1163
|
+
h('textarea', {
|
|
1164
|
+
className: 'cf-input min-h-[80px] resize-y',
|
|
1165
|
+
placeholder: props.placeholder || '',
|
|
1166
|
+
rows: props.rows || 4,
|
|
1167
|
+
value: formState[comp.id] ?? '',
|
|
1168
|
+
onChange: (e) => onFieldChange(comp.id, e.target.value),
|
|
1169
|
+
})
|
|
1170
|
+
);
|
|
1171
|
+
|
|
1172
|
+
case 'multiple_choice':
|
|
1173
|
+
return h(MultipleChoiceInput, { comp, formState, onFieldChange, isCover, compClass, compStyle });
|
|
1174
|
+
|
|
1175
|
+
case 'checkboxes':
|
|
1176
|
+
return h(CheckboxesInput, { comp, formState, onFieldChange, isCover, compClass, compStyle });
|
|
869
1177
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1178
|
+
case 'dropdown':
|
|
1179
|
+
return h('div', { className: 'space-y-1.5 ' + compClass, style: compStyle },
|
|
1180
|
+
props.label ? h('label', { className: 'block text-base font-medium ' + (isCover ? 'text-white' : 'text-gray-700') },
|
|
1181
|
+
props.label,
|
|
1182
|
+
(props.required) ? h('span', { className: 'text-red-500 ml-1' }, '*') : null
|
|
1183
|
+
) : null,
|
|
1184
|
+
h('select', {
|
|
1185
|
+
className: 'cf-input',
|
|
1186
|
+
value: formState[comp.id] ?? '',
|
|
1187
|
+
onChange: (e) => onFieldChange(comp.id, e.target.value),
|
|
1188
|
+
},
|
|
1189
|
+
h('option', { value: '' }, props.placeholder || 'Select...'),
|
|
1190
|
+
...(props.options || []).map((opt, j) =>
|
|
1191
|
+
h('option', { key: j, value: typeof opt === 'string' ? opt : opt.value },
|
|
1192
|
+
typeof opt === 'string' ? opt : opt.label || opt.value || ''
|
|
879
1193
|
)
|
|
880
1194
|
)
|
|
881
|
-
)
|
|
1195
|
+
)
|
|
1196
|
+
);
|
|
1197
|
+
|
|
1198
|
+
case 'slider':
|
|
1199
|
+
return h(SliderInput, { comp, formState, onFieldChange, isCover, compClass, compStyle });
|
|
1200
|
+
|
|
1201
|
+
case 'star_rating':
|
|
1202
|
+
return h(StarRatingInput, { comp, formState, onFieldChange, isCover, compClass, compStyle });
|
|
1203
|
+
|
|
1204
|
+
case 'switch':
|
|
1205
|
+
case 'checkbox':
|
|
1206
|
+
return h(SwitchInput, { comp, type, formState, onFieldChange, isCover, compClass, compStyle });
|
|
1207
|
+
|
|
1208
|
+
case 'payment':
|
|
1209
|
+
return h('div', { className: 'checkout-stub ' + compClass, style: compStyle },
|
|
1210
|
+
h('h3', null, 'Stripe Checkout (Dev Stub)'),
|
|
1211
|
+
h('p', null, 'Payment processing is disabled in local dev mode.'),
|
|
1212
|
+
props.amount ? h('p', { className: 'mt-2 font-bold text-lg' },
|
|
1213
|
+
(props.currency || 'USD').toUpperCase() + ' ' + (props.amount / 100).toFixed(2)
|
|
1214
|
+
) : null
|
|
1215
|
+
);
|
|
1216
|
+
|
|
1217
|
+
default:
|
|
1218
|
+
return h('div', {
|
|
1219
|
+
className: 'border-2 border-dashed border-gray-300 rounded-xl p-4 text-center text-gray-400 text-sm ' + compClass,
|
|
1220
|
+
style: compStyle,
|
|
1221
|
+
}, type + (props.label ? ': ' + props.label : '') + ' (' + comp.id + ')');
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// --- Sub-components ---
|
|
882
1226
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1227
|
+
function FaqItem({ question, answer, isCover }) {
|
|
1228
|
+
const [open, setOpen] = React.useState(false);
|
|
1229
|
+
return h('div', { className: 'rounded-2xl border border-gray-200 overflow-hidden' },
|
|
1230
|
+
h('button', {
|
|
1231
|
+
className: 'w-full flex items-center justify-between px-5 py-4 text-left text-sm font-semibold ' + (isCover ? 'text-white' : 'text-gray-900'),
|
|
1232
|
+
onClick: () => setOpen(!open),
|
|
1233
|
+
},
|
|
1234
|
+
h('span', null, question),
|
|
1235
|
+
h('svg', { className: 'w-4 h-4 transition-transform ' + (open ? 'rotate-180' : ''), fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', strokeWidth: 2 },
|
|
1236
|
+
h('path', { strokeLinecap: 'round', strokeLinejoin: 'round', d: 'M19 9l-7 7-7-7' })
|
|
1237
|
+
)
|
|
1238
|
+
),
|
|
1239
|
+
open ? h('div', { className: 'px-5 pb-4 text-sm text-gray-600 leading-relaxed', dangerouslySetInnerHTML: { __html: renderMarkdownish(answer || '') } }) : null
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function TextInput({ comp, type, formState, onFieldChange, isCover, compClass, compStyle }) {
|
|
1244
|
+
const props = comp.props || {};
|
|
1245
|
+
const inputType = type === 'email' ? 'email' : type === 'phone' ? 'tel' : type === 'url' ? 'url' : type === 'number' ? 'number' : type === 'password' ? 'password' : 'text';
|
|
1246
|
+
return h('div', { className: 'space-y-1.5 ' + compClass, style: compStyle },
|
|
1247
|
+
props.label ? h('label', { className: 'block text-base font-medium ' + (isCover ? 'text-white' : 'text-gray-700') },
|
|
1248
|
+
props.label,
|
|
1249
|
+
(props.required) ? h('span', { className: 'text-red-500 ml-1' }, '*') : null
|
|
1250
|
+
) : null,
|
|
1251
|
+
(props.sublabel || props.subheading) ? h('p', { className: 'text-xs font-medium ' + (isCover ? 'text-white/60' : 'text-gray-400') }, props.sublabel || props.subheading) : null,
|
|
1252
|
+
props.description ? h('p', { className: 'text-xs ' + (isCover ? 'text-white/70' : 'text-gray-500') }, props.description) : null,
|
|
1253
|
+
h('input', {
|
|
1254
|
+
type: inputType,
|
|
1255
|
+
className: 'cf-input' + (isCover ? ' bg-white/10 text-white border-white/20 placeholder-white/40' : ''),
|
|
1256
|
+
placeholder: props.placeholder || '',
|
|
1257
|
+
value: formState[comp.id] ?? '',
|
|
1258
|
+
onChange: (e) => onFieldChange(comp.id, e.target.value),
|
|
1259
|
+
})
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function MultipleChoiceInput({ comp, formState, onFieldChange, isCover, compClass, compStyle }) {
|
|
1264
|
+
const props = comp.props || {};
|
|
1265
|
+
const selected = formState[comp.id] ?? null;
|
|
1266
|
+
return h('div', { className: 'space-y-1.5 ' + compClass, style: compStyle },
|
|
1267
|
+
props.label ? h('label', { className: 'block text-base font-medium ' + (isCover ? 'text-white' : 'text-gray-700') },
|
|
1268
|
+
props.label,
|
|
1269
|
+
(props.required) ? h('span', { className: 'text-red-500 ml-1' }, '*') : null
|
|
1270
|
+
) : null,
|
|
1271
|
+
props.description ? h('p', { className: 'text-xs ' + (isCover ? 'text-white/70' : 'text-gray-500') }, props.description) : null,
|
|
1272
|
+
h('div', { className: 'space-y-2.5' },
|
|
1273
|
+
...(props.options || []).map((opt, j) => {
|
|
1274
|
+
const value = typeof opt === 'string' ? opt : opt.value;
|
|
1275
|
+
const label = typeof opt === 'string' ? opt : opt.label || opt.value || '';
|
|
1276
|
+
const isSelected = selected === value;
|
|
1277
|
+
return h('button', {
|
|
1278
|
+
key: j,
|
|
1279
|
+
className: 'cf-choice flex items-center gap-3',
|
|
1280
|
+
'data-selected': isSelected ? 'true' : 'false',
|
|
1281
|
+
style: isSelected ? { borderColor: themeColor, boxShadow: '0 0 0 3px ' + themeColor + '26' } : undefined,
|
|
1282
|
+
onClick: () => onFieldChange(comp.id, value),
|
|
1283
|
+
},
|
|
1284
|
+
h('span', {
|
|
1285
|
+
className: 'w-5 h-5 rounded-full border-2 flex items-center justify-center flex-shrink-0 transition-all',
|
|
1286
|
+
style: isSelected ? { borderColor: themeColor, backgroundColor: themeColor } : { borderColor: '#d1d5db' },
|
|
1287
|
+
}, isSelected ? h('span', { className: 'w-2 h-2 rounded-full bg-white' }) : null),
|
|
1288
|
+
h('span', { className: 'text-sm font-medium ' + (isCover ? 'text-white' : 'text-gray-700') }, label)
|
|
890
1289
|
);
|
|
1290
|
+
})
|
|
1291
|
+
)
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
891
1294
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1295
|
+
function CheckboxesInput({ comp, formState, onFieldChange, isCover, compClass, compStyle }) {
|
|
1296
|
+
const props = comp.props || {};
|
|
1297
|
+
const selected = formState[comp.id] || [];
|
|
1298
|
+
return h('div', { className: 'space-y-1.5 ' + compClass, style: compStyle },
|
|
1299
|
+
props.label ? h('label', { className: 'block text-base font-medium ' + (isCover ? 'text-white' : 'text-gray-700') },
|
|
1300
|
+
props.label,
|
|
1301
|
+
(props.required) ? h('span', { className: 'text-red-500 ml-1' }, '*') : null
|
|
1302
|
+
) : null,
|
|
1303
|
+
h('div', { className: 'space-y-2.5' },
|
|
1304
|
+
...(props.options || []).map((opt, j) => {
|
|
1305
|
+
const value = typeof opt === 'string' ? opt : opt.value;
|
|
1306
|
+
const label = typeof opt === 'string' ? opt : opt.label || opt.value || '';
|
|
1307
|
+
const isChecked = Array.isArray(selected) && selected.includes(value);
|
|
1308
|
+
return h('button', {
|
|
1309
|
+
key: j,
|
|
1310
|
+
className: 'cf-choice flex items-center gap-3',
|
|
1311
|
+
'data-selected': isChecked ? 'true' : 'false',
|
|
1312
|
+
style: isChecked ? { borderColor: themeColor, boxShadow: '0 0 0 3px ' + themeColor + '26' } : undefined,
|
|
1313
|
+
onClick: () => {
|
|
1314
|
+
const arr = Array.isArray(selected) ? [...selected] : [];
|
|
1315
|
+
if (isChecked) onFieldChange(comp.id, arr.filter(v => v !== value));
|
|
1316
|
+
else onFieldChange(comp.id, [...arr, value]);
|
|
1317
|
+
},
|
|
902
1318
|
},
|
|
903
|
-
|
|
904
|
-
className: '
|
|
905
|
-
|
|
906
|
-
}
|
|
1319
|
+
h('span', {
|
|
1320
|
+
className: 'w-5 h-5 rounded-md border-2 flex items-center justify-center flex-shrink-0 transition-all',
|
|
1321
|
+
style: isChecked ? { borderColor: themeColor, backgroundColor: themeColor } : { borderColor: '#d1d5db' },
|
|
1322
|
+
}, isChecked ? h('svg', { className: 'w-3 h-3 text-white', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', strokeWidth: 3 },
|
|
1323
|
+
h('path', { strokeLinecap: 'round', strokeLinejoin: 'round', d: 'M5 13l4 4L19 7' })
|
|
1324
|
+
) : null),
|
|
1325
|
+
h('span', { className: 'text-sm font-medium ' + (isCover ? 'text-white' : 'text-gray-700') }, label)
|
|
907
1326
|
);
|
|
1327
|
+
})
|
|
1328
|
+
)
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function SliderInput({ comp, formState, onFieldChange, isCover, compClass, compStyle }) {
|
|
1333
|
+
const props = comp.props || {};
|
|
1334
|
+
const min = props.min ?? 0, max = props.max ?? 100, step = props.step ?? 1;
|
|
1335
|
+
const value = formState[comp.id] ?? props.default_value ?? min;
|
|
1336
|
+
return h('div', { className: 'space-y-1.5 ' + compClass, style: compStyle },
|
|
1337
|
+
props.label ? h('label', { className: 'block text-base font-medium ' + (isCover ? 'text-white' : 'text-gray-700') }, props.label) : null,
|
|
1338
|
+
h('div', { className: 'flex items-center gap-4' },
|
|
1339
|
+
h('input', { type: 'range', min, max, step, value, className: 'flex-1', style: { accentColor: themeColor }, onChange: (e) => onFieldChange(comp.id, Number(e.target.value)) }),
|
|
1340
|
+
h('span', { className: 'text-sm font-semibold min-w-[3ch] text-right', style: { color: themeColor } }, value)
|
|
1341
|
+
)
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
908
1344
|
|
|
909
|
-
|
|
910
|
-
|
|
1345
|
+
function StarRatingInput({ comp, formState, onFieldChange, isCover, compClass, compStyle }) {
|
|
1346
|
+
const props = comp.props || {};
|
|
1347
|
+
const max = props.max ?? 5;
|
|
1348
|
+
const value = formState[comp.id] ?? 0;
|
|
1349
|
+
return h('div', { className: 'space-y-1.5 ' + compClass, style: compStyle },
|
|
1350
|
+
props.label ? h('label', { className: 'block text-base font-medium ' + (isCover ? 'text-white' : 'text-gray-700') }, props.label) : null,
|
|
1351
|
+
h('div', { className: 'flex gap-1' },
|
|
1352
|
+
...Array.from({ length: max }, (_, i) => h('button', {
|
|
1353
|
+
key: i,
|
|
1354
|
+
className: 'text-2xl transition-transform hover:scale-125',
|
|
1355
|
+
onClick: () => onFieldChange(comp.id, i + 1),
|
|
1356
|
+
}, i < value ? '\\u2605' : '\\u2606'))
|
|
1357
|
+
)
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
911
1360
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1361
|
+
function SwitchInput({ comp, type, formState, onFieldChange, isCover, compClass, compStyle }) {
|
|
1362
|
+
const props = comp.props || {};
|
|
1363
|
+
const checked = !!formState[comp.id];
|
|
1364
|
+
return h('div', { className: 'flex items-center gap-3 ' + compClass, style: compStyle },
|
|
1365
|
+
h('button', {
|
|
1366
|
+
className: 'relative w-11 h-6 rounded-full transition-colors',
|
|
1367
|
+
style: { backgroundColor: checked ? themeColor : '#d1d5db' },
|
|
1368
|
+
onClick: () => onFieldChange(comp.id, !checked),
|
|
1369
|
+
},
|
|
1370
|
+
h('span', { className: 'absolute top-0.5 w-5 h-5 rounded-full bg-white shadow transition-transform', style: { left: checked ? '22px' : '2px' } })
|
|
1371
|
+
),
|
|
1372
|
+
props.label ? h('span', { className: 'text-sm font-medium ' + (isCover ? 'text-white' : 'text-gray-700') }, props.label) : null,
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
920
1375
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1376
|
+
// --- Main App ---
|
|
1377
|
+
function CatalogPreview({ catalog }) {
|
|
1378
|
+
const pages = catalog.pages || {};
|
|
1379
|
+
const pageKeys = Object.keys(pages);
|
|
1380
|
+
const routing = catalog.routing || {};
|
|
1381
|
+
const [currentPageId, setCurrentPageId] = React.useState(routing.entry || pageKeys[0] || null);
|
|
1382
|
+
const [formState, setFormState] = React.useState({});
|
|
1383
|
+
const [history, setHistory] = React.useState([]);
|
|
1384
|
+
|
|
1385
|
+
const page = currentPageId ? pages[currentPageId] : null;
|
|
1386
|
+
const isCover = page?.layout === 'cover';
|
|
1387
|
+
const isLastPage = (() => {
|
|
1388
|
+
if (!routing.edges) return true;
|
|
1389
|
+
return !routing.edges.some(e => e.from === currentPageId);
|
|
1390
|
+
})();
|
|
1391
|
+
|
|
1392
|
+
const onFieldChange = React.useCallback((id, value) => {
|
|
1393
|
+
setFormState(prev => ({ ...prev, [id]: value }));
|
|
1394
|
+
}, []);
|
|
1395
|
+
|
|
1396
|
+
const handleNext = React.useCallback(() => {
|
|
1397
|
+
const nextId = getNextPageId(currentPageId, routing, formState);
|
|
1398
|
+
if (nextId && pages[nextId]) {
|
|
1399
|
+
setHistory(prev => [...prev, currentPageId]);
|
|
1400
|
+
setCurrentPageId(nextId);
|
|
1401
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
926
1402
|
}
|
|
927
|
-
}
|
|
1403
|
+
}, [currentPageId, routing, formState, pages]);
|
|
1404
|
+
|
|
1405
|
+
const handleBack = React.useCallback(() => {
|
|
1406
|
+
if (history.length > 0) {
|
|
1407
|
+
const prev = history[history.length - 1];
|
|
1408
|
+
setHistory(h => h.slice(0, -1));
|
|
1409
|
+
setCurrentPageId(prev);
|
|
1410
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
1411
|
+
}
|
|
1412
|
+
}, [history]);
|
|
928
1413
|
|
|
929
1414
|
if (!page) {
|
|
930
|
-
return
|
|
1415
|
+
return h('div', { className: 'min-h-screen flex items-center justify-center bg-gray-50' },
|
|
1416
|
+
h('p', { className: 'text-gray-500' }, 'No pages found in catalog.')
|
|
1417
|
+
);
|
|
931
1418
|
}
|
|
932
1419
|
|
|
933
1420
|
const components = page.components || [];
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1421
|
+
const bgImage = page.background_image || catalog.settings?.theme?.background_image;
|
|
1422
|
+
|
|
1423
|
+
// Cover page layout
|
|
1424
|
+
if (isCover) {
|
|
1425
|
+
return h('div', null,
|
|
1426
|
+
h('div', {
|
|
1427
|
+
className: 'cf-page cf-noise min-h-screen flex items-center justify-center relative overflow-hidden',
|
|
1428
|
+
style: {
|
|
1429
|
+
backgroundImage: bgImage ? 'url(' + bgImage + ')' : 'linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%)',
|
|
1430
|
+
backgroundSize: 'cover', backgroundPosition: 'center',
|
|
1431
|
+
},
|
|
1432
|
+
},
|
|
1433
|
+
h('div', { className: 'cf-cover-overlay absolute inset-0' }),
|
|
1434
|
+
h('div', { className: 'cf-cover-content relative max-w-2xl mx-auto px-6 py-20 text-center text-white' },
|
|
1435
|
+
h('div', { className: 'page-enter-active space-y-7 flex flex-col items-stretch text-center' },
|
|
1436
|
+
...components.map((comp, i) => h(RenderComponent, { key: comp.id || i, comp, isCover: true, formState, onFieldChange })),
|
|
1437
|
+
// CTA button
|
|
1438
|
+
h('div', { className: 'mt-8' },
|
|
1439
|
+
h('button', {
|
|
1440
|
+
className: 'cf-btn-primary w-full py-4 text-lg',
|
|
1441
|
+
style: { backgroundColor: themeColor },
|
|
1442
|
+
onClick: handleNext,
|
|
1443
|
+
}, page.submit_label || (isLastPage ? 'Submit' : 'Get Started'))
|
|
1444
|
+
)
|
|
1445
|
+
)
|
|
1446
|
+
)
|
|
1447
|
+
),
|
|
1448
|
+
// Page nav
|
|
1449
|
+
h(PageNav, { pageKeys, pages, currentPageId, onSelect: (id) => { setCurrentPageId(id); setHistory([]); } })
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// Standard page layout
|
|
1454
|
+
const topBar = catalog.settings?.top_bar;
|
|
1455
|
+
const progressSteps = catalog.settings?.progress_steps;
|
|
1456
|
+
const topBarEnabled = topBar?.enabled !== false && catalog.settings?.top_bar;
|
|
1457
|
+
|
|
1458
|
+
return h('div', null,
|
|
1459
|
+
h('div', {
|
|
1460
|
+
className: 'cf-page min-h-screen',
|
|
1461
|
+
style: { background: 'linear-gradient(180deg, #f8f9fc 0%, #f0f2f7 100%)' },
|
|
1462
|
+
},
|
|
1463
|
+
// Top bar
|
|
1464
|
+
topBarEnabled ? h('div', { className: 'cf-topbar fixed top-[28px] left-0 right-0 z-50 border-b border-gray-200/60' },
|
|
1465
|
+
h('div', { className: 'relative flex items-center justify-center px-4 py-3 min-h-[48px]' },
|
|
1466
|
+
history.length > 0 ? h('button', {
|
|
1467
|
+
className: 'absolute left-3 top-1/2 -translate-y-1/2 w-9 h-9 flex items-center justify-center rounded-xl text-gray-400 hover:text-gray-700 hover:bg-gray-100/80 transition-all',
|
|
1468
|
+
onClick: handleBack,
|
|
1469
|
+
},
|
|
1470
|
+
h('svg', { className: 'w-5 h-5', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', strokeWidth: 2 },
|
|
1471
|
+
h('path', { strokeLinecap: 'round', strokeLinejoin: 'round', d: 'M15.75 19.5L8.25 12l7.5-7.5' })
|
|
1472
|
+
)
|
|
1473
|
+
) : null,
|
|
1474
|
+
topBar?.title ? h('span', { className: 'text-sm font-medium text-gray-700' }, topBar.title) : null,
|
|
1475
|
+
progressSteps ? h(Stepper, { steps: progressSteps, currentPageId, themeColor }) : null,
|
|
1476
|
+
)
|
|
1477
|
+
) : null,
|
|
1478
|
+
|
|
1479
|
+
// Page content
|
|
1480
|
+
h('div', { className: 'max-w-2xl mx-auto px-6 pb-8', style: { paddingTop: topBarEnabled ? '100px' : '60px' } },
|
|
1481
|
+
page.description ? h('p', { className: 'text-sm text-gray-400 mb-8 text-center font-medium tracking-wide', style: { fontFamily: 'var(--font-display)' } }, page.description) : null,
|
|
1482
|
+
h('div', { className: 'page-enter-active space-y-5' },
|
|
1483
|
+
...components.map((comp, i) => h(RenderComponent, { key: comp.id || i, comp, isCover: false, formState, onFieldChange })),
|
|
1484
|
+
// Navigation button
|
|
1485
|
+
!page.hide_navigation ? h('div', { className: 'mt-8' },
|
|
1486
|
+
h('button', {
|
|
1487
|
+
className: 'cf-btn-primary inline-flex items-center gap-2 text-base',
|
|
1488
|
+
style: { backgroundColor: themeColor },
|
|
1489
|
+
onClick: handleNext,
|
|
1490
|
+
},
|
|
1491
|
+
page.submit_label || (isLastPage ? 'Submit' : 'Continue'),
|
|
1492
|
+
!isLastPage ? h('svg', { className: 'w-4 h-4', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', strokeWidth: 2.5 },
|
|
1493
|
+
h('path', { strokeLinecap: 'round', strokeLinejoin: 'round', d: 'M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3' })
|
|
1494
|
+
) : null
|
|
1495
|
+
),
|
|
1496
|
+
page.submit_reassurance ? h('p', { className: 'text-xs text-gray-400 mt-1.5' }, page.submit_reassurance) : null,
|
|
1497
|
+
) : null,
|
|
1498
|
+
),
|
|
1499
|
+
h('div', { className: 'mt-10 text-center text-[11px] text-gray-300 font-medium tracking-wide', style: { fontFamily: 'var(--font-display)' } }, 'Powered by Catalog Kit'),
|
|
946
1500
|
)
|
|
947
1501
|
),
|
|
948
|
-
// Page
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1502
|
+
// Page nav
|
|
1503
|
+
h(PageNav, { pageKeys, pages, currentPageId, onSelect: (id) => { setCurrentPageId(id); setHistory([]); } })
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function Stepper({ steps, currentPageId, themeColor }) {
|
|
1508
|
+
const currentStepIndex = steps.findIndex(s => s.pages && s.pages.includes(currentPageId));
|
|
1509
|
+
return h('div', { className: 'flex items-center justify-center gap-0', style: { fontFamily: 'var(--font-display)' } },
|
|
1510
|
+
...steps.map((step, i) => h(React.Fragment, { key: step.id || i },
|
|
1511
|
+
i > 0 ? h('div', { className: 'w-10 sm:w-16 h-0.5 rounded-full transition-all duration-500', style: { backgroundColor: i <= currentStepIndex ? themeColor : '#e8e9ee' } }) : null,
|
|
1512
|
+
h('div', { className: 'flex items-center gap-1.5' },
|
|
1513
|
+
h('div', { className: 'w-7 h-7 rounded-full flex items-center justify-center transition-all duration-300', style: {
|
|
1514
|
+
backgroundColor: i <= currentStepIndex ? themeColor : '#f0f1f5',
|
|
1515
|
+
boxShadow: i === currentStepIndex ? '0 0 0 4px ' + themeColor + '20' : 'none',
|
|
1516
|
+
} },
|
|
1517
|
+
i < currentStepIndex
|
|
1518
|
+
? h('svg', { className: 'w-3.5 h-3.5 text-white', fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', strokeWidth: 3 },
|
|
1519
|
+
h('path', { strokeLinecap: 'round', strokeLinejoin: 'round', d: 'M5 13l4 4L19 7' }))
|
|
1520
|
+
: i === currentStepIndex
|
|
1521
|
+
? h('div', { className: 'w-2 h-2 rounded-full bg-white' })
|
|
1522
|
+
: h('div', { className: 'w-2 h-2 rounded-full bg-gray-300' })
|
|
1523
|
+
),
|
|
1524
|
+
h('span', {
|
|
1525
|
+
className: 'text-[11px] font-bold tracking-wider uppercase transition-colors duration-300 ' + (i <= currentStepIndex ? '' : 'text-gray-400'),
|
|
1526
|
+
style: i <= currentStepIndex ? { color: themeColor } : undefined,
|
|
1527
|
+
}, step.label)
|
|
1528
|
+
)
|
|
1529
|
+
))
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function PageNav({ pageKeys, pages, currentPageId, onSelect }) {
|
|
1534
|
+
return h('div', { className: 'dev-page-nav' },
|
|
1535
|
+
...pageKeys.map(key => h('button', {
|
|
1536
|
+
key,
|
|
1537
|
+
className: key === currentPageId ? 'active' : '',
|
|
1538
|
+
onClick: () => onSelect(key),
|
|
1539
|
+
}, pages[key].title || key))
|
|
962
1540
|
);
|
|
963
1541
|
}
|
|
964
1542
|
|
|
1543
|
+
// --- Auto-reload via SSE ---
|
|
1544
|
+
const evtSource = new EventSource('/__dev_sse');
|
|
1545
|
+
evtSource.onmessage = () => window.location.reload();
|
|
1546
|
+
evtSource.onerror = () => {};
|
|
1547
|
+
|
|
1548
|
+
// --- Mount ---
|
|
965
1549
|
const root = ReactDOM.createRoot(document.getElementById('catalog-root'));
|
|
966
|
-
root.render(
|
|
1550
|
+
root.render(h(CatalogPreview, { catalog: schema }));
|
|
967
1551
|
</script>
|
|
968
1552
|
</body>
|
|
969
1553
|
</html>`;
|
|
@@ -998,8 +1582,30 @@ async function catalogDev(file, opts) {
|
|
|
998
1582
|
console.log(` Pages: ${Object.keys(schema.pages || {}).length}`);
|
|
999
1583
|
console.log(` Entry: ${schema.routing?.entry || "first page"}`);
|
|
1000
1584
|
console.log();
|
|
1585
|
+
const sseClients = /* @__PURE__ */ new Set();
|
|
1586
|
+
function notifyReload() {
|
|
1587
|
+
sseClients.forEach((client) => {
|
|
1588
|
+
try {
|
|
1589
|
+
client.write("data: reload\n\n");
|
|
1590
|
+
} catch {
|
|
1591
|
+
sseClients.delete(client);
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1001
1595
|
const server = createServer(async (req, res) => {
|
|
1002
1596
|
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
1597
|
+
if (url.pathname === "/__dev_sse") {
|
|
1598
|
+
res.writeHead(200, {
|
|
1599
|
+
"Content-Type": "text/event-stream",
|
|
1600
|
+
"Cache-Control": "no-cache",
|
|
1601
|
+
"Connection": "keep-alive",
|
|
1602
|
+
"Access-Control-Allow-Origin": "*"
|
|
1603
|
+
});
|
|
1604
|
+
res.write("data: connected\n\n");
|
|
1605
|
+
sseClients.add(res);
|
|
1606
|
+
req.on("close", () => sseClients.delete(res));
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1003
1609
|
if (url.pathname.startsWith("/assets/")) {
|
|
1004
1610
|
const relativePath = decodeURIComponent(url.pathname.slice("/assets/".length));
|
|
1005
1611
|
const filePath = join2(catalogDir, relativePath);
|
|
@@ -1054,7 +1660,8 @@ async function catalogDev(file, opts) {
|
|
|
1054
1660
|
return;
|
|
1055
1661
|
}
|
|
1056
1662
|
schema = resolveLocalAssets(schema, catalogDir, localBaseUrl);
|
|
1057
|
-
reloadSpinner.succeed(`Reloaded \u2014
|
|
1663
|
+
reloadSpinner.succeed(`Reloaded \u2014 auto-refreshing browser`);
|
|
1664
|
+
notifyReload();
|
|
1058
1665
|
} catch (err) {
|
|
1059
1666
|
reloadSpinner.warn(`Reload failed: ${err.message}`);
|
|
1060
1667
|
}
|