@deneb-ui/create-template 2.0.71 → 2.0.72

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/create-template",
3
- "version": "2.0.71",
3
+ "version": "2.0.72",
4
4
  "description": "Scaffold production-ready DENEB UI storefront templates for Next.js in seconds.",
5
5
  "bin": {
6
6
  "create-deneb-template": "bin/index.js",
@@ -5,34 +5,12 @@
5
5
  "contractVersion": 1,
6
6
  "mode": "strict",
7
7
  "controlOnlyPaths": [
8
- "__fivoraIntake.tone",
9
- "__fivoraIntake.outputLanguage",
10
- "__fivoraIntake.businessSummary",
11
- "__fivoraIntake.additionalBusinessDetails",
12
- "__fivoraIntake.referenceWebsiteUrl",
13
- "additionalPages[*].id",
14
- "additionalPages[*].route",
15
- "common.currency",
16
- "common.whatsappNumber",
17
- "common.whatsapp",
18
- "common.email",
19
- "common.address",
20
- "common.contactNumber",
21
- "common.openingHours",
22
- "common.newsletterTitle",
23
- "common.newsletterDescription",
24
- "contact.contactNumber",
25
- "contact.googleMapLink",
26
- "contact.whatsapp",
27
8
  "products[*].id",
28
9
  "products[*].isAvailable",
29
- "products[*].measurement",
30
- "products[*].unit",
31
- "testimonials[*].id",
32
- "categories[*].slug",
33
10
  "services[*].id",
34
11
  "products[*].currency",
35
- "about.collageImages[*].id"
12
+ "about.collageImages[*].id",
13
+ "contact.whatsapp"
36
14
  ]
37
15
  },
38
16
  "siteDataFile": "src/data/site-data.json",
@@ -429,12 +407,18 @@
429
407
  "key": "directMessageText",
430
408
  "type": "text",
431
409
  "label": "Direct message label"
410
+ },
411
+ {
412
+ "key": "whatsapp",
413
+ "type": "tel",
414
+ "label": "WhatsApp"
432
415
  }
433
416
  ]
434
417
  },
