@circlesquare/embedding-next 1.0.1 → 2.1.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
@@ -1,6 +1,7 @@
1
1
  # @circlesquare/embedding-next
2
2
 
3
- Next.js components for embedding your clubs events and court availability.
3
+ Next.js components for embedding your club's events and court availability, in your website's
4
+ colors, corners and typeface.
4
5
 
5
6
  ## Installation
6
7
 
@@ -10,14 +11,24 @@ npm install @circlesquare/embedding-next
10
11
  yarn add @circlesquare/embedding-next
11
12
  ```
12
13
 
14
+ The package needs React 18 or later.
15
+
13
16
  ## Usage
14
17
 
15
- All components must be used in client components. Add `'use client'` at the top of your file.
18
+ The components are client components (`'use client'`). You can render them from a Server
19
+ Component page or from your own client component.
20
+
21
+ Every component needs your merchant id. It looks like `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`, and
22
+ you find it in the manager under
23
+ [**Settings** > **Developer**](https://manager.circlesquare.app/settings/developer).
24
+
25
+ ### Config provider (optional)
16
26
 
17
- ### Config Provider (Optional)
27
+ The provider holds the options every component below it shares, so you set your merchant id and
28
+ your look once, for example in your root layout. A component's own props override the provider's
29
+ for that component only.
18
30
 
19
31
  ```tsx
20
- 'use client'
21
32
  import { CircleSquareEmbeddingConfigProvider } from '@circlesquare/embedding-next'
22
33
 
23
34
  export default function RootLayout({ children }) {
@@ -32,18 +43,22 @@ export default function RootLayout({ children }) {
32
43
  )
33
44
  }
34
45
  ```
35
- _Note: When using React 19 wrapping children with `<></>` might be required_
36
46
 
37
- #### Provider Config
38
- - `merchantId` (required): Your CircleSquare merchant ID
39
- - `backgroundColor` (optional): Background color for the embedded content
40
- - `language` (optional): Language of the embedded content (en / de). Will otherwise be selected automatically.
47
+ _Note: With React 19, wrapping children in `<></>` might be required._
48
+
49
+ #### Provider props
50
+
51
+ | Prop | Required | Description |
52
+ | ----------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
53
+ | `merchantId` | No | Your CircleSquare merchant ID, for every component below |
54
+ | `backgroundColor` | No | Background color for the embedded content |
55
+ | `language` | No | Language of the embedded content (`en` / `de`). Will otherwise be selected automatically. |
56
+ | Look options | No | `appearance`, `surfaceColor`, `textColor` and the others in [Match the widgets to your website](#match-the-widgets-to-your-website) |
41
57
 
42
- ### Events Component
58
+ ### Events component
43
59
 
44
60
  ```tsx
45
- 'use client'
46
- import { CSEvents, CsEventType } from "@circlesquare/embedding-next";
61
+ import { CSEvents, CsEventType } from '@circlesquare/embedding-next'
47
62
 
