@peax/consent-banner-react 0.1.0 → 0.3.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/README.md CHANGED
@@ -41,29 +41,32 @@ export function CookieBanner({ initialPreferences, onCommit }: Props) {
41
41
  }
42
42
 
43
43
  return (
44
- <>
45
- <button type="button" onClick={() => setOpen(true)}>Cookie choices</button>
46
- <Banner
47
- isOpen={open}
48
- cookiePolicyUrl="/cookies"
49
- preferencesContent={Object.fromEntries(
50
- Object.entries(preferences).map(([id, checked]) => [id, { checked }]),
51
- )}
52
- onMsg={msg => {
53
- switch (msg.type) {
54
- case 'on_accept_all':
55
- commit({ essential: true, analytics: true, marketing: true })
56
- break
57
- case 'on_reject_all':
58
- commit({ essential: true, analytics: false, marketing: false })
59
- break
60
- case 'on_apply_preferences':
61
- commit(msg.preferences)
62
- break
63
- }
64
- }}
65
- />
66
- </>
44
+ <Banner
45
+ isOpen={open}
46
+ cookiePolicyUrl="/cookies"
47
+ preferencesContent={Object.fromEntries(
48
+ Object.entries(preferences).map(([id, checked]) => [id, { checked }]),
49
+ )}
50
+ onMsg={msg => {
51
+ switch (msg.type) {
52
+ case 'on_accept_all':
53
+ commit({ essential: true, analytics: true, marketing: true })
54
+ break
55
+ case 'on_reject_all':
56
+ commit({ essential: true, analytics: false, marketing: false })
57
+ break
58
+ case 'on_apply_preferences':
59
+ commit(msg.preferences)
60
+ break
61
+ case 'on_open_preferences':
62
+ if (msg.source === 'floating-button') setOpen(true)
63
+ break
64
+ case 'on_close_preferences':
65
+ if (msg.source === 'floating-button') setOpen(false)
66
+ break
67
+ }
68
+ }}
69
+ />
67
70
  )
68
71
  }
