@officexapp/catalogs-cli 0.2.1 → 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 +774 -165
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import {
|
|
5
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
6
8
|
|
|
7
9
|
// src/config.ts
|
|
8
10
|
var DEFAULT_API_URL = "https://api.catalogkit.cc";
|
|
@@ -730,38 +732,158 @@ function getMime(filepath) {
|
|
|
730
732
|
}
|
|
731
733
|
function buildPreviewHtml(schema, port) {
|
|
732
734
|
const schemaJson = JSON.stringify(schema);
|
|
735
|
+
const themeColor = schema.settings?.theme?.primary_color || "#6366f1";
|
|
733
736
|
return `<!DOCTYPE html>
|
|
734
737
|
<html lang="en">
|
|
735
738
|
<head>
|
|
736
739
|
<meta charset="UTF-8" />
|
|
737
740
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
738
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>
|
|
739
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
|
+
}
|
|
740
757
|
*, *::before, *::after { box-sizing: border-box; }
|
|
741
|
-
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 */
|
|
742
843
|
.dev-banner {
|
|
743
844
|
position: fixed; top: 0; left: 0; right: 0; z-index: 99999;
|
|
744
845
|
background: #1a1a2e; color: #e0e0ff; font-size: 12px;
|
|
745
846
|
padding: 4px 12px; display: flex; align-items: center; gap: 8px;
|
|
746
847
|
font-family: monospace; border-bottom: 2px solid #6c63ff;
|
|
747
848
|
}
|
|
748
|
-
.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; } }
|
|
749
851
|
.dev-banner .label { opacity: 0.7; }
|
|
750
852
|
.dev-banner .slug { font-weight: bold; color: #a5b4fc; }
|
|
751
853
|
.dev-banner .stub-tags { margin-left: auto; display: flex; gap: 6px; }
|
|
752
|
-
.dev-banner .stub-tag {
|
|
753
|
-
|
|
754
|
-
|
|
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;
|
|
755
867
|
}
|
|
756
|
-
|
|
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 */
|
|
757
872
|
.checkout-stub {
|
|
758
|
-
background: #fef3c7
|
|
759
|
-
|
|
873
|
+
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
|
874
|
+
border: 2px dashed #f59e0b; border-radius: 16px;
|
|
875
|
+
padding: 24px; text-align: center;
|
|
760
876
|
}
|
|
761
|
-
.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; }
|
|
762
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
|
+
}
|
|
763
886
|
</style>
|
|
764
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
765
887
|
</head>
|
|
766
888
|
<body>
|
|
767
889
|
<div class="dev-banner">
|
|
@@ -779,189 +901,653 @@ function buildPreviewHtml(schema, port) {
|
|
|
779
901
|
import React from 'https://esm.sh/react@18?bundle';
|
|
780
902
|
import ReactDOM from 'https://esm.sh/react-dom@18/client?bundle';
|
|
781
903
|
|
|
904
|
+
const h = React.createElement;
|
|
782
905
|
const schema = ${schemaJson};
|
|
906
|
+
const themeColor = '${themeColor}';
|
|
783
907
|
|
|
784
|
-
//
|
|
785
|
-
function
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
function renderComponent(comp, i) {
|
|
793
|
-
const props = comp.props || {};
|
|
794
|
-
const type = comp.type;
|
|
795
|
-
|
|
796
|
-
switch (type) {
|
|
797
|
-
case 'heading':
|
|
798
|
-
const Tag = props.level === 2 ? 'h2' : props.level === 3 ? 'h3' : 'h1';
|
|
799
|
-
return React.createElement(Tag, { key: i, className: 'text-2xl font-bold mb-4 px-4', dangerouslySetInnerHTML: { __html: props.text || '' } });
|
|
800
|
-
|
|
801
|
-
case 'paragraph':
|
|
802
|
-
return React.createElement('p', { key: i, className: 'text-gray-700 mb-4 px-4 leading-relaxed', dangerouslySetInnerHTML: { __html: props.text || '' } });
|
|
803
|
-
|
|
804
|
-
case 'image':
|
|
805
|
-
return React.createElement('div', { key: i, className: 'px-4 mb-4' },
|
|
806
|
-
React.createElement('img', {
|
|
807
|
-
src: props.src || '',
|
|
808
|
-
alt: props.alt || '',
|
|
809
|
-
className: 'max-w-full rounded-lg',
|
|
810
|
-
style: { maxHeight: '400px', objectFit: 'contain' }
|
|
811
|
-
})
|
|
812
|
-
);
|
|
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
|
+
}
|
|
813
916
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
+
}
|
|
824
943
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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 })
|
|
843
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 || '' } });
|
|
844
1043
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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) } })
|
|
853
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
|
+
}
|
|
854
1064
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
|
865
1107
|
)
|
|
866
|
-
)
|
|
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 });
|
|
867
1177
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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 || ''
|
|
877
1193
|
)
|
|
878
1194
|
)
|
|
879
|
-
)
|
|
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 ---
|
|
880
1226
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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)
|
|
888
1289
|
);
|
|
1290
|
+
})
|
|
1291
|
+
)
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
889
1294
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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
|
+
},
|
|
900
1318
|
},
|
|
901
|
-
|
|
902
|
-
className: '
|
|
903
|
-
|
|
904
|
-
}
|
|
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)
|
|
905
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
|
+
}
|
|
906
1344
|
|
|
907
|
-
|
|
908
|
-
|
|
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
|
+
}
|
|
909
1360
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
+
}
|
|
918
1375
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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' });
|
|
924
1402
|
}
|
|
925
|
-
}
|
|
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]);
|
|
926
1413
|
|
|
927
1414
|
if (!page) {
|
|
928
|
-
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
|
+
);
|
|
929
1418
|
}
|
|
930
1419
|
|
|
931
1420
|
const components = page.components || [];
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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'),
|
|
944
1500
|
)
|
|
945
1501
|
),
|
|
946
|
-
// Page
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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))
|
|
960
1540
|
);
|
|
961
1541
|
}
|
|
962
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 ---
|
|
963
1549
|
const root = ReactDOM.createRoot(document.getElementById('catalog-root'));
|
|
964
|
-
root.render(
|
|
1550
|
+
root.render(h(CatalogPreview, { catalog: schema }));
|
|
965
1551
|
</script>
|
|
966
1552
|
</body>
|
|
967
1553
|
</html>`;
|
|
@@ -996,8 +1582,30 @@ async function catalogDev(file, opts) {
|
|
|
996
1582
|
console.log(` Pages: ${Object.keys(schema.pages || {}).length}`);
|
|
997
1583
|
console.log(` Entry: ${schema.routing?.entry || "first page"}`);
|
|
998
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
|
+
}
|
|
999
1595
|
const server = createServer(async (req, res) => {
|
|
1000
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
|
+
}
|
|
1001
1609
|
if (url.pathname.startsWith("/assets/")) {
|
|
1002
1610
|
const relativePath = decodeURIComponent(url.pathname.slice("/assets/".length));
|
|
1003
1611
|
const filePath = join2(catalogDir, relativePath);
|
|
@@ -1052,7 +1660,8 @@ async function catalogDev(file, opts) {
|
|
|
1052
1660
|
return;
|
|
1053
1661
|
}
|
|
1054
1662
|
schema = resolveLocalAssets(schema, catalogDir, localBaseUrl);
|
|
1055
|
-
reloadSpinner.succeed(`Reloaded \u2014
|
|
1663
|
+
reloadSpinner.succeed(`Reloaded \u2014 auto-refreshing browser`);
|
|
1664
|
+
notifyReload();
|
|
1056
1665
|
} catch (err) {
|
|
1057
1666
|
reloadSpinner.warn(`Reload failed: ${err.message}`);
|
|
1058
1667
|
}
|
|
@@ -1128,8 +1737,8 @@ async function whoami() {
|
|
|
1128
1737
|
}
|
|
1129
1738
|
|
|
1130
1739
|
// src/index.ts
|
|
1131
|
-
var
|
|
1132
|
-
var { version } =
|
|
1740
|
+
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
1741
|
+
var { version } = JSON.parse(readFileSync5(join3(__dirname, "../package.json"), "utf-8"));
|
|
1133
1742
|
var program = new Command();
|
|
1134
1743
|
program.name("catalogs").description("CLI for Catalog Kit \u2014 upload videos, push catalogs, manage assets").version(version).option("--token <token>", "Auth token (overrides CATALOG_KIT_TOKEN env var)").hook("preAction", (thisCommand) => {
|
|
1135
1744
|
const opts = thisCommand.opts();
|