@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.
Files changed (2) hide show
  1. package/dist/index.js +774 -165
  2. 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 { createRequire } from "module";
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 { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
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
- background: rgba(255,255,255,0.1); border-radius: 3px;
754
- padding: 1px 6px; font-size: 11px; color: #fbbf24;
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
- #catalog-root { padding-top: 28px; }
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; border: 2px dashed #f59e0b; border-radius: 8px;
759
- padding: 20px; text-align: center; margin: 16px;
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
- // Render catalog schema as a formatted preview
785
- function CatalogPreview({ catalog }) {
786
- const pages = catalog.pages || {};
787
- const pageKeys = Object.keys(pages);
788
- const [currentPage, setCurrentPage] = React.useState(catalog.routing?.entry || pageKeys[0] || null);
789
-
790
- const page = currentPage ? pages[currentPage] : null;
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
- case 'video':
815
- const videoSrc = props.src || props.hls_url || '';
816
- return React.createElement('div', { key: i, className: 'px-4 mb-4' },
817
- React.createElement('video', {
818
- src: videoSrc,
819
- controls: true,
820
- className: 'max-w-full rounded-lg',
821
- poster: props.poster || undefined
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
- case 'html':
826
- return React.createElement('div', {
827
- key: i,
828
- className: 'px-4 mb-4',
829
- dangerouslySetInnerHTML: { __html: props.content || '' }
830
- });
831
-
832
- case 'short_text':
833
- case 'email':
834
- case 'phone':
835
- case 'url':
836
- return React.createElement('div', { key: i, className: 'px-4 mb-4' },
837
- props.label ? React.createElement('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, props.label) : null,
838
- React.createElement('input', {
839
- type: type === 'email' ? 'email' : type === 'phone' ? 'tel' : type === 'url' ? 'url' : 'text',
840
- placeholder: props.placeholder || '',
841
- className: 'w-full border border-gray-300 rounded-lg px-3 py-2 text-sm'
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
- case 'long_text':
846
- return React.createElement('div', { key: i, className: 'px-4 mb-4' },
847
- props.label ? React.createElement('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, props.label) : null,
848
- React.createElement('textarea', {
849
- placeholder: props.placeholder || '',
850
- className: 'w-full border border-gray-300 rounded-lg px-3 py-2 text-sm',
851
- rows: 4
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
- case 'multiple_choice':
856
- return React.createElement('div', { key: i, className: 'px-4 mb-4' },
857
- props.label ? React.createElement('label', { className: 'block text-sm font-medium text-gray-700 mb-2' }, props.label) : null,
858
- React.createElement('div', { className: 'space-y-2' },
859
- ...(props.options || []).map((opt, j) =>
860
- React.createElement('button', {
861
- key: j,
862
- className: 'block w-full text-left border border-gray-300 rounded-lg px-4 py-3 text-sm hover:border-blue-500 hover:bg-blue-50 transition'
863
- }, typeof opt === 'string' ? opt : opt.label || opt.value || '')
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
- case 'dropdown':
869
- return React.createElement('div', { key: i, className: 'px-4 mb-4' },
870
- props.label ? React.createElement('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, props.label) : null,
871
- React.createElement('select', { className: 'w-full border border-gray-300 rounded-lg px-3 py-2 text-sm' },
872
- React.createElement('option', { value: '' }, props.placeholder || 'Select...'),
873
- ...(props.options || []).map((opt, j) =>
874
- React.createElement('option', { key: j, value: typeof opt === 'string' ? opt : opt.value },
875
- typeof opt === 'string' ? opt : opt.label || opt.value || ''
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
- case 'payment':
882
- return React.createElement('div', { key: i, className: 'checkout-stub' },
883
- React.createElement('h3', null, 'Stripe Checkout'),
884
- React.createElement('p', null, 'Payment would trigger here in production.'),
885
- props.amount ? React.createElement('p', { className: 'mt-2 font-bold' },
886
- (props.currency || 'USD').toUpperCase() + ' ' + (props.amount / 100).toFixed(2)
887
- ) : null
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
- case 'banner':
891
- const bannerColors = {
892
- info: 'bg-blue-50 border-blue-200 text-blue-800',
893
- success: 'bg-green-50 border-green-200 text-green-800',
894
- warning: 'bg-yellow-50 border-yellow-200 text-yellow-800',
895
- error: 'bg-red-50 border-red-200 text-red-800',
896
- };
897
- return React.createElement('div', {
898
- key: i,
899
- className: 'px-4 mb-4'
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
- React.createElement('div', {
902
- className: 'border rounded-lg px-4 py-3 text-sm ' + (bannerColors[props.style] || bannerColors.info),
903
- dangerouslySetInnerHTML: { __html: props.text || '' }
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
- case 'divider':
908
- return React.createElement('hr', { key: i, className: 'my-6 mx-4 border-gray-200' });
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
- case 'file_download':
911
- return React.createElement('div', { key: i, className: 'px-4 mb-4' },
912
- React.createElement('a', {
913
- href: props.src || '#',
914
- download: props.filename || 'file',
915
- className: 'inline-flex items-center gap-2 border border-gray-300 rounded-lg px-4 py-2 text-sm hover:bg-gray-50'
916
- }, '\u{1F4E5} ' + (props.filename || props.label || 'Download File'))
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
- default:
920
- return React.createElement('div', {
921
- key: i,
922
- className: 'px-4 mb-4 bg-gray-50 border border-dashed border-gray-300 rounded-lg p-3 text-xs text-gray-500'
923
- }, '[' + type + '] ' + (props.label || props.text || comp.id || ''));
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 React.createElement('div', { className: 'p-8 text-center text-gray-500' }, 'No pages found in catalog.');
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 pageTitle = page.title;
933
-
934
- return React.createElement('div', { className: 'max-w-2xl mx-auto py-8' },
935
- // Page navigation
936
- React.createElement('div', { className: 'flex gap-1 px-4 mb-6 overflow-x-auto' },
937
- ...pageKeys.map(key =>
938
- React.createElement('button', {
939
- key: key,
940
- onClick: () => setCurrentPage(key),
941
- className: 'px-3 py-1 text-xs rounded-full whitespace-nowrap ' +
942
- (key === currentPage ? 'bg-indigo-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200')
943
- }, pages[key].title || key)
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 title
947
- pageTitle ? React.createElement('h1', { className: 'text-xl font-bold px-4 mb-4' }, pageTitle) : null,
948
- // Components
949
- ...components.map(renderComponent),
950
- // Navigation buttons
951
- React.createElement('div', { className: 'px-4 mt-6 flex gap-3' },
952
- React.createElement('button', {
953
- className: 'px-6 py-2 bg-indigo-600 text-white rounded-lg text-sm font-medium hover:bg-indigo-700',
954
- onClick: () => {
955
- const idx = pageKeys.indexOf(currentPage);
956
- if (idx < pageKeys.length - 1) setCurrentPage(pageKeys[idx + 1]);
957
- }
958
- }, page.cta_text || 'Continue'),
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(React.createElement(CatalogPreview, { catalog: schema }));
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 refresh browser to see changes`);
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 require2 = createRequire(import.meta.url);
1132
- var { version } = require2("../../package.json");
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@officexapp/catalogs-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "CLI for Catalog Kit — upload videos, push catalogs, manage assets",
5
5
  "type": "module",
6
6
  "bin": {