69
72
  ```
@@ -73,13 +76,13 @@ export function CookieBanner({ initialPreferences, onCommit }: Props) {
73
76
  1. Restore saved consent before initializing this wrapper; keep server and client initial props consistent.
74
77
  2. Keep optional services blocked until the relevant consent is granted. Save and enforce decisions in `onCommit`, including later revocation.
75
78
  3. Adapt the Accept/Reject maps if you change the selected categories or their behavior.
76
- 4. Keep the banner mounted and change `isOpen` to play its exit animation. The “Cookie choices” button above reopens it.
79
+ 4. Keep the banner mounted and set `isOpen={false}` to dismiss it immediately. The floating cookie button requests a direct opening of preferences through `onMsg`.
77
80
 
78
81
  The banner manages UI and temporary drafts. It does not store consent, load trackers, or depend on `@peax/consent-core`.
79
82
 
80
83
  ## Props
81
84
 
82
- All `BannerProps` fields are optional. **`isOpen` defaults to `false`**, so pass it to display the banner.
85
+ All `BannerProps` fields are optional, but `floatingButtonPosition` requires `showFloatingButton: true`. **`isOpen` defaults to `false`**, so pass it to display the banner.
83
86
 
84
87
  ### Visibility and layout
85
88
 
@@ -87,7 +90,9 @@ All `BannerProps` fields are optional. **`isOpen` defaults to `false`**, so pass
87
90
  | --- | --- | --- |
88
91
  | `isOpen` | `false` | Show or hide the banner |
89
92
  | `variant` | `'compact'` | `'compact'` or `'stacked'` |
90
- | `showCustomizePreferences` | `true` | Show the preferences trigger, whether icon or text |
93
+ | `showCustomizePreferences` | `true` | Show the preferences trigger inside the collapsed banner |
94
+ | `showFloatingButton` | `true` | Show a floating cookie button while the banner is closed; set to `false` to hide it |
95
+ | `floatingButtonPosition` | Inherited | `'left'` or `'right'`; inherits `position`, with center/omitted falling back to right; requires `showFloatingButton: true` |
91
96
  | `hasAcceptRejectEqualWeight` | `false` | Give Accept and Reject the same secondary button appearance |
92
97
  | `position` | `'right'` | `'left'`, `'center'`, or `'right'` |
93
98
  | `slideFrom` | `'bottom'` | Bottom, or the matching side for left/right positioning |
@@ -122,11 +127,23 @@ All interactions use `onMsg`. The three decision events leave the banner open un
122
127
  | `on_reject_all` | None | Reject optional categories, save, enforce, and close |
123
128
  | `on_apply_preferences` | `preferences: Record<string, boolean>` | Save and enforce the supplied selection, then close |
124
129
  | `on_change_preference` | `sectionId`, `checked` | Observe a draft edit; do not treat it as applied consent |
125
- | `on_open_preferences` | None | Notification; the panel opens internally |
126
- | `on_close_preferences` | None | Notification; Close/Escape discards drafts and returns to the banner |
130
+ | `on_open_preferences` | `source: 'banner' \| 'floating-button'` | Banner navigation opens internally; floating requests need the application to set `isOpen={true}` |
131
+ | `on_close_preferences` | `source: 'banner' \| 'floating-button'` | Banner navigation returns to the collapsed banner immediately; floating sessions need the application to set `isOpen={false}` |
127
132
 
128
133
  Apply includes every selected section ID, including disabled sections. Closing preferences never grants consent. External closure through `isOpen={false}` resets the draft without emitting `on_close_preferences`.
129
134
 
135
+ ## Floating cookie button
136
+
137
+ The floating cookie button is enabled by default. Set `showFloatingButton={false}` to hide it. It appears when `isOpen` is false and the panel is removed, including on an initially closed banner. It remains hidden while an open banner is waiting for `appearanceDelay`. React does not infer consent status; your application can control whether the button is enabled.
138
+
139
+ `floatingButtonPosition` overrides the banner's placement. Without it, left stays left, right stays right, and center or omitted placement resolves to right. The TypeScript union permits an explicit floating position only with `showFloatingButton: true`.
140
+
141
+ Clicking emits `on_open_preferences` with `source: 'floating-button'`; set `isOpen` to true to open preferences directly beside the button. That session keeps the side captured on activation. Close/Escape emits the matching close request; set `isOpen` to false to discard drafts and return to the button. Until your application accepts either request, visibility stays unchanged. Apply retains its normal save-and-close integration. Disabling the floating button cancels a pending opening without closing an active panel.
142
+
143
+ `showCustomizePreferences` independently controls the collapsed banner's trigger. The floating control uses `customizePreferencesLabel` as its accessible name, with localized fallback for blank labels. Focus returns to the floating button when it was inside the dismissed panel; focus moved elsewhere is respected.
144
+
145
+ The control is 48px square by default and grows with control/icon tokens. It uses `--pxc-radius-xl`: zero makes a square, the standard value rounds the corners, and a full-radius value makes a circle. Existing theme colors, focus styles, spacing, safe-area insets, and `--pxc-z-index` apply.
146
+
130
147
  ## Preferences
131
148
 
132
149
  Select IDs, then override only the fields you need:
@@ -170,7 +187,7 @@ In compact mode, actions adapt to their actual label widths:
170
187
  | Only decisions fit together | Reject + Accept, then Customize preferences |
171
188
  | Decisions need separate rows | Accept → Reject → Customize preferences |
172
189
 
173
- Both variants support wrapping labels and scrolling on short viewports. Preferences opens the same nonmodal panel, with a scrolling body and fixed header/footer. Close/Escape restores focus to the visible trigger.
190
+ Both variants support wrapping labels and scrolling on short viewports. Preferences opens the same nonmodal panel, with a scrolling body and fixed header/footer. Close/Escape returns banner-opened preferences to the collapsed banner immediately and restores its trigger. Floating-opened preferences request external closure through `isOpen={false}`, restoring the floating trigger when enabled. Neither path has an exit animation; opening retains its existing motion.
174
191
 
175
192
  Keyboard order follows visual order where `reading-flow` is supported; otherwise compact controls retain Accept → Reject → Preferences DOM order. The preferences panel does not trap focus.
176
193
 
@@ -196,3 +213,5 @@ Keyboard order follows visual order where `reading-flow` is supported; otherwise
196
213
  Run `yarn workspace @peax/consent-banner-react dev` from the repository. The demo supports `?variant=stacked`, `?locale=de`, and `?showCustomizePreferences=false`. Theme edits update live from source.
197
214
 
198
215
  See [Banner verification](https://github.com/peaxco/consent/blob/main/docs/engineering/banner-verification.md) for browser setup and snapshot checks, and [Contributing](https://github.com/peaxco/consent/blob/main/CONTRIBUTING.md) for repository commands.
216
+
217
+ The adapter shares pure presentation contracts and UI state helpers with `@peax/consent-banner-shared`. Existing React exports and callback behavior remain unchanged. See [Vue integration](../../docs/vue.md) for the equivalent Vue package.
package/dist/index.d.ts CHANGED
@@ -1,55 +1,21 @@
1
- import { BannerMessages } from '@peax/consent-banner-locales';
2
- import { BuiltInPreferencesSectionId } from '@peax/consent-banner-locales';
1
+ import { BannerMsg as BannerMsg_2 } from '@peax/consent-banner-shared';
2
+ import { BannerOptions } from '@peax/consent-banner-shared';
3
3
  import { NamedExoticComponent } from 'react';
4
+ import { PreferencesSection } from '@peax/consent-banner-shared';
5
+ import { PreferencesSectionId } from '@peax/consent-banner-shared';
4
6
 
5
7
  export declare const Banner: NamedExoticComponent<BannerProps>;
6
8
 
7
- export declare type BannerMsg = {
8
- type: 'on_accept_all' | 'on_reject_all' | 'on_open_preferences' | 'on_close_preferences';
9
- } | {
10
- type: 'on_apply_preferences';
11
- preferences: Record<string, boolean>;
12
- } | {
13
- type: 'on_change_preference';
14
- sectionId: PreferencesSectionId;
15
- checked: boolean;
16
- };
9
+ export declare type BannerMsg = BannerMsg_2;
17
10
 
18
- export declare type BannerProps = Placement & {
19
- isOpen?: boolean;
20
- appearanceDelay?: number;
21
- hasAcceptRejectEqualWeight?: boolean;
22
- showCustomizePreferences?: boolean;
23
- variant?: 'compact' | 'stacked';
24
- locale?: string;
25
- cookiePolicyUrl?: string;
26
- bannerContent?: Partial<BannerMessages>;
27
- preferencesSections?: readonly PreferencesSectionId[];
28
- preferencesContent?: Readonly<Partial<Record<PreferencesSectionId, PreferencesSection>>>;
11
+ export declare type BannerProps = BannerOptions & {
29
12
  className?: string;
30
13
  style?: React.CSSProperties;
31
- theme?: 'light' | 'dark' | 'auto';
32
14
  onMsg?: (msg: BannerMsg) => void;
33
15
  };
34
16
 
35
- declare type Placement = {
36
- position?: 'right';
37
- slideFrom?: 'bottom' | 'right';
38
- } | {
39
- position: 'left';
40
- slideFrom?: 'bottom' | 'left';
41
- } | {
42
- position: 'center';
43
- slideFrom?: 'bottom';
44
- };
45
-
46
- export declare type PreferencesSection = Readonly<{
47
- title?: string;
48
- text?: string;
49
- checked?: boolean;
50
- disabled?: boolean;
51
- }>;
17
+ export { PreferencesSection }
52
18
 
53
- export declare type PreferencesSectionId = BuiltInPreferencesSectionId | (string & Record<never, never>);
19
+ export { PreferencesSectionId }
54
20
 
55
21
  export { }
package/dist/index.js CHANGED
@@ -1,95 +1,121 @@
1
1
  import e from "clsx";
2
2
  import { getMessages as t, getPreferencesMessages as n, parseMessage as r, resolveLocale as i } from "@peax/consent-banner-locales";
3
- import { forwardRef as a, memo as o, useCallback as s, useEffect as c, useId as l, useMemo as u, useRef as d, useState as f } from "react";
4
- import { AnimatePresence as p, LazyMotion as m, domAnimation as h, useIsPresent as g, useReducedMotion as _ } from "framer-motion";
5
- import * as v from "framer-motion/m";
6
- import { Fragment as y, jsx as b, jsxs as x } from "react/jsx-runtime";
7
- import { ScrollArea as S } from "@base-ui/react/scroll-area";
8
- import { Button as C } from "@base-ui/react/button";
9
- import { Switch as w } from "@base-ui/react/switch";
10
- //#region src/utils/notReachable.ts
11
- function T(e, t = { throwError: !0 }) {
12
- let n = `Should not be reached ${e}`;
13
- if (t.throwError) throw Error(n);
3
+ import { defaultPreferencesSections as a, entranceDuration as o, entranceEase as s, mergeBannerContent as c, normalizeAppearanceDelay as l, reconcileDraft as u, resolveFloatingPosition as d, resolveSections as f, resolveSlideFrom as p, safeHref as m, safeHref as h, transitionPreferencesView as g } from "@peax/consent-banner-shared";
4
+ import { forwardRef as _, memo as v, useCallback as y, useEffect as b, useId as x, useMemo as S, useRef as C, useState as w } from "react";
5
+ import { AnimatePresence as T, LazyMotion as E, domAnimation as D, useIsPresent as O, usePresenceData as k, useReducedMotion as A } from "framer-motion";
6
+ import * as j from "framer-motion/m";
7
+ import { Fragment as M, jsx as N, jsxs as P } from "react/jsx-runtime";
8
+ import { Button as F } from "@base-ui/react/button";
9
+ import { ScrollArea as I } from "@base-ui/react/scroll-area";
10
+ import { Switch as L } from "@base-ui/react/switch";
11
+ //#region src/hooks/usePreferencesView.ts
12
+ function R({ isOpen: e, showFloatingButton: t, showCustomizePreferences: n, floatingPosition: r, onMsg: i }) {
13
+ let [a, o] = w(null), [s, c] = w(!1), l = C(null);
14
+ return b(() => {
15
+ o((t) => g(t, {
16
+ type: "sync",
17
+ isOpen: e
18
+ })), (e || !t) && (l.current = null);
19
+ }, [e, t]), {
20
+ view: a,
21
+ hasPanel: s,
22
+ setHasPanel: c,
23
+ triggerRef: y((e) => {
24
+ if (!e) return;
25
+ let t = l.current;
26
+ l.current = null, t && (document.activeElement === t || document.activeElement === document.body) && e.focus();
27
+ }, []),
28
+ onExitFocus: y(() => (t && (l.current = document.activeElement), t), [t]),
29
+ openPreferences: (e) => {
30
+ o((t) => g(t, {
31
+ type: "open",
32
+ source: e,
33
+ position: r
34
+ })), i?.({
35
+ type: "on_open_preferences",
36
+ source: e
37
+ });
38
+ },
39
+ closePreferences: () => {
40
+ a && (a.source === "banner" && (l.current = n ? document.activeElement : null, o((e) => g(e, { type: "close" }))), i?.({
41
+ type: "on_close_preferences",
42
+ source: a.source
43
+ }));
44
+ }
45
+ };
14
46
  }
15
47
  //#endregion
16
48
  //#region src/components/Container/ContainerWrapper.tsx
17
- var E = ({ position: t, slideFrom: n, children: r }) => {
18
- let i = g(), a = _(), o = d(null), s = d(null);
19
- c(() => {
20
- let e = o.current;
49
+ var z = ({ position: t, slideFrom: n, onPresenceChange: r, children: i }) => {
50
+ let a = O(), c = k(), l = A(), u = C(null), d = C(null);
51
+ b(() => (r?.(!0), () => r?.(!1)), [r]), b(() => {
52
+ let e = u.current;
21
53
  if (e) {
22
- if (i) {
54
+ if (a) {
23
55
  e.removeAttribute("inert");
24
56
  let t = document.activeElement;
25
- t instanceof HTMLElement && !e.contains(t) && (s.current = t);
57
+ t instanceof HTMLElement && !e.contains(t) && (d.current = t);
26
58
  return;
27
59
  }
28
- if (e.contains(document.activeElement)) {
29
- let e = s.current;
60
+ if (e.contains(document.activeElement) && !c?.()) {
61
+ let e = d.current;
30
62
  e?.isConnected && e.focus({ preventScroll: !0 });
31
63
  }
32
64
  e.setAttribute("inert", "");
33
65
  }
34
- }, [i]);
35
- let l = n === t ? n : "bottom", u = { transform: a ? "none" : l === "bottom" ? "translateY(100%)" : `translateX(${l === "left" ? "-100%" : "100%"})` };
36
- return /* @__PURE__ */ b(v.div, {
37
- ref: o,
38
- exit: u,
39
- initial: u,
66
+ }, [a, c]);
67
+ let f = p(t, n), m = { transform: l ? "none" : f === "bottom" ? "translateY(100%)" : `translateX(${f === "left" ? "-100%" : "100%"})` };
68
+ return /* @__PURE__ */ N(j.div, {
69
+ ref: u,
70
+ exit: { transform: "none" },
71
+ initial: m,
40
72
  animate: { transform: "none" },
41
- "aria-hidden": !i || void 0,
42
- "data-state": i ? "open" : "closing",
73
+ "aria-hidden": !a || void 0,
74
+ "data-state": a ? "open" : "closing",
43
75
  className: e("pxc-wrapper", `pxc-wrapper--${t}`),
44
76
  transition: {
45
- duration: a ? 0 : i ? .36 : .28,
46
- ease: i ? [
47
- .16,
48
- 1,
49
- .3,
50
- 1
51
- ] : [
52
- .4,
53
- 0,
54
- .2,
55
- 1
56
- ]
77
+ duration: a && !l ? o : 0,
78
+ ease: [...s]
57
79
  },
58
- children: r
80
+ children: i
59
81
  });
60
- }, D = a(function({ isOpen: t = !0, appearanceDelay: n = 0, position: r = "right", slideFrom: i = "bottom", className: a, children: o, actions: s, ...l }, u) {
61
- let d = Number.isFinite(n) ? Math.max(0, n) : 0, [g, _] = f(d === 0);
62
- return c(() => {
63
- if (g || !t) return;
64
- let e = window.setTimeout(() => _(!0), d);
82
+ }, B = _(function({ isOpen: t = !0, appearanceDelay: n = 0, position: r = "right", slideFrom: i = "bottom", onPresenceChange: a, onExitFocus: o, className: s, children: c, actions: u, ...d }, f) {
83
+ let p = l(n), [m, h] = w(p === 0);
84
+ return b(() => {
85
+ if (m || !t) return;
86
+ let e = window.setTimeout(() => h(!0), p);
65
87
  return () => window.clearTimeout(e);
66
88
  }, [
67
- d,
89
+ p,
68
90
  t,
69
- g
70
- ]), /* @__PURE__ */ b(m, {
71
- features: h,
91
+ m
92
+ ]), /* @__PURE__ */ N(E, {
93
+ features: D,
72
94
  strict: !0,
73
- children: /* @__PURE__ */ b(p, { children: t && g && /* @__PURE__ */ b(E, {
74
- position: r,
75
- slideFrom: i,
76
- children: /* @__PURE__ */ b("div", {
77
- ref: u,
78
- className: e("pxc-container", s != null && "pxc-container--with-actions", a),
79
- ...l,
80
- children: s == null ? o : /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b("div", {
81
- className: "pxc-container__content",
82
- "data-container-content": !0,
83
- children: o
84
- }), /* @__PURE__ */ b("div", {
85
- className: "pxc-container__actions",
86
- "data-container-actions": !0,
87
- children: s
88
- })] })
89
- })
90
- }, "container") })
95
+ children: /* @__PURE__ */ N(T, {
96
+ custom: o,
97
+ children: t && m && /* @__PURE__ */ N(z, {
98
+ position: r,
99
+ slideFrom: i,
100
+ onPresenceChange: a,
101
+ children: /* @__PURE__ */ N("div", {
102
+ ref: f,
103
+ className: e("pxc-container", u != null && "pxc-container--with-actions", s),
104
+ ...d,
105
+ children: u == null ? c : /* @__PURE__ */ P(M, { children: [/* @__PURE__ */ N("div", {
106
+ className: "pxc-container__content",
107
+ "data-container-content": !0,
108
+ children: c
109
+ }), /* @__PURE__ */ N("div", {
110
+ className: "pxc-container__actions",
111
+ "data-container-actions": !0,
112
+ children: u
113
+ })] })
114
+ })
115
+ }, "container")
116
+ })
91
117
  });
92
- }), O = () => /* @__PURE__ */ x("svg", {
118
+ }), V = () => /* @__PURE__ */ P("svg", {
93
119
  width: "24",
94
120
  height: "24",
95
121
  viewBox: "0 0 24 24",
@@ -102,14 +128,14 @@ var E = ({ position: t, slideFrom: n, children: r }) => {
102
128
  "aria-hidden": "true",
103
129
  focusable: "false",
104
130
  children: [
105
- /* @__PURE__ */ b("path", { d: "M11 17H11.01" }),
106
- /* @__PURE__ */ b("path", { d: "M12.025 2.61499C12.054 2.29199 11.82 1.98399 11.496 1.99999C9.02138 2.12561 6.68155 3.16546 4.92997 4.91799C3.0533 6.79479 1.99905 9.34023 1.99915 11.9943C1.99924 14.6484 3.05367 17.1938 4.93047 19.0705C6.80728 20.9472 9.35272 22.0014 12.0068 22.0013C14.6609 22.0012 17.2063 20.9468 19.083 19.07C20.8344 17.3179 21.8732 14.9782 21.998 12.504C22.014 12.181 21.707 11.947 21.384 11.977C20.8082 12.0311 20.2276 11.9597 19.6821 11.7677C19.1366 11.5757 18.6392 11.2678 18.2242 10.865C17.8092 10.4623 17.4864 9.97436 17.2782 9.43485C17.07 8.89535 16.9812 8.31711 17.018 7.73999C17.0244 7.63881 17.0093 7.53742 16.9734 7.44257C16.9376 7.34773 16.882 7.26161 16.8103 7.18997C16.7385 7.11833 16.6523 7.06281 16.5574 7.02712C16.4626 6.99143 16.3611 6.97638 16.26 6.98299C15.6828 7.01952 15.1046 6.93048 14.5652 6.72199C14.0258 6.51351 13.538 6.19054 13.1355 5.77535C12.7329 5.36015 12.4252 4.86261 12.2335 4.31701C12.0417 3.77141 11.9706 3.19073 12.025 2.61499Z" }),
107
- /* @__PURE__ */ b("path", { d: "M12 12H12.01" }),
108
- /* @__PURE__ */ b("path", { d: "M16 16H16.01" }),
109
- /* @__PURE__ */ b("path", { d: "M7 14H7.01" }),
110
- /* @__PURE__ */ b("path", { d: "M9 8H9.01" })
131
+ /* @__PURE__ */ N("path", { d: "M11 17H11.01" }),
132
+ /* @__PURE__ */ N("path", { d: "M12.025 2.61499C12.054 2.29199 11.82 1.98399 11.496 1.99999C9.02138 2.12561 6.68155 3.16546 4.92997 4.91799C3.0533 6.79479 1.99905 9.34023 1.99915 11.9943C1.99924 14.6484 3.05367 17.1938 4.93047 19.0705C6.80728 20.9472 9.35272 22.0014 12.0068 22.0013C14.6609 22.0012 17.2063 20.9468 19.083 19.07C20.8344 17.3179 21.8732 14.9782 21.998 12.504C22.014 12.181 21.707 11.947 21.384 11.977C20.8082 12.0311 20.2276 11.9597 19.6821 11.7677C19.1366 11.5757 18.6392 11.2678 18.2242 10.865C17.8092 10.4623 17.4864 9.97436 17.2782 9.43485C17.07 8.89535 16.9812 8.31711 17.018 7.73999C17.0244 7.63881 17.0093 7.53742 16.9734 7.44257C16.9376 7.34773 16.882 7.26161 16.8103 7.18997C16.7385 7.11833 16.6523 7.06281 16.5574 7.02712C16.4626 6.99143 16.3611 6.97638 16.26 6.98299C15.6828 7.01952 15.1046 6.93048 14.5652 6.72199C14.0258 6.51351 13.538 6.19054 13.1355 5.77535C12.7329 5.36015 12.4252 4.86261 12.2335 4.31701C12.0417 3.77141 11.9706 3.19073 12.025 2.61499Z" }),
133
+ /* @__PURE__ */ N("path", { d: "M12 12H12.01" }),
134
+ /* @__PURE__ */ N("path", { d: "M16 16H16.01" }),
135
+ /* @__PURE__ */ N("path", { d: "M7 14H7.01" }),
136
+ /* @__PURE__ */ N("path", { d: "M9 8H9.01" })
111
137
  ]
112
- }), k = () => /* @__PURE__ */ x("svg", {
138
+ }), H = () => /* @__PURE__ */ P("svg", {
113
139
  xmlns: "http://www.w3.org/2000/svg",
114
140
  width: "24",
115
141
  height: "24",
@@ -122,23 +148,40 @@ var E = ({ position: t, slideFrom: n, children: r }) => {
122
148
  "aria-hidden": "true",
123
149
  focusable: "false",
124
150
  children: [
125
- /* @__PURE__ */ b("path", { d: "M14 17H5" }),
126
- /* @__PURE__ */ b("path", { d: "M19 7h-9" }),
127
- /* @__PURE__ */ b("circle", {
151
+ /* @__PURE__ */ N("path", { d: "M14 17H5" }),
152
+ /* @__PURE__ */ N("path", { d: "M19 7h-9" }),
153
+ /* @__PURE__ */ N("circle", {
128
154
  cx: "17",
129
155
  cy: "17",
130
156
  r: "3"
131
157
  }),
132
- /* @__PURE__ */ b("circle", {
158
+ /* @__PURE__ */ N("circle", {
133
159
  cx: "7",
134
160
  cy: "7",
135
161
  r: "3"
136
162
  })
137
163
  ]
138
- }), A = () => /* @__PURE__ */ b("div", {
164
+ }), U = _(function({ variant: t, className: n, children: r, ...i }, a) {
165
+ return /* @__PURE__ */ N(F, {
166
+ ref: a,
167
+ className: (r) => e("pxc-button", `pxc-button--${t}`, typeof n == "function" ? n(r) : n),
168
+ ...i,
169
+ focusableWhenDisabled: !1,
170
+ children: r
171
+ });
172
+ }), W = _(function({ label: e, position: t, onMsg: n }, r) {
173
+ return /* @__PURE__ */ N(U, {
174
+ ref: r,
175
+ variant: "iconed",
176
+ className: `pxc-floating-button pxc-floating-button--${t}`,
177
+ "aria-label": e,
178
+ onClick: () => n({ type: "on_open_preferences" }),
179
+ children: /* @__PURE__ */ N(V, {})
180
+ });
181
+ }), G = () => /* @__PURE__ */ N("div", {
139
182
  className: "pxc-lead-icon",
140
- children: /* @__PURE__ */ b(O, {})
141
- }), j = () => /* @__PURE__ */ b("svg", {
183
+ children: /* @__PURE__ */ N(V, {})
184
+ }), K = () => /* @__PURE__ */ N("svg", {
142
185
  viewBox: "0 0 24 24",
143
186
  fill: "none",
144
187
  stroke: "currentColor",
@@ -146,42 +189,36 @@ var E = ({ position: t, slideFrom: n, children: r }) => {
146
189
  strokeLinecap: "round",
147
190
  "aria-hidden": "true",
148
191
  focusable: "false",
149
- children: /* @__PURE__ */ b("path", { d: "m6 6 12 12M6 18 18 6" })
150
- }), M = a(function({ variant: t, className: n, children: r, ...i }, a) {
151
- return /* @__PURE__ */ b(C, {
152
- ref: a,
153
- className: (r) => e("pxc-button", `pxc-button--${t}`, typeof n == "function" ? n(r) : n),
154
- ...i,
155
- focusableWhenDisabled: !1,
156
- children: r
157
- });
158
- }), N = (e) => {
159
- if (e?.trim()) try {
160
- let t = new URL(e, "https://consent.invalid");
161
- return t.protocol === "http:" || t.protocol === "https:" ? e : void 0;
162
- } catch {
163
- return;
164
- }
165
- }, P = ({ message: e, links: t }) => u(() => r(e, Object.keys(t)), [e, t]).map((e) => {
192
+ children: /* @__PURE__ */ N("path", { d: "m6 6 12 12M6 18 18 6" })
193
+ });
194
+ //#endregion
195
+ //#region src/utils/notReachable.ts
196
+ function q(e, t = { throwError: !0 }) {
197
+ let n = `Should not be reached ${e}`;
198
+ if (t.throwError) throw Error(n);
199
+ }
200
+ //#endregion
201
+ //#region src/components/Text/RichText.tsx
202
+ var J = ({ message: e, links: t }) => S(() => r(e, Object.keys(t)), [e, t]).map((e) => {
166
203
  if (e.type === "text") return e.value;
167
- let n = N(t[e.name]);
168
- return n && e.value.trim() ? /* @__PURE__ */ b("a", {
204
+ let n = m(t[e.name]);
205
+ return n && e.value.trim() ? /* @__PURE__ */ N("a", {
169
206
  className: "pxc-link",
170
207
  href: n,
171
208
  children: e.value
172
209
  }, e.offset) : e.value;
173
- }), F = ({ section: e, checked: t, links: n, onMsg: r }) => {
174
- let i = l();
175
- return /* @__PURE__ */ x("section", {
210
+ }), Y = ({ section: e, checked: t, links: n, onMsg: r }) => {
211
+ let i = x();
212
+ return /* @__PURE__ */ P("section", {
176
213
  className: "pxc-preferences__section",
177
214
  "aria-labelledby": `${i}-title`,
178
- children: [/* @__PURE__ */ x("div", {
215
+ children: [/* @__PURE__ */ P("div", {
179
216
  className: "pxc-preferences__section-heading",
180
- children: [/* @__PURE__ */ b("h4", {
217
+ children: [/* @__PURE__ */ N("h4", {
181
218
  id: `${i}-title`,
182
219
  className: "pxc-text--h4",
183
220
  children: e.title
184
- }), /* @__PURE__ */ b(w.Root, {
221
+ }), /* @__PURE__ */ N(L.Root, {
185
222
  className: "pxc-switch",
186
223
  checked: t,
187
224
  disabled: e.disabled ?? !1,
@@ -192,242 +229,208 @@ var E = ({ position: t, slideFrom: n, children: r }) => {
192
229
  sectionId: e.id,
193
230
  checked: t
194
231
  }),
195
- children: /* @__PURE__ */ b(w.Thumb, { className: "pxc-switch__thumb" })
232
+ children: /* @__PURE__ */ N(L.Thumb, { className: "pxc-switch__thumb" })
196
233
  })]
197
- }), e.text && /* @__PURE__ */ b("p", {
234
+ }), e.text && /* @__PURE__ */ N("p", {
198
235
  id: `${i}-text`,
199
236
  className: "pxc-text--paragraph pxc-preferences__description",
200
- children: /* @__PURE__ */ b(P, {
237
+ children: /* @__PURE__ */ N(J, {
201
238
  message: e.text,
202
239
  links: n
203
240
  })
204
241
  })]
205
242
  });
206
- }, I = ({ sections: e, content: t, links: n, onMsg: r }) => {
207
- let i = l(), a = d(null), [o, s] = f(() => new Map(e.map((e) => [e.id, e.checked]))), u = new Map(e.map((e) => [e.id, e.disabled ? e.checked : o.get(e.id) ?? e.checked]));
208
- (o.size !== u.size || [...u].some(([e, t]) => o.get(e) !== t)) && s(u), c(() => {
243
+ }, X = ({ sections: e, content: t, links: n, onMsg: r }) => {
244
+ let i = x(), a = C(null), [o, s] = w(() => u(e)), c = u(e, o);
245
+ (o.size !== c.size || [...c].some(([e, t]) => o.get(e) !== t)) && s(c), b(() => {
209
246
  a.current?.focus({ preventScroll: !0 });
210
247
  }, []);
211
- let p = N(n.cookiePolicy);
212
- return /* @__PURE__ */ x("section", {
248
+ let l = h(n.cookiePolicy);
249
+ return /* @__PURE__ */ P("section", {
213
250
  className: "pxc-preferences",
214
251
  "aria-labelledby": i,
215
252
  onKeyDown: (e) => {
216
253
  e.key === "Escape" && (e.preventDefault(), e.stopPropagation(), r({ type: "on_close_preferences" }));
217
254
  },
218
255
  children: [
219
- /* @__PURE__ */ x("header", {
256
+ /* @__PURE__ */ P("header", {
220
257
  className: "pxc-preferences__header",
221
- children: [/* @__PURE__ */ b("h3", {
258
+ children: [/* @__PURE__ */ N("h3", {
222
259
  id: i,
223
260
  ref: a,
224
261
  tabIndex: -1,
225
262
  className: "pxc-text--h3",
226
263
  children: t.preferencesTitle
227
- }), /* @__PURE__ */ b(M, {
264
+ }), /* @__PURE__ */ N(U, {
228
265
  variant: "iconed",
229
266
  "aria-label": t.closePreferencesLabel,
230
267
  onClick: () => r({ type: "on_close_preferences" }),
231
- children: /* @__PURE__ */ b(j, {})
268
+ children: /* @__PURE__ */ N(K, {})
232
269
  })]
233
270
  }),
234
- /* @__PURE__ */ x(S.Root, {
271
+ /* @__PURE__ */ P(I.Root, {
235
272
  className: "pxc-preferences__scroll-area",
236
- children: [/* @__PURE__ */ b(S.Viewport, {
273
+ children: [/* @__PURE__ */ N(I.Viewport, {
237
274
  className: "pxc-preferences__body",
238
275
  role: "group",
239
276
  "aria-labelledby": i,
240
- children: /* @__PURE__ */ x(S.Content, {
277
+ children: /* @__PURE__ */ P(I.Content, {
241
278
  className: "pxc-preferences__content",
242
- children: [t.preferencesText && /* @__PURE__ */ b("p", {
279
+ children: [t.preferencesText && /* @__PURE__ */ N("p", {
243
280
  className: "pxc-text--paragraph pxc-preferences__introduction",
244
- children: /* @__PURE__ */ b(P, {
281
+ children: /* @__PURE__ */ N(J, {
245
282
  message: t.preferencesText,
246
283
  links: n
247
284
  })
248
- }), e.map((e) => /* @__PURE__ */ b(F, {
285
+ }), e.map((e) => /* @__PURE__ */ N(Y, {
249
286
  section: e,
250
- checked: u.get(e.id) ?? e.checked,
287
+ checked: c.get(e.id) ?? e.checked,
251
288
  links: n,
252
289
  onMsg: (e) => {
253
290
  switch (e.type) {
254
291
  case "on_change_preference":
255
292
  s((t) => new Map(t).set(e.sectionId, e.checked)), r(e);
256
293
  break;
257
- default: T(e.type);
294
+ default: q(e.type);
258
295
  }
259
296
  }
260
297
  }, e.id))]
261
298
  })
262
- }), /* @__PURE__ */ b(S.Scrollbar, {
299
+ }), /* @__PURE__ */ N(I.Scrollbar, {
263
300
  className: "pxc-preferences__scrollbar",
264
- children: /* @__PURE__ */ b(S.Thumb, {
301
+ children: /* @__PURE__ */ N(I.Thumb, {
265
302
  className: "pxc-preferences__scroll-thumb",
266
- children: /* @__PURE__ */ b("span", { className: "pxc-preferences__scroll-indicator" })
303
+ children: /* @__PURE__ */ N("span", { className: "pxc-preferences__scroll-indicator" })
267
304
  })
268
305
  })]
269
306
  }),
270
- /* @__PURE__ */ x("footer", {
307
+ /* @__PURE__ */ P("footer", {
271
308
  className: "pxc-preferences__footer",
272
- children: [/* @__PURE__ */ b(M, {
309
+ children: [/* @__PURE__ */ N(U, {
273
310
  variant: "primary",
274
311
  onClick: () => r({
275
312
  type: "on_apply_preferences",
276
- preferences: Object.fromEntries(u)
313
+ preferences: Object.fromEntries(c)
277
314
  }),
278
315
  children: t.applyPreferencesLabel
279
- }), p ? /* @__PURE__ */ b("a", {
316
+ }), l ? /* @__PURE__ */ N("a", {
280
317
  className: "pxc-link pxc-preferences__policy",
281
- href: p,
318
+ href: l,
282
319
  children: t.cookiePolicyLabel
283
- }) : /* @__PURE__ */ b("span", {
320
+ }) : /* @__PURE__ */ N("span", {
284
321
  className: "pxc-preferences__policy",
285
322
  children: t.cookiePolicyLabel
286
323
  })]
287
324
  })
288
325
  ]
289
326
  });
290
- }, L = [
291
- "essential",
292
- "analytics",
293
- "marketing"
294
- ];
295
- function R(e, t, n) {
296
- let r = new Map(Object.entries(n));
297
- return [...new Set(e.filter((e) => e.trim()))].map((e) => {
298
- let n = r.get(e), i = t && Object.hasOwn(t, e) ? t[e] : void 0;
299
- return {
300
- id: e,
301
- title: i?.title?.trim() ? i.title : n?.title ?? e,
302
- text: i?.text ?? n?.text ?? "",
303
- checked: i?.checked ?? e === "essential",
304
- disabled: i?.disabled ?? e === "essential"
305
- };
306
- });
307
- }
308
- //#endregion
309
- //#region src/components/Text/Text.tsx
310
- var z = ({ variant: e, tag: t, children: n }) => {
327
+ }, Z = ({ variant: e, tag: t, children: n }) => {
311
328
  switch (e) {
312
- case "label": return /* @__PURE__ */ b(t, {
329
+ case "label": return /* @__PURE__ */ N(t, {
313
330
  className: "pxc-text--label",
314
331
  children: n
315
332
  });
316
- case "heading": return /* @__PURE__ */ b(t, {
333
+ case "heading": return /* @__PURE__ */ N(t, {
317
334
  className: `pxc-text--${t}`,
318
335
  children: n
319
336
  });
320
- case "paragraph": return /* @__PURE__ */ b("p", {
337
+ case "paragraph": return /* @__PURE__ */ N("p", {
321
338
  className: "pxc-text--paragraph",
322
339
  children: n
323
340
  });
324
- default: return T(e);
341
+ default: return q(e);
325
342
  }
326
- }, B = ({ variant: e, content: t, equalWeight: n, showCustomizePreferences: r, preferencesRef: i, onMsg: a }) => /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ x("div", {
343
+ }, Q = ({ variant: e, content: t, equalWeight: n, showCustomizePreferences: r, preferencesRef: i, onMsg: a }) => /* @__PURE__ */ P(M, { children: [/* @__PURE__ */ P("div", {
327
344
  className: "pxc-banner__choices",
328
- children: [/* @__PURE__ */ b(M, {
345
+ children: [/* @__PURE__ */ N(U, {
329
346
  variant: n ? "secondary" : "primary",
330
347
  onClick: () => a({ type: "on_accept_all" }),
331
348
  children: t.acceptAllLabel
332
- }), /* @__PURE__ */ b(M, {
349
+ }), /* @__PURE__ */ N(U, {
333
350
  variant: "secondary",
334
351
  onClick: () => a({ type: "on_reject_all" }),
335
352
  children: t.rejectAllLabel
336
353
  })]
337
- }), r && /* @__PURE__ */ b("div", {
354
+ }), r && /* @__PURE__ */ N("div", {
338
355
  className: "pxc-banner__preferences",
339
- children: /* @__PURE__ */ b(M, {
356
+ children: /* @__PURE__ */ N(U, {
340
357
  ref: i,
341
358
  variant: "inline",
342
359
  className: e === "compact" ? "pxc-button--iconed" : void 0,
343
360
  "aria-label": t.customizePreferencesLabel,
344
361
  onClick: () => a({ type: "on_open_preferences" }),
345
- children: e === "compact" ? /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b(k, {}), /* @__PURE__ */ b("span", { children: t.customizePreferencesLabel })] }) : t.customizePreferencesLabel
362
+ children: e === "compact" ? /* @__PURE__ */ P(M, { children: [/* @__PURE__ */ N(H, {}), /* @__PURE__ */ N("span", { children: t.customizePreferencesLabel })] }) : t.customizePreferencesLabel
346
363
  })
347
- })] }), V = o(function({ bannerContent: r, cookiePolicyUrl: a, isOpen: o = !1, appearanceDelay: l = 0, preferencesSections: p = L, preferencesContent: m, hasAcceptRejectEqualWeight: h = !1, showCustomizePreferences: g = !0, variant: _ = "compact", theme: v = "auto", locale: S = "en", onMsg: C, className: w, style: E, ...O }) {
348
- let k = i(S), j = t(k), M = Object.fromEntries(Object.entries(r ?? {}).filter(([, e]) => e != null)), N = {
349
- ...j,
350
- ...M
351
- }, F = u(() => ({ cookiePolicy: a }), [a]), V = u(() => R(p, m, n(k)), [
364
+ })] }), $ = v(function({ bannerContent: r, cookiePolicyUrl: o, isOpen: s = !1, appearanceDelay: l = 0, preferencesSections: u = a, preferencesContent: p, hasAcceptRejectEqualWeight: m = !1, showCustomizePreferences: h = !0, showFloatingButton: g = !0, floatingButtonPosition: _, variant: v = "compact", theme: y = "auto", locale: b = "en", onMsg: x, className: C, style: w, ...T }) {
365
+ let E = i(b), D = t(E), O = c(D, r), k = S(() => ({ cookiePolicy: o }), [o]), A = S(() => f(u, p, n(E)), [
366
+ u,
352
367
  p,
353
- m,
354
- k
355
- ]), [H, U] = f(!1), W = d(!1), G = s((e) => {
356
- e && W.current && (W.current = !1, e.focus());
357
- }, []);
358
- c(() => {
359
- o || (W.current = !1, U(!1));
360
- }, [o]);
361
- let K = () => {
362
- U(!0), C?.({ type: "on_open_preferences" });
363
- }, q = () => {
364
- W.current = g, U(!1), C?.({ type: "on_close_preferences" });
368
+ E
369
+ ]), j = d(T.position, _), F = R({
370
+ isOpen: s,
371
+ showFloatingButton: g,
372
+ showCustomizePreferences: h,
373
+ floatingPosition: j,
374
+ onMsg: x
375
+ }), I = F.view !== null, L = F.view?.source === "floating-button" ? {
376
+ ...T,
377
+ position: F.view.position,
378
+ slideFrom: "bottom"
379
+ } : T, z = (e) => {
380
+ if (e.type === "on_open_preferences") return F.openPreferences("banner");
381
+ if (e.type === "on_close_preferences") return F.closePreferences();
382
+ x?.(e);
365
383
  };
366
- return /* @__PURE__ */ b("div", {
367
- "data-theme": v,
368
- "data-banner-variant": _,
369
- className: e("pxc", w),
384
+ return /* @__PURE__ */ P("div", {
385
+ "data-theme": y,
386
+ "data-banner-variant": v,
387
+ className: e("pxc", C),
370
388
  style: {
371
389
  fontFamily: "var(--pxc-font-family)",
372
- ...E
390
+ ...w
373
391
  },
374
- lang: k,
375
- children: /* @__PURE__ */ b(D, {
376
- ...O,
377
- isOpen: !!o,
392
+ lang: E,
393
+ children: [/* @__PURE__ */ N(B, {
394
+ ...L,
395
+ isOpen: s,
396
+ onExitFocus: F.onExitFocus,
397
+ onPresenceChange: F.setHasPanel,
378
398
  appearanceDelay: l,
379
- className: H ? "pxc-container--preferences" : void 0,
380
- actions: H ? null : /* @__PURE__ */ b(B, {
381
- variant: _,
382
- content: N,
383
- equalWeight: h,
384
- showCustomizePreferences: g,
385
- preferencesRef: G,
386
- onMsg: (e) => {
387
- switch (e.type) {
388
- case "on_open_preferences":
389
- K();
390
- break;
391
- case "on_accept_all":
392
- case "on_reject_all":
393
- C?.(e);
394
- break;
395
- default: T(e.type);
396
- }
397
- }
399
+ className: I ? "pxc-container--preferences" : void 0,
400
+ actions: I ? null : /* @__PURE__ */ N(Q, {
401
+ variant: v,
402
+ content: O,
403
+ equalWeight: m,
404
+ showCustomizePreferences: h,
405
+ preferencesRef: F.triggerRef,
406
+ onMsg: z
398
407
  }),
399
- children: H ? /* @__PURE__ */ b(I, {
400
- links: F,
401
- content: N,
402
- sections: V,
403
- onMsg: (e) => {
404
- switch (e.type) {
405
- case "on_close_preferences":
406
- q();
407
- break;
408
- case "on_apply_preferences":
409
- case "on_change_preference":
410
- C?.(e);
411
- break;
412
- default: T(e);
413
- }
414
- }
415
- }) : /* @__PURE__ */ x(y, { children: [_ === "stacked" ? /* @__PURE__ */ x("div", {
408
+ children: I ? /* @__PURE__ */ N(X, {
409
+ links: k,
410
+ content: O,
411
+ sections: A,
412
+ onMsg: z
413
+ }) : /* @__PURE__ */ P(M, { children: [v === "stacked" ? /* @__PURE__ */ P("div", {
416
414
  className: "pxc-banner__heading",
417
- children: [/* @__PURE__ */ b(A, {}), /* @__PURE__ */ b(z, {
415
+ children: [/* @__PURE__ */ N(G, {}), /* @__PURE__ */ N(Z, {
418
416
  variant: "heading",
419
417
  tag: "h3",
420
- children: N.title
418
+ children: O.title
421
419
  })]
422
- }) : /* @__PURE__ */ b(A, {}), /* @__PURE__ */ b(z, {
420
+ }) : /* @__PURE__ */ N(G, {}), /* @__PURE__ */ N(Z, {
423
421
  variant: "paragraph",
424
- children: /* @__PURE__ */ b(P, {
425
- message: N.collapsedText,
426
- links: F
422
+ children: /* @__PURE__ */ N(J, {
423
+ message: O.collapsedText,
424
+ links: k
427
425
  })
428
426
  })] })
429
- })
427
+ }), g && !s && !F.hasPanel && /* @__PURE__ */ N(W, {
428
+ ref: F.triggerRef,
429
+ position: j,
430
+ label: O.customizePreferencesLabel.trim() || D.customizePreferencesLabel,
431
+ onMsg: () => F.openPreferences("floating-button")
432
+ })]
430
433
  });
431
434
  });
432
435
  //#endregion
433
- export { V as Banner };
436
+ export { $ as Banner };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peax/consent-banner-react",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Accessible React bindings and components for Peax Consent.",
5
5
  "keywords": [
6
6
  "consent-management",
@@ -54,8 +54,9 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@base-ui/react": "^1.7.0",
57
- "@peax/consent-banner-locales": "^0.1.0",
58
- "@peax/consent-banner-theme": "^0.1.0",
57
+ "@peax/consent-banner-locales": "^0.3.0",
58
+ "@peax/consent-banner-shared": "^0.3.0",
59
+ "@peax/consent-banner-theme": "^0.3.0",
59
60
  "clsx": "^2.1.1",
60
61
  "framer-motion": "^13.1.1"
61
62
  },