@peax/consent-banner-react 0.1.0 → 0.2.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
 
package/dist/index.d.ts CHANGED
@@ -4,8 +4,8 @@ import { NamedExoticComponent } from 'react';
4
4
 
5
5
  export declare const Banner: NamedExoticComponent<BannerProps>;
6
6
 
7
- export declare type BannerMsg = {
8
- type: 'on_accept_all' | 'on_reject_all' | 'on_open_preferences' | 'on_close_preferences';
7
+ export declare type BannerMsg = PreferencesNavigationMsg | {
8
+ type: 'on_accept_all' | 'on_reject_all';
9
9
  } | {
10
10
  type: 'on_apply_preferences';
11
11
  preferences: Record<string, boolean>;
@@ -15,7 +15,7 @@ export declare type BannerMsg = {
15
15
  checked: boolean;
16
16
  };
17
17
 
18
- export declare type BannerProps = Placement & {
18
+ export declare type BannerProps = Placement & FloatingButtonProps & {
19
19
  isOpen?: boolean;
20
20
  appearanceDelay?: number;
21
21
  hasAcceptRejectEqualWeight?: boolean;
@@ -32,6 +32,16 @@ export declare type BannerProps = Placement & {
32
32
  onMsg?: (msg: BannerMsg) => void;
33
33
  };
34
34
 
35
+ declare type FloatingButtonPosition = 'left' | 'right';
36
+
37
+ declare type FloatingButtonProps = {
38
+ showFloatingButton?: false;
39
+ floatingButtonPosition?: never;
40
+ } | {
41
+ showFloatingButton: true;
42
+ floatingButtonPosition?: FloatingButtonPosition;
43
+ };
44
+
35
45
  declare type Placement = {
36
46
  position?: 'right';
37
47
  slideFrom?: 'bottom' | 'right';
@@ -43,6 +53,11 @@ declare type Placement = {
43
53
  slideFrom?: 'bottom';
44
54
  };
45
55
 
56
+ declare type PreferencesNavigationMsg = {
57
+ type: 'on_open_preferences' | 'on_close_preferences';
58
+ source: Source;
59
+ };
60
+
46
61
  export declare type PreferencesSection = Readonly<{
47
62
  title?: string;
48
63
  text?: string;
@@ -52,4 +67,6 @@ export declare type PreferencesSection = Readonly<{
52
67
 
53
68
  export declare type PreferencesSectionId = BuiltInPreferencesSectionId | (string & Record<never, never>);
54
69
 
70
+ declare type Source = 'banner' | 'floating-button';
71
+
55
72
  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
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";
4
+ import { AnimatePresence as p, LazyMotion as m, domAnimation as h, useIsPresent as g, usePresenceData as _, useReducedMotion as v } from "framer-motion";
5
+ import * as y from "framer-motion/m";
6
+ import { Fragment as b, jsx as x, jsxs as S } from "react/jsx-runtime";
8
7
  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);
8
+ import { ScrollArea as w } from "@base-ui/react/scroll-area";
9
+ import { Switch as T } from "@base-ui/react/switch";
10
+ //#region src/hooks/usePreferencesView.ts
11
+ function E({ isOpen: e, showFloatingButton: t, showCustomizePreferences: n, floatingPosition: r, onMsg: i }) {
12
+ let [a, o] = f(null), [l, u] = f(!1), p = d(null);
13
+ return c(() => {
14
+ e || o(null), (e || !t) && (p.current = null);
15
+ }, [e, t]), {
16
+ view: a,
17
+ hasPanel: l,
18
+ setHasPanel: u,
19
+ triggerRef: s((e) => {
20
+ if (!e) return;
21
+ let t = p.current;
22
+ p.current = null, t && (document.activeElement === t || document.activeElement === document.body) && e.focus();
23
+ }, []),
24
+ onExitFocus: s(() => (t && (p.current = document.activeElement), t), [t]),
25
+ openPreferences: (e) => {
26
+ o(e === "banner" ? { source: e } : {
27
+ source: e,
28
+ position: r
29
+ }), i?.({
30
+ type: "on_open_preferences",
31
+ source: e
32
+ });
33
+ },
34
+ closePreferences: () => {
35
+ a && (a.source === "banner" && (p.current = n ? document.activeElement : null, o(null)), i?.({
36
+ type: "on_close_preferences",
37
+ source: a.source
38
+ }));
39
+ }
40
+ };
14
41
  }
15
42
  //#endregion
16
43
  //#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;
44
+ var D = ({ position: t, slideFrom: n, onPresenceChange: r, children: i }) => {
45
+ let a = g(), o = _(), s = v(), l = d(null), u = d(null);
46
+ c(() => (r?.(!0), () => r?.(!1)), [r]), c(() => {
47
+ let e = l.current;
21
48
  if (e) {
22
- if (i) {
49
+ if (a) {
23
50
  e.removeAttribute("inert");
24
51
  let t = document.activeElement;
25
- t instanceof HTMLElement && !e.contains(t) && (s.current = t);
52
+ t instanceof HTMLElement && !e.contains(t) && (u.current = t);
26
53
  return;
27
54
  }
28
- if (e.contains(document.activeElement)) {
29
- let e = s.current;
55
+ if (e.contains(document.activeElement) && !o?.()) {
56
+ let e = u.current;
30
57
  e?.isConnected && e.focus({ preventScroll: !0 });
31
58
  }
32
59
  e.setAttribute("inert", "");
33
60
  }
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,
61
+ }, [a, o]);
62
+ let f = n === t ? n : "bottom", p = { transform: s ? "none" : f === "bottom" ? "translateY(100%)" : `translateX(${f === "left" ? "-100%" : "100%"})` };
63
+ return /* @__PURE__ */ x(y.div, {
64
+ ref: l,
65
+ exit: { transform: "none" },
66
+ initial: p,
40
67
  animate: { transform: "none" },
41
- "aria-hidden": !i || void 0,
42
- "data-state": i ? "open" : "closing",
68
+ "aria-hidden": !a || void 0,
69
+ "data-state": a ? "open" : "closing",
43
70
  className: e("pxc-wrapper", `pxc-wrapper--${t}`),
44
71
  transition: {
45
- duration: a ? 0 : i ? .36 : .28,
46
- ease: i ? [
72
+ duration: a && !s ? .36 : 0,
73
+ ease: [
47
74
  .16,
48
75
  1,
49
76
  .3,
50
77
  1
51
- ] : [
52
- .4,
53
- 0,
54
- .2,
55
- 1
56
78
  ]
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);
82
+ }, O = a(function({ isOpen: t = !0, appearanceDelay: n = 0, position: r = "right", slideFrom: i = "bottom", onPresenceChange: a, onExitFocus: o, className: s, children: l, actions: u, ...d }, g) {
83
+ let _ = Number.isFinite(n) ? Math.max(0, n) : 0, [v, y] = f(_ === 0);
62
84
  return c(() => {
63
- if (g || !t) return;
64
- let e = window.setTimeout(() => _(!0), d);
85
+ if (v || !t) return;
86
+ let e = window.setTimeout(() => y(!0), _);
65
87
  return () => window.clearTimeout(e);
66
88
  }, [
67
- d,
89
+ _,
68
90
  t,
69
- g
70
- ]), /* @__PURE__ */ b(m, {
91
+ v
92
+ ]), /* @__PURE__ */ x(m, {
71
93
  features: h,
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__ */ x(p, {
96
+ custom: o,
97
+ children: t && v && /* @__PURE__ */ x(D, {
98
+ position: r,
99
+ slideFrom: i,
100
+ onPresenceChange: a,
101
+ children: /* @__PURE__ */ x("div", {
102
+ ref: g,
103
+ className: e("pxc-container", u != null && "pxc-container--with-actions", s),
104
+ ...d,
105
+ children: u == null ? l : /* @__PURE__ */ S(b, { children: [/* @__PURE__ */ x("div", {
106
+ className: "pxc-container__content",
107
+ "data-container-content": !0,
108
+ children: l
109
+ }), /* @__PURE__ */ x("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
+ }), k = () => /* @__PURE__ */ S("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__ */ x("path", { d: "M11 17H11.01" }),
132
+ /* @__PURE__ */ x("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__ */ x("path", { d: "M12 12H12.01" }),
134
+ /* @__PURE__ */ x("path", { d: "M16 16H16.01" }),
135
+ /* @__PURE__ */ x("path", { d: "M7 14H7.01" }),
136
+ /* @__PURE__ */ x("path", { d: "M9 8H9.01" })
111
137
  ]
112
- }), k = () => /* @__PURE__ */ x("svg", {
138
+ }), A = () => /* @__PURE__ */ S("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__ */ x("path", { d: "M14 17H5" }),
152
+ /* @__PURE__ */ x("path", { d: "M19 7h-9" }),
153
+ /* @__PURE__ */ x("circle", {
128
154
  cx: "17",
129
155
  cy: "17",
130
156
  r: "3"
131
157
  }),
132
- /* @__PURE__ */ b("circle", {
158
+ /* @__PURE__ */ x("circle", {
133
159
  cx: "7",
134
160
  cy: "7",
135
161
  r: "3"
136
162
  })
137
163
  ]
138
- }), A = () => /* @__PURE__ */ b("div", {
164
+ }), j = a(function({ variant: t, className: n, children: r, ...i }, a) {
165
+ return /* @__PURE__ */ x(C, {
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
+ }), M = a(function({ label: e, position: t, onMsg: n }, r) {
173
+ return /* @__PURE__ */ x(j, {
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__ */ x(k, {})
180
+ });
181
+ }), N = () => /* @__PURE__ */ x("div", {
139
182
  className: "pxc-lead-icon",
140
- children: /* @__PURE__ */ b(O, {})
141
- }), j = () => /* @__PURE__ */ b("svg", {
183
+ children: /* @__PURE__ */ x(k, {})
184
+ }), P = () => /* @__PURE__ */ x("svg", {
142
185
  viewBox: "0 0 24 24",
143
186
  fill: "none",
144
187
  stroke: "currentColor",
@@ -146,42 +189,43 @@ 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) => {
192
+ children: /* @__PURE__ */ x("path", { d: "m6 6 12 12M6 18 18 6" })
193
+ });
194
+ //#endregion
195
+ //#region src/utils/notReachable.ts
196
+ function F(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 I = (e) => {
159
203
  if (e?.trim()) try {
160
204
  let t = new URL(e, "https://consent.invalid");
161
205
  return t.protocol === "http:" || t.protocol === "https:" ? e : void 0;
162
206
  } catch {
163
207
  return;
164
208
  }
165
- }, P = ({ message: e, links: t }) => u(() => r(e, Object.keys(t)), [e, t]).map((e) => {
209
+ }, L = ({ message: e, links: t }) => u(() => r(e, Object.keys(t)), [e, t]).map((e) => {
166
210
  if (e.type === "text") return e.value;
167
- let n = N(t[e.name]);
168
- return n && e.value.trim() ? /* @__PURE__ */ b("a", {
211
+ let n = I(t[e.name]);
212
+ return n && e.value.trim() ? /* @__PURE__ */ x("a", {
169
213
  className: "pxc-link",
170
214
  href: n,
171
215
  children: e.value
172
216
  }, e.offset) : e.value;
173
- }), F = ({ section: e, checked: t, links: n, onMsg: r }) => {
217
+ }), R = ({ section: e, checked: t, links: n, onMsg: r }) => {
174
218
  let i = l();
175
- return /* @__PURE__ */ x("section", {
219
+ return /* @__PURE__ */ S("section", {
176
220
  className: "pxc-preferences__section",
177
221
  "aria-labelledby": `${i}-title`,
178
- children: [/* @__PURE__ */ x("div", {
222
+ children: [/* @__PURE__ */ S("div", {
179
223
  className: "pxc-preferences__section-heading",
180
- children: [/* @__PURE__ */ b("h4", {
224
+ children: [/* @__PURE__ */ x("h4", {
181
225
  id: `${i}-title`,
182
226
  className: "pxc-text--h4",
183
227
  children: e.title
184
- }), /* @__PURE__ */ b(w.Root, {
228
+ }), /* @__PURE__ */ x(T.Root, {
185
229
  className: "pxc-switch",
186
230
  checked: t,
187
231
  disabled: e.disabled ?? !1,
@@ -192,60 +236,60 @@ var E = ({ position: t, slideFrom: n, children: r }) => {
192
236
  sectionId: e.id,
193
237
  checked: t
194
238
  }),
195
- children: /* @__PURE__ */ b(w.Thumb, { className: "pxc-switch__thumb" })
239
+ children: /* @__PURE__ */ x(T.Thumb, { className: "pxc-switch__thumb" })
196
240
  })]
197
- }), e.text && /* @__PURE__ */ b("p", {
241
+ }), e.text && /* @__PURE__ */ x("p", {
198
242
  id: `${i}-text`,
199
243
  className: "pxc-text--paragraph pxc-preferences__description",
200
- children: /* @__PURE__ */ b(P, {
244
+ children: /* @__PURE__ */ x(L, {
201
245
  message: e.text,
202
246
  links: n
203
247
  })
204
248
  })]
205
249
  });
206
- }, I = ({ sections: e, content: t, links: n, onMsg: r }) => {
250
+ }, z = ({ sections: e, content: t, links: n, onMsg: r }) => {
207
251
  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
252
  (o.size !== u.size || [...u].some(([e, t]) => o.get(e) !== t)) && s(u), c(() => {
209
253
  a.current?.focus({ preventScroll: !0 });
210
254
  }, []);
211
- let p = N(n.cookiePolicy);
212
- return /* @__PURE__ */ x("section", {
255
+ let p = I(n.cookiePolicy);
256
+ return /* @__PURE__ */ S("section", {
213
257
  className: "pxc-preferences",
214
258
  "aria-labelledby": i,
215
259
  onKeyDown: (e) => {
216
260
  e.key === "Escape" && (e.preventDefault(), e.stopPropagation(), r({ type: "on_close_preferences" }));
217
261
  },
218
262
  children: [
219
- /* @__PURE__ */ x("header", {
263
+ /* @__PURE__ */ S("header", {
220
264
  className: "pxc-preferences__header",
221
- children: [/* @__PURE__ */ b("h3", {
265
+ children: [/* @__PURE__ */ x("h3", {
222
266
  id: i,
223
267
  ref: a,
224
268
  tabIndex: -1,
225
269
  className: "pxc-text--h3",
226
270
  children: t.preferencesTitle
227
- }), /* @__PURE__ */ b(M, {
271
+ }), /* @__PURE__ */ x(j, {
228
272
  variant: "iconed",
229
273
  "aria-label": t.closePreferencesLabel,
230
274
  onClick: () => r({ type: "on_close_preferences" }),
231
- children: /* @__PURE__ */ b(j, {})
275
+ children: /* @__PURE__ */ x(P, {})
232
276
  })]
233
277
  }),
234
- /* @__PURE__ */ x(S.Root, {
278
+ /* @__PURE__ */ S(w.Root, {
235
279
  className: "pxc-preferences__scroll-area",
236
- children: [/* @__PURE__ */ b(S.Viewport, {
280
+ children: [/* @__PURE__ */ x(w.Viewport, {
237
281
  className: "pxc-preferences__body",
238
282
  role: "group",
239
283
  "aria-labelledby": i,
240
- children: /* @__PURE__ */ x(S.Content, {
284
+ children: /* @__PURE__ */ S(w.Content, {
241
285
  className: "pxc-preferences__content",
242
- children: [t.preferencesText && /* @__PURE__ */ b("p", {
286
+ children: [t.preferencesText && /* @__PURE__ */ x("p", {
243
287
  className: "pxc-text--paragraph pxc-preferences__introduction",
244
- children: /* @__PURE__ */ b(P, {
288
+ children: /* @__PURE__ */ x(L, {
245
289
  message: t.preferencesText,
246
290
  links: n
247
291
  })
248
- }), e.map((e) => /* @__PURE__ */ b(F, {
292
+ }), e.map((e) => /* @__PURE__ */ x(R, {
249
293
  section: e,
250
294
  checked: u.get(e.id) ?? e.checked,
251
295
  links: n,
@@ -254,45 +298,45 @@ var E = ({ position: t, slideFrom: n, children: r }) => {
254
298
  case "on_change_preference":
255
299
  s((t) => new Map(t).set(e.sectionId, e.checked)), r(e);
256
300
  break;
257
- default: T(e.type);
301
+ default: F(e.type);
258
302
  }
259
303
  }
260
304
  }, e.id))]
261
305
  })
262
- }), /* @__PURE__ */ b(S.Scrollbar, {
306
+ }), /* @__PURE__ */ x(w.Scrollbar, {
263
307
  className: "pxc-preferences__scrollbar",
264
- children: /* @__PURE__ */ b(S.Thumb, {
308
+ children: /* @__PURE__ */ x(w.Thumb, {
265
309
  className: "pxc-preferences__scroll-thumb",
266
- children: /* @__PURE__ */ b("span", { className: "pxc-preferences__scroll-indicator" })
310
+ children: /* @__PURE__ */ x("span", { className: "pxc-preferences__scroll-indicator" })
267
311
  })
268
312
  })]
269
313
  }),
270
- /* @__PURE__ */ x("footer", {
314
+ /* @__PURE__ */ S("footer", {
271
315
  className: "pxc-preferences__footer",
272
- children: [/* @__PURE__ */ b(M, {
316
+ children: [/* @__PURE__ */ x(j, {
273
317
  variant: "primary",
274
318
  onClick: () => r({
275
319
  type: "on_apply_preferences",
276
320
  preferences: Object.fromEntries(u)
277
321
  }),
278
322
  children: t.applyPreferencesLabel
279
- }), p ? /* @__PURE__ */ b("a", {
323
+ }), p ? /* @__PURE__ */ x("a", {
280
324
  className: "pxc-link pxc-preferences__policy",
281
325
  href: p,
282
326
  children: t.cookiePolicyLabel
283
- }) : /* @__PURE__ */ b("span", {
327
+ }) : /* @__PURE__ */ x("span", {
284
328
  className: "pxc-preferences__policy",
285
329
  children: t.cookiePolicyLabel
286
330
  })]
287
331
  })
288
332
  ]
289
333
  });
290
- }, L = [
334
+ }, B = [
291
335
  "essential",
292
336
  "analytics",
293
337
  "marketing"
294
338
  ];
295
- function R(e, t, n) {
339
+ function V(e, t, n) {
296
340
  let r = new Map(Object.entries(n));
297
341
  return [...new Set(e.filter((e) => e.trim()))].map((e) => {
298
342
  let n = r.get(e), i = t && Object.hasOwn(t, e) ? t[e] : void 0;
@@ -307,127 +351,116 @@ function R(e, t, n) {
307
351
  }
308
352
  //#endregion
309
353
  //#region src/components/Text/Text.tsx
310
- var z = ({ variant: e, tag: t, children: n }) => {
354
+ var H = ({ variant: e, tag: t, children: n }) => {
311
355
  switch (e) {
312
- case "label": return /* @__PURE__ */ b(t, {
356
+ case "label": return /* @__PURE__ */ x(t, {
313
357
  className: "pxc-text--label",
314
358
  children: n
315
359
  });
316
- case "heading": return /* @__PURE__ */ b(t, {
360
+ case "heading": return /* @__PURE__ */ x(t, {
317
361
  className: `pxc-text--${t}`,
318
362
  children: n
319
363
  });
320
- case "paragraph": return /* @__PURE__ */ b("p", {
364
+ case "paragraph": return /* @__PURE__ */ x("p", {
321
365
  className: "pxc-text--paragraph",
322
366
  children: n
323
367
  });
324
- default: return T(e);
368
+ default: return F(e);
325
369
  }
326
- }, B = ({ variant: e, content: t, equalWeight: n, showCustomizePreferences: r, preferencesRef: i, onMsg: a }) => /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ x("div", {
370
+ }, U = ({ variant: e, content: t, equalWeight: n, showCustomizePreferences: r, preferencesRef: i, onMsg: a }) => /* @__PURE__ */ S(b, { children: [/* @__PURE__ */ S("div", {
327
371
  className: "pxc-banner__choices",
328
- children: [/* @__PURE__ */ b(M, {
372
+ children: [/* @__PURE__ */ x(j, {
329
373
  variant: n ? "secondary" : "primary",
330
374
  onClick: () => a({ type: "on_accept_all" }),
331
375
  children: t.acceptAllLabel
332
- }), /* @__PURE__ */ b(M, {
376
+ }), /* @__PURE__ */ x(j, {
333
377
  variant: "secondary",
334
378
  onClick: () => a({ type: "on_reject_all" }),
335
379
  children: t.rejectAllLabel
336
380
  })]
337
- }), r && /* @__PURE__ */ b("div", {
381
+ }), r && /* @__PURE__ */ x("div", {
338
382
  className: "pxc-banner__preferences",
339
- children: /* @__PURE__ */ b(M, {
383
+ children: /* @__PURE__ */ x(j, {
340
384
  ref: i,
341
385
  variant: "inline",
342
386
  className: e === "compact" ? "pxc-button--iconed" : void 0,
343
387
  "aria-label": t.customizePreferencesLabel,
344
388
  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
389
+ children: e === "compact" ? /* @__PURE__ */ S(b, { children: [/* @__PURE__ */ x(A, {}), /* @__PURE__ */ x("span", { children: t.customizePreferencesLabel })] }) : t.customizePreferencesLabel
346
390
  })
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)), [
352
- 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" });
391
+ })] }), W = o(function({ bannerContent: r, cookiePolicyUrl: a, isOpen: o = !1, appearanceDelay: s = 0, preferencesSections: c = B, preferencesContent: l, hasAcceptRejectEqualWeight: d = !1, showCustomizePreferences: f = !0, showFloatingButton: p = !0, floatingButtonPosition: m, variant: h = "compact", theme: g = "auto", locale: _ = "en", onMsg: v, className: y, style: C, ...w }) {
392
+ let T = i(_), D = t(T), k = Object.fromEntries(Object.entries(r ?? {}).filter(([, e]) => e != null)), A = {
393
+ ...D,
394
+ ...k
395
+ }, j = u(() => ({ cookiePolicy: a }), [a]), P = u(() => V(c, l, n(T)), [
396
+ c,
397
+ l,
398
+ T
399
+ ]), F = m ?? (w.position === "left" ? "left" : "right"), I = E({
400
+ isOpen: o,
401
+ showFloatingButton: p,
402
+ showCustomizePreferences: f,
403
+ floatingPosition: F,
404
+ onMsg: v
405
+ }), R = I.view !== null, W = I.view?.source === "floating-button" ? {
406
+ ...w,
407
+ position: I.view.position,
408
+ slideFrom: "bottom"
409
+ } : w, G = (e) => {
410
+ if (e.type === "on_open_preferences") return I.openPreferences("banner");
411
+ if (e.type === "on_close_preferences") return I.closePreferences();
412
+ v?.(e);
365
413
  };
366
- return /* @__PURE__ */ b("div", {
367
- "data-theme": v,
368
- "data-banner-variant": _,
369
- className: e("pxc", w),
414
+ return /* @__PURE__ */ S("div", {
415
+ "data-theme": g,
416
+ "data-banner-variant": h,
417
+ className: e("pxc", y),
370
418
  style: {
371
419
  fontFamily: "var(--pxc-font-family)",
372
- ...E
420
+ ...C
373
421
  },
374
- lang: k,
375
- children: /* @__PURE__ */ b(D, {
376
- ...O,
377
- isOpen: !!o,
378
- 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
- }
422
+ lang: T,
423
+ children: [/* @__PURE__ */ x(O, {
424
+ ...W,
425
+ isOpen: o,
426
+ onExitFocus: I.onExitFocus,
427
+ onPresenceChange: I.setHasPanel,
428
+ appearanceDelay: s,
429
+ className: R ? "pxc-container--preferences" : void 0,
430
+ actions: R ? null : /* @__PURE__ */ x(U, {
431
+ variant: h,
432
+ content: A,
433
+ equalWeight: d,
434
+ showCustomizePreferences: f,
435
+ preferencesRef: I.triggerRef,
436
+ onMsg: G
398
437
  }),
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", {
438
+ children: R ? /* @__PURE__ */ x(z, {
439
+ links: j,
440
+ content: A,
441
+ sections: P,
442
+ onMsg: G
443
+ }) : /* @__PURE__ */ S(b, { children: [h === "stacked" ? /* @__PURE__ */ S("div", {
416
444
  className: "pxc-banner__heading",
417
- children: [/* @__PURE__ */ b(A, {}), /* @__PURE__ */ b(z, {
445
+ children: [/* @__PURE__ */ x(N, {}), /* @__PURE__ */ x(H, {
418
446
  variant: "heading",
419
447
  tag: "h3",
420
- children: N.title
448
+ children: A.title
421
449
  })]
422
- }) : /* @__PURE__ */ b(A, {}), /* @__PURE__ */ b(z, {
450
+ }) : /* @__PURE__ */ x(N, {}), /* @__PURE__ */ x(H, {
423
451
  variant: "paragraph",
424
- children: /* @__PURE__ */ b(P, {
425
- message: N.collapsedText,
426
- links: F
452
+ children: /* @__PURE__ */ x(L, {
453
+ message: A.collapsedText,
454
+ links: j
427
455
  })
428
456
  })] })
429
- })
457
+ }), p && !o && !I.hasPanel && /* @__PURE__ */ x(M, {
458
+ ref: I.triggerRef,
459
+ position: F,
460
+ label: A.customizePreferencesLabel.trim() || D.customizePreferencesLabel,
461
+ onMsg: () => I.openPreferences("floating-button")
462
+ })]
430
463
  });
431
464
  });
432
465
  //#endregion
433
- export { V as Banner };
466
+ export { W 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.2.0",
4
4
  "description": "Accessible React bindings and components for Peax Consent.",
5
5
  "keywords": [
6
6
  "consent-management",
@@ -54,8 +54,8 @@
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.2.0",
58
+ "@peax/consent-banner-theme": "^0.2.0",
59
59
  "clsx": "^2.1.1",
60
60
  "framer-motion": "^13.1.1"
61
61
  },