@deneb-ui/ui 2.0.35 → 2.0.37

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/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  <p align="center">
16
16
  <strong>Build beautiful, editable storefronts — on mobile, tablet, and desktop.</strong><br />
17
- The visual-first React component library for Next.js commerce and Fivora live editing.
17
+ The visual-first React component library for Next.js commerce, ARC Engine, and Fivora live visual editing.
18
18
  </p>
19
19
 
20
20
  <p align="center">
@@ -28,18 +28,28 @@
28
28
 
29
29
  ## Getting Started
30
30
 
31
- ### Install
31
+ ### Installation
32
32
 
33
33
  ```bash
34
34
  npm install @deneb-ui/ui
35
- # pnpm add @deneb-ui/ui
36
- # yarn add @deneb-ui/ui
35
+ # or
36
+ pnpm add @deneb-ui/ui
37
+ # or
38
+ yarn add @deneb-ui/ui
37
39
  ```
38
40
 
39
- ### Minimal example
41
+ ### Quick Start Example
40
42
 
41
43
  ```tsx
42
- import { SiteDataProvider, Navbar, Hero, Footer, Grid, ProductCard } from "@deneb-ui/ui";
44
+ import {
45
+ SiteDataProvider,
46
+ Navbar,
47
+ Hero,
48
+ GoogleFeedback,
49
+ TestimonialSection,
50
+ Map,
51
+ Footer
52
+ } from "@deneb-ui/ui";
43
53
  import siteData from "@/data/site-data.json";
44
54
 
45
55
  export default function Page() {
@@ -47,49 +57,342 @@ export default function Page() {
47
57
  <SiteDataProvider initialSiteData={siteData}>
48
58
  <Navbar />
49
59
  <Hero />
50
- <Grid columns={{ mobile: 1, tablet: 2, desktop: 3 }} gap="lg">
51
- {/* ProductCard items */}
52
- </Grid>
60
+ <GoogleFeedback basePath="feedback" />
61
+ <TestimonialSection basePath="testimonials" />
62
+ <div className="w-full h-96 rounded-2xl overflow-hidden shadow-lg">
63
+ <Map mapUrl={siteData.content.contact?.mapUrl} />
64
+ </div>
53
65
  <Footer />
54
66
  </SiteDataProvider>
55
67
  );
56
68
  }
57
69
  ```
58
70
 
59
- `SiteDataProvider` automatically injects **ResponsiveBaseStyles** — every component adapts across phone, tablet, and desktop without extra setup.
71
+ `SiteDataProvider` automatically injects **ResponsiveBaseStyles** — every component adapts seamlessly across phone, tablet, and desktop without extra configuration.
60
72
 
61
73
  ---
62
74
 
63
- ## Features
75
+ ## Canonical Component Aliases
64
76
 
65
- | Capability | Description |
66
- | :--- | :--- |
67
- | **Visual editing native** | Built-in `data-preview-field-path` bindings for Fivora and DENEB preview protocol |
68
- | **Responsive by default** | Mobile drawer nav, collapsible filters, fluid grids, `clamp()` section spacing |
69
- | **Smart commerce actions** | WhatsApp, phone, email, maps auto-hide when merchant data is missing |
70
- | **Zero styling lock-in** | Inline CSS + optional Tailwind; works in any React 18/19 app |
71
- | **40+ components** | Navbar, Hero, ProductGrid, CartDrawer, ContactForm, BusinessHours, and more |
77
+ `@deneb-ui/ui` provides both canonical shorthand aliases (shadcn / HeroUI style) and full `Editable*` identifiers:
78
+
79
+ | Canonical Alias | Full Component Identifier | Description |
80
+ | :--- | :--- | :--- |
81
+ | `GoogleFeedback` | `EditableGoogleFeedback` | Official Google review badge, aggregate rating, & review cards |
82
+ | `TestimonialSection` | `EditableTestimonialSection` | Editorial critic testimonials with typography & star ratings |
83
+ | `Testimonials` | `EditableTestimonialSection` | Alias for `EditableTestimonialSection` |
84
+ | `Map` | `EditableMap` | Universal Google Maps embed with automatic URL parsing |
85
+ | `CustomerReviews` | `EditableCustomerReviews` | Customer review summary & rating breakdown |
86
+ | `ProductCard` | `EditableProductCard` | Visual commerce product card with live price & badges |
87
+ | `ProductGrid` | `EditableProductGrid` | Responsive grid with visual editing markers |
88
+ | `Navbar` / `Header` | `EditableNavbar` | Responsive header with mobile drawer & cart trigger |
89
+ | `Hero` | `EditableHeroCentered` | High-converting split / centered storefront hero |
90
+ | `Footer` | `EditableFooter` | Multi-column commerce footer with social links & legal links |
91
+ | `CartDrawer` | `EditableCartDrawer` | Sliding visual cart drawer with item counter |
92
+ | `Accordion` / `FAQ`| `EditableFAQAccordion` | Collapsible FAQ accordion with Fivora list markers |
93
+ | `Button` | `EditableButton` | Visual button with action contract & ripple feedback |
94
+ | `Image` | `EditableImage` | Visual image with aspect ratio preservation & Fivora focus |
95
+ | `Text` | `EditableText` | Inline editable text with instant DOM sync |
72
96
 
73
97
  ---
74
98
 
75
- ## Documentation
99
+ ## Component Guides & API Reference
76
100
 
