@reblu/site-blocks 0.14.0 → 0.15.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @reblu/site-blocks
2
2
 
3
+ ## 0.15.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cc4e14e: REB-980: add `DataRequestForm` to `@reblu/site-blocks/client` — the drop-in public GDPR rights form (request type, name, email, optional message, consent checkbox). Requires a reCAPTCHA token from `getCaptchaToken` and never submits without one or without consent; shows the 72h response deadline in a `role="status"` region on success. Rejections are mapped by status to fixed, overridable copy (`400` validation, `422` captcha, `429` rate limit, otherwise generic) so no server text reaches the visitor. Default transport POSTs JSON to the SITE's `/api/data-requests` Route Handler (`endpoint`, or a custom `submit`). Also exports `DATA_REQUEST_TYPES` and `DEFAULT_DATA_REQUEST_FORM_LABELS`. README: page + Route Handler snippet.
8
+ - 2273e3c: Add `TrustBadges` — the drop-in for the tenant's trust badges from `client.trustBadges.list()` (REB-996). Server Component: groups badges by type under overridable headings (`DEFAULT_TRUST_BADGE_HEADINGS`, `SUBVENTION` → «Financiación»; `null` hides one), `types` filters and orders the groups, logos are images with `alt` = badge name, an optional http(s) link opens in a new window with `rel="noopener noreferrer"` and a visually hidden hint, and nothing is rendered when there are no badges. README: footer snippet.
9
+
10
+ ### Patch Changes
11
+
12
+ - f48712a: Consumer classNames keep custom theme tokens (REB-997). `CourseDescription`'s `headingClassName` / `contentClassName` and `ProductTrustItems`' `itemClassName` now replace their slot's stylistic defaults instead of being merged over them, and the consumer's `text-*` classes on `CourseDescription`, `ProductTrustItems`, `MarkdownRenderer` and `SafeHtml` reach the DOM verbatim — tailwind-merge read a custom font-size token such as `text-h2` as a colour and dropped it. Rendering without those props is unchanged.
13
+ - Updated dependencies [cc4e14e]
14
+ - @reblu/site-contracts@1.16.0
15
+
3
16
  ## 0.14.0
4
17
 
5
18
  ### Minor Changes
package/README.md CHANGED
@@ -30,11 +30,13 @@ colour on the red background. See `docs/theming/theming.md`.
30
30
  | `CartLineList`, `CartSummary`, `CartAdjustmentsNotice`, `AddedToCartToast`, `CheckoutLoginPrompt`, `CheckoutForm`, `OrderConfirmation` | `.` | Cart & checkout organisms. RSC-safe by construction. See "Cart & checkout" below. |
31
31
  | `PageBlocks`, `FaqAccordion` | `.` | Registry-block renderer (`PageDetail.blocks`) and native `<details>` FAQ. Server Components. See "Page blocks" below. |
32
32
  | `ProductTrustItems`, `DEFAULT_TRUST_ICONS` | `.` | Store-wide trust messages from `ShopConfig.productTrustItems`. Server Component. See "Product trust items" below. |
33
+ | `TrustBadges`, `DEFAULT_TRUST_BADGE_HEADINGS` | `.` | The tenant's trust badges from `client.trustBadges.list()`, grouped by type. Server Component. See "Trust badges" below. |
33
34
  | `buildFaqPageJsonLd`, `collectFaqItems` | `./seo` | schema.org `FAQPage` structured data. Directive-free (also on `.`). See "Page blocks" below. |
34
35
  | `useCollection` | `./client` | Headless filterable/sortable/paginated collection hook (facets, category, tag, search). Serves both page and blog indexes. |
35
36
  | `Collection` | `./client` | Token-driven presentational grid over `useCollection`. |
36
37
  | `ShareButtons` | `./client` | Social share (X / LinkedIn / WhatsApp / copy). |
