@reblu/site-next 1.20.0 → 1.21.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 +14 -0
- package/README.md +77 -1
- package/dist/analytics.js +1 -1
- package/dist/auth.js +3 -3
- package/dist/cart.cjs +220 -2
- package/dist/cart.cjs.map +1 -1
- package/dist/cart.d.cts +126 -1
- package/dist/cart.d.ts +126 -1
- package/dist/cart.js +214 -1
- package/dist/cart.js.map +1 -1
- package/dist/consent.js +1 -1
- package/dist/cta-buttons.cjs +13 -1
- package/dist/cta-buttons.cjs.map +1 -1
- package/dist/cta-buttons.d.cts +6 -2
- package/dist/cta-buttons.d.ts +6 -2
- package/dist/cta-buttons.js +16 -4
- package/dist/cta-buttons.js.map +1 -1
- package/dist/index.cjs +13 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +18 -6
- package/dist/index.js.map +1 -1
- package/dist/shop.js +2 -2
- package/package.json +4 -4
package/dist/cta-buttons.cjs
CHANGED
|
@@ -36,11 +36,23 @@ __export(cta_buttons_exports, {
|
|
|
36
36
|
module.exports = __toCommonJS(cta_buttons_exports);
|
|
37
37
|
var import_site_ui = require("@reblu/site-ui");
|
|
38
38
|
var import_link = __toESM(require("next/link"), 1);
|
|
39
|
+
var import_react = require("react");
|
|
39
40
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
40
41
|
var DEFAULT_NEW_WINDOW_HINT = "(se abre en una nueva ventana)";
|
|
41
42
|
function isExternal(href) {
|
|
42
43
|
return /^https?:\/\//i.test(href);
|
|
43
44
|
}
|
|
45
|
+
function renderLeadingIcon(icon) {
|
|
46
|
+
if (icon == null || icon === false) return null;
|
|
47
|
+
if ((0, import_react.isValidElement)(icon) && icon.type !== import_react.Fragment) {
|
|
48
|
+
const element = icon;
|
|
49
|
+
return (0, import_react.cloneElement)(element, {
|
|
50
|
+
"aria-hidden": true,
|
|
51
|
+
className: (0, import_site_ui.cn)(element.props.className, "shrink-0")
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", className: "contents", children: icon });
|
|
55
|
+
}
|
|
44
56
|
function CtaButtons({
|
|
45
57
|
ctas,
|
|
46
58
|
className,
|
|
@@ -54,7 +66,7 @@ function CtaButtons({
|
|
|
54
66
|
newWindowHint = DEFAULT_NEW_WINDOW_HINT
|
|
55
67
|
}) {
|
|
56
68
|
if (ctas.length === 0) return null;
|
|
57
|
-
const leadingIcon =
|
|
69
|
+
const leadingIcon = renderLeadingIcon(icon);
|
|
58
70
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className, "data-slot": "cta-buttons", children: ctas.map((cta, index) => {
|
|
59
71
|
const isPrimary = index === 0;
|
|
60
72
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
package/dist/cta-buttons.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cta-buttons.tsx"],"sourcesContent":["'use client'\n\nimport type { Cta } from '@reblu/site-contracts'\nimport { Button, cn, type ButtonVariantProps } from '@reblu/site-ui'\nimport Link from 'next/link'\nimport type
|
|
1
|
+
{"version":3,"sources":["../src/cta-buttons.tsx"],"sourcesContent":["'use client'\n\nimport type { Cta } from '@reblu/site-contracts'\nimport { Button, cn, type ButtonVariantProps } from '@reblu/site-ui'\nimport Link from 'next/link'\nimport { cloneElement, Fragment, isValidElement, type ReactElement, type ReactNode } from 'react'\n\ntype ButtonSize = 'sm' | 'default' | 'lg'\n\n/** A `@reblu/site-ui` Button variant (`default`, `outline`, `secondary`, …). */\nexport type CtaButtonVariant = NonNullable<ButtonVariantProps['variant']>\n\n/** Default screen-reader hint appended to CTAs that open in a new tab. */\nconst DEFAULT_NEW_WINDOW_HINT = '(se abre en una nueva ventana)'\n\nexport interface CtaButtonsProps {\n ctas: Cta[]\n /** Wrapper class (layout of the button group). */\n className?: string\n /** Class applied to each button. */\n buttonClassName?: string\n size?: ButtonSize\n /**\n * Leading icon rendered before every CTA label. Decorative: it is marked\n * `aria-hidden`, so the accessible name stays the label. A custom icon\n * component must forward `aria-hidden` and `className` to its root element. An element icon (an\n * `<svg>` or an icon component) is rendered as a direct child of the button,\n * so shadcn's `has-[>svg]` icon padding still matches; anything else is\n * wrapped in an `aria-hidden` `display: contents` span.\n */\n icon?: ReactNode\n /** Button variant of the primary CTA (the first one, by `sortOrder`). Default `default`. */\n primaryVariant?: CtaButtonVariant\n /** Button variant of every secondary CTA (all but the first). Default `default`. */\n secondaryVariant?: CtaButtonVariant\n /**\n * Extra class for the primary CTA, merged after `buttonClassName`. Lets a SITE\n * map the primary CTA to its own button style (e.g. a brand `cta` variant).\n */\n primaryClassName?: string\n /** Extra class for every secondary CTA, merged after `buttonClassName`. */\n secondaryClassName?: string\n /**\n * Visually-hidden text appended to CTAs that open in a new tab, so screen\n * readers announce the context change. Default «(se abre en una nueva ventana)».\n */\n newWindowHint?: string\n}\n\n/** External = absolute http(s) URL (e.g. a third-party booking link). */\nfunction isExternal(href: string): boolean {\n return /^https?:\\/\\//i.test(href)\n}\n\n/**\n * REB-930 — no wrapper around an element icon: `has-[>svg]:px-*` only matches an\n * svg that is a DIRECT child of the button. `display: contents` on the fallback\n * wrapper keeps it out of the flex layout, though a wrapped svg is still not a\n * direct child.\n */\nfunction renderLeadingIcon(icon: ReactNode): ReactNode {\n if (icon == null || icon === false) return null\n // A fragment cannot carry aria-hidden, so it takes the wrapper like a non-element.\n if (isValidElement(icon) && icon.type !== Fragment) {\n const element = icon as ReactElement<{ 'aria-hidden'?: boolean | 'true'; className?: string }>\n // shrink-0: the old wrapper kept any icon from shrinking in a narrow button;\n // shadcn's `[&_svg]:shrink-0` only covers svg.\n return cloneElement(element, {\n 'aria-hidden': true,\n className: cn(element.props.className, 'shrink-0'),\n })\n }\n return (\n <span aria-hidden='true' className='contents'>\n {icon}\n </span>\n )\n}\n\n/**\n * Presentational CTA renderer (REB-394, moved to `@reblu/site-next` in REB-418).\n * Pure — takes already-resolved CTAs and renders one styled button-link each.\n * Internal hrefs use `next/link` (client routing); external URLs open in a new,\n * safely-rel tab and carry a visually-hidden new-window hint. `analyticsId` is\n * surfaced as `data-cta-id` on the clickable element for GA `cta_click`\n * tracking. Renders nothing when there are no CTAs.\n *\n * The first CTA is the primary one; the rest are secondary. Each role takes its\n * own Button variant and extra class, so a SITE maps the CTAs to its own button\n * styles without forking this markup.\n *\n * Imports `Button` from `@reblu/site-ui` (public) — never `@reblu/ui` (private).\n */\nexport function CtaButtons({\n ctas,\n className,\n buttonClassName,\n size = 'lg',\n icon,\n primaryVariant = 'default',\n secondaryVariant = 'default',\n primaryClassName,\n secondaryClassName,\n newWindowHint = DEFAULT_NEW_WINDOW_HINT,\n}: CtaButtonsProps) {\n if (ctas.length === 0) return null\n\n const leadingIcon = renderLeadingIcon(icon)\n\n return (\n <div className={className} data-slot='cta-buttons'>\n {ctas.map((cta, index) => {\n const isPrimary = index === 0\n return (\n <Button\n key={cta.id}\n asChild\n size={size}\n variant={isPrimary ? primaryVariant : secondaryVariant}\n className={cn(buttonClassName, isPrimary ? primaryClassName : secondaryClassName)}\n data-cta-id={cta.analyticsId ?? undefined}\n >\n {isExternal(cta.href) ? (\n <a href={cta.href} target='_blank' rel='noopener noreferrer'>\n {leadingIcon}\n {cta.label}\n {newWindowHint ? (\n <>\n {' '}\n <span className='sr-only'>{newWindowHint}</span>\n </>\n ) : null}\n </a>\n ) : (\n <Link href={cta.href}>\n {leadingIcon}\n {cta.label}\n </Link>\n )}\n </Button>\n )\n })}\n </div>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,qBAAoD;AACpD,kBAAiB;AACjB,mBAA0F;AAoEtF;AA5DJ,IAAM,0BAA0B;AAqChC,SAAS,WAAW,MAAuB;AACzC,SAAO,gBAAgB,KAAK,IAAI;AAClC;AAQA,SAAS,kBAAkB,MAA4B;AACrD,MAAI,QAAQ,QAAQ,SAAS,MAAO,QAAO;AAE3C,UAAI,6BAAe,IAAI,KAAK,KAAK,SAAS,uBAAU;AAClD,UAAM,UAAU;AAGhB,eAAO,2BAAa,SAAS;AAAA,MAC3B,eAAe;AAAA,MACf,eAAW,mBAAG,QAAQ,MAAM,WAAW,UAAU;AAAA,IACnD,CAAC;AAAA,EACH;AACA,SACE,4CAAC,UAAK,eAAY,QAAO,WAAU,YAChC,gBACH;AAEJ;AAgBO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB;AAAA,EACA;AAAA,EACA,gBAAgB;AAClB,GAAoB;AAClB,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,QAAM,cAAc,kBAAkB,IAAI;AAE1C,SACE,4CAAC,SAAI,WAAsB,aAAU,eAClC,eAAK,IAAI,CAAC,KAAK,UAAU;AACxB,UAAM,YAAY,UAAU;AAC5B,WACE;AAAA,MAAC;AAAA;AAAA,QAEC,SAAO;AAAA,QACP;AAAA,QACA,SAAS,YAAY,iBAAiB;AAAA,QACtC,eAAW,mBAAG,iBAAiB,YAAY,mBAAmB,kBAAkB;AAAA,QAChF,eAAa,IAAI,eAAe;AAAA,QAE/B,qBAAW,IAAI,IAAI,IAClB,6CAAC,OAAE,MAAM,IAAI,MAAM,QAAO,UAAS,KAAI,uBACpC;AAAA;AAAA,UACA,IAAI;AAAA,UACJ,gBACC,4EACG;AAAA;AAAA,YACD,4CAAC,UAAK,WAAU,WAAW,yBAAc;AAAA,aAC3C,IACE;AAAA,WACN,IAEA,6CAAC,YAAAA,SAAA,EAAK,MAAM,IAAI,MACb;AAAA;AAAA,UACA,IAAI;AAAA,WACP;AAAA;AAAA,MAtBG,IAAI;AAAA,IAwBX;AAAA,EAEJ,CAAC,GACH;AAEJ;","names":["Link"]}
|
package/dist/cta-buttons.d.cts
CHANGED
|
@@ -14,8 +14,12 @@ interface CtaButtonsProps {
|
|
|
14
14
|
buttonClassName?: string;
|
|
15
15
|
size?: ButtonSize;
|
|
16
16
|
/**
|
|
17
|
-
* Leading icon rendered before every CTA label. Decorative: it is
|
|
18
|
-
*
|
|
17
|
+
* Leading icon rendered before every CTA label. Decorative: it is marked
|
|
18
|
+
* `aria-hidden`, so the accessible name stays the label. A custom icon
|
|
19
|
+
* component must forward `aria-hidden` and `className` to its root element. An element icon (an
|
|
20
|
+
* `<svg>` or an icon component) is rendered as a direct child of the button,
|
|
21
|
+
* so shadcn's `has-[>svg]` icon padding still matches; anything else is
|
|
22
|
+
* wrapped in an `aria-hidden` `display: contents` span.
|
|
19
23
|
*/
|
|
20
24
|
icon?: ReactNode;
|
|
21
25
|
/** Button variant of the primary CTA (the first one, by `sortOrder`). Default `default`. */
|
package/dist/cta-buttons.d.ts
CHANGED
|
@@ -14,8 +14,12 @@ interface CtaButtonsProps {
|
|
|
14
14
|
buttonClassName?: string;
|
|
15
15
|
size?: ButtonSize;
|
|
16
16
|
/**
|
|
17
|
-
* Leading icon rendered before every CTA label. Decorative: it is
|
|
18
|
-
*
|
|
17
|
+
* Leading icon rendered before every CTA label. Decorative: it is marked
|
|
18
|
+
* `aria-hidden`, so the accessible name stays the label. A custom icon
|
|
19
|
+
* component must forward `aria-hidden` and `className` to its root element. An element icon (an
|
|
20
|
+
* `<svg>` or an icon component) is rendered as a direct child of the button,
|
|
21
|
+
* so shadcn's `has-[>svg]` icon padding still matches; anything else is
|
|
22
|
+
* wrapped in an `aria-hidden` `display: contents` span.
|
|
19
23
|
*/
|
|
20
24
|
icon?: ReactNode;
|
|
21
25
|
/** Button variant of the primary CTA (the first one, by `sortOrder`). Default `default`. */
|
package/dist/cta-buttons.js
CHANGED
|
@@ -2,12 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cta-buttons.tsx
|
|
4
4
|
import { Button, cn } from "@reblu/site-ui";
|
|
5
|
-
import Link from "next/link";
|
|
6
|
-
import {
|
|
5
|
+
import Link from "next/link.js";
|
|
6
|
+
import { cloneElement, Fragment, isValidElement } from "react";
|
|
7
|
+
import { Fragment as Fragment2, jsx, jsxs } from "react/jsx-runtime";
|
|
7
8
|
var DEFAULT_NEW_WINDOW_HINT = "(se abre en una nueva ventana)";
|
|
8
9
|
function isExternal(href) {
|
|
9
10
|
return /^https?:\/\//i.test(href);
|
|
10
11
|
}
|
|
12
|
+
function renderLeadingIcon(icon) {
|
|
13
|
+
if (icon == null || icon === false) return null;
|
|
14
|
+
if (isValidElement(icon) && icon.type !== Fragment) {
|
|
15
|
+
const element = icon;
|
|
16
|
+
return cloneElement(element, {
|
|
17
|
+
"aria-hidden": true,
|
|
18
|
+
className: cn(element.props.className, "shrink-0")
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "contents", children: icon });
|
|
22
|
+
}
|
|
11
23
|
function CtaButtons({
|
|
12
24
|
ctas,
|
|
13
25
|
className,
|
|
@@ -21,7 +33,7 @@ function CtaButtons({
|
|
|
21
33
|
newWindowHint = DEFAULT_NEW_WINDOW_HINT
|
|
22
34
|
}) {
|
|
23
35
|
if (ctas.length === 0) return null;
|
|
24
|
-
const leadingIcon =
|
|
36
|
+
const leadingIcon = renderLeadingIcon(icon);
|
|
25
37
|
return /* @__PURE__ */ jsx("div", { className, "data-slot": "cta-buttons", children: ctas.map((cta, index) => {
|
|
26
38
|
const isPrimary = index === 0;
|
|
27
39
|
return /* @__PURE__ */ jsx(
|
|
@@ -35,7 +47,7 @@ function CtaButtons({
|
|
|
35
47
|
children: isExternal(cta.href) ? /* @__PURE__ */ jsxs("a", { href: cta.href, target: "_blank", rel: "noopener noreferrer", children: [
|
|
36
48
|
leadingIcon,
|
|
37
49
|
cta.label,
|
|
38
|
-
newWindowHint ? /* @__PURE__ */ jsxs(
|
|
50
|
+
newWindowHint ? /* @__PURE__ */ jsxs(Fragment2, { children: [
|
|
39
51
|
" ",
|
|
40
52
|
/* @__PURE__ */ jsx("span", { className: "sr-only", children: newWindowHint })
|
|
41
53
|
] }) : null
|
package/dist/cta-buttons.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cta-buttons.tsx"],"sourcesContent":["'use client'\n\nimport type { Cta } from '@reblu/site-contracts'\nimport { Button, cn, type ButtonVariantProps } from '@reblu/site-ui'\nimport Link from 'next/link'\nimport type
|
|
1
|
+
{"version":3,"sources":["../src/cta-buttons.tsx"],"sourcesContent":["'use client'\n\nimport type { Cta } from '@reblu/site-contracts'\nimport { Button, cn, type ButtonVariantProps } from '@reblu/site-ui'\nimport Link from 'next/link'\nimport { cloneElement, Fragment, isValidElement, type ReactElement, type ReactNode } from 'react'\n\ntype ButtonSize = 'sm' | 'default' | 'lg'\n\n/** A `@reblu/site-ui` Button variant (`default`, `outline`, `secondary`, …). */\nexport type CtaButtonVariant = NonNullable<ButtonVariantProps['variant']>\n\n/** Default screen-reader hint appended to CTAs that open in a new tab. */\nconst DEFAULT_NEW_WINDOW_HINT = '(se abre en una nueva ventana)'\n\nexport interface CtaButtonsProps {\n ctas: Cta[]\n /** Wrapper class (layout of the button group). */\n className?: string\n /** Class applied to each button. */\n buttonClassName?: string\n size?: ButtonSize\n /**\n * Leading icon rendered before every CTA label. Decorative: it is marked\n * `aria-hidden`, so the accessible name stays the label. A custom icon\n * component must forward `aria-hidden` and `className` to its root element. An element icon (an\n * `<svg>` or an icon component) is rendered as a direct child of the button,\n * so shadcn's `has-[>svg]` icon padding still matches; anything else is\n * wrapped in an `aria-hidden` `display: contents` span.\n */\n icon?: ReactNode\n /** Button variant of the primary CTA (the first one, by `sortOrder`). Default `default`. */\n primaryVariant?: CtaButtonVariant\n /** Button variant of every secondary CTA (all but the first). Default `default`. */\n secondaryVariant?: CtaButtonVariant\n /**\n * Extra class for the primary CTA, merged after `buttonClassName`. Lets a SITE\n * map the primary CTA to its own button style (e.g. a brand `cta` variant).\n */\n primaryClassName?: string\n /** Extra class for every secondary CTA, merged after `buttonClassName`. */\n secondaryClassName?: string\n /**\n * Visually-hidden text appended to CTAs that open in a new tab, so screen\n * readers announce the context change. Default «(se abre en una nueva ventana)».\n */\n newWindowHint?: string\n}\n\n/** External = absolute http(s) URL (e.g. a third-party booking link). */\nfunction isExternal(href: string): boolean {\n return /^https?:\\/\\//i.test(href)\n}\n\n/**\n * REB-930 — no wrapper around an element icon: `has-[>svg]:px-*` only matches an\n * svg that is a DIRECT child of the button. `display: contents` on the fallback\n * wrapper keeps it out of the flex layout, though a wrapped svg is still not a\n * direct child.\n */\nfunction renderLeadingIcon(icon: ReactNode): ReactNode {\n if (icon == null || icon === false) return null\n // A fragment cannot carry aria-hidden, so it takes the wrapper like a non-element.\n if (isValidElement(icon) && icon.type !== Fragment) {\n const element = icon as ReactElement<{ 'aria-hidden'?: boolean | 'true'; className?: string }>\n // shrink-0: the old wrapper kept any icon from shrinking in a narrow button;\n // shadcn's `[&_svg]:shrink-0` only covers svg.\n return cloneElement(element, {\n 'aria-hidden': true,\n className: cn(element.props.className, 'shrink-0'),\n })\n }\n return (\n <span aria-hidden='true' className='contents'>\n {icon}\n </span>\n )\n}\n\n/**\n * Presentational CTA renderer (REB-394, moved to `@reblu/site-next` in REB-418).\n * Pure — takes already-resolved CTAs and renders one styled button-link each.\n * Internal hrefs use `next/link` (client routing); external URLs open in a new,\n * safely-rel tab and carry a visually-hidden new-window hint. `analyticsId` is\n * surfaced as `data-cta-id` on the clickable element for GA `cta_click`\n * tracking. Renders nothing when there are no CTAs.\n *\n * The first CTA is the primary one; the rest are secondary. Each role takes its\n * own Button variant and extra class, so a SITE maps the CTAs to its own button\n * styles without forking this markup.\n *\n * Imports `Button` from `@reblu/site-ui` (public) — never `@reblu/ui` (private).\n */\nexport function CtaButtons({\n ctas,\n className,\n buttonClassName,\n size = 'lg',\n icon,\n primaryVariant = 'default',\n secondaryVariant = 'default',\n primaryClassName,\n secondaryClassName,\n newWindowHint = DEFAULT_NEW_WINDOW_HINT,\n}: CtaButtonsProps) {\n if (ctas.length === 0) return null\n\n const leadingIcon = renderLeadingIcon(icon)\n\n return (\n <div className={className} data-slot='cta-buttons'>\n {ctas.map((cta, index) => {\n const isPrimary = index === 0\n return (\n <Button\n key={cta.id}\n asChild\n size={size}\n variant={isPrimary ? primaryVariant : secondaryVariant}\n className={cn(buttonClassName, isPrimary ? primaryClassName : secondaryClassName)}\n data-cta-id={cta.analyticsId ?? undefined}\n >\n {isExternal(cta.href) ? (\n <a href={cta.href} target='_blank' rel='noopener noreferrer'>\n {leadingIcon}\n {cta.label}\n {newWindowHint ? (\n <>\n {' '}\n <span className='sr-only'>{newWindowHint}</span>\n </>\n ) : null}\n </a>\n ) : (\n <Link href={cta.href}>\n {leadingIcon}\n {cta.label}\n </Link>\n )}\n </Button>\n )\n })}\n </div>\n )\n}\n"],"mappings":";;;AAGA,SAAS,QAAQ,UAAmC;AACpD,OAAO,UAAU;AACjB,SAAS,cAAc,UAAU,sBAAyD;AAoEtF,SAsDc,YAAAA,WAtDd,KAsDc,YAtDd;AA5DJ,IAAM,0BAA0B;AAqChC,SAAS,WAAW,MAAuB;AACzC,SAAO,gBAAgB,KAAK,IAAI;AAClC;AAQA,SAAS,kBAAkB,MAA4B;AACrD,MAAI,QAAQ,QAAQ,SAAS,MAAO,QAAO;AAE3C,MAAI,eAAe,IAAI,KAAK,KAAK,SAAS,UAAU;AAClD,UAAM,UAAU;AAGhB,WAAO,aAAa,SAAS;AAAA,MAC3B,eAAe;AAAA,MACf,WAAW,GAAG,QAAQ,MAAM,WAAW,UAAU;AAAA,IACnD,CAAC;AAAA,EACH;AACA,SACE,oBAAC,UAAK,eAAY,QAAO,WAAU,YAChC,gBACH;AAEJ;AAgBO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB;AAAA,EACA;AAAA,EACA,gBAAgB;AAClB,GAAoB;AAClB,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,QAAM,cAAc,kBAAkB,IAAI;AAE1C,SACE,oBAAC,SAAI,WAAsB,aAAU,eAClC,eAAK,IAAI,CAAC,KAAK,UAAU;AACxB,UAAM,YAAY,UAAU;AAC5B,WACE;AAAA,MAAC;AAAA;AAAA,QAEC,SAAO;AAAA,QACP;AAAA,QACA,SAAS,YAAY,iBAAiB;AAAA,QACtC,WAAW,GAAG,iBAAiB,YAAY,mBAAmB,kBAAkB;AAAA,QAChF,eAAa,IAAI,eAAe;AAAA,QAE/B,qBAAW,IAAI,IAAI,IAClB,qBAAC,OAAE,MAAM,IAAI,MAAM,QAAO,UAAS,KAAI,uBACpC;AAAA;AAAA,UACA,IAAI;AAAA,UACJ,gBACC,qBAAAA,WAAA,EACG;AAAA;AAAA,YACD,oBAAC,UAAK,WAAU,WAAW,yBAAc;AAAA,aAC3C,IACE;AAAA,WACN,IAEA,qBAAC,QAAK,MAAM,IAAI,MACb;AAAA;AAAA,UACA,IAAI;AAAA,WACP;AAAA;AAAA,MAtBG,IAAI;AAAA,IAwBX;AAAA,EAEJ,CAAC,GACH;AAEJ;","names":["Fragment"]}
|
package/dist/index.cjs
CHANGED
|
@@ -60,11 +60,23 @@ var NextCacheAdapter = class {
|
|
|
60
60
|
// src/cta-buttons.tsx
|
|
61
61
|
var import_site_ui = require("@reblu/site-ui");
|
|
62
62
|
var import_link = __toESM(require("next/link"), 1);
|
|
63
|
+
var import_react = require("react");
|
|
63
64
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
64
65
|
var DEFAULT_NEW_WINDOW_HINT = "(se abre en una nueva ventana)";
|
|
65
66
|
function isExternal(href) {
|
|
66
67
|
return /^https?:\/\//i.test(href);
|
|
67
68
|
}
|
|
69
|
+
function renderLeadingIcon(icon) {
|
|
70
|
+
if (icon == null || icon === false) return null;
|
|
71
|
+
if ((0, import_react.isValidElement)(icon) && icon.type !== import_react.Fragment) {
|
|
72
|
+
const element = icon;
|
|
73
|
+
return (0, import_react.cloneElement)(element, {
|
|
74
|
+
"aria-hidden": true,
|
|
75
|
+
className: (0, import_site_ui.cn)(element.props.className, "shrink-0")
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", className: "contents", children: icon });
|
|
79
|
+
}
|
|
68
80
|
function CtaButtons({
|
|
69
81
|
ctas,
|
|
70
82
|
className,
|
|
@@ -78,7 +90,7 @@ function CtaButtons({
|
|
|
78
90
|
newWindowHint = DEFAULT_NEW_WINDOW_HINT
|
|
79
91
|
}) {
|
|
80
92
|
if (ctas.length === 0) return null;
|
|
81
|
-
const leadingIcon =
|
|
93
|
+
const leadingIcon = renderLeadingIcon(icon);
|
|
82
94
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className, "data-slot": "cta-buttons", children: ctas.map((cta, index) => {
|
|
83
95
|
const isPrimary = index === 0;
|
|
84
96
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/next-cache-adapter.ts","../src/cta-buttons.tsx","../src/sticky-mobile-cta.tsx","../src/editable-cta.tsx"],"sourcesContent":["/**\n * `@reblu/site-next` — Next.js RSC glue for Reblu SITEs (REB-418 / design doc §5).\n *\n * Two things live here, both framework-coupled to Next so the lower SDK layers\n * (`@reblu/site-client`, `@reblu/site-contracts`, `@reblu/site-ui`) stay\n * framework-agnostic:\n *\n * 1. {@link NextCacheAdapter} — implements `@reblu/site-client`'s\n * `RebluCacheAdapter`, translating cache policy into Next ISR `fetch` options.\n * 2. The editable-CTA glue (`EditableCta` server component + its `CtaButtons`\n * client boundary + `StickyMobileCta`), consuming the injected SDK client.\n *\n * `EditableCta` re-exports the `'use client'` `CtaButtons` from its own emitted\n * chunk, so the RSC boundary survives the build (see the R4 smoke test).\n */\nexport { NextCacheAdapter } from './next-cache-adapter'\nexport { CtaButtons, type CtaButtonsProps, type CtaButtonVariant } from './cta-buttons'\nexport { StickyMobileCta, type StickyMobileCtaProps } from './sticky-mobile-cta'\nexport { EditableCta, type EditableCtaProps, type CtaClient, type CtaSlot } from './editable-cta'\n","import type { RebluCacheAdapter, RebluCachePolicy } from '@reblu/site-client'\n\n/**\n * Next.js ISR cache adapter (REB-418 / design doc §5.2).\n *\n * Implements the framework-agnostic {@link RebluCacheAdapter} interface exported\n * by `@reblu/site-client`, translating a {@link RebluCachePolicy} into the\n * `fetch` options Next understands. This is the piece that restores the exact\n * ISR behavior demo-site hard-codes today — now injected instead of baked in:\n *\n * ```ts\n * createRebluClient({ baseUrl, apiKey, cache: new NextCacheAdapter(60) })\n * ```\n *\n * - `revalidate` > 0 → `{ next: { revalidate } }` (time-based ISR)\n * - `revalidate` `false` / `0` → `{ cache: 'no-store' }` (dynamic / preview)\n * - omitted `revalidate` → the constructor `defaultRevalidate`\n */\nexport class NextCacheAdapter implements RebluCacheAdapter {\n /**\n * @param defaultRevalidate ISR window (seconds) applied when a request omits\n * its own `revalidate`. Defaults to `60` for parity with demo-site's current\n * ISR default (`client.ts:45`). Pass `false`/`0` to default to no-store.\n */\n constructor(private readonly defaultRevalidate: number | false = 60) {}\n\n resolveRequestInit(policy: RebluCachePolicy): RequestInit {\n const revalidate = policy.revalidate ?? this.defaultRevalidate\n if (revalidate === false || revalidate === 0) {\n return { cache: 'no-store' }\n }\n return { next: { revalidate } } as RequestInit\n }\n}\n","'use client'\n\nimport type { Cta } from '@reblu/site-contracts'\nimport { Button, cn, type ButtonVariantProps } from '@reblu/site-ui'\nimport Link from 'next/link'\nimport type { ReactNode } from 'react'\n\ntype ButtonSize = 'sm' | 'default' | 'lg'\n\n/** A `@reblu/site-ui` Button variant (`default`, `outline`, `secondary`, …). */\nexport type CtaButtonVariant = NonNullable<ButtonVariantProps['variant']>\n\n/** Default screen-reader hint appended to CTAs that open in a new tab. */\nconst DEFAULT_NEW_WINDOW_HINT = '(se abre en una nueva ventana)'\n\nexport interface CtaButtonsProps {\n ctas: Cta[]\n /** Wrapper class (layout of the button group). */\n className?: string\n /** Class applied to each button. */\n buttonClassName?: string\n size?: ButtonSize\n /**\n * Leading icon rendered before every CTA label. Decorative: it is wrapped in\n * an `aria-hidden` span, so the accessible name stays the label.\n */\n icon?: ReactNode\n /** Button variant of the primary CTA (the first one, by `sortOrder`). Default `default`. */\n primaryVariant?: CtaButtonVariant\n /** Button variant of every secondary CTA (all but the first). Default `default`. */\n secondaryVariant?: CtaButtonVariant\n /**\n * Extra class for the primary CTA, merged after `buttonClassName`. Lets a SITE\n * map the primary CTA to its own button style (e.g. a brand `cta` variant).\n */\n primaryClassName?: string\n /** Extra class for every secondary CTA, merged after `buttonClassName`. */\n secondaryClassName?: string\n /**\n * Visually-hidden text appended to CTAs that open in a new tab, so screen\n * readers announce the context change. Default «(se abre en una nueva ventana)».\n */\n newWindowHint?: string\n}\n\n/** External = absolute http(s) URL (e.g. a third-party booking link). */\nfunction isExternal(href: string): boolean {\n return /^https?:\\/\\//i.test(href)\n}\n\n/**\n * Presentational CTA renderer (REB-394, moved to `@reblu/site-next` in REB-418).\n * Pure — takes already-resolved CTAs and renders one styled button-link each.\n * Internal hrefs use `next/link` (client routing); external URLs open in a new,\n * safely-rel tab and carry a visually-hidden new-window hint. `analyticsId` is\n * surfaced as `data-cta-id` on the clickable element for GA `cta_click`\n * tracking. Renders nothing when there are no CTAs.\n *\n * The first CTA is the primary one; the rest are secondary. Each role takes its\n * own Button variant and extra class, so a SITE maps the CTAs to its own button\n * styles without forking this markup.\n *\n * Imports `Button` from `@reblu/site-ui` (public) — never `@reblu/ui` (private).\n */\nexport function CtaButtons({\n ctas,\n className,\n buttonClassName,\n size = 'lg',\n icon,\n primaryVariant = 'default',\n secondaryVariant = 'default',\n primaryClassName,\n secondaryClassName,\n newWindowHint = DEFAULT_NEW_WINDOW_HINT,\n}: CtaButtonsProps) {\n if (ctas.length === 0) return null\n\n const leadingIcon =\n icon == null ? null : (\n <span aria-hidden='true' className='inline-flex shrink-0'>\n {icon}\n </span>\n )\n\n return (\n <div className={className} data-slot='cta-buttons'>\n {ctas.map((cta, index) => {\n const isPrimary = index === 0\n return (\n <Button\n key={cta.id}\n asChild\n size={size}\n variant={isPrimary ? primaryVariant : secondaryVariant}\n className={cn(buttonClassName, isPrimary ? primaryClassName : secondaryClassName)}\n data-cta-id={cta.analyticsId ?? undefined}\n >\n {isExternal(cta.href) ? (\n <a href={cta.href} target='_blank' rel='noopener noreferrer'>\n {leadingIcon}\n {cta.label}\n {newWindowHint ? <> <span className='sr-only'>{newWindowHint}</span></> : null}\n </a>\n ) : (\n <Link href={cta.href}>\n {leadingIcon}\n {cta.label}\n </Link>\n )}\n </Button>\n )\n })}\n </div>\n )\n}\n","import type { Cta } from '@reblu/site-contracts'\n\nimport { CtaButtons } from './cta-buttons'\n\nexport interface StickyMobileCtaProps {\n ctas: Cta[]\n}\n\n/**\n * StickyMobileCta (REB-394, moved to `@reblu/site-next` in REB-418) — a\n * mobile-only sticky bottom bar surfacing the `sticky-mobile` slot CTAs. Hidden\n * on `md+` (desktop has its own CTAs). Renders nothing when no CTA is configured,\n * so it never adds empty chrome.\n */\nexport function StickyMobileCta({ ctas }: StickyMobileCtaProps) {\n if (ctas.length === 0) return null\n\n return (\n <div className='fixed inset-x-0 bottom-0 z-50 md:hidden border-t border-border bg-background/95 px-4 py-3 backdrop-blur supports-[backdrop-filter]:bg-background/80'>\n <CtaButtons\n ctas={ctas}\n className='flex justify-center gap-3'\n buttonClassName='w-full rounded-lg font-semibold'\n size='lg'\n />\n </div>\n )\n}\n","import type { RebluClient } from '@reblu/site-client'\nimport type { Cta } from '@reblu/site-contracts'\nimport type { ReactNode } from 'react'\n\nimport { CtaButtons, type CtaButtonsProps } from './cta-buttons'\n\n/** The `Cta.slot` union — the valid placement keys, sourced from the contract. */\nexport type CtaSlot = Cta['slot']\n\n/**\n * The slice of the SDK client `EditableCta` needs: just the `ctas` namespace.\n * Typing it structurally lets a SITE pass either the full `RebluClient` or a\n * narrower singleton, without coupling this component to the whole surface.\n */\nexport type CtaClient = Pick<RebluClient, 'ctas'>\n\n/** Presentation props `EditableCta` forwards unchanged to {@link CtaButtons}. */\ntype ForwardedButtonProps = Omit<CtaButtonsProps, 'ctas'>\n\nexport interface EditableCtaProps extends ForwardedButtonProps {\n /** CtaSlot value: header | hero | blog-end | services | contact | sticky-mobile | footer. */\n slot: CtaSlot\n /** The Reblu SDK client instance the SITE owns (env-configured upstream). */\n client: CtaClient\n /**\n * Rendered when the slot has no configured CTA (or the upstream fetch fails).\n * Lets each placement keep its existing static CTA as a graceful fallback.\n */\n fallback?: ReactNode\n}\n\n/**\n * EditableCta (REB-394, moved to `@reblu/site-next` in REB-418) — drop-anywhere\n * async Server Component that renders the CTAs configured for a slot from nexus,\n * with no redeploy. Fetches active CTAs via the injected SDK client and degrades\n * gracefully: on empty/failure it renders `fallback` (a static CTA) instead of\n * breaking the page.\n *\n * The client is injected (prop or per-SITE singleton) so this package never\n * reads `process.env` and stays framework-glue only — the SITE owns the env and\n * wires the ISR-aware {@link NextCacheAdapter}.\n */\nexport async function EditableCta({ slot, client, fallback = null, ...buttonProps }: EditableCtaProps) {\n const { data: ctas } = await client.ctas\n .list(slot)\n .catch(() => ({ data: [] as Cta[] }))\n\n if (ctas.length === 0) return <>{fallback}</>\n\n return <CtaButtons ctas={ctas} {...buttonProps} />\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACkBO,IAAM,mBAAN,MAAoD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMzD,YAA6B,oBAAoC,IAAI;AAAxC;AAAA,EAAyC;AAAA,EAAzC;AAAA,EAE7B,mBAAmB,QAAuC;AACxD,UAAM,aAAa,OAAO,cAAc,KAAK;AAC7C,QAAI,eAAe,SAAS,eAAe,GAAG;AAC5C,aAAO,EAAE,OAAO,WAAW;AAAA,IAC7B;AACA,WAAO,EAAE,MAAM,EAAE,WAAW,EAAE;AAAA,EAChC;AACF;;;AC9BA,qBAAoD;AACpD,kBAAiB;AA4EX;AAnEN,IAAM,0BAA0B;AAiChC,SAAS,WAAW,MAAuB;AACzC,SAAO,gBAAgB,KAAK,IAAI;AAClC;AAgBO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB;AAAA,EACA;AAAA,EACA,gBAAgB;AAClB,GAAoB;AAClB,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,QAAM,cACJ,QAAQ,OAAO,OACb,4CAAC,UAAK,eAAY,QAAO,WAAU,wBAChC,gBACH;AAGJ,SACE,4CAAC,SAAI,WAAsB,aAAU,eAClC,eAAK,IAAI,CAAC,KAAK,UAAU;AACxB,UAAM,YAAY,UAAU;AAC5B,WACE;AAAA,MAAC;AAAA;AAAA,QAEC,SAAO;AAAA,QACP;AAAA,QACA,SAAS,YAAY,iBAAiB;AAAA,QACtC,eAAW,mBAAG,iBAAiB,YAAY,mBAAmB,kBAAkB;AAAA,QAChF,eAAa,IAAI,eAAe;AAAA,QAE/B,qBAAW,IAAI,IAAI,IAClB,6CAAC,OAAE,MAAM,IAAI,MAAM,QAAO,UAAS,KAAI,uBACpC;AAAA;AAAA,UACA,IAAI;AAAA,UACJ,gBAAgB,4EAAE;AAAA;AAAA,YAAC,4CAAC,UAAK,WAAU,WAAW,yBAAc;AAAA,aAAO,IAAM;AAAA,WAC5E,IAEA,6CAAC,YAAAA,SAAA,EAAK,MAAM,IAAI,MACb;AAAA;AAAA,UACA,IAAI;AAAA,WACP;AAAA;AAAA,MAjBG,IAAI;AAAA,IAmBX;AAAA,EAEJ,CAAC,GACH;AAEJ;;;AChGM,IAAAC,sBAAA;AALC,SAAS,gBAAgB,EAAE,KAAK,GAAyB;AAC9D,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,SACE,6CAAC,SAAI,WAAU,uJACb;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAU;AAAA,MACV,iBAAgB;AAAA,MAChB,MAAK;AAAA;AAAA,EACP,GACF;AAEJ;;;ACoBgC,IAAAC,sBAAA;AALhC,eAAsB,YAAY,EAAE,MAAM,QAAQ,WAAW,MAAM,GAAG,YAAY,GAAqB;AACrG,QAAM,EAAE,MAAM,KAAK,IAAI,MAAM,OAAO,KACjC,KAAK,IAAI,EACT,MAAM,OAAO,EAAE,MAAM,CAAC,EAAW,EAAE;AAEtC,MAAI,KAAK,WAAW,EAAG,QAAO,6EAAG,oBAAS;AAE1C,SAAO,6CAAC,cAAW,MAAa,GAAG,aAAa;AAClD;","names":["Link","import_jsx_runtime","import_jsx_runtime"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/next-cache-adapter.ts","../src/cta-buttons.tsx","../src/sticky-mobile-cta.tsx","../src/editable-cta.tsx"],"sourcesContent":["/**\n * `@reblu/site-next` — Next.js RSC glue for Reblu SITEs (REB-418 / design doc §5).\n *\n * Two things live here, both framework-coupled to Next so the lower SDK layers\n * (`@reblu/site-client`, `@reblu/site-contracts`, `@reblu/site-ui`) stay\n * framework-agnostic:\n *\n * 1. {@link NextCacheAdapter} — implements `@reblu/site-client`'s\n * `RebluCacheAdapter`, translating cache policy into Next ISR `fetch` options.\n * 2. The editable-CTA glue (`EditableCta` server component + its `CtaButtons`\n * client boundary + `StickyMobileCta`), consuming the injected SDK client.\n *\n * `EditableCta` re-exports the `'use client'` `CtaButtons` from its own emitted\n * chunk, so the RSC boundary survives the build (see the R4 smoke test).\n */\nexport { NextCacheAdapter } from './next-cache-adapter'\nexport { CtaButtons, type CtaButtonsProps, type CtaButtonVariant } from './cta-buttons'\nexport { StickyMobileCta, type StickyMobileCtaProps } from './sticky-mobile-cta'\nexport { EditableCta, type EditableCtaProps, type CtaClient, type CtaSlot } from './editable-cta'\n","import type { RebluCacheAdapter, RebluCachePolicy } from '@reblu/site-client'\n\n/**\n * Next.js ISR cache adapter (REB-418 / design doc §5.2).\n *\n * Implements the framework-agnostic {@link RebluCacheAdapter} interface exported\n * by `@reblu/site-client`, translating a {@link RebluCachePolicy} into the\n * `fetch` options Next understands. This is the piece that restores the exact\n * ISR behavior demo-site hard-codes today — now injected instead of baked in:\n *\n * ```ts\n * createRebluClient({ baseUrl, apiKey, cache: new NextCacheAdapter(60) })\n * ```\n *\n * - `revalidate` > 0 → `{ next: { revalidate } }` (time-based ISR)\n * - `revalidate` `false` / `0` → `{ cache: 'no-store' }` (dynamic / preview)\n * - omitted `revalidate` → the constructor `defaultRevalidate`\n */\nexport class NextCacheAdapter implements RebluCacheAdapter {\n /**\n * @param defaultRevalidate ISR window (seconds) applied when a request omits\n * its own `revalidate`. Defaults to `60` for parity with demo-site's current\n * ISR default (`client.ts:45`). Pass `false`/`0` to default to no-store.\n */\n constructor(private readonly defaultRevalidate: number | false = 60) {}\n\n resolveRequestInit(policy: RebluCachePolicy): RequestInit {\n const revalidate = policy.revalidate ?? this.defaultRevalidate\n if (revalidate === false || revalidate === 0) {\n return { cache: 'no-store' }\n }\n return { next: { revalidate } } as RequestInit\n }\n}\n","'use client'\n\nimport type { Cta } from '@reblu/site-contracts'\nimport { Button, cn, type ButtonVariantProps } from '@reblu/site-ui'\nimport Link from 'next/link'\nimport { cloneElement, Fragment, isValidElement, type ReactElement, type ReactNode } from 'react'\n\ntype ButtonSize = 'sm' | 'default' | 'lg'\n\n/** A `@reblu/site-ui` Button variant (`default`, `outline`, `secondary`, …). */\nexport type CtaButtonVariant = NonNullable<ButtonVariantProps['variant']>\n\n/** Default screen-reader hint appended to CTAs that open in a new tab. */\nconst DEFAULT_NEW_WINDOW_HINT = '(se abre en una nueva ventana)'\n\nexport interface CtaButtonsProps {\n ctas: Cta[]\n /** Wrapper class (layout of the button group). */\n className?: string\n /** Class applied to each button. */\n buttonClassName?: string\n size?: ButtonSize\n /**\n * Leading icon rendered before every CTA label. Decorative: it is marked\n * `aria-hidden`, so the accessible name stays the label. A custom icon\n * component must forward `aria-hidden` and `className` to its root element. An element icon (an\n * `<svg>` or an icon component) is rendered as a direct child of the button,\n * so shadcn's `has-[>svg]` icon padding still matches; anything else is\n * wrapped in an `aria-hidden` `display: contents` span.\n */\n icon?: ReactNode\n /** Button variant of the primary CTA (the first one, by `sortOrder`). Default `default`. */\n primaryVariant?: CtaButtonVariant\n /** Button variant of every secondary CTA (all but the first). Default `default`. */\n secondaryVariant?: CtaButtonVariant\n /**\n * Extra class for the primary CTA, merged after `buttonClassName`. Lets a SITE\n * map the primary CTA to its own button style (e.g. a brand `cta` variant).\n */\n primaryClassName?: string\n /** Extra class for every secondary CTA, merged after `buttonClassName`. */\n secondaryClassName?: string\n /**\n * Visually-hidden text appended to CTAs that open in a new tab, so screen\n * readers announce the context change. Default «(se abre en una nueva ventana)».\n */\n newWindowHint?: string\n}\n\n/** External = absolute http(s) URL (e.g. a third-party booking link). */\nfunction isExternal(href: string): boolean {\n return /^https?:\\/\\//i.test(href)\n}\n\n/**\n * REB-930 — no wrapper around an element icon: `has-[>svg]:px-*` only matches an\n * svg that is a DIRECT child of the button. `display: contents` on the fallback\n * wrapper keeps it out of the flex layout, though a wrapped svg is still not a\n * direct child.\n */\nfunction renderLeadingIcon(icon: ReactNode): ReactNode {\n if (icon == null || icon === false) return null\n // A fragment cannot carry aria-hidden, so it takes the wrapper like a non-element.\n if (isValidElement(icon) && icon.type !== Fragment) {\n const element = icon as ReactElement<{ 'aria-hidden'?: boolean | 'true'; className?: string }>\n // shrink-0: the old wrapper kept any icon from shrinking in a narrow button;\n // shadcn's `[&_svg]:shrink-0` only covers svg.\n return cloneElement(element, {\n 'aria-hidden': true,\n className: cn(element.props.className, 'shrink-0'),\n })\n }\n return (\n <span aria-hidden='true' className='contents'>\n {icon}\n </span>\n )\n}\n\n/**\n * Presentational CTA renderer (REB-394, moved to `@reblu/site-next` in REB-418).\n * Pure — takes already-resolved CTAs and renders one styled button-link each.\n * Internal hrefs use `next/link` (client routing); external URLs open in a new,\n * safely-rel tab and carry a visually-hidden new-window hint. `analyticsId` is\n * surfaced as `data-cta-id` on the clickable element for GA `cta_click`\n * tracking. Renders nothing when there are no CTAs.\n *\n * The first CTA is the primary one; the rest are secondary. Each role takes its\n * own Button variant and extra class, so a SITE maps the CTAs to its own button\n * styles without forking this markup.\n *\n * Imports `Button` from `@reblu/site-ui` (public) — never `@reblu/ui` (private).\n */\nexport function CtaButtons({\n ctas,\n className,\n buttonClassName,\n size = 'lg',\n icon,\n primaryVariant = 'default',\n secondaryVariant = 'default',\n primaryClassName,\n secondaryClassName,\n newWindowHint = DEFAULT_NEW_WINDOW_HINT,\n}: CtaButtonsProps) {\n if (ctas.length === 0) return null\n\n const leadingIcon = renderLeadingIcon(icon)\n\n return (\n <div className={className} data-slot='cta-buttons'>\n {ctas.map((cta, index) => {\n const isPrimary = index === 0\n return (\n <Button\n key={cta.id}\n asChild\n size={size}\n variant={isPrimary ? primaryVariant : secondaryVariant}\n className={cn(buttonClassName, isPrimary ? primaryClassName : secondaryClassName)}\n data-cta-id={cta.analyticsId ?? undefined}\n >\n {isExternal(cta.href) ? (\n <a href={cta.href} target='_blank' rel='noopener noreferrer'>\n {leadingIcon}\n {cta.label}\n {newWindowHint ? (\n <>\n {' '}\n <span className='sr-only'>{newWindowHint}</span>\n </>\n ) : null}\n </a>\n ) : (\n <Link href={cta.href}>\n {leadingIcon}\n {cta.label}\n </Link>\n )}\n </Button>\n )\n })}\n </div>\n )\n}\n","import type { Cta } from '@reblu/site-contracts'\n\nimport { CtaButtons } from './cta-buttons'\n\nexport interface StickyMobileCtaProps {\n ctas: Cta[]\n}\n\n/**\n * StickyMobileCta (REB-394, moved to `@reblu/site-next` in REB-418) — a\n * mobile-only sticky bottom bar surfacing the `sticky-mobile` slot CTAs. Hidden\n * on `md+` (desktop has its own CTAs). Renders nothing when no CTA is configured,\n * so it never adds empty chrome.\n */\nexport function StickyMobileCta({ ctas }: StickyMobileCtaProps) {\n if (ctas.length === 0) return null\n\n return (\n <div className='fixed inset-x-0 bottom-0 z-50 md:hidden border-t border-border bg-background/95 px-4 py-3 backdrop-blur supports-[backdrop-filter]:bg-background/80'>\n <CtaButtons\n ctas={ctas}\n className='flex justify-center gap-3'\n buttonClassName='w-full rounded-lg font-semibold'\n size='lg'\n />\n </div>\n )\n}\n","import type { RebluClient } from '@reblu/site-client'\nimport type { Cta } from '@reblu/site-contracts'\nimport type { ReactNode } from 'react'\n\nimport { CtaButtons, type CtaButtonsProps } from './cta-buttons'\n\n/** The `Cta.slot` union — the valid placement keys, sourced from the contract. */\nexport type CtaSlot = Cta['slot']\n\n/**\n * The slice of the SDK client `EditableCta` needs: just the `ctas` namespace.\n * Typing it structurally lets a SITE pass either the full `RebluClient` or a\n * narrower singleton, without coupling this component to the whole surface.\n */\nexport type CtaClient = Pick<RebluClient, 'ctas'>\n\n/** Presentation props `EditableCta` forwards unchanged to {@link CtaButtons}. */\ntype ForwardedButtonProps = Omit<CtaButtonsProps, 'ctas'>\n\nexport interface EditableCtaProps extends ForwardedButtonProps {\n /** CtaSlot value: header | hero | blog-end | services | contact | sticky-mobile | footer. */\n slot: CtaSlot\n /** The Reblu SDK client instance the SITE owns (env-configured upstream). */\n client: CtaClient\n /**\n * Rendered when the slot has no configured CTA (or the upstream fetch fails).\n * Lets each placement keep its existing static CTA as a graceful fallback.\n */\n fallback?: ReactNode\n}\n\n/**\n * EditableCta (REB-394, moved to `@reblu/site-next` in REB-418) — drop-anywhere\n * async Server Component that renders the CTAs configured for a slot from nexus,\n * with no redeploy. Fetches active CTAs via the injected SDK client and degrades\n * gracefully: on empty/failure it renders `fallback` (a static CTA) instead of\n * breaking the page.\n *\n * The client is injected (prop or per-SITE singleton) so this package never\n * reads `process.env` and stays framework-glue only — the SITE owns the env and\n * wires the ISR-aware {@link NextCacheAdapter}.\n */\nexport async function EditableCta({ slot, client, fallback = null, ...buttonProps }: EditableCtaProps) {\n const { data: ctas } = await client.ctas\n .list(slot)\n .catch(() => ({ data: [] as Cta[] }))\n\n if (ctas.length === 0) return <>{fallback}</>\n\n return <CtaButtons ctas={ctas} {...buttonProps} />\n}\n"],"mappings":"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","names":["Link","import_jsx_runtime","import_jsx_runtime"]}
|
package/dist/index.js
CHANGED
|
@@ -20,12 +20,24 @@ var NextCacheAdapter = class {
|
|
|
20
20
|
|
|
21
21
|
// src/cta-buttons.tsx
|
|
22
22
|
import { Button, cn } from "@reblu/site-ui";
|
|
23
|
-
import Link from "next/link";
|
|
24
|
-
import {
|
|
23
|
+
import Link from "next/link.js";
|
|
24
|
+
import { cloneElement, Fragment, isValidElement } from "react";
|
|
25
|
+
import { Fragment as Fragment2, jsx, jsxs } from "react/jsx-runtime";
|
|
25
26
|
var DEFAULT_NEW_WINDOW_HINT = "(se abre en una nueva ventana)";
|
|
26
27
|
function isExternal(href) {
|
|
27
28
|
return /^https?:\/\//i.test(href);
|
|
28
29
|
}
|
|
30
|
+
function renderLeadingIcon(icon) {
|
|
31
|
+
if (icon == null || icon === false) return null;
|
|
32
|
+
if (isValidElement(icon) && icon.type !== Fragment) {
|
|
33
|
+
const element = icon;
|
|
34
|
+
return cloneElement(element, {
|
|
35
|
+
"aria-hidden": true,
|
|
36
|
+
className: cn(element.props.className, "shrink-0")
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "contents", children: icon });
|
|
40
|
+
}
|
|
29
41
|
function CtaButtons({
|
|
30
42
|
ctas,
|
|
31
43
|
className,
|
|
@@ -39,7 +51,7 @@ function CtaButtons({
|
|
|
39
51
|
newWindowHint = DEFAULT_NEW_WINDOW_HINT
|
|
40
52
|
}) {
|
|
41
53
|
if (ctas.length === 0) return null;
|
|
42
|
-
const leadingIcon =
|
|
54
|
+
const leadingIcon = renderLeadingIcon(icon);
|
|
43
55
|
return /* @__PURE__ */ jsx("div", { className, "data-slot": "cta-buttons", children: ctas.map((cta, index) => {
|
|
44
56
|
const isPrimary = index === 0;
|
|
45
57
|
return /* @__PURE__ */ jsx(
|
|
@@ -53,7 +65,7 @@ function CtaButtons({
|
|
|
53
65
|
children: isExternal(cta.href) ? /* @__PURE__ */ jsxs("a", { href: cta.href, target: "_blank", rel: "noopener noreferrer", children: [
|
|
54
66
|
leadingIcon,
|
|
55
67
|
cta.label,
|
|
56
|
-
newWindowHint ? /* @__PURE__ */ jsxs(
|
|
68
|
+
newWindowHint ? /* @__PURE__ */ jsxs(Fragment2, { children: [
|
|
57
69
|
" ",
|
|
58
70
|
/* @__PURE__ */ jsx("span", { className: "sr-only", children: newWindowHint })
|
|
59
71
|
] }) : null
|
|
@@ -83,10 +95,10 @@ function StickyMobileCta({ ctas }) {
|
|
|
83
95
|
}
|
|
84
96
|
|
|
85
97
|
// src/editable-cta.tsx
|
|
86
|
-
import { Fragment as
|
|
98
|
+
import { Fragment as Fragment3, jsx as jsx3 } from "react/jsx-runtime";
|
|
87
99
|
async function EditableCta({ slot, client, fallback = null, ...buttonProps }) {
|
|
88
100
|
const { data: ctas } = await client.ctas.list(slot).catch(() => ({ data: [] }));
|
|
89
|
-
if (ctas.length === 0) return /* @__PURE__ */ jsx3(
|
|
101
|
+
if (ctas.length === 0) return /* @__PURE__ */ jsx3(Fragment3, { children: fallback });
|
|
90
102
|
return /* @__PURE__ */ jsx3(CtaButtons, { ctas, ...buttonProps });
|
|
91
103
|
}
|
|
92
104
|
export {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/next-cache-adapter.ts","../src/cta-buttons.tsx","../src/sticky-mobile-cta.tsx","../src/editable-cta.tsx"],"sourcesContent":["import type { RebluCacheAdapter, RebluCachePolicy } from '@reblu/site-client'\n\n/**\n * Next.js ISR cache adapter (REB-418 / design doc §5.2).\n *\n * Implements the framework-agnostic {@link RebluCacheAdapter} interface exported\n * by `@reblu/site-client`, translating a {@link RebluCachePolicy} into the\n * `fetch` options Next understands. This is the piece that restores the exact\n * ISR behavior demo-site hard-codes today — now injected instead of baked in:\n *\n * ```ts\n * createRebluClient({ baseUrl, apiKey, cache: new NextCacheAdapter(60) })\n * ```\n *\n * - `revalidate` > 0 → `{ next: { revalidate } }` (time-based ISR)\n * - `revalidate` `false` / `0` → `{ cache: 'no-store' }` (dynamic / preview)\n * - omitted `revalidate` → the constructor `defaultRevalidate`\n */\nexport class NextCacheAdapter implements RebluCacheAdapter {\n /**\n * @param defaultRevalidate ISR window (seconds) applied when a request omits\n * its own `revalidate`. Defaults to `60` for parity with demo-site's current\n * ISR default (`client.ts:45`). Pass `false`/`0` to default to no-store.\n */\n constructor(private readonly defaultRevalidate: number | false = 60) {}\n\n resolveRequestInit(policy: RebluCachePolicy): RequestInit {\n const revalidate = policy.revalidate ?? this.defaultRevalidate\n if (revalidate === false || revalidate === 0) {\n return { cache: 'no-store' }\n }\n return { next: { revalidate } } as RequestInit\n }\n}\n","'use client'\n\nimport type { Cta } from '@reblu/site-contracts'\nimport { Button, cn, type ButtonVariantProps } from '@reblu/site-ui'\nimport Link from 'next/link'\nimport type { ReactNode } from 'react'\n\ntype ButtonSize = 'sm' | 'default' | 'lg'\n\n/** A `@reblu/site-ui` Button variant (`default`, `outline`, `secondary`, …). */\nexport type CtaButtonVariant = NonNullable<ButtonVariantProps['variant']>\n\n/** Default screen-reader hint appended to CTAs that open in a new tab. */\nconst DEFAULT_NEW_WINDOW_HINT = '(se abre en una nueva ventana)'\n\nexport interface CtaButtonsProps {\n ctas: Cta[]\n /** Wrapper class (layout of the button group). */\n className?: string\n /** Class applied to each button. */\n buttonClassName?: string\n size?: ButtonSize\n /**\n * Leading icon rendered before every CTA label. Decorative: it is wrapped in\n * an `aria-hidden` span, so the accessible name stays the label.\n */\n icon?: ReactNode\n /** Button variant of the primary CTA (the first one, by `sortOrder`). Default `default`. */\n primaryVariant?: CtaButtonVariant\n /** Button variant of every secondary CTA (all but the first). Default `default`. */\n secondaryVariant?: CtaButtonVariant\n /**\n * Extra class for the primary CTA, merged after `buttonClassName`. Lets a SITE\n * map the primary CTA to its own button style (e.g. a brand `cta` variant).\n */\n primaryClassName?: string\n /** Extra class for every secondary CTA, merged after `buttonClassName`. */\n secondaryClassName?: string\n /**\n * Visually-hidden text appended to CTAs that open in a new tab, so screen\n * readers announce the context change. Default «(se abre en una nueva ventana)».\n */\n newWindowHint?: string\n}\n\n/** External = absolute http(s) URL (e.g. a third-party booking link). */\nfunction isExternal(href: string): boolean {\n return /^https?:\\/\\//i.test(href)\n}\n\n/**\n * Presentational CTA renderer (REB-394, moved to `@reblu/site-next` in REB-418).\n * Pure — takes already-resolved CTAs and renders one styled button-link each.\n * Internal hrefs use `next/link` (client routing); external URLs open in a new,\n * safely-rel tab and carry a visually-hidden new-window hint. `analyticsId` is\n * surfaced as `data-cta-id` on the clickable element for GA `cta_click`\n * tracking. Renders nothing when there are no CTAs.\n *\n * The first CTA is the primary one; the rest are secondary. Each role takes its\n * own Button variant and extra class, so a SITE maps the CTAs to its own button\n * styles without forking this markup.\n *\n * Imports `Button` from `@reblu/site-ui` (public) — never `@reblu/ui` (private).\n */\nexport function CtaButtons({\n ctas,\n className,\n buttonClassName,\n size = 'lg',\n icon,\n primaryVariant = 'default',\n secondaryVariant = 'default',\n primaryClassName,\n secondaryClassName,\n newWindowHint = DEFAULT_NEW_WINDOW_HINT,\n}: CtaButtonsProps) {\n if (ctas.length === 0) return null\n\n const leadingIcon =\n icon == null ? null : (\n <span aria-hidden='true' className='inline-flex shrink-0'>\n {icon}\n </span>\n )\n\n return (\n <div className={className} data-slot='cta-buttons'>\n {ctas.map((cta, index) => {\n const isPrimary = index === 0\n return (\n <Button\n key={cta.id}\n asChild\n size={size}\n variant={isPrimary ? primaryVariant : secondaryVariant}\n className={cn(buttonClassName, isPrimary ? primaryClassName : secondaryClassName)}\n data-cta-id={cta.analyticsId ?? undefined}\n >\n {isExternal(cta.href) ? (\n <a href={cta.href} target='_blank' rel='noopener noreferrer'>\n {leadingIcon}\n {cta.label}\n {newWindowHint ? <> <span className='sr-only'>{newWindowHint}</span></> : null}\n </a>\n ) : (\n <Link href={cta.href}>\n {leadingIcon}\n {cta.label}\n </Link>\n )}\n </Button>\n )\n })}\n </div>\n )\n}\n","import type { Cta } from '@reblu/site-contracts'\n\nimport { CtaButtons } from './cta-buttons'\n\nexport interface StickyMobileCtaProps {\n ctas: Cta[]\n}\n\n/**\n * StickyMobileCta (REB-394, moved to `@reblu/site-next` in REB-418) — a\n * mobile-only sticky bottom bar surfacing the `sticky-mobile` slot CTAs. Hidden\n * on `md+` (desktop has its own CTAs). Renders nothing when no CTA is configured,\n * so it never adds empty chrome.\n */\nexport function StickyMobileCta({ ctas }: StickyMobileCtaProps) {\n if (ctas.length === 0) return null\n\n return (\n <div className='fixed inset-x-0 bottom-0 z-50 md:hidden border-t border-border bg-background/95 px-4 py-3 backdrop-blur supports-[backdrop-filter]:bg-background/80'>\n <CtaButtons\n ctas={ctas}\n className='flex justify-center gap-3'\n buttonClassName='w-full rounded-lg font-semibold'\n size='lg'\n />\n </div>\n )\n}\n","import type { RebluClient } from '@reblu/site-client'\nimport type { Cta } from '@reblu/site-contracts'\nimport type { ReactNode } from 'react'\n\nimport { CtaButtons, type CtaButtonsProps } from './cta-buttons'\n\n/** The `Cta.slot` union — the valid placement keys, sourced from the contract. */\nexport type CtaSlot = Cta['slot']\n\n/**\n * The slice of the SDK client `EditableCta` needs: just the `ctas` namespace.\n * Typing it structurally lets a SITE pass either the full `RebluClient` or a\n * narrower singleton, without coupling this component to the whole surface.\n */\nexport type CtaClient = Pick<RebluClient, 'ctas'>\n\n/** Presentation props `EditableCta` forwards unchanged to {@link CtaButtons}. */\ntype ForwardedButtonProps = Omit<CtaButtonsProps, 'ctas'>\n\nexport interface EditableCtaProps extends ForwardedButtonProps {\n /** CtaSlot value: header | hero | blog-end | services | contact | sticky-mobile | footer. */\n slot: CtaSlot\n /** The Reblu SDK client instance the SITE owns (env-configured upstream). */\n client: CtaClient\n /**\n * Rendered when the slot has no configured CTA (or the upstream fetch fails).\n * Lets each placement keep its existing static CTA as a graceful fallback.\n */\n fallback?: ReactNode\n}\n\n/**\n * EditableCta (REB-394, moved to `@reblu/site-next` in REB-418) — drop-anywhere\n * async Server Component that renders the CTAs configured for a slot from nexus,\n * with no redeploy. Fetches active CTAs via the injected SDK client and degrades\n * gracefully: on empty/failure it renders `fallback` (a static CTA) instead of\n * breaking the page.\n *\n * The client is injected (prop or per-SITE singleton) so this package never\n * reads `process.env` and stays framework-glue only — the SITE owns the env and\n * wires the ISR-aware {@link NextCacheAdapter}.\n */\nexport async function EditableCta({ slot, client, fallback = null, ...buttonProps }: EditableCtaProps) {\n const { data: ctas } = await client.ctas\n .list(slot)\n .catch(() => ({ data: [] as Cta[] }))\n\n if (ctas.length === 0) return <>{fallback}</>\n\n return <CtaButtons ctas={ctas} {...buttonProps} />\n}\n"],"mappings":";AAkBO,IAAM,mBAAN,MAAoD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMzD,YAA6B,oBAAoC,IAAI;AAAxC;AAAA,EAAyC;AAAA,EAAzC;AAAA,EAE7B,mBAAmB,QAAuC;AACxD,UAAM,aAAa,OAAO,cAAc,KAAK;AAC7C,QAAI,eAAe,SAAS,eAAe,GAAG;AAC5C,aAAO,EAAE,OAAO,WAAW;AAAA,IAC7B;AACA,WAAO,EAAE,MAAM,EAAE,WAAW,EAAE;AAAA,EAChC;AACF;;;AC9BA,SAAS,QAAQ,UAAmC;AACpD,OAAO,UAAU;AA4EX,SAsB2B,UAtB3B,KAsB2B,YAtB3B;AAnEN,IAAM,0BAA0B;AAiChC,SAAS,WAAW,MAAuB;AACzC,SAAO,gBAAgB,KAAK,IAAI;AAClC;AAgBO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB;AAAA,EACA;AAAA,EACA,gBAAgB;AAClB,GAAoB;AAClB,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,QAAM,cACJ,QAAQ,OAAO,OACb,oBAAC,UAAK,eAAY,QAAO,WAAU,wBAChC,gBACH;AAGJ,SACE,oBAAC,SAAI,WAAsB,aAAU,eAClC,eAAK,IAAI,CAAC,KAAK,UAAU;AACxB,UAAM,YAAY,UAAU;AAC5B,WACE;AAAA,MAAC;AAAA;AAAA,QAEC,SAAO;AAAA,QACP;AAAA,QACA,SAAS,YAAY,iBAAiB;AAAA,QACtC,WAAW,GAAG,iBAAiB,YAAY,mBAAmB,kBAAkB;AAAA,QAChF,eAAa,IAAI,eAAe;AAAA,QAE/B,qBAAW,IAAI,IAAI,IAClB,qBAAC,OAAE,MAAM,IAAI,MAAM,QAAO,UAAS,KAAI,uBACpC;AAAA;AAAA,UACA,IAAI;AAAA,UACJ,gBAAgB,iCAAE;AAAA;AAAA,YAAC,oBAAC,UAAK,WAAU,WAAW,yBAAc;AAAA,aAAO,IAAM;AAAA,WAC5E,IAEA,qBAAC,QAAK,MAAM,IAAI,MACb;AAAA;AAAA,UACA,IAAI;AAAA,WACP;AAAA;AAAA,MAjBG,IAAI;AAAA,IAmBX;AAAA,EAEJ,CAAC,GACH;AAEJ;;;AChGM,gBAAAA,YAAA;AALC,SAAS,gBAAgB,EAAE,KAAK,GAAyB;AAC9D,MAAI,KAAK,WAAW,EAAG,QAAO;AAE9B,SACE,gBAAAA,KAAC,SAAI,WAAU,uJACb,0BAAAA;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAU;AAAA,MACV,iBAAgB;AAAA,MAChB,MAAK;AAAA;AAAA,EACP,GACF;AAEJ;;;ACoBgC,qBAAAC,WAAA,OAAAC,YAAA;AALhC,eAAsB,YAAY,EAAE,MAAM,QAAQ,WAAW,MAAM,GAAG,YAAY,GAAqB;AACrG,QAAM,EAAE,MAAM,KAAK,IAAI,MAAM,OAAO,KACjC,KAAK,IAAI,EACT,MAAM,OAAO,EAAE,MAAM,CAAC,EAAW,EAAE;AAEtC,MAAI,KAAK,WAAW,EAAG,QAAO,gBAAAA,KAAAD,WAAA,EAAG,oBAAS;AAE1C,SAAO,gBAAAC,KAAC,cAAW,MAAa,GAAG,aAAa;AAClD;","names":["jsx","Fragment","jsx"]}
|
|
1
|
+
{"version":3,"sources":["../src/next-cache-adapter.ts","../src/cta-buttons.tsx","../src/sticky-mobile-cta.tsx","../src/editable-cta.tsx"],"sourcesContent":["import type { RebluCacheAdapter, RebluCachePolicy } from '@reblu/site-client'\n\n/**\n * Next.js ISR cache adapter (REB-418 / design doc §5.2).\n *\n * Implements the framework-agnostic {@link RebluCacheAdapter} interface exported\n * by `@reblu/site-client`, translating a {@link RebluCachePolicy} into the\n * `fetch` options Next understands. This is the piece that restores the exact\n * ISR behavior demo-site hard-codes today — now injected instead of baked in:\n *\n * ```ts\n * createRebluClient({ baseUrl, apiKey, cache: new NextCacheAdapter(60) })\n * ```\n *\n * - `revalidate` > 0 → `{ next: { revalidate } }` (time-based ISR)\n * - `revalidate` `false` / `0` → `{ cache: 'no-store' }` (dynamic / preview)\n * - omitted `revalidate` → the constructor `defaultRevalidate`\n */\nexport class NextCacheAdapter implements RebluCacheAdapter {\n /**\n * @param defaultRevalidate ISR window (seconds) applied when a request omits\n * its own `revalidate`. Defaults to `60` for parity with demo-site's current\n * ISR default (`client.ts:45`). Pass `false`/`0` to default to no-store.\n */\n constructor(private readonly defaultRevalidate: number | false = 60) {}\n\n resolveRequestInit(policy: RebluCachePolicy): RequestInit {\n const revalidate = policy.revalidate ?? this.defaultRevalidate\n if (revalidate === false || revalidate === 0) {\n return { cache: 'no-store' }\n }\n return { next: { revalidate } } as RequestInit\n }\n}\n","'use client'\n\nimport type { Cta } from '@reblu/site-contracts'\nimport { Button, cn, type ButtonVariantProps } from '@reblu/site-ui'\nimport Link from 'next/link'\nimport { cloneElement, Fragment, isValidElement, type ReactElement, type ReactNode } from 'react'\n\ntype ButtonSize = 'sm' | 'default' | 'lg'\n\n/** A `@reblu/site-ui` Button variant (`default`, `outline`, `secondary`, …). */\nexport type CtaButtonVariant = NonNullable<ButtonVariantProps['variant']>\n\n/** Default screen-reader hint appended to CTAs that open in a new tab. */\nconst DEFAULT_NEW_WINDOW_HINT = '(se abre en una nueva ventana)'\n\nexport interface CtaButtonsProps {\n ctas: Cta[]\n /** Wrapper class (layout of the button group). */\n className?: string\n /** Class applied to each button. */\n buttonClassName?: string\n size?: ButtonSize\n /**\n * Leading icon rendered before every CTA label. Decorative: it is marked\n * `aria-hidden`, so the accessible name stays the label. A custom icon\n * component must forward `aria-hidden` and `className` to its root element. An element icon (an\n * `<svg>` or an icon component) is rendered as a direct child of the button,\n * so shadcn's `has-[>svg]` icon padding still matches; anything else is\n * wrapped in an `aria-hidden` `display: contents` span.\n */\n icon?: ReactNode\n /** Button variant of the primary CTA (the first one, by `sortOrder`). Default `default`. */\n primaryVariant?: CtaButtonVariant\n /** Button variant of every secondary CTA (all but the first). Default `default`. */\n secondaryVariant?: CtaButtonVariant\n /**\n * Extra class for the primary CTA, merged after `buttonClassName`. Lets a SITE\n * map the primary CTA to its own button style (e.g. a brand `cta` variant).\n */\n primaryClassName?: string\n /** Extra class for every secondary CTA, merged after `buttonClassName`. */\n secondaryClassName?: string\n /**\n * Visually-hidden text appended to CTAs that open in a new tab, so screen\n * readers announce the context change. Default «(se abre en una nueva ventana)».\n */\n newWindowHint?: string\n}\n\n/** External = absolute http(s) URL (e.g. a third-party booking link). */\nfunction isExternal(href: string): boolean {\n return /^https?:\\/\\//i.test(href)\n}\n\n/**\n * REB-930 — no wrapper around an element icon: `has-[>svg]:px-*` only matches an\n * svg that is a DIRECT child of the button. `display: contents` on the fallback\n * wrapper keeps it out of the flex layout, though a wrapped svg is still not a\n * direct child.\n */\nfunction renderLeadingIcon(icon: ReactNode): ReactNode {\n if (icon == null || icon === false) return null\n // A fragment cannot carry aria-hidden, so it takes the wrapper like a non-element.\n if (isValidElement(icon) && icon.type !== Fragment) {\n const element = icon as ReactElement<{ 'aria-hidden'?: boolean | 'true'; className?: string }>\n // shrink-0: the old wrapper kept any icon from shrinking in a narrow button;\n // shadcn's `[&_svg]:shrink-0` only covers svg.\n return cloneElement(element, {\n 'aria-hidden': true,\n className: cn(element.props.className, 'shrink-0'),\n })\n }\n return (\n <span aria-hidden='true' className='contents'>\n {icon}\n </span>\n )\n}\n\n/**\n * Presentational CTA renderer (REB-394, moved to `@reblu/site-next` in REB-418).\n * Pure — takes already-resolved CTAs and renders one styled button-link each.\n * Internal hrefs use `next/link` (client routing); external URLs open in a new,\n * safely-rel tab and carry a visually-hidden new-window hint. `analyticsId` is\n * surfaced as `data-cta-id` on the clickable element for GA `cta_click`\n * tracking. Renders nothing when there are no CTAs.\n *\n * The first CTA is the primary one; the rest are secondary. Each role takes its\n * own Button variant and extra class, so a SITE maps the CTAs to its own button\n * styles without forking this markup.\n *\n * Imports `Button` from `@reblu/site-ui` (public) — never `@reblu/ui` (private).\n */\nexport function CtaButtons({\n ctas,\n className,\n buttonClassName,\n size = 'lg',\n icon,\n primaryVariant = 'default',\n secondaryVariant = 'default',\n primaryClassName,\n secondaryClassName,\n newWindowHint = DEFAULT_NEW_WINDOW_HINT,\n}: CtaButtonsProps) {\n if (ctas.length === 0) return null\n\n const leadingIcon = renderLeadingIcon(icon)\n\n return (\n <div className={className} data-slot='cta-buttons'>\n {ctas.map((cta, index) => {\n const isPrimary = index === 0\n return (\n <Button\n key={cta.id}\n asChild\n size={size}\n variant={isPrimary ? primaryVariant : secondaryVariant}\n className={cn(buttonClassName, isPrimary ? primaryClassName : secondaryClassName)}\n data-cta-id={cta.analyticsId ?? undefined}\n >\n {isExternal(cta.href) ? (\n <a href={cta.href} target='_blank' rel='noopener noreferrer'>\n {leadingIcon}\n {cta.label}\n {newWindowHint ? (\n <>\n {' '}\n <span className='sr-only'>{newWindowHint}</span>\n </>\n ) : null}\n </a>\n ) : (\n <Link href={cta.href}>\n {leadingIcon}\n {cta.label}\n </Link>\n )}\n </Button>\n )\n })}\n </div>\n )\n}\n","import type { Cta } from '@reblu/site-contracts'\n\nimport { CtaButtons } from './cta-buttons'\n\nexport interface StickyMobileCtaProps {\n ctas: Cta[]\n}\n\n/**\n * StickyMobileCta (REB-394, moved to `@reblu/site-next` in REB-418) — a\n * mobile-only sticky bottom bar surfacing the `sticky-mobile` slot CTAs. Hidden\n * on `md+` (desktop has its own CTAs). Renders nothing when no CTA is configured,\n * so it never adds empty chrome.\n */\nexport function StickyMobileCta({ ctas }: StickyMobileCtaProps) {\n if (ctas.length === 0) return null\n\n return (\n <div className='fixed inset-x-0 bottom-0 z-50 md:hidden border-t border-border bg-background/95 px-4 py-3 backdrop-blur supports-[backdrop-filter]:bg-background/80'>\n <CtaButtons\n ctas={ctas}\n className='flex justify-center gap-3'\n buttonClassName='w-full rounded-lg font-semibold'\n size='lg'\n />\n </div>\n )\n}\n","import type { RebluClient } from '@reblu/site-client'\nimport type { Cta } from '@reblu/site-contracts'\nimport type { ReactNode } from 'react'\n\nimport { CtaButtons, type CtaButtonsProps } from './cta-buttons'\n\n/** The `Cta.slot` union — the valid placement keys, sourced from the contract. */\nexport type CtaSlot = Cta['slot']\n\n/**\n * The slice of the SDK client `EditableCta` needs: just the `ctas` namespace.\n * Typing it structurally lets a SITE pass either the full `RebluClient` or a\n * narrower singleton, without coupling this component to the whole surface.\n */\nexport type CtaClient = Pick<RebluClient, 'ctas'>\n\n/** Presentation props `EditableCta` forwards unchanged to {@link CtaButtons}. */\ntype ForwardedButtonProps = Omit<CtaButtonsProps, 'ctas'>\n\nexport interface EditableCtaProps extends ForwardedButtonProps {\n /** CtaSlot value: header | hero | blog-end | services | contact | sticky-mobile | footer. */\n slot: CtaSlot\n /** The Reblu SDK client instance the SITE owns (env-configured upstream). */\n client: CtaClient\n /**\n * Rendered when the slot has no configured CTA (or the upstream fetch fails).\n * Lets each placement keep its existing static CTA as a graceful fallback.\n */\n fallback?: ReactNode\n}\n\n/**\n * EditableCta (REB-394, moved to `@reblu/site-next` in REB-418) — drop-anywhere\n * async Server Component that renders the CTAs configured for a slot from nexus,\n * with no redeploy. Fetches active CTAs via the injected SDK client and degrades\n * gracefully: on empty/failure it renders `fallback` (a static CTA) instead of\n * breaking the page.\n *\n * The client is injected (prop or per-SITE singleton) so this package never\n * reads `process.env` and stays framework-glue only — the SITE owns the env and\n * wires the ISR-aware {@link NextCacheAdapter}.\n */\nexport async function EditableCta({ slot, client, fallback = null, ...buttonProps }: EditableCtaProps) {\n const { data: ctas } = await client.ctas\n .list(slot)\n .catch(() => ({ data: [] as Cta[] }))\n\n if (ctas.length === 0) return <>{fallback}</>\n\n return <CtaButtons ctas={ctas} {...buttonProps} />\n}\n"],"mappings":"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","names":["Fragment","jsx","Fragment","jsx"]}
|
package/dist/shop.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/shop/use-shop-filter-links.ts
|
|
4
|
-
import { usePathname, useSearchParams } from "next/navigation";
|
|
4
|
+
import { usePathname, useSearchParams } from "next/navigation.js";
|
|
5
5
|
|
|
6
6
|
// src/shop/params.ts
|
|
7
7
|
var MAX_Q_LEN = 200;
|
|
@@ -388,7 +388,7 @@ function useShopOrderStatus(sessionId, opts = {}) {
|
|
|
388
388
|
}
|
|
389
389
|
|
|
390
390
|
// src/shop/use-checkout-return.ts
|
|
391
|
-
import { useSearchParams as useSearchParams2 } from "next/navigation";
|
|
391
|
+
import { useSearchParams as useSearchParams2 } from "next/navigation.js";
|
|
392
392
|
function useCheckoutReturn() {
|
|
393
393
|
const searchParams = useSearchParams2();
|
|
394
394
|
const pago = searchParams.get("pago");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reblu/site-next",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0",
|
|
4
4
|
"description": "Next.js RSC glue for Reblu SITEs — cache + CTA + auth server/client components.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Reblu — Adolfo Unturbe",
|
|
@@ -250,9 +250,9 @@
|
|
|
250
250
|
"dependencies": {
|
|
251
251
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
252
252
|
"zustand": "^5.0.9",
|
|
253
|
-
"@reblu/site-
|
|
254
|
-
"@reblu/site-
|
|
255
|
-
"@reblu/site-
|
|
253
|
+
"@reblu/site-contracts": "^1.15.0",
|
|
254
|
+
"@reblu/site-ui": "^0.7.1",
|
|
255
|
+
"@reblu/site-client": "^5.7.0"
|
|
256
256
|
},
|
|
257
257
|
"devDependencies": {
|
|
258
258
|
"@testing-library/jest-dom": "^6.9.1",
|