77
- | Guide | Link |
78
- | :--- | :--- |
79
- | Introduction | [deneb.fivora.site/docs/introduction](https://deneb.fivora.site/docs/introduction) |
80
- | Installation & CLI init | [deneb.fivora.site/docs/installation](https://deneb.fivora.site/docs/installation) |
81
- | Responsive design | [deneb.fivora.site/docs/responsive-design](https://deneb.fivora.site/docs/responsive-design) |
82
- | Theming & tokens | [deneb.fivora.site/docs/theming](https://deneb.fivora.site/docs/theming) |
83
- | Component catalog | [deneb.fivora.site/docs/components/button](https://deneb.fivora.site/docs/components/button) |
101
+ ### 1. `GoogleFeedback` (`EditableGoogleFeedback`)
102
+
103
+ The **`GoogleFeedback`** component renders an authentic Google Reviews card experience. It features the official Google multi-colored badge, aggregate rating pill, verified buyer checks, and a responsive grid of customer reviews.
104
+
105
+ #### Key Capabilities
106
+ - **Google Design Fidelity**: Uses Google Material star vectors with authentic warm gold (`#fa7014`) and neutral grey (`#dadce0`).
107
+ - **Live 1–5 Star DOM Sync**: Typing numbers `1` through `5` into the Fivora live inspector immediately recalculates and repaints the star fills in real time via an internal `MutationObserver`.
108
+ - **Zero-Latency Visual Editing**: Built-in `data-preview-field-path` and `data-preview-list-path` attributes compliant with the Fivora Strict Visual Editing Contract.
109
+ - **Auto-Fallbacks**: Provides elegant fallback review data if no `feedbacks` array is passed.
110
+
111
+ #### Props Reference
112
+
113
+ | Prop | Type | Default | Description |
114
+ | :--- | :--- | :--- | :--- |
115
+ | `basePath` | `string` | `'feedback'` | Root key in `site-data.json` (`content[basePath]`) |
116
+ | `badgeIcon` | `string` | Google G SVG | URL or SVG data URI for the review platform badge |
117
+ | `badgeTitle` | `string` | `'Google'` | Review platform label displayed next to badge icon |
118
+ | `badgeRating` | `string \| number` | `'4.9'` | Aggregate rating number shown in the top header |
119
+ | `badgeReviewsCount` | `string \| number` | `'128 reviews'` | Total review count label (e.g., `'128 reviews'`) |
120
+ | `heading` | `string` | `'Loved by Coffee Lovers...'` | Section main heading |
121
+ | `subheading` | `string` | `'Real stories and reviews...'` | Section introductory paragraph |
122
+ | `feedbacks` | `FeedbackItem[]` | `DEFAULT_FEEDBACKS` | Array of customer review items |
123
+ | `maxStars` | `number` | `5` | Maximum number of rating stars to render per card |
124
+ | `className` | `string` | `''` | Custom CSS / Tailwind classes for section wrapper |
125
+ | `cardClassName` | `string` | `''` | Custom CSS / Tailwind classes for review cards |
126
+
127
+ #### `FeedbackItem` Schema
128
+
129
+ ```typescript
130
+ export interface FeedbackItem {
131
+ id?: string | number;
132
+ name?: string; // Reviewer full name
133
+ avatar?: string; // Avatar image URL
134
+ rating?: number | string; // Star rating value (1 to 5)
135
+ date?: string; // Relative date (e.g. "2 days ago")
136
+ comment?: string; // Review body text
137
+ verified?: boolean; // Verified customer badge
138
+ }
139
+ ```
140
+
141
+ #### Fivora Data Contract (`site-data.json`)
142
+
143
+ ```json
144
+ {
145
+ "content": {
146
+ "feedback": {
147
+ "badgeIcon": "https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg",
148
+ "badgeTitle": "Google",
149
+ "badgeRating": "4.9",
150
+ "badgeReviewsCount": "128 reviews",
151
+ "heading": "Loved by Coffee Lovers Across the World",
152
+ "subheading": "Real stories and reviews from our community of coffee purists and daily ritualists.",
153
+ "feedbacks": [
154
+ {
155
+ "id": "1",
156
+ "name": "Elena Vance",
157
+ "avatar": "https://images.unsplash.com/photo-1534528741775-53994a69daeb",
158
+ "rating": 5,
159
+ "date": "3 days ago",
160
+ "comment": "The Yirgacheffe pour-over is unmatched. Clean notes of jasmine and bergamot.",
161
+ "verified": true
162
+ }
163
+ ]
164
+ }
165
+ }
166
+ }
167
+ ```
168
+
169
+ #### Fivora `editorSchema` Section
170
+
171
+ ```json
172
+ {
173
+ "id": "feedback",
174
+ "path": "feedback",
175
+ "type": "object",
176
+ "label": "Google Customer Feedback",
177
+ "pageKey": "home",
178
+ "fields": [
179
+ { "key": "badgeIcon", "type": "image", "label": "Badge Icon URL" },
180
+ { "key": "badgeTitle", "type": "text", "label": "Badge Title" },
181
+ { "key": "badgeRating", "type": "text", "label": "Aggregate Rating" },
182
+ { "key": "badgeReviewsCount", "type": "text", "label": "Reviews Count" },
183
+ { "key": "heading", "type": "text", "label": "Section Heading" },
184
+ { "key": "subheading", "type": "textarea", "label": "Section Subheading" },
185
+ {
186
+ "key": "feedbacks",
187
+ "type": "list",
188
+ "label": "Customer Reviews",
189
+ "itemLabel": "Review",
190
+ "minItems": 1,
191
+ "maxItems": 12,
192
+ "fields": [
193
+ { "key": "name", "type": "text", "label": "Reviewer Name", "required": true },
194
+ { "key": "avatar", "type": "image", "label": "Profile Picture" },
195
+ { "key": "rating", "type": "number", "label": "Star Rating (1-5)" },
196
+ { "key": "date", "type": "text", "label": "Review Date" },
197
+ { "key": "comment", "type": "textarea", "label": "Review Comment", "required": true }
198
+ ]
199
+ }
200
+ ]
201
+ }
202
+ ```
203
+
204
+ #### Usage Example
205
+
206
+ ```tsx
207
+ import { GoogleFeedback, useSiteData } from "@deneb-ui/ui";
208
+
209
+ export function FeedbackSection() {
210
+ const { siteData } = useSiteData();
211
+ const feedbackData = siteData?.content?.feedback || {};
212
+
213
+ return (
214
+ <section className="py-20 bg-amber-50/40">
215
+ <GoogleFeedback
216
+ basePath="feedback"
217
+ badgeIcon={feedbackData.badgeIcon}
218
+ badgeTitle={feedbackData.badgeTitle}
219
+ badgeRating={feedbackData.badgeRating}
220
+ badgeReviewsCount={feedbackData.badgeReviewsCount}
221
+ heading={feedbackData.heading}
222
+ subheading={feedbackData.subheading}
223
+ feedbacks={feedbackData.feedbacks}
224
+ />
225
+ </section>
226
+ );
227
+ }
228
+ ```
229
+
230
+ ---
231
+
232
+ ### 2. `TestimonialSection` (`EditableTestimonialSection`)
233
+
234
+ The **`TestimonialSection`** component provides an editorial, magazine-grade layout for connoisseur quotes, culinary critics, and press reviews.
235
+
236
+ #### Key Capabilities
237
+ - **Editorial Typography**: Large quotation marks, serif italic styling, and prominent author metadata.
238
+ - **Accreditation Tags**: Includes specialty tags (e.g. *"Michelin Guide"*, *"World Barista Judge"*).
239
+ - **Synchronized Star Ratings**: Dynamic star rating with live inspector sync.
240
+ - **Clean Layout**: Fluid responsive grid adapting from 1 column on mobile to 3 columns on desktop.
241
+
242
+ #### Props Reference
243
+
244
+ | Prop | Type | Default | Description |
245
+ | :--- | :--- | :--- | :--- |
246
+ | `basePath` | `string` | `'testimonials'` | Root key in `site-data.json` (`content[basePath]`) |
247
+ | `badge` | `string` | `'Critic Acclaim'` | Small uppercase pill badge text |
248
+ | `heading` | `string` | `'What Connoisseurs Say'` | Main section title |
249
+ | `subheading` | `string` | `'Unfiltered impressions...'` | Subtitle description text |
250
+ | `testimonials` | `TestimonialSectionItem[]` | `DEFAULT_TESTIMONIALS` | Array of testimonial items |
251
+ | `maxStars` | `number` | `5` | Maximum number of stars per card |
252
+ | `className` | `string` | `''` | Section container CSS classes |
253
+ | `cardClassName` | `string` | `''` | Individual testimonial card CSS classes |
254
+
255
+ #### `TestimonialSectionItem` Schema
256
+
257
+ ```typescript
258
+ export interface TestimonialSectionItem {
259
+ id?: string | number;
260
+ quote?: string; // Testimonial quote text
261
+ author?: string; // Author full name
262
+ role?: string; // Profession, publication, or title
263
+ avatar?: string; // Author portrait URL
264
+ tag?: string; // Category or accreditation badge
265
+ rating?: number | string; // Star rating (1 to 5)
266
+ }
267
+ ```
268
+
269
+ #### Usage Example
270
+
271
+ ```tsx
272
+ import { TestimonialSection, useSiteData } from "@deneb-ui/ui";
273
+
274
+ export function EditorialReviews() {
275
+ const { siteData } = useSiteData();
276
+ const data = siteData?.content?.testimonials || {};
277
+
278
+ return (
279
+ <TestimonialSection
280
+ basePath="testimonials"
281
+ badge={data.badge}
282
+ heading={data.heading}
283
+ subheading={data.subheading}
284
+ testimonials={data.testimonials}
285
+ />
286
+ );
287
+ }
288
+ ```
289
+
290
+ ---
291
+
292
+ ### 3. `Map` (`EditableMap`)
293
+
294
+ The **`Map`** component is an intelligent universal Google Maps embedder. It converts any format of Google Maps URL or search query into a high-performance, responsive iframe embed.
295
+
296
+ #### Universal Link Parser Features
297
+ The underlying `parseGoogleMapsEmbedUrl` engine seamlessly processes:
298
+ 1. **Full `<iframe>` Snippets**: Extracts the `src` attribute automatically when developers or merchants paste standard Google Maps embed HTML.
299
+ 2. **Standard Embed URLs**: Passes through existing `google.com/maps/embed?...` URLs directly.
300
+ 3. **Coordinate URLs**: Parses `@lat,lng` patterns (e.g. `google.com/maps/@6.9271,79.8612,15z`).
301
+ 4. **Place URLs**: Extracts place names from `/maps/place/Name` paths.
302
+ 5. **Search Query URLs**: Parses `?q=...` parameters.
303
+ 6. **Short Links**: Handles `maps.app.goo.gl` links with automatic fallback to merchant address.
304
+ 7. **Raw Search Strings**: Directly wraps street addresses or city names into valid Google Maps embed queries.
305
+
306
+ #### Props Reference
307
+
308
+ | Prop | Type | Default | Description |
309
+ | :--- | :--- | :--- | :--- |
310
+ | `mapUrl` | `string` | `undefined` | Google Maps URL, share link, coordinate link, or embed code |
311
+ | `address` | `string` | `'Sri Lanka'` | Fallback physical address when `mapUrl` is empty or unresolved |
312
+ | `defaultLocation` | `string` | `undefined` | Additional location fallback |
313
+ | `data-preview-field-path` | `string` | `undefined` | Fivora visual editing binding path |
314
+ | `className` | `string` | `'w-full h-full border-0'` | CSS classes for the iframe element |
315
+ | `title` | `string` | `'Google Map Location'` | Accessibility title for the iframe |
316
+
317
+ #### Usage Example
318
+
319
+ ```tsx
320
+ import { Map } from "@deneb-ui/ui";
321
+
322
+ export function ContactMapSection({ mapUrl, address }: { mapUrl?: string; address?: string }) {
323
+ return (
324
+ <div className="w-full h-96 sm:h-[480px] rounded-3xl overflow-hidden shadow-2xl border border-stone-200">
325
+ <Map
326
+ data-preview-field-path="contact.mapUrl"
327
+ mapUrl={mapUrl}
328
+ address={address}
329
+ className="w-full h-full border-0"
330
+ />
331
+ </div>
332
+ );
333
+ }
334
+ ```
335
+
336
+ ---
337
+
338
+ ## ARC Engine & Fivora Platform Architecture
339
+
340
+ `@deneb-ui/ui` is built from the ground up for the **ARC Engine** (`@deneb-ui/cli`) and the **Fivora Visual Commerce Platform**.
341
+
342
+ ```
343
+ ┌─────────────────────────────────────────────────────────────┐
344
+ │ Fivora Admin Portal │
345
+ │ (Inspector / Live Studio) │
346
+ └──────────────────────────────┬──────────────────────────────┘
347
+ │ Live PostMessage Protocol
348
+
349
+ ┌─────────────────────────────────────────────────────────────┐
350
+ │ Fivora Preview Focus Bridge │
351
+ │ - Focus Target Identification (data-preview-field-path) │
352
+ │ - Fast DOM Mutation & Star Fill Repainting │
353
+ └──────────────────────────────┬──────────────────────────────┘
354
+ │ Instant DOM / Observer Sync
355
+
356
+ ┌─────────────────────────────────────────────────────────────┐
357
+ │ DENEB UI │
358
+ │ - GoogleFeedback (MutationObserver Dual-Layer Rating) │
359
+ │ - TestimonialSection (Editorial Critic Cards) │
360
+ │ - EditableMap (Universal URL Parsing Engine) │
361
+ └─────────────────────────────────────────────────────────────┘
362
+ ```
363
+
364
+ ### Dual-Layer Live Rating Architecture
365
+
366
+ When editing ratings in visual site builders, updating an element's text content usually destroys child SVG icons. DENEB UI solves this through a dual-layer architecture:
367
+
368
+ 1. **Inspector Target**: A dedicated `<span data-preview-field-path="...">` renders the numeric string (e.g. `"5"`).
369
+ 2. **Visual Star Array**: A sibling `<div data-fivora-stars-row="true">` contains the 5 vector SVG stars with fill colors `#fa7014` (gold) and `#dadce0` (grey).
370
+ 3. **`MutationObserver`**: An internal observer listens for inspector text mutations on the numeric span and immediately updates the SVG star fills without remounting or re-rendering errors.
371
+ 4. **Focus Bridge Support**: The CLI preview bridge recognizes star rating containers and synchronizes fills directly during live keystrokes.
372
+
373
+ ### ARC Engine Automated Scaffolding
374
+
375
+ When running the ARC Engine:
376
+ ```bash
377
+ npx @deneb-ui/cli arc init
378
+ ```
379
+
380
+ The engine automatically:
381
+ - Identifies `GoogleFeedback`, `TestimonialSection`, and `Map` components.
382
+ - Creates semantic field paths (`feedback.feedbacks`, `testimonials.testimonials`, `contact.mapUrl`).
383
+ - Generates typed `editorSchema` sections with proper numeric, image, and textarea descriptors.
384
+ - Adds non-editable internal IDs to `visualEditing.controlOnlyPaths` to guarantee 100% contract compliance with Fivora platform ingestion.
84
385
 
85
386
  ---
86
387
 
87
- ## Related packages
388
+ ## Related Packages
88
389
 
89
390
  | Package | Purpose |
90
391
  | :--- | :--- |
91
- | [`@deneb-ui/cli`](https://www.npmjs.com/package/@deneb-ui/cli) | Convert, validate, and package storefront templates |
92
- | [`@deneb-ui/create-template`](https://www.npmjs.com/package/@deneb-ui/create-template) | Scaffold a new DENEB storefront in one command |
392
+ | [`@deneb-ui/ui`](https://www.npmjs.com/package/@deneb-ui/ui) | Visual-First React component framework |
393
+ | [`@deneb-ui/cli`](https://www.npmjs.com/package/@deneb-ui/cli) | ARC Engine compiler, AST transformer & template lab |
394
+ | [`@deneb-ui/core`](https://www.npmjs.com/package/@deneb-ui/core) | Shared design tokens, typography registry & contracts |
395
+ | [`@deneb-ui/create-template`](https://www.npmjs.com/package/@deneb-ui/create-template) | Instant storefront generator |
93
396
 
94
397
  ---
95
398
 
@@ -97,8 +400,8 @@ export default function Page() {
97
400
 
98
401
  Created and maintained by **[Chamika Gayashan](https://github.com/chamikathereal)** and **[Induranga Kawishwara](https://github.com/Induranga-kawishwara)**.
99
402
 
100
- Part of the **DENEB UI** ecosystem · Collaborate with **[Fivora](https://fivora.site)**.
403
+ Part of the **DENEB UI** ecosystem · In collaboration with **[Fivora](https://fivora.site)**.
101
404
 
102
405
  <p align="center">
103
- <sub>MIT © DENEB UI</sub>
406
+ <sub>MIT © DENEB UI — The Visual-First React Framework</sub>
104
407
  </p>
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ export interface FeedbackItem {
3
+ id?: string | number;
4
+ name?: string;
5
+ avatar?: string;
6
+ rating?: number | string;
7
+ date?: string;
8
+ comment?: string;
9
+ verified?: boolean;
10
+ [key: string]: unknown;
11
+ }
12
+ export interface EditableGoogleFeedbackProps extends React.HTMLAttributes<HTMLElement> {
13
+ basePath?: string;
14
+ badgeIcon?: string;
15
+ badgeTitle?: string;
16
+ badgeRating?: string | number;
17
+ badgeReviewsCount?: string | number;
18
+ heading?: string;
19
+ subheading?: string;
20
+ feedbacks?: FeedbackItem[];
21
+ maxStars?: number;
22
+ className?: string;
23
+ cardClassName?: string;
24
+ }
25
+ export declare function EditableGoogleFeedback({ basePath, badgeIcon, badgeTitle, badgeRating, badgeReviewsCount, heading, subheading, feedbacks, maxStars, className, cardClassName, style, ...props }: EditableGoogleFeedbackProps): React.JSX.Element;
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EditableGoogleFeedback = EditableGoogleFeedback;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const DEFAULT_GOOGLE_ICON = 'https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg';
7
+ const DEFAULT_FEEDBACKS = [
8
+ {
9
+ id: 1,
10
+ name: 'Elena Vance',
11
+ avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?q=80&w=300&auto=format&fit=crop',
12
+ rating: 5,
13
+ date: '3 days ago',
14
+ comment: 'The Yirgacheffe pour-over is unmatched. You can taste the jasmine and wild bergamot notes immediately. The atmosphere of the salon is pure serenity.',
15
+ verified: true,
16
+ },
17
+ {
18
+ id: 2,
19
+ name: 'Julian Thorne',
20
+ avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?q=80&w=300&auto=format&fit=crop',
21
+ rating: 5,
22
+ date: '1 week ago',
23
+ comment: 'Hands down the best flat white on the island. The milk texture was like velvet and the single-origin espresso cut through with rich cacao sweetness.',
24
+ verified: true,
25
+ },
26
+ {
27
+ id: 3,
28
+ name: 'Maya Lin',
29
+ avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?q=80&w=300&auto=format&fit=crop',
30
+ rating: 5,
31
+ date: '2 weeks ago',
32
+ comment: 'Ordered 2 bags of the cast-iron roasted beans for my home setup. Shipping was lightning-fast and the roast profile was exceptionally dialed in.',
33
+ verified: true,
34
+ },
35
+ {
36
+ id: 4,
37
+ name: 'David Sterling',
38
+ avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?q=80&w=300&auto=format&fit=crop',
39
+ rating: 5,
40
+ date: '3 weeks ago',
41
+ comment: 'The 9-bar manual lever pull is pure art. Wonderful baristas who genuinely know and love their craft. Liceria & Co. has set a whole new standard.',
42
+ verified: true,
43
+ },
44
+ {
45
+ id: 5,
46
+ name: 'Sophia Aris',
47
+ avatar: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?q=80&w=300&auto=format&fit=crop',
48
+ rating: 5,
49
+ date: '1 month ago',
50
+ comment: 'Attended the private cupping session on Saturday. Educational, welcoming, and deeply flavorful. Easily our favorite coffee destination in the country.',
51
+ verified: true,
52
+ },
53
+ ];
54
+ function GoogleStarSvg({ filled = true, size = 18 }) {
55
+ return ((0, jsx_runtime_1.jsx)("svg", { viewBox: "0 0 24 24", style: {
56
+ width: `${size}px`,
57
+ height: `${size}px`,
58
+ fill: filled ? '#fa7014' : '#dadce0',
59
+ color: filled ? '#fa7014' : '#dadce0',
60
+ flexShrink: 0,
61
+ }, "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" }) }));
62
+ }
63
+ function FeedbackCard({ item, idx, basePath, maxStars, badgeIcon, cardClassName, }) {
64
+ const currentItem = item || {};
65
+ const rawRating = currentItem.rating != null && currentItem.rating !== '' ? currentItem.rating : 5;
66
+ const initialRating = Math.min(Math.max(Math.round(Number(rawRating) || 5), 1), maxStars);
67
+ const [currentRating, setCurrentRating] = (0, react_1.useState)(initialRating);
68
+ const ratingRef = (0, react_1.useRef)(null);
69
+ // Sync state when props change
70
+ (0, react_1.useEffect)(() => {
71
+ setCurrentRating(initialRating);
72
+ }, [initialRating]);
73
+ // Real-time MutationObserver to sync DOM live edits from Fivora inspector instantly
74
+ (0, react_1.useEffect)(() => {
75
+ const el = ratingRef.current;
76
+ if (!el)
77
+ return;
78
+ const parseValue = () => {
79
+ const text = el.textContent?.trim() ?? '';
80
+ if (!text)
81
+ return;
82
+ const num = Number(text);
83
+ if (!Number.isNaN(num) && Number.isFinite(num)) {
84
+ const clamped = Math.min(Math.max(Math.round(num), 1), maxStars);
85
+ setCurrentRating(clamped);
86
+ }
87
+ };
88
+ const observer = new MutationObserver(parseValue);
89
+ observer.observe(el, { characterData: true, childList: true, subtree: true });
90
+ return () => observer.disconnect();
91
+ }, [maxStars]);
92
+ const handleFocusRating = () => {
93
+ if (ratingRef.current) {
94
+ ratingRef.current.focus();
95
+ ratingRef.current.click();
96
+ }
97
+ };
98
+ const reviewerName = String(currentItem.name || 'Anonymous');
99
+ const reviewerAvatar = String(currentItem.avatar ||
100
+ 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?q=80&w=300&auto=format&fit=crop');
101
+ const reviewerComment = String(currentItem.comment || '');
102
+ const reviewerDate = String(currentItem.date || 'Recent review');
103
+ return ((0, jsx_runtime_1.jsx)("div", { "data-preview-item-path": `${basePath}.feedbacks[${idx}]`, className: `p-6 sm:p-7 rounded-3xl bg-[#ffffff] border border-[#3d2114]/12 shadow-sm hover:shadow-md transition-all duration-300 flex flex-col justify-between space-y-4 ${cardClassName || ''}`.trim(), children: (0, jsx_runtime_1.jsxs)("div", { className: "space-y-3.5", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between gap-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)("img", { src: reviewerAvatar, alt: reviewerName, "data-preview-field-path": `${basePath}.feedbacks[${idx}].avatar`, className: "w-11 h-11 rounded-full object-cover border border-[#3d2114]/15 shadow-sm" }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `${basePath}.feedbacks[${idx}].name`, className: "block text-sm font-black text-[#2b170e]", children: reviewerName }), (0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `${basePath}.feedbacks[${idx}].date`, className: "block text-xs text-[#8c7a6e]", children: reviewerDate })] })] }), (0, jsx_runtime_1.jsx)("img", { src: badgeIcon || DEFAULT_GOOGLE_ICON, alt: "Google", className: "w-5 h-5 object-contain opacity-90" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between gap-2 pt-1", children: [(0, jsx_runtime_1.jsxs)("div", { className: "inline-flex items-center gap-2 p-1 -ml-1 rounded-lg cursor-pointer transition-all hover:bg-[#fa7014]/10 group", onClick: handleFocusRating, title: `Rating: ${currentRating} of ${maxStars} (Click to edit)`, children: [(0, jsx_runtime_1.jsx)("div", { className: "flex items-center gap-[2px]", "data-fivora-stars-row": "true", children: Array.from({ length: maxStars }).map((_, sIdx) => ((0, jsx_runtime_1.jsx)(GoogleStarSvg, { filled: sIdx < currentRating, size: 18 }, sIdx))) }), (0, jsx_runtime_1.jsx)("span", { ref: ratingRef, "data-preview-field-path": `${basePath}.feedbacks[${idx}].rating`, "data-fivora-rating-text": "true", className: "text-xs font-black text-[#fa7014] bg-[#fa7014]/10 px-1.5 py-0.5 rounded-md border border-[#fa7014]/20 group-hover:bg-[#fa7014]/20 transition-colors tabular-nums", title: "Star count (1-5)", children: currentRating })] }), (0, jsx_runtime_1.jsxs)("div", { className: "inline-flex items-center gap-1 text-[11px] font-bold text-[#1a73e8] bg-[#e8f0fe] px-2.5 py-1 rounded-full border border-[#1a73e8]/20", children: [(0, jsx_runtime_1.jsx)("svg", { className: "w-3 h-3 fill-current", viewBox: "0 0 24 24", children: (0, jsx_runtime_1.jsx)("path", { d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" }) }), (0, jsx_runtime_1.jsx)("span", { children: "Verified" })] })] }), (0, jsx_runtime_1.jsxs)("p", { "data-preview-field-path": `${basePath}.feedbacks[${idx}].comment`, className: "text-sm sm:text-base text-[#3d2114] leading-relaxed italic font-normal pt-1", children: ["\u201C", reviewerComment, "\u201D"] })] }) }));
104
+ }
105
+ function EditableGoogleFeedback({ basePath = 'feedback', badgeIcon = DEFAULT_GOOGLE_ICON, badgeTitle = 'Google Reviews', badgeRating = '4.9', badgeReviewsCount = '340+ Verified Reviews', heading = 'PRAISED BY COFFEE LOVERS', subheading = 'Real experiences from patrons who visit our roastery parlor and brew our micro-lots.', feedbacks = DEFAULT_FEEDBACKS, maxStars = 5, className = '', cardClassName = '', style, ...props }) {
106
+ const items = (0, react_1.useMemo)(() => {
107
+ const list = Array.isArray(feedbacks) ? feedbacks : [];
108
+ if (list.length === 0) {
109
+ return DEFAULT_FEEDBACKS;
110
+ }
111
+ return list.map((item, idx) => {
112
+ const fallback = DEFAULT_FEEDBACKS[idx % DEFAULT_FEEDBACKS.length] || DEFAULT_FEEDBACKS[0];
113
+ if (!item || typeof item !== 'object') {
114
+ return { ...fallback, id: `fallback-${idx}` };
115
+ }
116
+ return {
117
+ ...fallback,
118
+ ...item,
119
+ id: item.id || `feedback-${idx}`,
120
+ rating: item.rating !== undefined && item.rating !== null ? item.rating : 5,
121
+ };
122
+ });
123
+ }, [feedbacks]);
124
+ return ((0, jsx_runtime_1.jsx)("section", { "data-design-section": "google-feedback", className: `editable-google-feedback w-full ${className}`.trim(), style: style, ...props, children: (0, jsx_runtime_1.jsxs)("div", { className: "max-w-[1600px] mx-auto px-4 sm:px-6 lg:px-10", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col md:flex-row md:items-end justify-between gap-6 mb-10 sm:mb-14", children: [(0, jsx_runtime_1.jsxs)("div", { className: "max-w-2xl space-y-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "inline-flex items-center gap-2.5 px-3.5 py-1.5 rounded-full bg-[#ede0d4]/80 border border-[#3d2114]/15 shadow-sm", children: [(0, jsx_runtime_1.jsx)("img", { src: badgeIcon || DEFAULT_GOOGLE_ICON, alt: "Google", "data-preview-field-path": `${basePath}.badgeIcon`, className: "w-4 h-4 object-contain" }), (0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `${basePath}.badgeTitle`, className: "text-xs uppercase tracking-widest font-black text-[#3d2114]", children: badgeTitle }), (0, jsx_runtime_1.jsx)("span", { className: "w-1 h-1 rounded-full bg-[#84431b]" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1.5", children: [(0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `${basePath}.badgeRating`, className: "text-xs font-black text-[#84431b]", children: badgeRating }), (0, jsx_runtime_1.jsx)("div", { className: "flex items-center gap-[2px]", children: Array.from({ length: 5 }).map((_, sIdx) => ((0, jsx_runtime_1.jsx)(GoogleStarSvg, { filled: true, size: 14 }, sIdx))) })] }), (0, jsx_runtime_1.jsxs)("span", { className: "text-[11px] text-[#786154] font-medium hidden sm:inline", children: ["(", (0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `${basePath}.badgeReviewsCount`, children: badgeReviewsCount }), ")"] })] }), (0, jsx_runtime_1.jsx)("h2", { "data-preview-field-path": `${basePath}.heading`, className: "font-heading text-2xl xs:text-3xl sm:text-4xl lg:text-5xl font-black text-[#2b170e] tracking-tight", children: heading }), (0, jsx_runtime_1.jsx)("p", { "data-preview-field-path": `${basePath}.subheading`, className: "text-xs sm:text-base text-[#5c493f] leading-relaxed", children: subheading })] }), (0, jsx_runtime_1.jsx)("div", { className: "hidden md:flex flex-col items-end text-right", children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `${basePath}.badgeRating`, className: "font-heading text-4xl font-black text-[#84431b]", children: badgeRating }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col items-start gap-0.5", children: [(0, jsx_runtime_1.jsx)("div", { className: "flex items-center gap-[2px]", children: Array.from({ length: 5 }).map((_, sIdx) => ((0, jsx_runtime_1.jsx)(GoogleStarSvg, { filled: true, size: 16 }, sIdx))) }), (0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `${basePath}.badgeReviewsCount`, className: "text-xs text-[#786154] font-bold", children: badgeReviewsCount })] })] }) })] }), (0, jsx_runtime_1.jsx)("div", { "data-preview-list-path": `${basePath}.feedbacks`, className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6", children: items.map((item, idx) => ((0, jsx_runtime_1.jsx)(FeedbackCard, { item: item, idx: idx, basePath: basePath, maxStars: maxStars, badgeIcon: badgeIcon, cardClassName: cardClassName }, item.id || `feedback-${idx}`))) })] }) }));
125
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ export interface EditableMapProps extends React.IframeHTMLAttributes<HTMLIFrameElement> {
3
+ id?: string;
4
+ 'data-preview-field-path'?: string;
5
+ mapUrl?: string;
6
+ address?: string;
7
+ defaultLocation?: string;
8
+ }
9
+ export declare function parseGoogleMapsEmbedUrl(urlOrLocation: string, addressFallback?: string): string;
10
+ export declare function EditableMap({ id, 'data-preview-field-path': previewFieldPath, mapUrl, address, defaultLocation, className, style, ...props }: EditableMapProps): React.JSX.Element;
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseGoogleMapsEmbedUrl = parseGoogleMapsEmbedUrl;
4
+ exports.EditableMap = EditableMap;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_1 = require("react");
7
+ function parseGoogleMapsEmbedUrl(urlOrLocation, addressFallback) {
8
+ let trimmed = (urlOrLocation || '').trim();
9
+ const fallback = (addressFallback || 'Sri Lanka').trim();
10
+ if (!trimmed) {
11
+ return `https://maps.google.com/maps?q=${encodeURIComponent(fallback)}&output=embed`;
12
+ }
13
+ // 1. If user pasted the full <iframe> embed tag from Google Maps
14
+ if (trimmed.includes('<iframe') && trimmed.includes('src=')) {
15
+ const srcMatch = trimmed.match(/src=["']([^"']+)["']/i);
16
+ if (srcMatch && srcMatch[1]) {
17
+ trimmed = srcMatch[1].trim();
18
+ }
19
+ }
20
+ // 2. Direct embed URL
21
+ if (trimmed.includes('google.com/maps/embed') || trimmed.includes('output=embed')) {
22
+ return trimmed;
23
+ }
24
+ // 3. Extract coordinates: @lat,lng
25
+ const atMatch = trimmed.match(/@(-?\d+\.\d+),(-?\d+\.\d+)/);
26
+ if (atMatch) {
27
+ return `https://maps.google.com/maps?q=${atMatch[1]},${atMatch[2]}&output=embed`;
28
+ }
29
+ // 4. Extract ll=lat,lng
30
+ const llMatch = trimmed.match(/[?&]ll=(-?\d+\.\d+),(-?\d+\.\d+)/);
31
+ if (llMatch) {
32
+ return `https://maps.google.com/maps?q=${llMatch[1]},${llMatch[2]}&output=embed`;
33
+ }
34
+ // 5. Extract place name from /place/Name or /search/Name
35
+ const placeMatch = trimmed.match(/\/maps\/(?:place|search)\/([^/@?#]+)/i);
36
+ if (placeMatch && placeMatch[1]) {
37
+ const decodedPlace = decodeURIComponent(placeMatch[1].replace(/\+/g, ' '));
38
+ return `https://maps.google.com/maps?q=${encodeURIComponent(decodedPlace)}&output=embed`;
39
+ }
40
+ // 6. Extract query param from Google Maps URLs (e.g. ?q=... or ?daddr=...)
41
+ if (trimmed.includes('google.com/maps') || trimmed.includes('maps.google.com')) {
42
+ const qMatch = trimmed.match(/[?&](?:q|daddr|query)=([^&#]+)/i);
43
+ if (qMatch && qMatch[1]) {
44
+ const decodedQ = decodeURIComponent(qMatch[1].replace(/\+/g, ' '));
45
+ return `https://maps.google.com/maps?q=${encodeURIComponent(decodedQ)}&output=embed`;
46
+ }
47
+ }
48
+ // 7. If it's still a raw URL (like an unresolved shortlink or http link),
49
+ // NEVER pass a raw URL to Google Maps q= parameter, because Google Maps treats
50
+ // it as a KML file, fails, and displays: "Some custom on-map content could not be displayed."
51
+ // Instead, fall back to the address or fallback location.
52
+ if (/^https?:\/\//i.test(trimmed)) {
53
+ return `https://maps.google.com/maps?q=${encodeURIComponent(fallback)}&output=embed`;
54
+ }
55
+ // 8. Plain text location (e.g. "Kandy", "Colombo", "Kandy, Sri Lanka")
56
+ return `https://maps.google.com/maps?q=${encodeURIComponent(trimmed)}&output=embed`;
57
+ }
58
+ function EditableMap({ id, 'data-preview-field-path': previewFieldPath, mapUrl, address, defaultLocation = 'Sri Lanka', className = '', style, ...props }) {
59
+ const path = previewFieldPath || id;
60
+ const [resolvedUrl, setResolvedUrl] = (0, react_1.useState)(null);
61
+ (0, react_1.useEffect)(() => {
62
+ let active = true;
63
+ const trimmed = (mapUrl || '').trim();
64
+ setResolvedUrl(null);
65
+ // If it's a shortlink, try to unshorten it with an unshortener API (with 3s timeout)
66
+ if (trimmed.includes('goo.gl') || trimmed.includes('maps.app.goo.gl')) {
67
+ const controller = new AbortController();
68
+ const timer = setTimeout(() => controller.abort(), 3000);
69
+ fetch(`https://unshorten.me/json/${encodeURIComponent(trimmed)}`, { signal: controller.signal })
70
+ .then((res) => res.json())
71
+ .then((data) => {
72
+ clearTimeout(timer);
73
+ if (active && data?.success && data?.resolved_url) {
74
+ setResolvedUrl(data.resolved_url);
75
+ }
76
+ })
77
+ .catch(() => {
78
+ clearTimeout(timer);
79
+ });
80
+ return () => {
81
+ active = false;
82
+ clearTimeout(timer);
83
+ controller.abort();
84
+ };
85
+ }
86
+ }, [mapUrl]);
87
+ const embedUrl = (0, react_1.useMemo)(() => {
88
+ const target = resolvedUrl || mapUrl || '';
89
+ return parseGoogleMapsEmbedUrl(target, address || defaultLocation);
90
+ }, [mapUrl, resolvedUrl, address, defaultLocation]);
91
+ return ((0, jsx_runtime_1.jsx)("iframe", { "data-preview-field-path": path, src: embedUrl, className: `editable-map ${className}`.trim(), style: { border: 0, ...style }, allowFullScreen: false, loading: "lazy", referrerPolicy: "no-referrer-when-downgrade", ...props }));
92
+ }
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import type { TestimonialItem } from './EditableTestimonialCard';
3
+ export interface TestimonialSectionItem extends TestimonialItem {
4
+ tag?: string;
5
+ [key: string]: unknown;
6
+ }
7
+ export interface EditableTestimonialSectionProps extends React.HTMLAttributes<HTMLElement> {
8
+ basePath?: string;
9
+ badge?: string;
10
+ heading?: string;
11
+ subheading?: string;
12
+ testimonials?: TestimonialSectionItem[];
13
+ maxStars?: number;
14
+ className?: string;
15
+ cardClassName?: string;
16
+ }
17
+ export declare function EditableTestimonialSection({ basePath, badge, heading, subheading, testimonials, maxStars, className, cardClassName, style, ...props }: EditableTestimonialSectionProps): React.JSX.Element;
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EditableTestimonialSection = EditableTestimonialSection;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const DEFAULT_TESTIMONIALS = [
7
+ {
8
+ id: 1,
9
+ quote: 'The thermal profile Liceria achieves on their refurbished drum is revelatory. You get bright floral acidity harmonized with unprecedented chocolate depth.',
10
+ author: 'Chef Antoine Laurent',
11
+ role: 'Michelin Star Restaurateur & Sommelier',
12
+ avatar: 'https://images.unsplash.com/photo-1583394838336-acd977736f90?q=80&w=300&auto=format&fit=crop',
13
+ rating: 5,
14
+ tag: 'Culinary Master',
15
+ },
16
+ {
17
+ id: 2,
18
+ quote: 'Manual spring-lever extraction pulled at 93.5°C with mineralized volcanic water. This is not just coffee; this is pure thermodynamic equilibrium.',
19
+ author: 'Clara Sorensen',
20
+ role: 'World Barista Championship Sensory Judge',
21
+ avatar: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?q=80&w=300&auto=format&fit=crop',
22
+ rating: 5,
23
+ tag: 'Sensory Panel',
24
+ },
25
+ {
26
+ id: 3,
27
+ quote: 'The flagship parlor is our neighborhood sanctuary. Whether for a quiet morning espresso or meeting international bean importers, there is nowhere else quite like it.',
28
+ author: 'Maximilian Sterling',
29
+ role: 'Architecture & Design Critic, Urban Loft',
30
+ avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?q=80&w=300&auto=format&fit=crop',
31
+ rating: 5,
32
+ tag: 'Design & Culture',
33
+ },
34
+ ];
35
+ function StarSvg({ filled = true, size = 16 }) {
36
+ return ((0, jsx_runtime_1.jsx)("svg", { viewBox: "0 0 24 24", style: {
37
+ width: `${size}px`,
38
+ height: `${size}px`,
39
+ fill: filled ? '#fa7014' : '#dadce0',
40
+ color: filled ? '#fa7014' : '#dadce0',
41
+ flexShrink: 0,
42
+ }, "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("path", { d: "M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" }) }));
43
+ }
44
+ function TestimonialCard({ item, idx, basePath, maxStars, cardClassName, }) {
45
+ const currentItem = item || {};
46
+ const rawRating = currentItem.rating != null && currentItem.rating !== '' ? currentItem.rating : 5;
47
+ const initialRating = Math.min(Math.max(Math.round(Number(rawRating) || 5), 1), maxStars);
48
+ const [currentRating, setCurrentRating] = (0, react_1.useState)(initialRating);
49
+ const ratingRef = (0, react_1.useRef)(null);
50
+ (0, react_1.useEffect)(() => {
51
+ setCurrentRating(initialRating);
52
+ }, [initialRating]);
53
+ (0, react_1.useEffect)(() => {
54
+ const el = ratingRef.current;
55
+ if (!el)
56
+ return;
57
+ const parseValue = () => {
58
+ const text = el.textContent?.trim() ?? '';
59
+ if (!text)
60
+ return;
61
+ const num = Number(text);
62
+ if (!Number.isNaN(num) && Number.isFinite(num)) {
63
+ const clamped = Math.min(Math.max(Math.round(num), 1), maxStars);
64
+ setCurrentRating(clamped);
65
+ }
66
+ };
67
+ const observer = new MutationObserver(parseValue);
68
+ observer.observe(el, { characterData: true, childList: true, subtree: true });
69
+ return () => observer.disconnect();
70
+ }, [maxStars]);
71
+ const handleFocusRating = () => {
72
+ if (ratingRef.current) {
73
+ ratingRef.current.focus();
74
+ ratingRef.current.click();
75
+ }
76
+ };
77
+ const quoteText = String(currentItem.quote || '');
78
+ const authorName = String(currentItem.author || 'Anonymous');
79
+ const authorRole = String(currentItem.role || '');
80
+ const authorAvatar = String(currentItem.avatar ||
81
+ 'https://images.unsplash.com/photo-1583394838336-acd977736f90?q=80&w=300&auto=format&fit=crop');
82
+ const tagText = String(currentItem.tag || '');
83
+ return ((0, jsx_runtime_1.jsxs)("div", { "data-preview-item-path": `${basePath}.testimonials[${idx}]`, className: `relative p-7 sm:p-8 rounded-3xl bg-[#ffffff] border border-[#3d2114]/12 shadow-md hover:shadow-xl transition-all duration-300 flex flex-col justify-between space-y-6 ${cardClassName || ''}`.trim(), children: [(0, jsx_runtime_1.jsxs)("div", { className: "space-y-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between gap-2", children: [Boolean(currentItem.tag) && ((0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `${basePath}.testimonials[${idx}].tag`, className: "px-3 py-1 rounded-full bg-[#f5ebe1] text-[#84431b] text-[11px] uppercase tracking-wider font-extrabold border border-[#3d2114]/10", children: tagText })), (0, jsx_runtime_1.jsxs)("div", { className: "inline-flex items-center gap-2 p-1 -mr-1 rounded-lg cursor-pointer transition-all hover:bg-[#fa7014]/10 group ml-auto", onClick: handleFocusRating, title: `Rating: ${currentRating} of ${maxStars} (Click to edit)`, children: [(0, jsx_runtime_1.jsx)("div", { className: "flex items-center gap-[2px]", "data-fivora-stars-row": "true", children: Array.from({ length: maxStars }).map((_, sIdx) => ((0, jsx_runtime_1.jsx)(StarSvg, { filled: sIdx < currentRating, size: 16 }, sIdx))) }), (0, jsx_runtime_1.jsx)("span", { ref: ratingRef, "data-preview-field-path": `${basePath}.testimonials[${idx}].rating`, "data-fivora-rating-text": "true", className: "text-xs font-black text-[#fa7014] bg-[#fa7014]/10 px-1.5 py-0.5 rounded-md border border-[#fa7014]/20 group-hover:bg-[#fa7014]/20 transition-colors tabular-nums", title: "Star count (1-5)", children: currentRating })] })] }), (0, jsx_runtime_1.jsxs)("blockquote", { "data-preview-field-path": `${basePath}.testimonials[${idx}].quote`, className: "font-serif-italic text-base sm:text-lg text-[#2b170e] leading-relaxed", children: ["\u201C", quoteText, "\u201D"] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "pt-4 border-t border-[#3d2114]/10 flex items-center gap-3.5", children: [(0, jsx_runtime_1.jsx)("img", { src: authorAvatar, alt: authorName, "data-preview-field-path": `${basePath}.testimonials[${idx}].avatar`, className: "w-12 h-12 rounded-full object-cover border border-[#3d2114]/15 shadow-sm" }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h4", { "data-preview-field-path": `${basePath}.testimonials[${idx}].author`, className: "font-heading font-black text-sm sm:text-base text-[#2b170e]", children: authorName }), (0, jsx_runtime_1.jsx)("p", { "data-preview-field-path": `${basePath}.testimonials[${idx}].role`, className: "text-xs text-[#786154] font-medium leading-snug", children: authorRole })] })] })] }));
84
+ }
85
+ function EditableTestimonialSection({ basePath = 'testimonials', badge = 'WORDS OF ACCLAIM', heading = 'WHAT THE CRITICS ARE SAYING', subheading = 'Reflections from international cuppers, culinary masters, and dedicated regulars.', testimonials = DEFAULT_TESTIMONIALS, maxStars = 5, className = '', cardClassName = '', style, ...props }) {
86
+ const items = (0, react_1.useMemo)(() => {
87
+ const list = Array.isArray(testimonials) ? testimonials : [];
88
+ if (list.length === 0) {
89
+ return DEFAULT_TESTIMONIALS;
90
+ }
91
+ return list.map((item, idx) => {
92
+ const fallback = DEFAULT_TESTIMONIALS[idx % DEFAULT_TESTIMONIALS.length] || DEFAULT_TESTIMONIALS[0];
93
+ if (!item || typeof item !== 'object') {
94
+ return { ...fallback, id: `fallback-${idx}` };
95
+ }
96
+ return {
97
+ ...fallback,
98
+ ...item,
99
+ id: item.id || `testimonial-${idx}`,
100
+ rating: item.rating !== undefined && item.rating !== null ? item.rating : 5,
101
+ };
102
+ });
103
+ }, [testimonials]);
104
+ return ((0, jsx_runtime_1.jsx)("section", { "data-design-section": "testimonials", className: `editable-testimonial-section w-full py-16 sm:py-24 px-4 sm:px-6 lg:px-10 ${className}`.trim(), style: style, ...props, children: (0, jsx_runtime_1.jsxs)("div", { className: "max-w-[1600px] mx-auto", children: [(0, jsx_runtime_1.jsxs)("div", { className: "text-center max-w-3xl mx-auto space-y-3 mb-12 sm:mb-16", children: [badge && ((0, jsx_runtime_1.jsx)("div", { className: "inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-[#ede0d4] text-[#3d2114] text-xs uppercase tracking-widest font-black border border-[#3d2114]/15", children: (0, jsx_runtime_1.jsx)("span", { "data-preview-field-path": `${basePath}.badge`, children: badge }) })), (0, jsx_runtime_1.jsx)("h2", { "data-preview-field-path": `${basePath}.heading`, className: "font-heading text-2xl xs:text-3xl sm:text-4xl lg:text-5xl font-black text-[#2b170e] tracking-tight", children: heading }), (0, jsx_runtime_1.jsx)("p", { "data-preview-field-path": `${basePath}.subheading`, className: "text-xs sm:text-base text-[#5c493f] leading-relaxed", children: subheading })] }), (0, jsx_runtime_1.jsx)("div", { "data-preview-list-path": `${basePath}.testimonials`, className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8", children: items.map((item, idx) => ((0, jsx_runtime_1.jsx)(TestimonialCard, { item: item, idx: idx, basePath: basePath, maxStars: maxStars, cardClassName: cardClassName }, item.id || `testimonial-${idx}`))) })] }) }));
105
+ }
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  export * from './PreviewField';
6
6
  export * from './EditableText';
7
7
  export * from './EditableImage';
8
+ export * from './EditableMap';
8
9
  export * from './EditableList';
9
10
  export * from './EditableBox';
10
11
  export * from './EditableGrid';
@@ -13,10 +14,12 @@ export * from './EditableProductCard';
13
14
  export * from './EditableProductGrid';
14
15
  export * from './EditableProductDetail';
15
16
  export * from './EditableCustomerReviews';
17
+ export * from './EditableGoogleFeedback';
16
18
  export * from './EditableServiceCard';
17
19
  export * from './EditableCard';
18
20
  export * from './EditablePricingCard';
19
21
  export * from './EditableTestimonialCard';
22
+ export * from './EditableTestimonialSection';
20
23
  export * from './EditableFAQAccordion';
21
24
  export * from './EditableContactForm';
22
25
  export * from './EditableNavbar';
@@ -56,6 +59,7 @@ export { EditableParagraph as Paragraph } from './EditableText';
56
59
  export { EditableBadge as Badge } from './EditableText';
57
60
  export { EditableQuote as Quote } from './EditableText';
58
61
  export { EditableImage as Image } from './EditableImage';
62
+ export { EditableMap as Map } from './EditableMap';
59
63
  export { EditableGrid as Grid } from './EditableGrid';
60
64
  export { EditableSection as Section } from './EditableSection';
61
65
  export { EditableBox as Box } from './EditableBox';
@@ -64,9 +68,11 @@ export { EditableProductCard as ProductCard } from './EditableProductCard';
64
68
  export { EditableProductGrid as ProductGrid } from './EditableProductGrid';
65
69
  export { EditableProductDetail as ProductDetail } from './EditableProductDetail';
66
70
  export { EditableCustomerReviews as CustomerReviews } from './EditableCustomerReviews';
71
+ export { EditableGoogleFeedback as GoogleFeedback } from './EditableGoogleFeedback';
67
72
  export { EditableServiceCard as ServiceCard } from './EditableServiceCard';
68
73
  export { EditablePricingCard as PricingCard } from './EditablePricingCard';
69
74
  export { EditableTestimonialCard as TestimonialCard } from './EditableTestimonialCard';
75
+ export { EditableTestimonialSection as TestimonialSection, EditableTestimonialSection as Testimonials } from './EditableTestimonialSection';
70
76
  export { EditableFAQAccordion as Accordion, EditableFAQAccordion as FAQ } from './EditableFAQAccordion';
71
77
  export { EditableContactForm as ContactForm } from './EditableContactForm';
72
78
  export { EditableNavbar as Navbar, EditableNavbar as Header } from './EditableNavbar';
package/dist/index.js CHANGED
@@ -18,11 +18,12 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
18
18
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
19
19
  };
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.Hero = exports.Footer = exports.Header = exports.Navbar = exports.ContactForm = exports.FAQ = exports.Accordion = exports.TestimonialCard = exports.PricingCard = exports.ServiceCard = exports.CustomerReviews = exports.ProductDetail = exports.ProductGrid = exports.ProductCard = exports.List = exports.Box = exports.Section = exports.Grid = exports.Image = exports.Quote = exports.Badge = exports.Paragraph = exports.Heading = exports.Text = exports.Dialog = exports.Card = exports.Button = exports.collectStyleTargetsFromHtml = exports.validateStyleTree = exports.styleToCssVariables = exports.patchStyleByPath = exports.patchElementStyle = exports.STYLE_TYPE_ATTRIBUTE = exports.STYLE_TARGET_ATTRIBUTE = exports.DENEB_STYLE_PATCH_MESSAGE = exports.STYLE_PATCH_MESSAGE = exports.formatResponsiveFontSize = exports.resolveInstallableFont = exports.normalizeFontId = exports.lookupFontDefinition = exports.listFontsByCategory = exports.collectFontIdsFromSiteData = exports.buildGoogleFontsStylesheetUrl = exports.DENEB_GOOGLE_FONT_COUNT = exports.DENEB_FONT_REGISTRY = exports.useDenebFonts = exports.DENEB_FONTS_LINK_ID = exports.FontLoader = exports.DenebComponentStyles = exports.ResponsiveBaseStyles = void 0;
22
- exports.DENEB_AUTHOR = exports.DENEB_FRAMEWORK_VERSION = exports.DENEB_FRAMEWORK_NAME = exports.FilterSidebar = exports.CartDrawer = exports.CategoryPills = exports.AnnouncementBar = exports.HeroSplit = void 0;
21
+ exports.ContactForm = exports.FAQ = exports.Accordion = exports.Testimonials = exports.TestimonialSection = exports.TestimonialCard = exports.PricingCard = exports.ServiceCard = exports.GoogleFeedback = exports.CustomerReviews = exports.ProductDetail = exports.ProductGrid = exports.ProductCard = exports.List = exports.Box = exports.Section = exports.Grid = exports.Map = exports.Image = exports.Quote = exports.Badge = exports.Paragraph = exports.Heading = exports.Text = exports.Dialog = exports.Card = exports.Button = exports.collectStyleTargetsFromHtml = exports.validateStyleTree = exports.styleToCssVariables = exports.patchStyleByPath = exports.patchElementStyle = exports.STYLE_TYPE_ATTRIBUTE = exports.STYLE_TARGET_ATTRIBUTE = exports.DENEB_STYLE_PATCH_MESSAGE = exports.STYLE_PATCH_MESSAGE = exports.formatResponsiveFontSize = exports.resolveInstallableFont = exports.normalizeFontId = exports.lookupFontDefinition = exports.listFontsByCategory = exports.collectFontIdsFromSiteData = exports.buildGoogleFontsStylesheetUrl = exports.DENEB_GOOGLE_FONT_COUNT = exports.DENEB_FONT_REGISTRY = exports.useDenebFonts = exports.DENEB_FONTS_LINK_ID = exports.FontLoader = exports.DenebComponentStyles = exports.ResponsiveBaseStyles = void 0;
22
+ exports.DENEB_AUTHOR = exports.DENEB_FRAMEWORK_VERSION = exports.DENEB_FRAMEWORK_NAME = exports.FilterSidebar = exports.CartDrawer = exports.CategoryPills = exports.AnnouncementBar = exports.HeroSplit = exports.Hero = exports.Footer = exports.Header = exports.Navbar = void 0;
23
23
  __exportStar(require("./PreviewField"), exports);
24
24
  __exportStar(require("./EditableText"), exports);
25
25
  __exportStar(require("./EditableImage"), exports);
26
+ __exportStar(require("./EditableMap"), exports);
26
27
  __exportStar(require("./EditableList"), exports);
27
28
  __exportStar(require("./EditableBox"), exports);
28
29
  __exportStar(require("./EditableGrid"), exports);
@@ -31,10 +32,12 @@ __exportStar(require("./EditableProductCard"), exports);
31
32
  __exportStar(require("./EditableProductGrid"), exports);
32
33
  __exportStar(require("./EditableProductDetail"), exports);
33
34
  __exportStar(require("./EditableCustomerReviews"), exports);
35
+ __exportStar(require("./EditableGoogleFeedback"), exports);
34
36
  __exportStar(require("./EditableServiceCard"), exports);
35
37
  __exportStar(require("./EditableCard"), exports);
36
38
  __exportStar(require("./EditablePricingCard"), exports);
37
39
  __exportStar(require("./EditableTestimonialCard"), exports);
40
+ __exportStar(require("./EditableTestimonialSection"), exports);
38
41
  __exportStar(require("./EditableFAQAccordion"), exports);
39
42
  __exportStar(require("./EditableContactForm"), exports);
40
43
  __exportStar(require("./EditableNavbar"), exports);
@@ -108,6 +111,8 @@ var EditableText_6 = require("./EditableText");
108
111
  Object.defineProperty(exports, "Quote", { enumerable: true, get: function () { return EditableText_6.EditableQuote; } });
109
112
  var EditableImage_1 = require("./EditableImage");
110
113
  Object.defineProperty(exports, "Image", { enumerable: true, get: function () { return EditableImage_1.EditableImage; } });
114
+ var EditableMap_1 = require("./EditableMap");
115
+ Object.defineProperty(exports, "Map", { enumerable: true, get: function () { return EditableMap_1.EditableMap; } });
111
116
  var EditableGrid_1 = require("./EditableGrid");
112
117
  Object.defineProperty(exports, "Grid", { enumerable: true, get: function () { return EditableGrid_1.EditableGrid; } });
113
118
  var EditableSection_1 = require("./EditableSection");
@@ -124,12 +129,17 @@ var EditableProductDetail_1 = require("./EditableProductDetail");
124
129
  Object.defineProperty(exports, "ProductDetail", { enumerable: true, get: function () { return EditableProductDetail_1.EditableProductDetail; } });
125
130
  var EditableCustomerReviews_1 = require("./EditableCustomerReviews");
126
131
  Object.defineProperty(exports, "CustomerReviews", { enumerable: true, get: function () { return EditableCustomerReviews_1.EditableCustomerReviews; } });
132
+ var EditableGoogleFeedback_1 = require("./EditableGoogleFeedback");
133
+ Object.defineProperty(exports, "GoogleFeedback", { enumerable: true, get: function () { return EditableGoogleFeedback_1.EditableGoogleFeedback; } });
127
134
  var EditableServiceCard_1 = require("./EditableServiceCard");
128
135
  Object.defineProperty(exports, "ServiceCard", { enumerable: true, get: function () { return EditableServiceCard_1.EditableServiceCard; } });
129
136
  var EditablePricingCard_1 = require("./EditablePricingCard");
130
137
  Object.defineProperty(exports, "PricingCard", { enumerable: true, get: function () { return EditablePricingCard_1.EditablePricingCard; } });
131
138
  var EditableTestimonialCard_1 = require("./EditableTestimonialCard");
132
139
  Object.defineProperty(exports, "TestimonialCard", { enumerable: true, get: function () { return EditableTestimonialCard_1.EditableTestimonialCard; } });
140
+ var EditableTestimonialSection_1 = require("./EditableTestimonialSection");
141
+ Object.defineProperty(exports, "TestimonialSection", { enumerable: true, get: function () { return EditableTestimonialSection_1.EditableTestimonialSection; } });
142
+ Object.defineProperty(exports, "Testimonials", { enumerable: true, get: function () { return EditableTestimonialSection_1.EditableTestimonialSection; } });
133
143
  var EditableFAQAccordion_1 = require("./EditableFAQAccordion");
134
144
  Object.defineProperty(exports, "Accordion", { enumerable: true, get: function () { return EditableFAQAccordion_1.EditableFAQAccordion; } });
135
145
  Object.defineProperty(exports, "FAQ", { enumerable: true, get: function () { return EditableFAQAccordion_1.EditableFAQAccordion; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/ui",
3
- "version": "2.0.35",
3
+ "version": "2.0.37",
4
4
  "description": "Visual-first React component library for editable commerce storefronts. Built for Next.js and Fivora.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -49,7 +49,7 @@
49
49
  ],
50
50
  "license": "MIT",
51
51
  "dependencies": {
52
- "@deneb-ui/core": "^2.0.35"
52
+ "@deneb-ui/core": "^2.0.37"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "react": "^18.0.0 || ^19.0.0",