37
38
  | `AddToCartButton`, `CartIcon` | `./client` | Add-to-cart feedback (REB-837) — **moved here from `.`**, see below. |
39
+ | `DataRequestForm` | `./client` | Drop-in public GDPR rights form over `POST /api/site/data-requests` (REB-980). See "GDPR rights form" below. |
38
40
 
39
41
  ## Boundary
40
42
 
@@ -42,6 +44,35 @@ colour on the red background. See `docs/theming/theming.md`.
42
44
  - **`@reblu/site-blocks`** = organisms + headless data hooks. Higher churn — its
43
45
  own package so a breaking change in an organism does not bump the stable atoms.
44
46
 
47
+ ## Styling a drop-in — the className rule (REB-997)
48
+
49
+ `cn` (tailwind-merge) only knows Tailwind's default scale. A SITE's own theme
50
+ token such as `text-h2` (a font size) is read as a text **colour**, so
51
+ `twMerge('text-xl font-semibold', 'text-h2 text-ink')` keeps `text-xl` and
52
+ drops `text-h2`. To keep custom tokens working, the typography slot props
53
+ follow one rule:
54
+
55
+ - **A slot className replaces that slot's stylistic defaults.** Passing
56
+ `CourseDescription`'s `headingClassName` drops its `text-xl font-semibold`;
57
+ `contentClassName` drops `text-muted-foreground`; `ProductTrustItems`'
58
+ `itemClassName` drops `text-sm text-muted-foreground`. Pass the full
59
+ typography you want for that slot.
60
+ - **Structural classes stay.** Layout an element needs (the trust row's
61
+ `flex items-center gap-3`, the `space-y-*` of a section or list, the
62
+ `max-w-none` of `MarkdownRenderer` / `SafeHtml`) is kept, and your layout
63
+ classes still override it through `cn` (`max-w-prose` beats `max-w-none`).
64
+ - **Your `text-*` classes reach the DOM verbatim** on those elements
65
+ (`CourseDescription`, `ProductTrustItems`, `MarkdownRenderer`, `SafeHtml`):
66
+ they never go through tailwind-merge, so `text-h2 text-ink` renders as both.
67
+ - **Without the prop, nothing changes** — the defaults render exactly as before.
68
+
69
+ The root `className` of interactive or boxed drop-ins (`AddToCartButton`,
70
+ `CartIcon`, `CartAdjustmentsNotice`, cards, toasts, the login prompt) is still
71
+ merged with `cn` over their defaults: those defaults pair a text colour with
72
+ its background, and a root className is mostly used for spacing. Put a custom
73
+ font-size token there on an element of your own (or wrap the drop-in) rather
74
+ than on the root className.
75
+
45
76
  ## Authoring notes
46
77
 
47
78
  What `MarkdownRenderer` does with tenant-authored Markdown and HTML. These are
