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