435
418
  {
436
419
  "id": "products",
437
420
  "path": "products",
421
+ "pageKey": "home",
438
422
  "type": "list",
439
423
  "label": "Products",
440
424
  "minItems": 0,
@@ -18,7 +18,7 @@
18
18
  "next": "15.5.23",
19
19
  "react": "19.2.8",
20
20
  "react-dom": "19.2.8",
21
- "@deneb-ui/ui": "^2.0.71"
21
+ "@deneb-ui/ui": "^2.0.72"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@eslint/eslintrc": "3.3.6",
@@ -28,7 +28,7 @@
28
28
  "eslint": "9.39.5",
29
29
  "eslint-config-next": "15.5.23",
30
30
  "typescript": "5.8.3",
31
- "@deneb-ui/cli": "^2.0.71",
31
+ "@deneb-ui/cli": "^2.0.72",
32
32
  "tailwindcss": "^3.4.17",
33
33
  "postcss": "8.5.26",
34
34
  "autoprefixer": "^10.4.20"
@@ -56,7 +56,6 @@ const PLATFORM_PATHS = [
56
56
  // Contact fields used as action links (href attributes)
57
57
  'contact.contactNumber',
58
58
  'contact.googleMapLink',
59
- 'contact.whatsapp',
60
59
  // Product system fields
61
60
  'products[*].id',
62
61
  'products[*].isAvailable',
@@ -65,6 +64,9 @@ const PLATFORM_PATHS = [
65
64
  // List item system keys
66
65
  'testimonials[*].id',
67
66
  'categories[*].slug',
67
+ 'services[*].id',
68
+ 'products[*].currency',
69
+ 'about.collageImages[*].id',
68
70
  ];
69
71
 
70
72
  const manifestPath = path.resolve(__dirname, '..', 'fivora-template.json');
@@ -76,12 +78,87 @@ if (!fs.existsSync(manifestPath)) {
76
78
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
77
79
  if (!manifest.visualEditing) manifest.visualEditing = {};
78
80
 
81
+ const siteDataPath = path.resolve(__dirname, '..', manifest.siteDataFile || 'src/data/site-data.json');
82
+ let siteData = {};
83
+ if (fs.existsSync(siteDataPath)) {
84
+ try {
85
+ siteData = JSON.parse(fs.readFileSync(siteDataPath, 'utf8'));
86
+ } catch {}
87
+ }
88
+
89
+ function pathExistsInContent(content, p) {
90
+ if (!content || typeof content !== 'object') return false;
91
+ const segments = p.split('.');
92
+ let current = [content];
93
+ for (const seg of segments) {
94
+ const isList = seg.endsWith('[*]');
95
+ const key = isList ? seg.slice(0, -3) : seg;
96
+ const next = [];
97
+ for (const item of current) {
98
+ if (!item || typeof item !== 'object') continue;
99
+ const val = item[key];
100
+ if (val === undefined) continue;
101
+ if (isList) {
102
+ if (Array.isArray(val)) {
103
+ next.push(...val);
104
+ }
105
+ } else {
106
+ next.push(val);
107
+ }
108
+ }
109
+ if (next.length === 0) return false;
110
+ current = next;
111
+ }
112
+ return true;
113
+ }
114
+
115
+ function pathExistsInSchema(schema, p) {
116
+ if (!schema || !Array.isArray(schema.sections)) return false;
117
+ const canonical = p.replace(/\[\d+\]/g, '[*]');
118
+
119
+ function checkNode(node, currentPath) {
120
+ if (currentPath === canonical) return true;
121
+ if (node.type === 'object' && Array.isArray(node.fields)) {
122
+ for (const field of node.fields) {
123
+ if (checkNode(field, `${currentPath}.${field.key}`)) return true;
124
+ }
125
+ }
126
+ if (node.type === 'list') {
127
+ const itemPath = `${currentPath}[*]`;
128
+ if (itemPath === canonical) return true;
129
+ if (node.itemField && itemPath === canonical) return true;
130
+ if (Array.isArray(node.fields)) {
131
+ for (const field of node.fields) {
132
+ if (checkNode(field, `${itemPath}.${field.key}`)) return true;
133
+ }
134
+ }
135
+ }
136
+ return false;
137
+ }
138
+
139
+ for (const section of schema.sections) {
140
+ if (checkNode(section, section.path)) return true;
141
+ }
142
+ return false;
143
+ }
144
+
145
+ function isKnownPath(p) {
146
+ return (
147
+ pathExistsInContent(siteData.content, p) ||
148
+ pathExistsInContent(siteData, p) ||
149
+ pathExistsInSchema(manifest.editorSchema, p)
150
+ );
151
+ }
152
+
79
153
  const existing = Array.isArray(manifest.visualEditing.controlOnlyPaths)
80
- ? manifest.visualEditing.controlOnlyPaths : [];
154
+ ? manifest.visualEditing.controlOnlyPaths.filter(isKnownPath)
155
+ : [];
81
156
 
82
- const merged = [...new Set([...PLATFORM_PATHS, ...existing])];
83
- const added = merged.length - existing.length;
157
+ const relevantPlatformPaths = PLATFORM_PATHS.filter(isKnownPath);
158
+ const merged = [...new Set([...relevantPlatformPaths, ...existing])];
84
159
  manifest.visualEditing.controlOnlyPaths = merged;
85
160
 
86
161
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
87
- console.log(`merge-platform-contract: +${added} platform paths merged (${merged.length} total)`);
162
+ console.log(`merge-platform-contract: ${merged.length} platform paths retained`);
163
+
164
+
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { HeritageCollage, EditableText, EditableImage } from '@deneb-ui/ui';
3
+ import { EditableText, EditableImage } from '@deneb-ui/ui';
4
4
  import { contentObject, contentText, contentList, useSiteData } from '@/lib/siteDataContext';
5
5
 
6
6
  export default function AboutPage() {
@@ -39,18 +39,90 @@ export default function AboutPage() {
39
39
  </section>
40
40
 
41
41
  <section className="page-section" data-design-section="about-heritage">
42
- <HeritageCollage
43
- collageHeading={contentText(about.collageHeading)}
44
- collageDescription={contentText(about.collageDescription)}
45
- historyHeading={contentText(about.historyHeading)}
46
- history={contentText(about.history)}
47
- images={collageImages}
48
- collageHeadingPath="about.collageHeading"
49
- collageDescriptionPath="about.collageDescription"
50
- historyHeadingPath="about.historyHeading"
51
- historyPath="about.history"
52
- listPath="about.collageImages"
53
- />
42
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '2rem', alignItems: 'start' }}>
43
+ <div>
44
+ <EditableText
45
+ variant="h2"
46
+ size="2xl"
47
+ weight="bold"
48
+ color="heading"
49
+ data-preview-field-path="about.collageHeading"
50
+ defaultValue={contentText(about.collageHeading)}
51
+ />
52
+ <EditableText
53
+ variant="p"
54
+ color="muted"
55
+ data-preview-field-path="about.collageDescription"
56
+ defaultValue={contentText(about.collageDescription)}
57
+ style={{ marginTop: '0.5rem', marginBottom: '1.5rem' }}
58
+ />
59
+ <EditableText
60
+ variant="h3"
61
+ size="xl"
62
+ weight="bold"
63
+ color="heading"
64
+ data-preview-field-path="about.historyHeading"
65
+ defaultValue={contentText(about.historyHeading)}
66
+ />
67
+ <EditableText
68
+ variant="p"
69
+ color="muted"
70
+ data-preview-field-path="about.history"
71
+ defaultValue={contentText(about.history)}
72
+ style={{ marginTop: '0.5rem' }}
73
+ />
74
+ </div>
75
+
76
+ <div
77
+ data-preview-list-path="about.collageImages"
78
+ style={{
79
+ display: 'grid',
80
+ gridTemplateColumns: 'repeat(2, 1fr)',
81
+ gap: '0.75rem',
82
+ }}
83
+ >
84
+ {collageImages.map((item, index) => (
85
+ <figure
86
+ key={item.id || index}
87
+ data-preview-item-path={`about.collageImages[${index}]`}
88
+ style={{
89
+ position: 'relative',
90
+ overflow: 'hidden',
91
+ borderRadius: '1rem',
92
+ border: '1px solid var(--color-border, #e2e8f0)',
93
+ margin: 0,
94
+ minHeight: '140px',
95
+ background: 'var(--color-secondary, #f1f5f9)',
96
+ }}
97
+ >
98
+ <img
99
+ src={item.image || '/placeholder.svg'}
100
+ alt={item.caption || ''}
101
+ data-preview-field-path={`about.collageImages[${index}].image`}
102
+ style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
103
+ />
104
+ {item.caption && (
105
+ <figcaption
106
+ style={{
107
+ position: 'absolute',
108
+ insetInline: 0,
109
+ bottom: 0,
110
+ padding: '0.75rem 1rem',
111
+ background: 'linear-gradient(transparent, rgba(15,23,42,0.85))',
112
+ color: '#fff',
113
+ fontSize: '0.8125rem',
114
+ fontWeight: 600,
115
+ }}
116
+ >
117
+ <span data-preview-field-path={`about.collageImages[${index}].caption`}>
118
+ {item.caption}
119
+ </span>
120
+ </figcaption>
121
+ )}
122
+ </figure>
123
+ ))}
124
+ </div>
125
+ </div>
54
126
  </section>
55
127
  </div>
56
128
  );
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useState, type FormEvent } from 'react';
4
- import { MapLink, EditableText, EditableBox } from '@deneb-ui/ui';
4
+ import { EditableText, EditableBox } from '@deneb-ui/ui';
5
5
  import { contentObject, contentText, useSiteData } from '@/lib/siteDataContext';
6
6
 
7
7
  export default function ContactPage() {
@@ -76,50 +76,64 @@ export default function ContactPage() {
76
76
  size="xl"
77
77
  weight="bold"
78
78
  color="heading"
79
+ data-preview-static="contact-heading"
79
80
  defaultValue="Direct Information"
80
81
  />
81
82
  <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', marginTop: '0.5rem' }}>
82
- {phone && (
83
- <a href={`tel:${phone}`} data-preview-field-path="contact.phone">
84
- {phone}
85
- </a>
86
- )}
87
- {email && (
88
- <a href={`mailto:${email}`} data-preview-field-path="contact.email">
89
- {email}
90
- </a>
91
- )}
92
- {address && (
93
- <EditableText
94
- variant="p"
95
- color="muted"
96
- data-preview-field-path="contact.address"
97
- defaultValue={address}
98
- />
99
- )}
83
+ <a href={phone ? `tel:${phone}` : ''} data-preview-field-path="contact.phone">
84
+ {phone}
85
+ </a>
86
+ <a href={email ? `mailto:${email}` : ''} data-preview-field-path="contact.email">
87
+ {email}
88
+ </a>
89
+ <EditableText
90
+ variant="p"
91
+ color="muted"
92
+ data-preview-field-path="contact.address"
93
+ defaultValue={address}
94
+ />
100
95
  </div>
101
96
  <div style={{ marginTop: '1.5rem', display: 'flex', flexWrap: 'wrap', gap: '0.75rem' }}>
102
- <MapLink
103
- mapUrl={directionsUrl}
104
- address={address}
105
- label={directionsLabel}
106
- labelFieldPath="contact.directionsLabel"
107
- urlFieldPath="contact.directionsUrl"
108
- variant="outline"
109
- size="md"
110
- />
111
- {whatsapp && (
112
- <a
113
- href={`https://wa.me/${whatsapp.replace(/[^0-9]/g, '')}`}
114
- target="_blank"
115
- rel="noopener noreferrer"
116
- className="button-secondary"
117
- >
118
- <span data-preview-field-path="contact.directMessageText">
119
- {directMessageText}
120
- </span>
121
- </a>
122
- )}
97
+ <a
98
+ href={directionsUrl || '#'}
99
+ data-preview-field-path="contact.directionsUrl"
100
+ target="_blank"
101
+ rel="noopener noreferrer"
102
+ className="deneb-map-link"
103
+ style={{
104
+ display: 'inline-flex',
105
+ alignItems: 'center',
106
+ justifyContent: 'center',
107
+ gap: '0.5rem',
108
+ fontWeight: 500,
109
+ textDecoration: 'none',
110
+ transition: 'all 0.2s ease',
111
+ cursor: 'pointer',
112
+ borderRadius: '0.5rem',
113
+ fontSize: '0.95rem',
114
+ padding: '0.5rem 1rem',
115
+ backgroundColor: 'transparent',
116
+ border: '1px solid var(--color-border, #e2e8f0)',
117
+ color: 'var(--color-text, #0f172a)',
118
+ }}
119
+ >
120
+ <svg data-preview-static="map-link-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
121
+ <path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" />
122
+ <circle cx="12" cy="10" r="3" />
123
+ </svg>
124
+ <span data-preview-field-path="contact.directionsLabel">{directionsLabel}</span>
125
+ </a>
126
+ <a
127
+ href={whatsapp ? `https://wa.me/${whatsapp.replace(/[^0-9]/g, '')}` : '#'}
128
+ target="_blank"
129
+ rel="noopener noreferrer"
130
+ className="button-secondary"
131
+ data-preview-field-path="contact.whatsapp"
132
+ >
133
+ <span data-preview-field-path="contact.directMessageText">
134
+ {directMessageText}
135
+ </span>
136
+ </a>
123
137
  </div>
124
138
  </EditableBox>
125
139
 
@@ -13,8 +13,7 @@ import {
13
13
  EditableText,
14
14
  EditableList,
15
15
  EditableCard,
16
- EditableProductCard,
17
- ListActionCta,
16
+ EditableImage,
18
17
  } from '@deneb-ui/ui';
19
18
 
20
19
 
@@ -140,14 +139,28 @@ export default function HomePage() {
140
139
  data-preview-field-path="home.introBody"
141
140
  defaultValue={contentText(home.introBody)}
142
141
  />
143
- <div style={{ display: 'flex', justifyContent: 'center', marginTop: '1.5rem' }}>
144
- <ListActionCta
145
- listPath="home.demoPreOrderCta"
146
- editable
147
- buttonLabel={demoCtaLabel}
148
- buttonUrl={demoCtaUrl}
149
- className="button-primary"
150
- />
142
+ <div
143
+ style={{ display: 'flex', justifyContent: 'center', gap: '0.75rem', marginTop: '1.5rem', flexWrap: 'wrap' }}
144
+ data-preview-list-path="home.demoPreOrderCta"
145
+ >
146
+ {contentList(home.demoPreOrderCta).map((rawCta, index) => {
147
+ const cta = contentObject(rawCta);
148
+ const label = contentText(cta.buttonLabel) || 'Book a Consultation';
149
+ const url = contentText(cta.buttonUrl) || '#';
150
+ return (
151
+ <a
152
+ key={index}
153
+ href={url}
154
+ className="button-primary"
155
+ data-preview-item-path={`home.demoPreOrderCta[${index}]`}
156
+ data-preview-field-path={`home.demoPreOrderCta[${index}].buttonUrl`}
157
+ >
158
+ <span data-preview-field-path={`home.demoPreOrderCta[${index}].buttonLabel`}>
159
+ {label}
160
+ </span>
161
+ </a>
162
+ );
163
+ })}
151
164
  </div>
152
165
  </div>
153
166
  </section>
@@ -229,38 +242,110 @@ export default function HomePage() {
229
242
  />
230
243
  </div>
231
244
 
232
- <EditableList
233
- data-preview-list-path="products"
234
- items={products}
235
- className="card-grid"
236
- itemAs={false}
237
- >
238
- {(rawProduct, index, itemPath) => {
245
+ <div className="card-grid" data-preview-list-path="products">
246
+ {products.map((rawProduct, index) => {
239
247
  const product = contentObject(rawProduct);
240
248
  const productId =
241
249
  typeof product.id === 'string' || typeof product.id === 'number'
242
250
  ? product.id
243
251
  : null;
252
+ const price = typeof product.price === 'number' ? product.price : parseFloat(String(product.price || 0));
253
+ const compareAt = typeof product.compareAtPrice === 'number' ? product.compareAtPrice : parseFloat(String(product.compareAtPrice || 0));
254
+ const currency = contentText(product.currency) || 'LKR';
255
+ const badge = contentText(product.badge);
256
+ const category = contentText(product.category);
244
257
 
245
258
  return (
246
- <EditableProductCard
259
+ <EditableCard
247
260
  key={productId ?? index}
248
- itemPath={itemPath}
249
- product={product}
250
- currency={contentText(product.currency) || 'LKR'}
251
- actionSlot={
252
- <a
253
- href={platformProductDetailHref(productId)}
254
- className="product-detail-link"
255
- data-preview-static="Open the stable live product detail page"
256
- >
257
- View details
258
- </a>
259
- }
260
- />
261
+ className="card product-card"
262
+ item={product as any}
263
+ itemPath={`products[${index}]`}
264
+ data-preview-item-path={`products[${index}]`}
265
+ balance
266
+ radius="xl"
267
+ style={{ display: 'flex', flexDirection: 'column' }}
268
+ >
269
+ <div style={{ position: 'relative', overflow: 'hidden', borderRadius: '12px', marginBottom: '0.75rem' }}>
270
+ <EditableImage
271
+ src={contentText(product.imageUrl) || '/placeholder.svg'}
272
+ alt={contentText(product.name)}
273
+ data-preview-field-path={`products[${index}].imageUrl`}
274
+ aspectRatio="4/3"
275
+ fit="cover"
276
+ radius="lg"
277
+ />
278
+ {badge && (
279
+ <span
280
+ className="badge-primary"
281
+ data-preview-field-path={`products[${index}].badge`}
282
+ style={{ position: 'absolute', top: '10px', left: '10px', zIndex: 10 }}
283
+ >
284
+ {badge}
285
+ </span>
286
+ )}
287
+ </div>
288
+
289
+ <div style={{ display: 'flex', flexDirection: 'column', flex: 1, gap: '0.5rem' }}>
290
+ {category && (
291
+ <span
292
+ className="tag"
293
+ data-preview-field-path={`products[${index}].category`}
294
+ style={{ fontSize: '0.75rem', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em' }}
295
+ >
296
+ {category}
297
+ </span>
298
+ )}
299
+ <EditableText
300
+ variant="h3"
301
+ size="lg"
302
+ weight="bold"
303
+ color="heading"
304
+ data-preview-field-path={`products[${index}].name`}
305
+ defaultValue={contentText(product.name)}
306
+ />
307
+ <EditableText
308
+ variant="p"
309
+ color="muted"
310
+ data-preview-field-path={`products[${index}].description`}
311
+ defaultValue={contentText(product.description)}
312
+ />
313
+
314
+ <div style={{ display: 'flex', alignItems: 'baseline', gap: '0.5rem', marginTop: 'auto', paddingTop: '0.75rem' }}>
315
+ <EditableText
316
+ as="span"
317
+ weight="bold"
318
+ size="xl"
319
+ color="primary"
320
+ data-preview-field-path={`products[${index}].price`}
321
+ defaultValue={`${currency} ${price}`}
322
+ />
323
+ {compareAt > 0 && (
324
+ <EditableText
325
+ as="span"
326
+ color="muted"
327
+ style={{ textDecoration: 'line-through', fontSize: '0.875rem' }}
328
+ data-preview-field-path={`products[${index}].compareAtPrice`}
329
+ defaultValue={`${currency} ${compareAt}`}
330
+ />
331
+ )}
332
+ </div>
333
+
334
+ <div style={{ marginTop: '0.75rem' }}>
335
+ <a
336
+ href={platformProductDetailHref(productId)}
337
+ className="button-secondary"
338
+ style={{ width: '100%', textAlign: 'center' }}
339
+ data-preview-static="Open the stable live product detail page"
340
+ >
341
+ View details
342
+ </a>
343
+ </div>
344
+ </div>
345
+ </EditableCard>
261
346
  );
262
- }}
263
- </EditableList>
347
+ })}
348
+ </div>
264
349
  </section>
265
350
  </div>
266
351
  );
@@ -3,5 +3,17 @@
3
3
  import { PlatformProductDetail } from '@deneb-ui/ui';
4
4
 
5
5
  export default function ProductDetailPage() {
6
- return <PlatformProductDetail backHref="/" />;
6
+ return (
7
+ <PlatformProductDetail
8
+ backHref="/"
9
+ loadingFallback={
10
+ <div
11
+ role="status"
12
+ className="mx-auto flex min-h-[40vh] max-w-7xl items-center justify-center px-4 py-16 text-sm text-slate-400"
13
+ >
14
+ <span data-preview-static="product-detail-loading">Loading product…</span>
15
+ </div>
16
+ }
17
+ />
18
+ );
7
19
  }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
- import { contentObject, contentText, useServices, useSiteData } from '@/lib/siteDataContext';
4
- import { EditableServiceCard, EditableText } from '@deneb-ui/ui';
3
+ import { contentList, contentObject, contentText, useServices, useSiteData } from '@/lib/siteDataContext';
4
+ import { EditableCard, EditableImage, EditableText } from '@deneb-ui/ui';
5
5
 
6
6
  export default function ServicesPage() {
7
7
  const siteData = useSiteData();
@@ -31,13 +31,71 @@ export default function ServicesPage() {
31
31
  </section>
32
32
  <section className="page-section alt" data-design-section="services-list">
33
33
  <div className="card-grid" data-preview-list-path="services">
34
- {services.map((service, index) => (
35
- <EditableServiceCard
36
- key={contentText(service.id) || index}
37
- itemPath={`services[${index}]`}
38
- service={service}
39
- />
40
- ))}
34
+ {services.map((rawService, index) => {
35
+ const service = contentObject(rawService);
36
+ const features = contentList(service.features);
37
+ const imageUrl = contentText(service.imageUrl);
38
+
39
+ return (
40
+ <EditableCard
41
+ key={contentText(service.id) || index}
42
+ className="card service-card"
43
+ item={service as any}
44
+ itemPath={`services[${index}]`}
45
+ data-preview-item-path={`services[${index}]`}
46
+ balance
47
+ radius="xl"
48
+ style={{ display: 'flex', flexDirection: 'column' }}
49
+ >
50
+ {imageUrl && (
51
+ <div style={{ position: 'relative', overflow: 'hidden', borderRadius: '12px', marginBottom: '0.75rem' }}>
52
+ <EditableImage
53
+ src={imageUrl}
54
+ alt={contentText(service.name)}
55
+ data-preview-field-path={`services[${index}].imageUrl`}
56
+ aspectRatio="16/9"
57
+ fit="cover"
58
+ radius="lg"
59
+ />
60
+ </div>
61
+ )}
62
+ <EditableText
63
+ variant="h3"
64
+ size="xl"
65
+ weight="bold"
66
+ color="heading"
67
+ data-preview-field-path={`services[${index}].name`}
68
+ defaultValue={contentText(service.name)}
69
+ />
70
+ <EditableText
71
+ variant="p"
72
+ color="muted"
73
+ data-preview-field-path={`services[${index}].description`}
74
+ defaultValue={contentText(service.description)}
75
+ style={{ marginTop: '0.5rem' }}
76
+ />
77
+ {features.length > 0 && (
78
+ <ul
79
+ className="service-features-list"
80
+ data-preview-list-path={`services[${index}].features`}
81
+ style={{ marginTop: '1rem', display: 'flex', flexDirection: 'column', gap: '0.375rem', paddingLeft: '1.25rem' }}
82
+ >
83
+ {features.map((feature, fIndex) => (
84
+ <li
85
+ key={fIndex}
86
+ data-preview-item-path={`services[${index}].features[${fIndex}]`}
87
+ style={{ fontSize: '0.875rem' }}
88
+ >
89
+ <span data-preview-field-path={`services[${index}].features[${fIndex}]`}>
90
+ {contentText(feature)}
91
+ </span>
92
+ </li>
93
+ ))}
94
+ </ul>
95
+ )}
96
+ </EditableCard>
97
+ );
98
+ })}
41
99
  </div>
42
100
  </section>
43
101
  </div>
@@ -47,6 +47,7 @@ export function ThemeToggle({ className = '', showLabel = false }: ThemeTogglePr
47
47
  <button
48
48
  type="button"
49
49
  aria-label="Toggle theme"
50
+ data-preview-static="theme-toggle"
50
51
  className={`w-9 h-9 rounded-lg border border-[var(--border-color,#e2e8f0)] bg-[var(--card-bg,#ffffff)] opacity-0 transition-opacity ${className}`}
51
52
  />
52
53
  );
@@ -56,6 +57,7 @@ export function ThemeToggle({ className = '', showLabel = false }: ThemeTogglePr
56
57
  <button
57
58
  type="button"
58
59
  onClick={toggleTheme}
60
+ data-preview-static="theme-toggle"
59
61
  aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
60
62
  title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
61
63
  className={`inline-flex items-center justify-center gap-2 h-9 px-2.5 rounded-lg border border-[var(--border-color,rgba(0,0,0,0.1))] bg-[var(--card-bg,#ffffff)] text-[var(--text-color,#0f172a)] hover:border-[var(--color-primary,#016a7e)] hover:text-[var(--color-primary,#016a7e)] shadow-sm transition-all duration-200 cursor-pointer ${className}`}