@@ -546,7 +577,9 @@ export default async function ProductPage() {
546
577
  A semantic `<ul>` (or `<ol>` via `as='ol'`) of `<li>` rows; each icon is
547
578
  `aria-hidden` because the text carries the meaning. Colours come from theme
548
579
  tokens only (`text-muted-foreground`, `text-foreground/60`). `className` styles
549
- the list, `itemClassName` each row.
580
+ the list, `itemClassName` each row — it **replaces** the row's default
581
+ `text-sm text-muted-foreground` and keeps its `flex items-center gap-3` layout
582
+ (REB-997).
550
583
 
551
584
  ### Overriding icons
552
585
 
@@ -566,6 +599,45 @@ import { Truck } from 'my-icon-set'
566
599
  `icons` cannot add a new icon key: an item whose `icon` this version does not
567
600
  know is still skipped.
568
601
 
602
+ ## Trust badges
603
+
604
+ `TrustBadges` renders the tenant's trust badges — partners, insurers, funding,
605
+ certifications, awards — managed in nexus (`/trust-badges`) and served by
606
+ `client.trustBadges.list()` (REB-996). The API only returns badges that are
607
+ switched on AND inside their optional validity window («Visible desde» /
608
+ «Visible hasta», whole days in the tenant's time zone), so toggling or expiring
609
+ a badge in nexus removes it from the SITE on the next revalidation, with no
610
+ deploy and no SITE change.
611
+
612
+ ```tsx
613
+ import { TrustBadges } from '@reblu/site-blocks'
614
+
615
+ export async function Footer() {
616
+ const { data: badges } = await client.trustBadges.list().catch(() => ({ data: [] }))
617
+ return (
618
+ <footer>
619
+ {/* … */}
620
+ <TrustBadges badges={badges} headingLevel={2} />
621
+ </footer>
622
+ )
623
+ }
624
+ ```
625
+
626
+ - Groups by type, each under a heading from `DEFAULT_TRUST_BADGE_HEADINGS`
627
+ (`SUBVENTION` → «Financiación», `CERTIFICATION` → «Certificaciones»,
628
+ `AWARD` → «Premios», `PARTNER` → «Colaboradores», `INSURANCE` →
629
+ «Aseguradoras»). Override any with `headings`; `null` hides one.
630
+ - `types` picks which groups render and in what order, e.g. only the funding
631
+ logos: `<TrustBadges badges={badges} types={['SUBVENTION']} />`. To fetch
632
+ only one type, `client.trustBadges.list({ type: 'SUBVENTION' })`.
633
+ - A logo is an `<img>` with `alt` = badge name; a badge without a logo shows
634
+ its name as text. A badge `url` (http/https only) opens in a new window with
635
+ `rel="noopener noreferrer"` and a visually hidden «(se abre en una ventana
636
+ nueva)» — override it with `newWindowLabel`.
637
+ - `null`, `undefined` or an empty list render nothing — never an empty heading.
638
+ - Styling hooks: `className`, `groupClassName`, `headingClassName`,
639
+ `listClassName`, `logoClassName` (e.g. `brightness-0 invert` on a dark footer).
640
+
569
641
  ## `ParentPage` (formerly `Specialty`)
570
642
 
571
643
  `ParentPage` renders a page that groups other pages: the hero and content
@@ -778,13 +850,76 @@ export default async function CoursePage({ params }: Props) {
778
850
  description's own headings under it (`headingLevel`, default `2`; override with
779
851
  `headingOffset`; `headingId` when the page renders it twice);
780
852
  - styling hooks: `className`, `headingClassName`, `contentClassName`, and
781
- `data-slot='course-description'` on the outer element.
853
+ `data-slot='course-description'` on the outer element. `headingClassName`
854
+ and `contentClassName` **replace** the default `text-xl font-semibold` /
855
+ `text-muted-foreground` (see [the className rule](#styling-a-drop-in--the-classname-rule-reb-997)).
782
856
 
783
857
  For any other rich-text Markdown field, `MarkdownRenderer` is the generic
784
858
  renderer (`headingOffset` demotes its headings under yours).
785
859
  `markdownToPlainText` drops images and anything the sanitiser strips, and cuts
786
860
  at a word boundary with `…` within `maxLength`.
787
861
 
862
+ ## GDPR rights form — `DataRequestForm` (REB-980)
863
+
864
+ Drop-in form for a SITE's public rights page (e.g. `/derechos-rgpd`): request
865
+ type, name, email, optional message and a consent checkbox. A submission lands
866
+ in the tenant's nexus queue (`/settings/data-requests`) with source
867
+ `PUBLIC_FORM` and its 72h clock running; the form then shows the response
868
+ deadline (`slaDueAt`) in a `role="status"` region.
869
+
870
+ ```tsx
871
+ 'use client'
872
+ import { useCallback, useState } from 'react'
873
+ import { DataRequestForm } from '@reblu/site-blocks/client'
874
+ import { RECAPTCHA_DATA_REQUEST_ACTION } from '@reblu/site-next/recaptcha'
875
+ import {
876
+ DeferredRecaptchaProvider,
877
+ useDeferredRecaptchaToken,
878
+ } from '@reblu/site-next/recaptcha-provider'
879
+
880
+ function Form({ onArm }: { onArm: () => void }) {
881
+ const getCaptchaToken = useDeferredRecaptchaToken(RECAPTCHA_DATA_REQUEST_ACTION, { onArm })
882
+ return <DataRequestForm getCaptchaToken={getCaptchaToken} onInteract={onArm} />
883
+ }
884
+
885
+ export function DataRequestSection({ siteKey }: { siteKey: string }) {
886
+ const [armed, setArmed] = useState(false)
887
+ const arm = useCallback(() => setArmed(true), [])
888
+ return (
889
+ <DeferredRecaptchaProvider siteKey={siteKey} armed={armed}>
890
+ <Form onArm={arm} />
891
+ </DeferredRecaptchaProvider>
892
+ )
893
+ }
894
+ ```
895
+
896
+ ```ts
897
+ // app/api/data-requests/route.ts — server side, the API key never reaches the browser
898
+ import { createRebluClient } from '@reblu/site-client'
899
+
900
+ export async function POST(request: Request) {
901
+ const client = createRebluClient({ baseUrl: process.env.REBLU_API_BASE_URL!, apiKey: process.env.REBLU_API_KEY! })
902
+ try {
903
+ return Response.json({ data: await client.dataRequests.submit(await request.json()) })
904
+ } catch (e) {
905
+ return Response.json({ error: 'rejected' }, { status: (e as { status?: number }).status ?? 502 })
906
+ }
907
+ }
908
+ ```
909
+
910
+ - **reCAPTCHA is required.** base-api rejects a request without a token, so the
911
+ form never submits when `getCaptchaToken` resolves `undefined`.
912
+ - **No server text reaches the visitor.** Rejections are mapped by status to
913
+ fixed copy: `400` → `validationError`, `422` → `captchaError`, `429` →
914
+ `rateLimitError`, anything else (or no answer) → `genericError`.
915
+ - **Transport.** By default the form POSTs JSON to `endpoint`
916
+ (`/api/data-requests`) and reads the `{ data }` envelope. Pass `submit` to
917
+ use your own transport; it returns `{ ok: true, data }` or `{ ok: false, status }`.
918
+ - **Copy** defaults to Spanish; override any string with `labels` (including
919
+ `requestTypes` per right and `success(deadline)`), the date locale with
920
+ `locale`, and the offered rights with `requestTypes`.
921
+ - `onSubmitted(ack)` fires after success — e.g. for analytics.
922
+
788
923
  ## Security boundaries
789
924
 
790
925
  Two sinks, two different helpers, in two different packages — do not reach for
package/dist/client.cjs CHANGED
@@ -34,6 +34,9 @@ __export(client_exports, {
34
34
  AddToCartButton: () => AddToCartButton,
35
35
  CartIcon: () => CartIcon,
36
36
  Collection: () => Collection,
37
+ DATA_REQUEST_TYPES: () => DATA_REQUEST_TYPES,
38
+ DEFAULT_DATA_REQUEST_FORM_LABELS: () => DEFAULT_DATA_REQUEST_FORM_LABELS,
39
+ DataRequestForm: () => DataRequestForm,
37
40
  ShareButtons: () => ShareButtons,
38
41
  useCollection: () => useCollection
39
42
  });
@@ -522,11 +525,267 @@ function CartIcon({ itemCount, href, hydrated, bumpOnAdd, labels, className }) {
522
525
  }
523
526
  );
524
527
  }
528
+
529
+ // src/components/data-request-form.tsx
530
+ var import_react4 = require("react");
531
+ var import_utils5 = require("@reblu/site-ui/utils");
532
+ var import_jsx_runtime5 = require("react/jsx-runtime");
533
+ var DATA_REQUEST_TYPES = [
534
+ "ACCESS",
535
+ "RECTIFICATION",
536
+ "ERASURE",
537
+ "OBJECTION",
538
+ "PORTABILITY",
539
+ "RESTRICTION",
540
+ "AUTOMATED_DECISION",
541
+ "INFORMATION"
542
+ ];
543
+ var DEFAULT_DATA_REQUEST_FORM_LABELS = {
544
+ requestTypeLabel: "Derecho que quieres ejercer",
545
+ requestTypePlaceholder: "Selecciona una opci\xF3n",
546
+ requestTypes: {
547
+ ACCESS: "Acceso",
548
+ RECTIFICATION: "Rectificaci\xF3n",
549
+ ERASURE: "Supresi\xF3n",
550
+ OBJECTION: "Oposici\xF3n",
551
+ PORTABILITY: "Portabilidad",
552
+ RESTRICTION: "Limitaci\xF3n del tratamiento",
553
+ AUTOMATED_DECISION: "No ser objeto de decisiones automatizadas",
554
+ INFORMATION: "Informaci\xF3n"
555
+ },
556
+ nameLabel: "Nombre y apellidos",
557
+ emailLabel: "Email",
558
+ detailsLabel: "Mensaje (opcional)",
559
+ detailsPlaceholder: "Describe tu solicitud",
560
+ consent: "Acepto que mis datos se traten para gestionar esta solicitud.",
561
+ submit: "Enviar solicitud",
562
+ submitting: "Enviando\u2026",
563
+ requestTypeRequired: "Elige el derecho que quieres ejercer.",
564
+ nameRequired: "Indica tu nombre.",
565
+ emailInvalid: "Introduce un email v\xE1lido.",
566
+ consentRequired: "Debes aceptar el tratamiento de tus datos para gestionar la solicitud.",
567
+ success: (deadline) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
568
+ "Hemos recibido tu solicitud. Te responderemos como m\xE1ximo el ",
569
+ deadline,
570
+ "."
571
+ ] }),
572
+ captchaError: "No hemos podido verificar que no eres un robot. Recarga la p\xE1gina e int\xE9ntalo de nuevo.",
573
+ validationError: "Revisa los datos del formulario e int\xE9ntalo de nuevo.",
574
+ rateLimitError: "Has enviado demasiadas solicitudes. Espera unos minutos e int\xE9ntalo de nuevo.",
575
+ genericError: "No hemos podido enviar tu solicitud. Int\xE9ntalo de nuevo m\xE1s tarde."
576
+ };
577
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
578
+ async function postToRoute(endpoint, input) {
579
+ try {
580
+ const res = await fetch(endpoint, {
581
+ method: "POST",
582
+ headers: { "Content-Type": "application/json" },
583
+ body: JSON.stringify(input)
584
+ });
585
+ if (!res.ok) return { ok: false, status: res.status };
586
+ const json = await res.json();
587
+ return json.data ? { ok: true, data: json.data } : { ok: false, status: res.status };
588
+ } catch {
589
+ return { ok: false, status: 0 };
590
+ }
591
+ }
592
+ function rejectionMessage(status, t) {
593
+ if (status === 400) return t.validationError;
594
+ if (status === 422) return t.captchaError;
595
+ if (status === 429) return t.rateLimitError;
596
+ return t.genericError;
597
+ }
598
+ var FIELD_CLASS = "w-full rounded-lg border bg-background px-3 py-2 text-sm transition focus:outline-none focus:ring-2 focus:ring-primary/20";
599
+ function DataRequestForm({
600
+ getCaptchaToken,
601
+ submit,
602
+ endpoint = "/api/data-requests",
603
+ onInteract,
604
+ onSubmitted,
605
+ requestTypes = DATA_REQUEST_TYPES,
606
+ labels,
607
+ locale = "es-ES",
608
+ className
609
+ }) {
610
+ const t = {
611
+ ...DEFAULT_DATA_REQUEST_FORM_LABELS,
612
+ ...labels,
613
+ requestTypes: { ...DEFAULT_DATA_REQUEST_FORM_LABELS.requestTypes, ...labels?.requestTypes }
614
+ };
615
+ const id = (0, import_react4.useId)();
616
+ const [requestType, setRequestType] = (0, import_react4.useState)("");
617
+ const [name, setName] = (0, import_react4.useState)("");
618
+ const [email, setEmail] = (0, import_react4.useState)("");
619
+ const [details, setDetails] = (0, import_react4.useState)("");
620
+ const [consent, setConsent] = (0, import_react4.useState)(false);
621
+ const [errors, setErrors] = (0, import_react4.useState)({});
622
+ const [formError, setFormError] = (0, import_react4.useState)(null);
623
+ const [submitting, setSubmitting] = (0, import_react4.useState)(false);
624
+ const [ack, setAck] = (0, import_react4.useState)(null);
625
+ async function handleSubmit(event) {
626
+ event.preventDefault();
627
+ if (submitting) return;
628
+ setFormError(null);
629
+ const next = {};
630
+ if (!requestType) next.requestType = t.requestTypeRequired;
631
+ if (!name.trim()) next.name = t.nameRequired;
632
+ if (!EMAIL_PATTERN.test(email.trim())) next.email = t.emailInvalid;
633
+ if (!consent) next.consent = t.consentRequired;
634
+ setErrors(next);
635
+ if (Object.keys(next).length > 0 || !requestType) return;
636
+ setSubmitting(true);
637
+ try {
638
+ const captcha = await getCaptchaToken().catch(() => void 0);
639
+ if (!captcha) {
640
+ setFormError(t.captchaError);
641
+ return;
642
+ }
643
+ const input = {
644
+ requestType,
645
+ name: name.trim(),
646
+ email: email.trim(),
647
+ ...details.trim() ? { details: details.trim() } : {},
648
+ captcha
649
+ };
650
+ const result = await (submit ?? ((body) => postToRoute(endpoint, body)))(input).catch(
651
+ () => ({ ok: false, status: 0 })
652
+ );
653
+ if (!result.ok) {
654
+ setFormError(rejectionMessage(result.status, t));
655
+ return;
656
+ }
657
+ setAck(result.data);
658
+ onSubmitted?.(result.data);
659
+ } finally {
660
+ setSubmitting(false);
661
+ }
662
+ }
663
+ if (ack) {
664
+ const deadline = new Intl.DateTimeFormat(locale, {
665
+ dateStyle: "long",
666
+ timeStyle: "short"
667
+ }).format(new Date(ack.slaDueAt));
668
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { role: "status", "data-slot": "data-request-form-success", className: (0, import_utils5.cn)("rounded-lg border border-border p-6 text-sm", className), children: t.success(deadline) });
669
+ }
670
+ const errorId = (field) => errors[field] ? `${id}-${field}-error` : void 0;
671
+ const fieldError = (field) => errors[field] ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { id: `${id}-${field}-error`, className: "text-xs text-destructive", children: errors[field] }) : null;
672
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
673
+ "form",
674
+ {
675
+ noValidate: true,
676
+ onSubmit: handleSubmit,
677
+ onFocusCapture: onInteract,
678
+ onPointerDown: onInteract,
679
+ "data-slot": "data-request-form",
680
+ className: (0, import_utils5.cn)("space-y-4", className),
681
+ children: [
682
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "space-y-1", children: [
683
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { htmlFor: `${id}-type`, className: "text-sm font-medium", children: t.requestTypeLabel }),
684
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
685
+ "select",
686
+ {
687
+ id: `${id}-type`,
688
+ value: requestType,
689
+ onChange: (e) => setRequestType(e.target.value),
690
+ "aria-invalid": errors.requestType ? true : void 0,
691
+ "aria-describedby": errorId("requestType"),
692
+ className: (0, import_utils5.cn)(FIELD_CLASS, errors.requestType ? "border-destructive" : "border-border"),
693
+ children: [
694
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: "", children: t.requestTypePlaceholder }),
695
+ requestTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: type, children: t.requestTypes[type] }, type))
696
+ ]
697
+ }
698
+ ),
699
+ fieldError("requestType")
700
+ ] }),
701
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "space-y-1", children: [
702
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { htmlFor: `${id}-name`, className: "text-sm font-medium", children: t.nameLabel }),
703
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
704
+ "input",
705
+ {
706
+ id: `${id}-name`,
707
+ type: "text",
708
+ autoComplete: "name",
709
+ value: name,
710
+ onChange: (e) => setName(e.target.value),
711
+ "aria-invalid": errors.name ? true : void 0,
712
+ "aria-describedby": errorId("name"),
713
+ className: (0, import_utils5.cn)(FIELD_CLASS, errors.name ? "border-destructive" : "border-border")
714
+ }
715
+ ),
716
+ fieldError("name")
717
+ ] }),
718
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "space-y-1", children: [
719
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { htmlFor: `${id}-email`, className: "text-sm font-medium", children: t.emailLabel }),
720
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
721
+ "input",
722
+ {
723
+ id: `${id}-email`,
724
+ type: "email",
725
+ autoComplete: "email",
726
+ value: email,
727
+ onChange: (e) => setEmail(e.target.value),
728
+ "aria-invalid": errors.email ? true : void 0,
729
+ "aria-describedby": errorId("email"),
730
+ className: (0, import_utils5.cn)(FIELD_CLASS, errors.email ? "border-destructive" : "border-border")
731
+ }
732
+ ),
733
+ fieldError("email")
734
+ ] }),
735
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "space-y-1", children: [
736
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { htmlFor: `${id}-details`, className: "text-sm font-medium", children: t.detailsLabel }),
737
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
738
+ "textarea",
739
+ {
740
+ id: `${id}-details`,
741
+ rows: 4,
742
+ value: details,
743
+ onChange: (e) => setDetails(e.target.value),
744
+ placeholder: t.detailsPlaceholder,
745
+ className: (0, import_utils5.cn)(FIELD_CLASS, "border-border")
746
+ }
747
+ )
748
+ ] }),
749
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "space-y-1", children: [
750
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-start gap-2", children: [
751
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
752
+ "input",
753
+ {
754
+ id: `${id}-consent`,
755
+ type: "checkbox",
756
+ checked: consent,
757
+ onChange: (e) => setConsent(e.target.checked),
758
+ "aria-invalid": errors.consent ? true : void 0,
759
+ "aria-describedby": errorId("consent"),
760
+ className: "mt-1"
761
+ }
762
+ ),
763
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { htmlFor: `${id}-consent`, className: "text-sm text-foreground/80", children: t.consent })
764
+ ] }),
765
+ fieldError("consent")
766
+ ] }),
767
+ formError && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { role: "alert", className: "text-sm text-destructive", children: formError }),
768
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
769
+ "button",
770
+ {
771
+ type: "submit",
772
+ disabled: submitting,
773
+ className: "rounded-lg bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground disabled:opacity-60",
774
+ children: submitting ? t.submitting : t.submit
775
+ }
776
+ )
777
+ ]
778
+ }
779
+ );
780
+ }
525
781
  // Annotate the CommonJS export names for ESM import in node:
526
782
  0 && (module.exports = {
527
783
  AddToCartButton,
528
784
  CartIcon,
529
785
  Collection,
786
+ DATA_REQUEST_TYPES,
787
+ DEFAULT_DATA_REQUEST_FORM_LABELS,
788
+ DataRequestForm,
530
789
  ShareButtons,
531
790
  useCollection
532
791
  });