48
63
  export default function EventsPage() {
49
64
  return (
@@ -59,48 +74,169 @@ export default function EventsPage() {
59
74
  }
60
75
  ```
61
76
 
62
- #### CSEvents Config
63
- - `merchantId` (optional if in provider): Your CircleSquare merchant ID
64
- - `hostId` (optional): Filter events by host ID
65
- - `backgroundColor` (optional): Background color for the embedded content
66
- - `language` (optional): Language of the embedded content (en / de). Will otherwise be selected automatically.
67
- - `eventTypes` (optional): Array of event types to filter
68
- - `OpenPlay`
69
- - `Training`
70
- - `Other`
71
- - `League`
72
- - `Americano`
73
- - `AmericanoTournament`
74
- - `KnockOutTournament`
75
- - `RoundRobinTournament`
76
- - `className` (optional): Additional CSS class for the container
77
- - `fallback` (optional): A component to display in case the embedding iframe can not be loaded or displayed. If not provided, the default component will be used.
78
- - `timeoutMs` (optional): Time after which the fallback component is displayed. Defaults to 3000 (3 seconds)
79
-
80
-
81
- ### Court Availability Component
77
+ #### CSEvents props
78
+
79
+ | Prop | Required | Description |
80
+ | ----------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------- |
81
+ | `merchantId` | Unless set in a provider | Your CircleSquare merchant ID. With only `hostId`, the component lists that host's events. |
82
+ | `hostId` | No | Filter events by host ID |
83
+ | `eventTypes` | No | Array of event types to filter, from `CsEventType` (below). All types by default. |
84
+ | `sports` | No | Array of sports to filter, from `CsSport` (below). All of your club's sports by default. |
85
+ | `sportIds` | No | Deprecated, use `sports`. Array of sport IDs to filter. |
86
+ | `backgroundColor` | No | Background color for the embedded content |
87
+ | `language` | No | Language of the embedded content (`en` / `de`). Will otherwise be selected automatically. |
88
+ | Look options | No | See [Match the widgets to your website](#match-the-widgets-to-your-website) |
89
+ | `className` | No | Additional CSS class for the element that holds the widget |
90
+ | `fallback` | No | A component to display in case the widget can not be loaded or displayed. If not provided, the default is used. |
91
+ | `timeoutMs` | No | Time after which the fallback component is displayed. Defaults to 8000 (8 seconds). |
92
+
93
+ `CsEventType` values:
94
+
95
+ - `OpenPlay`
96
+ - `Training`
97
+ - `Other`
98
+ - `League`
99
+ - `Americano`
100
+ - `AmericanoTournament`
101
+ - `RoundRobinTournament`
102
+ - `Tournament`
103
+ - `LevelingSession`
104
+
105
+ `CsSport` values:
106
+
107
+ - `Tennis`
108
+ - `Padel`
109
+ - `Pickleball`
110
+ - `BeachVolleyball`
111
+ - `Football`
112
+ - `Badminton`
113
+ - `Squash`
114
+
115
+ ```tsx
116
+ <CSEvents sports={[CsSport.Padel, CsSport.Pickleball]} />
117
+ ```
118
+
119
+ A sport the widget doesn't know is left out, and the browser console names it. If no sport you
120
+ named is left, the widget shows all of them.
121
+
122
+ ### Court availability component
82
123
 
83
124
  ```tsx
84
- 'use client'
85
125
  import { CSCourtAvailability } from '@circlesquare/embedding-next'
86
126
 
87
127
  export default function CourtsPage() {
88
128
  return (
89
129
  <div>
90
130
  <h1>Court Availability</h1>
91
- <CSCourtAvailability
92
- merchantId="your-merchant-id"
93
- backgroundColor="#F5F5F5"
94
- />
131
+ <CSCourtAvailability merchantId="your-merchant-id" backgroundColor="#F5F5F5" />
95
132
  </div>
96
133
  )
97
134
  }
98
135
  ```
99
136
 
100
- #### CSCourtAvailability Config
101
- - `merchantId` (optional if in provider): Your CircleSquare merchant ID
102
- - `language` (optional): Language of the embedded content (en / de). Will otherwise be selected automatically.
103
- - `backgroundColor` (optional): Background color for the embedded content
104
- - `className` (optional): Additional CSS class for the container
105
- - `fallback` (optional): A component to display in case the embedding iframe can not be loaded or displayed. If not provided, the default component will be used.
106
- - `timeoutMs` (optional): Time after which the fallback component is displayed. Defaults to 3000 (3 seconds)
137
+ #### CSCourtAvailability props
138
+
139
+ | Prop | Required | Description |
140
+ | ----------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------- |
141
+ | `merchantId` | Unless set in a provider | Your CircleSquare merchant ID |
142
+ | `sports` | No | Array of sports to show, from `CsSport` as for `CSEvents`. All of your club's sports by default. |
143
+ | `sportIds` | No | Deprecated, use `sports`. Array of sport IDs to show. |
144
+ | `language` | No | Language of the embedded content (`en` / `de`). Will otherwise be selected automatically. |
145
+ | `backgroundColor` | No | Background color for the embedded content |
146
+ | Look options | No | See [Match the widgets to your website](#match-the-widgets-to-your-website) |
147
+ | `className` | No | Additional CSS class for the element that holds the widget |
148
+ | `fallback` | No | A component to display in case the widget can not be loaded or displayed. If not provided, the default is used. |
149
+ | `timeoutMs` | No | Time after which the fallback component is displayed. Defaults to 8000 (8 seconds). |
150
+
151
+ A component without a merchant ID (or, for `CSEvents`, without a host ID) renders nothing and logs
152
+ the reason in the browser console.
153
+
154
+ ## Match the widgets to your website
155
+
156
+ You can give the widgets your website's colors, corners and typeface. Every option is optional.
157
+ Without them, the widgets use CircleSquare's own look on a white background. Set them on the
158
+ provider for every widget, or on a single component.
159
+
160
+ ```tsx
161
+ <CircleSquareEmbeddingConfigProvider
162
+ merchantId="your-merchant-id"
163
+ appearance="dark"
164
+ backgroundColor="transparent"
165
+ surfaceColor="#0E1828"
166
+ textColor="#FFFFFF"
167
+ accentColor="#C6F432"
168
+ accentTextColor="#0A1220"
169
+ eventColor="#38BDF8"
170
+ borderRadius={24}
171
+ fontFamily="Inter"
172
+ >
173
+ {children}
174
+ </CircleSquareEmbeddingConfigProvider>
175
+ ```
176
+
177
+ | Option | What it changes | Default |
178
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
179
+ | `appearance` | `"light"` or `"dark"`. Use `"dark"` on a website with a dark background. | `"light"` |
180
+ | `backgroundColor` | The background behind the widget. `"transparent"` lets your page show through. | White, or none with `appearance="dark"` |
181
+ | `surfaceColor` | The widget's own surfaces: the card around the court grid, popovers and dialogs. | CircleSquare's |
182
+ | `textColor` | The text. Secondary text, lines and unavailable time slots are lighter shades of it. | CircleSquare's |
183
+ | `accentColor` | Your brand color: the visitor's own bookings, the selected time slot, the main button and the focus outline. | CircleSquare's |
184
+ | `accentTextColor` | Text on the accent color, such as the label of the main button. | Black or white, whichever is easier to read on `accentColor` |
185
+ | `eventColor` | Events shown in the court grid. | CircleSquare's |
186
+ | `borderRadius` | The corners of cards and buttons. A number is in pixels (`8`). A string in `px` or `rem` works too (`"0.5rem"`). Smaller elements such as grid cells scale with it, and `0` gives square corners. | `12` |
187
+ | `fontFamily` | The typeface, one from the list below. | `"Inter"` |
188
+
189
+ Colors can be written as hex (`#C6F432`), with a color function such as `rgb(198, 244, 50)`,
190
+ `hsl()` or `oklch()`, or as a CSS color name (`white`). `backgroundColor` also takes a CSS
191
+ gradient. CSS variables such as `var(--brand)` don't work, because the widget runs in a frame that
192
+ can't read your page's styles.
193
+
194
+ The widget ignores a value it can't read and keeps its own color, corner or typeface. For colors,
195
+ the browser console also shows a warning such as
196
+ `[CircleSquare] accentColor "#C6F43" is not a colour, leaving the widget's own`.
197
+
198
+ **Typefaces.** The widget can't use the fonts your website loads, so `fontFamily` takes one of the
199
+ typefaces CircleSquare serves: `Barlow`, `DM Sans`, `Inter`, `Lato`, `Manrope`, `Montserrat`,
200
+ `Nunito`, `Open Sans`, `Oswald`, `Poppins` or `Roboto`. CircleSquare serves the font files itself,
201
+ so the widget sends no requests to Google Fonts and needs no cookie consent.
202
+
203
+ ### Settings for a single widget
204
+
205
+ Each component takes the same options as props, and they override the provider's for that widget
206
+ only:
207
+
208
+ ```tsx
209
+ <CSEvents accentColor="#B33E25" />
210
+ ```
211
+
212
+ ## When a widget can't load
213
+
214
+ If a widget hasn't loaded after 8 seconds, for example because the visitor's ad blocker drops it,
215
+ a notice with a link to your club's page on CircleSquare takes its place. The notice uses your
216
+ colors and corners. If the widget loads later, it replaces the notice. `timeoutMs` changes how long
217
+ the component waits, and `fallback` replaces the notice with your own:
218
+
219
+ ```tsx
220
+ <CSCourtAvailability timeoutMs={12000} fallback={<p>Book your court at the front desk.</p>} />
221
+ ```
222
+
223
+ If your website has a content security policy, it needs `frame-src https://circlesquare.app`.
224
+
225
+ ## Changes in 2.1
226
+
227
+ - `sports` names the sports with `CsSport` (`[CsSport.Padel]`), so you no longer need our sport
228
+ IDs.
229
+ - `sportIds` is deprecated. It still works, alongside `sports`, until 3.0.
230
+
231
+ ## Changes in 2.0
232
+
233
+ - The look: `appearance`, `surfaceColor`, `textColor`, `accentColor`, `accentTextColor`,
234
+ `eventColor`, `borderRadius` and `fontFamily`, on the provider or on each component.
235
+ - `sportIds` on both components; `hostId` alone lists a host's events.
236
+ - `CsEventType.Tournament` and `CsEventType.LevelingSession`.
237
+ - `CsEventType.KnockOutTournament` is gone.
238
+ - The widget keeps loading behind the fallback notice and replaces it once it shows. The default
239
+ `timeoutMs` is 8000 instead of 3000.
240
+ - `className` goes on the element that holds the widget, not on the iframe itself.
241
+ - The components are client components themselves, so a Server Component can render them.
242
+ - The package is an ES module with an `exports` map, and needs no `next` peer.
@@ -0,0 +1,6 @@
1
+ import { type EmbeddedFrameOptions } from './embedded-frame.js';
2
+ import type { CsCourtAvailabilityConfig } from './types.js';
3
+ export interface CSCourtAvailabilityProps extends CsCourtAvailabilityConfig, EmbeddedFrameOptions {
4
+ }
5
+ /** The club's courts, bookable by the day, framed from CircleSquare. */
6
+ export declare function CSCourtAvailability({ className, fallback, timeoutMs, ...own }: CSCourtAvailabilityProps): import("react").JSX.Element | null;
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useEffect } from 'react';
4
+ import { EmbeddedFrame } from './embedded-frame.js';
5
+ import { mergeConfig, useCircleSquareEmbeddingConfig } from './embedding-config.js';
6
+ /** The club's courts, bookable by the day, framed from CircleSquare. */
7
+ export function CSCourtAvailability({ className, fallback, timeoutMs, ...own }) {
8
+ const config = mergeConfig(useCircleSquareEmbeddingConfig(), own);
9
+ const hasClub = Boolean(config.merchantId);
10
+ useEffect(() => {
11
+ if (!hasClub)
12
+ console.error('[CircleSquare] merchantId is required for court availability');
13
+ }, [hasClub]);
14
+ if (!hasClub)
15
+ return null;
16
+ return (_jsx(EmbeddedFrame, { widget: "courts", config: config, className: className, fallback: fallback, timeoutMs: timeoutMs }));
17
+ }
@@ -0,0 +1,6 @@
1
+ import { type EmbeddedFrameOptions } from './embedded-frame.js';
2
+ import type { CsEventsConfig } from './types.js';
3
+ export interface CSEventsProps extends CsEventsConfig, EmbeddedFrameOptions {
4
+ }
5
+ /** The club's events, or an organizer's with `hostId`, framed from CircleSquare. */
6
+ export declare function CSEvents({ className, fallback, timeoutMs, ...own }: CSEventsProps): import("react").JSX.Element | null;
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useEffect } from 'react';
4
+ import { EmbeddedFrame } from './embedded-frame.js';
5
+ import { mergeConfig, useCircleSquareEmbeddingConfig } from './embedding-config.js';
6
+ /** The club's events, or an organizer's with `hostId`, framed from CircleSquare. */
7
+ export function CSEvents({ className, fallback, timeoutMs, ...own }) {
8
+ const config = mergeConfig(useCircleSquareEmbeddingConfig(), own);
9
+ const hasOwner = Boolean(config.merchantId || config.hostId);
10
+ useEffect(() => {
11
+ if (!hasOwner)
12
+ console.error('[CircleSquare] merchantId or hostId is required for events');
13
+ }, [hasOwner]);
14
+ if (!hasOwner)
15
+ return null;
16
+ return (_jsx(EmbeddedFrame, { widget: "events", config: config, className: className, fallback: fallback, timeoutMs: timeoutMs }));
17
+ }
@@ -0,0 +1,16 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type EmbeddedWidget, type FrameConfig } from './frame-url.js';
3
+ export declare const DEFAULT_TIMEOUT_MS = 8000;
4
+ export interface EmbeddedFrameOptions {
5
+ /** A class for the element that holds the frame, or the notice in its place. */
6
+ className?: string;
7
+ /** Shown instead of the default notice when the frame does not show in time. */
8
+ fallback?: ReactNode;
9
+ /** How long a frame may take to report its height before the fallback shows. */
10
+ timeoutMs?: number;
11
+ }
12
+ /** One framed page on the host's page, as tall as its content. */
13
+ export declare function EmbeddedFrame({ widget, config, className, fallback, timeoutMs, }: EmbeddedFrameOptions & {
14
+ widget: EmbeddedWidget;
15
+ config: FrameConfig;
16
+ }): import("react").JSX.Element;
@@ -0,0 +1,95 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef, useState } from 'react';
4
+ import { FallbackNotice } from './fallback-notice.js';
5
+ import { getConfigWarning, getFrameUrl } from './frame-url.js';
6
+ /* A FRAME THAT DOES NOT SHOW, because our site is down, the visitor's
7
+ network or a blocker drops it, or the host's policy forbids frames,
8
+ leaves an empty box where the club sells its courts. A frame shows once
9
+ it reports its height, so a frame that has not done so in time gives way
10
+ to a notice with a link to the club on CircleSquare. The frame keeps
11
+ loading behind the notice and replaces it when it reports, so the wait is
12
+ the loader's: a slow phone sees the notice only when the frame would keep
13
+ it waiting long. */
14
+ export const DEFAULT_TIMEOUT_MS = 8000;
15
+ /* What the frame posts whenever its content resizes (apps/web/src/lib/embed.ts). */
16
+ const isSetHeightMessage = (data, containerId) => {
17
+ if (typeof data !== 'object' || data === null)
18
+ return false;
19
+ const message = data;
20
+ return (message.type === 'setHeight' &&
21
+ message.containerId === containerId &&
22
+ typeof message.height === 'number' &&
23
+ Number.isFinite(message.height));
24
+ };
25
+ /* The frame's name for a screen reader. The server renders it, where the
26
+ visitor's language is unknown, so only a configured language changes it. */
27
+ const FRAME_TITLES = {
28
+ courts: { en: 'CircleSquare court availability', de: 'CircleSquare-Platzverfügbarkeit' },
29
+ events: { en: 'CircleSquare events', de: 'CircleSquare-Events' },
30
+ };
31
+ const getFrameTitle = (widget, { language }) => FRAME_TITLES[widget][language?.toLowerCase() === 'de' ? 'de' : 'en'];
32
+ const isBrowserColour = (value) => typeof CSS === 'undefined' || CSS.supports('color', value);
33
+ /** One framed page on the host's page, as tall as its content. */
34
+ export function EmbeddedFrame({ widget, config, className, fallback, timeoutMs = DEFAULT_TIMEOUT_MS, }) {
35
+ const containerId = useId();
36
+ const frameUrl = getFrameUrl({ widget, containerId, config });
37
+ const { href, origin } = frameUrl;
38
+ const frameRef = useRef(null);
39
+ const [height, setHeight] = useState();
40
+ const [hasTimedOut, setHasTimedOut] = useState(false);
41
+ const configWarning = getConfigWarning(config, { isColour: isBrowserColour });
42
+ useEffect(() => {
43
+ if (configWarning)
44
+ console.warn(configWarning);
45
+ }, [configWarning]);
46
+ useEffect(() => {
47
+ const fallbackTimer = window.setTimeout(() => {
48
+ console.warn(`[CircleSquare] the ${widget} frame did not show within ${timeoutMs} ms, linking to CircleSquare instead`);
49
+ setHasTimedOut(true);
50
+ }, timeoutMs);
51
+ /* The host's window receives every message on its page, the host's own
52
+ included: only a height message from this very frame, sent by the
53
+ origin it was loaded from and naming this container, is ours. */
54
+ const handleMessage = (event) => {
55
+ if (event.source !== frameRef.current?.contentWindow || event.origin !== origin)
56
+ return;
57
+ if (!isSetHeightMessage(event.data, containerId))
58
+ return;
59
+ window.clearTimeout(fallbackTimer);
60
+ setHasTimedOut(false);
61
+ setHeight(event.data.height);
62
+ };
63
+ /* A press on the host's page never reaches the frame's document, so a
64
+ popover open in the frame would stay open: the frame is told of every
65
+ click, in the capture phase, so a host's handler that stops the click
66
+ cannot keep it from the frame. */
67
+ const handleHostPress = () => frameRef.current?.contentWindow?.postMessage({ type: 'hostPress' }, origin);
68
+ window.addEventListener('message', handleMessage);
69
+ document.addEventListener('click', handleHostPress, true);
70
+ /* The frame reports its height when it first lays out and then only
71
+ when it resizes, so it loads once this listens: a frame in the
72
+ server's HTML could report before the host's page hydrates, and that
73
+ report would be lost. Setting the same address again would reload it. */
74
+ const frame = frameRef.current;
75
+ if (frame && frame.src !== href)
76
+ frame.src = href;
77
+ return () => {
78
+ window.clearTimeout(fallbackTimer);
79
+ window.removeEventListener('message', handleMessage);
80
+ document.removeEventListener('click', handleHostPress, true);
81
+ };
82
+ }, [href, origin, containerId, widget, timeoutMs]);
83
+ return (_jsxs("div", { className: className, style: { width: '100%' }, children: [_jsx("iframe", { ref: frameRef, title: getFrameTitle(widget, config), style: {
84
+ display: 'block',
85
+ width: '100%',
86
+ border: 'none',
87
+ height: hasTimedOut ? 0 : height,
88
+ /* The framed page's root is light in both appearances, a dark frame
89
+ dark from <body> down (apps/web/src/lib/embed-theme.ts). An iframe
90
+ in another scheme than its document gets an opaque canvas, which
91
+ hides the host's page behind it, so the iframe stays light on a
92
+ host that declares itself dark too. */
93
+ colorScheme: 'light',
94
+ } }), hasTimedOut && (fallback ?? _jsx(FallbackNotice, { widget: widget, config: config }))] }));
95
+ }
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { CsEmbeddingBaseConfig } from './types.js';
3
+ /** The configuration the nearest provider shares, or null outside one. */
4
+ export declare const useCircleSquareEmbeddingConfig: () => CsEmbeddingBaseConfig | null;
5
+ export interface CircleSquareEmbeddingConfigProviderProps extends CsEmbeddingBaseConfig {
6
+ children: ReactNode;
7
+ }
8
+ /** The configuration every widget below shares, the club's look included; a widget's own props win. */
9
+ export declare function CircleSquareEmbeddingConfigProvider({ children, ...config }: CircleSquareEmbeddingConfigProviderProps): import("react").JSX.Element;
10
+ export declare function mergeConfig<T extends CsEmbeddingBaseConfig>(shared: CsEmbeddingBaseConfig | null, own: T): T;
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext } from 'react';
4
+ const CircleSquareEmbeddingConfigContext = createContext(null);
5
+ /** The configuration the nearest provider shares, or null outside one. */
6
+ export const useCircleSquareEmbeddingConfig = () => useContext(CircleSquareEmbeddingConfigContext)?.config ?? null;
7
+ /** The configuration every widget below shares, the club's look included; a widget's own props win. */
8
+ export function CircleSquareEmbeddingConfigProvider({ children, ...config }) {
9
+ return (_jsx(CircleSquareEmbeddingConfigContext.Provider, { value: { config }, children: children }));
10
+ }
11
+ /* An option a widget leaves undefined keeps the provider's, as the old
12
+ package had it; the loader's `Object.assign` would let an explicit
13
+ `undefined` erase it, which a React prop passed through often is. */
14
+ export function mergeConfig(shared, own) {
15
+ const defined = Object.fromEntries(Object.entries(own).filter(([, value]) => value !== undefined));
16
+ return { ...shared, ...defined };
17
+ }
@@ -0,0 +1,9 @@
1
+ import { type EmbeddedWidget, type FrameConfig } from './frame-url.js';
2
+ /**
3
+ * What a frame that does not show is replaced with: a link to the club on CircleSquare, in the
4
+ * host's look as far as its options give it.
5
+ */
6
+ export declare function FallbackNotice({ widget, config }: {
7
+ widget: EmbeddedWidget;
8
+ config: FrameConfig;
9
+ }): import("react").JSX.Element;
@@ -0,0 +1,76 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { getFallbackUrl } from './frame-url.js';
4
+ const FALLBACK_TEXTS = {
5
+ courts: {
6
+ en: {
7
+ title: 'Court availability cannot be displayed',
8
+ text: 'You can still book your court directly on CircleSquare.',
9
+ action: 'Book a court on CircleSquare',
10
+ },
11
+ de: {
12
+ title: 'Die Platzverfügbarkeit kann nicht angezeigt werden',
13
+ text: 'Du kannst deinen Platz aber direkt bei CircleSquare buchen.',
14
+ action: 'Platz bei CircleSquare buchen',
15
+ },
16
+ },
17
+ events: {
18
+ en: {
19
+ title: 'Events cannot be displayed',
20
+ text: 'You can find all our events directly on CircleSquare.',
21
+ action: 'View events on CircleSquare',
22
+ },
23
+ de: {
24
+ title: 'Events können nicht angezeigt werden',
25
+ text: 'Alle unsere Events findest du direkt bei CircleSquare.',
26
+ action: 'Events bei CircleSquare anzeigen',
27
+ },
28
+ },
29
+ };
30
+ /* The kit's ink, its text and its primary button, for a host that sets neither. */
31
+ const KIT_INK = '#10261E';
32
+ /* The notice shows only after a timeout, in the browser, so it may read the visitor's language. */
33
+ const getTexts = (widget, config) => {
34
+ const language = (config.language || window.navigator.language || '').slice(0, 2).toLowerCase();
35
+ return FALLBACK_TEXTS[widget][language === 'de' ? 'de' : 'en'];
36
+ };
37
+ /* A bare number is pixels, as the framed page reads `borderRadius`. */
38
+ const toRadius = (radius) => typeof radius === 'number' || /^\d+(\.\d+)?$/.test(radius ?? '') ? `${radius}px` : radius;
39
+ /* Inline, as everything here, to hold against the host's rules for `p` and `a`. */
40
+ const PARAGRAPH = { color: 'inherit', fontFamily: 'inherit', lineHeight: 1.4 };
41
+ /**
42
+ * What a frame that does not show is replaced with: a link to the club on CircleSquare, in the
43
+ * host's look as far as its options give it.
44
+ */
45
+ export function FallbackNotice({ widget, config }) {
46
+ const texts = getTexts(widget, config);
47
+ const radius = toRadius(config.borderRadius);
48
+ const accent = config.accentColor || KIT_INK;
49
+ return (_jsxs("div", { "data-circlesquare-fallback": "", style: {
50
+ boxSizing: 'border-box',
51
+ display: 'flex',
52
+ flexDirection: 'column',
53
+ alignItems: 'center',
54
+ gap: 8,
55
+ padding: '32px 24px',
56
+ textAlign: 'center',
57
+ border: '1px solid rgba(128, 128, 128, 0.3)',
58
+ borderRadius: radius || 12,
59
+ background: config.surfaceColor || config.backgroundColor || 'transparent',
60
+ color: config.textColor || (config.appearance === 'dark' ? '#FFFFFF' : KIT_INK),
61
+ fontFamily: config.fontFamily ? `"${config.fontFamily}", system-ui, sans-serif` : 'inherit',
62
+ }, children: [_jsx("p", { style: { ...PARAGRAPH, margin: 0, fontSize: 18, fontWeight: 700 }, children: texts.title }), _jsx("p", { style: { ...PARAGRAPH, margin: '0 0 8px', fontSize: 15, opacity: 0.75 }, children: texts.text }), _jsx("a", { href: getFallbackUrl({ widget, config }).href, target: "_blank", rel: "noopener", style: {
63
+ display: 'inline-block',
64
+ padding: '10px 20px',
65
+ borderRadius: radius || 999,
66
+ background: accent,
67
+ color: '#FFFFFF',
68
+ fontFamily: 'inherit',
69
+ fontSize: 15,
70
+ fontWeight: 700,
71
+ lineHeight: 1.4,
72
+ textDecoration: 'none',
73
+ }, children: _jsx("span", { style: {
74
+ color: config.accentTextColor || `oklch(from ${accent} clamp(0, (0.62 - l) * 1000, 1) 0 0)`,
75
+ }, children: texts.action }) })] }));
76
+ }
@@ -0,0 +1,30 @@
1
+ import { type CsEventsConfig } from './types.js';
2
+ export declare const DEFAULT_CS_BASE_URL = "https://circlesquare.app";
3
+ export type EmbeddedWidget = 'courts' | 'events';
4
+ /** A widget's configuration once the provider's and the component's are merged. */
5
+ export type FrameConfig = CsEventsConfig;
6
+ /**
7
+ * The frame's address. `csEmbedded` is sent as the loader sends it; the framed page knows it is
8
+ * framed from its path.
9
+ */
10
+ export declare function getFrameUrl({ widget, containerId, config, }: {
11
+ widget: EmbeddedWidget;
12
+ containerId: string;
13
+ config: FrameConfig;
14
+ }): URL;
15
+ /**
16
+ * Where the fallback notice sends a visitor: the club's page on the tab the widget stands for, or
17
+ * the events on CircleSquare for an organizer's list.
18
+ */
19
+ export declare function getFallbackUrl({ widget, config }: {
20
+ widget: EmbeddedWidget;
21
+ config: FrameConfig;
22
+ }): URL;
23
+ /**
24
+ * What the host asked for that the frame will not show, as one console warning, or null. The page
25
+ * leaves out a colour it cannot read and keeps the kit's own; the host's browser says so here,
26
+ * before the host wonders why the club's colour does not show. `isColour` is the browser's check.
27
+ */
28
+ export declare function getConfigWarning(config: FrameConfig, { isColour }: {
29
+ isColour: (value: string) => boolean;
30
+ }): string | null;